aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 18:05:55 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 18:05:55 -0500
commit055aceb0ea41c394f32f49baa95b8d072f5f0fd0 (patch)
tree38be8893c8f306baff871573907021b153156ff3
parent2ec6fc8cdb34968f752d018e3a41285d35cf66d2 (diff)
downloadarchsetup-055aceb0ea41c394f32f49baa95b8d072f5f0fd0.tar.gz
archsetup-055aceb0ea41c394f32f49baa95b8d072f5f0fd0.zip
feat(gallery): bring the CRT-through-engraved ten to the extraction bar
roundCrt, attitudeIndicator, headingBug, chartRecorder, verticalTape, twinNeedle, comfortMeter, badges, tabularReadout, and engravedLabel each get the contract comment. The three DOM builders take dupre- prefixes across their markup and CSS blocks: badges (dupre-badge with the kit-wide dupre-red plus a new dupre-ghost), tabularReadout (dupre-readout and dupre-unit), and engravedLabel (dupre-engrave and dupre-cnt). The rename surfaced a dead rule: .readout .u selects a descendant, but the unit caption is a sibling, so its style never applied. The caption now styles through a standalone .dupre-unit rule. The builders now enforce their documented domains at build, not only in set(): twinNeedle and comfortMeter clamp their initial opts through set(), headingBug normalizes opts.value to 0..360, and badges coerces junk variants through one norm() shared by build and set(). Two probe checks cover the gates and drive the renamed DOM. Extraction audit: 94/111 contract comments.
-rw-r--r--docs/prototypes/widgets.js164
-rw-r--r--tests/gallery-probes/probe.mjs84
2 files changed, 213 insertions, 35 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index dc37bd4..bd5c79f 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -3746,7 +3746,17 @@ DUPRE.edgeMeter = function (host, opts = {}) {
};
/* R17 round CRT scope — pale phosphor face; the trace is the instrument's own
- animation, so it lives here with its reduced-motion gate */
+ animation, so it lives here with its reduced-motion gate.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(amp, 'Vpp N') fires on every animation tick, including
+ the initial paint — the demo waveform is instrument-owned.
+ handle: el, tick() — one animation step (the reduced-motion path paints
+ once and never ticks again on its own).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate, except the
+ face gradient, which reads screen-family vars from this instrument's
+ subtree and so lives in a local defs. */
DUPRE.roundCrt = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 110, 104);
@@ -3797,7 +3807,16 @@ DUPRE.roundCrt = function (host, opts = {}) {
return { el: s, tick };
};
-/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */
+/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: bank (initial degrees, -60..60, default 0); pitch (initial
+ degrees, -20..20, default 0); onChange({bank, pitch}, 'BANK …
+ · PITCH …') fires on every set, including the initial paint.
+ handle: el, get() ({bank, pitch}), set(bank, pitch) — both clamp to
+ their ranges; the stage itself is a 2D drag surface.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.attitudeIndicator = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3853,7 +3872,16 @@ DUPRE.attitudeIndicator = function (host, opts = {}) {
};
/* R44 heading bug + servo needle — drag parks the command; the needle chases
- with honest servo lag (instrument-owned animation, reduced-motion gated) */
+ with honest servo lag (instrument-owned animation, reduced-motion gated).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial commanded heading in degrees, default 90 —
+ normalized to 0..360); onChange({cmd, act}, 'CMD … · ACT …')
+ fires on every set and on every servo step.
+ handle: el, get() ({cmd, act}), set(v) — v normalizes to 0..360;
+ dragging the stage moves the command bug, the needle chases.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.headingBug = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3884,7 +3912,7 @@ DUPRE.headingBug = function (host, opts = {}) {
fill: '#bfc4d0', stroke: '#4a4e58', 'stroke-width': .6
});
svgEl(s, 'circle', { cx, cy, r: 4.5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let cmd = opts.value ?? 90, act = cmd;
+ let cmd = ((opts.value ?? 90) % 360 + 360) % 360, act = cmd;
const draw = () => {
bug.setAttribute('transform', `rotate(${cmd.toFixed(1)},${cx},${cy})`);
needle.setAttribute('transform', `rotate(${act.toFixed(1)},${cx},${cy})`);
@@ -3904,7 +3932,15 @@ DUPRE.headingBug = function (host, opts = {}) {
};
/* R53 circular chart recorder — a day per revolution; the pen draws the cycle
- (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper */
+ (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(hour, 'HH:MM · N%') fires on every pen step — the demo
+ day-cycle signal is instrument-owned.
+ handle: el, get() (current hour, 0..24), reset() — fresh paper, pen to
+ 00:00; clicking the stage does the same.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.chartRecorder = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg press', 130, 130);
@@ -3954,7 +3990,16 @@ DUPRE.chartRecorder = function (host, opts = {}) {
return { el: s, get: () => hour, reset };
};
-/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive */
+/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial units, 5..35 on the printed RPM x100 scale,
+ default 24); onChange(value, 'RPM N') fires on every set,
+ including the initial paint.
+ handle: el, get() (units), set(v) — v clamps 5..35; the stage drags
+ vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.verticalTape = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 70, 130);
@@ -3996,7 +4041,16 @@ DUPRE.verticalTape = function (host, opts = {}) {
return { el: s, get: () => v, set };
};
-/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles */
+/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: fuel (initial, 0..4, default 2.4); oil (initial, 0..4, default
+ 3.1); onChange([fuel, oil], 'FUEL n.n · OIL n.n') fires on every
+ set, including the initial paint.
+ handle: el, get() ([fuel, oil]), set(fuel, oil) — both clamp 0..4;
+ each half of the stage drags its own needle vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.twinNeedle = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 120, 110);
@@ -4045,7 +4099,7 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
const nF = needle('#e0523a'), nO = needle('#bfc4d0');
svgEl(s, 'circle', { cx, cy, r: 5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let vF = opts.fuel ?? 2.4, vO = opts.oil ?? 3.1;
+ let vF, vO;
const draw = () => {
nF.setAttribute('transform', `rotate(${angL(vF).toFixed(1)},${cx},${cy})`);
nO.setAttribute('transform', `rotate(${angR(vO).toFixed(1)},${cx},${cy})`);
@@ -4060,11 +4114,21 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
half(0, () => vF, v => vF = v);
half(60, () => vO, v => vO = v);
- draw();
+ set(opts.fuel ?? 2.4, opts.oil ?? 3.1);
return { el: s, get: () => [vF, vO], set };
};
-/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts */
+/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: temp (initial °F, 40..100, default 72); humidity (initial %,
+ 0..100, default 45); onChange([temp, humidity], 'NF · N% RH ·
+ VERDICT') fires on every set, including the initial paint.
+ handle: el, get() ([temp, humidity]), set(temp, humidity) — both clamp
+ to their scales; the left half of the stage drags temp, the
+ right half humidity.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.comfortMeter = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 122);
@@ -4137,7 +4201,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
const nT = needle(), nH = needle();
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]})`);
nH.setAttribute('transform', `translate(${PH[0]},${PH[1]})`);
- let vT = opts.temp ?? 72, vH = opts.humidity ?? 45;
+ let vT, vH;
const verdict = () => vT > 78 ? 'warm' : vT < 62 ? 'cold' : vH > 60 ? 'humid' : vH < 30 ? 'dry' : 'right';
const draw = () => {
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`);
@@ -4154,7 +4218,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
};
half(0, () => vT, v => vT = v, 40, 100);
half(65, () => vH, v => vH = v, 0, 100);
- draw();
+ set(opts.temp ?? 72, opts.humidity ?? 45);
return { el: s, get: () => [vT, vH], set };
};
@@ -4192,15 +4256,25 @@ DUPRE.statusLamps = function (host, opts = {}) {
return { el: wrap, get: () => states.slice(), set };
};
-/* 19 badges — labelled flags; click a badge to cycle its variant */
+/* 19 badges — labelled flags; click a badge to cycle its variant.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: items (array of [label, variant] pairs, variant 0 gold · 1 red ·
+ 2 ghost, default a three-badge demo set); onChange(variants array
+ copy, label) fires per change and once at build with a count
+ summary.
+ handle: el, get() (variants copy), set(i, v) — v wraps modulo 3.
+ CSS lives in the .dupre-badge block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.badges = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const CLS = ['badge', 'badge red', 'badge ghost'];
+ const CLS = ['dupre-badge', 'dupre-badge dupre-red', 'dupre-badge dupre-ghost'];
+ const norm = v => (((v | 0) % CLS.length) + CLS.length) % CLS.length;
const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]];
- const vars = items.map(it => it[1]);
+ const vars = items.map(it => norm(it[1]));
const wrap = document.createElement('span');
- const els = items.map(([txt, v], i) => {
- const b = document.createElement('span'); b.className = CLS[v]; b.textContent = txt; b.style.cursor = 'pointer';
+ const els = items.map(([txt], i) => {
+ const b = document.createElement('span'); b.className = CLS[vars[i]]; b.textContent = txt; b.style.cursor = 'pointer';
b.addEventListener('click', () => set(i, vars[i] + 1));
wrap.appendChild(b);
if (i < items.length - 1) wrap.appendChild(document.createTextNode(' '));
@@ -4208,7 +4282,7 @@ DUPRE.badges = function (host, opts = {}) {
});
host.appendChild(wrap);
function set(i, v) {
- vars[i] = ((v % CLS.length) + CLS.length) % CLS.length;
+ vars[i] = norm(v);
els[i].className = CLS[vars[i]];
onChange(vars.slice(), items[i][0]);
}
@@ -4217,13 +4291,24 @@ DUPRE.badges = function (host, opts = {}) {
};
/* 20 tabular readout — mm:ss countdown; builder owns the state, the page
- drives tick() on its own clock (tick contract); click pauses/resumes */
+ drives tick() on its own clock (tick contract); click pauses/resumes.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: secs (initial seconds, wraps modulo 3600, default 24:10); run
+ (initial running state, default true); unit (caption under the
+ digits, default 'timer'); onChange(secs, 'running'/'paused')
+ fires on every set, including the initial paint.
+ handle: el, get() (secs), set(v) — v wraps modulo 3600; tick() — one
+ second down when running (the page's clock drives it); clicking
+ the digits pauses/resumes.
+ CSS lives in the .dupre-readout block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.tabularReadout = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const wrap = document.createElement('div'); wrap.style.textAlign = 'center';
- wrap.innerHTML = '<div class="readout"></div><span class="u"></span>';
- const out = wrap.querySelector('.readout');
- wrap.querySelector('.u').textContent = opts.unit || 'timer';
+ wrap.innerHTML = '<div class="dupre-readout"></div><span class="dupre-unit"></span>';
+ const out = wrap.querySelector('.dupre-readout');
+ wrap.querySelector('.dupre-unit').textContent = opts.unit || 'timer';
host.appendChild(wrap);
let secs, run = opts.run !== undefined ? !!opts.run : true;
const draw = () => {
@@ -4236,12 +4321,21 @@ DUPRE.tabularReadout = function (host, opts = {}) {
return { el: wrap, get: () => secs, set, tick: () => { if (run) set(secs - 1); } };
};
-/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9 */
+/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label (caption text, default 'outputs'); count (initial count,
+ default 3); onChange(count, 'count N') fires on every set,
+ including the initial paint.
+ handle: el, get() (count), set(v); clicking the label cycles the count
+ 1..9.
+ CSS lives in the .dupre-engrave block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.engravedLabel = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const e = document.createElement('span'); e.className = 'engrave';
+ const e = document.createElement('span'); e.className = 'dupre-engrave';
e.append(opts.label || 'outputs');
- const c = document.createElement('span'); c.className = 'cnt'; e.appendChild(c);
+ const c = document.createElement('span'); c.className = 'dupre-cnt'; e.appendChild(c);
host.appendChild(e);
let n;
const set = v => { n = v; c.textContent = '· ' + n; onChange(n, 'count ' + n); };
@@ -5420,9 +5514,9 @@ const DUPRE_CSS = `
.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)}
+.dupre-badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
+.dupre-badge.dupre-red{background:var(--fail);color:var(--cream)}
+.dupre-badge.dupre-ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
/* fader */
.dupre-fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
@@ -5489,9 +5583,9 @@ const DUPRE_CSS = `
.dupre-ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums}
/* tabular readout */
-.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
-.readout small{color:var(--dim);font-size:12px;font-weight:400}
-.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
+.dupre-readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
+.dupre-readout small{color:var(--dim);font-size:12px;font-weight:400}
+.dupre-unit{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
/* sparkline */
.dupre-spark{width:170px;height:44px}
@@ -5515,11 +5609,11 @@ const DUPRE_CSS = `
.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;
+.dupre-engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
display:flex;align-items:center;gap:9px}
-.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
-.engrave::before{max-width:10px}
-.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
+.dupre-engrave::before,.dupre-engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.dupre-engrave::before{max-width:10px}
+.dupre-engrave .dupre-cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
/* waveform strip */
.dupre-wave{width:170px;height:38px}
diff --git a/tests/gallery-probes/probe.mjs b/tests/gallery-probes/probe.mjs
index 0539da0..81ca016 100644
--- a/tests/gallery-probes/probe.mjs
+++ b/tests/gallery-probes/probe.mjs
@@ -1402,6 +1402,90 @@ try {
fwdT: 'rotate(-42deg)', rflT: 'rotate(42deg)', thH: '75%', bdT: 'rotate(0deg)',
cmT: 'rotate(0deg)', penT: '3px' }), b8dom);
+ // batch-9 domain gates on detached hosts: the flight instruments clamp (or
+ // normalize) both set() and their build opts, badges coerces junk variants,
+ // the readout wraps its clock.
+ const clamps9 = await evl(`(() => {
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'), { bank: -999, pitch: 5 });
+ const aiB = ai.get().bank;
+ ai.set(999, 999); const aiHi = ai.get();
+ const hb = DUPRE.headingBug(document.createElement('div'), { value: 450 });
+ const hbB = hb.get().cmd;
+ const hbNdl = hb.el.querySelector('g polygon').parentNode.getAttribute('transform');
+ hb.set(-30); const hbSet = hb.get().cmd;
+ const vt = DUPRE.verticalTape(document.createElement('div'), { value: 99 });
+ const vtB = vt.get(); vt.set(0); const vtLo = vt.get();
+ const vtT = vt.el.querySelector('g g').getAttribute('transform');
+ const tn = DUPRE.twinNeedle(document.createElement('div'), { fuel: 99, oil: -5 });
+ const tnB = tn.get();
+ let cmTxt; const cm = DUPRE.comfortMeter(document.createElement('div'),
+ { temp: 999, humidity: -40, onChange: (v, t) => cmTxt = t });
+ const cmB = cm.get();
+ const bg = DUPRE.badges(document.createElement('div'), { items: [['X', 7], ['Y', 'junk']] });
+ const bgVars = bg.get();
+ const tr = DUPRE.tabularReadout(document.createElement('div'), { secs: -10 });
+ const trB = tr.get();
+ return JSON.stringify({ aiB, aiHi, hbB, hbNdl, hbSet, vtB, vtLo, vtT, tnB, cmB, cmTxt, bgVars, trB });
+ })()`);
+ ok('batch-9 domain gates: flight-instrument clamps at build and set, badges variant coercion, readout wrap',
+ clamps9 === JSON.stringify({ aiB: -60, aiHi: { bank: 60, pitch: 20 }, hbB: 90,
+ hbNdl: 'rotate(90.0,65,65)', hbSet: 330, vtB: 35, vtLo: 5, vtT: 'translate(0,92.5)',
+ tnB: [4, 0], cmB: [100, 0], cmTxt: '100F · 0% RH · TOO WARM', bgVars: [1, 0], trB: 3590 }), clamps9);
+
+ // batch-9 state through the renamed DOM (catches rename stragglers): badges
+ // swap variant classes, the readout counts down through dupre-readout and
+ // carries a styled dupre-unit (the old .readout .u rule was dead — .u is a
+ // sibling, so the caption rendered unstyled), the engraved label counts
+ // through dupre-cnt, and the SVG flight instruments track set().
+ const b9dom = await evl(`(() => {
+ const bg = DUPRE.badges(document.createElement('div'));
+ const nBadge = bg.el.querySelectorAll('.dupre-badge').length;
+ const red0 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const ghost0 = bg.el.querySelectorAll('.dupre-badge.dupre-ghost').length;
+ bg.set(0, 1);
+ const red1 = bg.el.querySelectorAll('.dupre-badge.dupre-red').length;
+ const trHost = document.createElement('div'); document.body.appendChild(trHost);
+ const tr = DUPRE.tabularReadout(trHost);
+ const trTxt = tr.el.querySelector('.dupre-readout').textContent;
+ const unit = tr.el.querySelector('.dupre-unit');
+ const unitTxt = unit.textContent;
+ const unitDisp = getComputedStyle(unit).display;
+ tr.tick(); const trTick = tr.el.querySelector('.dupre-readout').textContent;
+ tr.el.querySelector('.dupre-readout').click(); tr.tick();
+ const trPaused = tr.el.querySelector('.dupre-readout').textContent;
+ trHost.remove();
+ const el = DUPRE.engravedLabel(document.createElement('div'));
+ const cnt0 = el.el.querySelector('.dupre-cnt').textContent;
+ el.set(7); const cnt7 = el.el.querySelector('.dupre-cnt').textContent;
+ el.el.click(); const cnt8 = el.el.querySelector('.dupre-cnt').textContent;
+ const tn = DUPRE.twinNeedle(document.createElement('div'));
+ tn.set(0, 4);
+ const tnF = tn.el.querySelector('polygon[fill="#e0523a"]').parentNode.getAttribute('transform');
+ const tnO = tn.el.querySelector('polygon[fill="#bfc4d0"]').parentNode.getAttribute('transform');
+ const cm = DUPRE.comfortMeter(document.createElement('div'));
+ cm.set(70, 80);
+ const humid = [...cm.el.querySelectorAll('text')].find(t => t.textContent === 'TOO HUMID').getAttribute('fill');
+ const ai = DUPRE.attitudeIndicator(document.createElement('div'));
+ ai.set(30, -10);
+ const aiT = ai.el.querySelector('g g').getAttribute('transform');
+ const rc = DUPRE.roundCrt(document.createElement('div'));
+ const p1 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ rc.tick();
+ const p2 = rc.el.querySelector('polyline[stroke="#f4fcf4"]').getAttribute('points');
+ const cr = DUPRE.chartRecorder(document.createElement('div'));
+ cr.reset();
+ const crH = cr.get();
+ const crD = cr.el.querySelector('path[stroke="#8f2416"]').getAttribute('d');
+ return JSON.stringify({ nBadge, red0, ghost0, red1, trTxt, unitTxt, unitDisp, trTick, trPaused,
+ cnt0, cnt7, cnt8, tnF, tnO, humid, aiT, crtMoves: p1 !== p2 && !p2.includes('NaN'), crH, crD });
+ })()`);
+ ok('batch-9 renamed DOM tracks state: badge variants, readout clock + styled unit, engraved count, instruments follow set()',
+ b9dom === JSON.stringify({ nBadge: 3, red0: 1, ghost0: 1, red1: 2, trTxt: '24:10',
+ unitTxt: 'timer', unitDisp: 'block', trTick: '24:09', trPaused: '24:09',
+ cnt0: '· 3', cnt7: '· 7', cnt8: '· 8', tnF: 'rotate(-170.0,60,62)', tnO: 'rotate(10.0,60,62)',
+ humid: '#c23a28', aiT: 'rotate(-30.0,65,65) translate(0,-16.0)',
+ crtMoves: true, crH: 0, crD: '' }), b9dom);
+
// late exceptions from interactions
const errs2 = events.filter(e => e.method === 'Runtime.exceptionThrown');
ok('no exceptions after interaction', errs2.length === 0);