From c41412dc083fab137380cafcc4964887e83964ac Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 16 Jul 2026 15:12:23 -0500 Subject: feat(gallery): add the ABC entry keypad MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/prototypes/panel-widget-gallery.html | 11 +++ docs/prototypes/widgets.js | 108 ++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) (limited to 'docs/prototypes') diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 007b4cd..455b0ff 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -684,6 +684,14 @@ const INFO={ origin:'PDP-11/70 and IMSAI front panels.',difficulty:'Intuitive to watch, expert to read.', prefer:'A spinner says busy; blinkenlights say HOW busy and with what rhythm.', period:'1965-80 minicomputer era; the icon of computing at work.'}, +'R57':{input:'Click keys to type; DEL takes back one character, CLR wipes the lot, ENT commits. Click-only, so it ports to Emacs unchanged.', + solves:'Free alphanumeric text on a panel that has no keyboard — the one job the rest of the kit cannot do.', + use:'Specialty. Shines where a panel must accept a string it could not know in advance: a passphrase, an SSID, a callsign, a label.', + limits:'Slow by design — a 20-character passphrase is 20 clicks. Any real keyboard beats it for volume; this is for panels that have none.', + origin:'Fleet, kiosk and access-control keypads — a membrane plate for the colour-coded function keys, a stainless one for the letters-left layout and the backspace.', + difficulty:'Intuitive to use, tedious past a few words.', + prefer:'Text is genuinely unbounded. R16 is the digits-only sibling and is faster where the alphabet is not needed.', + ref:'../../working/retro-stereo-widgets/references/2026-07-16-abc-keypad-membrane-color.png'}, 'R56':{input:'Drag the left half for temperature, the right for humidity. Drag-only, two surfaces.', solves:'A categorical verdict from two values — the crossing point falls into a printed judgment.', use:'Common on weather stations and comfort meters. Shines when the pair only matters as a combined condition: comfort, safe-operating region, duty envelope.', @@ -1341,6 +1349,9 @@ card(C,'R50','Two-hand safety control', card(C,'R51','Voice-loop keyset', (st,rd)=>GW.voiceLoop(st,{onChange:(v,t)=>rd(t)}), 'attention management, not selection. Each loop is independent: click to monitor (green bar, flickering with activity), again to talk (amber, exclusive — one voice out), again to drop. Flight controllers ran a dozen of these at once. After a mission-control comm keyset.'); +card(C,'R57','ABC entry keypad', + (st,rd)=>GW.abcKeypad(st,{onChange:(v,t)=>rd(t)}), + 'letters on a faceplate. A-Z laid out alphabetically, not QWERTY — the industrial convention wherever the operator is not assumed to touch-type. Type into the window; DEL takes back one character, CLR wipes the lot, ENT commits. R16 enters digits; this is its alphanumeric sibling, and the only card in the kit that takes free text. After fleet and kiosk keypads: the membrane plate\'s colour-coded CLR / ENT, the stainless plate\'s letters-left arrangement and its backspace. Its CANCEL is dropped — that plate is a whole terminal with a transaction to abandon, where this is one control in a panel that owns its own dismiss.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); 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; -- cgit v1.2.3