Repository navigation
Expand file tree
/
Copy pathindex.html
More file actions
470 lines (424 loc) · 25.4 KB
/
Copy pathindex.html
File metadata and controls
470 lines (424 loc) · 25.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Control Function</title>
<meta name="description" content="A visual editor for physical keyboard layouts.">
<meta name="theme-color" id="theme-color" content="#fafafa">
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" href="icons/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="icons/icon-180.png">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<main id="stage" aria-label="Keyboard layout canvas">
<div id="world"></div>
<div id="marquee" hidden></div>
</main>
<!-- Properties. Two panels, one column: whatever the board panel has to say
stacks above the key panel rather than replacing it, since a board and a
set of keys can be selected at the same time. -->
<div id="panel-stack">
<aside id="pcb-sidebar" class="panel" hidden aria-label="Circuit board properties">
<header class="panel-head">
<h2 id="pcb-sidebar-title" tabindex="0" role="button" data-tip="Click to rename" aria-label="Rename board">Circuit board</h2>
<input id="pcb-name-input" class="pcb-name-input" type="text" autocomplete="off" spellcheck="false" maxlength="40" placeholder="Circuit board" aria-label="Board name" hidden>
<button id="pcb-sidebar-close" class="icon-btn" type="button" data-tip="Deselect the board (Esc)" aria-label="Deselect board">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</header>
<div class="field">
<span class="field-label" id="l-board-shape">Shape</span>
<div class="shape-picker" role="group" aria-labelledby="l-board-shape" id="pcb-shape-picker">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-shape="rectangular" data-tip="The whole placed rectangle" aria-label="Rectangular board" aria-pressed="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="board-rectangular" aria-hidden="true"></svg>
</button>
<button class="shape-btn" type="button" data-shape="cutout" data-tip="Cut the board back to the keys on it" aria-label="Cutout board" aria-pressed="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="cutout" aria-hidden="true"></svg>
</button>
</div>
</div>
<!-- Only meaningful for a cutout — js/sidebar.js keeps it visible but greys
it out for a rectangular board, rather than hiding it, so switching
shapes doesn't relocate the field. -->
<div class="field" id="pcb-offset-field">
<span class="field-label" id="l-board-offset">Offset</span>
<div class="stepper" role="group" aria-labelledby="l-board-offset">
<button class="step-btn" type="button" data-delta="-1" data-tip="Closer to the keys" aria-label="Less offset">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="minus" aria-hidden="true"></svg>
</button>
<output class="step-value" id="f-board-offset">0u</output>
<button class="step-btn" type="button" data-delta="1" data-tip="Further from the keys" aria-label="More offset">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="plus" aria-hidden="true"></svg>
</button>
</div>
</div>
<div class="field">
<span class="field-label" id="l-board-controller">Controller</span>
<select id="f-board-controller" aria-labelledby="l-board-controller">
<option value="">None</option>
<option value="Pro Micro">Pro Micro</option>
<option value="Elite-C">Elite-C</option>
<option value="RP2040">RP2040</option>
<option value="nice!nano">nice!nano</option>
<option value="__other__">Other…</option>
</select>
<input id="f-board-controller-other" type="text" autocomplete="off" spellcheck="false" maxlength="40" placeholder="Controller name" aria-label="Controller name" hidden>
</div>
<label class="checkbox-field">
<input id="f-board-hotswap" type="checkbox">
<span>Hotswap sockets</span>
</label>
<label class="checkbox-field">
<input id="f-board-diode-default" type="checkbox">
<span>One diode per switch</span>
</label>
<label class="checkbox-field">
<input id="f-board-backlight" type="checkbox">
<span>Backlight / underglow</span>
</label>
<div class="field">
<span class="field-label" id="l-board-stab">Stabilizers</span>
<div class="shape-picker" role="group" aria-labelledby="l-board-stab" id="pcb-stab-picker">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-stab="screw-in" data-tip="Screw-in stabilizers" aria-label="Screw-in">Screw</button>
<button class="shape-btn" type="button" data-stab="snap-in" data-tip="Snap-in stabilizers" aria-label="Snap-in">Snap</button>
<button class="shape-btn" type="button" data-stab="plate-mount" data-tip="Plate-mounted stabilizers" aria-label="Plate-mount">Plate</button>
</div>
</div>
<!-- No marker placed is itself a valid state (see README), so the field
starts as a single placement button rather than an empty picker —
js/sidebar.js swaps it for the connector picker once one exists.
Single-board selection only, like the rename above: the marker's
position is inherently one board's own, not a shared value. -->
<div class="field" id="pcb-usb-field" hidden>
<span class="field-label" id="l-board-usb">USB connector</span>
<button id="pcb-usb-add" class="pill-btn is-block" type="button" data-tip="Place a marker on the board's edge, then drag it to reposition">Place USB connector</button>
<div id="pcb-usb-controls" hidden>
<div class="shape-picker" role="group" aria-labelledby="l-board-usb" id="pcb-usb-picker">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-usb="usb-c" aria-label="USB-C">USB-C</button>
<button class="shape-btn" type="button" data-usb="micro-usb" aria-label="Micro-USB">Micro</button>
</div>
<button id="pcb-usb-remove" class="icon-btn" type="button" data-tip="Remove the USB marker" aria-label="Remove USB marker">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</div>
</div>
<!-- Only once a board is part of a wireless link — see README. Battery
fields live on the board unconditionally in the data either way; this
is a visibility rule, not a data-model one. -->
<div class="field" id="pcb-battery-field" hidden>
<span class="field-label">Battery</span>
<label class="checkbox-field">
<input id="f-board-battery-present" type="checkbox">
<span>Battery present</span>
</label>
<input id="f-board-battery-capacity" type="text" autocomplete="off" spellcheck="false" maxlength="20" placeholder="Capacity, e.g. 1100mAh" aria-label="Battery capacity">
</div>
<!-- A link's properties also surface here, on whichever of its own boards
are selected, so this and the same field on its other linked board
(and on the link's own panel below, once selected directly) all read
and write the one shared link object underneath. -->
<div class="field" id="pcb-link-field" hidden>
<span class="field-label" id="l-board-link">Link</span>
<div class="shape-picker" role="group" aria-labelledby="l-board-link" id="pcb-link-picker">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-connector="trrs" data-tip="Wired, TRRS (4-conductor)" aria-label="TRRS">TRRS</button>
<button class="shape-btn" type="button" data-connector="trs" data-tip="Wired, TRS (3-conductor)" aria-label="TRS">TRS</button>
<button class="shape-btn" type="button" data-connector="wireless" data-tip="No physical connection between the boards" aria-label="Wireless">Wireless</button>
</div>
<button id="pcb-unlink" class="pill-btn is-block has-icon" type="button" data-tip="Dissolve this link">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="unlink" aria-hidden="true"></svg>
<span>Unlink</span>
</button>
</div>
<button id="link-boards" class="pill-btn is-block has-icon" type="button" hidden data-tip="Link the selected boards, e.g. two halves of a split">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="code-branch" aria-hidden="true"></svg>
<span>Link boards</span>
</button>
</aside>
<!-- A link's own panel — shown only when its line is selected directly (see
js/interact.js's `linkOutlineAt`), exclusive with the board and key
panels the way store.selectLink itself is. The title names the two
boards it connects rather than taking a name of its own; a link has no
separate identity worth typing a label for. -->
<aside id="link-sidebar" class="panel" hidden aria-label="Link properties">
<header class="panel-head">
<h2 id="link-sidebar-title">Link</h2>
<button id="link-sidebar-close" class="icon-btn" type="button" data-tip="Deselect the link (Esc)" aria-label="Deselect link">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</header>
<div class="field">
<span class="field-label" id="l-link-connector">Connector</span>
<div class="shape-picker" role="group" aria-labelledby="l-link-connector" id="link-connector-picker">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-connector="trrs" data-tip="Wired, TRRS (4-conductor)" aria-label="TRRS">TRRS</button>
<button class="shape-btn" type="button" data-connector="trs" data-tip="Wired, TRS (3-conductor)" aria-label="TRS">TRS</button>
<button class="shape-btn" type="button" data-connector="wireless" data-tip="No physical connection between the boards" aria-label="Wireless">Wireless</button>
</div>
</div>
<button id="link-unlink" class="pill-btn is-block has-icon" type="button" data-tip="Dissolve this link">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="unlink" aria-hidden="true"></svg>
<span>Unlink</span>
</button>
</aside>
<aside id="sidebar" class="panel" hidden aria-label="Key properties">
<header class="panel-head">
<h2 id="sidebar-title">Key</h2>
<button id="sidebar-close" class="icon-btn" type="button" data-tip="Deselect (Esc)" aria-label="Deselect">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</header>
<!-- `display: contents` in the stylesheet, so grouping these for the mode
switch below doesn't disturb the panel's own column layout. -->
<div id="key-fields">
<div class="field">
<span class="field-label" id="l-legends">Legends</span>
<div class="legend-grid" role="group" aria-labelledby="l-legends">
<input id="f-secondary" type="text" autocomplete="off" spellcheck="false" maxlength="24" placeholder="Secondary" aria-label="Secondary (top left)">
<input id="f-tertiary" type="text" autocomplete="off" spellcheck="false" maxlength="24" placeholder="Tertiary" aria-label="Tertiary (top right)">
<input id="f-primary" type="text" autocomplete="off" spellcheck="false" maxlength="24" placeholder="Primary" aria-label="Primary (bottom left)">
<input id="f-fn" type="text" autocomplete="off" spellcheck="false" maxlength="24" placeholder="Function" aria-label="Function (bottom right)">
</div>
</div>
<label class="checkbox-field">
<input id="f-home" type="checkbox">
<span>Home key</span>
</label>
<div class="field">
<span class="field-label" id="l-shape">Shape</span>
<div class="shape-picker" role="group" aria-labelledby="l-shape">
<span class="shape-thumb" aria-hidden="true"></span>
<button class="shape-btn" type="button" data-shape="rectangular" data-tip="Rectangular" aria-label="Rectangular" aria-pressed="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="key-rectangular" aria-hidden="true"></svg>
</button>
<button class="shape-btn" type="button" data-shape="iso-enter" data-tip="ISO Enter" aria-label="ISO Enter shape" aria-pressed="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="iso-enter" aria-hidden="true"></svg>
</button>
<button class="shape-btn" type="button" data-shape="big-ass-enter" data-tip="Big-Ass Enter" aria-label="Big-Ass Enter shape" aria-pressed="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="big-ass-enter" aria-hidden="true"></svg>
</button>
</div>
</div>
<div class="field-row">
<div class="field">
<span class="field-label" id="l-width">Width</span>
<div class="stepper" role="group" aria-labelledby="l-width">
<button class="step-btn" type="button" data-prop="w" data-delta="-0.25" aria-label="Narrower">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="minus" aria-hidden="true"></svg>
</button>
<output class="step-value" id="f-width">1u</output>
<button class="step-btn" type="button" data-prop="w" data-delta="0.25" aria-label="Wider">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="plus" aria-hidden="true"></svg>
</button>
</div>
</div>
<div class="field">
<span class="field-label" id="l-height">Height</span>
<div class="stepper" role="group" aria-labelledby="l-height">
<button class="step-btn" type="button" data-prop="h" data-delta="-0.25" aria-label="Shorter">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="minus" aria-hidden="true"></svg>
</button>
<output class="step-value" id="f-height">1u</output>
<button class="step-btn" type="button" data-prop="h" data-delta="0.25" aria-label="Taller">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="plus" aria-hidden="true"></svg>
</button>
</div>
</div>
</div>
<button id="group-selection" class="pill-btn is-block" type="button" data-tip="Group the selected keys so they can be moved and rotated together">Make rotation group</button>
</div>
<div id="circuits-fields" hidden>
<p class="panel-note">Keycaps are frozen in Circuits mode — select them to put them on a board.</p>
<!-- The one key property Circuits mode owns: which board these keys are
on. Adding, removing and moving between boards are all this one
write, so there is no separate control for any of them. -->
<div class="field" id="key-board-field">
<span class="field-label" id="l-key-board">Circuit board</span>
<select id="f-key-board" aria-labelledby="l-key-board" data-tip="Which board these keys are on"></select>
</div>
<button id="add-board-selection" class="pill-btn is-block has-icon" type="button" data-tip="Draw a new circuit board around the selected keys">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="circuitry" aria-hidden="true"></svg>
<span>Add to new board</span>
</button>
<!-- Same one-control shape as the board field above, once per axis: a
key's row or column is set or moved with this select. Unlike the
board field, assigning one never pulls in the rest of a rotation
group — see store.js's `axis()`. The × button beside it is a
shortcut for the select's own "— none —" option, not a separate
action — same write, faster to reach than opening the dropdown for
it every time, the way `pcb-usb-remove` is a shortcut next to that
picker rather than a third state of its own. -->
<div class="field" id="key-row-field">
<span class="field-label" id="l-key-row">Row</span>
<div class="key-axis-controls">
<select id="f-key-row" aria-labelledby="l-key-row" data-tip="Which matrix row these keys are on"></select>
<button id="key-row-clear" class="icon-btn" type="button" data-tip="Take the selected keys off their row" aria-label="Clear row">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</div>
</div>
<button id="add-row-selection" class="pill-btn is-block" type="button" data-tip="Wire the selected keys together as a new matrix row">Add to new row</button>
<div class="field" id="key-col-field">
<span class="field-label" id="l-key-col">Column</span>
<div class="key-axis-controls">
<select id="f-key-col" aria-labelledby="l-key-col" data-tip="Which matrix column these keys are on"></select>
<button id="key-col-clear" class="icon-btn" type="button" data-tip="Take the selected keys off their column" aria-label="Clear column">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</div>
</div>
<button id="add-col-selection" class="pill-btn is-block" type="button" data-tip="Wire the selected keys together as a new matrix column">Add to new column</button>
</div>
</aside>
</div>
<!-- Layout files -->
<div id="topbar-left">
<div class="bar" id="wipebar" aria-label="Reset">
<button id="wipe-layout" class="circle-btn is-danger" type="button" data-tip="Reset the board" aria-label="Reset the board">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="eraser" aria-hidden="true"></svg>
</button>
</div>
<div class="bar" id="filebar" aria-label="Layout file">
<button id="save-layout" class="pill-btn has-icon" type="button" data-tip="Export board to a JSON file" aria-label="Export board">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="export" aria-hidden="true"></svg>
<span>Export</span>
</button>
<button id="import-layout" class="circle-btn" type="button" data-tip="Import board from a JSON file" aria-label="Import board">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="download" aria-hidden="true"></svg>
</button>
<input id="file-input" type="file" accept="application/json,.json" hidden>
</div>
</div>
<!-- Design modes -->
<div id="topbar-center">
<div class="bar segmented" id="modebar" role="group" aria-label="Design mode">
<span class="segmented-thumb" aria-hidden="true"></span>
<button type="button" class="segment" data-mode="keycaps" aria-pressed="true" data-tip="Lay out, size and label the keycaps">Keycaps</button>
<button type="button" class="segment" data-mode="switches" aria-pressed="false" data-tip="Switches — not built yet">Switches</button>
<button type="button" class="segment" data-mode="circuits" aria-pressed="false" data-tip="Place the circuit boards under the keys">Circuits</button>
<button type="button" class="segment" data-mode="firmware" aria-pressed="false" data-tip="Firmware — not built yet">Firmware</button>
</div>
</div>
<div id="topbar">
<div class="bar" id="appearance" aria-label="Appearance">
<button id="theme-trigger" class="circle-btn" type="button" data-tip="Theme" aria-label="Theme" aria-haspopup="true" aria-expanded="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="palette" aria-hidden="true"></svg>
</button>
<div id="themebar" role="group" aria-label="Theme">
<span class="theme-thumb" aria-hidden="true"></span>
<button class="swatch swatch-auto" type="button" data-theme="auto" aria-pressed="true" data-tip="Auto" aria-label="Follow system theme">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="dark-mode" aria-hidden="true"></svg>
</button>
<button class="swatch" type="button" data-theme="light" aria-pressed="false" data-tip="Light" aria-label="Light theme"><span class="swatch-dot" aria-hidden="true"></span></button>
<button class="swatch" type="button" data-theme="retro" aria-pressed="false" data-tip="Retro" aria-label="Retro theme"><span class="swatch-dot" aria-hidden="true"></span></button>
<button class="swatch" type="button" data-theme="nord" aria-pressed="false" data-tip="Nord" aria-label="Nord theme"><span class="swatch-dot" aria-hidden="true"></span></button>
<button class="swatch" type="button" data-theme="dark" aria-pressed="false" data-tip="Dark" aria-label="Dark theme"><span class="swatch-dot" aria-hidden="true"></span></button>
</div>
</div>
<div id="tipsrow" class="bar" aria-label="Tips">
<button id="tips-btn" class="circle-btn" type="button" data-tip="Tips & shortcuts"
aria-label="Show tips" aria-haspopup="dialog" aria-expanded="false">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="circle-question" aria-hidden="true"></svg>
</button>
</div>
</div>
<div id="toast" role="status" aria-live="polite" hidden></div>
<div id="tooltip" role="tooltip" hidden></div>
<div id="tips-overlay" hidden></div>
<div id="tips-popup" class="panel" hidden role="dialog" aria-modal="true" aria-labelledby="tips-title">
<header class="panel-head">
<h2 id="tips-title">Control Function</h2>
<button id="tips-close" class="icon-btn" type="button" data-tip="Close" aria-label="Close">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="close" aria-hidden="true"></svg>
</button>
</header>
<section class="tips-section">
<h3>Welcome</h3>
<p>Control Function is a visual editor for physical keyboard layouts — lay keys and knobs out on a grid, size and label them, group and rotate clusters, and draw the circuit boards under them. Largely built by AI.</p>
</section>
<section class="tips-section">
<h3>Navigation</h3>
<div class="tips-shortcuts">
<span class="pill"><kbd>1</kbd><kbd>2</kbd><kbd>3</kbd><kbd>4</kbd> Modes</span>
<span class="pill"><kbd>click</kbd> Add to selection</span>
<span class="pill"><kbd>⇧</kbd><kbd>click</kbd> Replace selection</span>
<span class="pill"><kbd>Space</kbd> Deselect all</span>
<span class="pill"><kbd>right‑drag</kbd> Pan</span>
<span class="pill"><kbd>middle‑drag</kbd> <span id="middle-drag-hint">Zoom</span></span>
<span class="pill"><kbd>Space</kbd><kbd>drag</kbd> Pan</span>
</div>
</section>
<section class="tips-section">
<h3><kbd>1</kbd> Keycaps</h3>
<div class="tips-shortcuts">
<span class="pill"><kbd>K</kbd> Add key</span>
<span class="pill"><kbd>D</kbd> Demo keyboard (wired)</span>
<span class="pill"><kbd>M</kbd> Function row</span>
<span class="pill"><kbd>G</kbd> Group</span>
<span class="pill"><kbd>⌥</kbd><kbd>drag</kbd> Duplicate</span>
</div>
<p>The full editor: lay keycaps and knobs out on the grid, size and label them, and bundle clusters into rotation groups that move, rotate, mirror and pivot as one piece.</p>
</section>
<section class="tips-section">
<h3><kbd>2</kbd> Switches</h3>
<p>The switches under the caps. Not built yet.</p>
</section>
<section class="tips-section">
<h3><kbd>3</kbd> Circuits</h3>
<div class="tips-shortcuts">
<span class="pill"><kbd>P</kbd> Add to new board</span>
<span class="pill"><kbd>R</kbd> Add to new row</span>
<span class="pill"><kbd>C</kbd> Add to new column</span>
</div>
<p>Draw the circuit boards beneath the caps and wire switches into matrix rows and columns; the caps freeze to a selectable backdrop. A board <em>is</em> its keys — drawn around them, dragged with them, dissolved when the last one leaves. Click a board's outline for its panel and its DXF, KiCad and BOM exports.</p>
<p><kbd>D</kbd> works here too, and the demo keyboard arrives complete — board, and a 6 × 22 matrix already wired — which is the fastest way to see what rows and columns do.</p>
</section>
<section class="tips-section">
<h3><kbd>4</kbd> Firmware</h3>
<p>The firmware that scans the matrix. Not built yet.</p>
</section>
<section class="tips-section">
<h3>Trackpad vs. mouse</h3>
<p>Trackpad: scroll pans, pinch or middle‑drag zooms. Mouse: scroll zooms, middle‑drag pans.</p>
</section>
<section class="tips-section">
<h3>Layout file</h3>
<p><strong>Export board</strong> saves the layout to a JSON file, <strong>Import board</strong> loads one back.</p>
</section>
</div>
<!-- Zoom -->
<div id="zoomrow">
<div class="bar segmented" id="inputmode" role="group" aria-label="Input device">
<span class="segmented-thumb" aria-hidden="true"></span>
<button type="button" class="segment" id="input-mode-trackpad" aria-pressed="true" data-tip="Trackpad navigation" aria-label="Trackpad">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="trackpad" aria-hidden="true"></svg>
</button>
<button type="button" class="segment" id="input-mode-mouse" aria-pressed="false" data-tip="Mouse navigation" aria-label="Mouse">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="mouse" aria-hidden="true"></svg>
</button>
</div>
<div id="fitbar" class="bar" aria-label="Fit to window">
<button id="zoom-fit" class="circle-btn" type="button" data-tip="Fit to window (F)" aria-label="Fit to window">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="normal-screen" aria-hidden="true"></svg>
</button>
</div>
<div id="zoombar" aria-label="Zoom">
<button id="zoom-out" class="circle-btn" type="button" data-tip="Zoom out" aria-label="Zoom out">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="zoom-out" aria-hidden="true"></svg>
</button>
<button id="zoom-reset" class="pill-btn" type="button" data-tip="Reset to 100% (0)">100%</button>
<button id="zoom-in" class="circle-btn" type="button" data-tip="Zoom in" aria-label="Zoom in">
<svg class="ui-icon" viewBox="0 0 24 24" data-icon="zoom-in" aria-hidden="true"></svg>
</button>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>