aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-18 13:27:11 -0500
committerCraig Jennings <c@cjennings.net>2026-07-18 13:27:11 -0500
commit0b596f1c3e98d57f23aee10931348aea39da727d (patch)
tree89162b39462a38a49028343bd6f654d4d33f440e /docs/prototypes
parent63fd47c2a1a03da7cdce47219c541fef90545b78 (diff)
downloadarchsetup-0b596f1c3e98d57f23aee10931348aea39da727d.tar.gz
archsetup-0b596f1c3e98d57f23aee10931348aea39da727d.zip
feat(gallery): bring ten core controls to the extraction bar
Batch 2 of the extraction sweep: consoleKeys, abcKeypad, indexPlate, faderH, faderV, knob, segmented, chipToggle, armButton and lampRow now carry the split-flap-shape contract comment. Every class they own wears the dupre- prefix, internals and modifiers included (dupre-key dupre-green, dupre-fader dupre-cap), with every in-kit consumer updated in the same pass: the dsky and annunciator pads, the rotarySelector and encoder spindles, the page's size bar, and the probe selectors. I also removed indexPlate's dead '#dupre-defs' lookup (nothing ever created that id) and dropped the unused .key.off rule. The faders, knob and lampRow now cache their element refs instead of re-querying every set(), and segmented and lampRow clamp set() the way consoleKeys does. The audit's contract count moves from 14 to 24 of 111.
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/panel-widget-gallery.html14
-rw-r--r--docs/prototypes/widgets.js257
2 files changed, 174 insertions, 97 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}