diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-18 10:50:18 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-18 10:50:18 -0500 |
| commit | efd2e551dd727153463d22bd91d5cbe5484e7c3f (patch) | |
| tree | 919ae62ef4cb4982baf521649179c3fcb0037354 /docs/prototypes/panel-widget-gallery.html | |
| parent | b0db5e842d93564b1cb2e4b142ba0a0de1843fbd (diff) | |
| download | archsetup-efd2e551dd727153463d22bd91d5cbe5484e7c3f.tar.gz archsetup-efd2e551dd727153463d22bd91d5cbe5484e7c3f.zip | |
refactor(gallery): rename GW namespace to DUPRE, kit to The Dupre Kit
The gallery and its library are now The Dupre Kit, named for the dupre color theme. GW was too opaque to remember. The stylesheet id is now dupre-css, the card handle property is .dupre, and page prose says instrument instead of widget. File names stay put. Renaming them is a separate decision.
Historical log entries in todo.org keep their GW references as a record.
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 356 |
1 files changed, 178 insertions, 178 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 9eaf18a..0bf3ab8 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> -<title>Panel widget gallery — dupre instrument console (interactive)</title> +<title>The Dupre Kit — retro instrument console (interactive)</title> <style> :root{ /* @tokens:start */ @@ -72,8 +72,8 @@ h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} .toc .scr{position:absolute;width:5px;height:5px;border-radius:50%; background:radial-gradient(circle at 35% 30%,#524d43,#161310);box-shadow:0 .5px 0 rgba(255,255,255,.08)} html{scroll-behavior:smooth} -/* WIDGETS/ROW sets an explicit column count (1-4); the stage zoom scales with it - so a widget fills its column — fewer per row, bigger widgets. The card text +/* INSTRUMENTS/ROW sets an explicit column count (1-4); the stage zoom scales with it + so an instrument fills its column — fewer per row, bigger instruments. The card text stays at its own (slightly raised) size, untouched by the zoom. */ body[data-cols="1"] .grid{grid-template-columns:1fr} body[data-cols="2"] .grid{grid-template-columns:repeat(2,1fr)} @@ -83,7 +83,7 @@ body[data-cols="1"] .stagew{zoom:2.8} body[data-cols="2"] .stagew{zoom:2.1} body[data-cols="3"] .stagew{zoom:1.55} body[data-cols="4"] .stagew{zoom:1.15} -/* intrinsically tiny widgets get an extra bump so they do not float in an empty stage */ +/* intrinsically tiny instruments get an extra bump so they do not float in an empty stage */ .stagew.boost{zoom:1.5} body[data-cols="1"] .stagew.boost{zoom:4.2} body[data-cols="2"] .stagew.boost{zoom:3.15} @@ -208,17 +208,17 @@ a.no{text-decoration:none;cursor:pointer} <div class="wrap"> <header class="masthead"> <div class="eyebrow">archsetup · dupre panel family · live kit</div> - <h1>Widget gallery — the instrument-console kit, fully driveable</h1> + <h1>The Dupre Kit — the instrument console, fully driveable</h1> <div class="mastrow"> <div class="mastintro"> <p>Every control + display idiom in the dupre faceplate language, merged into one live console — the base kit, the candidate batch, and the growing reference batch (R: modeled on period hardware), all rendering from the same tokens the net / bt / sound panels use. <b>Controls</b> take input; <b>meters & gauges</b> show a live analog value; <b>indicators & - readouts</b> show state or a number. Every widget is operable: drag the faders, knobs and gauges; click the + readouts</b> show state or a number. Every instrument is operable: drag the faders, knobs and gauges; click the 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">widgets/row</span> + <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> @@ -240,7 +240,7 @@ a.no{text-decoration:none;cursor:pointer} <span class="scr" style="top:6px;left:6px"></span><span class="scr" style="top:6px;right:6px"></span> <span class="scr" style="bottom:6px;left:6px"></span><span class="scr" style="bottom:6px;right:6px"></span> <div class="tt">Policy</div> - <div class="pnote">What each widget's colour is bound by, and what it may change and stay true. Click a row to walk its cards; open a card's spec sheet for the reason and the authentic range.</div> + <div class="pnote">What each instrument's colour is bound by, and what it may change and stay true. Click a row to walk its cards; open a card's spec sheet for the reason and the authentic range.</div> <div class="vtally ptally" id="ptally"></div> </nav> </div> @@ -264,7 +264,7 @@ a.no{text-decoration:none;cursor:pointer} <script src="widgets.js"></script> <script> const $ = id => document.getElementById(id); -const {svgEl,polar,dragX,dragY,dragDelta,seg7,buildBars,VUDB,vuDb,SCREEN_FAMS}=GW; +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.body.dataset.cols=b.dataset.cols; @@ -275,7 +275,7 @@ document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',( 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));})(); -/* boost the intrinsically tiny widgets (audit: content under 10% of the stage) */ +/* boost the intrinsically tiny instruments (audit: content under 10% of the stage) */ const BOOST=['01','07','08','12','13','N14','18']; /* ---- palette: the named instrument colors, grouped by role. No codes shown — names and uses. ---- */ @@ -339,7 +339,7 @@ const PALETTE=[ host.appendChild(tile);}} })(); -/* ---- screen-color families: period-authentic alternates for the screen widgets ---- +/* ---- screen-color families: period-authentic alternates for the screen instruments ---- green = P1 CRT phosphor (radar/scopes; hi reuses --phos, graticule keeps the kit's scope green) amber = P3 phosphor / backlit amber (the gold family the kit already speaks) red = neon orange-red, the nixie's color pulled out as a family @@ -348,7 +348,7 @@ const PALETTE=[ white = monochrome LCD/CRT white Exploration data, deliberately NOT in tokens.json yet — families get tokenized once picked, the same way amber earned its tokens. Values keyed by the CSS custom property they drive. */ -/* SCREEN_FAMS lives in widgets.js (GW.SCREEN_FAMS) */ +/* SCREEN_FAMS lives in widgets.js (DUPRE.SCREEN_FAMS) */ function screenChips(no,el,fams,def){ const cardEl=el.closest('.card'); if(!cardEl)return; const row=document.createElement('div'); row.className='famchips'; @@ -364,9 +364,9 @@ function screenChips(no,el,fams,def){ cardEl.querySelector('.opts').appendChild(row); } /* ---- per-card spec sheets ---- - input how you drive it, incl. the model: click widgets port to every target (Emacs click-regions - included); drag widgets have no Emacs drag equivalent and need a click/key idiom there; - display widgets take no input in real use (gallery interaction is demo only). + input how you drive it, incl. the model: click instruments port to every target (Emacs click-regions + included); drag instruments have no Emacs drag equivalent and need a click/key idiom there; + display instruments take no input in real use (gallery interaction is demo only). period present only when the component is clearly not timeless — use it to keep a console period-consistent (swap a modern part for its historical counterpart or vice versa). */ const INFO={ @@ -1214,26 +1214,26 @@ function auditStop(){const p=$('vaudit');if(p)p.remove();} addEventListener('keydown',e=>{if(e.key==='Escape')auditStop();}); /* card(host, no, name, htmlOrBuild, note) — the declarative card record. htmlOrBuild: a legacy stage-HTML string, or a builder function (stage, rd) => handle - that instantiates a GW.* widget into the stage; rd(txt) writes the card readout. */ -/* Recover a card's builder from the arrow it was handed — the first GW.<name>( + that instantiates a DUPRE.* instrument into the stage; rd(txt) writes the card readout. */ +/* Recover a card's builder from the arrow it was handed — the first DUPRE.<name>( in the source — and read its POLICY record off it. Single source: the record - lives on GW.<builder>.POLICY in widgets.js and the card only displays it, so the + lives on DUPRE.<builder>.POLICY in widgets.js and the card only displays it, so the page can't drift from the code. Unclassified cards read '—', which is the point: the gallery becomes the colour pass's visible worklist. */ function cardPolicy(html){ if(typeof html!=='function') return null; - /* Assumes the FIRST GW.<name>( in the arrow is the builder — true because every - card is (st,rd)=>GW.builder(st,{...}) and helpers are destructured, called - bare. An arrow that referenced a second GW.<name>( before its builder would + /* Assumes the FIRST DUPRE.<name>( in the arrow is the builder — true because every + card is (st,rd)=>DUPRE.builder(st,{...}) and helpers are destructured, called + bare. An arrow that referenced a second DUPRE.<name>( before its builder would mis-derive; none do. A wrapped or renamed builder reads '—' rather than wrong, which is safe. */ - const m=html.toString().match(/GW\.(\w+)\s*\(/); - return (m && GW[m[1]] && GW[m[1]].POLICY) || null; + const m=html.toString().match(/DUPRE\.(\w+)\s*\(/); + return (m && DUPRE[m[1]] && DUPRE[m[1]].POLICY) || null; } function card(host, no, name, html, note){ const isBuild=typeof html==='function'; const P=cardPolicy(html), kind=P&&P.kind; - const meta=kind&&GW.POLICIES[kind]; + const meta=kind&&DUPRE.POLICIES[kind]; const free=meta&&meta.free; const polTip=meta?(meta.gist||''):'policy not yet classified'; const c=document.createElement('div'); c.className='card'; c.id='card-'+no; @@ -1264,7 +1264,7 @@ function card(host, no, name, html, note){ d.innerHTML=`<summary>spec sheet</summary><div class="igrid">${rows}</div>`; c.appendChild(d);} host.appendChild(c); - if(isBuild)c.gw=html(c.querySelector('.stagew'),txt=>setRd(no,txt)); + if(isBuild)c.dupre=html(c.querySelector('.stagew'),txt=>setRd(no,txt)); return c; } function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;} @@ -1290,376 +1290,376 @@ function updatePolTally(){ if(!hits.length)return; const i=PJUMP[g]++%hits.length; location.hash=''; location.hash=hits[i].id;})); } -/* drag helpers, seg7, buildBars live in widgets.js (GW) */ +/* drag helpers, seg7, buildBars live in widgets.js (DUPRE) */ /* ============ CONTROLS ============ */ const C=$('controls'); card(C,'01','Slide toggle', - (st,rd)=>GW.slideToggle(st,{on:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.slideToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>on / off, the touchscreen way.</b> The one born-digital control in the kit — the smartphone pill toggle the live net/bt/audio panels use. Click to flip; readout shows the state. Physical cousins: the bat-handle (R03) and the rocker (N01).'); card(C,'02','Console key', - (st,rd)=>GW.consoleKeys(st,{onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.consoleKeys(st,{onChange:(i,t)=>rd(t)}), '<b>physical push button.</b> Mutually exclusive — click one to engage. Green = live, terracotta = muted, gold = any other engaged key.'); card(C,'03','Horizontal fader', - (st,rd)=>GW.faderH(st,{value:68,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.faderH(st,{value:68,onChange:(v,t)=>rd(t)}), '<b>continuous 0-100.</b> Per-device volume, brightness. Drag the gold cap; readout tracks.'); card(C,'04','Vertical fader', (st,rd)=>{let ready=false; const upd=()=>{if(ready)rd('A '+Math.round(a.get())+' · B '+Math.round(b.get()));}; - const a=GW.faderV(st,{value:60,onChange:upd}); - const b=GW.faderV(st,{value:35,onChange:upd}); + const a=DUPRE.faderV(st,{value:60,onChange:upd}); + const b=DUPRE.faderV(st,{value:35,onChange:upd}); ready=true;upd();return {a,b};}, '<b>channel-strip style.</b> A mixer column per device. Drag each fader up or down.'); card(C,'05','Rotary knob', - (st,rd)=>GW.knob(st,{value:53,onChange:v=>rd('gain '+Math.round(v))}), + (st,rd)=>DUPRE.knob(st,{value:53,onChange:v=>rd('gain '+Math.round(v))}), '<b>dial in a value.</b> Volume/gain the analog way. Drag up/down to turn; readout shows the level.'); card(C,'06','Segmented selector', - (st,rd)=>GW.segmented(st,{onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.segmented(st,{onChange:(i,t)=>rd(t)}), '<b>pick one of a few.</b> Timer type, layout mode. Click a segment; readout names the choice. The lit segment ships in amber, green, or red — the accent chips below switch it.'); card(C,'07','Chip toggle', - (st,rd)=>GW.chipToggle(st,{on:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.chipToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>inline binary.</b> A soft toggle inside a line of text. Click to flip. The lit colour is the consumer\'s to pick from the accent family, because "on" is good news in one panel and a fault in the next — amber by default.'); card(C,'08','Arm-to-fire', - (st,rd)=>GW.armButton(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.armButton(st,{onChange:(v,t)=>rd(t)}), '<b>two-stage confirm.</b> Destructive actions. First click arms (red), second fires. Readout shows the stage.'); card(C,'09','Lamp row', - (st,rd)=>GW.lampRow(st,{onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.lampRow(st,{onChange:(i,t)=>rd(t)}), '<b>actionable list item.</b> Lamp + name + status. Click to cycle idle → connecting → connected.'); card(C,'24','Rotary selector', - (st,rd)=>GW.rotarySelector(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.rotarySelector(st,{onChange:(v,t)=>rd(t)}), '<b>pick one of N by position.</b> Printed detents, the pointer names the value. Click to turn; readout shows it.'); card(C,'25','Slide-rule dial', - (st,rd)=>GW.slideRule(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.slideRule(st,{onChange:(v,t)=>rd(t)}), '<b>value on a printed scale.</b> A lit pointer glides a warm-backlit strip. Printed numerals are the majors; the units between them carry minor ticks. Click any of them to jump, or focus and press ←/→ (↑/↓) to step a unit; readout shows it. Four faces — warm backlit, chrome, 80s black glass, marantz blue — on the chips below.'); card(C,'N01','Rocker power switch', - (st,rd)=>GW.rocker(st,{on:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.rocker(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>hard on / off, lit legend.</b> A master power paddle — the pressed half glows. Click to rock.'); card(C,'N02','Transport cluster', - (st,rd)=>GW.transport(st,{mode:'play',animate:!reduced,onChange:(m,t)=>rd(t)}), + (st,rd)=>DUPRE.transport(st,{mode:'play',animate:!reduced,onChange:(m,t)=>rd(t)}), '<b>run / pause / capture.</b> One mode lit at a time, reels turn while it plays. Click a button.'); card(C,'N03','Radio preset bank', - (st,rd)=>GW.presetBank(st,{onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.presetBank(st,{onChange:(i,t)=>rd(t)}), '<b>pick one, the rest release.</b> The car-radio preset row — mechanically exclusive. Click one.'); card(C,'N04','Concentric dual knob', - (st,rd)=>GW.dualKnob(st,{outer:50,inner:50,onChange:(o,i)=>rd('bass '+Math.round(o)+' · treble '+Math.round(i))}), + (st,rd)=>DUPRE.dualKnob(st,{outer:50,inner:50,onChange:(o,i)=>rd('bass '+Math.round(o)+' · treble '+Math.round(i))}), '<b>two values on one spindle.</b> Bass / treble, coarse / fine. Drag the outer ring or inner cap — each is independent.'); card(C,'N05','Rotary encoder + LED ring', - (st,rd)=>GW.encoder(st,{value:7,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.encoder(st,{value:7,onChange:(v,t)=>rd(t)}), '<b>endless dial, lit position.</b> A detentless encoder; the LED arc lights to the level and the value keeps accumulating. Drag to turn.'); card(C,'N06','Keyed mode switch', - (st,rd)=>GW.keySwitch(st,{index:1,onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.keySwitch(st,{index:1,onChange:(i,t)=>rd(t)}), '<b>guarded three-position mode.</b> Off · on · run. The key-bit points at the live position. Click to turn.'); card(C,'N07','Center-detented crossfader', - (st,rd)=>GW.crossfader(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.crossfader(st,{onChange:(v,t)=>rd(t)}), '<b>throw to either side of zero.</b> A fader notched at center — balance, L/R mix. Drag; readout shows the signed value.'); card(C,'N08','Thumbwheel', - (st,rd)=>GW.thumbwheel(st,{value:42,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.thumbwheel(st,{value:42,onChange:(v,t)=>rd(t)}), '<b>ribbed edge-wheel, one value.</b> A knurled wheel with a windowed number. Drag up/down to roll.'); card(C,'N09','DIP-switch bank', - (st,rd)=>GW.dipBank(st,{onChange:(w,t)=>rd(t)}), + (st,rd)=>DUPRE.dipBank(st,{onChange:(w,t)=>rd(t)}), '<b>a bank of hard flags.</b> Hardware slide switches — up is on. Click each to flip; readout is the binary word.'); card(C,'N10','Jog / shuttle wheel', - (st,rd)=>GW.jogWheel(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.jogWheel(st,{onChange:(v,t)=>rd(t)}), '<b>scrub fine, shuttle fast.</b> The edit-deck wheel. Drag to jog the inner wheel; readout shows the position.'); card(C,'R02','Calibrated vernier dial', - (st,rd)=>GW.vernierDial(st,{value:42.0,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.vernierDial(st,{value:42.0,onChange:(v,t)=>rd(t)}), '<b>precision on a rotating disc.</b> The big lab dial — the calibrated disc turns under a fixed hairline. Drag up/down; readout reads to a tenth. After a 1950s lab-oscillator vernier.'); card(C,'R03','Bat-handle toggle', - (st,rd)=>GW.batToggle(st,{on:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.batToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>the period power switch.</b> Chrome bat lever on a hex bushing, snaps up ON / down OFF. Click to throw it. After a lab-instrument panel toggle.'); card(C,'R04','Bakelite fluted knob', - (st,rd)=>GW.flutedKnob(st,{value:63,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.flutedKnob(st,{value:63,onChange:(v,t)=>rd(t)}), '<b>the skirted console knob.</b> Scalloped bakelite skirt, glossy dome, amber index over a printed 0-10 scale. Drag up/down to turn. After a vintage console mixer knob.'); card(C,'R05','Filter slider bank', - (st,rd)=>GW.filterBank(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.filterBank(st,{onChange:(v,t)=>rd(t)}), '<b>a wall of band faders.</b> Twelve bands on a screwed faceplate, dB rails both sides. Drag any cap; readout names band and level. Skins split three independent axes — panel (silver hi-fi after the Pioneer SG-9500 / studio black after the Technics SH-8065), cap shape (tall block fader after the Zaxcom Oasis / short ribbed / chrome T), and cap color (black white-index / red / green / blue / amber stripes / chrome / cream). The chips below mix them freely.'); card(C,'R06','Chicken-head selector', - (st,rd)=>GW.chickenHead(st,{index:2,onChange:(i,t)=>rd(t)}), + (st,rd)=>DUPRE.chickenHead(st,{index:2,onChange:(i,t)=>rd(t)}), '<b>the pointer-lever switch.</b> The tapered bakelite lever IS the indicator — it aims at the engraved position. Click to step through. After a modulator mode switch.'); card(C,'R12','Chrome slot fader', - (st,rd)=>GW.slotFader(st,{value:-4,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.slotFader(st,{value:-4,onChange:(v,t)=>rd(t)}), '<b>the console gain slide.</b> A chrome T-handle rides a recessed slot, the dB scale engraved on the plate — gain above zero, cut below. Drag; readout shows signed dB. After a vintage channel-strip output fader.'); card(C,'R14','Spade-pointer tuning knob', - (st,rd)=>GW.spadeKnob(st,{value:8.3,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.spadeKnob(st,{value:8.3,onChange:(v,t)=>rd(t)}), '<b>the pointer rides the knob.</b> A knurled bakelite knob carries a bright spade pointer over a scale engraved into the panel itself. Drag up/down to tune. After a 1920s radio receiver dial.'); card(C,'R15','Multi-band dial', - (st,rd)=>GW.multiBandDial(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.multiBandDial(st,{onChange:(v,t)=>rd(t)}), '<b>four scales, one needle.</b> Nested band arcs on a cream face; the active ring reads bold. Drag the left dial to tune; click the bandspread dial to switch bands. After a shortwave receiver dial pair.'); card(C,'R16','Entry keypad', - (st,rd)=>GW.entryKeypad(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.entryKeypad(st,{onChange:(v,t)=>rd(t)}), '<b>a composed entry instrument.</b> Worn keycaps feed the amber display; the lamp watches the buffer; the amber keys confirm and clear. Click the keys. After an industrial keypad prop.'); card(C,'R18','Thumb-slide attenuator pair', - (st,rd)=>GW.thumbSlide(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.thumbSlide(st,{onChange:(v,t)=>rd(t)}), '<b>a lit column of numbers, a thumb tab.</b> The scale glows inside the rail; the cream tab rides beside it and the nearest figure lights. Drag either channel. After a compressor blend/mix pair.'); card(C,'R19','Waveform region editor', - (st,rd)=>GW.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.waveRegion(st,{start:22,end:76,onChange:(v,t)=>rd(t)}), '<b>pick a region on the wave.</b> The monochrome LCD shows the sample; drag near either flag to move START or END, and the selection draws bright. After a sampler edit screen.'); card(C,'R20','Drum roller selector', - (st,rd)=>GW.drumRoller(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.drumRoller(st,{onChange:(v,t)=>rd(t)}), '<b>roll to the number.</b> The numbered drum shows through a tall window and the center value sits on an inverted chip. Drag either drum. After an equalizer tone selector.'); card(C,'R21','LED program row', - (st,rd)=>GW.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.ledRow(st,{index:5,onChange:(v,t)=>rd(t)}), '<b>identical keys, one lit dot.</b> The LED above the button carries the selection, not the key itself — the classic program-select row. Click a key; readout names the program. After a digital reverb’s program bank.'); card(C,'R22','Three-position slide', - (st,rd)=>GW.pillSlide(st,{index:1,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.pillSlide(st,{index:1,onChange:(v,t)=>rd(t)}), '<b>a detented pill, three stops.</b> The chrome pill parks at A, AB, or B, and the LED pair below tells the truth — both light on AB. Click a position. After a drum machine variation switch.'); card(C,'R23','Spun-aluminum knob', - (st,rd)=>GW.spunKnob(st,{value:62,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.spunKnob(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>bright metal, machined rings.</b> A spun face with concentric tooling marks in a knurled grip ring, red index over dot ticks. Drag up/down. After a boutique pedal knob, confirmed by an avionics cluster.'); card(C,'R24','Stomp switch + jewel', - (st,rd)=>GW.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.stompSwitch(st,{on:false,onChange:(v,t)=>rd(t)}), '<b>the engage pair.</b> A chrome dome stomp and the faceted jewel that reports it — press to engage, the jewel lights; press again to bypass. Click it. After a tremolo pedal footswitch.'); card(C,'R27','Winged gain selector', - (st,rd)=>GW.wingSelector(st,{index:7,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.wingSelector(st,{index:7,onChange:(v,t)=>rd(t)}), '<b>the red T-bar, stepped.</b> A bar-grip handle over a dot ring, snapping between labeled gain detents. Drag up/down to step it. After a classic mic-preamp gain switch.'); card(C,'R28','Rotary disc switch', - (st,rd)=>GW.discSwitch(st,{index:1,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.discSwitch(st,{index:1,onChange:(v,t)=>rd(t)}), '<b>the master power disc.</b> The whole red disc turns, grip bar molded in — a heavy three-position selector for OFF, ON, and COMBINE. Click to step. After a marine battery switch.'); card(C,'R29','Guarded toggle', - (st,rd)=>GW.guardedToggle(st,{on:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.guardedToggle(st,{on:true,onChange:(v,t)=>rd(t)}), '<b>friction sized to consequence.</b> Guard posts flank the lever and a red collar marks it critical — the avionics way to prevent an accidental throw. Click to throw it deliberately. After a flight-deck switch panel.'); card(C,'R32','Mechanical timer dial', - (st,rd)=>GW.timerDial(st,{minutes:0,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.timerDial(st,{minutes:0,onChange:(v,t)=>rd(t)}), '<b>turn to start, push to stop.</b> Drag the knurled dial to wind minutes onto the fixed red index; it winds back down to OFF on its own (demo runs a minute per second). Click the red knob to stop it dead. After a mechanical interval timer.'); card(C,'R33','Four-way rocker', - (st,rd)=>GW.rockerPad(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.rockerPad(st,{onChange:(v,t)=>rd(t)}), '<b>discrete direction, one press at a time.</b> Click an arrow (the whole quadrant is the target) to step that way; the readout tracks the cursor position it drives. After a remote-control navigation pad.'); card(C,'R34','Four-way toggle selector', - (st,rd)=>GW.fourWayToggle(st,{position:'C',onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.fourWayToggle(st,{position:'C',onChange:(v,t)=>rd(t)}), '<b>a lever that lives in one of four states.</b> Click a quadrant and the ball lever throws to A, B, C or D; the matching corner lamp takes the light. The rocker steps — this one selects. After a test-panel quadrant switch.'); card(C,'R37','Pin routing matrix', - (st,rd)=>GW.pinMatrix(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.pinMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>many-to-many routing you can read.</b> Sources are rows, destinations are columns; click an intersection to seat a pin and the route exists. Every simultaneous route stays visible. After the EMS VCS3 pin matrix.'); card(C,'R38','Dead-man button', - (st,rd)=>GW.deadMan(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.deadMan(st,{onChange:(v,t)=>rd(t)}), '<b>active only while held.</b> Press and hold — the ring arms, the lamp runs, the dwell clock counts. Let go anywhere and it drops to SAFE. The kit has toggles and momentaries; this is the only control whose state IS your grip.'); card(C,'R39','Rotary telephone dial', - (st,rd)=>GW.telephoneDial(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.telephoneDial(st,{onChange:(v,t)=>rd(t)}), '<b>numeric entry embodied as time.</b> Click a hole: the finger wheel winds to the stop and spins back, and the digit lands in the number. Bigger digits take longer, exactly like the pulses did. After a Western Electric dial.'); card(C,'R40','Circuit breaker panel', - (st,rd)=>GW.breakerPanel(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.breakerPanel(st,{onChange:(v,t)=>rd(t)}), '<b>the first control the system can throw.</b> Click a breaker to close or open it; hit TRIP and a fault pops one out to the tripped mid-position, amber collar showing. A tripped breaker must be pulled to reset before it closes again. After an aircraft breaker row.'); card(C,'R41','DSKY verb-noun panel', - (st,rd)=>GW.dsky(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.dsky(st,{onChange:(v,t)=>rd(t)}), '<b>a command grammar, not a button per command.</b> VERB is the action, NOUN is the operand: press VERB 3 5 ENTR for the lamp test. Wrong grammar lights OPR ERR. Two keys plus digits reach every function — the Apollo answer to a panel with too few buttons.'); card(C,'R42','Cam-timer program drum', - (st,rd)=>GW.camTimer(st,{position:0,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.camTimer(st,{position:0,onChange:(v,t)=>rd(t)}), '<b>a procedure as a rotating position.</b> The ring is the whole program — fill, wash, rinse, spin — and the pointer is where you are in it. Click to advance (or start from OFF); it then walks itself through the steps. After a washing-machine cam timer.'); card(C,'R48','Knife switch', - (st,rd)=>GW.knifeSwitch(st,{closed:true,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.knifeSwitch(st,{closed:true,onChange:(v,t)=>rd(t)}), '<b>the visible disconnect.</b> Copper blades either sit in their jaws or hang open in the air — you can SEE the circuit is broken, which is the entire safety argument. Click the handle to throw it. After early power-station switchboards.'); card(C,'R49','Decade switch box', - (st,rd)=>GW.decadeBox(st,{digits:[3,5,0,0],onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.decadeBox(st,{digits:[3,5,0,0],onChange:(v,t)=>rd(t)}), '<b>a number composed a digit at a time.</b> Four skirted knobs, one per decade — drag each to its digit and the total just IS their sum. The thumbwheel does one digit; this is how a lab built a precise value. After a General Radio decade resistance box.'); card(C,'R50','Two-hand safety control', - (st,rd)=>GW.twoHandSafety(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.twoHandSafety(st,{onChange:(v,t)=>rd(t)}), '<b>both hands, deliberately.</b> Click one palm button to arm the window, the other within half a second to cycle the press. Too slow — TIE-DOWN fault, start over. The hardware keeps hands out of the die; the grammar survives on screen. After OSHA press controls.'); card(C,'R51','Voice-loop keyset', - (st,rd)=>GW.voiceLoop(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.voiceLoop(st,{onChange:(v,t)=>rd(t)}), '<b>attention management, not selection.</b> Each loop is independent: click to monitor (green bar, flickering with activity), again to talk (amber, exclusive — one voice out), again to drop. Flight controllers ran a dozen of these at once. After a mission-control comm keyset.'); card(C,'R57','ABC entry keypad', - (st,rd)=>GW.abcKeypad(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.abcKeypad(st,{onChange:(v,t)=>rd(t)}), '<b>letters on a faceplate.</b> A-Z laid out alphabetically, not QWERTY — the industrial convention wherever the operator is not assumed to touch-type. Click the keys, or click the plate and type: it takes the keyboard once focused, and every key routes through the same handle a click does. DEL takes back one character, CLR wipes the lot, ENT commits. R16 enters digits; this is its alphanumeric sibling, and the only card in the kit that takes free text. After fleet and kiosk keypads: the membrane plate\'s colour-coded CLR / ENT, the stainless plate\'s letters-left arrangement and its backspace. Its CANCEL is dropped — that plate is a whole terminal with a transaction to abandon, where this is one control in a panel that owns its own dismiss.'); card(C,'R58','Index typewriter', - (st,rd)=>GW.indexPlate(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.indexPlate(st,{onChange:(v,t)=>rd(t)}), '<b>type without a keyboard.</b> One hand walks the stylus over a printed plate, the other pulls the lever, and only the lever prints — hunt as long as you like and nothing happens until the second hand moves. Select and commit on two separate controls, which no other card in the kit does. The plate is the AEG Mignon Model 4\'s own (1924), transcribed from the reference: both full cases so there is no shift key, the accents and fractions a keyboard skips, and two economies worth noticing. There is <b>no 1 and no 0</b> — you type them with lowercase l and capital O. And the inversion is positional, not by case: the capitals block is dark discs on a light panel and the lowercase block is its photographic negative, but J and j sit out on the halftone ring, un-inverted, with the punctuation.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); card(M,'10','Needle gauge', - (st,rd)=>GW.needleGauge(st,{value:50,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.needleGauge(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>analog dial.</b> Throughput, battery, volume. Drag up/down to sweep the needle; readout tracks. <b>Smooth sweep wants a Cairo/GTK area.</b>'); card(M,'11','Stereo VU (LED bar)', - (st,rd)=>GW.vuPair(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.vuPair(st,{onChange:(v,t)=>rd(t)}), '<b>live signal level.</b> The sound panel meter row, peak-hold outline. Runs a live signal; readout shows L/R.'); card(M,'12','Mini signal (4-bar)', - (st,rd)=>GW.miniSig(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.miniSig(st,{onChange:(v,t)=>rd(t)}), '<b>compact activity.</b> Per-row "is this device playing". Live signal; readout shows activity %.'); card(M,'13','Signal ladder', - (st,rd)=>GW.signalLadder(st,{value:3,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.signalLadder(st,{value:3,onChange:(v,t)=>rd(t)}), '<b>discrete strength.</b> Wifi bars, bt RSSI — a stepped 0-4. Click to cycle strength; readout shows bars.'); card(M,'14','Linear fuel bar', - (st,rd)=>{const a=GW.fuelBar(st,{value:72,onChange:(v,t)=>rd(t)}); - const b=GW.fuelBar(st,{value:40,onChange:(v,t)=>rd(t)}); + (st,rd)=>{const a=DUPRE.fuelBar(st,{value:72,onChange:(v,t)=>rd(t)}); + const b=DUPRE.fuelBar(st,{value:40,onChange:(v,t)=>rd(t)}); a.set(72);return {a,b};}, '<b>a single 0-100.</b> Battery, disk, download. Drag either bar; warn tint under threshold; readout tracks.'); card(M,'15','Radial ring', - (st,rd)=>GW.radialRing(st,{value:68,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.radialRing(st,{value:68,onChange:(v,t)=>rd(t)}), '<b>percentage as a donut.</b> CPU, battery. Drag up/down to set; conic-gradient tracks the readout.'); card(M,'16','Sparkline', - (st,rd)=>GW.sparkline(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.sparkline(st,{onChange:(v,t)=>rd(t)}), '<b>recent history.</b> Throughput/CPU over the last minute. Live trace; readout shows the current value.'); card(M,'17','Waveform strip', - (st,rd)=>GW.waveStrip(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.waveStrip(st,{onChange:(v,t)=>rd(t)}), '<b>audio waveform / scope.</b> A richer signal view. Live trace; readout shows amplitude. <b>Needs a drawing surface.</b>'); card(M,'N11','Oscilloscope', - (st,rd)=>GW.scope(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.scope(st,{onChange:(v,t)=>rd(t)}), '<b>a live phosphor trace.</b> A waveform swept over a graticule. Live; readout shows Vpp. <b>Needs a Cairo/GTK area for a smooth trace.</b>'); card(M,'N12','Spectrum / EQ bars', - (st,rd)=>GW.eqBars(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.eqBars(st,{onChange:(v,t)=>rd(t)}), '<b>level across bands.</b> A live multi-band bar graph — spectrum, per-core CPU. Live; readout shows the peak band.'); card(M,'N13','Crossed-needle meter', - (st,rd)=>GW.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.crossNeedle(st,{value:55,onChange:(v,t)=>rd(t)}), '<b>two readings, one face.</b> Forward + reflected needles cross. Drag up/down to drive power; readout shows both. <b>Round build wants a drawing surface.</b>'); card(M,'N14','Thermometer column', - (st,rd)=>GW.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.thermometer(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>a rising column.</b> The classic mercury tube — temperature, a climbing load. Drag up/down; readout shows the value.'); card(M,'N15','Bourdon pressure gauge', - (st,rd)=>GW.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.bourdon(st,{value:45,onChange:(v,t)=>rd(t)}), '<b>a round dial with a red zone.</b> Needle over a printed arc, redline in terracotta. Drag to set PSI; readout tracks. <b>Round sweep is a drawing-area job.</b>'); card(M,'N16','Strip-chart recorder', - (st,rd)=>GW.stripChart(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.stripChart(st,{onChange:(v,t)=>rd(t)}), '<b>a scrolling pen trace.</b> History scrolls left, the pen writes at the right edge. Live; readout shows the current value. <b>Real trace wants a drawing surface.</b>'); card(M,'N17','Correlation meter (±)', - (st,rd)=>GW.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.corrMeter(st,{value:58,onChange:(v,t)=>rd(t)}), '<b>deviation from center.</b> The needle rests at 0 and swings ±. Drag left/right; readout shows the signed value.'); card(M,'N18','Battery-cell gauge', - (st,rd)=>GW.battCells(st,{value:62,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.battCells(st,{value:62,onChange:(v,t)=>rd(t)}), '<b>charge as discrete cells.</b> Filled cells count up, the last warn in terracotta. Drag left/right to set; readout shows %.'); card(M,'R01','Moving-coil VU meter', - (st,rd)=>GW.mcVu(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.mcVu(st,{onChange:(v,t)=>rd(t)}), '<b>the classic cream-faced VU.</b> Printed dB arc, red zone past 0, ballistic needle. Runs the live signal; readout shows VU. After a vintage console meter pair.'); card(M,'R07','Round panel meter', - (st,rd)=>GW.roundMeter(st,{value:50,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.roundMeter(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>the porthole lab meter.</b> Cream dial in a round black bezel, printed dB arc, black needle. Drag up/down to drive it; readout shows dB. After a modulator front meter.'); card(M,'R08','Chrome MIN/MAX indicator', - (st,rd)=>GW.chromeMinMax(st,{value:50,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.chromeMinMax(st,{value:50,onChange:(v,t)=>rd(t)}), '<b>the hi-fi trim window.</b> Polished chrome ring, brushed dome, a dark pointer sweeping MIN to MAX over the metal. Drag up/down; readout shows the position. After a hi-fi amplifier level window.'); card(M,'R09','Black-face aviation gauge', - (st,rd)=>GW.aviationGauge(st,{value:52,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.aviationGauge(st,{value:52,onChange:(v,t)=>rd(t)}), '<b>amber on black, zone-marked.</b> The cockpit round: amber ticks and numerals on a black face, painted caution and limit sectors, amber needle. Drag up/down; readout tracks. After a flight-instrument cluster.'); card(M,'R13','Edgewise strip meter', - (st,rd)=>GW.edgeMeter(st,{value:.62,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.edgeMeter(st,{value:.62,onChange:(v,t)=>rd(t)}), '<b>a needle on a vertical scale.</b> The parchment strip reads attenuation down a compressed log scale; the bar rides its edge. Drag up/down; readout shows dB. After a channel-strip gain-reduction meter.'); card(M,'R43','Attitude indicator', - (st,rd)=>GW.attitudeIndicator(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.attitudeIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>orientation, not a number.</b> The horizon rolls and shifts behind the fixed amber airplane — two axes in one read. Drag it: left/right banks, up/down pitches. The first two-axis instrument in the kit. After a cockpit artificial horizon.'); card(M,'R44','Heading bug + servo needle', - (st,rd)=>GW.headingBug(st,{value:90,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.headingBug(st,{value:90,onChange:(v,t)=>rd(t)}), '<b>commanded versus actual on one dial.</b> Drag to park the amber bug where you want to go; the needle chases it with honest servo lag. The gap between them IS the information. After an HSI heading bug and synchro repeaters.'); card(M,'R54','Vertical tape instrument', - (st,rd)=>GW.verticalTape(st,{value:24,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.verticalTape(st,{value:24,onChange:(v,t)=>rd(t)}), '<b>the scale moves, the index does not.</b> A scrolling tape behind a fixed amber index — your value is always at eye level and the neighborhood above and below stays visible. Drag to spin the tape. After the Ki-57 remote tachometer; modern cockpits tape everything.'); card(M,'R55','Twin-needle gauge', - (st,rd)=>GW.twinNeedle(st,{fuel:2.4,oil:3.1,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.twinNeedle(st,{fuel:2.4,oil:3.1,onChange:(v,t)=>rd(t)}), '<b>two values, one housing, mirrored scales.</b> Fuel presses up the left arc, oil up the right, each with its own needle from the shared hub. Drag either half to set that side. Not the crossed-needle (N13) — these are read separately, not at their intersection. After the Ki-57 fuel/oil pressure gauge.'); card(M,'R56','Comfort-zone crossed needles', - (st,rd)=>GW.comfortMeter(st,{temp:72,humidity:45,onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.comfortMeter(st,{temp:72,humidity:45,onChange:(v,t)=>rd(t)}), '<b>two values, one verdict.</b> Temperature and humidity needles cross over a face printed with judgments — the crossing point lands in TOO WARM, TOO DRY, or JUST RIGHT. N13 derives a number from the crossing; this one derives an opinion. Drag each half. After a brass weather-station comfort meter.'); card(M,'R53','Circular chart recorder', - (st,rd)=>GW.chartRecorder(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.chartRecorder(st,{onChange:(v,t)=>rd(t)}), '<b>cyclic time on round paper.</b> The pen sweeps a full day per revolution, so "same time yesterday" is the same angle and daily rhythm draws itself as a flower. Click for fresh paper. The strip chart shows a window; this shows the cycle. After a Partlow round-chart recorder.'); card(M,'R17','Round CRT scope', - (st,rd)=>GW.roundCrt(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.roundCrt(st,{onChange:(v,t)=>rd(t)}), '<b>the round-tube scope.</b> Pale phosphor face in a screwed porthole bezel, dark graticule, a bright live trace. Runs live; readout shows Vpp. After a 1950s bench oscilloscope.'); /* ============ INDICATORS & READOUTS ============ */ const I=$('indicators'); card(I,'18','Status lamp', - (st,rd)=>GW.statusLamps(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.statusLamps(st,{onChange:(v,t)=>rd(t)}), '<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. Click any lamp to cycle its state.'); card(I,'19','Badge / tag', - (st,rd)=>GW.badges(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.badges(st,{onChange:(v,t)=>rd(t)}), '<b>a labelled flag.</b> MUTED, AIRPLANE, a band tag. Click a badge to cycle its variant.'); card(I,'20','Tabular readout', - (st,rd)=>GW.tabularReadout(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.tabularReadout(st,{onChange:(v,t)=>rd(t)}), '<b>a precise number.</b> Clock, countdown, volume %. Runs a live countdown; click to pause/resume.'); card(I,'21','Engraved label', - (st,rd)=>GW.engravedLabel(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.engravedLabel(st,{onChange:(v,t)=>rd(t)}), '<b>section divider.</b> The hairline-flanked caps label with a count. Click to bump the count.'); card(I,'22','Output well', - (st,rd)=>GW.outputWell(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.outputWell(st,{onChange:(v,t)=>rd(t)}), '<b>streaming step log.</b> Lamp-per-step with evidence. Click to stream the next step.'); card(I,'23','Toast / status line', - (st,rd)=>GW.toast(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.toast(st,{onChange:(v,t)=>rd(t)}), '<b>transient confirmation.</b> The one-line result after an action. Click to fire the next toast.'); card(I,'26','Nixie tube', - (st,rd)=>GW.nixie(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.nixie(st,{onChange:(v,t)=>rd(t)}), '<b>a single warm-glowing numeral.</b> One lit digit per tube, leading zeros dark. Click to increment the count.'); card(I,'N20','Split-flap display', - (st)=>GW.splitFlap(st,{rows:3,cells:6,words:['SYSTEM','VOLUME','SIGNAL','RECORD','ONLINE','STEREO','STATUS','FILTER','TUNING','NEEDLE','SWITCH','TOGGLE','DIMMER','CHROME','MODULE','OUTPUT','SCREEN','COPPER','DYNAMO','PISTON','MAGNET','BEACON','RADIAL','BRIDGE'],animate:!reduced,skin:'paper',font:'helv'}), + (st)=>DUPRE.splitFlap(st,{rows:3,cells:6,words:['SYSTEM','VOLUME','SIGNAL','RECORD','ONLINE','STEREO','STATUS','FILTER','TUNING','NEEDLE','SWITCH','TOGGLE','DIMMER','CHROME','MODULE','OUTPUT','SCREEN','COPPER','DYNAMO','PISTON','MAGNET','BEACON','RADIAL','BRIDGE'],animate:!reduced,skin:'paper',font:'helv'}), '<b>flips to the new value.</b> A three-row, six-cell board; every advance sends each row to a different random word from the pool, and each cell runs at its own mechanical rate, so letters and whole words finish at different times. After settling it dwells two seconds, then advances. Click to advance now. Skins: white cards with dark lettering (default), white ink on black, the kit cream, or the ivory board; face in Helvetica (default) or Berkeley Mono. The slider sets the flap rate.'); card(I,'N21','Seven-segment display', - (st,rd)=>GW.sevenSeg(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.sevenSeg(st,{onChange:(v,t)=>rd(t)}), '<b>a lit-segment number.</b> Green segments, distinct from the nixie glow. Live countdown; click to add a minute.'); card(I,'N22','VFD marquee', - (st,rd)=>GW.vfdMarquee(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.vfdMarquee(st,{onChange:(v,t)=>rd(t)}), '<b>scrolling status line.</b> Teal glow, dot-matrix mesh, text tracks across. Scrolls live; click to change the message.'); card(I,'N23','Annunciator panel', - (st,rd)=>GW.annunciator(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.annunciator(st,{onChange:(v,t)=>rd(t)}), '<b>a grid of named alarms with the alarm lifecycle.</b> Cells light amber to warn, red to fault; any active alarm flashes MSTR CAUTION until ACK steadies it, a new alarm re-flashes it, RESET clears the board, TEST proves the bulbs. Click cells to raise alarms.'); card(I,'N24','Jewel pilot lamps', - (st,rd)=>GW.jewels(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.jewels(st,{onChange:(v,t)=>rd(t)}), '<b>a faceted glass indicator.</b> The chunky bezel pilot lamp with a real bloom. Click a jewel to cycle red · amber · green · dark.'); card(I,'N25','Tape counter', - (st,rd)=>GW.tapeCounter(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.tapeCounter(st,{onChange:(v,t)=>rd(t)}), '<b>rolling odometer digits.</b> Numbered wheels roll to the next figure. Rolls live; readout shows the total.'); card(I,'N26','Analog clock', - (st,rd)=>GW.analogClock(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.analogClock(st,{onChange:(v,t)=>rd(t)}), '<b>time on a real face.</b> Engraved ticks, three hands, live seconds sweep. Runs live; readout shows the time.'); card(I,'N27','Frequency-dial scale', - (st,rd)=>GW.freqScale(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.freqScale(st,{onChange:(v,t)=>rd(t)}), '<b>a printed log scale.</b> Marks crowd low, spread high — a real logarithmic axis. Drag the pointer; readout shows the frequency.'); card(I,'N28','Patch-bay jack field', - (st,rd)=>GW.patchBay(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.patchBay(st,{onChange:(v,t)=>rd(t)}), '<b>routing shown as patches.</b> A grid of jacks with cables between connected pairs. Click one jack then another to add/remove a cable.'); card(I,'R10','Data matrix readout', - (st,rd)=>GW.dataMatrix(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.dataMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>a page of amber fields.</b> The console data block — several labeled values on one lit matrix. Click to cycle pages; readout names the page. After a cockpit data readout.'); card(I,'R11','Warning flag window', - (st,rd)=>GW.warningFlag(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.warningFlag(st,{onChange:(v,t)=>rd(t)}), '<b>the mechanical alarm flag.</b> A striped barber-pole slides into a window when the condition trips — not a lamp, a flag. Click to trip and clear it. After the VIB flag on an altimeter.'); card(I,'R25','Fourteen-segment display', - (st,rd)=>GW.seg14(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.seg14(st,{onChange:(v,t)=>rd(t)}), '<b>segments that spell.</b> The starburst alphanumeric — diagonals and split bars let it write words, not just digits. Click to cycle the word. After a pedal preset display.'); card(I,'R26','Response graph', - (st,rd)=>GW.responseGraph(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.responseGraph(st,{onChange:(v,t)=>rd(t)}), '<b>a curve on labeled axes.</b> Log frequency across, dB up the side, and the amber peak is yours to place — drag it anywhere on the plot. After the response display of a mastering processor.'); card(I,'R30','Telegraph indicator', - (st,rd)=>GW.telegraphIndicator(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.telegraphIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>state on a pie of sectors.</b> The pointer and its little flag name the active state, engine-telegraph style. Click to step the state. After an electric indicator dial.'); card(I,'R31','Radar sweep', - (st,rd)=>GW.radarSweep(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.radarSweep(st,{onChange:(v,t)=>rd(t)}), '<b>the rotating scan.</b> A sweep beam circles the bearing ring, contacts bloom as it passes and fade after. Click to mark the current bearing. After a radar PPI scope.'); card(I,'R35','Day-date disc calendar', - (st,rd)=>GW.dayDateCal(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.dayDateCal(st,{onChange:(v,t)=>rd(t)}), '<b>two coaxial discs, one index.</b> The outer ring carries the date, the inner disc the weekday; both read where the yellow hand points. Starts on today; click to roll midnight forward and watch the discs step. After a watch day-date movement.'); card(I,'R36','LED dot matrix', - (st,rd)=>GW.dotMatrix(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.dotMatrix(st,{onChange:(v,t)=>rd(t)}), '<b>a bitmap, one red dot at a time.</b> 64 discrete LEDs behind a tinted window — segments draw digits, but a matrix draws anything. Click dots to paint the pattern; the readout counts what is lit. After the K4816 pattern generator faceplate.'); card(I,'R45','Flip-disc tile array', - (st,rd)=>GW.flipDisc(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.flipDisc(st,{onChange:(v,t)=>rd(t)}), '<b>the mechanical pixel.</b> Each disc flips between its yellow face and its black back and then STAYS — no power, no glow, state that persists. Click a disc and watch it flip. The dot matrix glows; this one clacks. After transit flip-dot signs.'); card(I,'R46','Dekatron counting ring', - (st,rd)=>GW.dekatron(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.dekatron(st,{onChange:(v,t)=>rd(t)}), '<b>a count as a position, and the counting is visible.</b> Each click is a pulse; the neon glow steps one cathode around the ring and carries on wrap. Nixies show the digit — the Dekatron shows the arithmetic. After glow-transfer counter tubes.'); card(I,'R47','Landing gear indicator', - (st,rd)=>GW.gearIndicator(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.gearIndicator(st,{onChange:(v,t)=>rd(t)}), '<b>three greens or nothing.</b> A spatial lamp panel: nose and mains each get a light, green only when down and locked; the amber transit pulse means neither state. Click the wheel lever to cycle the gear. After a cockpit gear panel.'); card(I,'R52','Blinkenlights front panel', - (st,rd)=>GW.blinkenlights(st,{onChange:(v,t)=>rd(t)}), + (st,rd)=>DUPRE.blinkenlights(st,{onChange:(v,t)=>rd(t)}), '<b>the machine thinking, out loud.</b> Address and data lamps ripple with the live computation; the switch register below is yours — flip bits and watch the pattern change. DIP switches hold config; this shows work happening. After a PDP-11/70 front panel.'); /* ============ CONTROL WIRING ============ */ /* 01 toggle */ -/* 01-09, 24, 25, N01-N10 extracted to widgets.js (GW.*) — wired at their card records */ +/* 01-09, 24, 25, N01-N10 extracted to widgets.js (DUPRE.*) — wired at their card records */ /* ============ METER WIRING (drag-driven) ============ */ -/* 10-17, N11-N18 extracted to widgets.js (GW.*) — wired at their card records */ +/* 10-17, N11-N18 extracted to widgets.js (DUPRE.*) — wired at their card records */ /* ============ INDICATOR WIRING ============ */ -/* 18-23, 26 extracted to widgets.js (GW.*) — wired at their card records */ -/* N20-N28 extracted to widgets.js (GW.*) — wired at their card records */ +/* 18-23, 26 extracted to widgets.js (DUPRE.*) — wired at their card records */ +/* N20-N28 extracted to widgets.js (DUPRE.*) — wired at their card records */ -/* ============ REFERENCE-BATCH (R) WIDGETS — SVG builders ============ */ -/* svgEl, polar, VUDB live in widgets.js (GW) */ +/* ============ REFERENCE-BATCH (R) INSTRUMENTS — SVG builders ============ */ +/* svgEl, polar, VUDB live in widgets.js (DUPRE) */ /* R01 ballistics: the page owns the signal chase; the meter just takes set(t) */ function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12); const t=MH.mcvu.get();MH.mcvu.set(t+(target-t)*(target>t?0.32:0.10));} -/* R02-R06, R12, R14, R15 extracted to widgets.js (GW.*) */ +/* R02-R06, R12, R14, R15 extracted to widgets.js (DUPRE.*) */ @@ -1669,26 +1669,26 @@ function tickMcVu(){const a=lvl();const target=Math.min(1.02,a*1.12); -/* R07-R09 extracted to widgets.js (GW.*) */ +/* R07-R09 extracted to widgets.js (DUPRE.*) */ -/* R10, R11 extracted to widgets.js (GW.*) */ +/* R10, R11 extracted to widgets.js (DUPRE.*) */ -/* R13, R17 extracted to widgets.js (GW.*) */ +/* R13, R17 extracted to widgets.js (DUPRE.*) */ -/* R25, R26, R30, R31 extracted to widgets.js (GW.*) */ +/* R25, R26, R30, R31 extracted to widgets.js (DUPRE.*) */ /* ============ LIVE SIGNAL LOOPS ============ */ /* extracted meters: the page keeps the clock + demo signal, drives card handles */ -const MH={vu:$('card-11').gw,mini:$('card-12').gw,spark:$('card-16').gw,wave:$('card-17').gw, - scope:$('card-N11').gw,eq:$('card-N12').gw,strip:$('card-N16').gw,mcvu:$('card-R01').gw}; -const IH={flap:$('card-N20').gw,seven:$('card-N21').gw,vfd:$('card-N22').gw, - counter:$('card-N25').gw,clock:$('card-N26').gw}; +const MH={vu:$('card-11').dupre,mini:$('card-12').dupre,spark:$('card-16').dupre,wave:$('card-17').dupre, + scope:$('card-N11').dupre,eq:$('card-N12').dupre,strip:$('card-N16').dupre,mcvu:$('card-R01').dupre}; +const IH={flap:$('card-N20').dupre,seven:$('card-N21').dupre,vfd:$('card-N22').dupre, + counter:$('card-N25').dupre,clock:$('card-N26').dupre}; let ph=0; const eqBands=Array.from({length:11},(_,i)=>({v:0.4,ph:i*0.6})); @@ -1716,7 +1716,7 @@ const FLAP_DWELL_MS=2000; let flapTimer; // a fixed interval would retarget mid-cascade and the panel would never rest. if(!reduced)IH.flap.onSettle(()=>{clearTimeout(flapTimer);flapTimer=setTimeout(()=>IH.flap.next(),FLAP_DWELL_MS);}); function tickSeven(){IH.seven.tick();} -function tickT(){$('card-20').gw.tick();} +function tickT(){$('card-20').dupre.tick();} function tickVfd(){IH.vfd.tick();} function tickCounter(){IH.counter.set(IH.counter.get()+137);} @@ -1748,29 +1748,29 @@ if(!reduced){ document.querySelectorAll('.reel.spin').forEach(r=>r.style.animationPlayState='paused'); } -/* R35, R36 extracted to widgets.js (GW.*) */ +/* R35, R36 extracted to widgets.js (DUPRE.*) */ -/* R43, R44 extracted to widgets.js (GW.*) */ +/* R43, R44 extracted to widgets.js (DUPRE.*) */ -/* R45-R47, R52 extracted to widgets.js (GW.*) */ +/* R45-R47, R52 extracted to widgets.js (DUPRE.*) */ -/* R53-R56 extracted to widgets.js (GW.*) */ +/* R53-R56 extracted to widgets.js (DUPRE.*) */ -/* ---- screen-family chips: first round, the clearly-screen widgets ---- - Defaults match each widget's shipped color (var fallbacks), so no family is applied until a chip is clicked. +/* ---- screen-family chips: first round, the clearly-screen instruments ---- + Defaults match each instrument's shipped color (var fallbacks), so no family is applied until a chip is clicked. The nixie stays chipless on purpose: neon is only ever orange, so alternates would be historically false. */ -screenChips('R10',$('card-R10').gw.el,['amber','green','red','blue','vfd'],'amber'); -screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'green'); -screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white'); -screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber'); -screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green'); +screenChips('R10',$('card-R10').dupre.el,['amber','green','red','blue','vfd'],'amber'); +screenChips('R17',$('card-R17').dupre.el,['green','amber','red','blue','vfd'],'green'); +screenChips('R19',$('card-R19').dupre.el,['white','green','amber','blue','vfd'],'white'); +screenChips('R31',$('card-R31').dupre.el,['amber','green','red','blue','vfd'],'amber'); +screenChips('N11',$('card-N11').dupre.el,['green','amber','red','blue','vfd'],'green'); /* all six, where its siblings each carry five: a passphrase window has no reason to prefer one phosphor, so it offers the whole set including the marquee cyan */ -screenChips('R57',$('card-R57').gw.el,['amber','green','red','blue','vfd','white'],'amber'); +screenChips('R57',$('card-R57').dupre.el,['amber','green','red','blue','vfd','white'],'amber'); BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');}); @@ -1786,16 +1786,16 @@ BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); PRESETS (optional) adds a leading group of named combinations: a preset chip applies all axes at once and resyncs the axis chips beneath it, and changing any axis afterwards clears the preset — the chips must never claim a preset - the widget has since diverged from. */ + the instrument has since diverged from. */ function styleChips(no,STYLES,AXES,PRESETS,defPreset){ - const h=$('card-'+no)?.gw; if(!h)return; + const h=$('card-'+no)?.dupre; if(!h)return; const cardEl=h.el.closest('.card'); const row=document.createElement('div'); row.className='famchips'; const sel={}, sync={}, groups=[]; let clearPreset=()=>{}; /* Re-apply every axis on any change rather than just the one clicked: axes are not independent (onText overrides the ink `on` sets), so a lone setStyle can drop a selection the chips still show as lit — the card would then display a - combination the widget isn't in. Applying all of them in AXES order makes + combination the instrument isn't in. Applying all of them in AXES order makes the outcome independent of click order. */ const applyAll=()=>{for(const [,axis] of AXES) if(sel[axis]) h.setStyle(axis,sel[axis]);}; for(const [label,axis,def] of AXES){ @@ -1829,21 +1829,21 @@ function styleChips(no,STYLES,AXES,PRESETS,defPreset){ groups.forEach(g=>row.appendChild(g)); cardEl.querySelector('.opts').appendChild(row); } -styleChips('N20',GW.splitFlap.STYLES,[['skin','skin','paper'],['font','font','helv']]); -(()=>{const h=$('card-N20').gw,row=document.querySelector('#card-N20 .famchips');if(!h||!row)return; +styleChips('N20',DUPRE.splitFlap.STYLES,[['skin','skin','paper'],['font','font','helv']]); +(()=>{const h=$('card-N20').dupre,row=document.querySelector('#card-N20 .famchips');if(!h||!row)return; const gr=document.createElement('span');gr.className='fgroup'; gr.innerHTML='<span class="lab">flap</span><input type="range" min="30" max="160" value="100" style="width:70px;accent-color:var(--gold);vertical-align:middle"><span class="lab fms">100ms</span>'; const sl=gr.querySelector('input'),ms=gr.querySelector('.fms'); sl.addEventListener('input',()=>{h.setFlapMs(+sl.value);ms.textContent=sl.value+'ms';}); row.appendChild(gr);})(); -styleChips('01',GW.slideToggle.STYLES, +styleChips('01',DUPRE.slideToggle.STYLES, [['on','on','amber'],['on text','onText','panel'],['off','off','dark'], ['off text','offText','white'],['thumb','thumb','light']], - GW.slideToggle.PRESETS,'panel'); -styleChips('R05',GW.filterBank.STYLES,[['panel','panel','silver'],['shape','caps','block'],['color','capColor','black']]); -styleChips('06',GW.segmented.STYLES,[['accent','accent','amber']]); -styleChips('07',GW.chipToggle.STYLES,[['accent','accent','amber']]); -styleChips('25',GW.slideRule.STYLES,[['face','skin','warm']]); + DUPRE.slideToggle.PRESETS,'panel'); +styleChips('R05',DUPRE.filterBank.STYLES,[['panel','panel','silver'],['shape','caps','block'],['color','capColor','black']]); +styleChips('06',DUPRE.segmented.STYLES,[['accent','accent','amber']]); +styleChips('07',DUPRE.chipToggle.STYLES,[['accent','accent','amber']]); +styleChips('25',DUPRE.slideRule.STYLES,[['face','skin','warm']]); /* final tally pass: setV fires per card during build, but each card is still detached at that moment, so the running counts lag by one — recount now */ |
