diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-16 15:12:23 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-16 15:12:23 -0500 |
| commit | c41412dc083fab137380cafcc4964887e83964ac (patch) | |
| tree | 190c5731bd6279943d099242ddf5efcac9022b73 /docs/prototypes/widgets.js | |
| parent | 9320bb32cc5db4595fc007d8cc731623e26e20c0 (diff) | |
| download | archsetup-c41412dc083fab137380cafcc4964887e83964ac.tar.gz archsetup-c41412dc083fab137380cafcc4964887e83964ac.zip | |
feat(gallery): add the ABC entry keypad
The kit entered digits three ways and text no way at all, so the panels that need a WiFi password had nothing to reach for. R57 takes free alphanumeric text. A-Z is laid out alphabetically rather than QWERTY, the industrial convention wherever the operator isn't assumed to touch-type, and what makes this a faceplate instead of a keyboard.
I followed two reference plates and departed from both, arguing each departure at its site. Their letters are blue and the kit has no blue control colour, so letters take the standard pale cap and digits a darker one: the photos' two-tone grouping without the foreign hue. Letters sit left and digits right so the alphabet reads down one unbroken block. With the digits on the left it stops column-aligning with itself halfway down.
DEL is new, because without a backspace one mistyped character costs the whole entry. It sits beside the digits in amber while CLR is exiled to the corner in red. DEL is constant and costs a character, CLR is rare and costs everything, so the safe key gets the good spot and the colour grades the cost before the legend is read. CANCEL is gone: on a plate that is the whole terminal it has a transaction to abandon, but here it did exactly what CLR does.
The window draws spaces as ␣. SVG collapses whitespace, and past the truncation boundary there are no pad dots left for a space to displace, so SPACE was a keypress with no feedback at all. The buffer keeps the real character.
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 108 |
1 files changed, 108 insertions, 0 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 147c782..48820cd 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -241,6 +241,114 @@ GW.consoleKeys.DEFAULT_KEYS = [ { label: 'SCAN' }, { label: 'LIVE', tone: 'green' }, { label: 'MUTED', tone: 'red' }, ]; +/* R57 ABC entry keypad — alphanumeric text on a faceplate. + Modelled on the industrial membrane keypad (reference: fleet/kiosk gear, + 2026-07-16-abc-keypad-membrane-color.png): a 0-9 block beside A-Z laid out + ALPHABETICALLY, not QWERTY. That is the convention wherever the operator + can't be assumed to touch-type, and it is what makes this a faceplate rather + than a keyboard — R16's alphanumeric sibling. + Departures from the photo, all deliberate and all noted at their site: + - Its letters are blue. The kit has no blue control colour (blue appears + only as a screen phosphor and a jewel lens), so the letters take the + standard pale keycap and the digits a darker one — which keeps the + photo's two-tone digit/letter grouping without importing a foreign hue. + CLEAR/NO and ENTER/YES keep the reference's red and green, which are + already --fail and --pass. + - Letters left, digits right (the stainless reference's arrangement). + - DEL added, CANCEL dropped. See the layout block below for both. */ +GW.abcKeypad = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const MAX = opts.max || 16; + const s = stageSvg(host, 'rsvg', 232, 226); + gradDef('abcKey', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#dbd8cf'], ['1', '#a29d92']]); + gradDef('abcNum', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#6f6a61'], ['1', '#464239']]); + gradDef('abcRed', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#d98a6f'], ['1', 'var(--fail)']]); + gradDef('abcGrn', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#9cbf5e'], ['1', 'var(--pass)']]); + 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)' }); + + let buf = ''; + /* The window shows the tail: a passphrase outruns the plate long before MAX. + Spaces are drawn as ␣ rather than as themselves. SVG collapses whitespace, + so a real space is invisible — and past the truncation boundary there are no + pad dots left for it to displace, which makes SPACE a keypress with no + feedback at all: press, see nothing, press again, and now carry two spaces + you can't see in a passphrase you can't read back. Only the DISPLAY is + substituted; buf keeps the real character. */ + const show = t => t.replace(/ /g, '␣'); + const render = () => { + disp.textContent = buf.length > 13 ? '‹' + show(buf.slice(-12)) : show(buf).padEnd(13, '·'); + }; + const press = k => { + 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; } + if (buf.length >= MAX) { onChange(buf, 'full · ' + buf); return; } + buf += (k === 'SPC' ? ' ' : k); render(); onChange(buf, buf); + }; + /* [label, col, row, width-in-cols, tone]. + Letters left, numbers right — the stainless reference's arrangement rather + than the membrane's. The membrane photo puts its digit block on the left, + which lands A-L in columns 3-5 while M-X starts at column 0: the alphabet + stops column-aligning with itself halfway down and the eye has to jump. + This way A-Z reads straight down the plate in one continuous block and the + digits keep their own quadrant. */ + const L = []; + const DIG = ['1','2','3','4','5','6','7','8','9']; + [...'ABCDEFGHIJKL'].forEach((c, i) => L.push([c, i % 3, Math.floor(i / 3), 1, 'key'])); + [...'MNOPQRSTUVWX'].forEach((c, i) => L.push([c, i % 6, 4 + Math.floor(i / 6), 1, 'key'])); + /* DEL earns its own key: without a backspace the only exit from a typo is CLR, + which on a long passphrase means retyping the lot. + The three function keys read as a ladder, so the colour says what a key + costs before you read its legend: amber DEL takes one character back, red + CLR throws the whole entry away, green ENT commits it. Plain caps do + nothing you'd regret. (The stainless reference's backspace is plain steel, + but that plate has no other coloured key to be graded against.) + DEL sits in the block beside the digits and CLR is exiled to the far corner, + which is the opposite of where they started. Two reasons, pulling the same + way: DEL is the one you reach for constantly and CLR is the one you reach + for almost never, and a mis-hit on DEL costs one character where a mis-hit + on CLR costs the whole entry. Frequency and blast radius both say the safe + key gets the good spot. + The reference plate also carries CANCEL, and this one deliberately doesn't. + On that device the plate IS the whole terminal, so CANCEL has a transaction + to abandon; here the keypad is one control inside a panel that owns its own + dismiss, which left CANCEL doing exactly what CLR does. A key that means + nothing the panel doesn't already mean is a key worth removing. */ + L.push(['Y', 0, 6, 1, 'key'], ['Z', 1, 6, 1, 'key'], ['SPC', 2, 6, 3, 'key'], + ['CLR', 5, 6, 1, 'red']); + DIG.forEach((d, i) => L.push([d, 3 + (i % 3), Math.floor(i / 3), 1, 'num'])); + L.push(['DEL', 3, 3, 1, 'amb'], ['0', 4, 3, 1, 'num'], ['ENT', 5, 3, 1, 'grn']); + const FILL = { key: 'url(#abcKey)', num: 'url(#abcNum)', red: 'url(#abcRed)', grn: 'url(#abcGrn)', amb: 'url(#abcAmb)' }; + const INK = { key: '#2b2721', num: '#efeae0', red: 'var(--cream)', grn: 'var(--cream)', amb: 'var(--panel)' }; + /* every function key wears a three-letter legend, so they read as one family + and each fits a single-column cap; only SPACE has the width to spell out */ + const FACE = { SPC: 'SPACE' }; + /* per-key rotation: a real plate is printed, not typeset — the legends sit a + fraction off true. Same trick R16 uses, keyed off the index so it is stable. */ + const jitter = i => [-1.4, .9, -.6, 1.2, 0, -1.1, .7, -.9, 1.3, .4, -.5, 1][i % 12]; + L.forEach(([k, c, r, w, tone], i) => { + const x = 14 + c * 35.5, y = 50 + r * 24.5, wd = w * 35.5 - 5.5; + const g = svgEl(s, 'g', {}); g.setAttribute('class', 'kp-key'); g.dataset.k = k; + g.style.cursor = 'pointer'; g.style.transition = 'transform .07s'; + svgEl(g, 'rect', { x, y, width: wd, height: 20, rx: 3.5, fill: FILL[tone], stroke: '#4e4a42', 'stroke-width': 1, 'stroke-opacity': .8 }); + const t = svgEl(g, 'text', { x: x + wd / 2, y: y + 14.5, 'text-anchor': 'middle', + 'font-size': w > 1 ? 8 : 11, 'font-weight': 700, 'font-family': 'var(--mono)', fill: INK[tone], + transform: `rotate(${jitter(i)},${x + wd / 2},${y + 10})` }); + t.textContent = FACE[k] || k; + g.addEventListener('click', () => { + g.style.transform = 'translateY(1.5px)'; + setTimeout(() => { g.style.transform = ''; }, 80); + press(k); + }); + }); + render(); onChange('', 'type a passphrase'); + return { el: s, get: () => buf, press }; +}; + /* 03 horizontal fader — continuous 0-100 */ GW.faderH = function (host, opts = {}) { const onChange = opts.onChange || noop; |
