diff options
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 14 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 257 | ||||
| -rw-r--r-- | tests/gallery-probes/probe-fams.mjs | 8 | ||||
| -rw-r--r-- | tests/gallery-probes/probe.mjs | 102 |
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( |
