diff options
Diffstat (limited to 'docs/prototypes')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 11 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 108 |
2 files changed, 119 insertions, 0 deletions
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)}), '<b>attention management, not selection.</b> 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)}), + '<b>letters on a faceplate.</b> 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; |
