aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-16 15:56:54 -0500
committerCraig Jennings <c@cjennings.net>2026-07-16 15:56:54 -0500
commita6e5161dd98987043c4112e3f26d33a3aed11f97 (patch)
tree17e4d4e851d43f4238d67f3a6cba4023e588bc80 /docs
parent10964001cf3b391011b7b48bd0d41f2e73ee261b (diff)
downloadarchsetup-a6e5161dd98987043c4112e3f26d33a3aed11f97.tar.gz
archsetup-a6e5161dd98987043c4112e3f26d33a3aed11f97.zip
feat(gallery): give the ABC keypad a keyboard and screen families
Twenty clicks for a passphrase was always the wrong answer. The plate takes focus on click and reads the keyboard now: lowercase normalises, Space maps to SPC and shows as ␣, Backspace to DEL, Enter to ENT. Every key routes through the same press() a click does, so the two paths can't drift. press() is where the filter belongs, not the keydown handler. I had it in the handler first, which guards the web and leaves the Emacs port wide open, since that port installs KEYS into a keymap and calls press directly with no handler in the stack. It gates on ACTIONS rather than KEYS because they're different sets: CLR is a real key on the plate that no keystroke reaches. The focus ring uses :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 takes focus, so the ring would have shown only when tabbed to. On a 110-card page that means typing into a card with no sign it was listening. The window is a screen like the scope's or the marquee's, so it takes the screen families through the --scr-* vars with the shipped colours as fallbacks: nothing moves until a chip is clicked. Glass and ink both recolour, because a screen that changes its text and keeps its backlight isn't a screen. It offers all six families where its siblings carry five, since a passphrase window has no reason to prefer one phosphor.
Diffstat (limited to 'docs')
-rw-r--r--docs/prototypes/panel-widget-gallery.html11
-rw-r--r--docs/prototypes/widgets.js61
2 files changed, 66 insertions, 6 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 455b0ff..790a2da 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -684,12 +684,12 @@ 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.',
+'R57':{input:'Click keys to type, or click the plate and type on the keyboard (letters, digits, space, Backspace, Enter). DEL takes back one character, CLR wipes the lot, ENT commits.',
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.',
+ limits:'Clicking is slow by design: a 20-character passphrase is 20 clicks, which is why the plate also takes the keyboard when focused. It is for panels with no keyboard of their own, and a real one still beats it for volume.',
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.',
+ difficulty:'Intuitive to use. Clicking is tedious past a few words, which is what the keyboard is for.',
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.',
@@ -1351,7 +1351,7 @@ card(C,'R51','Voice-loop keyset',
'<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.');
+ '<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. Click the keys, or click the plate and type: it takes the keyboard once focused, and every key routes through the same handle a click does. 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');
@@ -1653,6 +1653,9 @@ screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'gree
screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white');
screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber');
screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green');
+/* all six, where its siblings each carry five: a passphrase window has no reason
+ to prefer one phosphor, so it offers the whole set including the marquee cyan */
+screenChips('R57',$('card-R57').gw.el,['amber','green','red','blue','vfd','white'],'amber');
BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no);
if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');});
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)}