aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/prototypes/widgets.js262
-rw-r--r--tests/gallery-probes/probe.mjs57
2 files changed, 240 insertions, 79 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index bd5c79f..57ad33e 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -4346,15 +4346,28 @@ DUPRE.engravedLabel = function (host, opts = {}) {
/* 22 output well — streaming step log, lamp per step; click streams the next
demo step; push([lampCls, name, evidence]) appends programmatically */
+/* 22 output well — a scrolling log of status steps; click streams the next demo
+ step, oldest rows scroll off once the well is full.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: seed (initial rows, default two); steps (the click-cycle demo pool);
+ keep (max visible rows before the oldest scrolls off, default 5).
+ Each row is [tone, label, detail]; tone is '' | 'gold' | 'red' and
+ lights the row's dupre-lamp accent. onChange(row|null, caption) fires
+ on every push and once at build with a hint.
+ handle: el, push(row) — appends a row and trims the well to keep. Click
+ advances through steps and pushes the next.
+ CSS lives in the "output well" block of DUPRE_CSS; the row lamp is the shared
+ .dupre-lamp primitive tinted by the tone accent. */
DUPRE.outputWell = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const seed = opts.seed || [['', 'Link', 'wlp170s0 · @Hyatt'], ['gold', 'DNS', 'resolving…']];
const steps = opts.steps || [['gold', 'Probe', '8.8.8.8 …'], ['', 'Gateway', '10.0.0.1 ok'], ['', 'DNS', '1.1.1.1 ok'], ['red', 'Retry', 'timeout']];
const keep = opts.keep || 5;
- const w = document.createElement('div'); w.className = 'owell'; host.appendChild(w);
+ const w = document.createElement('div'); w.className = 'dupre-owell'; host.appendChild(w);
const add = s => {
- const d = document.createElement('div'); d.className = 'ostep';
- d.innerHTML = `<span class="dupre-lamp${s[0] ? ' dupre-' + s[0] : ''}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`;
+ const d = document.createElement('div'); d.className = 'dupre-ostep';
+ d.innerHTML = `<span class="dupre-lamp${s[0] ? ' dupre-' + s[0] : ''}"></span><span><b>${s[1]}</b><span class="dupre-ev">${s[2]}</span></span>`;
w.appendChild(d); while (w.children.length > keep) w.removeChild(w.firstChild);
};
seed.forEach(add);
@@ -4366,11 +4379,19 @@ DUPRE.outputWell = function (host, opts = {}) {
};
/* 23 toast — one-line transient confirmation; click fires the next demo
- message; fire(msg) shows any message with the fade-in */
+ message; fire(msg) shows any message with the fade-in.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: msgs (the demo message pool, cycled by click); text (initial
+ message, default the last pool entry); onChange(n, 'fired n') fires
+ on each click and once at build with a hint.
+ handle: el, fire(msg) — shows any message with the opacity fade-in; the
+ page can drive it with its own text. Click cycles the demo pool.
+ CSS lives in the "toast" block of DUPRE_CSS; no other styles involved. */
DUPRE.toast = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const msgs = opts.msgs || ['link up · 300 Mbps', 'bt paired — WH-1000XM4', 'profile switched', 'joined @Hyatt_WiFi — saved'];
- const t = document.createElement('span'); t.className = 'toastw';
+ const t = document.createElement('span'); t.className = 'dupre-toastw';
t.textContent = opts.text || msgs[msgs.length - 1];
host.appendChild(t);
const fire = msg => {
@@ -4663,28 +4684,42 @@ DUPRE.vfdMarquee = function (host, opts = {}) {
};
/* N23 annunciator — named alarm grid with the raise → MSTR CAUTION → ACK → RESET
- lifecycle; TEST proves the bulbs then restores the board */
+ lifecycle; TEST proves the bulbs then restores the board.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: cells (array of [label, state] where state is 0 ok · 1 warn ·
+ 2 fault; defaults to a six-cell demo). onChange fires on every
+ transition: (count, summary) after a refresh, (index, 'label → name')
+ after a cell change, (null, 'lamp test') during TEST.
+ handle: el, get() (per-cell state array 0/1/2), set(i, st) — st clamps to
+ the 0..2 lens range at build AND at set, so an out-of-range caller
+ can't paint an undefined class. A new warn/fault re-arms the
+ MSTR CAUTION flasher; ACK stops it, RESET clears the board.
+ CSS lives in the "annunciator" block of DUPRE_CSS; the ACK/TEST/RESET buttons
+ are the shared .dupre-key primitive; the board uses the kit-wide dupre-warn
+ modifier and MSTR CAUTION the kit-wide dupre-on. */
DUPRE.annunciator = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const CLS = ['acell', 'acell warn', 'acell fault'];
+ const CLS = ['dupre-acell', 'dupre-acell dupre-warn', 'dupre-acell dupre-fault'];
const NAMES = ['ok', 'warn', 'fault'];
+ const clampSt = st => Math.max(0, Math.min(CLS.length - 1, st | 0));
const cells = opts.cells || [['SYNC', 0], ['LOW BATT', 1], ['LINK', 0], ['NO DNS', 2], ['VPN', 0], ['DISK', 0]];
- const wrap = document.createElement('div'); wrap.className = 'annwrap';
- const grid = document.createElement('div'); grid.className = 'annun'; wrap.appendChild(grid);
- const bar = document.createElement('div'); bar.className = 'annbar';
- const mc = document.createElement('span'); mc.className = 'mc'; mc.textContent = 'MSTR CAUTION'; bar.appendChild(mc);
+ const wrap = document.createElement('div'); wrap.className = 'dupre-annwrap';
+ const grid = document.createElement('div'); grid.className = 'dupre-annun'; wrap.appendChild(grid);
+ const bar = document.createElement('div'); bar.className = 'dupre-annbar';
+ const mc = document.createElement('span'); mc.className = 'dupre-mc'; mc.textContent = 'MSTR CAUTION'; bar.appendChild(mc);
['ACK', 'TEST', 'RESET'].forEach(a => {
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]) => {
- const c = document.createElement('span'); c.className = CLS[st]; c.textContent = label; grid.appendChild(c); return c;
+ const c = document.createElement('span'); c.className = CLS[clampSt(st)]; c.textContent = label; grid.appendChild(c); return c;
});
let acked = false;
- const active = () => grid.querySelectorAll('.warn,.fault').length;
+ const active = () => grid.querySelectorAll('.dupre-warn,.dupre-fault').length;
const refresh = () => {
const n = active();
- mc.classList.toggle('on', n > 0); mc.classList.toggle('fl', n > 0 && !acked);
+ mc.classList.toggle('dupre-on', n > 0); mc.classList.toggle('dupre-fl', n > 0 && !acked);
onChange(n, n === 0 ? 'clear' : n + ' active · ' + (acked ? 'ACK' : 'UNACK'));
};
els.forEach((c, i) => c.addEventListener('click', () => {
@@ -4695,18 +4730,18 @@ DUPRE.annunciator = function (host, opts = {}) {
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(); }
+ else if (a === 'RESET') { els.forEach(c => c.className = 'dupre-acell'); acked = false; refresh(); }
else if (a === 'TEST') {
const prev = els.map(c => c.className);
- els.forEach(c => c.className = 'acell fault');
- mc.classList.add('on', 'fl'); onChange(null, 'lamp test');
+ els.forEach(c => c.className = 'dupre-acell dupre-fault');
+ mc.classList.add('dupre-on', 'dupre-fl'); onChange(null, 'lamp test');
setTimeout(() => { els.forEach((c, i) => c.className = prev[i]); refresh(); }, 1100);
}
}));
refresh();
return {
el: wrap, get: () => els.map(c => Math.max(0, CLS.indexOf(c.className.trim()))),
- set: (i, st) => { els[i].className = CLS[st]; if (st > 0) acked = false; refresh(); }
+ set: (i, st) => { els[i].className = CLS[clampSt(st)]; if (clampSt(st) > 0) acked = false; refresh(); }
};
};
@@ -4749,14 +4784,22 @@ DUPRE.jewels = function (host, opts = {}) {
return { el: wrap, get: () => states.slice(), set };
};
-/* N25 tape counter — odometer wheels; set(total) rolls each wheel to its digit */
+/* N25 tape counter — odometer wheels; set(total) rolls each wheel to its digit.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: wheels (digit count, default 6); redFrom (index from which the
+ high-order wheels wear the red band, default 4); value (initial,
+ default 471300); onChange(value, zero-padded string) fires on set.
+ handle: el, get() (current total), set(total) — rounds fractional input
+ and wraps modulo 10^wheels, then rolls each wheel to its digit.
+ CSS lives in the "tape counter" block of DUPRE_CSS; no other styles involved. */
DUPRE.tapeCounter = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const wheels = opts.wheels || 6, redFrom = opts.redFrom !== undefined ? opts.redFrom : 4;
- const ct = document.createElement('span'); ct.className = 'counter'; host.appendChild(ct);
+ const ct = document.createElement('span'); ct.className = 'dupre-counter'; host.appendChild(ct);
for (let idx = 0; idx < wheels; idx++) {
- const w = document.createElement('span'); w.className = 'cwheel' + (idx >= redFrom ? ' redw' : '');
- const col = document.createElement('span'); col.className = 'col';
+ const w = document.createElement('span'); w.className = 'dupre-cwheel' + (idx >= redFrom ? ' dupre-redw' : '');
+ const col = document.createElement('span'); col.className = 'dupre-col';
for (let n = -1; n <= 10; n++) { const sp = document.createElement('span'); sp.textContent = ((n + 10) % 10); col.appendChild(sp); }
w.appendChild(col); ct.appendChild(w);
}
@@ -4765,7 +4808,7 @@ DUPRE.tapeCounter = function (host, opts = {}) {
const set = x => {
v = ((Math.round(x) % max) + max) % max;
const s = String(v).padStart(wheels, '0');
- [...ct.children].forEach((w, i) => { w.querySelector('.col').style.top = (-(+s[i] + 1) * 34) + 'px'; });
+ [...ct.children].forEach((w, i) => { w.querySelector('.dupre-col').style.top = (-(+s[i] + 1) * 34) + 'px'; });
onChange(v, s);
};
set(opts.value !== undefined ? opts.value : 471300);
@@ -4799,7 +4842,18 @@ DUPRE.analogClock = function (host, opts = {}) {
};
/* N27 frequency-dial scale — printed log axis, marks crowd low and spread high;
- drag the pointer to tune */
+ drag the pointer to tune.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: width (px, default 184); marks (labeled major marks, log-spaced,
+ default 0.5..20); unit (band label, default 'MHz'); value (initial
+ pointer percent 0..100, default 45); onChange(freq, 'nn unit') fires
+ on set and drag.
+ handle: el, get() (pointer percent 0..100), set(pct) — clamps to 0..100
+ and maps to a frequency along the log axis. dragX drives it.
+ CSS lives in the "frequency-dial scale" block of DUPRE_CSS; the ticks and
+ marks reuse the shared scoped dupre-tick/dupre-mk names (like the tuner and
+ dualknob), and the unit label is dupre-fs-band (dupre-band is the EQ's). */
DUPRE.freqScale = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const W = opts.width || 184;
@@ -4807,16 +4861,16 @@ DUPRE.freqScale = function (host, opts = {}) {
const unit = opts.unit || 'MHz';
const lo = Math.log10(marks[0]), hi = Math.log10(marks[marks.length - 1]);
const px = m => 6 + ((Math.log10(m) - lo) / (hi - lo)) * (W - 12);
- const fs = document.createElement('span'); fs.className = 'freqscale';
- const band = document.createElement('span'); band.className = 'band'; band.textContent = unit; fs.appendChild(band);
+ const fs = document.createElement('span'); fs.className = 'dupre-freqscale';
+ const band = document.createElement('span'); band.className = 'dupre-fs-band'; band.textContent = unit; fs.appendChild(band);
marks.forEach(m => {
- const tk = document.createElement('span'); tk.className = 'tick'; tk.style.left = px(m) + 'px'; tk.style.height = '11px'; fs.appendChild(tk);
- const mk = document.createElement('span'); mk.className = 'mk'; mk.style.left = px(m) + 'px'; mk.textContent = m; fs.appendChild(mk);
+ const tk = document.createElement('span'); tk.className = 'dupre-tick'; tk.style.left = px(m) + 'px'; tk.style.height = '11px'; fs.appendChild(tk);
+ const mk = document.createElement('span'); mk.className = 'dupre-mk'; mk.style.left = px(m) + 'px'; mk.textContent = m; fs.appendChild(mk);
});
for (let m = marks[0]; m <= marks[marks.length - 1]; m += (m < 2 ? 0.25 : m < 10 ? 1 : 2)) {
- const tk = document.createElement('span'); tk.className = 'tick'; tk.style.left = px(m) + 'px'; tk.style.height = '6px'; tk.style.opacity = '.5'; fs.appendChild(tk);
+ const tk = document.createElement('span'); tk.className = 'dupre-tick'; tk.style.left = px(m) + 'px'; tk.style.height = '6px'; tk.style.opacity = '.5'; fs.appendChild(tk);
}
- const ptr = document.createElement('span'); ptr.className = 'fptr'; fs.appendChild(ptr);
+ const ptr = document.createElement('span'); ptr.className = 'dupre-fptr'; fs.appendChild(ptr);
host.appendChild(fs);
let pct;
const set = p => {
@@ -4831,15 +4885,25 @@ DUPRE.freqScale = function (host, opts = {}) {
};
/* N28 patch bay — jack grid with SVG cables; click one jack then another to
- patch or unpatch the pair; redraws itself on window resize */
+ patch or unpatch the pair; redraws itself on window resize.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: rows (default 2), cols (default 4); conns (initial cables as
+ 'a-b' jack-index strings, default ['0-5', '2-7']); onChange(conns
+ array, summary) fires on every patch/unpatch and once at build.
+ handle: el, get() (conns copy), set(conns) — replaces the patch set and
+ redraws the cables. Click one jack then another to patch or
+ unpatch; the SVG cables re-solve on window resize.
+ CSS lives in the "patch-bay" block of DUPRE_CSS; a live jack wears the
+ kit-wide dupre-hot modifier. */
DUPRE.patchBay = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const rows = opts.rows || 2, cols = opts.cols || 4;
- const patch = document.createElement('div'); patch.className = 'patch';
+ const patch = document.createElement('div'); patch.className = 'dupre-patch';
const jacks = [];
for (let r = 0; r < rows; r++) {
- const rowEl = document.createElement('div'); rowEl.className = 'row';
- for (let c = 0; c < cols; c++) { const j = document.createElement('span'); j.className = 'jack'; j.dataset.id = (r * cols + c); rowEl.appendChild(j); jacks.push(j); }
+ const rowEl = document.createElement('div'); rowEl.className = 'dupre-row';
+ for (let c = 0; c < cols; c++) { const j = document.createElement('span'); j.className = 'dupre-jack'; j.dataset.id = (r * cols + c); rowEl.appendChild(j); jacks.push(j); }
patch.appendChild(rowEl);
}
const svg = document.createElementNS(SVGNS, 'svg'); patch.appendChild(svg);
@@ -4849,7 +4913,7 @@ DUPRE.patchBay = function (host, opts = {}) {
const draw = () => {
while (svg.firstChild) svg.removeChild(svg.firstChild);
const pr = patch.getBoundingClientRect();
- jacks.forEach(j => j.classList.remove('hot'));
+ jacks.forEach(j => j.classList.remove('dupre-hot'));
conns.forEach(pair => {
const [a, b] = pair.split('-').map(Number);
const ra = jacks[a].getBoundingClientRect(), rb = jacks[b].getBoundingClientRect();
@@ -4857,17 +4921,17 @@ DUPRE.patchBay = function (host, opts = {}) {
const x2 = rb.left - pr.left + rb.width / 2, y2 = rb.top - pr.top + rb.height / 2;
const p = document.createElementNS(SVGNS, 'path');
const my = Math.max(y1, y2) + 16; p.setAttribute('d', `M${x1} ${y1} C${x1} ${my} ${x2} ${my} ${x2} ${y2}`); svg.appendChild(p);
- jacks[a].classList.add('hot'); jacks[b].classList.add('hot');
+ jacks[a].classList.add('dupre-hot'); jacks[b].classList.add('dupre-hot');
});
onChange(conns.slice(), conns.length ? conns.map(c => { const [a, b] = c.split('-'); return a + '↔' + b; }).join(' ') : 'no cables');
};
jacks.forEach(j => {
j.addEventListener('click', () => {
const id = +j.dataset.id;
- if (pending == null) { pending = id; j.classList.add('sel'); return; }
- if (pending === id) { pending = null; j.classList.remove('sel'); return; }
+ if (pending == null) { pending = id; j.classList.add('dupre-sel'); return; }
+ if (pending === id) { pending = null; j.classList.remove('dupre-sel'); return; }
const k = key(pending, id); const i = conns.indexOf(k); if (i >= 0) conns.splice(i, 1); else conns.push(k);
- jacks[pending].classList.remove('sel'); pending = null; draw();
+ jacks[pending].classList.remove('dupre-sel'); pending = null; draw();
});
});
draw(); window.addEventListener('resize', draw);
@@ -4875,7 +4939,20 @@ DUPRE.patchBay = function (host, opts = {}) {
};
/* R10 data matrix readout — a page of labeled amber fields; click cycles pages;
- a page marked live re-renders on the builder's own 1s clock (reduced-motion gated) */
+ a page marked live re-renders on the builder's own 1s clock (reduced-motion gated).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: pages (array of [name, rowsFn, live?] tuples, default SYS/NET/TIME;
+ rowsFn() returns the three text rows, and a truthy third element
+ marks a page that re-renders on the 1s clock); page (initial index,
+ default 0); onChange(index, 'page NAME') fires on set.
+ handle: el, get() (page index), set(i) — wraps modulo the page count.
+ Click cycles pages; a live page re-renders on the builder's own
+ 1s clock (reduced-motion gated).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block of
+ DUPRE_CSS; the glow filter registers in the shared defs plate, but the
+ screen-family background gradient lives in this instrument's local defs (it
+ reads --scr-* vars from its own subtree). */
DUPRE.dataMatrix = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const pages = opts.pages || [
@@ -4927,7 +5004,16 @@ DUPRE.dataMatrix = function (host, opts = {}) {
};
/* R11 warning flag window — the striped mechanical flag slides into the window
- when the condition trips; click trips and clears it */
+ when the condition trips; click trips and clears it.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label (window legend, default 'VIB'); on (initial tripped state,
+ default false); onChange(on, 'FLAG'|'clear') fires on set.
+ handle: el, get() (tripped bool), set(v) — slides the striped flag into
+ the window when truthy, retracts it when not. Click toggles.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block of
+ DUPRE_CSS; the barber-stripe pattern registers in the shared defs plate, the
+ window clip in this instrument's local defs. */
DUPRE.warningFlag = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg press', 120, 64);
@@ -4956,7 +5042,16 @@ DUPRE.warningFlag = function (host, opts = {}) {
};
/* R25 fourteen-segment display — the starburst alphanumeric that spells words;
- click cycles the word */
+ click cycles the word.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: words (the four-letter word pool cycled by click, default
+ ZOOM/ECHO/TAPE/…; letters outside the segment MAP render blank);
+ index (initial, default 0); onChange(index, word) fires on set.
+ handle: el, get() (word index), set(i) — wraps modulo the pool and lights
+ the fourteen-segment cells to spell the word. Click cycles.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block of
+ DUPRE_CSS; the segments are filled inline (lit vs --sevoff), no shared defs. */
DUPRE.seg14 = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const words = opts.words || ['ZOOM', 'ECHO', 'TAPE', 'MOOD', 'HALL', 'COMP'];
@@ -5014,7 +5109,16 @@ DUPRE.seg14 = function (host, opts = {}) {
};
/* R26 response graph — log-frequency axes with a draggable amber peak; 2D drag
- places the peak in both axes at once */
+ places the peak in both axes at once.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: fc (initial center frequency Hz, default 1200); gain (initial dB,
+ default 6.5); onChange([fc, gain], 'f · ±g dB') fires on set and drag.
+ handle: el, get() ([fc, gain]), set(f, g) — fc clamps 32..16000 Hz, gain
+ clamps ±12 dB; draws the bell curve and moves the peak handle. A
+ 2D drag on the plot places the peak in both axes at once.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block of
+ DUPRE_CSS; the amber glow filter registers in the shared defs plate. */
DUPRE.responseGraph = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 190, 110);
@@ -5620,13 +5724,13 @@ const DUPRE_CSS = `
.dupre-wave svg{width:100%;height:100%;display:block}
/* toast */
-.toastw{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:5px 10px;cursor:pointer}
+.dupre-toastw{font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px;padding:5px 10px;cursor:pointer}
/* output well (log step) */
-.owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px;cursor:pointer}
-.ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0}
-.ostep .dupre-lamp{margin-top:3px;width:7px;height:7px}
-.ostep b{color:var(--cream);font-weight:700}.ostep .ev{color:var(--steel);display:block;font-size:10.5px}
+.dupre-owell{width:200px;background:var(--well);border:1px solid var(--wash);border-radius:8px;padding:7px 9px;font-size:11px;cursor:pointer}
+.dupre-ostep{display:flex;gap:7px;align-items:flex-start;padding:2px 0}
+.dupre-ostep .dupre-lamp{margin-top:3px;width:7px;height:7px}
+.dupre-ostep b{color:var(--cream);font-weight:700}.dupre-ostep .dupre-ev{color:var(--steel);display:block;font-size:10.5px}
/* rotary selector */
.dupre-rotsel{position:relative;width:118px;height:74px}
@@ -5916,19 +6020,19 @@ const DUPRE_CSS = `
background-image:radial-gradient(rgba(0,0,0,.6) 40%,transparent 41%);background-size:3px 3px}
/* annunciator */
-.annwrap{display:flex;flex-direction:column;gap:6px}
-.annbar{display:flex;gap:5px;align-items:center}
-.mc{font-size:7px;letter-spacing:.08em;padding:4px 8px;border-radius:3px;background:#141210;color:#5c574c;
+.dupre-annwrap{display:flex;flex-direction:column;gap:6px}
+.dupre-annbar{display:flex;gap:5px;align-items:center}
+.dupre-mc{font-size:7px;letter-spacing:.08em;padding:4px 8px;border-radius:3px;background:#141210;color:#5c574c;
border:1px solid #26221c;text-align:center}
-.mc.on{background:linear-gradient(180deg,#d98a6f,var(--fail));color:var(--cream);font-weight:700;
+.dupre-mc.dupre-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 .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;
+.dupre-mc.dupre-on.dupre-fl{animation:pulse var(--pulse-rate) ease-in-out infinite}
+.dupre-annbar .dupre-key{padding:3px 7px;font-size:7.5px;border-radius:4px}
+.dupre-annun{display:grid;grid-template-columns:repeat(3,1fr);gap:3px}
+.dupre-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}
-.acell.warn{color:var(--panel);background:linear-gradient(180deg,var(--amber-warn),var(--gold));font-weight:700;box-shadow:0 0 8px rgba(var(--glow-lo),.4)}
-.acell.fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite}
+.dupre-acell.dupre-warn{color:var(--panel);background:linear-gradient(180deg,var(--amber-warn),var(--gold));font-weight:700;box-shadow:0 0 8px rgba(var(--glow-lo),.4)}
+.dupre-acell.dupre-fault{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));font-weight:700;box-shadow:0 0 8px rgba(203,107,77,.4);animation:pulse var(--pulse-rate) ease-in-out infinite}
/* jewel */
.dupre-jewel{width:24px;height:24px;border-radius:50%;position:relative;cursor:pointer;
@@ -5940,17 +6044,17 @@ const DUPRE_CSS = `
.dupre-jewel.dupre-dim{background:radial-gradient(circle at 36% 30%,rgba(120,120,120,.3),#241f1b 55%,#0e0c0a);box-shadow:inset 0 -2px 3px rgba(0,0,0,.5)}
/* tape counter */
-.counter{display:inline-flex;gap:2px;padding:4px;background:#0c0b0a;border:1px solid #2c261d;border-radius:4px;
+.dupre-counter{display:inline-flex;gap:2px;padding:4px;background:#0c0b0a;border:1px solid #2c261d;border-radius:4px;
box-shadow:inset 0 1px 3px rgba(0,0,0,.6)}
-.cwheel{width:18px;height:34px;border-radius:2px;position:relative;overflow:hidden;
+.dupre-cwheel{width:18px;height:34px;border-radius:2px;position:relative;overflow:hidden;
background:linear-gradient(180deg,#efe9d6,#c7c0ac);box-shadow:inset 0 0 3px rgba(0,0,0,.3)}
-.cwheel .col{position:absolute;left:0;right:0;text-align:center;color:#1a1613;font-size:18px;font-weight:700;line-height:34px;
+.dupre-cwheel .dupre-col{position:absolute;left:0;right:0;text-align:center;color:#1a1613;font-size:18px;font-weight:700;line-height:34px;
transition:top .35s cubic-bezier(.4,1.4,.5,1)}
-.cwheel .col span{display:block;height:34px}
-.cwheel::before,.cwheel::after{content:"";position:absolute;left:0;right:0;height:9px;z-index:2;pointer-events:none}
-.cwheel::before{top:0;background:linear-gradient(180deg,rgba(0,0,0,.4),transparent)}
-.cwheel::after{bottom:0;background:linear-gradient(0deg,rgba(0,0,0,.4),transparent)}
-.counter .redw{background:linear-gradient(180deg,#e7b46a,#cf9440)}
+.dupre-cwheel .dupre-col span{display:block;height:34px}
+.dupre-cwheel::before,.dupre-cwheel::after{content:"";position:absolute;left:0;right:0;height:9px;z-index:2;pointer-events:none}
+.dupre-cwheel::before{top:0;background:linear-gradient(180deg,rgba(0,0,0,.4),transparent)}
+.dupre-cwheel::after{bottom:0;background:linear-gradient(0deg,rgba(0,0,0,.4),transparent)}
+.dupre-counter .dupre-redw{background:linear-gradient(180deg,#e7b46a,#cf9440)}
/* analog clock */
.dupre-clock{width:78px;height:78px;border-radius:50%;position:relative;
@@ -5965,25 +6069,25 @@ const DUPRE_CSS = `
.dupre-clock .dupre-pin{position:absolute;left:50%;top:50%;width:6px;height:6px;margin:-3px;border-radius:50%;background:var(--gold)}
/* frequency-dial scale */
-.freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none;
+.dupre-freqscale{width:184px;height:44px;position:relative;border-radius:5px;overflow:hidden;cursor:pointer;touch-action:none;
background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c;
box-shadow:inset 0 0 16px rgba(var(--glow-lo),.1)}
-.freqscale .tick{position:absolute;top:5px;width:1px;background:var(--steel);transform:translateX(-50%)}
-.freqscale .mk{position:absolute;bottom:6px;transform:translateX(-50%);color:var(--steel);font-size:9px}
-.freqscale .band{position:absolute;bottom:2px;left:6px;color:var(--gold);font-size:8px;letter-spacing:.18em}
-.freqscale .fptr{position:absolute;top:3px;bottom:14px;width:2px;margin-left:-1px;border-radius:1px;
+.dupre-freqscale .dupre-tick{position:absolute;top:5px;width:1px;background:var(--steel);transform:translateX(-50%)}
+.dupre-freqscale .dupre-mk{position:absolute;bottom:6px;transform:translateX(-50%);color:var(--steel);font-size:9px}
+.dupre-freqscale .dupre-fs-band{position:absolute;bottom:2px;left:6px;color:var(--gold);font-size:8px;letter-spacing:.18em}
+.dupre-freqscale .dupre-fptr{position:absolute;top:3px;bottom:14px;width:2px;margin-left:-1px;border-radius:1px;
background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85)}
/* patch-bay */
-.patch{padding:7px 9px;background:#141210;border:1px solid #2c261d;border-radius:5px;position:relative}
-.patch .row{display:flex;gap:9px}
-.patch .row+.row{margin-top:9px}
-.patch .jack{width:13px;height:13px;border-radius:50%;cursor:pointer;background:radial-gradient(circle at 40% 35%,#3a352c,#0a0908 70%);
+.dupre-patch{padding:7px 9px;background:#141210;border:1px solid #2c261d;border-radius:5px;position:relative}
+.dupre-patch .dupre-row{display:flex;gap:9px}
+.dupre-patch .dupre-row+.dupre-row{margin-top:9px}
+.dupre-patch .dupre-jack{width:13px;height:13px;border-radius:50%;cursor:pointer;background:radial-gradient(circle at 40% 35%,#3a352c,#0a0908 70%);
border:1px solid #4a443a;box-shadow:inset 0 1px 2px rgba(0,0,0,.8)}
-.patch .jack.hot{background:radial-gradient(circle at 40% 35%,var(--amber-edge),#1a1408 70%)}
-.patch .jack.sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(var(--glow-hi),.7)}
-.patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%}
-.patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round}
+.dupre-patch .dupre-jack.dupre-hot{background:radial-gradient(circle at 40% 35%,var(--amber-edge),#1a1408 70%)}
+.dupre-patch .dupre-jack.dupre-sel{border-color:var(--gold-hi);box-shadow:0 0 6px 1px rgba(var(--glow-hi),.7)}
+.dupre-patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%}
+.dupre-patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round}
/* ===== reference-batch (R) instruments — SVG-first, after period hardware ===== */
.rsvg{display:block}
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 81ca016..3db09a5 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -1486,6 +1486,63 @@ try {
humid: '#c23a28', aiT: 'rotate(-30.0,65,65) translate(0,-16.0)',
crtMoves: true, crH: 0, crD: '' }), b9dom);
+ // batch-10 domain gate: the annunciator documents a 0..2 state per cell, so
+ // an out-of-range initial state or set() must clamp rather than paint an
+ // undefined class (the mcVu build-enforcement precedent). Without the clamp,
+ // CLS[9] is undefined and get() reads back 0 — this fails RED on that.
+ const clamps10 = await evl(`(() => {
+ const an = DUPRE.annunciator(document.createElement('div'), { cells: [['A', 9], ['B', -3], ['C', 1]] });
+ const anB = an.get();
+ an.set(0, 9); an.set(1, -5);
+ const anSet = an.get();
+ return JSON.stringify({ anB, anSet });
+ })()`);
+ ok('batch-10 domain gate: annunciator state clamps to 0..2 at build and set',
+ clamps10 === JSON.stringify({ anB: [2, 0, 1], anSet: [2, 0, 1] }), clamps10);
+
+ // batch-10 state through the renamed DOM (catches rename stragglers): the six
+ // extracted DOM builders each expose their handle and paint the dupre- classes
+ // — a JS/CSS name mismatch would render unstyled without throwing, so assert
+ // the class names directly. patchBay lights hot jacks and freqScale carries
+ // the compound dupre-fs-band unit label (dupre-band is the EQ's).
+ const b10dom = await evl(`(() => {
+ const ow = DUPRE.outputWell(document.createElement('div'));
+ const owRoot = ow.el.className, owSeed = ow.el.querySelectorAll('.dupre-ostep').length;
+ ow.push(['red', 'Err', 'boom']);
+ const owN = ow.el.querySelectorAll('.dupre-ostep').length;
+ const owEv = ow.el.querySelector('.dupre-ostep:last-child .dupre-ev').textContent;
+ const owRed = ow.el.querySelector('.dupre-ostep:last-child .dupre-lamp').classList.contains('dupre-red');
+ const to = DUPRE.toast(document.createElement('div'));
+ const toRoot = to.el.className; to.fire('ping'); const toTxt = to.el.textContent;
+ const tc = DUPRE.tapeCounter(document.createElement('div'));
+ const tcRoot = tc.el.className;
+ const tcWheels = tc.el.querySelectorAll('.dupre-cwheel').length;
+ const tcRed = tc.el.querySelectorAll('.dupre-redw').length;
+ tc.set(120); const tcV = tc.get();
+ const fq = DUPRE.freqScale(document.createElement('div'));
+ const fqBand = fq.el.querySelector('.dupre-fs-band').textContent;
+ const fqTicks = fq.el.querySelectorAll('.dupre-tick').length > 0;
+ const fqPtr = !!fq.el.querySelector('.dupre-fptr');
+ fq.set(50); const fqV = fq.get();
+ const pb = DUPRE.patchBay(document.createElement('div'));
+ const pbJacks = pb.el.querySelectorAll('.dupre-jack').length;
+ const pbHot = pb.el.querySelectorAll('.dupre-jack.dupre-hot').length;
+ const pbConns = pb.get();
+ const an = DUPRE.annunciator(document.createElement('div'));
+ const anCells = an.el.querySelectorAll('.dupre-acell').length;
+ const anWarn = an.el.querySelectorAll('.dupre-warn').length;
+ const anFault = an.el.querySelectorAll('.dupre-fault').length;
+ const anMcOn = an.el.querySelector('.dupre-mc').classList.contains('dupre-on');
+ return JSON.stringify({ owRoot, owSeed, owN, owEv, owRed, toRoot, toTxt,
+ tcRoot, tcWheels, tcRed, tcV, fqBand, fqTicks, fqPtr, fqV,
+ pbJacks, pbHot, pbConns, anCells, anWarn, anFault, anMcOn });
+ })()`);
+ ok('batch-10 renamed DOM tracks state: well/toast/counter/freqscale/patchbay/annunciator handles + dupre- classes',
+ b10dom === JSON.stringify({ owRoot: 'dupre-owell', owSeed: 2, owN: 3, owEv: 'boom', owRed: true,
+ toRoot: 'dupre-toastw', toTxt: 'ping', tcRoot: 'dupre-counter', tcWheels: 6, tcRed: 2, tcV: 120,
+ fqBand: 'MHz', fqTicks: true, fqPtr: true, fqV: 50, pbJacks: 8, pbHot: 4, pbConns: ['0-5', '2-7'],
+ anCells: 6, anWarn: 1, anFault: 1, anMcOn: true }), b10dom);
+
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);