diff options
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 390 |
1 files changed, 195 insertions, 195 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 8af2666..2766353 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -1,12 +1,12 @@ -/* widgets.js — retro-instrument widget library (GW namespace). +/* widgets.js — The Dupre Kit: retro-instrument component library (DUPRE namespace). Classic script: load after the token :root block (tokens.json → gen_tokens.py), - before any GW.* call. Works from file:// — no modules, no build step. + before any DUPRE.* call. Works from file:// — no modules, no build step. Contract: each gallery card in panel-widget-gallery.html is the visual + behavioral spec its builder is judged against. Spec: docs/specs/2026-07-12-component-generation-spec.org */ (function () { 'use strict'; -const GW = {}; +const DUPRE = {}; /* ================= shared engine ================= */ @@ -107,19 +107,19 @@ const SCREEN_FAMS = { '--crt-face1': '#d0d2d0', '--crt-face2': '#a8aaa8', '--crt-glow': '#f7f7f7' }, }; -/* ================= widget builders ================= - Every builder: GW.name(host, opts) → handle. host is an empty element the - widget renders into. opts.onChange(value, text) fires on every state change, - including the initial paint; text is the widget's canonical readout string. */ +/* ================= instrument builders ================= + Every builder: DUPRE.name(host, opts) → handle. host is an empty element the + instrument renders into. opts.onChange(value, text) fires on every state change, + including the initial paint; text is the instrument's canonical readout string. */ const noop = () => {}; /* ---- the accent family ---- - One named set of lit colours for the widgets whose colour IS their claim: a + One named set of lit colours for the instruments whose colour IS their claim: a chip, a lamp, a badge, a status line. "On" is good in one panel, a warning in the next and a fault in the one after, so the colour belongs to the consumer rather than the builder. - Deliberately NOT applied to widgets whose colour is the object rather than a + Deliberately NOT applied to instruments whose colour is the object rather than a state — the nixie (the palette is explicit that neon is only ever orange), the flip-disc's yellow, the dekatron's glow, a red needle — nor to the ones where the colour is a standard rather than a preference: three greens on the landing @@ -134,60 +134,60 @@ const ACCENTS = { white: 'var(--cream)', vfd: 'var(--vfd)', }; -GW.accentStyles = varName => Object.fromEntries( +DUPRE.accentStyles = varName => Object.fromEntries( Object.entries(ACCENTS).map(([name, colour]) => [name, { dot: colour, vars: { [varName]: colour } }])); /* ---- policy ---- - Every widget's colour is either the consumer's to pick or locked for a reason, - and the reason is one of these six kinds. Declared per builder (GW.<name>.POLICY) + Every instrument's colour is either the consumer's to pick or locked for a reason, + and the reason is one of these six kinds. Declared per builder (DUPRE.<name>.POLICY) so it's a checked property rather than something the next person recalls — the colour pass kept turning up cards where "is this one a standard?" was answered from memory and answered wrong. The two FREE kinds get chips; the four LOCKED ones must not, and the probe enforces the split. - Policy is a property of the WIDGET, not the colour: the same vfd cyan is a free + Policy is a property of the INSTRUMENT, not the colour: the same vfd cyan is a free accent on a chip and a locked emissive identity on the marquee. A few cards are mixed (a free display with a coded red-line) and declare per element in time. Each builder's POLICY is a record { kind, why, authentic }: kind — one of the six below. - why — why THIS widget is bound that way, in its own terms. + why — why THIS instrument is bound that way, in its own terms. authentic — what may change and still be true to the reference (the range that actually existed), or 'nothing' when the colour is fixed. */ -GW.POLICIES = { +DUPRE.POLICIES = { accent: { free: true, gist: 'A lit state whose meaning varies by panel; the consumer picks from the accent family.' }, screen: { free: true, gist: 'A display whose phosphor was made in several real colours; the consumer picks from the screen family.' }, coded: { free: false, gist: 'Colour is meaning fixed by an external standard; a recolour misleads a trained operator (landing-gear three-greens, breaker trip).' }, emissive: { free: false, gist: 'Colour is what the physical source emits and no other was made; unrecognisable otherwise (nixie neon, dekatron glow).' }, - relational: { free: false, gist: 'Colour means what it does only by contrast with another on the same widget; the set is a scale or legend (VU zones, crossed needles).' }, + relational: { free: false, gist: 'Colour means what it does only by contrast with another on the same instrument; the set is a scale or legend (VU zones, crossed needles).' }, material: { free: false, gist: 'Colour of a static physical part, not a state — nothing to parameterise as a signal (needle, brass bezel, knife blade).' }, }; /* 01 slide toggle — on/off pill. State colors ride CSS vars (--sw-*). opts.onStyle / offStyle / offText / thumb pick a named style per axis from - GW.slideToggle.STYLES; defaults match the stylesheet fallbacks. The handle's + DUPRE.slideToggle.STYLES; defaults match the stylesheet fallbacks. The handle's setStyle(axis, name) restyles a live instance (the gallery chips use it). */ -GW.slideToggle = function (host, opts = {}) { +DUPRE.slideToggle = function (host, opts = {}) { const onChange = opts.onChange || noop; const sw = document.createElement('span'); sw.className = 'switch'; host.appendChild(sw); const setStyle = (axis, name) => { - const o = (GW.slideToggle.STYLES[axis] || {})[name]; + const o = (DUPRE.slideToggle.STYLES[axis] || {})[name]; if (!o) return; for (const [k, v] of Object.entries(o.vars)) sw.style.setProperty(k, v); }; /* opts.preset names an intent; per-axis opts still win over it, so a caller can take 'armed' and swap only the thumb. setPreset re-applies all four axes at once and returns the axis map so a host UI can resync its own controls. */ - const preset = GW.slideToggle.PRESETS[opts.preset] || GW.slideToggle.PRESETS.panel; + const preset = DUPRE.slideToggle.PRESETS[opts.preset] || DUPRE.slideToggle.PRESETS.panel; /* AXIS_ORDER is load-bearing, not cosmetic: onText overrides the ink that the `on` style sets, so applying the axes in any other order silently drops it. */ const setPreset = name => { - const p = GW.slideToggle.PRESETS[name]; + const p = DUPRE.slideToggle.PRESETS[name]; if (!p) return null; - for (const axis of GW.slideToggle.AXIS_ORDER) if (p[axis]) setStyle(axis, p[axis]); + for (const axis of DUPRE.slideToggle.AXIS_ORDER) if (p[axis]) setStyle(axis, p[axis]); return p; }; const pick = { on: opts.onStyle, onText: opts.onText, off: opts.offStyle, offText: opts.offText, thumb: opts.thumb }; - for (const axis of GW.slideToggle.AXIS_ORDER) setStyle(axis, pick[axis] || preset[axis]); + for (const axis of DUPRE.slideToggle.AXIS_ORDER) setStyle(axis, pick[axis] || preset[axis]); let on; const set = v => { on = !!v; sw.classList.toggle('on', on); onChange(on, on ? 'ON' : 'OFF'); }; sw.addEventListener('click', () => set(!on)); @@ -198,8 +198,8 @@ GW.slideToggle = function (host, opts = {}) { actually reaches for; the STYLES axes below are how one is built. The `on` tone does the semantic work and the thumb material backs it: a touchscreen pill wears plastic, a run or armed switch wears metal. */ -GW.slideToggle.AXIS_ORDER = ['on', 'onText', 'off', 'offText', 'thumb']; -GW.slideToggle.PRESETS = { +DUPRE.slideToggle.AXIS_ORDER = ['on', 'onText', 'off', 'offText', 'thumb']; +DUPRE.slideToggle.PRESETS = { panel: { on: 'amber', onText: 'panel', off: 'dark', offText: 'white', thumb: 'light' }, run: { on: 'green', onText: 'panel', off: 'dark', offText: 'dim', thumb: 'chrome' }, armed: { on: 'red', onText: 'cream', off: 'dark', offText: 'white', thumb: 'chrome' }, @@ -210,7 +210,7 @@ GW.slideToggle.PRESETS = { dark: { on: 'dark', onText: 'green', off: 'dark', offText: 'red', thumb: 'chrome' }, }; /* named styles per axis: dot = swatch color for pickers, vars = --sw-* overrides */ -GW.slideToggle.STYLES = { +DUPRE.slideToggle.STYLES = { on: { amber: { dot: 'var(--gold-hi)', vars: { '--sw-on-bg': 'linear-gradient(180deg,var(--amber-grad-top),var(--gold))', '--sw-on-brd': 'var(--gold-hi)', '--sw-on-ink': 'var(--panel)' } }, green: { dot: '#8fb944', vars: { '--sw-on-bg': 'linear-gradient(180deg,#a9c95f,var(--pass))', '--sw-on-brd': '#a9c95f', '--sw-on-ink': 'var(--panel)' } }, @@ -259,9 +259,9 @@ GW.slideToggle.STYLES = { 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"). */ -GW.consoleKeys = function (host, opts = {}) { +DUPRE.consoleKeys = function (host, opts = {}) { const onChange = opts.onChange || noop; - const keys = opts.keys || GW.consoleKeys.DEFAULT_KEYS; + 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; @@ -281,10 +281,10 @@ GW.consoleKeys = function (host, opts = {}) { resting state for a console and it shows the green. A caller supplying its own keys gets the first one unless it says otherwise. */ set(opts.active !== undefined ? opts.active - : (keys === GW.consoleKeys.DEFAULT_KEYS ? 1 : 0)); + : (keys === DUPRE.consoleKeys.DEFAULT_KEYS ? 1 : 0)); return { el: wrap, get: () => idx, set }; }; -GW.consoleKeys.DEFAULT_KEYS = [ +DUPRE.consoleKeys.DEFAULT_KEYS = [ { label: 'SCAN' }, { label: 'LIVE', tone: 'green' }, { label: 'MUTED', tone: 'red' }, ]; @@ -303,7 +303,7 @@ GW.consoleKeys.DEFAULT_KEYS = [ already --fail and --pass. - Letters left, digits right (the stainless reference's arrangement). - DEL added, CANCEL dropped. See the layout block below for both. */ -GW.abcKeypad = function (host, opts = {}) { +DUPRE.abcKeypad = function (host, opts = {}) { const onChange = opts.onChange || noop; const MAX = opts.max || 16; const s = stageSvg(host, 'rsvg', 232, 226); @@ -342,7 +342,7 @@ GW.abcKeypad = function (host, opts = {}) { Gated on ACTIONS rather than on KEYS because they are different sets — CLR is a real key on the plate that no keystroke maps to. */ const press = k => { - if (!GW.abcKeypad.ACTIONS.has(k)) return; + if (!DUPRE.abcKeypad.ACTIONS.has(k)) return; if (k === 'ENT') { onChange(buf, buf ? 'ENTER · ' + buf : 'empty'); return; } if (k === 'DEL') { buf = buf.slice(0, -1); render(); onChange(buf, buf || 'empty'); return; } if (k === 'CLR') { buf = ''; render(); onChange(buf, 'cleared'); return; } @@ -408,14 +408,14 @@ GW.abcKeypad = function (host, opts = {}) { /* Keyboard, per the README's keyboard contract. The listener is bound to the 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. GW.slideRule is the precedent. */ + own Escape handler. DUPRE.slideRule is the precedent. */ s.setAttribute('class', 'rsvg kp-pad'); s.setAttribute('tabindex', '0'); s.addEventListener('click', () => s.focus()); s.addEventListener('keydown', e => { if (e.ctrlKey || e.metaKey || e.altKey) return; /* leave shortcuts alone */ const name = e.key === ' ' ? 'Space' : (e.key.length === 1 ? e.key.toUpperCase() : e.key); - const k = GW.abcKeypad.KEYS[name]; + const k = DUPRE.abcKeypad.KEYS[name]; if (!k) return; /* not on the plate: let it bubble */ /* Spend preventDefault only where there is a default worth killing — Space scrolls the page, Backspace can navigate back. Tab and Escape are never @@ -430,10 +430,10 @@ GW.abcKeypad = function (host, opts = {}) { /* The plate's keys, declared as a table so every target reads the same intent. Deliberately not a function over a DOM event: the Emacs port installs this into a keymap and never sees a keydown, so a function would force it to - re-derive what the widget accepts and the two bindings would drift apart. + re-derive what the instrument accepts and the two bindings would drift apart. This is also press()'s allowlist — press appends whatever it is handed, so without the table a stray 'F1' would land in the buffer as text. */ -GW.abcKeypad.KEYS = (() => { +DUPRE.abcKeypad.KEYS = (() => { const m = {}; for (const c of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') m[c] = c; m.Space = 'SPC'; m.Backspace = 'DEL'; m.Enter = 'ENT'; @@ -442,7 +442,7 @@ GW.abcKeypad.KEYS = (() => { /* The plate's whole vocabulary — every argument press accepts, from any caller. A superset of the KEYS values: CLR is on the plate but no keystroke reaches it (Escape is the obvious candidate and belongs to the gallery's audit stepper). */ -GW.abcKeypad.ACTIONS = new Set([ +DUPRE.abcKeypad.ACTIONS = new Set([ ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 'SPC', 'DEL', 'ENT', 'CLR', ]); @@ -463,10 +463,10 @@ GW.abcKeypad.ACTIONS = new Set([ 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. */ -GW.indexPlate = function (host, opts = {}) { +DUPRE.indexPlate = function (host, opts = {}) { const onChange = opts.onChange || noop; const MAX = opts.max || 22; - const L = GW.indexPlate.LAYOUT; + const L = DUPRE.indexPlate.LAYOUT; const COLS = Math.max(...L.map(r => r.length)), ROWS = L.length; /* Width is driven by the plate, not guessed: the lever and CLR live in a gutter to its right. Sized from the layout table so a wider plate can't slide them @@ -495,7 +495,7 @@ GW.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('#gw-defs') || svgEl(s, 'defs', {}); + const defs = s.ownerDocument.querySelector('#dupre-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 }); @@ -507,11 +507,11 @@ GW.indexPlate = function (host, opts = {}) { a dark one for the lowercase — the plate's photographic-negative trick */ const ZFILL = { caps: '#e9e0bb', lower: '#231d13' }; const zoneAt = (r, c) => { - const z = GW.indexPlate.ZONES.find(z => + const z = DUPRE.indexPlate.ZONES.find(z => r >= z.rows[0] && r <= z.rows[1] && c >= z.cols[0] && c <= z.cols[1]); return z ? z.zone : 'ring'; }; - GW.indexPlate.ZONES.forEach(z => { + DUPRE.indexPlate.ZONES.forEach(z => { svgEl(s, 'rect', { x: PX + z.cols[0] * CW - 1, y: PY + z.rows[0] * CH - 1, width: (z.cols[1] - z.cols[0] + 1) * CW + 2, height: (z.rows[1] - z.rows[0] + 1) * CH + 2, @@ -527,7 +527,7 @@ GW.indexPlate = function (host, opts = {}) { /* The stylus: a cone on a short shaft, hovering over the selected cell. The 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 widget must not do. A tip is enough to say "the pointer is here". */ + 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'); 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 }); @@ -601,7 +601,7 @@ GW.indexPlate = function (host, opts = {}) { primary input on this card would skip it and every probe would stay green. Gated on ACTIONS, so it selects nothing it has no cell for. */ const press = k => { - if (!GW.indexPlate.ACTIONS.has(k)) return; + if (!DUPRE.indexPlate.ACTIONS.has(k)) return; if (k === 'PRINT') return print(); if (k === 'CLR') return fresh(); select(k); @@ -617,7 +617,7 @@ GW.indexPlate = function (host, opts = {}) { s.addEventListener('click', () => s.focus()); s.addEventListener('keydown', e => { if (e.ctrlKey || e.metaKey || e.altKey) return; - const k = GW.indexPlate.KEYS[e.key]; + const k = DUPRE.indexPlate.KEYS[e.key]; if (!k) return; e.preventDefault(); /* Enter would submit a form; a cell key has no default worth keeping */ press(k); @@ -639,7 +639,7 @@ GW.indexPlate = function (host, opts = {}) { - J and j live out on the ring, not in the case blocks, which is why the inversion below is positional and not "capitals are dark". The two dashes differ: a long one top-right, a hyphen bottom-right. */ -GW.indexPlate.LAYOUT = [ +DUPRE.indexPlate.LAYOUT = [ ['&','(',')',':','"','!', '?',"'",'ä','ö','ü','—'], ['§','P','F','U','G','Q', 'p','f','u','g','q',';'], ['J','V','I','N','A','B', 'v','i','n','a','b','j'], @@ -654,7 +654,7 @@ GW.indexPlate.LAYOUT = [ the outer ring stays light-on-halftone — including the capital J, which is why no case-based rule can describe this. Move a block, move its rectangle. rows and cols are inclusive [start, end] indices into LAYOUT. */ -GW.indexPlate.ZONES = [ +DUPRE.indexPlate.ZONES = [ { rows: [1, 5], cols: [1, 5], zone: 'caps' }, { rows: [1, 5], cols: [6, 10], zone: 'lower' }, ]; @@ -663,18 +663,18 @@ GW.indexPlate.ZONES = [ Every plate character maps to itself (no case folding — the plate has both, so Shift does the work a shift key would), and Enter is the lever. Space is deliberately absent: there's no space cell yet, so Space isn't ours to claim. */ -GW.indexPlate.KEYS = (() => { +DUPRE.indexPlate.KEYS = (() => { const m = {}; - for (const c of GW.indexPlate.LAYOUT.flat()) if (c) m[c] = c; + for (const c of DUPRE.indexPlate.LAYOUT.flat()) if (c) m[c] = c; m.Enter = 'PRINT'; return m; })(); /* Every argument press accepts. A superset of the KEYS values: CLR is a real control that no keystroke reaches, the same shape as the keypad's. */ -GW.indexPlate.ACTIONS = new Set([...GW.indexPlate.LAYOUT.flat().filter(Boolean), 'PRINT', 'CLR']); +DUPRE.indexPlate.ACTIONS = new Set([...DUPRE.indexPlate.LAYOUT.flat().filter(Boolean), 'PRINT', 'CLR']); /* 03 horizontal fader — continuous 0-100 */ -GW.faderH = function (host, opts = {}) { +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>'; @@ -692,7 +692,7 @@ GW.faderH = function (host, opts = {}) { }; /* 04 vertical fader — one channel-strip fader; compose per channel */ -GW.faderV = function (host, opts = {}) { +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>'; @@ -710,7 +710,7 @@ GW.faderV = function (host, opts = {}) { }; /* 05 rotary knob — drag up/down to turn, -150°..+150° sweep */ -GW.knob = function (host, opts = {}) { +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'; @@ -728,14 +728,14 @@ GW.knob = function (host, opts = {}) { }; /* 06 segmented selector — pick one of a few. opts.accent picks the lit-segment - color from GW.segmented.STYLES (amber / green / red); defaults match the + color from DUPRE.segmented.STYLES (amber / green / red); defaults match the stylesheet fallbacks. setStyle(axis, name) restyles a live instance. */ -GW.segmented = function (host, opts = {}) { +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 setStyle = (axis, name) => { - const o = (GW.segmented.STYLES[axis] || {})[name]; + const o = (DUPRE.segmented.STYLES[axis] || {})[name]; if (!o) return; for (const [k, v] of Object.entries(o.vars)) seg.style.setProperty(k, v); }; @@ -748,7 +748,7 @@ GW.segmented = function (host, opts = {}) { return { el: seg, get: () => idx, set, setStyle }; }; /* named styles per axis: dot = swatch color for pickers, vars = --seg-* overrides */ -GW.segmented.STYLES = { +DUPRE.segmented.STYLES = { accent: { amber: { dot: 'var(--gold-hi)', vars: { '--seg-on-bg': 'linear-gradient(180deg,var(--amber-grad-top),var(--gold))', '--seg-on-ink': 'var(--panel)' } }, green: { dot: '#8fb944', vars: { '--seg-on-bg': 'linear-gradient(180deg,#a9c95f,var(--pass))', '--seg-on-ink': 'var(--panel)' } }, @@ -757,13 +757,13 @@ GW.segmented.STYLES = { }; /* 07 chip toggle — inline binary inside a line of text */ -GW.chipToggle = function (host, opts = {}) { +DUPRE.chipToggle = function (host, opts = {}) { const onChange = opts.onChange || noop; const chip = document.createElement('span'); chip.className = 'chip'; chip.textContent = opts.label || 'discoverable on'; host.appendChild(chip); const setStyle = (axis, name) => { - const o = (GW.chipToggle.STYLES[axis] || {})[name]; + const o = (DUPRE.chipToggle.STYLES[axis] || {})[name]; if (!o) return; for (const [k, v] of Object.entries(o.vars)) chip.style.setProperty(k, v); }; @@ -778,10 +778,10 @@ GW.chipToggle = function (host, opts = {}) { let the chip say exactly one thing — but "on" is good in one panel, a warning in the next, and a fault in the one after. The colour is the claim, so it belongs to the consumer. */ -GW.chipToggle.STYLES = { accent: GW.accentStyles('--chip-on') }; +DUPRE.chipToggle.STYLES = { accent: DUPRE.accentStyles('--chip-on') }; /* 08 arm-to-fire — two-stage confirm for destructive actions */ -GW.armButton = function (host, opts = {}) { +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; @@ -796,7 +796,7 @@ GW.armButton = function (host, opts = {}) { }; /* 09 lamp row — actionable list item: lamp + name + status, click to cycle */ -GW.lampRow = function (host, opts = {}) { +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']]; @@ -817,7 +817,7 @@ GW.lampRow = function (host, opts = {}) { }; /* 24 rotary selector — pick one of five printed detents by position */ -GW.rotarySelector = function (host, opts = {}) { +DUPRE.rotarySelector = function (host, opts = {}) { const onChange = opts.onChange || noop; const values = opts.values || [4, 6, 8, 10, 12]; const fmt = opts.fmt || (v => 'size ' + v); @@ -846,9 +846,9 @@ GW.rotarySelector = function (host, opts = {}) { are the majors; the integer units between them get minor ticks and are selectable too. Click a numeral, a mark, or between marks; ←/→ (↑/↓) step one unit. set() takes a stop index; get() returns the value. opts.skin picks - a face from GW.slideRule.STYLES (warm backlit / chrome / 80s black glass / marantz blue); + a face from DUPRE.slideRule.STYLES (warm backlit / chrome / 80s black glass / marantz blue); setStyle(axis, name) restyles a live instance. */ -GW.slideRule = function (host, opts = {}) { +DUPRE.slideRule = function (host, opts = {}) { const onChange = opts.onChange || noop; const majors = opts.values || [4, 6, 8, 10, 12]; const fmt = opts.fmt || (v => 'pos ' + v); @@ -865,7 +865,7 @@ GW.slideRule = function (host, opts = {}) { const t = document.createElement('span'); t.className = 'tuner'; t.tabIndex = 0; t.setAttribute('role', 'slider'); t.setAttribute('aria-label', 'slide-rule value'); const setStyle = (axis, name) => { - const o = (GW.slideRule.STYLES[axis] || {})[name]; + const o = (DUPRE.slideRule.STYLES[axis] || {})[name]; if (!o) return; for (const [k, v] of Object.entries(o.vars)) t.style.setProperty(k, v); }; @@ -902,7 +902,7 @@ GW.slideRule = function (host, opts = {}) { }; /* named faces: dot = swatch color for pickers, vars = --tn-* overrides. warm restates the stylesheet fallbacks so switching back is exact. */ -GW.slideRule.STYLES = { +DUPRE.slideRule.STYLES = { skin: { warm: { dot: 'var(--gold-hi)', vars: { '--tn-bg': 'linear-gradient(180deg,#191510,#0b0908)', '--tn-brd': '#2a251c', @@ -928,7 +928,7 @@ GW.slideRule.STYLES = { }; /* N01 rocker power switch — hard on/off, lit legend */ -GW.rocker = function (host, opts = {}) { +DUPRE.rocker = function (host, opts = {}) { const onChange = opts.onChange || noop; const r = document.createElement('span'); r.className = 'rocker'; r.innerHTML = `<span class="half top">${opts.onLabel || 'ON'}</span><span class="half bot">${opts.offLabel || 'OFF'}</span>`; @@ -942,7 +942,7 @@ GW.rocker = function (host, opts = {}) { /* N02 transport cluster — rew/play/stop/rec, one lit; reels turn while playing. opts.animate: reels obey play state (default: not prefers-reduced-motion) */ -GW.transport = function (host, opts = {}) { +DUPRE.transport = function (host, opts = {}) { const onChange = opts.onChange || noop; const animate = opts.animate !== undefined ? opts.animate : !matchMedia('(prefers-reduced-motion: reduce)').matches; const MODES = { rew: '⏮', play: '▶', stop: '⏹', rec: '⏺' }; @@ -973,7 +973,7 @@ GW.transport = function (host, opts = {}) { }; /* N03 radio preset bank — mechanically exclusive presets */ -GW.presetBank = function (host, opts = {}) { +DUPRE.presetBank = function (host, opts = {}) { const onChange = opts.onChange || noop; const items = opts.items || ['WIFI', 'ETH', 'CELL', 'OFF']; const bank = document.createElement('div'); bank.className = 'radiobank'; host.appendChild(bank); @@ -986,7 +986,7 @@ GW.presetBank = function (host, opts = {}) { }; /* N04 concentric dual knob — two values on one spindle, outer ring + inner cap */ -GW.dualKnob = function (host, opts = {}) { +DUPRE.dualKnob = function (host, opts = {}) { const onChange = opts.onChange || noop; const dk = document.createElement('span'); dk.className = 'dualknob'; dk.innerHTML = `<span class="outer"><span class="tick" style="transform:rotate(-120deg)"></span><span class="tick" style="transform:rotate(120deg)"></span></span><span class="inner"><span class="ind"></span></span>`; @@ -1007,7 +1007,7 @@ GW.dualKnob = function (host, opts = {}) { }; /* N05 rotary encoder + LED ring — endless dial, lit arc tracks the level */ -GW.encoder = function (host, opts = {}) { +DUPRE.encoder = function (host, opts = {}) { const onChange = opts.onChange || noop; const enc = document.createElement('span'); enc.className = 'encoder'; host.appendChild(enc); const R = 27, cx = 33, cy = 33; @@ -1032,7 +1032,7 @@ GW.encoder = function (host, opts = {}) { }; /* N06 keyed mode switch — guarded three-position mode, key-bit points at the live one */ -GW.keySwitch = function (host, opts = {}) { +DUPRE.keySwitch = function (host, opts = {}) { const onChange = opts.onChange || noop; const items = opts.items || ['OFF', 'ON', 'RUN']; const ANG = [-70, 0, 70], POS = [[16, 64], [50, 12], [84, 64]]; @@ -1058,7 +1058,7 @@ GW.keySwitch = function (host, opts = {}) { }; /* N07 center-detented crossfader — throw to either side of zero */ -GW.crossfader = function (host, opts = {}) { +DUPRE.crossfader = function (host, opts = {}) { const onChange = opts.onChange || noop; const xf = document.createElement('div'); xf.className = 'xfader'; xf.innerHTML = `<div class="slot"></div><div class="detent"></div><span class="end" style="left:0">${opts.aLabel || 'A'}</span><span class="end" style="right:0">${opts.bLabel || 'B'}</span><div class="cap"></div>`; @@ -1076,7 +1076,7 @@ GW.crossfader = function (host, opts = {}) { }; /* N08 thumbwheel — knurled edge-wheel with a windowed two-digit value */ -GW.thumbwheel = function (host, opts = {}) { +DUPRE.thumbwheel = function (host, opts = {}) { const onChange = opts.onChange || noop; const w = document.createElement('div'); w.className = 'thumbw'; w.innerHTML = '<span class="thumbwheel"></span><span class="win"></span>'; @@ -1094,7 +1094,7 @@ GW.thumbwheel = function (host, opts = {}) { }; /* N09 DIP-switch bank — hard flags, up is on; readout is the binary word */ -GW.dipBank = function (host, opts = {}) { +DUPRE.dipBank = function (host, opts = {}) { const onChange = opts.onChange || noop; const bits = opts.bits || [true, false, true, true, false, false]; const bank = document.createElement('span'); bank.className = 'dip'; host.appendChild(bank); @@ -1110,7 +1110,7 @@ GW.dipBank = function (host, opts = {}) { }; /* N10 jog / shuttle wheel — scrub fine; position accumulates without limit */ -GW.jogWheel = function (host, opts = {}) { +DUPRE.jogWheel = function (host, opts = {}) { const onChange = opts.onChange || noop; const j = document.createElement('span'); j.className = 'jog'; j.innerHTML = `<span class="shuttle" style="--sh:40deg"></span><span class="inner"><span class="dimple"></span></span>`; @@ -1127,7 +1127,7 @@ GW.jogWheel = function (host, opts = {}) { }; /* ---- shared SVG defs: gradients/filters referenced by url(#id) are - document-scoped, and several are used across widgets. Builders ensure the + document-scoped, and several are used across instruments. Builders ensure the defs they use; the first caller creates it, later calls are no-ops. ---- */ let defsRoot = null; const defsMade = new Set(); @@ -1161,7 +1161,7 @@ let uidN = 0; function uid(prefix) { return prefix + '-' + (++uidN); } /* R02 calibrated vernier dial — the disc turns under a fixed hairline */ -GW.vernierDial = function (host, opts = {}) { +DUPRE.vernierDial = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 150, 150), cx = 75, cy = 75; gradDef('vernFace', 'radialGradient', { cx: '50%', cy: '45%', r: '60%' }, [['0', '#f7f2da'], ['1', '#e4dfc2']]); @@ -1191,7 +1191,7 @@ GW.vernierDial = function (host, opts = {}) { }; /* R03 bat-handle toggle — chrome lever throws between lit legends */ -GW.batToggle = function (host, opts = {}) { +DUPRE.batToggle = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 70, 90), cx = 35, cy = 44; gradDef('nutG', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#8a8578'], ['1', '#4e4a42']]); @@ -1222,7 +1222,7 @@ GW.batToggle = function (host, opts = {}) { }; /* R04 bakelite fluted knob — scallop skirt over a printed 0-10 arc */ -GW.flutedKnob = function (host, opts = {}) { +DUPRE.flutedKnob = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 110), cx = 55, cy = 54; gradDef('bakeSk', 'radialGradient', { cx: '42%', cy: '36%', r: '80%' }, [['0', '#312d27'], ['1', '#050404']]); @@ -1249,7 +1249,7 @@ GW.flutedKnob = function (host, opts = {}) { }; /* R05 filter slider bank — a dense fader wall on a real faceplate. Period skins - as constructor opts backed by GW.filterBank.STYLES, three independent axes: + as constructor opts backed by DUPRE.filterBank.STYLES, three independent axes: panel (silver hi-fi aluminum / studio black), caps — the cap SHAPE (chrome T / short ribbed / tall block fader), and capColor — the cap FINISH (black white-index / color stripes / chrome / cream). opts.style @@ -1257,13 +1257,13 @@ GW.flutedKnob = function (host, opts = {}) { opts.panel / opts.caps / opts.capColor override an axis; setStyle(axis, name) restyles live (values kept). After the Pioneer SG-9500, the Technics SH-8065, and the Zaxcom Oasis block faders. */ -GW.filterBank = function (host, opts = {}) { +DUPRE.filterBank = function (host, opts = {}) { const onChange = opts.onChange || noop; const freqs = opts.freqs || [78, 113, 163, 235, 339, 487, 701, 1010, 1450, 2090, 3010, 4340]; const vals = (opts.values || [18, 30, 42, 25, 35, 55, 20, 48, 38, 26, 44, 32]).slice(0, freqs.length); while (vals.length < freqs.length) vals.push(30); const fmtHz = f => f < 1000 ? f : (f / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - const ST = GW.filterBank.STYLES; + const ST = DUPRE.filterBank.STYLES; const native = { silver: { caps: 'chrome', capColor: 'chrome' }, studio: { caps: 'ribbed', capColor: 'red' }, @@ -1352,7 +1352,7 @@ GW.filterBank = function (host, opts = {}) { /* named styles per axis: dot = swatch color for pickers. caps is the cap shape; capColor is the cap finish (body + rib + index line), applicable to any shape. */ -GW.filterBank.STYLES = { +DUPRE.filterBank.STYLES = { panel: { silver: { dot: '#c9c9c5', plate: 'url(#fbPlateSilver)', edge: '#8e8e88', bevel: 'rgba(255,255,255,.4)', ink: '#33332f', dim: '#5c5c56', screw: '#9c9c96', track: '#121210', trackW: 2.0, trackHi: null }, studio: { dot: '#1c1a18', plate: 'url(#fbPlateStudio)', edge: '#000', bevel: 'rgba(255,255,255,.06)', ink: '#b9b4a6', dim: '#6f6a5e', screw: '#3c3832', track: '#050505', trackW: 2.2, trackHi: 'rgba(255,255,255,.05)' }, @@ -1374,7 +1374,7 @@ GW.filterBank.STYLES = { }; /* R06 chicken-head selector — tapered lever aims at the position */ -GW.chickenHead = function (host, opts = {}) { +DUPRE.chickenHead = function (host, opts = {}) { const onChange = opts.onChange || noop; const items = opts.items || [['OFF', -60], ['LO', -20], ['MID', 20], ['HI', 60]]; const s = stageSvg(host, 'rsvg press', 110, 96), cx = 55, cy = 58; @@ -1405,7 +1405,7 @@ GW.chickenHead = function (host, opts = {}) { }; /* R12 chrome slot fader — engraved dB scale, chrome T-handle in a screwed plate */ -GW.slotFader = function (host, opts = {}) { +DUPRE.slotFader = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 90, 150), cx = 45, yTop = 30, yBot = 120; const yOf = db => yTop + (12 - db) / 36 * (yBot - yTop); @@ -1442,7 +1442,7 @@ GW.slotFader = function (host, opts = {}) { }; /* R14 spade-pointer tuning knob — engraved relief arc, knurl ring turns with it */ -GW.spadeKnob = function (host, opts = {}) { +DUPRE.spadeKnob = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 140, 124), cx = 70, cy = 82; const sweep = v => -80 + v / 10 * 160; @@ -1481,7 +1481,7 @@ GW.spadeKnob = function (host, opts = {}) { }; /* R15 multi-band dial — nested arcs, one needle; the bandspread dial selects the ring */ -GW.multiBandDial = function (host, opts = {}) { +DUPRE.multiBandDial = function (host, opts = {}) { const onChange = opts.onChange || noop; const ranges = opts.ranges || [[0.54, 1.6], [1.6, 5.1], [5.1, 15.5], [15.5, 30.5]]; const s = stageSvg(host, 'rsvg', 190, 110), cx = 62, cy = 62; @@ -1532,7 +1532,7 @@ GW.multiBandDial = function (host, opts = {}) { }; /* R16 entry keypad — worn keys feed the amber display; lamps watch state */ -GW.entryKeypad = function (host, opts = {}) { +DUPRE.entryKeypad = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 150, 190); gradDef('kpKey', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#dbd8cf'], ['1', '#a29d92']]); @@ -1570,7 +1570,7 @@ GW.entryKeypad = function (host, opts = {}) { }; /* R18 thumb-slide attenuator pair — lit numeral strip, cream side tab */ -GW.thumbSlide = function (host, opts = {}) { +DUPRE.thumbSlide = function (host, opts = {}) { const onChange = opts.onChange || noop; const chans = (opts.channels || [{ name: 'BLEND', v: 74 }, { name: 'MIX', v: 92 }]).map(c => ({ name: c.name, v: c.v })); const s = stageSvg(host, 'rsvg', 130, 150), y0 = 22, y1 = 122; @@ -1616,7 +1616,7 @@ GW.thumbSlide = function (host, opts = {}) { }; /* R19 waveform region editor — monochrome LCD, draggable S/E flags */ -GW.waveRegion = function (host, opts = {}) { +DUPRE.waveRegion = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 190, 100), x0 = 10, x1 = 180, yMid = 48, N = 85; svgEl(s, 'rect', { x: 1, y: 1, width: 188, height: 98, rx: 6, fill: 'var(--scr-bg1,#0b0c0b)', stroke: 'var(--scr-brd,#2a2c2a)', 'stroke-width': 2 }); @@ -1667,7 +1667,7 @@ GW.waveRegion = function (host, opts = {}) { }; /* R20 drum roller selector — numbered paper drum in a window, center chip reads it */ -GW.drumRoller = function (host, opts = {}) { +DUPRE.drumRoller = function (host, opts = {}) { const onChange = opts.onChange || noop; const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]).map(c => ({ name: c.name, v: c.v })); const s = stageSvg(host, 'rsvg', 130, 140), cy = 76, step = 17; @@ -1710,7 +1710,7 @@ GW.drumRoller = function (host, opts = {}) { }; /* R21 LED program row — exclusive select, the LED above the key carries the state */ -GW.ledRow = function (host, opts = {}) { +DUPRE.ledRow = function (host, opts = {}) { const onChange = opts.onChange || noop; const items = opts.items || ['Sm Hall B', 'VocPlate', 'Lg Hall B', 'Chamber', 'ParcPlate', 'Sm Hall A', 'Room A', 'Const Plate']; const s = stageSvg(host, 'rsvg', 190, 58); @@ -1741,7 +1741,7 @@ GW.ledRow = function (host, opts = {}) { }; /* R22 three-position slide — chrome pill between detents, honest LED pair */ -GW.pillSlide = function (host, opts = {}) { +DUPRE.pillSlide = function (host, opts = {}) { const onChange = opts.onChange || noop; const positions = opts.positions || ['A', 'AB', 'B']; const s = stageSvg(host, 'rsvg press', 110, 64), detX = [34, 55, 76]; @@ -1776,7 +1776,7 @@ GW.pillSlide = function (host, opts = {}) { }; /* R23 spun-aluminum knob — machined rings in a knurled grip, red index */ -GW.spunKnob = function (host, opts = {}) { +DUPRE.spunKnob = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 110), cx = 55, cy = 52; gradDef('spunFace', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#e8e6e0'], ['.7', '#b3b0a8'], ['1', '#8a877e']]); @@ -1801,7 +1801,7 @@ GW.spunKnob = function (host, opts = {}) { }; /* R24 stomp switch + jewel — press to engage, the jewel reports */ -GW.stompSwitch = function (host, opts = {}) { +DUPRE.stompSwitch = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55; gradDef('stompDome', 'radialGradient', { cx: '40%', cy: '30%', r: '80%' }, [['0', '#f4f2ec'], ['.55', '#b9b6ae'], ['1', '#6e6b63']]); @@ -1835,7 +1835,7 @@ GW.stompSwitch = function (host, opts = {}) { }; /* R27 winged gain selector — red T-bar over a dot ring, stepped detents */ -GW.wingSelector = function (host, opts = {}) { +DUPRE.wingSelector = function (host, opts = {}) { const onChange = opts.onChange || noop; const steps = opts.steps || [-80, -70, -60, -50, -40, -30, -20, -10, -5, 0, 5, 10]; const s = stageSvg(host, 'rsvg drag', 110, 110), cx = 55, cy = 52; @@ -1868,7 +1868,7 @@ GW.wingSelector = function (host, opts = {}) { }; /* R28 rotary disc switch — the whole disc turns between heavy positions */ -GW.discSwitch = function (host, opts = {}) { +DUPRE.discSwitch = function (host, opts = {}) { const onChange = opts.onChange || noop; const positions = opts.positions || [['OFF', -45], ['ON', 45], ['COMBINE', 135]]; const s = stageSvg(host, 'rsvg press', 110, 116), cx = 55, cy = 58; @@ -1903,7 +1903,7 @@ GW.discSwitch = function (host, opts = {}) { }; /* R29 guarded toggle — guard posts + red collar mark the critical throw */ -GW.guardedToggle = function (host, opts = {}) { +DUPRE.guardedToggle = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 90, 100), cx = 45, cy = 52; gradDef('discRed', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#d98a6f'], ['.65', 'var(--fail)'], ['1', '#7a2a1a']]); @@ -1942,7 +1942,7 @@ GW.guardedToggle = function (host, opts = {}) { /* R32 mechanical timer dial — dial rotates under a fixed index; wind by drag, stop by the red knob. Runs its own 1 Hz wind-down (a demo minute per second) unless reduced motion is set. */ -GW.timerDial = function (host, opts = {}) { +DUPRE.timerDial = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 158, 132), cx = 62, cy = 58; gradDef('mtFace', 'radialGradient', { cx: '50%', cy: '42%', r: '75%' }, [['0', '#282320'], ['1', '#14110e']]); @@ -2003,7 +2003,7 @@ GW.timerDial = function (host, opts = {}) { }; /* R33 four-way rocker — quadrant clicks step a tracked cursor; arrows flash on press */ -GW.rockerPad = function (host, opts = {}) { +DUPRE.rockerPad = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55, cy = 55; gradDef('rk4Pad', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#33302b'], ['1', '#141210']]); @@ -2042,7 +2042,7 @@ GW.rockerPad = function (host, opts = {}) { }; /* R34 four-way toggle selector — ball lever throws to a diagonal; corner lamps show the state */ -GW.fourWayToggle = function (host, opts = {}) { +DUPRE.fourWayToggle = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65; gradDef('fw4Chrome', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 1 }, [['0', '#e8e6e0'], ['.5', '#9a968c'], ['1', '#55524a']]); @@ -2100,7 +2100,7 @@ GW.fourWayToggle = function (host, opts = {}) { }; /* R37 pin routing matrix — click an intersection to seat/pull a pin; many-to-many */ -GW.pinMatrix = function (host, opts = {}) { +DUPRE.pinMatrix = function (host, opts = {}) { const onChange = opts.onChange || noop; const ROWS = opts.rows || ['OSC1', 'OSC2', 'LFO', 'NOIS', 'ENV']; const COLS = opts.cols || ['VCF', 'VCA', 'PAN', 'DLY', 'OUT', 'MOD']; @@ -2128,7 +2128,7 @@ GW.pinMatrix = function (host, opts = {}) { }; /* R38 dead-man button — state exists only while the pointer holds it down */ -GW.deadMan = function (host, opts = {}) { +DUPRE.deadMan = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 120, 110), cx = 60, cy = 58; gradDef('dmBtn', 'radialGradient', { cx: '40%', cy: '32%', r: '85%' }, [['0', '#3a3631'], ['1', '#16130f']]); @@ -2168,7 +2168,7 @@ GW.deadMan = function (host, opts = {}) { }; /* R39 rotary telephone dial — click a hole; the wheel winds to the stop and returns */ -GW.telephoneDial = function (host, opts = {}) { +DUPRE.telephoneDial = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 63; const DIGITS = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0']; @@ -2220,7 +2220,7 @@ GW.telephoneDial = function (host, opts = {}) { }; /* R40 circuit breaker panel — on/off by click, TRIP pops one to the amber mid-state, reset is two-step */ -GW.breakerPanel = function (host, opts = {}) { +DUPRE.breakerPanel = function (host, opts = {}) { const onChange = opts.onChange || noop; const NAMES = opts.names || ['MAIN', 'PUMP', 'LAMP', 'AUX']; const s = stageSvg(host, 'rsvg press', 160, 110); @@ -2260,7 +2260,7 @@ GW.breakerPanel = function (host, opts = {}) { }; /* R41 DSKY — verb/noun command grammar with status lamps and a lamp-test verb */ -GW.dsky = function (host, opts = {}) { +DUPRE.dsky = function (host, opts = {}) { const onChange = opts.onChange || noop; const LAMPS = ['UPLINK', 'TEMP', 'GIMBAL', 'PROG', 'RESTART', 'OPR ERR']; const el = document.createElement('div'); el.className = 'dsky'; host.appendChild(el); @@ -2312,7 +2312,7 @@ GW.dsky = function (host, opts = {}) { /* R42 cam-timer program drum — the program is a ring; the pointer self-advances through it. Runs its own reduced-motion-gated 2 s step interval once started. */ -GW.camTimer = function (host, opts = {}) { +DUPRE.camTimer = function (host, opts = {}) { const onChange = opts.onChange || noop; const STEPS = opts.steps || ['OFF', 'FILL', 'WASH', 'WASH', 'WASH', 'RINSE', 'RINSE', 'DRAIN', 'SPIN', 'SPIN', 'FLUFF', 'COOL']; const COLS = opts.colors || { OFF: '#3a3426', FILL: '#54677d', WASH: '#d29638', RINSE: '#46b89e', DRAIN: '#969385', SPIN: '#cb6b4d', FLUFF: '#c9b98a', COOL: '#7c99b0' }; @@ -2348,7 +2348,7 @@ GW.camTimer = function (host, opts = {}) { }; /* R48 knife switch (side view) — the blade hinges at the left post and lands in the right jaw */ -GW.knifeSwitch = function (host, opts = {}) { +DUPRE.knifeSwitch = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 130, 110); gradDef('ksCu', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#d09a5c'], ['1', '#8a5a2a']]); @@ -2383,7 +2383,7 @@ GW.knifeSwitch = function (host, opts = {}) { }; /* R49 decade box — four skirted knobs, one digit each; the value is their sum */ -GW.decadeBox = function (host, opts = {}) { +DUPRE.decadeBox = function (host, opts = {}) { const onChange = opts.onChange || noop; const MUL = [1000, 100, 10, 1], LBL = ['x1000', 'x100', 'x10', 'x1']; const digs = (opts.digits || [3, 5, 0, 0]).slice(); @@ -2419,7 +2419,7 @@ GW.decadeBox = function (host, opts = {}) { }; /* R50 two-hand safety — one palm button arms a 500ms window; the other completes the cycle */ -GW.twoHandSafety = function (host, opts = {}) { +DUPRE.twoHandSafety = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 160, 100); svgEl(s, 'rect', { x: 2, y: 2, width: 156, height: 96, rx: 8, fill: '#1c1916', stroke: '#060505', 'stroke-width': 1.5 }); @@ -2462,7 +2462,7 @@ GW.twoHandSafety = function (host, opts = {}) { }; /* R51 voice-loop keyset — independent monitor states, exclusive talk, activity flicker */ -GW.voiceLoop = function (host, opts = {}) { +DUPRE.voiceLoop = function (host, opts = {}) { const onChange = opts.onChange || noop; const LOOPS = opts.loops || ['FD', 'GNC', 'ECOM', 'SURG', 'A/G', 'NET1', 'NET2', 'PAO']; const el = document.createElement('div'); el.className = 'vloop'; host.appendChild(el); @@ -2508,7 +2508,7 @@ GW.voiceLoop = function (host, opts = {}) { belongs to the instrument (peak-hold, history buffers) lives in here. */ /* 10 needle gauge — drag up/down sweeps the needle over a 120° arc */ -GW.needleGauge = function (host, opts = {}) { +DUPRE.needleGauge = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 50; const el = document.createElement('div'); el.className = 'gauge'; @@ -2529,7 +2529,7 @@ GW.needleGauge = function (host, opts = {}) { }; /* 11 stereo VU — two LED bars with peak-hold; set(l, r) drives both */ -GW.vuPair = function (host, opts = {}) { +DUPRE.vuPair = function (host, opts = {}) { const onChange = opts.onChange || noop; const n = opts.bars || 16; const el = document.createElement('div'); el.className = 'vu'; @@ -2558,7 +2558,7 @@ GW.vuPair = function (host, opts = {}) { }; /* 12 mini 4-bar signal — compact activity meter; set(level) */ -GW.miniSig = function (host, opts = {}) { +DUPRE.miniSig = function (host, opts = {}) { const onChange = opts.onChange || noop; const el = document.createElement('span'); el.className = 'sig'; el.innerHTML = '<i></i><i></i><i></i><i></i>'; @@ -2574,7 +2574,7 @@ GW.miniSig = function (host, opts = {}) { }; /* 13 signal ladder — stepped 0-4 strength; click cycles */ -GW.signalLadder = function (host, opts = {}) { +DUPRE.signalLadder = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 3; const el = document.createElement('span'); el.className = 'ladder'; @@ -2592,7 +2592,7 @@ GW.signalLadder = function (host, opts = {}) { }; /* 14 linear fuel bar — one 0-100 bar, warn tint under the threshold; drag to set */ -GW.fuelBar = function (host, opts = {}) { +DUPRE.fuelBar = function (host, opts = {}) { const onChange = opts.onChange || noop; const warnAt = opts.warnAt ?? 20; let v = opts.value ?? 50; @@ -2612,7 +2612,7 @@ GW.fuelBar = function (host, opts = {}) { }; /* 15 radial ring — percentage donut; drag up/down to set */ -GW.radialRing = function (host, opts = {}) { +DUPRE.radialRing = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 68; const el = document.createElement('span'); el.className = 'ring'; @@ -2631,7 +2631,7 @@ GW.radialRing = function (host, opts = {}) { }; /* 16 sparkline — rolling history trace; push(v) appends a sample, fill(v) levels it */ -GW.sparkline = function (host, opts = {}) { +DUPRE.sparkline = function (host, opts = {}) { const onChange = opts.onChange || noop; const n = opts.samples || 40; const hist = Array.from({ length: n }, () => opts.value ?? 0.5); @@ -2650,7 +2650,7 @@ GW.sparkline = function (host, opts = {}) { }; /* 17 waveform strip — sampled trace; set(samples, amp) with samples in -1..1 */ -GW.waveStrip = function (host, opts = {}) { +DUPRE.waveStrip = function (host, opts = {}) { const onChange = opts.onChange || noop; const el = document.createElement('span'); el.className = 'wave'; el.innerHTML = '<svg viewBox="0 0 170 38" preserveAspectRatio="none"><path fill="none" stroke="var(--gold)" stroke-width="1.2"/></svg>'; @@ -2671,7 +2671,7 @@ GW.waveStrip = function (host, opts = {}) { }; /* N11 oscilloscope — sampled phosphor trace; set(samples, vpp) with samples in -1..1 */ -GW.scope = function (host, opts = {}) { +DUPRE.scope = function (host, opts = {}) { const onChange = opts.onChange || noop; const el = document.createElement('span'); el.className = 'scope'; el.innerHTML = '<span class="grat"></span><svg viewBox="0 0 176 78" preserveAspectRatio="none"><polyline/></svg>'; @@ -2688,7 +2688,7 @@ GW.scope = function (host, opts = {}) { }; /* N12 spectrum / EQ bars — set(values) paints one column per band */ -GW.eqBars = function (host, opts = {}) { +DUPRE.eqBars = function (host, opts = {}) { const onChange = opts.onChange || noop; const bands = opts.bands || 11, cells = opts.cells || 9; const el = document.createElement('span'); el.className = 'eq'; @@ -2717,7 +2717,7 @@ GW.eqBars = function (host, opts = {}) { }; /* N13 crossed-needle meter — one drive value, FWD and RFL needles cross */ -GW.crossNeedle = function (host, opts = {}) { +DUPRE.crossNeedle = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 55; const el = document.createElement('div'); el.className = 'crossm'; @@ -2738,7 +2738,7 @@ GW.crossNeedle = function (host, opts = {}) { }; /* N14 thermometer column — mercury rises with the value */ -GW.thermometer = function (host, opts = {}) { +DUPRE.thermometer = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 58; const el = document.createElement('div'); el.className = 'thermo'; @@ -2757,7 +2757,7 @@ GW.thermometer = function (host, opts = {}) { }; /* N15 bourdon pressure gauge — needle over a printed arc with a redline */ -GW.bourdon = function (host, opts = {}) { +DUPRE.bourdon = function (host, opts = {}) { const onChange = opts.onChange || noop; let v = opts.value ?? 45; const el = document.createElement('div'); el.className = 'bourdon'; @@ -2778,7 +2778,7 @@ GW.bourdon = function (host, opts = {}) { /* N16 strip-chart recorder — scrolling history, pen rides the newest value; push(v) appends, set(samples, current) replaces the trace (values 0..1) */ -GW.stripChart = function (host, opts = {}) { +DUPRE.stripChart = function (host, opts = {}) { const onChange = opts.onChange || noop; const n = opts.samples || 60; const hist = Array.from({ length: n }, () => opts.value ?? 0.5); @@ -2802,7 +2802,7 @@ GW.stripChart = function (host, opts = {}) { }; /* N17 correlation meter — needle rests at 0 and swings ±; drag left/right */ -GW.corrMeter = function (host, opts = {}) { +DUPRE.corrMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; let p = opts.value ?? 58; const el = document.createElement('div'); el.className = 'corr'; @@ -2822,7 +2822,7 @@ GW.corrMeter = function (host, opts = {}) { }; /* N18 battery-cell gauge — charge as discrete cells, the low end warns */ -GW.battCells = function (host, opts = {}) { +DUPRE.battCells = function (host, opts = {}) { const onChange = opts.onChange || noop; const cells = opts.cells || 8, warnAt = opts.warnAt ?? 25; let p = opts.value ?? 62; @@ -2844,7 +2844,7 @@ GW.battCells = function (host, opts = {}) { /* R01 moving-coil VU — pivot below the face, authentic nonlinear dB scale; set(t) positions instantly (0..1.02); ballistics belong to the signal owner */ -GW.mcVu = function (host, opts = {}) { +DUPRE.mcVu = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 150, 96); const cx = 75, cy = 112, sweep = t => -43 + t * 86; @@ -2896,7 +2896,7 @@ GW.mcVu = function (host, opts = {}) { }; /* R07 round panel meter — porthole bezel, same VU law as R01; drag up/down */ -GW.roundMeter = function (host, opts = {}) { +DUPRE.roundMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 104); const cx = 55, fy = 54, py = 74, sweep = t => -40 + t * 80; @@ -2948,7 +2948,7 @@ GW.roundMeter = function (host, opts = {}) { }; /* R08 chrome MIN/MAX indicator — dark pointer over a brushed dome */ -GW.chromeMinMax = function (host, opts = {}) { +DUPRE.chromeMinMax = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 104); const cx = 55, cy = 58; @@ -2991,7 +2991,7 @@ GW.chromeMinMax = function (host, opts = {}) { }; /* R09 black-face aviation gauge — zone arcs per the airspeed-indicator scheme */ -GW.aviationGauge = function (host, opts = {}) { +DUPRE.aviationGauge = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg drag', 110, 104); const cx = 55, cy = 52, sweep = t => -135 + t * 270; @@ -3045,7 +3045,7 @@ GW.aviationGauge = function (host, opts = {}) { }; /* R13 edgewise strip meter — compressed log scale, the bar rides the edge */ -GW.edgeMeter = function (host, opts = {}) { +DUPRE.edgeMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; const SCALE = [[0, .08], [3, .26], [6, .44], [12, .62], [20, .78], [40, .94]]; const s = stageSvg(host, 'rsvg drag', 70, 130); @@ -3086,9 +3086,9 @@ GW.edgeMeter = function (host, opts = {}) { return { el: s, get: () => frac, set }; }; -/* R17 round CRT scope — pale phosphor face; the trace is the widget's own +/* R17 round CRT scope — pale phosphor face; the trace is the instrument's own animation, so it lives here with its reduced-motion gate */ -GW.roundCrt = function (host, opts = {}) { +DUPRE.roundCrt = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 110, 104); const cx = 55, cy = 52; @@ -3098,7 +3098,7 @@ GW.roundCrt = function (host, opts = {}) { const fl = svgEl(d, 'filter', { id: 'avGlow', x: '-60%', y: '-60%', width: '220%', height: '220%' }); svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.4 }); }); - /* the face gradient reads screen-family vars from this widget's subtree, + /* the face gradient reads screen-family vars from this instrument's subtree, so it must live in the local defs, not the shared def sink */ const defs = svgEl(s, 'defs', {}); const ph = svgEl(defs, 'radialGradient', { id: faceId, cx: '50%', cy: '44%', r: '70%' }); @@ -3139,7 +3139,7 @@ GW.roundCrt = function (host, opts = {}) { }; /* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */ -GW.attitudeIndicator = function (host, opts = {}) { +DUPRE.attitudeIndicator = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 130); const cx = 65, cy = 65; @@ -3194,8 +3194,8 @@ GW.attitudeIndicator = function (host, opts = {}) { }; /* R44 heading bug + servo needle — drag parks the command; the needle chases - with honest servo lag (widget-owned animation, reduced-motion gated) */ -GW.headingBug = function (host, opts = {}) { + with honest servo lag (instrument-owned animation, reduced-motion gated) */ +DUPRE.headingBug = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 130); const cx = 65, cy = 65; @@ -3245,8 +3245,8 @@ GW.headingBug = function (host, opts = {}) { }; /* R53 circular chart recorder — a day per revolution; the pen draws the cycle - (widget-owned clock, reduced-motion paints the full day once); click for fresh paper */ -GW.chartRecorder = function (host, opts = {}) { + (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper */ +DUPRE.chartRecorder = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 130, 130); const cx = 65, cy = 65; @@ -3296,7 +3296,7 @@ GW.chartRecorder = function (host, opts = {}) { }; /* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive */ -GW.verticalTape = function (host, opts = {}) { +DUPRE.verticalTape = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 70, 130); const CX = 38, CY = 65; @@ -3338,7 +3338,7 @@ GW.verticalTape = function (host, opts = {}) { }; /* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles */ -GW.twinNeedle = function (host, opts = {}) { +DUPRE.twinNeedle = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 120, 110); const cx = 60, cy = 62, R = 44; @@ -3406,7 +3406,7 @@ GW.twinNeedle = function (host, opts = {}) { }; /* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts */ -GW.comfortMeter = function (host, opts = {}) { +DUPRE.comfortMeter = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 130, 122); const cx = 65, cy = 60; @@ -3502,7 +3502,7 @@ GW.comfortMeter = function (host, opts = {}) { /* ================= indicators & readouts ================= */ /* 18 status lamps — one lamp per health state; click any lamp to cycle it */ -GW.statusLamps = function (host, opts = {}) { +DUPRE.statusLamps = function (host, opts = {}) { const onChange = opts.onChange || noop; const CLS = ['lamp', 'lamp gold', 'lamp red', 'lamp off', 'lamp busy']; const NAMES = ['ok', 'engaged', 'fault', 'off', 'busy']; @@ -3525,7 +3525,7 @@ GW.statusLamps = function (host, opts = {}) { }; /* 19 badges — labelled flags; click a badge to cycle its variant */ -GW.badges = function (host, opts = {}) { +DUPRE.badges = function (host, opts = {}) { const onChange = opts.onChange || noop; const CLS = ['badge', 'badge red', 'badge ghost']; const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]]; @@ -3550,7 +3550,7 @@ GW.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 */ -GW.tabularReadout = function (host, opts = {}) { +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>'; @@ -3569,7 +3569,7 @@ GW.tabularReadout = function (host, opts = {}) { }; /* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9 */ -GW.engravedLabel = function (host, opts = {}) { +DUPRE.engravedLabel = function (host, opts = {}) { const onChange = opts.onChange || noop; const e = document.createElement('span'); e.className = 'engrave'; e.append(opts.label || 'outputs'); @@ -3584,7 +3584,7 @@ GW.engravedLabel = function (host, opts = {}) { /* 22 output well — streaming step log, lamp per step; click streams the next demo step; push([lampCls, name, evidence]) appends programmatically */ -GW.outputWell = function (host, opts = {}) { +DUPRE.outputWell = function (host, opts = {}) { const onChange = opts.onChange || noop; const seed = opts.seed || [['', 'Link', 'wlp170s0 · @Hyatt'], ['gold', 'DNS', 'resolving…']]; const steps = opts.steps || [['gold', 'Probe', '8.8.8.8 …'], ['', 'Gateway', '10.0.0.1 ok'], ['', 'DNS', '1.1.1.1 ok'], ['red', 'Retry', 'timeout']]; @@ -3605,7 +3605,7 @@ GW.outputWell = function (host, opts = {}) { /* 23 toast — one-line transient confirmation; click fires the next demo message; fire(msg) shows any message with the fade-in */ -GW.toast = function (host, opts = {}) { +DUPRE.toast = function (host, opts = {}) { const onChange = opts.onChange || noop; const msgs = opts.msgs || ['link up · 300 Mbps', 'bt paired — WH-1000XM4', 'profile switched', 'joined @Hyatt_WiFi — saved']; const t = document.createElement('span'); t.className = 'toastw'; @@ -3622,7 +3622,7 @@ GW.toast = function (host, opts = {}) { }; /* 26 nixie tubes — one lit numeral per tube, leading zeros dark; click increments */ -GW.nixie = function (host, opts = {}) { +DUPRE.nixie = function (host, opts = {}) { const onChange = opts.onChange || noop; const digits = opts.digits || 2; const max = Math.pow(10, digits); @@ -3671,8 +3671,8 @@ GW.nixie = function (host, opts = {}) { setStyle(axis, name), chars, flapMs()/setFlapMs(ms) (base flap rate, clamped 20-400), onSettle(cb) — cb(reading) fires once per command when every cell has arrived (dwell hook). - CSS lives in the "split-flap" block of GW_CSS; no other styles involved. */ -GW.splitFlap = function (host, opts = {}) { + CSS lives in the "split-flap" block of DUPRE_CSS; no other styles involved. */ +DUPRE.splitFlap = function (host, opts = {}) { const onChange = opts.onChange || noop; const words = opts.words || ['DNS ', 'LINK', 'SYNC', 'WIFI', 'SCAN']; const animate = opts.animate !== undefined ? !!opts.animate : true; @@ -3802,7 +3802,7 @@ GW.splitFlap = function (host, opts = {}) { // construction axes (skin, font): swap this axis's classes per STYLES. // Polarity and typeface are construction, not accent — no colour policy. const setStyle = (axis, name) => { - const ax = GW.splitFlap.STYLES[axis]; const o = ax && ax[name]; + const ax = DUPRE.splitFlap.STYLES[axis]; const o = ax && ax[name]; if (!o) return; Object.values(ax).forEach(v => { if (v.cls) f.classList.remove(v.cls); }); if (o.cls) f.classList.add(o.cls); @@ -3818,7 +3818,7 @@ GW.splitFlap = function (host, opts = {}) { flapMs: () => flapMs, setFlapMs: ms => { flapMs = clampMs(ms); }, }; }; -GW.splitFlap.STYLES = { +DUPRE.splitFlap.STYLES = { // board polarity + ink: dark board with the kit's cream ink (default), // dark board with true-white ink, or the inverse ivory board // dot: the board colour; the gallery card letters each chip in the skin's @@ -3838,7 +3838,7 @@ GW.splitFlap.STYLES = { /* N21 seven-segment countdown — mm:ss in lit segments; the page drives tick(); click adds a minute */ -GW.sevenSeg = function (host, opts = {}) { +DUPRE.sevenSeg = function (host, opts = {}) { const onChange = opts.onChange || noop; const sv = document.createElement('span'); sv.className = 'seven'; host.appendChild(sv); let secs; @@ -3854,7 +3854,7 @@ GW.sevenSeg = function (host, opts = {}) { }; /* N22 VFD marquee — teal dot-matrix scroll; the page drives tick(); click cycles the message */ -GW.vfdMarquee = function (host, opts = {}) { +DUPRE.vfdMarquee = function (host, opts = {}) { const onChange = opts.onChange || noop; const msgs = opts.msgs || ['ARCHSETUP · NET OK · BT 2 · SND 62%', 'WIFI @Hyatt · 300 Mbps · VPN UP', 'BATTERY 84% · DISK 61% · TEMP 47C']; const W = opts.width || 176; @@ -3876,7 +3876,7 @@ GW.vfdMarquee = function (host, opts = {}) { /* N23 annunciator — named alarm grid with the raise → MSTR CAUTION → ACK → RESET lifecycle; TEST proves the bulbs then restores the board */ -GW.annunciator = function (host, opts = {}) { +DUPRE.annunciator = function (host, opts = {}) { const onChange = opts.onChange || noop; const CLS = ['acell', 'acell warn', 'acell fault']; const NAMES = ['ok', 'warn', 'fault']; @@ -3923,7 +3923,7 @@ GW.annunciator = function (host, opts = {}) { }; /* N24 jewel pilot lamps — faceted bezel indicators; click cycles red · amber · green · dark */ -GW.jewels = function (host, opts = {}) { +DUPRE.jewels = function (host, opts = {}) { const onChange = opts.onChange || noop; const cols = ['var(--jewel-r)', 'var(--jewel-a)', 'var(--jewel-g)']; const NAMES = ['red', 'amber', 'green']; @@ -3947,7 +3947,7 @@ GW.jewels = function (host, opts = {}) { }; /* N25 tape counter — odometer wheels; set(total) rolls each wheel to its digit */ -GW.tapeCounter = function (host, opts = {}) { +DUPRE.tapeCounter = function (host, opts = {}) { const onChange = opts.onChange || noop; const wheels = opts.wheels || 6, redFrom = opts.redFrom !== undefined ? opts.redFrom : 4; const ct = document.createElement('span'); ct.className = 'counter'; host.appendChild(ct); @@ -3971,7 +3971,7 @@ GW.tapeCounter = function (host, opts = {}) { /* N26 analog clock — engraved ticks + three hands; the page owns the time source and drives set(h, m, s); silent until the first set, like the live original */ -GW.analogClock = function (host, opts = {}) { +DUPRE.analogClock = function (host, opts = {}) { const onChange = opts.onChange || noop; const cl = document.createElement('span'); cl.className = 'clock'; for (let i = 0; i < 12; i++) { const t = document.createElement('span'); t.className = 'tk'; t.style.transform = `rotate(${i * 30}deg)`; cl.appendChild(t); } @@ -3989,7 +3989,7 @@ GW.analogClock = function (host, opts = {}) { /* N27 frequency-dial scale — printed log axis, marks crowd low and spread high; drag the pointer to tune */ -GW.freqScale = function (host, opts = {}) { +DUPRE.freqScale = function (host, opts = {}) { const onChange = opts.onChange || noop; const W = opts.width || 184; const marks = opts.marks || [0.5, 1, 2, 3, 5, 7, 10, 14, 20]; @@ -4021,7 +4021,7 @@ GW.freqScale = function (host, opts = {}) { /* N28 patch bay — jack grid with SVG cables; click one jack then another to patch or unpatch the pair; redraws itself on window resize */ -GW.patchBay = function (host, opts = {}) { +DUPRE.patchBay = function (host, opts = {}) { const onChange = opts.onChange || noop; const rows = opts.rows || 2, cols = opts.cols || 4; const patch = document.createElement('div'); patch.className = 'patch'; @@ -4065,7 +4065,7 @@ GW.patchBay = function (host, opts = {}) { /* R10 data matrix readout — a page of labeled amber fields; click cycles pages; a page marked live re-renders on the builder's own 1s clock (reduced-motion gated) */ -GW.dataMatrix = function (host, opts = {}) { +DUPRE.dataMatrix = function (host, opts = {}) { const onChange = opts.onChange || noop; const pages = opts.pages || [ ['SYS', () => ['CPU 42 MEM 61', 'DSK 58 TMP 47C', 'ARCH 6.18 LTS']], @@ -4081,7 +4081,7 @@ GW.dataMatrix = function (host, opts = {}) { const fl = svgEl(d, 'filter', { id: 'dmxGlow', x: '-30%', y: '-30%', width: '160%', height: '160%' }); svgEl(fl, 'feGaussianBlur', { in: 'SourceGraphic', stdDeviation: 1.1 }); }); - /* the background gradient reads screen-family vars from this widget's subtree, + /* the background gradient reads screen-family vars from this instrument's subtree, so it lives in the local defs, not the shared def sink */ const bgId = uid('dmxBg'); const defs = svgEl(s, 'defs', {}); @@ -4117,7 +4117,7 @@ GW.dataMatrix = function (host, opts = {}) { /* R11 warning flag window — the striped mechanical flag slides into the window when the condition trips; click trips and clears it */ -GW.warningFlag = function (host, opts = {}) { +DUPRE.warningFlag = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 120, 64); def('barber', d => { @@ -4146,7 +4146,7 @@ GW.warningFlag = function (host, opts = {}) { /* R25 fourteen-segment display — the starburst alphanumeric that spells words; click cycles the word */ -GW.seg14 = function (host, opts = {}) { +DUPRE.seg14 = function (host, opts = {}) { const onChange = opts.onChange || noop; const words = opts.words || ['ZOOM', 'ECHO', 'TAPE', 'MOOD', 'HALL', 'COMP']; const MAP = { @@ -4204,7 +4204,7 @@ GW.seg14 = function (host, opts = {}) { /* R26 response graph — log-frequency axes with a draggable amber peak; 2D drag places the peak in both axes at once */ -GW.responseGraph = function (host, opts = {}) { +DUPRE.responseGraph = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg', 190, 110); const RG = { x0: 30, x1: 182, y0: 8, y1: 86, fLo: Math.log10(32), fHi: Math.log10(16000), db: 12 }; @@ -4272,7 +4272,7 @@ GW.responseGraph = function (host, opts = {}) { /* R30 telegraph indicator — the pointer names the active state on labeled sectors, engine-telegraph style; click steps the state */ -GW.telegraphIndicator = function (host, opts = {}) { +DUPRE.telegraphIndicator = function (host, opts = {}) { const onChange = opts.onChange || noop; const states = opts.states || ['OK', 'TEST', 'BUSY', 'STOP', 'ATTN', 'FAULT']; const s = stageSvg(host, 'rsvg press', 110, 110), cx = 55, cy = 55, N = states.length; @@ -4305,7 +4305,7 @@ GW.telegraphIndicator = function (host, opts = {}) { /* R31 radar sweep — the beam circles the bearing ring on the builder's own clock (reduced-motion gated), contacts bloom as it passes; click marks the bearing */ -GW.radarSweep = function (host, opts = {}) { +DUPRE.radarSweep = function (host, opts = {}) { const onChange = opts.onChange || noop; const contacts = opts.contacts || [[60, .55], [205, .7], [318, .4]]; const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65; @@ -4379,7 +4379,7 @@ GW.radarSweep = function (host, opts = {}) { /* R35 day-date disc calendar — coaxial printed discs rotate so today reads under the fixed hand; starts on today; click rolls midnight forward */ -GW.dayDateCal = function (host, opts = {}) { +DUPRE.dayDateCal = function (host, opts = {}) { const onChange = opts.onChange || noop; const DAYS = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; const s = stageSvg(host, 'rsvg press', 130, 130), cx = 65, cy = 65, HAND = 135; @@ -4435,7 +4435,7 @@ GW.dayDateCal = function (host, opts = {}) { /* R36 LED dot matrix — 8x8 paintable bitmap behind a tinted window; click dots to paint; starts on the reference K */ -GW.dotMatrix = function (host, opts = {}) { +DUPRE.dotMatrix = function (host, opts = {}) { const onChange = opts.onChange || noop; const N = 8, STEP = 11, X0 = 10.5, Y0 = 10.5; const glyph = opts.glyph || ['10000100', '10001000', '10010000', '11100000', '10100000', '10010000', '10001000', '10000100']; @@ -4464,7 +4464,7 @@ GW.dotMatrix = function (host, opts = {}) { /* R45 flip-disc tile array — bistable mechanical pixels; scaleX flip with the color swap at the midpoint; click a disc to flip it */ -GW.flipDisc = function (host, opts = {}) { +DUPRE.flipDisc = function (host, opts = {}) { const onChange = opts.onChange || noop; const COLS = 7, ROWS = 5, STEP = 14, X0 = 13, Y0 = 13; const glyph = opts.glyph || ['0000100', '0000010', '1111111', '0000010', '0000100']; /* arrow right */ @@ -4493,7 +4493,7 @@ GW.flipDisc = function (host, opts = {}) { /* R46 dekatron counting ring — each click pulses the neon glow one cathode around; the wrap flashes the carry dot */ -GW.dekatron = function (host, opts = {}) { +DUPRE.dekatron = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 120, 120), cx = 60, cy = 60; svgEl(s, 'circle', { cx, cy, r: 52, fill: '#171310', stroke: '#060505', 'stroke-width': 2 }); @@ -4538,7 +4538,7 @@ GW.dekatron = function (host, opts = {}) { /* R47 landing gear indicator — three greens or nothing; the lever cycles down → transit (amber pulse) → up */ -GW.gearIndicator = function (host, opts = {}) { +DUPRE.gearIndicator = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 140, 110); svgEl(s, 'rect', { x: 2, y: 2, width: 136, height: 106, rx: 9, fill: '#1c1916', stroke: '#060505', 'stroke-width': 1.5 }); @@ -4589,7 +4589,7 @@ GW.gearIndicator = function (host, opts = {}) { /* R52 blinkenlights front panel — address/data lamps ripple with a live pseudo-PC (builder-owned clock, reduced-motion gated) that folds in the switch register; flip the SR bits and the pattern changes */ -GW.blinkenlights = function (host, opts = {}) { +DUPRE.blinkenlights = function (host, opts = {}) { const onChange = opts.onChange || noop; const s = stageSvg(host, 'rsvg press', 170, 100); svgEl(s, 'rect', { x: 2, y: 2, width: 166, height: 96, rx: 8, fill: '#3a1420', stroke: '#060505', 'stroke-width': 1.5 }); @@ -4637,9 +4637,9 @@ GW.blinkenlights = function (host, opts = {}) { return { el: s, get: () => sr, tick }; }; -/* ---- widget CSS: injected once, grows as builders move in ---- */ -/* widget-internal CSS (moved from the gallery <style> block; gallery keeps page furniture) */ -const GW_CSS = ` +/* ---- instrument CSS: injected once, grows as builders move in ---- */ +/* instrument-internal CSS (moved from the gallery <style> block; gallery keeps page furniture) */ +const DUPRE_CSS = ` /* ---- shared primitives ---- */ .lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);box-shadow:0 0 6px 1px rgba(116,147,47,.55)} .lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)} @@ -5161,7 +5161,7 @@ const GW_CSS = ` .patch svg{position:absolute;inset:0;pointer-events:none;width:100%;height:100%} .patch svg path{fill:none;stroke:var(--gold);stroke-width:2.4;opacity:.85;stroke-linecap:round} -/* ===== reference-batch (R) widgets — SVG-first, after period hardware ===== */ +/* ===== reference-batch (R) instruments — SVG-first, after period hardware ===== */ .rsvg{display:block} .rsvg.drag{cursor:ns-resize;touch-action:none} .rsvg.press{cursor:pointer} @@ -5171,44 +5171,44 @@ const GW_CSS = ` .reel.spin{animation:reelspin 2.6s linear infinite} `; function ensureCss() { - if (document.getElementById('gw-css') || !GW_CSS) return; - const st = document.createElement('style'); st.id = 'gw-css'; st.textContent = GW_CSS; + if (document.getElementById('dupre-css') || !DUPRE_CSS) return; + const st = document.createElement('style'); st.id = 'dupre-css'; st.textContent = DUPRE_CSS; document.head.appendChild(st); } if (document.head) ensureCss(); else document.addEventListener('DOMContentLoaded', ensureCss); -/* Policy classification (see GW.POLICIES). This is the colour pass's worklist made +/* Policy classification (see DUPRE.POLICIES). This is the colour pass's worklist made explicit: every card gets a record here as we review it — the kind it's bound by, why in its own terms, and the range it may vary and stay authentic. This round covers the cards already touched; the rest are unclassified on purpose, the review still to do card by card, not a memory dump to fill now. Assigned after the builders exist, in one place, so it reads as a catalogue. */ -GW.slideToggle.POLICY = { kind: 'accent', - why: 'On means good in one panel and a fault in the next, so the lit colour is the consumer’s claim, not the widget’s.', +DUPRE.slideToggle.POLICY = { kind: 'accent', + why: 'On means good in one panel and a fault in the next, so the lit colour is the consumer’s claim, not the instrument’s.', authentic: 'The on-colour from the accent family (red, amber, green, white, vfd), plus the pill and thumb finishes. The switch form stays.' }; -GW.segmented.POLICY = { kind: 'accent', +DUPRE.segmented.POLICY = { kind: 'accent', why: 'The active segment signals a mode whose meaning depends on the panel it sits in.', authentic: 'The accent colour of the active segment. The segment count and labels are the deployment’s, not a colour choice.' }; -GW.chipToggle.POLICY = { kind: 'accent', +DUPRE.chipToggle.POLICY = { kind: 'accent', why: 'A filter chip’s on-state reads as good, warning or fault entirely by where it is used.', authentic: 'The lit colour from the accent family. The chip keeps its inline weight and dotted underline.' }; -GW.dataMatrix.POLICY = { kind: 'screen', +DUPRE.dataMatrix.POLICY = { kind: 'screen', why: 'LED and LCD dot-matrix modules were sold in several emitter colours, so no one colour is definitive.', authentic: 'The screen family (amber, green, red, blue, vfd, white) — each a panel that was actually built. Not an arbitrary hue.' }; -GW.roundCrt.POLICY = { kind: 'screen', +DUPRE.roundCrt.POLICY = { kind: 'screen', why: 'CRT phosphors were manufactured in more than one colour (P1 green, P3 amber, white), so the trace colour is a real choice.', authentic: 'The screen family, limited to phosphors that existed. The face tint follows the phosphor.' }; -GW.waveRegion.POLICY = { kind: 'screen', +DUPRE.waveRegion.POLICY = { kind: 'screen', why: 'A backlit editor LCD was made in several tints; the ink colour is the panel’s, not fixed.', authentic: 'The screen family. The waveform and region handles recolour with the screen, staying legible on it.' }; -GW.radarSweep.POLICY = { kind: 'screen', +DUPRE.radarSweep.POLICY = { kind: 'screen', why: 'PPI radar scopes ran amber and green phosphors both; neither is the one true colour.', authentic: 'The screen family, phosphors that shipped. The sweep and afterglow track the chosen screen.' }; -GW.abcKeypad.POLICY = { kind: 'screen', +DUPRE.abcKeypad.POLICY = { kind: 'screen', why: 'The entry window is a screen like any other; its phosphor was made in several colours. (Mixed card: the keys are fixed-function and not a colour choice.)', authentic: 'The window’s screen family. The keycap colours are functional and stay put.' }; -Object.assign(GW, { SVGNS, svgEl, polar, dragX, dragY, dragDelta, SEG, seg7, buildBars, VUDB, vuDb, SCREEN_FAMS }); -window.GW = GW; +Object.assign(DUPRE, { SVGNS, svgEl, polar, dragX, dragY, dragDelta, SEG, seg7, buildBars, VUDB, vuDb, SCREEN_FAMS }); +window.DUPRE = DUPRE; })(); |
