aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 13:27:11 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 13:27:11 -0500
commit0b596f1c3e98d57f23aee10931348aea39da727d (patch)
tree89162b39462a38a49028343bd6f654d4d33f440e
parent63fd47c2a1a03da7cdce47219c541fef90545b78 (diff)
downloadarchsetup-0b596f1c3e98d57f23aee10931348aea39da727d.tar.gz
archsetup-0b596f1c3e98d57f23aee10931348aea39da727d.zip
feat(gallery): bring ten core controls to the extraction bar
Batch 2 of the extraction sweep: consoleKeys, abcKeypad, indexPlate, faderH, faderV, knob, segmented, chipToggle, armButton and lampRow now carry the split-flap-shape contract comment. Every class they own wears the dupre- prefix, internals and modifiers included (dupre-key dupre-green, dupre-fader dupre-cap), with every in-kit consumer updated in the same pass: the dsky and annunciator pads, the rotarySelector and encoder spindles, the page's size bar, and the probe selectors. I also removed indexPlate's dead '#dupre-defs' lookup (nothing ever created that id) and dropped the unused .key.off rule. The faders, knob and lampRow now cache their element refs instead of re-querying every set(), and segmented and lampRow clamp set() the way consoleKeys does. The audit's contract count moves from 14 to 24 of 111.
-rw-r--r--docs/prototypes/panel-widget-gallery.html14
-rw-r--r--docs/prototypes/widgets.js257
-rw-r--r--tests/gallery-probes/probe-fams.mjs8
-rw-r--r--tests/gallery-probes/probe.mjs102
4 files changed, 229 insertions, 152 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 0bf3ab8..3d31b6b 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -219,10 +219,10 @@ a.no{text-decoration:none;cursor:pointer}
switches, keys and lamps. Each card carries a live readout that tracks what you do. The trace displays run
a live signal; the pointer types drive from your drag.</p>
<div class="szbar"><span class="lab">instruments/row</span>
- <button class="key" data-cols="1">1</button>
- <button class="key" data-cols="2">2</button>
- <button class="key on" data-cols="3">3</button>
- <button class="key" data-cols="4">4</button>
+ <button class="dupre-key" data-cols="1">1</button>
+ <button class="dupre-key" data-cols="2">2</button>
+ <button class="dupre-key dupre-on" data-cols="3">3</button>
+ <button class="dupre-key" data-cols="4">4</button>
</div>
</div>
<nav class="toc">
@@ -266,15 +266,15 @@ a.no{text-decoration:none;cursor:pointer}
const $ = id => document.getElementById(id);
const {svgEl,polar,dragX,dragY,dragDelta,seg7,buildBars,VUDB,vuDb,SCREEN_FAMS}=DUPRE;
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
-document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',()=>{
+document.querySelectorAll('.szbar .dupre-key').forEach(b=>b.addEventListener('click',()=>{
document.body.dataset.cols=b.dataset.cols;
localStorage.setItem('gv-cols',b.dataset.cols);
- document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k===b));
+ document.querySelectorAll('.szbar .dupre-key').forEach(k=>k.classList.toggle('dupre-on',k===b));
}));
(function(){const s=localStorage.getItem('gv-cols');
if(!s||!/^[1234]$/.test(s))return;
document.body.dataset.cols=s;
- document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k.dataset.cols===s));})();
+ document.querySelectorAll('.szbar .dupre-key').forEach(k=>k.classList.toggle('dupre-on',k.dataset.cols===s));})();
/* boost the intrinsically tiny instruments (audit: content under 10% of the stage) */
const BOOST=['01','07','08','12','13','N14','18'];
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index e6f02ce..1665b23 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -258,21 +258,28 @@ DUPRE.slideToggle.STYLES = {
tone picks the engaged look: undefined = gold (the default lit key), 'green' =
run green, 'red' = terracotta. Reading order runs safe -> live -> muted, and
LIVE takes green because that is what --pass means everywhere else in the kit
- (the palette names it "run lamps, gear greens, monitor bars, LIVE lamps"). */
+ (the palette names it "run lamps, gear greens, monitor bars, LIVE lamps").
+ Contract (everything a consumer needs; no page globals touched):
+ opts: keys ([{label, tone}], default DEFAULT_KEYS); active (index; the
+ default set rests on LIVE, a caller's own set on its first key);
+ onChange(idx, label) fires on every engage.
+ handle: el, get() (engaged index), set(i) (clamped).
+ CSS lives in the "console keys" block of DUPRE_CSS; dsky and annunciator
+ reuse the key class for their pads. */
DUPRE.consoleKeys = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const keys = opts.keys || DUPRE.consoleKeys.DEFAULT_KEYS;
const wrap = document.createElement('span'); host.appendChild(wrap);
const btns = keys.map(k => {
- const b = document.createElement('button'); b.className = 'key'; b.textContent = k.label;
+ const b = document.createElement('button'); b.className = 'dupre-key'; b.textContent = k.label;
wrap.appendChild(b); return b;
});
let idx;
const set = i => {
idx = Math.max(0, Math.min(keys.length - 1, i));
btns.forEach((b, j) => {
- b.classList.remove('on', 'green', 'red');
- if (j === idx) b.classList.add(keys[idx].tone || 'on');
+ b.classList.remove('dupre-on', 'dupre-green', 'dupre-red');
+ if (j === idx) b.classList.add('dupre-' + (keys[idx].tone || 'on'));
});
onChange(idx, keys[idx].label);
};
@@ -302,7 +309,15 @@ DUPRE.consoleKeys.DEFAULT_KEYS = [
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. */
+ - DEL added, CANCEL dropped. See the layout block below for both.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: max (16, buffer cap); onChange(buf, label) on every press — ENT
+ reports 'ENTER · <buf>', DEL/CLR report the remaining buffer.
+ handle: el (focusable; keyboard per KEYS), get() (the buffer), press(k)
+ (any ACTIONS member: plate characters, SPC, DEL, ENT, CLR).
+ CSS lives in the "keypad / index-plate focus" block of DUPRE_CSS (the
+ faceplate itself is drawn SVG); the entry window recolours with the shared
+ --scr-* screen vars. */
DUPRE.abcKeypad = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const MAX = opts.max || 16;
@@ -318,7 +333,7 @@ DUPRE.abcKeypad = function (host, opts = {}) {
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,
+ svgEl(s, 'rect', { class: 'dupre-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))' });
@@ -392,7 +407,7 @@ DUPRE.abcKeypad = function (host, opts = {}) {
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;
+ const g = svgEl(s, 'g', {}); g.setAttribute('class', 'dupre-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',
@@ -409,7 +424,7 @@ DUPRE.abcKeypad = function (host, opts = {}) {
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. DUPRE.slideRule is the precedent. */
- s.setAttribute('class', 'rsvg kp-pad');
+ s.setAttribute('class', 'rsvg dupre-kp-pad');
s.setAttribute('tabindex', '0');
s.addEventListener('click', () => s.focus());
s.addEventListener('keydown', e => {
@@ -462,7 +477,16 @@ DUPRE.abcKeypad.ACTIONS = new Set([
the entire interaction. Ours is alphabetical, capitals beside lowercase at the
same column offset: find the letter, then pick the case.
The layout is a table rather than drawing, because Craig has already said the
- keys will be revisited and a layout welded into the geometry never is. */
+ keys will be revisited and a layout welded into the geometry never is.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: max (22, line cap); onChange(buf, label) on select, print and clear.
+ handle: el (focusable; typing selects a cell, Enter pulls the lever),
+ get() (the printed line), select(c), print(), press(k) (any
+ ACTIONS member: plate characters, PRINT, CLR), selected() (the
+ stylus position, null before the first select).
+ CSS lives in the "keypad / index-plate focus" block of DUPRE_CSS (the
+ machine itself is drawn SVG); the paper recolours with the shared --scr-*
+ screen vars. */
DUPRE.indexPlate = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const MAX = opts.max || 22;
@@ -480,7 +504,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
legend and then the lever. Growth was always safe; shrink was the trap. */
const VW = PLATE_R + GUT;
const VH = Math.max(PY - 10 + ROWS * CH + 16 + 16, PY + 130);
- const s = stageSvg(host, 'rsvg ix-pad', VW, VH);
+ const s = stageSvg(host, 'rsvg dupre-ix-pad', VW, VH);
gradDef('ixPlate', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#efe6c4'], ['1', '#d8caa0']]);
gradDef('ixBody', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#26221c'], ['1', '#100e0b']]);
gradDef('ixSteel', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 1 }, [['0', '#e6e9ef'], ['1', '#8d93a1']]);
@@ -495,7 +519,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
The ring area carries a halftone screen, which is what the real plate prints
its outer characters onto. */
if (!document.getElementById('ixHalf')) {
- const defs = s.ownerDocument.querySelector('#dupre-defs') || svgEl(s, 'defs', {});
+ const defs = svgEl(s, 'defs', {});
const pat = svgEl(defs, 'pattern', { id: 'ixHalf', width: 4, height: 4, patternUnits: 'userSpaceOnUse' });
svgEl(pat, 'rect', { width: 4, height: 4, fill: 'url(#ixPlate)' });
svgEl(pat, 'circle', { cx: 1, cy: 1, r: 1.05, fill: '#2b2318', 'fill-opacity': .55 });
@@ -528,7 +552,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
Mignon's arm reaches back to a pivot, but a full arm drawn here crosses the
plate and hides the characters the operator is trying to read — the one thing
this instrument must not do. A tip is enough to say "the pointer is here". */
- const arm = svgEl(s, 'g', {}); arm.setAttribute('class', 'ix-stylus');
+ const arm = svgEl(s, 'g', {}); arm.setAttribute('class', 'dupre-ix-stylus');
svgEl(arm, 'line', { x1: 0, y1: -9, x2: 0, y2: -20, stroke: 'url(#ixSteel)', 'stroke-width': 2.6 });
svgEl(arm, 'circle', { cx: 0, cy: -21, r: 3.4, fill: 'url(#ixSteel)', stroke: '#5c626e', 'stroke-width': .6 });
svgEl(arm, 'path', { d: 'M -3.6 -9 L 3.6 -9 L 0 0 Z', fill: 'url(#ixSteel)', stroke: '#41464f', 'stroke-width': .7 });
@@ -553,7 +577,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
const z = zoneAt(r, i);
const DISC = { caps: '#231d13', lower: '#f2ecd6', ring: '#f2ecd6' };
const INK = { caps: '#f2ecd6', lower: '#231d13', ring: '#231d13' };
- const g = svgEl(s, 'g', {}); g.setAttribute('class', 'ix-cell');
+ const g = svgEl(s, 'g', {}); g.setAttribute('class', 'dupre-ix-cell');
g.dataset.c = c; g.dataset.zone = z;
g.style.cursor = 'pointer';
const ring = svgEl(g, 'circle', { cx: x, cy: y, r: 8.6, fill: DISC[z],
@@ -568,7 +592,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
/* the lever: the only thing that prints. Lives in the gutter right of the plate */
const LX = PLATE_R + GUT / 2;
- const lever = svgEl(s, 'g', {}); lever.setAttribute('class', 'ix-lever');
+ const lever = svgEl(s, 'g', {}); lever.setAttribute('class', 'dupre-ix-lever');
lever.style.cursor = 'pointer'; lever.style.transition = 'transform .08s';
svgEl(lever, 'rect', { x: LX - 3.5, y: PY + 6, width: 7, height: 72, rx: 3.5, fill: 'url(#ixSteel)', stroke: '#5c626e', 'stroke-width': .8 });
svgEl(lever, 'circle', { cx: LX, cy: PY + 2, r: 8, fill: 'url(#ixSteel)', stroke: '#5c626e', 'stroke-width': 1 });
@@ -585,7 +609,7 @@ DUPRE.indexPlate = function (host, opts = {}) {
s.appendChild(lever);
/* fresh paper */
- const clr = svgEl(s, 'g', {}); clr.setAttribute('class', 'ix-clear'); clr.style.cursor = 'pointer';
+ const clr = svgEl(s, 'g', {}); clr.setAttribute('class', 'dupre-ix-clear'); clr.style.cursor = 'pointer';
/* anchored to PY like the rest of the gutter stack, not to VH — mixing the two
is what let a shorter plate slide this up onto the PRINT legend */
svgEl(clr, 'rect', { x: LX - 16, y: PY + 104, width: 32, height: 18, rx: 3, fill: '#3a332a', stroke: '#5c5348', 'stroke-width': 1 });
@@ -673,17 +697,22 @@ DUPRE.indexPlate.KEYS = (() => {
control that no keystroke reaches, the same shape as the keypad's. */
DUPRE.indexPlate.ACTIONS = new Set([...DUPRE.indexPlate.LAYOUT.flat().filter(Boolean), 'PRINT', 'CLR']);
-/* 03 horizontal fader — continuous 0-100 */
+/* 03 horizontal fader — continuous 0-100; drag anywhere on the slot to seek.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 68); onChange(value, label) on every move.
+ handle: el, get(), set(v) (clamped 0-100).
+ CSS lives in the "fader" block of DUPRE_CSS; no other styles involved. */
DUPRE.faderH = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const f = document.createElement('div'); f.className = 'fader';
- f.innerHTML = '<div class="slot"><div class="fill"></div></div><div class="cap"></div>';
+ const f = document.createElement('div'); f.className = 'dupre-fader';
+ f.innerHTML = '<div class="dupre-slot"><div class="dupre-fill"></div></div><div class="dupre-cap"></div>';
host.appendChild(f);
+ const fill = f.querySelector('.dupre-fill'), cap = f.querySelector('.dupre-cap');
let val;
const set = p => {
val = Math.max(0, Math.min(100, p));
- f.querySelector('.fill').style.width = val + '%';
- f.querySelector('.cap').style.left = val + '%';
+ fill.style.width = val + '%';
+ cap.style.left = val + '%';
onChange(val, 'level ' + Math.round(val));
};
dragX(f, set);
@@ -691,17 +720,24 @@ DUPRE.faderH = function (host, opts = {}) {
return { el: f, get: () => val, set };
};
-/* 04 vertical fader — one channel-strip fader; compose per channel */
+/* 04 vertical fader — continuous 0-100, one channel-strip fader; compose per
+ channel.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (0-100, default 60); onChange(value, label) on every move.
+ handle: el, get(), set(v) (clamped 0-100).
+ CSS lives in the "vertical fader" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.faderV = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const f = document.createElement('div'); f.className = 'vfader';
- f.innerHTML = '<div class="slot"><div class="fill"></div></div><div class="cap"></div>';
+ const f = document.createElement('div'); f.className = 'dupre-vfader';
+ f.innerHTML = '<div class="dupre-slot"><div class="dupre-fill"></div></div><div class="dupre-cap"></div>';
host.appendChild(f);
+ const fill = f.querySelector('.dupre-fill'), cap = f.querySelector('.dupre-cap');
let val;
const set = p => {
val = Math.max(0, Math.min(100, p));
- f.querySelector('.fill').style.height = val + '%';
- f.querySelector('.cap').style.bottom = val + '%';
+ fill.style.height = val + '%';
+ cap.style.bottom = val + '%';
onChange(val, 'level ' + Math.round(val));
};
dragY(f, set);
@@ -709,17 +745,23 @@ DUPRE.faderV = function (host, opts = {}) {
return { el: f, get: () => val, set };
};
-/* 05 rotary knob — drag up/down to turn, -150°..+150° sweep */
+/* 05 rotary knob — drag up/down to turn, -150°..+150° sweep.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: min (0), max (100), value (53); onChange(value, label) per step.
+ handle: el, get(), set(v) (clamped min..max).
+ CSS lives in the "rotary knob" block of DUPRE_CSS; rotarySelector and
+ encoder reuse the knob classes for their spindles. */
DUPRE.knob = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const min = opts.min !== undefined ? opts.min : 0, max = opts.max !== undefined ? opts.max : 100;
- const k = document.createElement('span'); k.className = 'knob';
- k.innerHTML = '<span class="ind"></span>';
+ const k = document.createElement('span'); k.className = 'dupre-knob';
+ k.innerHTML = '<span class="dupre-ind"></span>';
host.appendChild(k);
+ const ind = k.querySelector('.dupre-ind');
let val;
const set = v => {
val = Math.max(min, Math.min(max, v));
- k.querySelector('.ind').style.transform = `rotate(${-150 + (val - min) / (max - min) * 300}deg)`;
+ ind.style.transform = `rotate(${-150 + (val - min) / (max - min) * 300}deg)`;
onChange(val, String(Math.round(val)));
};
dragDelta(k, () => val, set, { min, max });
@@ -729,11 +771,17 @@ DUPRE.knob = function (host, opts = {}) {
/* 06 segmented selector — pick one of a few. opts.accent picks the lit-segment
color from DUPRE.segmented.STYLES (amber / green / red); defaults match the
- stylesheet fallbacks. setStyle(axis, name) restyles a live instance. */
+ stylesheet fallbacks. setStyle(axis, name) restyles a live instance.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: items (labels, default TIMER/ALARM/POMO); active (0); accent
+ ('amber', a STYLES name); onChange(idx, label) on every pick.
+ handle: el, get(), set(i) (clamped), setStyle(axis, name).
+ CSS lives in the "stepper / segmented selector" block of DUPRE_CSS; no
+ other styles involved. */
DUPRE.segmented = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const items = opts.items || ['TIMER', 'ALARM', 'POMO'];
- const seg = document.createElement('div'); seg.className = 'seg'; host.appendChild(seg);
+ const seg = document.createElement('div'); seg.className = 'dupre-seg'; host.appendChild(seg);
const setStyle = (axis, name) => {
const o = (DUPRE.segmented.STYLES[axis] || {})[name];
if (!o) return;
@@ -742,7 +790,11 @@ DUPRE.segmented = function (host, opts = {}) {
setStyle('accent', opts.accent || 'amber');
const btns = items.map(t => { const b = document.createElement('button'); b.textContent = t; seg.appendChild(b); return b; });
let idx;
- const set = i => { idx = i; btns.forEach((b, j) => b.classList.toggle('on', j === i)); onChange(i, items[i]); };
+ const set = i => {
+ idx = Math.max(0, Math.min(items.length - 1, i));
+ btns.forEach((b, j) => b.classList.toggle('dupre-on', j === idx));
+ onChange(idx, items[idx]);
+ };
btns.forEach((b, i) => b.addEventListener('click', () => set(i)));
set(opts.active || 0);
return { el: seg, get: () => idx, set, setStyle };
@@ -756,10 +808,16 @@ DUPRE.segmented.STYLES = {
},
};
-/* 07 chip toggle — inline binary inside a line of text */
+/* 07 chip toggle — inline binary inside a line of text.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label ('discoverable on'); on (true); accent ('amber', a STYLES
+ name); onChange(on, 'ON'|'OFF') on every flip.
+ handle: el, get(), set(bool), setStyle(axis, name).
+ CSS lives in the "chip toggle" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.chipToggle = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const chip = document.createElement('span'); chip.className = 'chip';
+ const chip = document.createElement('span'); chip.className = 'dupre-chip';
chip.textContent = opts.label || 'discoverable on';
host.appendChild(chip);
const setStyle = (axis, name) => {
@@ -769,7 +827,7 @@ DUPRE.chipToggle = function (host, opts = {}) {
};
setStyle('accent', opts.accent || 'amber');
let on;
- const set = v => { on = !!v; chip.classList.toggle('on', on); onChange(on, on ? 'ON' : 'OFF'); };
+ const set = v => { on = !!v; chip.classList.toggle('dupre-on', on); onChange(on, on ? 'ON' : 'OFF'); };
chip.addEventListener('click', () => set(!on));
set(opts.on !== undefined ? opts.on : true);
return { el: chip, get: () => on, set, setStyle };
@@ -780,36 +838,52 @@ DUPRE.chipToggle = function (host, opts = {}) {
to the consumer. */
DUPRE.chipToggle.STYLES = { accent: DUPRE.accentStyles('--chip-on') };
-/* 08 arm-to-fire — two-stage confirm for destructive actions */
+/* 08 arm-to-fire — two-stage confirm for destructive actions: first click
+ arms, second fires and resets to safe.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label ('forget'); armLabel (label + '? again'); onChange(state,
+ label) with state 'safe' | 'armed' | 'fired'.
+ handle: el, get() ('armed'|'safe'), fire() (clicks through one stage).
+ CSS lives in the "arm-to-fire" block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.armButton = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const label = opts.label || 'forget', armLabel = opts.armLabel || label + '? again';
- const a = document.createElement('button'); a.className = 'arm'; a.textContent = label;
+ const a = document.createElement('button'); a.className = 'dupre-arm'; a.textContent = label;
host.appendChild(a);
let armed = false;
a.addEventListener('click', () => {
- if (!armed) { armed = true; a.classList.add('armed'); a.textContent = armLabel; onChange('armed', 'ARMED'); }
- else { armed = false; a.classList.remove('armed'); a.textContent = label; onChange('fired', 'FIRED · reset'); }
+ if (!armed) { armed = true; a.classList.add('dupre-armed'); a.textContent = armLabel; onChange('armed', 'ARMED'); }
+ else { armed = false; a.classList.remove('dupre-armed'); a.textContent = label; onChange('fired', 'FIRED · reset'); }
});
onChange('safe', 'SAFE');
return { el: a, get: () => (armed ? 'armed' : 'safe'), fire: () => a.click() };
};
-/* 09 lamp row — actionable list item: lamp + name + status, click to cycle */
+/* 09 lamp row — actionable list item: lamp + name + status, click to cycle.
+ states are [lampTone, label] pairs; lampTone '' | 'gold' | 'busy' | 'red'
+ maps onto the shared dupre-lamp classes.
+ Contract (everything a consumer needs; no page globals touched):
+ opts: name ('WH-1000XM4'); states ([[tone, label], ...]); state (0);
+ onChange(idx, label) on every step.
+ handle: el, get(), set(i) (clamped).
+ CSS lives in the "lamp row (list item)" block of DUPRE_CSS; the lamp
+ itself is the shared dupre-lamp. */
DUPRE.lampRow = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const name = opts.name || 'WH-1000XM4';
const states = opts.states || [['gold', 'tap to connect'], ['busy', 'connecting…'], ['', 'connected']];
- const row = document.createElement('div'); row.className = 'lrow';
- row.innerHTML = `<span class="dupre-lamp"></span><span class="who"><b></b></span><span class="what"></span>`;
+ const row = document.createElement('div'); row.className = 'dupre-lrow';
+ row.innerHTML = `<span class="dupre-lamp"></span><span class="dupre-who"><b></b></span><span class="dupre-what"></span>`;
row.querySelector('b').textContent = name;
host.appendChild(row);
+ const lamp = row.querySelector('.dupre-lamp'), what = row.querySelector('.dupre-what');
let idx;
const set = i => {
- idx = i;
- row.querySelector('.dupre-lamp').className = 'dupre-lamp' + (states[i][0] ? ' dupre-' + states[i][0] : '');
- row.querySelector('.what').textContent = states[i][1];
- onChange(i, states[i][1]);
+ idx = Math.max(0, Math.min(states.length - 1, i));
+ lamp.className = 'dupre-lamp' + (states[idx][0] ? ' dupre-' + states[idx][0] : '');
+ what.textContent = states[idx][1];
+ onChange(idx, states[idx][1]);
};
row.addEventListener('click', () => set((idx + 1) % states.length));
set(opts.state || 0);
@@ -828,12 +902,12 @@ DUPRE.rotarySelector = function (host, opts = {}) {
p.style.left = POS[i][0] + '%'; p.style.top = POS[i][1] + '%'; p.textContent = v;
rs.appendChild(p);
});
- const k = document.createElement('span'); k.className = 'knob'; k.innerHTML = '<span class="ind"></span>'; rs.appendChild(k);
+ const k = document.createElement('span'); k.className = 'dupre-knob'; k.innerHTML = '<span class="dupre-ind"></span>'; rs.appendChild(k);
host.appendChild(rs);
let idx;
const set = i => {
idx = ((i % values.length) + values.length) % values.length;
- rs.querySelector('.ind').style.transform = `rotate(${ANG[idx]}deg)`;
+ rs.querySelector('.dupre-ind').style.transform = `rotate(${ANG[idx]}deg)`;
rs.querySelectorAll('.pos').forEach((p, j) => p.classList.toggle('on', j === idx));
onChange(values[idx], fmt(values[idx]));
};
@@ -1017,7 +1091,7 @@ DUPRE.encoder = function (host, opts = {}) {
d.style.left = (cx + R * Math.cos(a)) + 'px'; d.style.top = (cy + R * Math.sin(a)) + 'px';
enc.appendChild(d);
}
- const k = document.createElement('span'); k.className = 'knob'; k.innerHTML = '<span class="ind"></span>'; enc.appendChild(k);
+ const k = document.createElement('span'); k.className = 'dupre-knob'; k.innerHTML = '<span class="dupre-ind"></span>'; enc.appendChild(k);
let val;
const set = v => {
val = v;
@@ -2302,7 +2376,7 @@ DUPRE.dsky = function (host, opts = {}) {
vals.verb = ' '; vals.noun = ' '; entry = ''; mode = null; hot();
};
KEYS.forEach(k => {
- const b = document.createElement('button'); b.className = 'key'; b.textContent = k;
+ const b = document.createElement('button'); b.className = 'dupre-key'; b.textContent = k;
b.addEventListener('click', () => {
if (k === 'VERB' || k === 'NOUN') { mode = k.toLowerCase(); entry = ''; vals[mode] = ' '; setWin(mode, ' '); hot(); }
else if (k === 'CLR') { if (mode) { vals[mode] = ' '; entry = ''; setWin(mode, ' '); } }
@@ -3965,7 +4039,7 @@ DUPRE.annunciator = function (host, opts = {}) {
const bar = document.createElement('div'); bar.className = 'annbar';
const mc = document.createElement('span'); mc.className = 'mc'; mc.textContent = 'MSTR CAUTION'; bar.appendChild(mc);
['ACK', 'TEST', 'RESET'].forEach(a => {
- const b = document.createElement('button'); b.className = 'key'; b.dataset.k = a; b.textContent = a; bar.appendChild(b);
+ const b = document.createElement('button'); b.className = 'dupre-key'; b.dataset.k = a; b.textContent = a; bar.appendChild(b);
});
wrap.appendChild(bar); host.appendChild(wrap);
const els = cells.map(([label, st]) => {
@@ -3983,7 +4057,7 @@ DUPRE.annunciator = function (host, opts = {}) {
if (j > 0) acked = false; /* a new alarm re-arms the flasher */
refresh(); onChange(j, cells[i][0] + ' → ' + NAMES[j]);
}));
- bar.querySelectorAll('.key').forEach(k => k.addEventListener('click', () => {
+ bar.querySelectorAll('.dupre-key').forEach(k => k.addEventListener('click', () => {
const a = k.dataset.k;
if (a === 'ACK') { if (active() > 0) acked = true; refresh(); }
else if (a === 'RESET') { els.forEach(c => c.className = 'acell'); acked = false; refresh(); }
@@ -4782,48 +4856,51 @@ const DUPRE_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.
+/* keypad / index-plate focus — both take keys, so each 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,.ix-pad{outline:none}
-.kp-pad:focus,.ix-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;
+.dupre-kp-pad,.dupre-ix-pad{outline:none}
+.dupre-kp-pad:focus,.dupre-ix-pad:focus{outline:2px solid var(--gold-hi);outline-offset:3px;border-radius:9px}
+
+/* console keys */
+.dupre-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)}
-.key:hover{color:var(--gold);border-color:var(--gold)}
-.key:active{transform:translateY(1px)}
-.key.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700}
-.key.green{color:var(--cream);background:linear-gradient(180deg,#9cbf5e,var(--pass));border-color:var(--pass);font-weight:700}
-.key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
-.key.off{opacity:.4}
+.dupre-key:hover{color:var(--gold);border-color:var(--gold)}
+.dupre-key:active{transform:translateY(1px)}
+.dupre-key.dupre-on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700}
+.dupre-key.dupre-green{color:var(--cream);background:linear-gradient(180deg,#9cbf5e,var(--pass));border-color:var(--pass);font-weight:700}
+.dupre-key.dupre-red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)}
-.chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
-.chip.on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))}
+/* chip toggle */
+.dupre-chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
+.dupre-chip.dupre-on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))}
.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
.badge.red{background:var(--fail);color:var(--cream)}
.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
/* fader */
-.fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
-.fader .slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
-.fader .fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold))}
-.fader .cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px;
+.dupre-fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
+.dupre-fader .dupre-slot{position:absolute;top:6px;left:0;right:0;height:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
+.dupre-fader .dupre-fill{position:absolute;top:0;left:0;bottom:0;background:linear-gradient(90deg,var(--amber-grad-bot),var(--gold))}
+.dupre-fader .dupre-cap{position:absolute;top:1px;width:7px;height:14px;border-radius:2px;margin-left:-3.5px;
background:linear-gradient(180deg,var(--amber-grad-top),var(--amber-grad-mid));border:1px solid var(--amber-edge);box-shadow:0 1px 2px rgba(0,0,0,.5)}
/* vertical fader */
-.vfader{width:16px;height:64px;position:relative;cursor:pointer;touch-action:none}
-.vfader .slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
-.vfader .fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,var(--amber-grad-bot),var(--gold))}
-.vfader .cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px;
+.dupre-vfader{width:16px;height:64px;position:relative;cursor:pointer;touch-action:none}
+.dupre-vfader .dupre-slot{position:absolute;left:6px;top:0;bottom:0;width:4px;border-radius:2px;background:#0d0f10;border:1px solid #231f18;overflow:hidden}
+.dupre-vfader .dupre-fill{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,var(--amber-grad-bot),var(--gold))}
+.dupre-vfader .dupre-cap{position:absolute;left:1px;height:7px;width:14px;border-radius:2px;margin-top:-3.5px;
background:linear-gradient(90deg,var(--amber-grad-mid),var(--amber-grad-top));border:1px solid var(--amber-edge)}
/* rotary knob */
-.knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none;
+.dupre-knob{width:52px;height:52px;border-radius:50%;position:relative;cursor:ns-resize;touch-action:none;
background:radial-gradient(circle at 40% 35%,#2a2622,#141210);border:1px solid #3a352c;
box-shadow:inset 0 2px 3px rgba(255,255,255,.05),0 3px 6px rgba(0,0,0,.5)}
-.knob .ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi);
+.dupre-knob .dupre-ind{position:absolute;left:50%;top:5px;width:2px;height:16px;background:var(--gold-hi);
margin-left:-1px;transform-origin:50% 21px;border-radius:1px;box-shadow:0 0 5px rgba(var(--glow-hi),.6)}
/* needle gauge */
@@ -4880,21 +4957,21 @@ const DUPRE_CSS = `
.spark svg{display:block;width:100%;height:100%}
/* lamp row (list item) */
-.lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px}
-.lrow:hover{background:var(--wash)}
-.lrow .who{color:var(--silver)}.lrow .who b{color:var(--cream)}
-.lrow .what{margin-left:auto;color:var(--dim);font-size:11px}
+.dupre-lrow{width:190px;display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:7px;background:#141210;cursor:pointer;font-size:12.5px}
+.dupre-lrow:hover{background:var(--wash)}
+.dupre-lrow .dupre-who{color:var(--silver)}.dupre-lrow .dupre-who b{color:var(--cream)}
+.dupre-lrow .dupre-what{margin-left:auto;color:var(--dim);font-size:11px}
/* arm-to-fire */
-.arm{font:inherit;font-size:11.5px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b;
+.dupre-arm{font:inherit;font-size:11.5px;color:var(--silver);cursor:pointer;background:#191715;border:1px solid #33302b;
border-radius:8px;padding:7px 12px}
-.arm.armed{background:rgba(203,107,77,.12);border-color:var(--fail);color:var(--fail)}
+.dupre-arm.dupre-armed{background:rgba(203,107,77,.12);border-color:var(--fail);color:var(--fail)}
/* stepper / segmented selector */
-.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden}
-.seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer}
-.seg button:last-child{border-right:0}
-.seg button.on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700}
+.dupre-seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden}
+.dupre-seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer}
+.dupre-seg button:last-child{border-right:0}
+.dupre-seg button.dupre-on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700}
/* engraved section label */
.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
@@ -4918,7 +4995,7 @@ const DUPRE_CSS = `
/* rotary selector */
.rotsel{position:relative;width:118px;height:74px}
-.rotsel>.knob{position:absolute;left:50%;top:20px;margin-left:-26px;cursor:pointer}
+.rotsel>.dupre-knob{position:absolute;left:50%;top:20px;margin-left:-26px;cursor:pointer}
.rotsel .pos{position:absolute;font-size:9px;color:var(--dim);transform:translate(-50%,-50%);letter-spacing:.02em}
.rotsel .pos.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.55)}
@@ -5001,8 +5078,8 @@ const DUPRE_CSS = `
.encoder .led{position:absolute;width:5px;height:5px;border-radius:50%;background:var(--wash);
left:50%;top:50%;margin:-2.5px}
.encoder .led.on{background:var(--gold);box-shadow:0 0 5px 1px rgba(var(--glow-lo),.7)}
-.encoder .knob{width:40px;height:40px;cursor:ns-resize}
-.encoder .knob .ind{transform-origin:50% 15px;top:4px;height:12px}
+.encoder .dupre-knob{width:40px;height:40px;cursor:ns-resize}
+.encoder .dupre-knob .dupre-ind{transform-origin:50% 15px;top:4px;height:12px}
/* keyed mode switch */
.keylock{position:relative;width:110px;height:70px}
@@ -5193,7 +5270,7 @@ const DUPRE_CSS = `
.dsky .win .wd .seg7{width:13px;height:24px}
.dsky .win.hot .wd{border-color:var(--gold);box-shadow:0 0 5px rgba(var(--glow-lo),.4)}
.dsky .pad{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}
-.dsky .pad .key{padding:4px 0;font-size:8.5px;border-radius:5px;text-align:center;letter-spacing:.03em}
+.dsky .pad .dupre-key{padding:4px 0;font-size:8.5px;border-radius:5px;text-align:center;letter-spacing:.03em}
/* VFD marquee */
.dupre-vfdm{width:176px;height:34px;border-radius:5px;overflow:hidden;position:relative;cursor:pointer;
@@ -5211,7 +5288,7 @@ const DUPRE_CSS = `
.mc.on{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream);font-weight:700;
box-shadow:0 0 8px rgba(203,107,77,.5)}
.mc.on.fl{animation:pulse var(--pulse-rate) ease-in-out infinite}
-.annbar .key{padding:3px 7px;font-size:7.5px;border-radius:4px}
+.annbar .dupre-key{padding:3px 7px;font-size:7.5px;border-radius:4px}
.annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px}
.acell{font-size:8.5px;letter-spacing:.08em;text-align:center;color:var(--dim);padding:6px 4px;border-radius:3px;cursor:pointer;
background:#141210;border:1px solid #262320;line-height:1.2}
diff --git a/tests/gallery-probes/probe-fams.mjs b/tests/gallery-probes/probe-fams.mjs
index 993d22d..c1b5f9d 100644
--- a/tests/gallery-probes/probe-fams.mjs
+++ b/tests/gallery-probes/probe-fams.mjs
@@ -71,17 +71,17 @@ try {
// Default is the shipped gold, unchanged until a chip is clicked. It reads the
// same as the amber family's --scr-hi because --gold-hi IS #ffbe54 — which is
// what makes the amber default pixel-identical rather than a near-miss.
- const kpInk0 = await evl(`getComputedStyle(document.querySelector('#card-R57 .kp-pad text')).fill`);
+ const kpInk0 = await evl(`getComputedStyle(document.querySelector('#card-R57 .dupre-kp-pad text')).fill`);
ok('R57 default ink = gold-hi', kpInk0 === 'rgb(255, 190, 84)', kpInk0);
// vfd recolors both the ink and the window behind it — a screen is not just its text
await evl(`document.querySelector('#card-R57 .fc[title="vfd"]').click()`);
- const kpInk1 = await evl(`getComputedStyle(document.querySelector('#card-R57 .kp-pad text')).fill`);
+ const kpInk1 = await evl(`getComputedStyle(document.querySelector('#card-R57 .dupre-kp-pad text')).fill`);
ok('R57 vfd chip recolors the ink to marquee cyan', kpInk1 === 'rgb(99, 230, 200)', kpInk1);
- /* .kp-win, not the first rect on the pad — that one is the faceplate, and
+ /* .dupre-kp-win, not the first rect on the pad — that one is the faceplate, and
reading it made this check report the plate's colour and fail for the wrong
reason. */
- const kpBg = await evl(`getComputedStyle(document.querySelector('#card-R57 .kp-win')).fill`);
+ const kpBg = await evl(`getComputedStyle(document.querySelector('#card-R57 .dupre-kp-win')).fill`);
ok('R57 vfd chip recolors the window too', kpBg === 'rgb(6, 16, 13)', kpBg);
ok('no exceptions after chip clicks', events.filter(e => e.method === 'Runtime.exceptionThrown').length === 0);
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 4892188..a242c41 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -97,7 +97,7 @@ try {
// 1d. The chip actually recolours, and its default is untouched until asked —
// the same fallback discipline the screen families use.
const chipInk = await evl(`(()=>{
- const chip = document.querySelector('#card-07 .chip');
+ const chip = document.querySelector('#card-07 .dupre-chip');
const lit = () => getComputedStyle(chip).color;
const before = lit();
document.querySelector('#card-07 .fc[title="vfd"]').click();
@@ -181,10 +181,10 @@ try {
// resize to match. One-per-row cards must be much wider than four-per-row,
// and the count actually reaches the grid.
ok('default is 3 instruments/row', await evl(`document.body.dataset.cols`) === '3');
- await evl(`document.querySelector('.szbar .key[data-cols="1"]').click()`);
+ await evl(`document.querySelector('.szbar .dupre-key[data-cols="1"]').click()`);
const w1col = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
const cols1 = await evl(`getComputedStyle(document.querySelector('.grid')).gridTemplateColumns.split(' ').length`);
- await evl(`document.querySelector('.szbar .key[data-cols="4"]').click()`);
+ await evl(`document.querySelector('.szbar .dupre-key[data-cols="4"]').click()`);
const w4col = await evl(`document.querySelector('.card').getBoundingClientRect().width`);
const cols4 = await evl(`getComputedStyle(document.querySelector('.grid')).gridTemplateColumns.split(' ').length`);
ok('1/row is much wider than 4/row', w1col > w4col * 2.5, `w1=${Math.round(w1col)} w4=${Math.round(w4col)}`);
@@ -194,10 +194,10 @@ try {
// 3. behavioral, zoomed: fader drag on card 03 changes readout. Run at 2/row,
// not 1/row: at one-per-row the card is wide enough that its controls fall
// outside the 1600px probe window and the dispatched mouse events miss.
- await evl(`document.querySelector('.szbar .key[data-cols="2"]').click()`);
+ await evl(`document.querySelector('.szbar .dupre-key[data-cols="2"]').click()`);
await evl(`document.querySelectorAll('.card')[2].scrollIntoView({block:'center'}); ''`);
await sleep(200);
- const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
+ const fr = await evl(`(()=>{const c=document.querySelectorAll('.card')[2];const f=c.querySelector('.dupre-fader');const r=f.getBoundingClientRect();return [r.left,r.top,r.width,r.height];})()`);
const before = await evl(`document.getElementById('rd-03').textContent`);
await drag(fr[0] + fr[2] * 0.2, fr[1] + fr[3] / 2, fr[0] + fr[2] * 0.9, fr[1] + fr[3] / 2);
await sleep(150);
@@ -218,21 +218,21 @@ try {
// 5. card 02 console keys: reading order and per-key tone. LIVE is green
// because --pass is what the kit means by live everywhere else; gold stays
// the generic engaged look. Craig's call, 2026-07-16.
- const order = await evl(`[...document.querySelectorAll('#card-02 .key')].map(b => b.textContent)`);
+ const order = await evl(`[...document.querySelectorAll('#card-02 .dupre-key')].map(b => b.textContent)`);
ok('card 02 keys read SCAN, LIVE, MUTED',
JSON.stringify(order) === JSON.stringify(['SCAN', 'LIVE', 'MUTED']), JSON.stringify(order));
const engaged = await evl(`(()=>{
- const lit = [...document.querySelectorAll('#card-02 .key')].filter(b => /\\b(on|green|red)\\b/.test(b.className));
- return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('key ','') : 'lit=' + lit.length;
+ const lit = [...document.querySelectorAll('#card-02 .dupre-key')].filter(b => /\\bdupre-(on|green|red)\\b/.test(b.className));
+ return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('dupre-key dupre-','') : 'lit=' + lit.length;
})()`);
ok('card 02 engages LIVE in green by default', engaged === 'LIVE:green', engaged);
const muted = await evl(`(()=>{
- const keys = [...document.querySelectorAll('#card-02 .key')];
+ const keys = [...document.querySelectorAll('#card-02 .dupre-key')];
keys.find(b => b.textContent === 'MUTED').click();
- const lit = keys.filter(b => /\\b(on|green|red)\\b/.test(b.className));
- return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('key ','') : 'lit=' + lit.length;
+ const lit = keys.filter(b => /\\bdupre-(on|green|red)\\b/.test(b.className));
+ return lit.length === 1 ? lit[0].textContent + ':' + lit[0].className.replace('dupre-key dupre-','') : 'lit=' + lit.length;
})()`);
ok('card 02 MUTED engages red and releases LIVE', muted === 'MUTED:red', muted);
@@ -360,7 +360,7 @@ try {
// the operator can't be assumed to touch-type, and it is what Craig's
// reference photos show. A QWERTY drift here would silently lose the idiom.
const abcOrder = await evl(`(()=>{
- const letters = [...document.querySelectorAll('#card-R57 .kp-key')]
+ const letters = [...document.querySelectorAll('#card-R57 .dupre-kp-key')]
.map(k => k.dataset.k).filter(k => /^[A-Z]$/.test(k));
const want = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];
return JSON.stringify(letters) === JSON.stringify(want)
@@ -369,7 +369,7 @@ try {
ok('R57 carries A-Z in alphabetical order', abcOrder === 'ok', abcOrder);
const abcDigits = await evl(`(()=>{
- const d = [...document.querySelectorAll('#card-R57 .kp-key')]
+ const d = [...document.querySelectorAll('#card-R57 .dupre-kp-key')]
.map(k => k.dataset.k).filter(k => /^[0-9]$/.test(k));
return d.length === 10 ? 'ok' : 'got ' + d.length + ': ' + d.join('');
})()`);
@@ -383,7 +383,7 @@ try {
// start at column 3 while M-X started at column 0).
const layout = await evl(`(()=>{
const x = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? Math.round(g.querySelector('rect').getBBox().x) : null;
};
const colStarts = ['A','D','G','J','M','S','Y'].map(x);
@@ -400,7 +400,7 @@ try {
// deliberate inversion of where they started, easy to "tidy" back.
const reach = await evl(`(()=>{
const box = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? g.querySelector('rect').getBBox() : null;
};
const del = box('DEL'), clr = box('CLR'), ent = box('ENT');
@@ -418,7 +418,7 @@ try {
// the palette may be retuned, the distinction may not collapse.
const ladder = await evl(`(()=>{
const fill = k => {
- const g = [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const g = [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
return g ? g.querySelector('rect').getAttribute('fill') : null;
};
const f = { DEL: fill('DEL'), CLR: fill('CLR'), ENT: fill('ENT'), plain: fill('A'), digit: fill('1') };
@@ -432,7 +432,7 @@ try {
// 11. typing accumulates, in order. A keypad that registers presses but drops
// or reorders them is the failure that matters for a password field.
const typed = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
for (const c of ['W','I','F','I','7']) key(c).dispatchEvent(new MouseEvent('click', {bubbles:true}));
return document.getElementById('rd-R57').textContent;
@@ -443,7 +443,7 @@ try {
// wiping the whole entry, which on a 20-character passphrase means
// starting over — so the check that matters is that DEL is not CLR.
const del = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
click('CLR');
for (const c of ['C','A','B','S']) click(c);
@@ -466,7 +466,7 @@ try {
// passphrase they can't read back. Checked past the 13-char truncation
// boundary, where there are no pad dots left for a space to displace.
const space = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
const win = () => document.querySelector('#card-R57 text[font-size="14"]').textContent;
click('CLR');
@@ -486,7 +486,7 @@ try {
// the checks above — they mutate it, so a check that assumed their leftovers
// would pass or fail on their behaviour instead of its own.
const committed = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const click = k => key(k).dispatchEvent(new MouseEvent('click', {bubbles:true}));
click('CLR');
for (const c of ['N','E','T','5']) click(c);
@@ -523,7 +523,7 @@ try {
// binds globally types into itself from anywhere on a 110-card page.
// Typing at the body with the card unfocused must do nothing at all.
const unfocused = await evl(`(()=>{
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
document.activeElement.blur();
for (const c of ['A','B','C']) document.body.dispatchEvent(
@@ -535,8 +535,8 @@ try {
// 13c. Focused, the same keys land — and land through press(), so click and key
// cannot drift apart.
const typedByKey = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
@@ -554,8 +554,8 @@ try {
// identical buffer and readout. Duplicated logic in the handler would
// pass every check above this one and fail here.
const drift = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
const rd = () => document.getElementById('rd-R57').textContent;
const seq = ['A','B','SPC','7'];
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
@@ -578,7 +578,7 @@ try {
const card = document.getElementById('card-R57');
const h = card.dupre;
if (!h || !h.press) return 'no handle';
- const key = k => [...card.querySelectorAll('.kp-key')].find(e => e.dataset.k === k);
+ const key = k => [...card.querySelectorAll('.dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
h.press('F1'); h.press('ArrowLeft'); h.press('');
const junk = h.get();
@@ -600,7 +600,7 @@ try {
// how the page is navigable and Escape belongs to the audit stepper, so a
// instrument that swallows either breaks something it cannot see.
const defaults = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
pad.focus();
const fired = k => {
const e = new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true });
@@ -621,8 +621,8 @@ try {
// 13e. press() is an allowlist, not a mailbox: an unmapped key must not append.
const unmapped = await evl(`(()=>{
- const pad = document.querySelector('#card-R57 .kp-pad');
- const key = k => [...document.querySelectorAll('#card-R57 .kp-key')].find(e => e.dataset.k === k);
+ const pad = document.querySelector('#card-R57 .dupre-kp-pad');
+ const key = k => [...document.querySelectorAll('#card-R57 .dupre-kp-key')].find(e => e.dataset.k === k);
key('CLR').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
['F1','ArrowLeft','Home','é','!'].forEach(k =>
@@ -639,10 +639,10 @@ try {
// while hunting ("stylus over g"), and asserting on it would fail a correct
// instrument for showing the operator where the pointer is.
const grammar = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
const buf = () => document.getElementById('card-R58').dupre.get();
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
const empty = buf();
['M','i','g'].forEach(c => cell(c).dispatchEvent(new MouseEvent('click', {bubbles:true})));
const afterSelecting = buf();
@@ -654,9 +654,9 @@ try {
// 14b. The lever prints whatever the stylus is resting on, once per pull — the
// operator's two hands are two separate acts.
const spelled = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
for (const c of ['M','i','g','n','o','n']) {
cell(c).dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
@@ -668,9 +668,9 @@ try {
// 14c. Pulling the lever twice prints the character twice: the selection stays
// put, which is what lets you type 'ss' without re-aiming.
const repeat = await evl(`(()=>{
- const cell = c => document.querySelector('#card-R58 .ix-cell[data-c="' + c + '"]');
- const lever = document.querySelector('#card-R58 .ix-lever');
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ const cell = c => document.querySelector('#card-R58 .dupre-ix-cell[data-c="' + c + '"]');
+ const lever = document.querySelector('#card-R58 .dupre-ix-lever');
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
cell('s').dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
lever.dispatchEvent(new MouseEvent('click', {bubbles:true}));
@@ -690,7 +690,7 @@ try {
// That economy is an old value worth keeping, so it's asserted rather than
// tolerated: adding a 1 key would be a silent departure from the reference.
const charset = await evl(`(()=>{
- const have = new Set([...document.querySelectorAll('#card-R58 .ix-cell')].map(c => c.dataset.c));
+ const have = new Set([...document.querySelectorAll('#card-R58 .dupre-ix-cell')].map(c => c.dataset.c));
const need = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ', ...'abcdefghijklmnopqrstuvwxyz',
...'23456789', 'ä','ö','ü','§','½','¼','¾'];
const missing = need.filter(c => !have.has(c));
@@ -707,7 +707,7 @@ try {
// are dark" is wrong, and only position decides.
const zones = await evl(`(()=>{
const disc = c => {
- const g = [...document.querySelectorAll('#card-R58 .ix-cell')].find(e => e.dataset.c === c);
+ const g = [...document.querySelectorAll('#card-R58 .dupre-ix-cell')].find(e => e.dataset.c === c);
return g ? g.querySelector('circle').getAttribute('fill') : null;
};
const caps = disc('P'), lower = disc('p'), ringCap = disc('J'), ringPunct = disc('&');
@@ -725,7 +725,7 @@ try {
const L = DUPRE.indexPlate && DUPRE.indexPlate.LAYOUT;
if (!L) return 'no LAYOUT';
if (!Array.isArray(L) || !L.every(Array.isArray)) return 'LAYOUT is not a grid';
- const cells = document.querySelectorAll('#card-R58 .ix-cell').length;
+ const cells = document.querySelectorAll('#card-R58 .dupre-ix-cell').length;
const declared = L.flat().filter(Boolean).length;
return cells === declared ? 'ok' : 'drawn ' + cells + ' but declared ' + declared;
})()`);
@@ -738,10 +738,10 @@ try {
// controls back onto the characters.
const clear = await evl(`(()=>{
const box = sel => { const e = document.querySelector('#card-R58 ' + sel); return e ? e.getBBox() : null; };
- const cells = [...document.querySelectorAll('#card-R58 .ix-cell')].map(c => c.getBBox());
+ const cells = [...document.querySelectorAll('#card-R58 .dupre-ix-cell')].map(c => c.getBBox());
const right = Math.max(...cells.map(b => b.x + b.width));
const hits = [];
- for (const sel of ['.ix-lever', '.ix-clear']) {
+ for (const sel of ['.dupre-ix-lever', '.dupre-ix-clear']) {
const b = box(sel);
if (!b) { hits.push(sel + ' missing'); continue; }
if (b.x < right) hits.push(sel + ' starts at ' + Math.round(b.x) + ', left of the plate edge ' + Math.round(right));
@@ -757,7 +757,7 @@ try {
// shrink was the trap, which is why VH now takes a floor.
const stack = await evl(`(()=>{
const bb = sel => { const e = document.querySelector('#card-R58 ' + sel); return e ? e.getBBox() : null; };
- const lever = bb('.ix-lever'), clr = bb('.ix-clear');
+ const lever = bb('.dupre-ix-lever'), clr = bb('.dupre-ix-clear');
const legend = [...document.querySelectorAll('#card-R58 text')].find(t => t.textContent === 'PRINT');
if (!lever || !clr || !legend) return 'missing gutter part';
const lg = legend.getBBox();
@@ -766,7 +766,7 @@ try {
if (overlaps(lever, clr)) return 'lever overlaps CLR';
if (overlaps(lg, clr)) return 'PRINT legend overlaps CLR';
if (overlaps(lg, lever)) return 'PRINT legend overlaps the lever';
- const vb = document.querySelector('#card-R58 .ix-pad').viewBox.baseVal;
+ const vb = document.querySelector('#card-R58 .dupre-ix-pad').viewBox.baseVal;
if (clr.y + clr.height > vb.height) return 'CLR falls outside the viewBox';
return 'ok';
})()`);
@@ -815,10 +815,10 @@ try {
// quietly become R57 with a nicer plate, and the one thing it exists to
// demonstrate is gone.
const ixType = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
const h = document.getElementById('card-R58').dupre;
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
['M','i','g'].forEach(send);
const afterTyping = h.get();
@@ -833,10 +833,10 @@ try {
// uppercase; this one must not, and that difference is the plate's whole
// argument for having no shift key.
const ixCase = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
const h = document.getElementById('card-R58').dupre;
const send = k => pad.dispatchEvent(new KeyboardEvent('keydown', { key: k, bubbles: true, cancelable: true }));
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
pad.focus();
send('a'); send('Enter');
send('A'); send('Enter');
@@ -848,7 +848,7 @@ try {
// always. (The missing space cell is a known gap — the real Mignon has a
// separate space key.)
const ixSpace = await evl(`(()=>{
- const pad = document.querySelector('#card-R58 .ix-pad');
+ const pad = document.querySelector('#card-R58 .dupre-ix-pad');
pad.focus();
const e = new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true });
pad.dispatchEvent(e);
@@ -863,7 +863,7 @@ try {
// stylus legitimately stays where the previous check left it.
const ixBlur = await evl(`(()=>{
const h = document.getElementById('card-R58').dupre;
- document.querySelector('#card-R58 .ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
+ document.querySelector('#card-R58 .dupre-ix-clear').dispatchEvent(new MouseEvent('click', {bubbles:true}));
document.activeElement.blur();
const before = JSON.stringify([h.get(), h.selected()]);
['Q','Z'].forEach(k => document.body.dispatchEvent(