diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-18 13:27:11 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-18 13:27:11 -0500 |
| commit | 0b596f1c3e98d57f23aee10931348aea39da727d (patch) | |
| tree | 89162b39462a38a49028343bd6f654d4d33f440e /docs | |
| parent | 63fd47c2a1a03da7cdce47219c541fef90545b78 (diff) | |
| download | archsetup-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')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 14 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 257 |
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} |
