diff options
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 61 |
1 files changed, 59 insertions, 2 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 48820cd..6620ddf 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -267,8 +267,14 @@ GW.abcKeypad = function (host, opts = {}) { gradDef('abcAmb', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'var(--amber-grad-top)'], ['1', 'var(--gold)']]); /* faceplate + the recessed window the legend prints into */ svgEl(s, 'rect', { x: 2, y: 2, width: 228, height: 222, rx: 7, fill: '#17140f', stroke: '#0a0908', 'stroke-width': 2 }); - svgEl(s, 'rect', { x: 12, y: 10, width: 208, height: 30, rx: 4, fill: '#0a0806', stroke: '#2c261d', 'stroke-width': 2 }); - const disp = svgEl(s, 'text', { x: 20, y: 31, 'font-size': 14, 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' }); + /* The window is a screen like the scope's or the marquee's, so it takes the + screen families through the --scr-* vars, shipped colours as the fallbacks: + nothing moves until a chip is clicked. Both the glass and the ink recolour — + a screen that changes its text and keeps its backlight isn't a screen. */ + svgEl(s, 'rect', { class: 'kp-win', x: 12, y: 10, width: 208, height: 30, rx: 4, + fill: 'var(--scr-bg1, #0a0806)', stroke: 'var(--scr-brd, #2c261d)', 'stroke-width': 2 }); + const disp = svgEl(s, 'text', { x: 20, y: 31, 'font-size': 14, 'letter-spacing': '.14em', + 'font-family': 'var(--mono)', fill: 'var(--scr-hi, var(--gold-hi))' }); let buf = ''; /* The window shows the tail: a passphrase outruns the plate long before MAX. @@ -282,7 +288,14 @@ GW.abcKeypad = function (host, opts = {}) { const render = () => { disp.textContent = buf.length > 13 ? '‹' + show(buf.slice(-12)) : show(buf).padEnd(13, '·'); }; + /* press is the allowlist, not the keydown handler above it. Filtering only in + the handler guards the web and leaves the Emacs port wide open: that port + installs KEYS into a keymap and calls press directly, with no handler in the + stack, so an unguarded press would append a stray 'F1' as literal text. + Gated on ACTIONS rather than on KEYS because they are different sets — CLR + is a real key on the plate that no keystroke maps to. */ const press = k => { + if (!GW.abcKeypad.ACTIONS.has(k)) return; if (k === 'ENT') { onChange(buf, buf ? 'ENTER · ' + buf : 'empty'); return; } if (k === 'DEL') { buf = buf.slice(0, -1); render(); onChange(buf, buf || 'empty'); return; } if (k === 'CLR') { buf = ''; render(); onChange(buf, 'cleared'); return; } @@ -345,9 +358,46 @@ GW.abcKeypad = function (host, opts = {}) { press(k); }); }); + /* Keyboard, per the README's keyboard contract. The listener is bound to the + pad's own focusable element, never to the document: a global binding would + type into this card from anywhere on a 110-card page and fight the gallery's + own Escape handler. GW.slideRule is the precedent. */ + s.setAttribute('class', 'rsvg kp-pad'); + s.setAttribute('tabindex', '0'); + s.addEventListener('click', () => s.focus()); + s.addEventListener('keydown', e => { + if (e.ctrlKey || e.metaKey || e.altKey) return; /* leave shortcuts alone */ + const name = e.key === ' ' ? 'Space' : (e.key.length === 1 ? e.key.toUpperCase() : e.key); + const k = GW.abcKeypad.KEYS[name]; + if (!k) return; /* not on the plate: let it bubble */ + /* Spend preventDefault only where there is a default worth killing — Space + scrolls the page, Backspace can navigate back. Tab and Escape are never + ours: Tab is how the page stays navigable, Escape belongs to the audit + stepper, and neither reaches here anyway because they are not in KEYS. */ + if (name === 'Space' || name === 'Backspace') e.preventDefault(); + press(k); + }); render(); onChange('', 'type a passphrase'); return { el: s, get: () => buf, press }; }; +/* The plate's keys, declared as a table so every target reads the same intent. + Deliberately not a function over a DOM event: the Emacs port installs this + into a keymap and never sees a keydown, so a function would force it to + re-derive what the widget accepts and the two bindings would drift apart. + This is also press()'s allowlist — press appends whatever it is handed, so + without the table a stray 'F1' would land in the buffer as text. */ +GW.abcKeypad.KEYS = (() => { + const m = {}; + for (const c of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') m[c] = c; + m.Space = 'SPC'; m.Backspace = 'DEL'; m.Enter = 'ENT'; + return m; +})(); +/* The plate's whole vocabulary — every argument press accepts, from any caller. + A superset of the KEYS values: CLR is on the plate but no keystroke reaches it + (Escape is the obvious candidate and belongs to the gallery's audit stepper). */ +GW.abcKeypad.ACTIONS = new Set([ + ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 'SPC', 'DEL', 'ENT', 'CLR', +]); /* 03 horizontal fader — continuous 0-100 */ GW.faderH = function (host, opts = {}) { @@ -4174,6 +4224,13 @@ const GW_CSS = ` .switch.red::before{content:"OFF";order:1;color:var(--cream)} .switch.red::after{order:2} +/* The ABC keypad takes keys, so it must show when it is the one listening: an + unlit focus state means typing vanishes into a card you thought was live. + :focus, not :focus-visible — Chrome won't match :focus-visible on a + mouse-driven focus of a non-text element, and clicking the plate IS how it + gets focus here, so the ring would have appeared only when tabbed to. */ +.kp-pad{outline:none} +.kp-pad:focus{outline:2px solid var(--gold-hi);outline-offset:3px;border-radius:9px} .key{font:inherit;font-size:11.5px;letter-spacing:.06em;color:var(--silver);cursor:pointer; background:linear-gradient(180deg,#23211e,#191715);border:1px solid #33302b;border-bottom-color:#0c0b0a; border-radius:8px;padding:8px 12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.04),0 2px 3px rgba(0,0,0,.4)} |
