From f1c661f50d6af2a115fc1430ae0478f2acef2dec Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 22 Jul 2026 15:15:02 -0500 Subject: docs(settings-panel): seal the prototype arc and open the build Prototypes P12-P37 land, along with the night-watch and levels-compare pages plus the shared-engine updates and the kit-candidate file. The spec flips from DRAFT through READY to DOING with rewritten implementation phases and seven recorded build findings. The build tasks are filed in todo.org, and phase 1 is closed against the dotfiles commits. --- ...7-02-desktop-settings-panel-levels-compare.html | 107 ++ ...6-07-02-desktop-settings-panel-prototype-1.html | 2 +- ...-07-02-desktop-settings-panel-prototype-10.html | 194 +++ ...-07-02-desktop-settings-panel-prototype-11.html | 205 ++++ ...-07-02-desktop-settings-panel-prototype-12.html | 283 +++++ ...-07-02-desktop-settings-panel-prototype-13.html | 304 +++++ ...-07-02-desktop-settings-panel-prototype-14.html | 308 +++++ ...-07-02-desktop-settings-panel-prototype-15.html | 299 +++++ ...-07-02-desktop-settings-panel-prototype-16.html | 312 +++++ ...-07-02-desktop-settings-panel-prototype-17.html | 311 +++++ ...-07-02-desktop-settings-panel-prototype-18.html | 321 +++++ ...-07-02-desktop-settings-panel-prototype-19.html | 336 ++++++ ...6-07-02-desktop-settings-panel-prototype-2.html | 3 + ...-07-02-desktop-settings-panel-prototype-20.html | 360 ++++++ ...-07-02-desktop-settings-panel-prototype-21.html | 287 +++++ ...-07-02-desktop-settings-panel-prototype-22.html | 291 +++++ ...-07-02-desktop-settings-panel-prototype-23.html | 305 +++++ ...-07-02-desktop-settings-panel-prototype-24.html | 304 +++++ ...-07-02-desktop-settings-panel-prototype-25.html | 292 +++++ ...-07-02-desktop-settings-panel-prototype-26.html | 295 +++++ ...-07-02-desktop-settings-panel-prototype-27.html | 313 +++++ ...-07-02-desktop-settings-panel-prototype-28.html | 296 +++++ ...-07-02-desktop-settings-panel-prototype-29.html | 297 +++++ ...6-07-02-desktop-settings-panel-prototype-3.html | 2 +- ...-07-02-desktop-settings-panel-prototype-30.html | 310 +++++ ...-07-02-desktop-settings-panel-prototype-31.html | 319 +++++ ...-07-02-desktop-settings-panel-prototype-32.html | 548 +++++++++ ...-07-02-desktop-settings-panel-prototype-33.html | 613 ++++++++++ ...-07-02-desktop-settings-panel-prototype-34.html | 720 +++++++++++ ...-07-02-desktop-settings-panel-prototype-35.html | 717 +++++++++++ ...-07-02-desktop-settings-panel-prototype-36.html | 901 ++++++++++++++ ...-07-02-desktop-settings-panel-prototype-37.html | 915 ++++++++++++++ ...6-07-02-desktop-settings-panel-prototype-4.html | 1 + ...6-07-02-desktop-settings-panel-prototype-5.html | 2 + ...6-07-02-desktop-settings-panel-prototype-6.html | 343 ++++++ ...6-07-02-desktop-settings-panel-prototype-7.html | 322 +++++ ...6-07-02-desktop-settings-panel-prototype-8.html | 360 ++++++ ...6-07-02-desktop-settings-panel-prototype-9.html | 211 ++++ ...7-02-desktop-settings-panel-scenes-compare.html | 177 +++ ...-07-21-night-watch-screensaver-prototype-1.html | 165 +++ docs/prototypes/2026-07-22-watch-preview.png | Bin 0 -> 88870 bytes docs/prototypes/desktop-settings-shared.css | 12 +- docs/prototypes/desktop-settings-shared.js | 16 +- docs/prototypes/dupre-kit-additions.js | 1256 ++++++++++++++++++++ .../2026-07-02-desktop-settings-panel-spec.org | 884 +++++++++++++- 45 files changed, 14790 insertions(+), 29 deletions(-) create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html create mode 100644 docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html create mode 100644 docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html create mode 100644 docs/prototypes/2026-07-22-watch-preview.png create mode 100644 docs/prototypes/dupre-kit-additions.js (limited to 'docs') diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html b/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html new file mode 100644 index 0000000..6f2427d --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-levels-compare.html @@ -0,0 +1,107 @@ + + + + + +Desktop Settings — levels casting: detent fader + single drum + + + + +
+ Levels casting. Brightness: the new multi-detent slide attenuator — + drag slowly across 25 / 50 / 75 and feel the thumb park until you push + through (the tick lights while parked). Keyboard: the generalized drum + roller running a single drum, 0-10. +
+ +
+
Levels
+ +
Brightness — detent fader (new)
+
+
+ +
Keyboard — drum roller ×10 (upgraded)
+
+
+ +
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html index e57d13e..1671eda 100644 --- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html @@ -52,7 +52,7 @@ function render() { // Toggle tiles col.appendChild(DS.engrave("Toggles")); const grid = DS.h("div", "ds-grid2"); - ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((k) => + ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"].forEach((k) => grid.appendChild(DS.toggleTile(k))); if (DS.state.laptop) grid.appendChild(DS.toggleTile("airplane")); col.appendChild(grid); diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html new file mode 100644 index 0000000..ea26651 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-10.html @@ -0,0 +1,194 @@ + + + + + +Desktop Settings — Prototype 10 (spacecraft DSKY, Dupre Kit) + + + + +
+ Direction 10 — Spacecraft DSKY (Dupre Kit). The THUMB SLIDE + attenuator (drag the tabs) sets both levels. Toggles are slide switches + under a live caution board (amber cell = engaged). Scenes are commanded on + the DSKY: key VERB, two digits, ENTR. Power is the keyed mode switch. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html new file mode 100644 index 0000000..d58fbfc --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-11.html @@ -0,0 +1,205 @@ + + + + + +Desktop Settings — Prototype 11 (mixing desk, Dupre Kit) + + + + +
+ Direction 11 — Mixing desk (Dupre Kit). Channel strips: slot faders + ride the two levels in dB (the VU bridge follows), scenes punch in on the + radio preset bank, toggles are lit chips with DND on a stomp pedal, power + is a rotary selector. Lock/suspend/wallpaper are arm-to-fire. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html new file mode 100644 index 0000000..866fa80 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-12.html @@ -0,0 +1,283 @@ + + + + + +Desktop Settings — Prototype 12 (the casting composite) + + + + +
+ Prototype 12 — the casting composite. Every control as cast + 2026-07-21: tuner-dial scenes + presets, detent fader, single drum, + chicken-head power, rocker bank, guarded airplane, wind-up caffeine + timer, wallpaper sub-view. Warm-gold receiver chrome. Lock/suspend + live on wlogout, not here. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html new file mode 100644 index 0000000..401644f --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-13.html @@ -0,0 +1,304 @@ + + + + + +Desktop Settings — Prototype 13 (P12 + Craig's iteration) + + + + +
+ Prototype 13 — P12 + Craig's iteration. Presets moved above the + dial (Preset 2 contains its text); screen + keyboard are drum partners + left of a larger caffeine timer with an always-on rocker; compact + chicken-head upper right; switch wall of six with airplane's guarded + lever in the mix; weather-geo dropped from the panel; wallpaper where + it was. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html new file mode 100644 index 0000000..af02eab --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html @@ -0,0 +1,308 @@ + + + + + +Desktop Settings — Prototype 14 (timer bat, centered dial) + + + + +
+ Prototype 14 — P13 + timer refinements. The HOLD rocker is gone: + a bat toggle sits at the timer's upper right where the winding arrow + was — up TIMED, down HOLD (always on). The dial is vertically centered + against the drums. Everything else as P13. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html new file mode 100644 index 0000000..7bdc336 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-15.html @@ -0,0 +1,299 @@ + + + + + +Desktop Settings — Prototype 15 (power slide, full-width tuner) + + + + +
+ Prototype 15 — P14 + power slide. The chicken-head is out: the + tuner dial is restored to full width, and beneath it a three-position + slide switch of equal length carries power — a status lamp per position, + labeled PERFORMANCE / BALANCE / POWER SAVE. Everything else as P14. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html new file mode 100644 index 0000000..a2d76e0 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-16.html @@ -0,0 +1,312 @@ + + + + + +Desktop Settings — Prototype 16 (rebalanced: quiet chassis) + + + + +
+ Prototype 16 — rebalanced. Same instruments, honest loudness: + three uniform plates on one grid; thumb-slide pair replaces the drums; + the timer is de-throned (steel knob, one engraving, amber index) with + the bat docked on its stage; rockers carry their function name on the + lit half (quiet dark when off); red survives only on the tuner needle + and the airplane disc. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html new file mode 100644 index 0000000..3372694 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-17.html @@ -0,0 +1,311 @@ + + + + + +Desktop Settings — Prototype 17 (switches under their presets) + + + + +
+ Prototype 17 — switches under their presets. The switch wall + moves up beside its cause: scenes on top, the switches they program + directly beneath, the power slide just below the wall — with a dark + knob now. Levels · caffeine follow. Everything else as P16. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html new file mode 100644 index 0000000..c0ff06c --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-18.html @@ -0,0 +1,321 @@ + + + + + +Desktop Settings — Prototype 18 (program chart, real proportions) + + + + +
+ Prototype 18 — the program chart. The missing analog component: + a faceplate program chart (washing-machine style) printed between the + scenes and their switches — filled dot = the program closes it, ring = + opens it, dash = leaves it alone, letter = power; the active program's + row lights. Airplane is gone (the network panel owns its master + switch). OFF on the dial now reads CUSTOM. Slides and timer re-scaled + toward their real-life sizes. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html new file mode 100644 index 0000000..442e39e --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-19.html @@ -0,0 +1,336 @@ + + + + + +Desktop Settings — Prototype 19 (punch-card chart, green switches) + + + + +
+ Prototype 19 — the chart becomes a punch card. Program-chart + lights are white (future state, not live amber) and clickable: a click + reprograms the preset — dots cycle closes/opens/untouched, the power + letter cycles P/B/S/–. Switches engage GREEN and shrink to real rocker + size; the slides grow. Less amber everywhere. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html index af2160c..c215f85 100644 --- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html @@ -72,6 +72,7 @@ function render() { ])); const powerNodes = []; + powerNodes.push(DS.toggleKey("caffeine")); if (DS.state.laptop) powerNodes.push(DS.toggleKey("airplane")); powerNodes.push(DS.profileSeg()); const acts = DS.h("div", "ds-grid2"); @@ -82,6 +83,8 @@ function render() { col.appendChild(section("Notifications", [DS.toggleKey("dnd")])); + col.appendChild(section("Weather", [DS.toggleKey("weathergeo")])); + const scenes = DS.h("div", "ds-grid4"); Object.keys(DS.SCENES).forEach((n) => scenes.appendChild(DS.sceneKey(n))); col.appendChild(section("Scenes", [scenes])); diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html new file mode 100644 index 0000000..9f01322 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-20.html @@ -0,0 +1,360 @@ + + + + + +Desktop Settings — Prototype 20 (the switch matrix) + + + + +
+ Prototype 20 — the switch matrix. The panel's axis is the + switches: each row is a live switch, the four preset buttons run across + the top (names only), and the white punch lights under each preset + program what it recalls. TPAD and MOUSE (never scene-touched) drop to + their own strip below the levels. Screen, keyboard, and power are + horizontal thumb slides. The slide-rule dial is retired. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html new file mode 100644 index 0000000..274ae78 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-21.html @@ -0,0 +1,287 @@ + + + + + +Desktop Settings — Prototype 21 (pin routing matrix) + + + + +
+ Prototype 21 — the pin routing matrix. The interface is the + matrix: switch names label the rows, the program buttons head the + columns, pins route settings into programs. The NOW column is live — + green pins are current state, click to toggle. Power is a radio row + group (one pin per column). CAFF is an ordinary switch; the dial is + now a SLEEP TIMER (wind minutes until suspend). The drums are back. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html new file mode 100644 index 0000000..f67e990 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-22.html @@ -0,0 +1,291 @@ + + + + + +Desktop Settings — Prototype 22 (the programmable matrix) + + + + +
+ Prototype 22 — the programmable matrix. No NOW column: press a + program head (FOCUS · PRESENT · BATTERY · NIGHT) and its column's pins + go white → green, because the active program IS the live state. + Editing the green column edits the present; editing a white column + edits a future. Power rows are radio (a pin moves, never pulls). +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html new file mode 100644 index 0000000..d0ddf85 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-23.html @@ -0,0 +1,305 @@ + + + + + +Desktop Settings — Prototype 23 (pushwheel CPU row) + + + + +
+ Prototype 23 — the pushwheel CPU row. CPU power collapses to a + single letter row: each program carries P / B / S (click cycles), and + the row ends in a push-button thumbwheel — tape-counter window showing + the LIVE letter, triangle steppers to walk it (up toward P). Row + labels right-align to the buttons' left edge. Active column still + floods green, letters included. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html new file mode 100644 index 0000000..c473ce0 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-24.html @@ -0,0 +1,304 @@ + + + + + +Desktop Settings — Prototype 24 (tape wheel, jewel verdicts) + + + + +
+ Prototype 24 — tape wheel + jewel verdicts. The triangles are + gone: click the tape-counter wheel itself to cycle the live CPU letter + (P → B → S, wrapping). In the active column every switch row shows a + verdict as a small jewel pilot lamp — green seated, red for a socket + the program holds off. The wheel cell is bigger, cream paper and + cylinder shade like the kit tape counter. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html new file mode 100644 index 0000000..e0bbb1d --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-25.html @@ -0,0 +1,292 @@ + + + + + +Desktop Settings — Prototype 25 (wheels in every column) + + + + +
+ Prototype 25 — wheels in every column. The side wheel was a + NOW-column vestige: every program column now carries its own tape + wheel in the CPU row — click any wheel to cycle that program's letter; + the active column's wheel wears the green bezel and IS the live value. + Jewel pilot lamps sized up to match the taller rows. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html new file mode 100644 index 0000000..c0d0cb4 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-26.html @@ -0,0 +1,295 @@ + + + + + +Desktop Settings — Prototype 26 (physical keys, arrow wheels) + + + + +
+ Prototype 26 — physical keys, arrow wheels. Program heads are + real keys now: raised bevel at rest, depressed when active, green + lighting only the legend. Row labels engraved bigger on the faceplate. + The CPU wheels print ↑ – ↓ instead of letters, no live highlight on + the wheel — the depressed key and the jewels say what's live. The + whole panel is wider to give it room. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html new file mode 100644 index 0000000..617ee5a --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-27.html @@ -0,0 +1,313 @@ + + + + + +Desktop Settings — Prototype 27 (console keys, six slots, one bench) + + + + +
+ Prototype 27 — console keys, six slots, one bench. Heads are + kit console keys lighting green; two blank program slots grow the + matrix rightward. The lower level is one bench: drums · groove · TPAD + over MOUSE · groove · sleep dial. Next conversation: what the dial + fires when it runs out. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html new file mode 100644 index 0000000..769066e --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-28.html @@ -0,0 +1,296 @@ + + + + + +Desktop Settings — Prototype 28 (the idle tripper rail) + + + + +
+ Prototype 28 — the idle tripper rail. The dial is retired: the + panel programs futures, so idleness gets a time-switch tripper rail — + drag the DIM / LOCK / WATCH / SCREEN OFF / SUSPEND tabs along the + idle-minutes scale; slide one into the OFF siding to disable it. + Engage CAFFEINE and the whole policy dims — bypassed. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html new file mode 100644 index 0000000..9a05b81 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-29.html @@ -0,0 +1,297 @@ + + + + + +Desktop Settings — Prototype 29 (the idle tripper dial) + + + + +
+ Prototype 29 — the idle tripper dial. Craig's actual mental + image: the round segment-timer with four COLORED tripper tabs on the + rim — DIM · LOCK · SCREEN OFF · SUSPEND (the night watch is the lock + screen's face, not its own stage). Drag a tab around the idle scale; + drop it through the bottom OFF notch to disable. Side legend maps + color → stage → minutes. CAFFEINE still bypasses the lot. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html index 8c383f1..49661e4 100644 --- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html @@ -93,7 +93,7 @@ function render() { col.appendChild(mh); const grid2 = DS.h("div", "ds-grid2"); - ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((key) => + ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd", "weathergeo"].forEach((key) => grid2.appendChild(DS.toggleTile(key))); if (DS.state.laptop) grid2.appendChild(DS.toggleTile("airplane")); col.appendChild(grid2); diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html new file mode 100644 index 0000000..5081165 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-30.html @@ -0,0 +1,310 @@ + + + + + +Desktop Settings — Prototype 30 (family chrome) + + + + +
+ Prototype 30 — family chrome. Aligned with the maintenance / + net / bt family: theme gold (#dab53d), raised gradient faceplates with + inset highlights, maint-style header. Slots labeled 1 · 2 (slim keys); + factory presets programmed; PROGRAMMABLE PRESETS on its own + high-contrast plate, no divider; dial + drums enlarged, dial numerals + lighter. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html new file mode 100644 index 0000000..9d9a0c1 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-31.html @@ -0,0 +1,319 @@ + + + + + +Desktop Settings — Prototype 31 (encircled groups) + + + + +
+ Prototype 31 — encircled groups. TPAD and MOUSE become console + keys (lighting green), horizontal above the dial; the bench reorganizes + into three bordered instrument groups with their labels set into the + border: BRIGHTNESS · POINTERS · IDLE. Dial unchanged, drums grown to + proportion. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html new file mode 100644 index 0000000..bc241de --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-32.html @@ -0,0 +1,548 @@ + + + + + +Desktop Settings — Prototype 32 (the wallpaper channels) + + + + +
+ Prototype 32 — the wallpaper channels. The sub-view is a channel + bank: seven mode cards (night watch · sun pair · single · random · + video · clock · world), each wearing its own face — slashed thumbs, + deck fans, mini clock matrices. The active channel lights green; the + config surface below switches per channel. Pairs are MINTED: fill the + day and night frames, confirm, and the slashed chip joins the tray + forever. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html new file mode 100644 index 0000000..7dc67c8 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-33.html @@ -0,0 +1,613 @@ + + + + + +Desktop Settings — Prototype 33 (sources + pair scrap) + + + + +
+ Prototype 32 — the wallpaper channels. The sub-view is a channel + bank: seven mode cards (night watch · sun pair · single · random · + video · clock · world), each wearing its own face — slashed thumbs, + deck fans, mini clock matrices. The active channel lights green; the + config surface below switches per channel. Pairs are MINTED: fill the + day and night frames, confirm, and the slashed chip joins the tray + forever. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html new file mode 100644 index 0000000..3692ada --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-34.html @@ -0,0 +1,720 @@ + + + + + +Desktop Settings — Prototype 34 (annotation batch one) + + + + +
+ Prototype 32 — the wallpaper channels. The sub-view is a channel + bank: seven mode cards (night watch · sun pair · single · random · + video · clock · world), each wearing its own face — slashed thumbs, + deck fans, mini clock matrices. The active channel lights green; the + config surface below switches per channel. Pairs are MINTED: fill the + day and night frames, confirm, and the slashed chip joins the tray + forever. +
+ +
19:24
+ +
+
+ + SETTINGS + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html new file mode 100644 index 0000000..9296af2 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html @@ -0,0 +1,717 @@ + + + + + +Desktop Settings — Prototype 35 (FINAL candidate) + + + + +
+ Prototype 35 — the final candidate. The consolidated machine: + the programmable presets matrix (glyph keys, four numbered slots, + punch pins, arrow-and-equals CPU wheels), the instrument bench + (drums · pointer keys · idle tripper dial), and the wallpaper + channels with descriptions, galleries, previews, the pair press, and + sources. Normative reference for the spec. +
+ +
+
+ + SETTINGS + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html new file mode 100644 index 0000000..ac39b16 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-36.html @@ -0,0 +1,901 @@ + + + + + +Desktop Settings — Prototype 36 (annotation batch two) + + + + +
+ Prototype 36 — annotation batch two. Uniform slim preset keys; + pointers above brightness with the dial enlarged, legend below; + SOLID COLOR joins the channels; RANDOM gains labeled, count-badged + SETS; sort keys stack left of sortable galleries; custom scrollbars + with even framing; scrollable sources; a dozen seeded pairs so the + scroller shows itself. +
+ +
+
+ + SETTINGS + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html new file mode 100644 index 0000000..952b9d1 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-37.html @@ -0,0 +1,915 @@ + + + + + +Desktop Settings — Prototype 37 (annotation batch three) + + + + +
+ Prototype 37 — annotation batch three. Iconic channel faces + (sun/crescent pair card, dupre-blue solid, honest mini clocks); slots + announce as 1-4; the idle dial reads exact minutes while dragging and + IDLE stands visible; smaller video preview; WALLPAPER capitalized. + FOCUS tries the headphones glyph — confirm or veto. +
+ +
+
+ + SETTINGS + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html index 23d72fb..67b9f56 100644 --- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html @@ -141,6 +141,7 @@ function render() { ["autodim", "nightlight", "touchpad", "mouse", "dnd"].forEach((k) => well.appendChild(lampRow(k))); if (DS.state.laptop) well.appendChild(lampRow("airplane")); well.appendChild(lampRow("caffeine")); + well.appendChild(lampRow("weathergeo")); col.appendChild(well); col.appendChild(DS.engrave("Scenes")); diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html index 662e9f6..8cbaa45 100644 --- a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html @@ -116,8 +116,10 @@ function render() { col.appendChild(h("div", "sep")); ["touchpad", "mouse"].forEach((k) => col.appendChild(toggleRow(k))); + col.appendChild(toggleRow("caffeine")); if (DS.state.laptop) col.appendChild(toggleRow("airplane")); col.appendChild(toggleRow("dnd")); + col.appendChild(toggleRow("weathergeo")); col.appendChild(h("div", "sep")); col.appendChild(expandRow("\u{f0241}", "Power profile", DS.state.powerprofile, () => DS.profileSeg())); diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html new file mode 100644 index 0000000..952e4d9 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-6.html @@ -0,0 +1,343 @@ + + + + + +Desktop Settings — Prototype 6 (hi-fi receiver) + + + + +
+ Direction 6 — Hi-fi receiver. Scenes are station presets on a backlit + tuner dial; the needle slides to the tuned scene. Rotary knobs + VU meters + for the levels, a backlit switch bank for toggles, an input selector for + power. Drag knobs sideways; click stations or preset keys. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html new file mode 100644 index 0000000..dc7ac0a --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-7.html @@ -0,0 +1,322 @@ + + + + + +Desktop Settings — Prototype 7 (chronograph) + + + + +
+ Direction 7 — Chronograph. The panel is a watch. Drag around the + bezel to set brightness (the gold hand + digital window follow). Subdials: + keyboard backlight (drag or click), power profile and scene (click to step). + Crowns on the case are lock / suspend / wallpaper; the bracelet links are + the toggles. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html new file mode 100644 index 0000000..13fed0b --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-8.html @@ -0,0 +1,360 @@ + + + + + +Desktop Settings — Prototype 8 (gauge cluster) + + + + +
+ Direction 8 — Gauge cluster. A dashboard binnacle: brightness and + keyboard are needle gauges (drag across the dial), toggles are a rocker + bank with backlit legends, scenes are push-button drive modes, power is an + ignition rotary, actions are dash pull-knobs. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html new file mode 100644 index 0000000..bc15e94 --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-9.html @@ -0,0 +1,211 @@ + + + + + +Desktop Settings — Prototype 9 (engine room, Dupre Kit) + + + + +
+ Direction 9 — Engine room (Dupre Kit). Real kit instruments: the + DRUM ROLLER sets both levels (drag the drums), a split-flap board reads the + scene tuned on the rotary selector, breaker panels carry the toggles, + airplane is a guarded lever, power is an engine-order telegraph (click to + ring the next order). Lock/suspend/wallpaper are arm-to-fire. +
+ +
19:24
+ +
+
+ + Settings + + +
+
+
+
+
+ + + + + + + diff --git a/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html b/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html new file mode 100644 index 0000000..9efc1dc --- /dev/null +++ b/docs/prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html @@ -0,0 +1,177 @@ + + + + + +Desktop Settings — scene control casting: tuner dial vs rotary + split-flap + + + + +
+ Scene-control casting. Both candidates drive the same scene state — + tune one, the other follows. Left: tuner dial (kit slideRule) + preset + keys, with skin chips. Right: rotary selector + split-flap board. +
+ +
+
Candidate 1
+
Tuner dial + presets
+
+
+
+
+
+ +
+
Candidate 3
+
Rotary + split-flap
+
+
+
+
+ +
+ + + + + + diff --git a/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html b/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html new file mode 100644 index 0000000..7ccc071 --- /dev/null +++ b/docs/prototypes/2026-07-21-night-watch-screensaver-prototype-1.html @@ -0,0 +1,165 @@ + + + + + +Dupre Night Watch — idle-stage screensaver prototype 1 + + + +
DUPRE · NIGHT WATCH
+
+ + + + + diff --git a/docs/prototypes/2026-07-22-watch-preview.png b/docs/prototypes/2026-07-22-watch-preview.png new file mode 100644 index 0000000..9b8419b Binary files /dev/null and b/docs/prototypes/2026-07-22-watch-preview.png differ diff --git a/docs/prototypes/desktop-settings-shared.css b/docs/prototypes/desktop-settings-shared.css index 95aec22..ec677c7 100644 --- a/docs/prototypes/desktop-settings-shared.css +++ b/docs/prototypes/desktop-settings-shared.css @@ -34,6 +34,7 @@ body.ds-stage { margin: 0; min-height: 100vh; + zoom: 1.6; /* the 360px capsule reads small on a large monitor (Craig, 2026-07-21) */ background: radial-gradient(1200px 600px at 80% -10%, #17151322, transparent), #050505; @@ -126,8 +127,11 @@ body.ds-stage { background: linear-gradient(90deg, var(--line), transparent); } -/* Status lamp: a colored dot with a soft glow; off is a dim wash. */ -.ds-lamp { +/* Status lamp: retired 2026-07-21 (Craig: the glowing dot pulls the eye). + State reads from the control's own gold glyph/text instead. The spans + still render; this hides them everywhere at once. */ +.ds-lamp { display: none; } +.ds-lamp-unused { width: 9px; height: 9px; border-radius: 50%; background-color: var(--pass); box-shadow: 0 0 6px 1px rgba(116,147,47,.55); @@ -228,7 +232,9 @@ body.ds-stage { background: var(--cream); border: 1px solid var(--gold); cursor: pointer; - box-shadow: -200px 0 0 194px var(--gold); /* gold fill to the left of the knob */ + /* The old box-shadow fill hack bled a huge gold disc outside the 6px + track (the thumb is taller than the input, so overflow can't clip it). + The gold fill is now painted by slider() as an inline gradient. */ } .ds-slider input[type=range]::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; diff --git a/docs/prototypes/desktop-settings-shared.js b/docs/prototypes/desktop-settings-shared.js index 39bc76a..f96a03a 100644 --- a/docs/prototypes/desktop-settings-shared.js +++ b/docs/prototypes/desktop-settings-shared.js @@ -16,6 +16,7 @@ const DS = (() => { airplane: false, nightlight: false, dnd: false, + weathergeo: false, brightness: 70, kbd: 40, powerprofile: "balanced", // performance | balanced | saver @@ -38,6 +39,7 @@ const DS = (() => { airplane: { glyph: "\u{f001d}", name: "Airplane", on: "on", off: "off" }, nightlight:{ glyph: "\u{f0594}", name: "Night light", on: "warm", off: "off" }, dnd: { glyph: "\u{f009b}", name: "Do Not Disturb",on:"silenced",off: "notifying" }, + weathergeo:{ glyph: "\u{f034e}", name: "Weather geo", on: "whereami",off: "pinned" }, }; const SLIDERS = { brightness: { glyph: "\u{f00df}", name: "Brightness", floor: 5 }, @@ -161,7 +163,13 @@ const DS = (() => { const inp = h("input"); inp.type = "range"; inp.min = meta.floor; inp.max = 100; inp.value = state[key]; const pct = h("span", "s-pct", state[key] + "%"); - inp.oninput = () => { pct.textContent = inp.value + "%"; }; + // Gold fill left of the knob, painted as a gradient (no shadow hack). + const paint = () => { + const p = ((inp.value - meta.floor) / (100 - meta.floor)) * 100; + inp.style.background = `linear-gradient(90deg, var(--gold) ${p}%, var(--well) ${p}%)`; + }; + paint(); + inp.oninput = () => { pct.textContent = inp.value + "%"; paint(); }; inp.onchange = () => setSlider(key, +inp.value, row); row.appendChild(inp); row.appendChild(pct); return row; @@ -202,7 +210,11 @@ const DS = (() => { "linear-gradient(135deg,#5c5030,#2e2810)", ]; function wallpaperSubview(closeCb) { - const sv = h("div", "ds-subview"); + // plain flow container: the CALLER's #subview owns the ds-subview + // overlay styling — carrying the class here left the content + // display:none inside an open overlay (empty wallpaper view, found + // testing prototype 12) + const sv = h("div", "ds-col"); const head = h("div", "ds-sub-head"); head.innerHTML = `‹ BackWallpaper`; head.querySelector(".back").onclick = closeCb; diff --git a/docs/prototypes/dupre-kit-additions.js b/docs/prototypes/dupre-kit-additions.js new file mode 100644 index 0000000..005a577 --- /dev/null +++ b/docs/prototypes/dupre-kit-additions.js @@ -0,0 +1,1256 @@ +/* dupre-kit-additions.js — kit-candidate builders developed in the + * desktop-settings casting work, written to the widgets.js conventions + * (DUPRE.name(host, opts) → {el, get, set, ...}; onChange(value, text) on + * every state change including the initial paint; no page globals touched) + * so they can merge back into the kit with gallery cards. + * + * Load AFTER widgets.js. Contents: + * - DUPRE.detentFader (NEW) — multi-detent slide attenuator with + * speedbump drag physics. + * - DUPRE.drumRoller (UPGRADE) — backward-compatible redefinition: + * 1..N channels (stock hardcodes exactly two), configurable min/max + * range and stage height. Defaults reproduce the stock instrument. + * - DUPRE.guardedToggle (UPGRADE) — rotateX throw (no planar spin). + * - DUPRE.batToggle (UPGRADE) — same throw fix. + * - DUPRE.slideRule (UPGRADE) — width option; the printed scale + * spreads its stops proportionally. Default keeps stock 180px. + * - DUPRE.slideSelector (NEW) — stretchable n-position slide switch + * with a status lamp per position (pillSlide's full-size descendant). + * - DUPRE.programChart (NEW) — faceplate program matrix / punch card. + * - DUPRE.thumbSlideH (NEW) — horizontal thumb-slide attenuator; + * continuous 0-100 or detented positions (thumbSlide turned on its + * side, one channel, stretchable). + * - DUPRE.pinMatrix (UPGRADE) — configurable geometry, per-column + * pin colors, clickable button column-heads, group dividers, set(). + * Defaults reproduce the stock 5×6 synth patch exactly. + * - DUPRE.programMatrix (NEW) — the programmable matrix: programs as + * column heads, settings as rows, white pins program, the ACTIVE + * column's pins run green because the recalled program IS the live + * state. No separate live column needed. + * - DUPRE.tripRail (NEW) — idle tripper rail: a time-switch tripper + * scale for "when idle reaches here, throw this" — draggable stage + * tabs on a sqrt-spaced minutes rail, with a parking siding to + * disable a stage and a bypass state (caffeine dims the policy). + * - DUPRE.tripDial (NEW) — the round form Craig actually imagined: + * the Intermatic segment-timer dial with colored tripper tabs on the + * rim, an OFF notch at the bottom, and a side legend. Same policy + * semantics as the rail. + */ +(() => { + const noop = () => {}; + const SVGNS = 'http://www.w3.org/2000/svg'; + + /* local mini-helpers (widgets.js keeps its own private; additions carry + theirs so the file is self-contained until merged) */ + const svgEl = (parent, tag, attrs) => { + const e = document.createElementNS(SVGNS, tag); + for (const k in (attrs || {})) e.setAttribute(k, attrs[k]); + parent.appendChild(e); return e; + }; + const stageSvg = (host, cls, vw, vh) => { + const s = document.createElementNS(SVGNS, 'svg'); + s.setAttribute('class', cls); s.setAttribute('viewBox', `0 0 ${vw} ${vh}`); + s.setAttribute('width', vw); s.setAttribute('height', vh); + host.appendChild(s); return s; + }; + let uidN = 0; + const uid = p => p + '-a' + (++uidN); + + /* additions-scoped gradients (ids prefixed so they never collide with the + kit's shared defs plate; on merge these fold into gradDef) */ + let defsRoot = null; + const grad = (id, kind, geom, stops) => { + if (!defsRoot) { + const svg = document.createElementNS(SVGNS, 'svg'); + svg.setAttribute('width', 0); svg.setAttribute('height', 0); + svg.setAttribute('aria-hidden', 'true'); + svg.style.position = 'absolute'; + defsRoot = document.createElementNS(SVGNS, 'defs'); + svg.appendChild(defsRoot); + document.body.appendChild(svg); + } + if (defsRoot.querySelector('#' + id)) return; + const g = document.createElementNS(SVGNS, kind); + g.setAttribute('id', id); + for (const k in geom) g.setAttribute(k, geom[k]); + stops.forEach(([o, c]) => { + const st = document.createElementNS(SVGNS, 'stop'); + st.setAttribute('offset', o); st.setAttribute('stop-color', c); + g.appendChild(st); + }); + defsRoot.appendChild(g); + }; + + /* vertical drag with delta accumulation (the kit's dragDelta shape) */ + const dragDelta = (el, getV, setV, o) => { + el.style.touchAction = 'none'; + el.addEventListener('pointerdown', e => { + el.setPointerCapture(e.pointerId); + const y0 = e.clientY, v0 = getV(); + const move = m => setV(Math.max(o.min, Math.min(o.max, v0 + (y0 - m.clientY) * o.sens))); + const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); }; + el.addEventListener('pointermove', move); + el.addEventListener('pointerup', up); + e.preventDefault(); + }); + }; + + /* additions CSS, injected once (the kit pattern: DUPRE_CSS ships inside + widgets.js; these rules move into it on merge) */ + if (!document.getElementById('dupre-additions-css')) { + const st = document.createElement('style'); + st.id = 'dupre-additions-css'; + st.textContent = ` +.dupre-dfader{position:relative;display:inline-block;width:230px;height:36px;cursor:ew-resize;touch-action:none} +.dupre-dfader .df-slot{position:absolute;left:8px;right:8px;top:15px;height:6px;border-radius:4px; + background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 1px 2px #000} +.dupre-dfader .df-fill{position:absolute;left:9px;top:16px;height:4px;border-radius:3px; + background:linear-gradient(180deg,var(--amber-grad-top,#f2c76a),var(--gold,#e2a038));pointer-events:none} +.dupre-dfader .df-tick{position:absolute;top:9px;width:2px;height:18px;border-radius:1px; + background:var(--steel,#969385);opacity:.55;transform:translateX(-50%);pointer-events:none} +.dupre-dfader .df-tick.df-park{background:var(--gold-hi,#ffbe54);opacity:1; + box-shadow:0 0 5px rgba(var(--glow-hi,255,190,84),.7)} +.dupre-dfader .df-cap{position:absolute;top:5px;width:12px;height:26px;border-radius:3px;transform:translateX(-50%); + background:linear-gradient(90deg,#7e7a70,#e8e5db 45%,#8a867c);border:1px solid #4e4a42; + box-shadow:0 2px 4px #000a;pointer-events:none} +.dupre-dfader .df-cap::after{content:"";position:absolute;left:50%;top:3px;bottom:3px;width:2px; + transform:translateX(-50%);background:rgba(0,0,0,.35);border-radius:1px} +.dupre-dfader.df-parked .df-cap{box-shadow:0 2px 4px #000a,0 0 7px rgba(var(--glow-hi,255,190,84),.55)} +.dupre-slsel{display:block;width:220px;cursor:pointer;touch-action:none;user-select:none} +.dupre-slsel .sl-cols{display:grid} +.dupre-slsel .sl-col{display:flex;flex-direction:column;align-items:center;gap:3px;padding-bottom:5px} +.dupre-slsel .sl-lamp{width:11px;height:11px;border-radius:50%; + background:radial-gradient(circle at 38% 32%,#2e2a24,#171310);border:1px solid #060505} +.dupre-slsel .sl-col.sl-on .sl-lamp{background:radial-gradient(circle at 38% 32%,var(--gold-hi,#ffbe54),var(--gold,#e2a038) 60%,#8f671f); + box-shadow:0 0 7px rgba(var(--glow-hi,255,190,84),.75)} +.dupre-slsel .sl-lbl{font-size:8px;letter-spacing:.1em;color:var(--dim,#7c838a);text-transform:uppercase;font-family:var(--mono)} +.dupre-slsel .sl-col.sl-on .sl-lbl{color:var(--gold-hi,#ffbe54)} +.dupre-slsel .sl-track{position:relative;height:20px;border-radius:10px; + background:var(--well,#0a0c0d);border:1px solid #0a0908;box-shadow:inset 0 2px 4px #000} +.dupre-slsel .sl-knob{position:absolute;top:2px;bottom:2px;border-radius:8px;transform:translateX(-50%); + background:linear-gradient(180deg,#f0efec,#c4c1b9 45%,#75726a);border:1px solid #4e4a42; + box-shadow:0 2px 4px #000a;transition:left .16s ease} +.dupre-slsel .sl-knob::after{content:"";position:absolute;left:50%;top:4px;bottom:4px;width:2px; + transform:translateX(-50%);background:rgba(0,0,0,.3);border-radius:1px} +.dupre-rocker.dupre-quiet:not(.dupre-on) .dupre-bot{background:linear-gradient(180deg,#141210,#100e0c); + color:var(--dim,#7c838a);box-shadow:inset 0 3px 5px rgba(0,0,0,.55)} +.dupre-rocker.dupre-green.dupre-on .dupre-top{background:linear-gradient(180deg,#a9c95f,var(--pass,#74932f)); + color:var(--panel,#100f0f);box-shadow:0 0 10px rgba(116,147,47,.35)} +.dupre-slsel.sl-dark .sl-knob{background:linear-gradient(180deg,#33302a,#1c1a16);border-color:#3f3b33} +.dupre-slsel.sl-dark .sl-knob::after{background:rgba(255,255,255,.16)} +.dupre-pchart{display:block;font-family:var(--mono);pointer-events:none} +.dupre-pchart .pc-grid{display:grid;row-gap:2px} +.dupre-pchart .pc-cell{display:flex;align-items:center;justify-content:center;height:14px} +.dupre-pchart .pc-h{font-size:6.5px;letter-spacing:.08em;color:var(--steel,#969385);text-transform:uppercase} +.dupre-pchart .pc-name{font-size:7.5px;letter-spacing:.06em;color:var(--dim,#7c838a); + text-transform:uppercase;justify-content:flex-end;padding-right:6px} +.dupre-pchart .pc-dot{width:6px;height:6px;border-radius:50%} +.dupre-pchart .pc-dot.pc-set{background:#cfd3cf} +.dupre-pchart .pc-dot.pc-clear{border:1px solid var(--steel,#969385);opacity:.6} +.dupre-pchart .pc-dash{width:6px;height:1px;background:var(--wash,#2c2f32)} +.dupre-pchart .pc-pwr{font-size:7.5px;color:var(--steel,#969385)} +.dupre-pchart .pc-row-on .pc-name{color:#f2f4f2} +.dupre-pchart .pc-row-on .pc-dot.pc-set{background:#f2f4f2; + box-shadow:0 0 5px rgba(242,244,242,.6)} +.dupre-pchart .pc-row-on .pc-pwr{color:#f2f4f2} +.dupre-pchart.pc-edit{pointer-events:auto} +.dupre-pchart.pc-edit .pc-cell:not(.pc-h):not(.pc-name){cursor:pointer;border-radius:4px} +.dupre-pchart.pc-edit .pc-cell:not(.pc-h):not(.pc-name):hover{background:#ffffff10} +`; + document.head.appendChild(st); + } + + /* NEW — multi-detent slide attenuator: a horizontal fader whose thumb + parks at each detent; the pointer must push an escape margin past the + detent to break free (a speedbump, not a snap). Crossfader's center + detent generalized to a set. + + Contract (everything a consumer needs; no page globals touched): + opts: value (0-100, default 68); detents (values 0-100, default [50] — + the crossfader case); magnet (± units that capture the thumb, + default 3); escape (extra units past the magnet edge needed to + break out, default 3); onChange(v, label) on every change, + label 'N' or 'N · detent' while parked, including the initial + paint. + handle: el, get(), set(v) (clamped 0-100; set() obeys detent capture + so a programmatic sweep feels the same bumps), parked() + (the detent value or null). + CSS lives in the additions block (.dupre-dfader); no other styles + involved. */ + DUPRE.detentFader = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const detents = (opts.detents || [50]).slice().sort((a, b) => a - b); + const MAGNET = opts.magnet !== undefined ? opts.magnet : 3; + const ESCAPE = opts.escape !== undefined ? opts.escape : 3; + const el = document.createElement('span'); el.className = 'dupre-dfader'; + el.innerHTML = ''; + const ticks = detents.map(d => { + const t = document.createElement('span'); t.className = 'df-tick'; + t.style.left = `calc(8px + ${d / 100} * (100% - 16px))`; + el.appendChild(t); return t; + }); + const cap = document.createElement('span'); cap.className = 'df-cap'; el.appendChild(cap); + host.appendChild(el); + + let v, park = null; + const paint = () => { + const left = `calc(8px + ${v / 100} * (100% - 16px))`; + cap.style.left = left; + el.querySelector('.df-fill').style.width = `calc(${v / 100} * (100% - 18px))`; + el.classList.toggle('df-parked', park !== null); + ticks.forEach((t, i) => t.classList.toggle('df-park', detents[i] === park)); + }; + /* speedbump core: raw is where the pointer says the thumb should be; + the thumb only goes there if it isn't captured, and capture only + breaks when raw clears the magnet+escape band. */ + const apply = raw => { + raw = Math.max(0, Math.min(100, raw)); + if (park !== null) { + if (Math.abs(raw - park) <= MAGNET + ESCAPE) raw = park; /* still parked */ + else park = null; /* broke through */ + } + if (park === null) { + const near = detents.find(d => Math.abs(raw - d) <= MAGNET); + if (near !== undefined) { park = near; raw = near; } + } + const nv = Math.round(raw); + if (nv === v && el.dataset.painted) return; + v = nv; el.dataset.painted = 1; + paint(); + onChange(v, park !== null ? v + ' · detent' : String(v)); + }; + + el.addEventListener('pointerdown', e => { + el.setPointerCapture(e.pointerId); + const rect = () => el.getBoundingClientRect(); + const pct = m => ((m.clientX - rect().left - 8) / (rect().width - 16)) * 100; + apply(pct(e)); + const move = m => apply(pct(m)); + const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); }; + el.addEventListener('pointermove', move); + el.addEventListener('pointerup', up); + e.preventDefault(); + }); + + v = -1; park = null; + apply(opts.value !== undefined ? opts.value : 68); + return { el, get: () => v, set: apply, parked: () => park }; + }; + + /* UPGRADE — drum roller selector, generalized. Stock hardcodes exactly two + drums (the onChange label indexes chans[1] unconditionally, so one + channel throws) and a fixed 1-10 range. This redefinition accepts 1..N + channels and a min/max range; every default reproduces the stock + instrument, so existing callers are unaffected. + + Contract (everything a consumer needs; no page globals touched): + opts: title (engraved header, default 'EQUALIZER'); channels (array of + { name, v } drums, any count ≥ 1, default HIGH/LOW pair); + min / max (drum range, default 1 / 10); height (stage height, + default 140 — a taller stage opens a longer paper window and + shows more of the scale); onChange(values array, + 'NAME v · NAME v · …') fires on every set, including the + initial paint. + handle: el, get() (values array), set(i, v) — v clamps to min..max; + each drum drags vertically on its own hit strip. + SVG-built: styling is inline attributes plus the shared .rsvg stage + block of DUPRE_CSS; gradients are additions-scoped until merge. */ + DUPRE.drumRoller = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const MIN = opts.min !== undefined ? opts.min : 1; + const MAX = opts.max !== undefined ? opts.max : 10; + const chans = (opts.channels || [{ name: 'HIGH', v: 6 }, { name: 'LOW', v: 8 }]) + .map(c => ({ name: c.name, v: c.v })); + const N = chans.length; + const vw = Math.max(130, N * 50 + 40); + const vh = Math.max(100, opts.height !== undefined ? opts.height : 140); + const railY = 24, railH = vh - 36, winY = railY + 4, winH = railH - 8; + const s = stageSvg(host, 'rsvg', vw, vh), cy = winY + winH / 2, step = 17; + grad('dkaRail', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]); + grad('dkaPaper', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]); + grad('dkaCurve', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.4)'], ['.28', 'rgba(0,0,0,0)'], ['.72', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.4)']]); + const localDefs = svgEl(s, 'defs', {}); + if (opts.title !== '') svgEl(s, 'text', { x: vw / 2, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = opts.title || 'EQUALIZER'; + const grps = []; + const label = () => chans.map(c => `${c.name} ${Math.round(c.v)}`).join(' · '); + const set = (i, v) => { + v = Math.max(MIN, Math.min(MAX, v)); chans[i].v = v; + const p = grps[i]; + p.nums.style.transform = `translateY(${(v - p.base) * step}px)`; + p.chip.textContent = Math.round(v); + onChange(chans.map(c => c.v), label()); + }; + const x0 = (vw - (N - 1) * 50) / 2; /* drums centered whatever the count */ + chans.forEach((ch, i) => { + const x = x0 + i * 50, cid = uid('dkaClip'); + svgEl(s, 'text', { x: x + 18, y: winY + 8, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '+'; + svgEl(s, 'text', { x: x + 18, y: winY + winH - 2, 'text-anchor': 'middle', 'font-size': 8, fill: 'var(--steel)' }).textContent = '−'; + svgEl(s, 'rect', { x: x - 12, y: railY, width: 24, height: railH, rx: 4, fill: 'url(#dkaRail)', stroke: '#0a0908', 'stroke-width': 1.2 }); + const clip = svgEl(localDefs, 'clipPath', { id: cid }); + svgEl(clip, 'rect', { x: x - 9, y: winY, width: 18, height: winH }); + svgEl(s, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#dkaPaper)' }); + const g = svgEl(s, 'g', { 'clip-path': `url(#${cid})` }); + const nums = svgEl(g, 'g', {}); nums.style.transition = 'transform .12s'; + for (let n = MIN; n <= MAX; n++) + svgEl(nums, 'text', { x, y: cy + 3 + (ch.v - n) * step, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' }).textContent = n; + svgEl(g, 'rect', { x: x - 9, y: winY, width: 18, height: winH, fill: 'url(#dkaCurve)', 'pointer-events': 'none' }); + svgEl(s, 'rect', { x: x - 8, y: cy - 6.5, width: 16, height: 13, rx: 1.5, fill: '#1a1613', opacity: .92 }); + const chip = svgEl(s, 'text', { x, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9, 'font-weight': 700, 'font-family': 'var(--mono)', fill: 'var(--cream)' }); + svgEl(s, 'text', { x, y: vh - 4, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = ch.name; + const hit = svgEl(s, 'rect', { x: x - 14, y: railY, width: 28, height: railH, fill: 'transparent' }); + hit.style.cursor = 'ns-resize'; + grps.push({ nums, chip, base: ch.v }); + dragDelta(hit, () => chans[i].v, v => set(i, v), { min: MIN, max: MAX, sens: (MAX - MIN) / (winH * 1.1) }); + }); + chans.forEach((c, i) => set(i, c.v)); + return { el: s, get: () => chans.map(c => c.v), set }; + }; + + /* UPGRADE — guarded toggle, realistic throw. Stock animates the lever with + a 180° planar rotate, so mid-transition the handle points sideways (the + Linda Blair problem — Craig, 2026-07-21). A real switch bat flips toward + the viewer: this redefinition throws with rotateX, so the lever + foreshortens through the pivot and re-extends downward, never sweeping + sideways. Contract unchanged from stock: opts onLabel/offLabel/on/ + onChange(on, 'ON'|'OFF'); handle {el, get, set}. */ + DUPRE.guardedToggle = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 90, 100), cx = 45, cy = 52; + grad('dkaDiscRed', 'radialGradient', { cx: '42%', cy: '34%', r: '85%' }, [['0', '#d98a6f'], ['.65', 'var(--fail)'], ['1', '#7a2a1a']]); + grad('dkaNut', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#8a8578'], ['1', '#4e4a42']]); + grad('dkaChrome', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#7e7a70'], ['.45', '#e8e5db'], ['1', '#8a867c']]); + grad('dkaTip', 'radialGradient', { cx: '40%', cy: '32%', r: '75%' }, [['0', '#f2efe8'], ['1', '#7e7a70']]); + const lblOn = svgEl(s, 'text', { x: cx, y: 12, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' }); + lblOn.textContent = opts.onLabel || 'ON'; + const lblOff = svgEl(s, 'text', { x: cx, y: 96, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }); + lblOff.textContent = opts.offLabel || 'OFF'; + svgEl(s, 'circle', { cx, cy, r: 13, fill: 'url(#dkaDiscRed)', stroke: '#4e150a', 'stroke-width': 1 }); + const pts = []; for (let k = 0; k < 6; k++) { const a = (k * 60 + 30) * Math.PI / 180; pts.push((cx + 11 * Math.cos(a)) + ',' + (cy + 11 * Math.sin(a))); } + svgEl(s, 'polygon', { points: pts.join(' '), fill: 'url(#dkaNut)', stroke: '#2b2822' }); + svgEl(s, 'circle', { cx, cy, r: 6, fill: '#14110e', stroke: '#000' }); + for (const gx of [16, 74]) { + svgEl(s, 'rect', { x: gx - 6, y: cy - 24, width: 12, height: 48, rx: 6, fill: 'url(#dkaNut)', stroke: '#2b2822', 'stroke-width': 1 }); + svgEl(s, 'rect', { x: gx - 4, y: cy - 21, width: 3.5, height: 42, rx: 2, fill: 'rgba(255,255,255,.14)' }); + } + const lever = svgEl(s, 'g', {}); + svgEl(lever, 'path', { d: `M ${cx - 3} ${cy} L ${cx - 2} 26 L ${cx + 2} 26 L ${cx + 3} ${cy} Z`, fill: 'url(#dkaChrome)', stroke: '#4e4a42', 'stroke-width': .5 }); + svgEl(lever, 'rect', { x: cx - 4.5, y: 16, width: 9, height: 12, rx: 2, fill: 'url(#dkaTip)', stroke: '#5e5a52', 'stroke-width': .6 }); + for (let r = 0; r < 4; r++) svgEl(lever, 'line', { x1: cx - 3.5, y1: 18.5 + r * 2.4, x2: cx + 3.5, y2: 18.5 + r * 2.4, stroke: 'rgba(0,0,0,.25)', 'stroke-width': .8 }); + lever.style.transformBox = 'view-box'; + lever.style.transformOrigin = `${cx}px ${cy}px`; + lever.style.transition = 'transform .16s ease-in-out'; + let on; + const set = v => { + on = !!v; + /* rotateX: the bat flips through the pivot plane (foreshortens, then + re-extends downward) — the head-on view of a real throw. */ + lever.style.transform = on ? 'rotateX(0deg)' : 'rotateX(180deg)'; + lblOn.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--dim)'); + lblOff.setAttribute('fill', on ? 'var(--dim)' : 'var(--gold-hi)'); + onChange(on, on ? 'ON' : 'OFF'); + }; + s.addEventListener('click', () => set(!on)); + set(opts.on !== undefined ? opts.on : true); + return { el: s, get: () => on, set }; + }; + + /* UPGRADE — bat toggle, realistic throw. Same disease and same cure as + guardedToggle: stock spins the bat 180° in the plane (sideways at the + midpoint); this flips it with rotateX like a real switch bat. Contract + unchanged: opts onLabel/offLabel/on/onChange(on, 'ON'|'OFF'); handle + {el, get, set}. */ + DUPRE.batToggle = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const s = stageSvg(host, 'rsvg press', 70, 90), cx = 35, cy = 44; + grad('dkaNut', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#8a8578'], ['1', '#4e4a42']]); + grad('dkaChrome', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#7e7a70'], ['.45', '#e8e5db'], ['1', '#8a867c']]); + grad('dkaTip', 'radialGradient', { cx: '40%', cy: '32%', r: '75%' }, [['0', '#f2efe8'], ['1', '#7e7a70']]); + const lblOn = svgEl(s, 'text', { x: cx, y: 9, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--gold-hi)' }); + lblOn.textContent = opts.onLabel || 'ON'; + const lblOff = svgEl(s, 'text', { x: cx, y: 87, 'text-anchor': 'middle', 'font-size': 7, 'letter-spacing': '.1em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }); + lblOff.textContent = opts.offLabel || 'OFF'; + const pts = []; for (let k = 0; k < 6; k++) { const a = (k * 60 + 30) * Math.PI / 180; pts.push((cx + 17 * Math.cos(a)) + ',' + (cy + 17 * Math.sin(a))); } + svgEl(s, 'polygon', { points: pts.join(' '), fill: 'url(#dkaNut)', stroke: '#2b2822' }); + svgEl(s, 'circle', { cx, cy, r: 9, fill: '#14110e', stroke: '#000' }); + const lever = svgEl(s, 'g', {}); + svgEl(lever, 'path', { d: `M ${cx - 3.2} ${cy} L ${cx - 1.8} 21 L ${cx + 1.8} 21 L ${cx + 3.2} ${cy} Z`, fill: 'url(#dkaChrome)', stroke: '#4e4a42', 'stroke-width': .5 }); + svgEl(lever, 'ellipse', { cx, cy: 17, rx: 6.5, ry: 8, fill: 'url(#dkaTip)', stroke: '#5e5a52', 'stroke-width': .6 }); + lever.style.transformBox = 'view-box'; + lever.style.transformOrigin = `${cx}px ${cy}px`; + lever.style.transition = 'transform .16s ease-in-out'; + let on; + const set = v => { + on = !!v; + lever.style.transform = on ? 'rotateX(0deg)' : 'rotateX(180deg)'; + lblOn.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--dim)'); + lblOff.setAttribute('fill', on ? 'var(--dim)' : 'var(--gold-hi)'); + onChange(on, on ? 'ON' : 'OFF'); + }; + s.addEventListener('click', () => set(!on)); + set(opts.on !== undefined ? opts.on : true); + return { el: s, get: () => on, set }; + }; + + /* NEW — n-position slide switch with a status lamp per position. + pillSlide's full-size descendant: the pill slide is a fixed 110px SVG + with mix LEDs; this one is DOM-built so it stretches to any width, and + each position carries its own lamp + label (only the active one lit). + + Contract (everything a consumer needs; no page globals touched): + opts: items (position labels, default ['A','B','C']); index (initial + position, default 0); width (px, default 220 — set it to match + a neighboring instrument); knob ('chrome' default, or 'dark' + for a machined-black cap); onChange(index, label) fires on + every set, including the initial paint. + handle: el, get() (the index), set(i) (clamped); click or drag lands + on the nearest position. + CSS lives in the additions block (.dupre-slsel); the lamp is + self-styled (the shared dupre-lamp is display-managed by consumers). */ + DUPRE.slideSelector = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const items = opts.items || ['A', 'B', 'C']; + const N = items.length; + const el = document.createElement('div'); el.className = 'dupre-slsel' + (opts.knob === 'dark' ? ' sl-dark' : ''); + if (opts.width) el.style.width = opts.width + 'px'; + const cols = document.createElement('div'); cols.className = 'sl-cols'; + cols.style.gridTemplateColumns = `repeat(${N},1fr)`; + const colEls = items.map(lbl => { + const c = document.createElement('div'); c.className = 'sl-col'; + c.innerHTML = `${lbl}`; + cols.appendChild(c); return c; + }); + el.appendChild(cols); + const track = document.createElement('div'); track.className = 'sl-track'; + const knob = document.createElement('div'); knob.className = 'sl-knob'; + track.appendChild(knob); el.appendChild(track); + host.appendChild(el); + knob.style.width = `calc(${100 / N}% - 8px)`; + let idx; + const set = i => { + idx = Math.max(0, Math.min(N - 1, i | 0)); + knob.style.left = ((idx + 0.5) / N * 100) + '%'; + colEls.forEach((c, k) => c.classList.toggle('sl-on', k === idx)); + onChange(idx, items[idx]); + }; + const pick = e => { + const r = track.getBoundingClientRect(); + set(Math.floor(((e.clientX - r.left) / r.width) * N)); + }; + el.addEventListener('pointerdown', e => { + el.setPointerCapture(e.pointerId); + pick(e); + const move = m => pick(m); + const up = () => { el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); }; + el.addEventListener('pointermove', move); + el.addEventListener('pointerup', up); + e.preventDefault(); + }); + set(opts.index !== undefined ? opts.index : 0); + return { el, get: () => idx, set }; + }; + + /* UPGRADE — slide-rule dial with a width option. Stock prints its stops + at hardcoded x positions inside a fixed 180px window; this redefinition + spreads majors evenly across opts.width (default keeps stock geometry). + Contract otherwise unchanged: values/value/index/fmt/skin, + onChange(v, label); handle {el, get, set, setStyle}; unit stops still + interpolate between adjacent numeric majors; ←/→ step. */ + (() => { + const OLD_STYLES = DUPRE.slideRule.STYLES; + 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); + const W = opts.width || 180; + /* custom widths get a wider edge margin so end-stop labels stay inside */ + const M = opts.width ? 20 : 12; + const X = majors.map((_, i) => M + i * (W - 2 * M) / (majors.length - 1)); + const stops = []; + majors.forEach((v, i) => { + stops.push({ v, x: X[i], major: true }); + const b = majors[i + 1]; + if (typeof v === 'number' && typeof b === 'number') + for (let u = v + 1; u < b; u++) + stops.push({ v: u, x: X[i] + (X[i + 1] - X[i]) * (u - v) / (b - v), major: false }); + }); + const t = document.createElement('span'); t.className = 'dupre-tuner'; + if (opts.width) t.style.width = W + 'px'; + t.tabIndex = 0; t.setAttribute('role', 'slider'); t.setAttribute('aria-label', 'slide-rule value'); + const setStyle = (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); + }; + setStyle('skin', opts.skin || 'warm'); + stops.forEach(sp => { + t.insertAdjacentHTML('beforeend', sp.major + ? `${sp.v}` + : ``); + }); + const ndl = document.createElement('span'); ndl.className = 'dupre-ndl'; t.appendChild(ndl); + host.appendChild(t); + let idx; + const set = i => { + idx = Math.max(0, Math.min(stops.length - 1, i)); + ndl.style.left = stops[idx].x + 'px'; + t.querySelectorAll('.dupre-mk').forEach(m => + m.classList.toggle('dupre-on', m.textContent === String(stops[idx].v))); + onChange(stops[idx].v, fmt(stops[idx].v)); + }; + t.addEventListener('click', e => { + const r = t.getBoundingClientRect(); + const x = (e.clientX - r.left) * (t.offsetWidth / r.width); + let best = 0, bd = 1e9; stops.forEach((sp, j) => { const d = Math.abs(sp.x - x); if (d < bd) { bd = d; best = j; } }); + set(best); t.focus(); + }); + t.addEventListener('keydown', e => { + if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); set(idx - 1); } + else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); set(idx + 1); } + }); + const initV = opts.value !== undefined ? opts.value : majors[opts.index !== undefined ? opts.index : 2]; + const init = stops.findIndex(sp => sp.v === initV); + set(init < 0 ? 0 : init); + return { el: t, get: () => stops[idx].v, set, setStyle }; + }; + DUPRE.slideRule.STYLES = OLD_STYLES; + })(); + + /* UPGRADE — rocker with a `quiet` option. Stock paints the off state's + bottom half warning-red, which reads as a fault for switches whose off + state is unremarkable (auto-dim off is not an alarm). quiet: true keeps + the off half plain dark. Red-off stays the default for switches where + off IS the notable state. Contract otherwise unchanged. + Intended pairing: onLabel carries the switch's FUNCTION name (lit when + engaged), offLabel is empty — state reads from light and position, not + from ON/OFF prose. opts.accent: 'amber' (default) or 'green' — green + for run/engaged semantics per the kit accent family. */ + DUPRE.rocker = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const r = document.createElement('span'); r.className = 'dupre-rocker' + + (opts.quiet ? ' dupre-quiet' : '') + (opts.accent === 'green' ? ' dupre-green' : ''); + r.innerHTML = `${opts.onLabel !== undefined ? opts.onLabel : 'ON'}${opts.offLabel !== undefined ? opts.offLabel : 'OFF'}`; + host.appendChild(r); + let on; + const set = v => { on = !!v; r.classList.toggle('dupre-on', on); onChange(on, on ? 'ON' : 'OFF'); }; + r.addEventListener('click', () => set(!on)); + set(opts.on !== undefined ? opts.on : true); + return { el: r, get: () => on, set }; + }; + + /* NEW — program chart: the washing-machine faceplate matrix, now a punch + card. One row per program, one column per contact; a filled WHITE dot + means the program will close it on recall, a ring means it will open + it, a dash means it leaves it alone; letter cells carry enums. The dots + are future state, so they light white — deliberately outside the amber + live-state family. With editable: true, clicking a cell reprograms the + preset: dot cells cycle set → clear → untouched, letter cells cycle + the `letters` list; onEdit reports the change and the consumer owns + the stored program. + + Contract (everything a consumer needs; no page globals touched): + opts: cols (column labels); rows ({ name, cells } aligned with cols: + true dot / false ring / null dash / string letter); active + (lit row, -1 none); width (px, default 260); editable (false); + letters (cycle for letter columns, default ['P','B','S',null]); + onEdit(rowIdx, colIdx, newValue) on every cell edit; + onChange(index, row name | 'custom') on every set, including + the initial paint. + handle: el, get() (active index), set(i) (-1..rows-1). + CSS lives in the additions block (.dupre-pchart); no other styles + involved. */ + DUPRE.programChart = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const onEdit = opts.onEdit || noop; + const cols = opts.cols || ['A', 'B', 'C']; + const rows = (opts.rows || []).map(r => ({ name: r.name, cells: r.cells.slice() })); + const LETTERS = opts.letters || ['P', 'B', 'S', null]; + /* a column is a letter column if any row carries a string there */ + const letterCol = ci => rows.some(r => typeof r.cells[ci] === 'string'); + const el = document.createElement('div'); + el.className = 'dupre-pchart' + (opts.editable ? ' pc-edit' : ''); + el.style.width = (opts.width || 260) + 'px'; + const grid = document.createElement('div'); grid.className = 'pc-grid'; + grid.style.gridTemplateColumns = `minmax(52px,auto) repeat(${cols.length},1fr)`; + const cell = cls => { const c = document.createElement('div'); c.className = 'pc-cell' + (cls ? ' ' + cls : ''); grid.appendChild(c); return c; }; + cell('pc-h'); + cols.forEach(c => { cell('pc-h').textContent = c; }); + const paintCell = (c, v) => { + c.classList.remove('pc-pwr'); + if (v === true) c.innerHTML = ''; + else if (v === false) c.innerHTML = ''; + else if (typeof v === 'string') { c.classList.add('pc-pwr'); c.textContent = v; } + else c.innerHTML = ''; + }; + const rowEls = rows.map((r, ri) => { + const els = [cell('pc-name')]; + els[0].textContent = r.name; + cols.forEach((_, ci) => { + const c = cell(); + paintCell(c, r.cells[ci]); + if (opts.editable) c.addEventListener('click', () => { + const v = rows[ri].cells[ci]; + let nv; + if (letterCol(ci)) { + const cur = LETTERS.indexOf(v === undefined ? null : v); + nv = LETTERS[(cur + 1) % LETTERS.length]; + } else if (v === true) nv = false; + else if (v === false) nv = null; + else nv = true; + rows[ri].cells[ci] = nv; + paintCell(c, nv); + onEdit(ri, ci, nv); + }); + els.push(c); + }); + return els; + }); + el.appendChild(grid); host.appendChild(el); + let idx; + const set = i => { + idx = Math.max(-1, Math.min(rows.length - 1, i)); + rowEls.forEach((els, ri) => els.forEach(c => c.classList.toggle('pc-row-on', ri === idx))); + onChange(idx, idx < 0 ? 'custom' : rows[idx].name); + }; + set(opts.active !== undefined ? opts.active : -1); + return { el, get: () => idx, set }; + }; + + /* NEW — horizontal thumb-slide attenuator. The vertical thumbSlide turned + on its side: one channel, a dark rail with a chrome thumb tab, printed + scale beneath. Continuous by default; pass positions for a detented + selector (the three-position switch case) — the tab snaps and the + active position label lights. + + Contract (everything a consumer needs; no page globals touched): + opts: width (px, default 220); value (0-100, default 50 — continuous + mode); positions (label array — switches to detent mode); + index (initial detent, default 0); onChange — continuous: + (value, 'N'); detented: (index, label). Fires on every set, + including the initial paint. + handle: el, get() (value or index), set(v|i). Drag or click anywhere + on the rail. + SVG-built: styling is inline attributes plus the shared .rsvg stage + block of DUPRE_CSS; gradients are additions-scoped until merge. */ + DUPRE.thumbSlideH = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const W = opts.width || 220; + const POS = opts.positions || null; + const H = 36, x0 = 14, x1 = W - 14, railY = 14; + grad('dkaThRail', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#3a3733'], ['.5', '#211f1c'], ['1', '#161412']]); + grad('dkaThTab', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f2efe6'], ['1', '#c6c1b3']]); + const s = stageSvg(host, 'rsvg', W, H); + svgEl(s, 'rect', { x: x0 - 8, y: railY - 7, width: x1 - x0 + 16, height: 14, rx: 3, fill: 'url(#dkaThRail)', stroke: '#0a0908', 'stroke-width': 1 }); + svgEl(s, 'rect', { x: x0 - 3, y: railY - 3, width: x1 - x0 + 6, height: 6, fill: '#0d0a08', stroke: '#050404' }); + const labels = []; + const xAt = f => x0 + f * (x1 - x0); + if (POS) { + POS.forEach((lbl, i) => { + const x = xAt(POS.length === 1 ? 0.5 : i / (POS.length - 1)); + svgEl(s, 'line', { x1: x, y1: railY + 5, x2: x, y2: railY + 8, stroke: 'var(--steel)', 'stroke-width': 1 }); + const t = svgEl(s, 'text', { x, y: H - 3, 'text-anchor': 'middle', 'font-size': 6, 'letter-spacing': '.08em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }); + t.textContent = lbl; labels.push(t); + }); + } else { + for (let n = 0; n <= 10; n++) { + const x = xAt(n / 10); + svgEl(s, 'line', { x1: x, y1: railY + 5, x2: x, y2: railY + (n % 2 ? 6.5 : 8), stroke: 'var(--steel)', 'stroke-width': n % 2 ? .5 : .9, opacity: n % 2 ? .5 : .8 }); + if (n % 2 === 0) { + const t = svgEl(s, 'text', { x, y: H - 3, 'text-anchor': 'middle', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: .55 }); + t.textContent = n * 10; labels.push(t); + } + } + } + const tab = svgEl(s, 'g', {}); + svgEl(tab, 'rect', { x: -6, y: railY - 9, width: 12, height: 18, rx: 2.5, fill: 'url(#dkaThTab)', stroke: '#7a766a', 'stroke-width': .6 }); + svgEl(tab, 'rect', { x: -1, y: railY - 6.5, width: 2, height: 13, fill: 'rgba(0,0,0,.25)' }); + let val; + const paint = () => { + const f = POS ? (POS.length === 1 ? 0.5 : val / (POS.length - 1)) : val / 100; + tab.setAttribute('transform', `translate(${xAt(f)},0)`); + if (POS) labels.forEach((t, i) => { + t.setAttribute('fill', i === val ? 'var(--gold-hi)' : 'var(--dim)'); + }); + else labels.forEach((t, i) => { + const on = Math.abs(i * 20 - val) <= 10; + t.setAttribute('fill', on ? 'var(--gold-hi)' : 'var(--gold)'); t.setAttribute('opacity', on ? '1' : '.55'); + }); + }; + const set = v => { + val = POS ? Math.max(0, Math.min(POS.length - 1, Math.round(v))) + : Math.max(0, Math.min(100, Math.round(v))); + paint(); + if (POS) onChange(val, POS[val]); + else onChange(val, String(val)); + }; + const fromPointer = e => { + const r = s.getBoundingClientRect(); + const f = Math.max(0, Math.min(1, ((e.clientX - r.left) * (W / r.width) - x0) / (x1 - x0))); + set(POS ? f * (POS.length - 1) : f * 100); + }; + s.style.cursor = 'ew-resize'; s.style.touchAction = 'none'; + s.addEventListener('pointerdown', e => { + s.setPointerCapture(e.pointerId); + fromPointer(e); + const move = m => fromPointer(m); + const up = () => { s.removeEventListener('pointermove', move); s.removeEventListener('pointerup', up); }; + s.addEventListener('pointermove', move); + s.addEventListener('pointerup', up); + e.preventDefault(); + }); + set(POS ? (opts.index !== undefined ? opts.index : 0) : (opts.value !== undefined ? opts.value : 50)); + return { el: s, get: () => val, set }; + }; + + /* UPGRADE — pin routing matrix, generalized. Stock is a fixed 160×110 + five-by-six with gold pins, text heads, and no set(). This + redefinition keeps those defaults exactly and adds: width/dy geometry, + colColors (pin fill per column), colHeads: 'button' (column labels + become clickable key plates; setActiveCol lights one), groups (row + counts drawing hairline dividers between row groups), set(key, on), + and an onPin convenience callback. + + Contract (everything a consumer needs; no page globals touched): + opts: rows / cols (labels); pins ('ROW>COL' keys); width (px — + custom geometry mode; omit for stock); dy (row pitch, default + 16 in custom mode); x0 (label gutter, default 56 in custom + mode); colColors (css fill per column index, default gold); + colHeads ('text' default | 'button'); activeCol (initial lit + head, -1); groups (e.g. [4,3] → divider after row 4); + onChange(pins array, label) on every seat/pull including the + initial paint; onPin(rowLabel, colLabel, seated) per click; + onColHead(colIndex) when a button head is clicked. + handle: el, get() (pin keys), set(key, on), setActiveCol(i). + SVG-built: styling is inline attributes plus the shared .rsvg stage + block of DUPRE_CSS. */ + DUPRE.pinMatrix = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const onPin = opts.onPin || noop; + const onColHead = opts.onColHead || noop; + const ROWS = opts.rows || ['OSC1', 'OSC2', 'LFO', 'NOIS', 'ENV']; + const COLS = opts.cols || ['VCF', 'VCA', 'PAN', 'DLY', 'OUT', 'MOD']; + const custom = !!opts.width; + const W = opts.width || 160; + const X0 = custom ? (opts.x0 || 56) : 42; + const DY = custom ? (opts.dy || 16) : 15.5; + const Y0 = custom ? (opts.colHeads === 'button' ? 40 : 28) : 30; + /* button heads are wider than a pin column; leave them room at the edge */ + const DX = custom ? (W - X0 - (opts.colHeads === 'button' ? 36 : 16)) / (COLS.length - 1) : 19; + const H = custom ? Y0 + (ROWS.length - 1) * DY + 14 : 110; + const COLC = opts.colColors || []; + const groups = opts.groups || null; + const pins = new Set((opts.pins || (custom ? [] : ['OSC1>VCF', 'LFO>PAN', 'ENV>VCA'])) + .filter(k => { const [r, c] = k.split('>'); return ROWS.includes(r) && COLS.includes(c) && k === r + '>' + c; })); + const s = stageSvg(host, 'rsvg press', W, H); + svgEl(s, 'rect', { x: 2, y: 2, width: W - 4, height: H - 4, rx: 8, fill: '#1c1916', stroke: '#060505', 'stroke-width': 1.5 }); + let activeCol = opts.activeCol !== undefined ? opts.activeCol : -1; + const headEls = []; + COLS.forEach((c, j) => { + const cx = X0 + j * DX; + if (opts.colHeads === 'button') { + const bw = Math.min(DX - 4, 58); + const g = svgEl(s, 'g', {}); + const rect = svgEl(g, 'rect', { x: cx - bw / 2, y: 8, width: bw, height: 20, rx: 5, + fill: '#131110', stroke: '#33302b', 'stroke-width': 1 }); + const txt = svgEl(g, 'text', { x: cx, y: 21, 'text-anchor': 'middle', 'font-size': 6, + 'letter-spacing': '.04em', 'font-family': 'var(--mono)', fill: 'var(--silver)' }); + txt.textContent = c; + g.style.cursor = 'pointer'; + g.addEventListener('click', () => onColHead(j)); + headEls.push({ rect, txt }); + } else { + svgEl(s, 'text', { x: cx, y: custom ? 18 : 16, 'text-anchor': 'middle', 'font-size': custom ? 6 : 5.4, + 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = c; + headEls.push(null); + } + }); + const paintHeads = () => headEls.forEach((he, j) => { + if (!he) return; + he.rect.setAttribute('stroke', j === activeCol ? 'var(--gold)' : '#33302b'); + he.rect.setAttribute('fill', j === activeCol ? '#2a2618' : '#131110'); + he.txt.setAttribute('fill', j === activeCol ? 'var(--cream)' : 'var(--silver)'); + }); + paintHeads(); + ROWS.forEach((r, i) => svgEl(s, 'text', { x: X0 - 12, y: Y0 + i * DY + 2, 'text-anchor': 'end', + 'font-size': custom ? 6 : 5.4, 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = r); + if (groups) { + let acc = 0; + groups.slice(0, -1).forEach(n => { + acc += n; + const y = Y0 + (acc - 0.5) * DY; + svgEl(s, 'line', { x1: 10, y1: y, x2: W - 10, y2: y, stroke: '#2c2820', 'stroke-width': 1 }); + }); + } + const caps = {}; + ROWS.forEach((r, i) => COLS.forEach((c, j) => { + const cx = X0 + j * DX, cy = Y0 + i * DY, key = r + '>' + c; + svgEl(s, 'circle', { cx, cy, r: 3.4, fill: '#0a0908', stroke: '#2c2820', 'stroke-width': 1 }); + const fill = COLC[j] || 'var(--gold)'; + const cap = svgEl(s, 'circle', { cx, cy, r: 4.4, fill, stroke: 'rgba(0,0,0,.45)', 'stroke-width': 1, + opacity: pins.has(key) ? 1 : 0, filter: 'drop-shadow(0 1px 2px rgba(0,0,0,.6))' }); + caps[key] = cap; + const hit = svgEl(s, 'circle', { cx, cy, r: Math.min(8, DX / 2 - 1), fill: 'transparent' }); + hit.style.cursor = 'pointer'; + hit.addEventListener('click', () => { + const on = !pins.has(key); + if (on) pins.add(key); else pins.delete(key); + cap.setAttribute('opacity', on ? 1 : 0); + onPin(r, c, on); + onChange([...pins], (on ? r + ' → ' + c : 'pulled ' + r + ' → ' + c) + ' · ' + pins.size + ' routes'); + }); + })); + onChange([...pins], pins.size + ' routes'); + return { + el: s, + get: () => [...pins], + set: (key, on) => { + if (!caps[key]) return; + if (on) pins.add(key); else pins.delete(key); + caps[key].setAttribute('opacity', on ? 1 : 0); + }, + setActiveCol: i => { activeCol = i; paintHeads(); }, + }; + }; + + /* NEW — programmable matrix. The pin routing matrix specialized into a + program store: each column is a stored program, each row a setting; a + WHITE jewel pin means the program includes that setting. Pressing a + head key activates the program and the active column renders its + verdicts — active-is-live: a seated pin becomes a green jewel, an + empty socket lights a smaller red one (the program holds it off). + Editing the active column edits the present; a white column, a + future. + + Head keys are the kit console-key primitive (.dupre-key), lighting + GREEN when their program is active. Programs may be strings or + { id, label, glyph, slim } — a glyph renders large on the key in + place of the label (label still names the program in callbacks); + blank labels make unassigned slots; ids keep pin keys unique. Rows are strings (pin rows) or { label, values } + — a LETTER ROW: every program cell is a tape-counter wheel (cream + paper, bezeled window, printed glyph from `values`); clicking a wheel + cycles that program's value; the active column's wheel IS the live + value and carries no highlight (paper doesn't glow). + + Contract (everything a consumer needs; no page globals touched): + opts: rows (strings | {label, values}); programs (strings | + {id, label}); pins ('ROW>PROGID' keys); letters + ({'ROW>PROGID': glyph}); radios (row-label arrays: one pin + per program in the group; a radio pin moves, never pulls); + groups (row counts → hairline dividers); active (program + index, -1); width / x0 / dy (508/128/23-ish); engraved (row + labels as infilled relief); onActivate(index, programId) on + head press; onPin(rowLabel, programId, seated, isActive); + onLetter(rowLabel, programId, glyph, isActive) on wheel click. + handle: el (the wrap), get() (pins), set(row, programId, on), + setLetter(row, programId, glyph), setActive(i), getActive(). + Composite-built: DOM console keys over an SVG pin field. */ + DUPRE.programMatrix = function (host, opts = {}) { + const onActivate = opts.onActivate || noop; + const onPin = opts.onPin || noop; + const onLetter = opts.onLetter || noop; + const ROWS = (opts.rows || []).map(r => typeof r === 'string' ? { label: r } : r); + const PROGS = (opts.programs || ['P1', 'P2']).map(p => typeof p === 'string' ? { id: p, label: p } : p); + /* slim columns (e.g. unassigned slots) take a narrower band */ + const weights = PROGS.map(p => p.slim ? 0.55 : 1); + const wSum = weights.reduce((a, b) => a + b, 0); + const radios = (opts.radios || []).map(g => g.slice()); + const W = opts.width || 306; + const X0 = opts.x0 || 104; + const DY = opts.dy || 21; + const Y0 = 52; + const span = W - X0 - 30; + const colX = []; /* column centers by cumulative weight bands */ + { let acc = 0; weights.forEach(w => { colX.push(X0 - 14 + (acc + w / 2) / wSum * span); acc += w; }); } + const colW = weights.map(w => w / wSum * span); + const DX = colW.length > 1 ? Math.min(...colW) : span; /* hit-radius basis */ + const H = Y0 + (ROWS.length - 1) * DY + 18; + const WHITE = '#cfd3cf', GREEN = '#a9c95f'; + grad('dkaJewelG', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#dcebae'], ['.5', '#a9c95f'], ['1', '#55702a']]); + grad('dkaJewelR', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#e0937e'], ['.5', '#c0503a'], ['1', '#6e2517']]); + grad('dkaJewelW', 'radialGradient', { cx: '38%', cy: '30%', r: '80%' }, [['0', '#f2f4f2'], ['.55', '#cfd3cf'], ['1', '#8e948e']]); + grad('dkaPaper2', 'linearGradient', { x1: 0, y1: 0, x2: 1, y2: 0 }, [['0', '#cfc6a8'], ['.5', '#ece4c8'], ['1', '#c6bd9e']]); + grad('dkaCurve2', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', 'rgba(0,0,0,.45)'], ['.3', 'rgba(0,0,0,0)'], ['.7', 'rgba(0,0,0,0)'], ['1', 'rgba(0,0,0,.45)']]); + const pins = new Set(opts.pins || []); + const letters = Object.assign({}, opts.letters || {}); + let active = opts.active !== undefined ? opts.active : -1; + /* container: a DOM head row of kit console keys over the SVG pin field */ + const wrap = document.createElement('div'); + wrap.style.cssText = 'position:relative;width:' + W + 'px'; + host.appendChild(wrap); + const s = stageSvg(wrap, 'rsvg press', W, H); + svgEl(s, 'rect', { x: 2, y: 2, width: W - 4, height: H - 4, rx: 8, fill: '#0e0c0a', stroke: '#060505', 'stroke-width': 1.5 }); + const heads = PROGS.map((pgm, j) => { + const cx = colX[j]; + const headBw = Math.min(colW[j] - 8, 82); + const b = document.createElement('button'); + b.className = 'dupre-key'; + b.textContent = pgm.glyph || pgm.label; + if (pgm.glyph) b.style.fontSize = '15px'; + b.style.cssText = 'position:absolute;top:8px;left:' + (cx - headBw / 2) + 'px;width:' + headBw + + 'px;height:24px;padding:0;text-align:center;font-size:7.5px;letter-spacing:.08em;box-sizing:border-box'; + b.addEventListener('click', () => { b.blur(); onActivate(j, pgm.id); }); + wrap.appendChild(b); + return b; + }); + /* row labels right-align to the first head's LEFT edge; engraved-and- + infilled at faceplate size */ + const labelX = colX[0] - Math.min(colW[0] - 8, 82) / 2 - 4; + ROWS.forEach((r, i) => { + const y = Y0 + i * DY + 2; + if (opts.engraved) { + svgEl(s, 'text', { x: labelX, y: y - 1, 'text-anchor': 'end', 'font-size': 7.6, + 'letter-spacing': '.1em', 'font-weight': 600, 'font-family': 'var(--mono)', fill: 'rgba(0,0,0,.8)' }).textContent = r.label; + svgEl(s, 'text', { x: labelX, y, 'text-anchor': 'end', 'font-size': 7.6, + 'letter-spacing': '.1em', 'font-weight': 600, 'font-family': 'var(--mono)', fill: '#a59d8c' }).textContent = r.label; + } else { + svgEl(s, 'text', { x: labelX, y, 'text-anchor': 'end', 'font-size': 6, + 'letter-spacing': '.05em', 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = r.label; + } + }); + if (opts.groups) { + let acc = 0; + opts.groups.slice(0, -1).forEach(n => { + acc += n; + const y = Y0 + (acc - 0.5) * DY; + svgEl(s, 'line', { x1: 10, y1: y, x2: W - 10, y2: y, stroke: '#2c2820', 'stroke-width': 1 }); + }); + } + const caps = {}, letterEls = {}; + const paint = () => { + heads.forEach((b, j) => { + b.classList.remove('dupre-on', 'dupre-green', 'dupre-red'); + if (j === active) b.classList.add('dupre-green'); + }); + ROWS.forEach(r => PROGS.forEach((pgm, j) => { + const key = r.label + '>' + pgm.id; + if (r.values) { /* wheels carry no live highlight — paper doesn't glow */ } + else if (caps[key]) { + const seated = pins.has(key); + if (j === active) { + caps[key].setAttribute('fill', seated ? 'url(#dkaJewelG)' : 'url(#dkaJewelR)'); + caps[key].setAttribute('opacity', 1); + caps[key].setAttribute('r', seated ? 5.6 : 4.8); + } else { + caps[key].setAttribute('fill', 'url(#dkaJewelW)'); + caps[key].setAttribute('opacity', seated ? 1 : 0); + caps[key].setAttribute('r', 5.6); + } + } + })); + }; + ROWS.forEach((r, i) => { + const cy = Y0 + i * DY; + PROGS.forEach((pgm, j) => { + const cx = colX[j], key = r.label + '>' + pgm.id; + if (r.values) { + svgEl(s, 'rect', { x: cx - 13, y: cy - 10.5, width: 26, height: 21, rx: 3, + fill: '#0a0908', stroke: '#3a352c', 'stroke-width': 1.2 }); + svgEl(s, 'rect', { x: cx - 10, y: cy - 7.5, width: 20, height: 15, fill: 'url(#dkaPaper2)' }); + const t = svgEl(s, 'text', { x: cx, y: cy + 3.8, 'text-anchor': 'middle', 'font-size': 11, + 'font-weight': 700, 'font-family': 'var(--mono)', fill: '#2b2418' }); + t.textContent = letters[key] !== undefined ? letters[key] : r.values[0]; + svgEl(s, 'rect', { x: cx - 10, y: cy - 7.5, width: 20, height: 15, fill: 'url(#dkaCurve2)', 'pointer-events': 'none' }); + letterEls[key] = { t }; + const hit = svgEl(s, 'rect', { x: cx - 12, y: cy - 10, width: 24, height: 20, fill: 'transparent' }); + hit.style.cursor = 'pointer'; + hit.addEventListener('click', () => { + const cur = r.values.indexOf(t.textContent); + const nv = r.values[(cur + 1) % r.values.length]; + t.textContent = nv; letters[key] = nv; + onLetter(r.label, pgm.id, nv, j === active); + }); + } else { + svgEl(s, 'circle', { cx, cy, r: 3.4, fill: '#0a0908', stroke: '#2c2820', 'stroke-width': 1 }); + const cap = svgEl(s, 'circle', { cx, cy, r: 5.6, fill: 'url(#dkaJewelW)', stroke: 'rgba(0,0,0,.45)', 'stroke-width': 1, + opacity: pins.has(key) ? 1 : 0, filter: 'drop-shadow(0 1px 2px rgba(0,0,0,.6))' }); + caps[key] = cap; + const hit = svgEl(s, 'circle', { cx, cy, r: Math.min(9, DX / 2 - 1), fill: 'transparent' }); + hit.style.cursor = 'pointer'; + hit.addEventListener('click', () => { + const group = radios.find(g => g.includes(r.label)); + const seated = !pins.has(key); + if (group) { + if (!seated) return; /* a radio pin moves, never pulls */ + group.forEach(gr => pins.delete(gr + '>' + pgm.id)); + pins.add(key); + } else { + if (seated) pins.add(key); else pins.delete(key); + } + paint(); + onPin(r.label, pgm.id, seated, j === active); + }); + } + }); + }); + paint(); + return { + el: wrap, + get: () => [...pins], + set: (r, c, on) => { const k = r + '>' + c; if (!caps[k]) return; if (on) pins.add(k); else pins.delete(k); paint(); }, + setLetter: (r, c, v) => { const k = r + '>' + c; if (letterEls[k]) { letterEls[k].t.textContent = v; letters[k] = v; } }, + setActive: i => { active = Math.max(-1, Math.min(PROGS.length - 1, i)); paint(); }, + getActive: () => active, + }; + }; + + /* NEW — idle tripper rail. The mechanical time-switch tripper translated + from clock time to idle time: a horizontal engraved scale of idle + minutes with clamp tabs along it, each engraved with the action it + throws when idleness reaches its mark. Nothing moves by itself — the + rail is a standing policy, readable left to right. Tabs keep their + stage order (they clamp between neighbors), and the siding past the + scale end parks a tab OFF (stage disabled). setBypassed(true) dims the + whole policy — the caffeine-engaged look. + + Contract (everything a consumer needs; no page globals touched): + opts: stages ([{ id, label, minutes }] in pipeline order; minutes + null = parked/disabled); max (scale end, default 60); + width (px, default 460); onChange(id, minutes|null, stages + snapshot) on every drag commit, including the initial paint + (id null on the initial paint). + handle: el, get() (stages snapshot), set(id, minutes|null), + setBypassed(bool). + SVG-built: styling is inline attributes plus the shared .rsvg stage + block of DUPRE_CSS; sqrt scale spreads the crowded early minutes. */ + DUPRE.tripRail = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const MAX = opts.max || 60; + const W = opts.width || 460; + const H = 86; + const stages = (opts.stages || []).map(st => ({ id: st.id, label: st.label, minutes: st.minutes })); + const x0 = 16, x1 = W - 64, railY = 44; + const sideX = x1 + 14; + const xOf = m => x0 + Math.sqrt(Math.max(0, m) / MAX) * (x1 - x0); + const mOf = x => Math.pow(Math.max(0, x - x0) / (x1 - x0), 2) * MAX; + grad('dkaTabM', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0efec'], ['.45', '#c4c1b9'], ['1', '#75726a']]); + const s = stageSvg(host, 'rsvg', W, H); + const all = svgEl(s, 'g', {}); + /* rail groove + sqrt tick scale */ + svgEl(all, 'rect', { x: x0 - 8, y: railY - 4, width: x1 - x0 + 16, height: 8, rx: 3, fill: '#0d0a08', stroke: '#050404' }); + [0, 1, 2, 5, 10, 15, 20, 30, 45, 60].filter(m => m <= MAX).forEach(m => { + const x = xOf(m); + svgEl(all, 'line', { x1: x, y1: railY + 6, x2: x, y2: railY + 10, stroke: 'var(--steel)', 'stroke-width': .9, opacity: .8 }); + svgEl(all, 'text', { x, y: railY + 18, 'text-anchor': 'middle', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--steel)', opacity: .8 }).textContent = m; + }); + svgEl(all, 'text', { x: (x0 + x1) / 2, y: railY + 28, 'text-anchor': 'middle', 'font-size': 5.5, + 'letter-spacing': '.18em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'MINUTES IDLE'; + /* parking siding */ + svgEl(all, 'rect', { x: sideX - 4, y: railY - 7, width: 46, height: 14, rx: 4, fill: 'none', stroke: '#2c2820', 'stroke-width': 1, 'stroke-dasharray': '3 2' }); + svgEl(all, 'text', { x: sideX + 19, y: railY + 20, 'text-anchor': 'middle', 'font-size': 5.5, + 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'OFF'; + const tabs = stages.map((st) => { + const g = svgEl(s, 'g', {}); + const line = svgEl(g, 'line', { x1: 0, y1: 0, x2: 0, y2: 0, stroke: '#3a352c', 'stroke-width': 1 }); + const lbl = svgEl(g, 'text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': 6, + 'letter-spacing': '.08em', 'font-family': 'var(--mono)', fill: '#a59d8c' }); + lbl.textContent = st.label; + const min = svgEl(g, 'text', { x: 0, y: 0, 'text-anchor': 'middle', 'font-size': 5.5, + 'font-family': 'var(--mono)', fill: 'var(--gold)' }); + svgEl(g, 'rect', { x: -5, y: railY - 8, width: 10, height: 16, rx: 2, fill: 'url(#dkaTabM)', stroke: '#5e5a52', 'stroke-width': .7 }); + svgEl(g, 'rect', { x: -1, y: railY - 5, width: 2, height: 10, fill: 'rgba(0,0,0,.3)' }); + g.style.cursor = 'ew-resize'; + return { g, line, lbl, min, st }; + }); + const paint = () => { + /* flag sides alternate among ACTIVE tabs so neighbors never share a + line even when parking breaks the built order */ + let rank = 0; + tabs.forEach(t => { + const parked = t.st.minutes === null || t.st.minutes === undefined; + const up = parked ? true : (rank++ % 2 === 0); + const flagY = up ? 14 : railY + 34; + t.line.setAttribute('y1', up ? flagY + 3 : railY + 6); + t.line.setAttribute('y2', up ? railY - 6 : flagY - 8); + t.lbl.setAttribute('y', flagY); + t.min.setAttribute('y', up ? flagY + 8 : flagY - 12); + const x = parked ? sideX + 6 + tabs.filter(o => (o.st.minutes == null) && stages.indexOf(o.st) < stages.indexOf(t.st)).length * 12 : xOf(t.st.minutes); + t.g.setAttribute('transform', 'translate(' + x + ',0)'); + t.min.textContent = parked ? 'off' : Math.round(t.st.minutes) + 'm'; + t.g.setAttribute('opacity', parked ? .55 : 1); + }); + }; + /* drag: clamp between neighboring active stages; past the scale end parks */ + tabs.forEach((t, i) => { + t.g.addEventListener('pointerdown', e => { + t.g.setPointerCapture(e.pointerId); + const move = m => { + const r = s.getBoundingClientRect(); + const px = (m.clientX - r.left) * (W / r.width); + if (px > x1 + 8) { t.st.minutes = null; paint(); return; } + let mm = Math.round(mOf(Math.min(px, x1)) * 2) / 2; + const prev = tabs.slice(0, i).reverse().find(o => o.st.minutes != null); + const next = tabs.slice(i + 1).find(o => o.st.minutes != null); + if (prev) mm = Math.max(mm, prev.st.minutes + 0.5); + if (next) mm = Math.min(mm, next.st.minutes - 0.5); + t.st.minutes = Math.max(0.5, Math.min(MAX, mm)); + /* exact-number counter: the dial's center reads the dragged value */ + centerTxt.textContent = (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm'; + paint(); + }; + const up = () => { + t.g.removeEventListener('pointermove', move); + t.g.removeEventListener('pointerup', up); + centerTxt.textContent = 'IDLE'; + onChange(t.st.id, t.st.minutes, stages.map(o => ({ ...o }))); + }; + t.g.addEventListener('pointermove', move); + t.g.addEventListener('pointerup', up); + e.preventDefault(); + }); + }); + const stamp = svgEl(s, 'text', { x: (x0 + x1) / 2, y: 12, 'text-anchor': 'middle', 'font-size': 6.5, + 'letter-spacing': '.22em', 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: 0 }); + stamp.textContent = 'BYPASSED · CAFFEINE'; + let bypassed = false; + paint(); + onChange(null, null, stages.map(o => ({ ...o }))); + return { + el: s, + get: () => stages.map(o => ({ ...o })), + set: (id, m) => { const t = stages.find(o => o.id === id); if (t) { t.minutes = m; paint(); } }, + setBypassed: b => { + bypassed = !!b; + all.setAttribute('opacity', bypassed ? .3 : 1); + tabs.forEach(t => t.g.setAttribute('opacity', bypassed ? .3 : (t.st.minutes == null ? .55 : 1))); + stamp.setAttribute('opacity', bypassed ? 1 : 0); + }, + }; + }; + + /* NEW — idle tripper dial. The mechanical segment-timer dial (Intermatic + idiom) translated from clock time to idle time: a circular sqrt-spaced + minutes scale sweeping 300° with COLORED tripper tabs clamped on the + rim — drag a tab around the rim to retune when its stage throws; drag + it into the bottom OFF notch to disable the stage. A side legend maps + color → stage → minutes. Nothing self-moves: the dial is a standing + policy. setBypassed(true) dims the face under a BYPASSED · CAFFEINE + stamp. + + Contract (everything a consumer needs; no page globals touched): + opts: stages ([{ id, label, color, minutes }] in pipeline order; + minutes null = parked in the OFF notch); max (scale end, + default 60); size (dial diameter px, default 132); legend + (true draws the color/label/minutes legend — legendPos + 'right' default, or 'bottom' beneath the dial); + onChange(id, minutes|null, stages snapshot) on every drag + commit, plus once at build (id null). + handle: el, get() (snapshot), set(id, minutes|null), + setBypassed(bool). + SVG-built: styling is inline attributes plus the shared .rsvg stage + block of DUPRE_CSS. */ + DUPRE.tripDial = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const MAX = opts.max || 60; + const SIZE = opts.size || 132; + const stages = (opts.stages || []).map(st => ({ ...st })); + const LEG_BOTTOM = opts.legend && opts.legendPos === 'bottom'; + const LEG_W = opts.legend && !LEG_BOTTOM ? 96 : 0; + const W = SIZE + LEG_W + 8; + const H = SIZE + 8 + (LEG_BOTTOM ? stages.length * 13 + 8 : 0); + const cx = SIZE / 2 + 4, cy = SIZE / 2 + 4; + const R = SIZE / 2 - 4; /* bezel radius */ + const TR = R - 7; /* tab track radius */ + const START = -150, SWEEP = 300; /* bottom 60° = the OFF notch */ + const aOf = m => START + Math.sqrt(Math.max(0, m) / MAX) * SWEEP; + const mOf = a => Math.pow((a - START) / SWEEP, 2) * MAX; + const rad = a => (a - 90) * Math.PI / 180; + const at = (r, a) => [cx + r * Math.cos(rad(a)), cy + r * Math.sin(rad(a))]; + const s = stageSvg(host, 'rsvg', W, H); + const all = svgEl(s, 'g', {}); + svgEl(all, 'circle', { cx, cy, r: R, fill: '#17140f', stroke: '#060505', 'stroke-width': 2 }); + svgEl(all, 'circle', { cx, cy, r: R - 14, fill: '#100e0a', stroke: '#2a231c', 'stroke-width': 1 }); + /* sqrt scale ticks + numerals */ + [0, 1, 2, 5, 10, 15, 20, 30, 45, 60].filter(m => m <= MAX).forEach(m => { + const a = aOf(m); + const [x1, y1] = at(R - 2, a), [x2, y2] = at(R - 8, a); + svgEl(all, 'line', { x1, y1, x2, y2, stroke: 'var(--cream)', 'stroke-width': .9, opacity: .75 }); + const [tx, ty] = at(R - 20, a); + svgEl(all, 'text', { x: tx, y: ty + 2, 'text-anchor': 'middle', 'font-size': 6.2, + 'font-family': 'var(--mono)', fill: 'var(--cream)', opacity: .85 }).textContent = m; + }); + /* OFF notch at the bottom gap */ + const [nx, ny] = at(R - 20, 180); + svgEl(all, 'text', { x: nx, y: ny + 2, 'text-anchor': 'middle', 'font-size': 5.5, + 'letter-spacing': '.12em', 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'OFF'; + const centerTxt = svgEl(all, 'text', { x: cx, y: cy + 3, 'text-anchor': 'middle', 'font-size': 9, + 'letter-spacing': '.2em', 'font-family': 'var(--mono)', fill: 'var(--cream)', opacity: .85 }); + centerTxt.textContent = 'IDLE'; + /* legend */ + const legendEls = {}; + if (opts.legend) { + stages.forEach((st, i) => { + const lx = LEG_BOTTOM ? 14 : SIZE + 12; + const ly = LEG_BOTTOM ? SIZE + 12 + i * 13 : 18 + i * 18; + const lw = LEG_BOTTOM ? W - 14 : W; + svgEl(s, 'rect', { x: lx, y: ly - 6, width: 8, height: 8, rx: 2, fill: st.color, stroke: 'rgba(0,0,0,.5)' }); + svgEl(s, 'text', { x: lx + 13, y: ly + 1, 'font-size': 6, 'letter-spacing': '.06em', + 'font-family': 'var(--mono)', fill: '#a59d8c' }).textContent = st.label; + legendEls[st.id] = svgEl(s, 'text', { x: lw - 4, y: ly + 1, 'text-anchor': 'end', 'font-size': 6, + 'font-family': 'var(--mono)', fill: 'var(--gold)' }); + }); + } + /* tabs: colored clamps riding the rim */ + const tabs = stages.map(st => { + const g = svgEl(s, 'g', {}); + svgEl(g, 'rect', { x: -4.5, y: -TR - 8, width: 9, height: 15, rx: 2, fill: st.color, stroke: 'rgba(0,0,0,.55)', 'stroke-width': 1 }); + svgEl(g, 'rect', { x: -1, y: -TR - 5, width: 2, height: 9, fill: 'rgba(0,0,0,.3)' }); + g.style.cursor = 'grab'; + return { g, st }; + }); + const paint = () => { + const parkedIds = stages.filter(o => o.minutes == null).map(o => o.id); + tabs.forEach(t => { + const parked = t.st.minutes == null; + const a = parked ? 172 + parkedIds.indexOf(t.st.id) * 9 : aOf(t.st.minutes); + t.g.setAttribute('transform', 'translate(' + cx + ',' + cy + ') rotate(' + a + ')'); + t.g.setAttribute('opacity', parked ? .55 : 1); + if (legendEls[t.st.id]) legendEls[t.st.id].textContent = parked ? 'off' + : (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm'; + }); + }; + tabs.forEach((t, i) => { + t.g.addEventListener('pointerdown', e => { + t.g.setPointerCapture(e.pointerId); + const move = m => { + const r = s.getBoundingClientRect(); + const px = (m.clientX - r.left) * (W / r.width) - cx; + const py = (m.clientY - r.top) * (H / r.height) - cy; + let a = Math.atan2(px, -py) * 180 / Math.PI; /* 0 up, cw */ + if (a > 150 || a < -150) { t.st.minutes = null; paint(); return; } + let mm = Math.round(mOf(Math.max(START, Math.min(START + SWEEP, a))) * 2) / 2; + const prev = tabs.slice(0, i).reverse().find(o => o.st.minutes != null); + const next = tabs.slice(i + 1).find(o => o.st.minutes != null); + if (prev) mm = Math.max(mm, prev.st.minutes + 0.5); + if (next) mm = Math.min(mm, next.st.minutes - 0.5); + t.st.minutes = Math.max(0.5, Math.min(MAX, mm)); + /* exact-number counter: the dial's center reads the dragged value */ + centerTxt.textContent = (t.st.minutes % 1 ? t.st.minutes.toFixed(1) : Math.round(t.st.minutes)) + 'm'; + paint(); + }; + const up = () => { + t.g.removeEventListener('pointermove', move); + t.g.removeEventListener('pointerup', up); + centerTxt.textContent = 'IDLE'; + onChange(t.st.id, t.st.minutes, stages.map(o => ({ ...o }))); + }; + t.g.addEventListener('pointermove', move); + t.g.addEventListener('pointerup', up); + e.preventDefault(); + }); + }); + const stamp = svgEl(s, 'text', { x: cx, y: cy - 14, 'text-anchor': 'middle', 'font-size': 5.5, + 'letter-spacing': '.14em', 'font-family': 'var(--mono)', fill: 'var(--gold)', opacity: 0 }); + stamp.textContent = 'BYPASSED'; + paint(); + onChange(null, null, stages.map(o => ({ ...o }))); + return { + el: s, + get: () => stages.map(o => ({ ...o })), + set: (id, m) => { const t = stages.find(o => o.id === id); if (t) { t.minutes = m; paint(); } }, + setBypassed: b => { + all.setAttribute('opacity', b ? .3 : 1); + tabs.forEach(t => t.g.setAttribute('opacity', b ? .3 : (t.st.minutes == null ? .55 : 1))); + stamp.setAttribute('opacity', b ? 1 : 0); + }, + }; + }; +})(); diff --git a/docs/specs/2026-07-02-desktop-settings-panel-spec.org b/docs/specs/2026-07-02-desktop-settings-panel-spec.org index 3aef647..f2ac773 100644 --- a/docs/specs/2026-07-02-desktop-settings-panel-spec.org +++ b/docs/specs/2026-07-02-desktop-settings-panel-spec.org @@ -4,17 +4,26 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED -* DRAFT Desktop-Settings Dropdown Panel +* DOING Desktop-Settings Dropdown Panel :PROPERTIES: :ID: d6bb1e73-ec90-4327-85ee-bfa762da5bce :END: - 2026-07-04 Sat @ 12:36:56 -0500 — retrofitted by spec-sort; status set to DRAFT (evidence-based, human-confirmed) -* DRAFT Status +* DOING Status :PROPERTIES: :ID: fb7eec22-a214-4568-82c4-903612f4832f :END: -- [2026-07-19 Sun] DRAFT — folded Craig's control-set decisions (home->archsetup handoff): added night-light, DND, lock/suspend, power profile, and scenes to the controls table; recorded volume and theme as out-of-scope with reasons; pulled the format pickers into a separate display-format sibling spec (stub filed). The "few other things" scoping decision and the scenes decision are now closed. One decision stays open — the wallpaper manager's shape (row-that-opens-a-sub-view vs its own sub-spec), to resolve at spec-review. UI prototyping (ui-prototyping rule: research -> 5 directions -> iterate) is not done yet and is the gate before READY. +- [2026-07-22 Wed] DOING — spec-response: the four rewritten implementation phases decomposed into build tasks in todo.org under a :SPEC_ID:-stamped parent, with the test surface and the final flip-to-IMPLEMENTED task emitted. The build proceeds in dotfiles settings/ per the location decision; archsetup drives. +- [2026-07-22 Wed] READY — spec-review passed (rubric: Ready with caveats; see Review findings — two findings fixed in-run, two non-blocking build-time picks recorded). The prototyping gate closed at prototype 37, sealed by Craig's "Build it. Anything more would be bit fiddling." The FOCUS headphones glyph stands confirmed by the same words. Normative reference: prototype 37. +- [2026-07-22 Wed] DRAFT — UI PROTOTYPING COMPLETE. Thirty-five prototypes ended at [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-35.html][Prototype 35, the final candidate]] (linked as the normative reference under Design sketch), sealed by Craig's "I think that's it" after his annotation batch applied clean. The design evolved far past the original controls table — programmable-presets matrix, instrument bench, wallpaper channels — with every turn recorded under Prototype iterations. The gate before READY is satisfied: research cited, ~5 directions grew to 35 with the winner iterated to final, every UI decision made against working prototypes. Next: spec-review (reconcile the Design sketch's aging architecture prose against what the prototypes actually settled), then spec-response into build phases. +- [2026-07-21 Tue] DRAFT — casting sitting complete: Craig cast every control role live against working candidates (see "Casting" under Prototype iterations). Scenes = tuner dial + presets; brightness = new detent fader with speedbump physics; keyboard = single-drum roller; power = chicken-head; toggles = rocker bank; airplane = guarded lever (throw fixed); caffeine = wind-up timer dial; chrome = warm-gold receiver plate. Three kit-candidate components accrued in dupre-kit-additions.js with a merge task filed. Next: compose prototype 12 and iterate to final. +- [2026-07-21 Tue] DRAFT — scope change (Craig): lock/suspend withdrawn from the panel (they live on the wlogout screen, Super+Shift+Q); a new open decision "Idle pipeline" proposes staged idle timeouts (dim/lock/screen-off/power action) and caffeine as a timed inhibit, replacing the bare on/off. DND semantics finding recorded in the controls table (pause dams and floods; dead-drop needs an explicit queue clear). +- [2026-07-21 Tue] DRAFT — three Dupre Kit directions (9 engine room, 10 spacecraft DSKY, 11 mixing desk) composed from the real widgets.js builders after Craig pointed the phase at the panel-widget-gallery catalogue; his two favorite level controls (drum roller, thumb-slide attenuator) each carry the levels in one direction for a working comparison. Eleven directions total; pick still pending. +- [2026-07-21 Tue] DRAFT — three more direction prototypes (6 hi-fi receiver, 7 chronograph, 8 gauge cluster) built to Craig's aesthetic brief: retro hi-fi stereo / chronograph / automobile dashboard. Craig loved the scenes-as-station-presets idea, so the tuner dial is direction 6's hero. Also retired the status lamps across all directions (the glowing dot pulled the eye) and fixed a slider-fill CSS hack that bled a huge gold disc over directions 1/2/3/5. Direction pick still pending. +- [2026-07-21 Tue] DRAFT — UI-prototyping phase: recorded the quick-settings prior-art survey (see Prior art under Design sketch) and brought the five 2026-07-19 direction prototypes up to the current controls table (weather-geo toggle added to the shared engine and every direction; caffeine restored to directions 2 and 5, which had dropped it). All five verified rendering error-free headless. Awaiting Craig's direction pick before the iterate-to-final pass. +- [2026-07-21 Tue] DRAFT — added a weather-geolocation toggle to the controls table (use_geo in the weather config: whereami WiFi fix vs pinned home coords). Both machines run use_geo=true for testing as of today; ratio may flip back by hand until the panel ships the switch. +- [2026-07-19 Sun] DRAFT — folded Craig's control-set decisions (home->archsetup handoff): added night-light, DND, lock/suspend, power profile, and scenes to the controls table; recorded volume and theme as out-of-scope with reasons; pulled the format pickers into a separate display-format sibling spec (stub filed). The "few other things" scoping decision and the scenes decision are now closed. The wallpaper-shape decision closed the same day (row-that-opens-a-sub-view — see Decisions), leaving no open decisions. UI prototyping (ui-prototyping rule: research -> 5 directions -> iterate) is not done yet and is the gate before READY. - [2026-07-04 Sat] DRAFT — all four decisions resolved by Craig (dim + airplane collapse into the panel, touchpad + caffeine stay on the bar; Super+Shift+G keybind; code in dotfiles settings/ beside net/; 5% brightness floor). The four collapse/keybind/location/floor decisions are closed; one open scoping question remains ("few other things" — see Decisions) before a spec-review can flip it READY. - [2026-07-03 Fri] DRAFT update — from the waybar/panel-family design discussion: adopt the instrument-console faceplate aesthetic net + bt @@ -26,11 +35,528 @@ dropdown panel" (2026-06-24 review), updated for the Blueprint/GTK4 pipeline the net panel stood up 2026-07-01. +** Prototype iterations + +Five direction prototypes (2026-07-19), all full working layouts over one +shared mock engine ([[file:../prototypes/desktop-settings-shared.js][desktop-settings-shared.js]] + +[[file:../prototypes/desktop-settings-shared.css][desktop-settings-shared.css]]) so the directions are genuinely comparable: +same state model, same verify-everything simulation (apply, mock readback, +green verify flash + toast), same wallpaper sub-view. + +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-1.html][Direction 1 — tile grid]]: the Android/GNOME quick-settings idiom; scenes + strip, 2-column toggle tiles, sliders and power below. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-2.html][Direction 2 — sectioned rack]]: full-width console keys under engraved + section labels; closest to the net/bt panel idiom. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-3.html][Direction 3 — scenes-first]]: scenes as the hero controls, everything else + beneath. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-4.html][Direction 4 — instrument cluster]]: ring gauges for the levels, a rotary + power-profile selector, lamp rows; leans hardest into the faceplate + aesthetic. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-5.html][Direction 5 — compact expandable rows]]: one slim row per control, + progressive disclosure for power/scenes/wallpaper. + +2026-07-21: all five updated to the current controls table (weather-geo +added; caffeine restored to directions 2 and 5) and verified rendering +error-free. + +Three more directions added 2026-07-21 to Craig's aesthetic brief — retro +hi-fi stereo, chronograph, automobile dashboard — treating the panel as a +physical object rather than a software surface. Same shared engine, same +control roster: + +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-6.html][Direction 6 — hi-fi receiver]]: scenes as station presets on a backlit + tuner dial (sliding needle) + preset keys; rotary knobs with VU meters + for the levels; a backlit switch bank; an input-selector knob for power. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-7.html][Direction 7 — chronograph]]: the panel is a watch. Brightness is the main + dial (drag the bezel; gold hand + digital window), chrono subdials carry + keyboard/power/scene, crowns are the momentary actions, bracelet links + are the toggles. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-8.html][Direction 8 — gauge cluster]]: a dashboard binnacle with twin needle + gauges (drag to set), a rocker-switch bank with backlit legends, + push-button drive modes for scenes, an ignition rotary for power, dash + pull-knobs for actions. + +Three further directions added 2026-07-21, composed from the real Dupre Kit +builders (widgets.js) rather than hand-rolled lookalikes — so each doubles +as a component-selection exercise for the eventual GTK port. Craig's two +favorite level controls get the same job in two different directions so +they can be compared in working form: the drum roller in direction 9, the +thumb-slide attenuator in direction 10. + +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-9.html][Direction 9 — engine room]]: drum-roller levels, split-flap scene board + read from a rotary selector, breaker-panel toggles, guarded airplane + lever, engine-order telegraph for power, arm-to-fire actions. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-10.html][Direction 10 — spacecraft DSKY]]: thumb-slide levels, slide-switch bank + under a live caution board, scenes commanded by DSKY verb entry + (V01-V04, V00 off), keyed mode switch for power. +- [[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-11.html][Direction 11 — mixing desk]]: slot faders in dB with a VU bridge, + radio-preset scene bank, chip toggles with DND on a stomp pedal, rotary + power selector. + +Kit-integration notes for the port, learned building these: the kit's +CSS-built selector spans (.dupre-rotsel, .dupre-keylock) size themselves +with width/height on inline display, so consumers outside the gallery need +display:inline-block on them; breakerPanel has no set() and hardcodes its +initial states, so reflecting external state means synthesizing clicks on +its hit rects (a set() upstream would fix it); and the shared mock engine +now carries weathergeo in its state object (it was metadata-only, and +kit toggles default-on when opts.on is undefined). + +*** Casting (2026-07-21) — instrument per role, picked live + +Rather than one direction wholesale, Craig cast each control role from the +bench in a live sitting (candidates driven in working form; the scenes and +levels casting pages below were built for the head-to-heads): + +- Scenes (panel hero, top): kit slideRule tuner dial — stops + OFF/FOCUS/PRES/BATT/NIGHT, lit stop label, sliding needle — over + PRESET 1-4 keys. Beat rotary + split-flap head-to-head + ([[file:../prototypes/2026-07-02-desktop-settings-panel-scenes-compare.html][scenes casting page]]). +- Screen brightness: detentFader (NEW kit-candidate component) — full-width + horizontal fader, detents at 25/50/75 with speedbump physics (magnet + + escape hysteresis; the thumb parks until the pointer pushes through). + Craig's revision of the plain faderH pick. +- Keyboard backlight: drumRoller, single drum 0-10 (kit builder generalized + to 1..N channels + min/max — stock was hardcoded to two drums). + ([[file:../prototypes/2026-07-02-desktop-settings-panel-levels-compare.html][levels casting page]] carries both level controls.) +- Power profile: chicken-head knob, three detents. +- Toggle bank: kit rocker bank — lit engraved legends carry state (no lamp + dots; the status-lamp retirement holds). +- Airplane: guardedToggle, laptop-only, patched to a rotateX throw (the + stock 180° planar spin swept the lever sideways mid-transition). +- Caffeine / idle inhibit: kit timerDial as a wind-up timed inhibit (real + minutes at build; demo clock is min/sec). +- Chrome: direction 6's receiver faceplate in warm Dupre gold — family-kin + with net/bt/audio. (Marantz-blue slideRule skin seen and passed on.) + +The three kit-candidate components (detentFader NEW, drumRoller and +guardedToggle UPGRADEs) live in +[[file:../prototypes/dupre-kit-additions.js][dupre-kit-additions.js]] pending the +kit merge (tracked in todo.org). + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-12.html][Prototype 12 — the casting composite]] (2026-07-21, overnight on Craig's +go): every cast instrument composed on the warm-gold receiver chassis with +the current roster — tuner + presets, detent fader, single drum, +chicken-head, wind-up caffeine timer, rocker bank, guarded airplane, +wallpaper sub-view; lock/suspend absent per the wlogout amendment. All +interactions verified headless (scene tune + preset punch, fader detents, +drum drag, chicken step, timer wind → caffeine, rockers, lever, sub-view +round-trip). Composing it surfaced and fixed a latent shared-engine bug: +wallpaperSubview returned a div carrying the ds-subview class +(display:none outside .open), so the wallpaper sub-view had been opening +empty in every direction prototype. Iteration to final happens against +this file. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-13.html][Prototype 13 — P12 + Craig's first iteration]] (2026-07-22): presets moved +above the dial full-width (Preset 2 contains "Presentation" whole); screen +brightness RECAST from the detent fader to the drum — screen + keyboard now +run as the classic two-drum pair left of an enlarged caffeine timer, which +gains a HOLD/TIMED rocker so caffeine can be held always-on; compact +chicken-head in the upper-right quadrant; switch wall of six with the +guarded airplane lever in the mix; weather-geo DROPPED from the panel +entirely (controls-table row removed — the config stays hand-edited); +wallpaper row unchanged. The detentFader remains a kit-merge candidate on +its own merits. Verified: HOLD drives caffeine, drum drag drives +brightness, preset containment and tuner stop visibility checked +programmatically. Second pass same night: the drums grew to run flush with +the timer column (drum top level with the dial top, bottom with the HOLD +rocker — verified 0px deltas); drumRoller gained a height option for it (a +taller stage opens a longer paper window showing more of the scale), which +joins the kit-merge candidate's contract. Third pass: the caffeine timer +was unwindable live — crossing zero mid-drag toggled caffeine, the +re-render destroyed the dial under the pointer, and every rebuilt +timerDial leaked its countdown interval as a ghost writing stale minutes. +Fix: self-clocked instruments persist across re-renders (reparent, never +rebuild) and state sync is debounced past the drag. This is a +build-architecture rule for the GTK port too: instruments that own a +clock own their widget lifetime. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-14.html][Prototype 14 — timer refinements]] (2026-07-22): the HOLD/TIMED rocker is +replaced by a bat toggle seated at the timer's upper-right where the +winding-direction arrow was (arrow removed) — bat up TIMED, down HOLD +(always-on caffeine); the timer dial is vertically centered against the +drum column instead of top-flush. batToggle joins the kit-merge additions +with the same rotateX throw fix as guardedToggle (stock had the identical +180° planar spin). Verified: bat drives hold-caffeine both ways, center +delta 0, arrow absent. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-15.html][Prototype 15 — power slide]] (2026-07-22): the chicken-head is replaced by +a three-position slide switch under the tuner — a status lamp per +position, labeled PERFORMANCE / BALANCE / POWER SAVE, stretching to +exactly the tuner's length; the tuner itself is restored to full plate +width. Two more kit-candidates for it: slideSelector (NEW — stretchable +n-position slide with per-position lamps, pillSlide's full-size +descendant) and a slideRule width option (the printed scale spreads its +stops proportionally; custom widths take a wider edge margin so end-stop +labels stay inside). Verified: slide clicks and drags land positions and +drive the profile, exactly one lamp lit, scenes move the slide (Focus +sets BALANCE), tuner tunes at full width with end labels contained. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-16.html][Prototype 16 — rebalanced]] (2026-07-22): built from a design critique +Craig requested ("it still feels off") whose diagnosis was inverted +salience — the least-used control (the caffeine timer, largest object, +only big red) shouted loudest while the heroes (scenes, brightness) +whispered. Changes, same instruments throughout: three uniform plates on +one grid (Scenes·Power / Levels·Caffeine / Switches); the drum pair +replaced by the thumb-slide attenuator pair at Craig's call (the cream +paper drums were the panel's bright blob and out of family with the +slide/fader chrome); the timer de-throned — steel stop knob, amber +index/OFF, single engraving, bat docked on its stage; rockers carry their +function name on the lit half with a new `quiet` option killing the +warning-red off state (a fourth kit-candidate UPGRADE — red-off stays +stock default for switches where off is the notable state); the airplane +lever sits on the rocker baseline with trimmed AIR/SAFE labels. Red now +appears exactly twice, both semantically: the tuner needle (vintage +heritage) and the airplane danger disc. Salience rule recorded for the +GTK build: visual loudness follows frequency × importance — scenes and +brightness loudest, switches calm, caffeine polite. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-17.html][Prototype 17 — switches under their presets]] (2026-07-22): plate order +rearranged for causality — the presets program the switches, so the wall +now sits directly beneath the scenes plate ("strange to have the preset +buttons on top controlling switches on the bottom" — Craig), with the +power slide just under the wall in a shared Switches · Power plate; +Levels · Caffeine follows. The power slide's knob goes dark (a knob +option — chrome default / machined-dark — added to the slideSelector +kit candidate). Verified: plate order wall → slide → levels, dark knob +class active, and the adjacency in action — the Night preset lights the +rockers directly beneath it and slides power to POWER SAVE. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-18.html][Prototype 18 — the program chart]] (2026-07-22): Craig asked what analog +component the missing "one selector recalls a configuration" pattern maps +to. Answer: two — the behavioral half is the organ combination action / +flying-fader scene recall (controls physically move on recall, which the +panel already does), and the legible half is the washing-machine faceplate +program chart, which was the missing piece. New kit-candidate +DUPRE.programChart: one row per program, one column per contact — filled +dot closes it, ring opens it, dash leaves it alone, letters carry enums — +active row lit, display-only, derived straight from the scene definitions +(which makes visible that scenes never touch TPAD/MOUSE). Also this pass: +airplane removed from the panel (network panel owns it — table row and +wall slot dropped); the dial's OFF stop relabeled CUSTOM (no scene = a +hand-set configuration, not "off"); a real-proportion pass — thumb slides +up toward hand size, timer down toward kitchen-timer size. Verified: +chart rows follow scene changes, CUSTOM releases the scene while the +switches hold state, airplane absent. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-19.html][Prototype 19 — the chart becomes a punch card]] (2026-07-22): the program +chart turns editable and white — its lights indicate FUTURE state (what +the preset will set on recall), so they sit deliberately outside the +amber live family; clicking a cell reprograms the preset (dots cycle +closes → opens → untouched, the power letter cycles P/B/S/–), with edits +written back to the scene definitions and honored on the next recall +(verified: adding DND to Battery's program, then recalling Battery, +engaged DND for real). Switches engage GREEN (a rocker accent option — +run/engaged semantics) and shrink to true rocker size; slides grow. +Three-register color semantics recorded for the build: amber = scene +family and live selection, green = engaged switches, white = programmed +future state. Craig's observation logged with it: the panel is converging +on a master-preset architecture — the presets program everything below +except the wound timer minutes; the chart is that programming surface. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-20.html][Prototype 20 — the switch matrix]] (2026-07-22): the master-preset +architecture made literal. The panel's axis IS the switches: preset row, +punch chart, and switch wall fuse into one matrix — each row a live +switch (rocker / caffeine lamp / a mini three-detent power slide), the +four preset buttons running across the top as column heads (names only, +no PRESET N), white punch lights under each program column, every cell +editable. The slide-rule tuner is retired (preset buttons + CUSTOM-by- +touching carry scene selection); TPAD and MOUSE — which no scene touches +— drop to their own Pointing strip; SCREEN, KBD, and power go horizontal +via a new kit-candidate, thumbSlideH (the thumb slide turned on its side: +continuous 0-100 or detented positions, stretchable). Build note: the +matrix itself is page-level composition for now — if it survives +iteration it should become a kit switchMatrix component; and component- +scoped CSS bit again (the chart's dot classes are scoped to .dupre-pchart, +so the matrix carries its own copy). Verified: preset buttons recall and +light the rockers, punch edits write back and hold, horizontal slides +drive both levels, the matrix power slide sets the profile, tuner absent. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-21.html][Prototype 21 — the pin routing matrix]] (2026-07-22): P20's accidental +switch-relocation told Craig what the interface actually is — a pin +routing matrix (EMS-Synthi patch idiom, already in the kit). The kit +pinMatrix got the upgrade treatment (kit-candidate #9: configurable +geometry, per-column pin colors, clickable button column-heads with an +active state, row-group dividers, set(); stock 5×6 defaults preserved) +and became the whole interface: switch names label the rows (the physical +rockers are gone — "remove all the switches, which now become labels"), +program buttons head the columns, a green live NOW column carries current +state (click to toggle; the live power pin can't be pulled, only moved), +white pins program the four scene columns, and the power rows are a radio +group per column. Program semantics simplified to complete definitions: +recall makes the column's pin pattern the truth (pin on, absent off). +CAFF is an ordinary switch row; the caffeine timer job is retired and the +dial is repurposed as a SLEEP TIMER (wind minutes until suspend — the +receiver classic; pairs with CAFF as the two directions of the idle +fight), bat toggle retired with it. The drums return for SCREEN/KBD at +Craig's call ("I love the drum"). Verified end to end: NOW pins toggle +live state, program pins write back, power radio enforces one pin per +column, column-head recall applies the full pattern (including a +just-seated CAFF pin), sleep timer winds/runs down/fires the mock +suspend, button heads contained after a geometry margin fix. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-22.html][Prototype 22 — the programmable matrix]] (2026-07-22): Craig named the +component the design was building toward, and it exists now — +DUPRE.programMatrix (kit-candidate #10): programs as caps column-head key +plates (FOCUS · PRESENT · BATTERY · NIGHT — "Presentation" shortened), +settings as spelled-out engraved rows (AUTO-DIM, NIGHT LIGHT, DO NOT +DISTURB, CAFFEINE; engraved-and-infilled relief after the pure kit +engrave idiom proved illegible at 6px), radio row groups, and the core +semantic: NO live column — pressing a head activates the program and its +column's pins turn white → green, because active-is-live: the recalled +pattern IS the machine's state. Editing the green column edits the +present (live state and stored program move together, and the column +STAYS active); editing a white column edits a future. Build finding for +the GTK port: the mock engine cleared the active scene on any member +write, which silently dropped active-is-live — the real PanelModel must +let program-membership writes survive without deactivating the program. +Verified: activation floods the column green and applies the pattern, +active-column DND pull changed live + program while staying in NIGHT, +radio PERF move followed suit, inactive-column CAFFEINE seat edited the +program without touching live state. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-23.html][Prototype 23 — the pushwheel CPU row]] (2026-07-22): CPU power collapses +from three radio rows to ONE letter row. Craig asked for the analog +referent — triangle up/down steppers beside a lettered tape-counter +window — which is the industrial push-button thumbwheel (BCD pushwheel) +switch; no kit component existed (chromeMinMax checked: it's a dome +gauge), so programMatrix grew letter rows: program cells carry one of +the row's values (P/B/S here, click cycles; ↑/~/↓ noted as an alternate +glyph set), and stepper: true appends the pushwheel at the row's right — +window showing the LIVE letter, triangles stepping it (up toward P, +clamped). Active-is-live holds: stepping while a program is active +writes the program's letter too and stays active. Row labels now +right-align to the column buttons' left edge (Craig). Verified: stepper +walks B→S and back up to P, inactive letter cycles edit programs only, +NIGHT activation floods letters green, active stepping moves live + +program together; a margin fix cleared the fourth column's letter from +the pushwheel window. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-24.html][Prototype 24 — tape wheel + jewel verdicts]] (2026-07-22): the triangle +steppers retire after one prototype — the tape-counter wheel IS the +button now (click cycles the live CPU letter, wrapping), rendered as a +proper wheel cell: cream paper, cylinder shading, 13px printed letter, +after the kit tapeCounter's look. And the active column gains verdict +lamps per Craig's read that they're jewel pilot lamps "just not so large +and flashy": every switch row in the live column shows a definite state — +a domed green jewel for seated, a smaller red jewel for a socket the +program holds off; inactive seated pins wear a frost-white jewel dome. +Verified: wheel cycles B→S→P→B, NIGHT activation shows three green +verdicts + one red (CAFFEINE held off) + the green S letter, program +edits and active-is-live unchanged. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-25.html][Prototype 25 — wheels in every column]] (2026-07-22): Craig caught the +side wheel as another NOW-column vestige — under active-is-live the +active column's value IS the live value, so the tape wheel belongs in +every program column, not off to the side. Each CPU POWER cell is now +its own bezeled tape wheel (click cycles that program's letter); the +active column's wheel wears a green bezel and is the machine's live CPU +state. Jewel pilot lamps sized up (5.6 / 4.8) with taller rows (dy 21) +to match. Verified: inactive wheels edit programs only, the active wheel +cycles live + program together and stays active, activation applies the +program's letter. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-26.html][Prototype 26 — physical keys, arrow wheels]] (2026-07-22): the program +heads become real keys — raised bevel over a static shadow at rest, +visibly DEPRESSED onto it when active, with green lighting only the +engraved legend (a backlit key, not a bordered chip); the active wheel's +green bezel is dropped (paper doesn't glow — the depressed key and the +jewel verdicts carry live indication); row labels engraved larger and +brighter-infilled for faceplate presence; the CPU wheels print ↑ – ↓ +(performance / balanced / saver) instead of letters; and the whole panel +widens to 434px (matrix 382) to give it all room. Verified: NIGHT's key +depresses with a green legend while the others sit proud, wheels carry +the arrow glyphs, the active wheel still cycles live + program together +without dropping the program. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-27.html][Prototype 27 — console keys, six slots, one bench]] (2026-07-22): the +head keys become the kit's own console-key primitive (.dupre-key, +lighting full green when active — the component now composes DOM keys +over the SVG pin field); TWO blank program slots grow the matrix +rightward (programs are {id, label} so blank labels keep unique pin +keys; the slots recall as honest all-off programs — verified: Slot5 +lights four red verdicts and a dash); the panel window widens to 560. +The lower level becomes one bench: drums · machined groove · TPAD over +MOUSE · groove · sleep dial, and the separate Pointing plate dissolves. +Maintenance note: a string-surgery patch had earlier fused pinMatrix and +programMatrix (shared anchor text matched the wrong builder — the +assignment silently vanished); both were rebuilt clean, which the kit +merge should treat as the canonical text. Keys blur after click so only +meaningful light remains. Next design conversation, per Craig: how the +sleep dial associates with the action it fires at zero. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-28.html][Prototype 28 — the idle tripper rail]] (2026-07-22): the dial-association +conversation resolved the dial out of existence. Craig's question — "in X +minutes from now, or when idle for X minutes?" — was answered B, with the +principle stated: THIS PANEL PROGRAMS FUTURES, not immediate actions. A +wound countdown lies for intent B (an idle threshold re-arms on input), +so the sleep dial retired and the idle pipeline surfaced as its honest +analog: the tripper rail (kit-candidate #12, DUPRE.tripRail — the +mechanical time-switch tripper translated from clock time to idle time). +A sqrt-spaced idle-minutes scale with clamp tabs — DIM 5 · LOCK 7 · +WATCH 8 · SCREEN OFF 10 · SUSPEND 30 — dragged to retune (stages clamp +between neighbors, keeping pipeline order), a dashed OFF siding parks a +stage disabled, flags re-alternate among active tabs so labels never +collide, and engaged CAFFEINE renders the whole rail dimmed with a +BYPASSED · CAFFEINE stamp — the pin's meaning made visible. This +supersedes the idle-pipeline decision's "delays live in hypridle.conf" +placement: the rail IS the panel-native programming surface for those +same values (the build writes hypridle.conf and reloads). Verified: +dragging SUSPEND 30→45 commits, LOCK clamps below its neighbor, WATCH +parks to off, the bypass dims and stamps. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-29.html][Prototype 29 — the idle tripper dial]] (2026-07-22): the rail was my +translation; Craig's actual mental image was the ROUND form — the +Intermatic segment-timer dial with four COLORED tripper tabs clamped on +the rim (kit-candidate #13, DUPRE.tripDial; the rail stays a candidate in +its own right). A 300° sqrt-spaced idle scale with a bottom OFF notch: +drag a tab around the rim to retune its stage, drop it through the notch +to disable; a side legend maps color → stage → minutes (DIM amber · +LOCK slate · SCREEN OFF steel · SUSPEND terracotta). Two design +resolutions rode in with it: WATCH IS THE LOCK SCREEN — the night watch +becomes the face the lock stage shows, not a pipeline stage of its own +(the idle-decision's separate eye-candy stage folds into LOCK; the +night-watch build gains "runs as/over hyprlock" as its integration +target); and the dial returns to the bench, restoring drums · groove · +pointing · groove · dial. Verified: rim drag commits (SUSPEND 30→45), +order clamping holds (LOCK stopped at DIM+0.5), the notch parks a stage, +CAFFEINE dims the face under the BYPASSED stamp. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-30.html][Prototype 30 — family chrome]] (2026-07-22): Craig flagged the panel as +noticeably off-family from maint/net/bt; diagnosis: the prototypes had +been wearing the widget gallery's amber (#e2a038) since P9, while the +family runs THEME gold (#dab53d / #ffd75f) — plus flat brushed plates +where the family uses raised gradient faceplates. Aligned: theme-gold +token family, maint-style body glow and panel background, the maint +header (raised face, cream wide-tracked title), raised-gradient plates +with inset highlights, and a darker matrix field inset into a lighter +plate (the requested contrast — empty sockets now read as visible +punch-card rings). Also this pass: slots labeled 1 · 2 as SLIM keys +(programMatrix gains weighted column widths; window compacts to 540); +factory presets programmed (FOCUS 1011↑ · PRESENT 0011– · BATTERY 1000↓ +· NIGHT 1101↓, slots all-off, all programs persist edits); the section +renamed PROGRAMMABLE PRESETS; the caffeine/CPU divider removed; drums +and dial enlarged with lighter cream dial numerals. Open question +parked: where TPAD/MOUSE live (Craig floated right-of-the-presets; +recommendation recorded in session notes to keep them off the +programming faceplate). + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-31.html][Prototype 31 — encircled groups]] (2026-07-22): the TPAD/MOUSE question +resolved — Craig accepted keeping them off the programming faceplate but +wanted them smaller: the rockers retire and the pair become console keys +(same primitive as the preset heads, lighting green when engaged), +horizontal, seated above the dial. The bench reorganizes into three +ENCIRCLED instrument groups — bordered boxes with their labels set into +the border break, the classic faceplate group idiom: BRIGHTNESS (the +drum pair, grown to 204 to match proportion), POINTERS (the two keys), +IDLE (the tripper dial, size unchanged). Grooves retired with the +rockers. Verified: keys toggle live state and light green, groups render +with break labels, no bench overflow. With this pass Craig declared the +settings panel's cosmetics done; next surface: the wallpaper sub-view. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-32.html][Prototype 32 — the wallpaper channels]] (2026-07-22): built from Craig's +mode-table reframe (see "Wallpaper sub-view — the mode table" under +Design sketch): the units of choice are MODES, not framed pictures, so +the sub-view is a CHANNEL BANK — seven mode cards, each wearing its own +face per the table: WATCH (mini radar face), SUN PAIR (diagonal slashed +thumbnail of the selected pair), SINGLE (the chosen thumb), RANDOM (a +fanned deck — the answer to the table's open representation question), +VIDEO (play-badged frame), CLOCK (drawn face), WORLD (mini-clock +matrix). The active channel rings green; the config surface below +switches per channel — library grid for single, the PAIR PRESS for +sun pair (fill the day frame, the night frame auto-arms, MINT PAIR +strikes the slashed chip into a persistent tray of selectable pair +objects, exactly the mint semantics Craig specified), the interval wheel +(tape-wheel idiom, 5/10/15/30/60) for random, a video picker, and +honest no-options plates for the generators. The wallpaper row's state +text reads the tuned channel. Verified: channel switching, single +selection, the full mint flow (day pick auto-arms night, minted pair +joins tray selected), interval cycling. Mechanisms like the carousel +remain available for the eventual real-image library picker, demoted to +the one layer where units are uniform. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-33.html][Prototype 33 — sources + pair scrap]] (2026-07-22): two missing +capabilities land. SOURCES: a directory list at the sub-view's foot (all +channels draw from it) — folder rows with remove (guarded: the last +source can't be removed) and an ADD DIRECTORY key (mock candidates; the +real build opens a directory picker). PAIR SCRAP: hovering a minted chip +reveals ✕; the first strike arms it red ("strike again to scrap"), the +second deletes — arm-to-fire friction for a destructive act, consistent +with the panel's danger idiom; selection index self-heals including the +empty-tray case. Verified: add/remove sources with the last-source +guard, scrap flow arms then deletes, empty tray renders honestly. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-34.html][Prototype 34 — annotation batch one]] (2026-07-22): Craig's new iteration +method in action — he annotated via org-capture into the roam inbox +(ten archsetup notes), I claimed and applied the batch. Settings side: +SETTINGS header amber-caps (verified against the net/bt prototype, whose +header word is gold — the P30 family pass had wrongly copied maint's +cream); preset keys become GLYPHS (crescent moon for NIGHT, half battery +F007E for BATTERY, projector screen for PRESENT, and — answering Craig's +"focus = ? help with this" — the crosshairs reticle F0210, aim-and- +attend, over eyeglasses which read as "reading"); the narrower keys buy +FOUR numbered slots; CPU balanced glyph – → =; TOUCHPAD spelled out; the +drums' redundant LEVELS ×10 title removed (per Craig's screenshot) and +the drums grown into the space; the toast's idle "waiting…" hidden. +Wallpaper side (his sun-pair architecture + the extend-everywhere +directive): every mode gets a DESCRIPTION panel (helpful operating text, +never "no options"); galleries are side-scrolling strips (7-10 thumbs); +the section order for sun pair runs modes → minted pairs → pair press → +pictures → sources; the minted-pair gallery sorts alpha/recency via two +small keys, shows labels under chips (nothing when unlabeled), and each +chip carries ~ (edit: reopens day/night/label in the press and leaves +the gallery, re-mint to return) and − (delete; recreate to recover); +the press gains its label field; the picture gallery feeds the armed +frame and notes the extension filtering; PREVIEW sections show watch +(Craig's real night-watch screenshot, trimmed to +prototypes/2026-07-22-watch-preview.png), clock, world, single, and +video; SOURCES appears only for the directory-drawing modes (single, +pair, random, video) and hides for the generators. All flows verified +headless including the edit-remint round trip and sort toggles. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-36.html][Prototype 36 — annotation batch two]] (2026-07-22): eight more roam +captures applied. Settings: all matrix keys uniform (43px — glyph keys +slimmed to the numbered keys' size, glyphs bumped to 15px for +recognizability); the bench rearranged — POINTERS above BRIGHTNESS on +the left, the IDLE dial alone on the right, enlarged to 184 with its +legend moved BENEATH the dial (a legendPos option on the tripDial +kit-candidate); drums centered in their frame. Wallpaper: a NEW SOLID +COLOR channel (eighth card) in the sun-pair format — minted labeled +colors above, color picker + label + MINT below, no sources; RANDOM +gains minted SETS — labeled, count-badged chips (first picture + count), +sortable, with ~/− edit/delete, built in a set press by clicking +pictures in and out, minimum two, interval wheel retained; sort keys now +stack vertically LEFT of every sortable gallery; all side-scroll +galleries wear custom themed scrollbars inside evenly-padded wells; +SOURCES scrolls vertically past four entries; and the pair tray is +seeded with a dozen pairs so the scroller's position shows. Verified: +uniform key row, bench order and dial legend, pair tray overflow, solid +mint, set press round trip (mint "blues" of three), card count badge. +P35 remains the linked final for the matrix/bench; the wallpaper +sub-view's normative reference advances to this file. + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-37.html][Prototype 37 — annotation batch three]] (2026-07-22): nine more captures. +Channel faces go ICONIC — the sun-pair card is a fixed yellow/navy +diagonal with a dark sun glyph on the gold and a cream crescent on the +navy (per Craig's spec), SOLID wears dupre blue, and the world card's +mini clocks are redrawn as honest clocks (cream faces, hour + minute +hands at six different times; the world preview matches). Numbered slots +register as scenes named 1-4 so activation announces "▶ 1" instead of a +slot codename. The idle dial's center reads the EXACT minutes (halves +included) live while a tab drags, reverting to a now-visible IDLE (9px +cream) on release; the legend shows exact values too. The video +channel's preview shrinks to 55% so sources fit; the sub-view title +capitalizes to WALLPAPER. FOCUS tries the headphones glyph (F02CB) as +the deep-work icon — awaiting Craig's confirmation, crosshairs retired. +All seventeen applied annotations (batches two + three) swept from the +roam inbox after application; seven foreign items remain for their +projects. + * Metadata | Field | Value | |--------+----------------------------------------------| -| Status | draft | +| Status | doing | |--------+----------------------------------------------| | Owner | Craig Jennings | |--------+----------------------------------------------| @@ -65,6 +591,68 @@ gathers them. * Design sketch +** Prior art — how the major desktops solve the quick-settings UX + +Surveyed 2026-07-21 for the prototyping phase (ui-prototyping rule: research +before picking a direction). + +- *GNOME Shell 43+ Quick Settings* — replaced the decade-old system menu + with a grid of pill-shaped toggle tiles. Each pill is dual-function: click + the body to toggle, click a small arrow to expand a submenu that draws + over the grid rather than opening a new surface. Sliders span two grid + columns; on/off state is carried by the pill's fill colour. Component + anatomy: QuickToggle, QuickMenuToggle (toggle + popup submenu), + QuickSlider, in a column-based grid. + Sources: [[https://www.debugpoint.com/gnome-43-quick-settings/][debugpoint on the GNOME 43 redesign]], + [[https://gjs.guide/extensions/topics/quick-settings.html][gjs.guide Quick Settings anatomy]]. +- *macOS Control Center* — a menu-bar hub of modules. Controls are + dual-function here too: clicking a module's icon toggles it, clicking its + name or arrow opens a deeper sub-view expanded in place (Wi-Fi, Bluetooth, + Display, Sound). Modules can be dragged out to the menu bar for permanent + glanceable placement — the same two-layer bar/panel split this spec + commits to (touchpad + caffeine stay on the bar; the rest collapses in). + Sources: [[https://support.apple.com/guide/mac-help/quickly-change-settings-mchl50f94f8f/mac][Apple Support — Use Control Center on Mac]], + [[https://macmost.com/customizing-and-using-control-center-on-your-mac.html][macmost Control Center walkthrough]]. +- *Windows 11 Quick Settings* — a taskbar flyout: customizable button grid + with sliders below it. The trend line is consolidation: 26H2 adds a + light/dark toggle to the flyout so the Settings app isn't needed, and the + hardware volume/brightness flyouts are being restyled and made + repositionable. + Sources: [[https://pureinfotech.com/windows-11-26h2-quick-settings-dark-mode-toggle/][pureinfotech on the 26H2 flyout]], + [[https://pureinfotech.com/change-quick-settings-buttons-windows-11/][pureinfotech on button customization]]. +- *KDE Plasma 6* — the counter-example: no single consolidated panel; + per-applet popups hang off individual system-tray icons (6.4 raised + control contrast and added tray-entry hiding). This is the + scattered-toggles status quo the spec exists to fix. + Source: [[https://www.omgubuntu.co.uk/2025/06/kde-plasma-6-4-review-new-features-linux-desktop][OMG! Ubuntu Plasma 6.4 review]]. +- *iOS / macOS Focus modes* — the prior art for scenes: named modes that + flip several settings in one action, surfaced as first-class controls in + Control Center. The shared prototype engine's SCENES model mirrors this. + +Convergent conventions the directions are judged against: a two-layer model +(glanceable bar + consolidated panel); dual-function controls (toggle on the +control itself, expand for depth); compact toggle grid with full-width +sliders; sub-views drawn inside the panel rather than as new windows +(matches the wallpaper-sub-view decision); scenes as first-class controls. + +** Final prototype (normative reference) + +[[file:../prototypes/2026-07-02-desktop-settings-panel-prototype-37.html][Prototype 37 — the sealed final]] (2026-07-22, Craig's "Build it"), +superseding P35 as the single normative reference after annotation +batches two and three: +the consolidated machine every build phase points at. Structure: the +PROGRAMMABLE PRESETS matrix (glyph program keys + four numbered slots, +white punch pins with jewel verdicts, arrow/equals CPU tape wheels, +active-is-live semantics); the instrument bench of three encircled +groups (BRIGHTNESS drum pair, POINTERS console keys, IDLE tripper dial +with color-tabbed stages and caffeine bypass); and the WALLPAPER +channel sub-view (seven mode cards, per-mode descriptions, side-scroll +galleries, previews, the labeled pair press with mint/edit/delete, and +per-mode sources). Verified by a 17-point end-to-end suite, all +passing. The full design history lives under Prototype iterations in +the Status section; the kit-candidate components accumulated along the +way live in prototypes/dupre-kit-additions.js pending the kit merge. + ** Architecture — clone the net panel's proven stack - GTK4 + gtk4-layer-shell, Blueprint .blp sources compiled to committed .ui @@ -90,14 +678,25 @@ gathers them. |--------------------+----------+----------------------------------------------| | Auto-dim toggle | toggle | hyprctl decoration:dim_inactive (dim-toggle) | |--------------------+----------+----------------------------------------------| -| Idle / caffeine | toggle | hypridle start/stop (caffeine-toggle) | +| Idle / caffeine | toggle | hypridle start/stop (caffeine-toggle). | +| | | Back to a plain toggle as of P21 — the timed | +| | | wind-up moved to the sleep timer below. | +|--------------------+----------+----------------------------------------------| +| Idle program rail | tripper | the hypridle pipeline as a time-switch | +| | rail | tripper rail: drag DIM / LOCK / WATCH / | +| | | SCREEN OFF / SUSPEND tabs along an | +| | | idle-minutes scale (writes hypridle.conf + | +| | | reload); a tab in the OFF siding disables | +| | | its stage; engaged caffeine renders the | +| | | whole rail bypassed. Replaced the P21 sleep | +| | | timer at P28 — Craig ratified intent B | +| | | ("when idle for X", not "in X from now"): | +| | | this panel programs futures. | |--------------------+----------+----------------------------------------------| | Touchpad toggle | toggle | toggle-touchpad + touchpad-state file | |--------------------+----------+----------------------------------------------| | Mouse toggle | toggle | same mechanism, mouse-state file | |--------------------+----------+----------------------------------------------| -| Airplane mode | toggle | airplane-mode script (laptop-only row) | -|--------------------+----------+----------------------------------------------| | Screen brightness | slider | brightnessctl (backlight class), slider + % | |--------------------+----------+----------------------------------------------| | Keyboard backlight | slider | brightnessctl (kbd_backlight class), slider | @@ -105,14 +704,21 @@ gathers them. | Night light / | toggle | color-temperature shift (candidate: | | color temperature | | hyprsunset; confirm the tool at build) | |--------------------+----------+----------------------------------------------| -| Do Not Disturb | toggle | dunstctl set-paused (pause notifications) | +| Do Not Disturb | toggle | dunstctl set-paused (pause notifications). | +| | | Verified 2026-07-21: pause queues, then | +| | | floods everything on unpause — never drops. | +| | | DECIDED (Craig, 2026-07-21): class-split. | +| | | Ordinary notifications DROP at unpause; | +| | | alarms (urgency critical + rules promoting | +| | | known alarm sources) SURVIVE DND and show. | +| | | Build detail: dunst pause levels with | +| | | override_pause_level lets alarms punch | +| | | through live vs held-to-unpause — pick at | +| | | build. History keeps everything either way. | |--------------------+----------+----------------------------------------------| | Power profile | selector | powerprofilesctl (performance / balanced / | | | | power-saver, three-way) | |--------------------+----------+----------------------------------------------| -| Lock / suspend | actions | hyprlock / systemctl suspend (momentary | -| | | buttons, not stateful toggles) | -|--------------------+----------+----------------------------------------------| | Scenes / profiles | composite| one control that flips several toggles at | | | | once: Focus, Presentation, Battery-saver, | | | | Night (see the Scenes decision below) | @@ -124,10 +730,50 @@ gathers them. Slider changes apply live (throttled) and read back the actual level after apply — verify-everything. Toggles re-read their source of truth after firing, same as the bar indicators do, and the bar modules get their refresh -signals so both surfaces agree. The lock/suspend buttons are momentary -actions with no persistent state to reflect. A scene reads back each toggle it +signals so both surfaces agree. A scene reads back each toggle it sets and reports the composite as applied only when every member verified. +** Wallpaper sub-view — the mode table (Craig, 2026-07-22) + +Craig's reframe, thinking out loud: every mechanism proposal (carousel, +light table, film strip) assumed uniform framed objects, but the real +units of choice are MODES — heterogeneous programs for what the screen +shows. His inventory: + +| Mode | Representation | Options | +|-------------------+--------------------------------+--------------------------| +| Night watch | to be designed | none | +|-------------------+--------------------------------+--------------------------| +| Sunup/sundown | slashed thumbnail — the thumb | choose + save pic PAIRS | +| | split vertically or diagonally | | +| | (top-right to bottom-left), | | +| | day pic one side, night the | | +| | other | | +|-------------------+--------------------------------+--------------------------| +| Single | thumbnail | none | +|-------------------+--------------------------------+--------------------------| +| Randomize | open (deck/fan candidate) | interval spinner (mins | +| | | before next random item) | +|-------------------+--------------------------------+--------------------------| +| Video (Apple-TV | video thumbnail | none | +| aerial style) | | | +|-------------------+--------------------------------+--------------------------| +| Custom clock | picture of the clock | none | +|-------------------+--------------------------------+--------------------------| +| World clock | matrix of mini-clocks | none | +|-------------------+--------------------------------+--------------------------| + +Clock-channel backings (Craig, 2026-07-22): neither clock is a new +design. CUSTOM CLOCK renders the clock that already appears when the +waybar time module is clicked; WORLD CLOCK renders a matrix of miniature +clocks with their timezones and cities — the same data the time module's +hover tooltip shows. One owner per clock design; the wallpaper channels +are projections of those existing surfaces onto the desktop. + +Pair semantics (Craig): pairs are SAVED OBJECTS — build one by choosing a +day pic and a night pic, alter either, confirm; the pair is minted and +remains selectable as a unit thereafter. The pair library persists. + ** Open/close behavior Gear glyph module on the bar right cluster; click toggles the panel @@ -140,6 +786,8 @@ Close button, matching the net panel. Keybind: Super+Shift+G (decision B). CLOSED: [2026-07-04 Sat] Resolved (Craig, 2026-07-04): touchpad and caffeine stay on the bar (glanceable state); auto-dim and airplane move into the panel (panel-only, freeing bar width). The airplane Super+Shift+A toggle keybind stays as the quick lane — only its bar indicator collapses in. +Amended 2026-07-22 (Craig, during the first live drive of the panel): caffeine's bar indicator collapses into the panel too. The bar keeps only the touchpad indicator; dim and caffeine are panel-owned (their Super+Shift+D / Super+I keybinds stay as quick lanes). Applied live same day — modules and their state CSS removed from the waybar config. + ** DONE Keybind for the panel? CLOSED: [2026-07-04 Sat] Resolved (Craig, 2026-07-04): Super+Shift+G (gear), for parity with the other panels' fast path. @@ -165,6 +813,18 @@ OUT of this panel, with reasons: volume / master-mute stays with the audio panel (this panel does not mirror it — one owner per control); theme light/dark goes to the separate theme-studio task, not here. +Amended 2026-07-22 (Craig): airplane mode is withdrawn from the panel too — +it's about the network, and the network panel carries it as its main +on/off. Same one-owner-per-control reasoning; keeping a copy here +overcomplicated the switch wall. (The Super+Shift+A keybind and the +network panel's master switch are the two lanes.) + +Amended 2026-07-21 (Craig): lock and suspend are withdrawn from the panel. +Session-exit actions live on the wlogout screen (Super+Shift+Q: +lock/reboot/shutdown/logout/suspend/hibernate) — same one-owner-per-control +reasoning as volume. The direction prototypes still show lock/suspend +buttons; the iterate-to-final pass drops them. + Format pickers (time/date/weather display format) are pulled out entirely into their own future sibling spec. The real problem there is a single source of truth — many programs store their own format, so the design question is where @@ -179,6 +839,71 @@ Confirmed the panel gathers the mouse/trackpad toggle, a no-sleep (idle-inhibit) - switch between two pictures at sunup / sundown (a day/night pair). That last one implies a sun-time source (a lat/long or a sunrise/sunset lookup). +** DONE Idle pipeline — staged timeouts and an idle power action +CLOSED: [2026-07-21 Tue] +Resolved (Craig, 2026-07-21), the lean package with one revision: dim +5 min/10% and lock 7 min stay; add screen-off (DPMS) at 10 min; idle +suspend-then-hibernate at 30 min — default ON for velox, and PRESENT on +ratio too (not hidden by battery detection) but default OFF with an +armed-trigger confirmation before enabling, since an idle-suspended desktop +drops tailscale sessions and hourly backups. Hyprlock grace 5 s. The panel +surfaces only the situational control (the caffeine timer dial); stage +delays live in hypridle.conf. Original analysis below. + +Amended same evening (Craig, 2026-07-21): the eye-candy stage is IN after +all — as the Dupre Instrument Night Watch, a fullscreen board of the kit's +own instruments on watch (split-flap + movement + seven-seg carrying real +time, radar sweeping, scope tracing, drift chart, spectrum, VU programme, +blinkenlights core, elapsed tape counter, systems lamps, voice loops, VFD +wire, engine-order telegraph that rings a new order every couple of +minutes). Prototype: +[[file:../prototypes/2026-07-21-night-watch-screensaver-prototype-1.html][night-watch prototype 1]]. +It runs between lock and screen-off as a hypridle stage (fullscreen +browser/webview launched on-timeout, killed on-resume); the power cost is +accepted for the stage's duration since DPMS still follows at its delay. +Kit note for the merge task: span-built instruments (scope, stripChart, +vfdMarquee, analogClock, rotsel, keylock) need a blockifying host outside +the gallery's card grid — the night-watch page carries the one-rule fix. +Opened 2026-07-21 from Craig's question: "shutdown/reboot/suspend/hibernate +when idle for X minutes?" Yes — hypridle listeners run arbitrary on-timeout +commands, so any systemctl action can be a stage. Today's pipeline is only +dim-to-10% at 5 min → lock at 7 min; there is no screen-off (DPMS) stage and +no power action, so an idle machine glows at 10% forever. + +Proposed idle group for the panel, replacing the bare caffeine on/off: +- Dim delay + dim level (currently hardcoded 300 s / 10%). +- Lock delay (currently 420 s). +- Screen-off (DPMS) stage after N minutes — new. +- Idle power action after X minutes — one of none / suspend / hibernate / + suspend-then-hibernate / shutdown. Ordering guarantee: lock always fires + before the power action so the machine wakes locked. +- Caffeine becomes a timed inhibit (wind-up timer, e.g. the kit timerDial: + 30/60/120 min or until-off) instead of killing the whole daemon. + +Added 2026-07-21 (Craig approved recording these alongside the stages): +- Hyprlock grace period (currently grace = 0): N seconds after lock where + any input wakes without a password — softens an aggressive lock delay. +- Optional eye-candy stage: an animated "screensaver" (mpvpaper loop, + terminal art, shader) launched by a hypridle stage and killed on resume. + Sits before the DPMS stage if offered at all; it holds GPU + display + awake, so it trades power for style. +- Lock-face notes for the build: path = screenshot shows a blurred image + of the work session (privacy consideration; wallpaper/solid is the + alternative), and hyprlock labels can run shell commands on an interval + (cmd[update:N]) — the lock face can carry weather/now-playing/battery. + +Build notes: Wayland's idle-inhibit protocol already keeps the pipeline at +bay during media playback (video players and browsers inhibit idle, and +hypridle honors it), so caffeine's real audience is passive non-media use — +long reads, watching a build, screen-share without input. systemd +suspend-then-hibernate is the likely default for velox +(suspends now, hibernates when the battery drains); shutdown-on-idle is the +risky option (kills the session and anything unsaved) and wants a long delay +if offered at all; AC-vs-battery differentiation is possible by making the +on-timeout a script that checks the power source. Decisions needed from +Craig: which stages to expose, the default delays, and whether the power +action appears on both machines or only the laptop. + ** DONE Scenes / profiles — one control that flips several toggles at once CLOSED: [2026-07-19 Sun] Resolved (Craig, 2026-07-19): the panel offers named scenes that set several @@ -199,12 +924,127 @@ build detail for the sub-view. * Implementation phases -1. settings.py backings (brightness get/set, kbd backlight, toggle - state readers) — pure engine, TDD with fake brightnessctl/hyprctl. -2. PanelModel presenter (rows, conditional visibility, verify-after-apply - semantics) — unit-tested, no GTK. -3. Blueprint UI + gear bar module + open/close wiring; palette css factored - to a shared asset; AT-SPI smoke. -4. Bar-module consolidation per decision A: drop the dim and airplane bar - modules (now panel-only), keep touchpad and caffeine on the bar, wire the - refresh signals so bar and panel agree, and bind Super+Shift+G. +All phases live in dotfiles =settings/= (archsetup drives the lifecycle). +The normative reference for every UI decision is prototype 37; the design +history is the Prototype iterations subsection. + +1. *Backings engine* — pure Python, TDD with fake binaries. Brightness + + keyboard backlight (brightnessctl, 5% floor, ×10 drum mapping); toggle + backings (hyprctl dim_inactive, touchpad/mouse state scripts, hypridle + caffeine inhibit); DND via dunst pause levels implementing the + class-split semantic (ordinary notifications drop, alarms survive — + punch-through vs held decided here); powerprofilesctl three-way; the + hypridle.conf writer + reload for the idle pipeline (four stages, per- + stage enable/disable, hyprlock grace); the wallpaper engine — persisted + mode store (channel, single, minted pairs/sets/colors with labels, + video, interval, sources), setter adapters (images via the chosen + setter, solid colors, video via mpvpaper, clock/world/watch as + projected surfaces), sunrise/sundown calculation from lat/long, the + random-draw timer, and extension filtering for the galleries. +2. *Presenters* — GTK-free models, unit-tested to 100% like the net + PanelModel. The program-matrix model (programs as complete + definitions; active-is-live with program-membership writes surviving + activation; pin, letter, and radio row kinds; slots 1-4 as first-class + programs); the bench model (drum mapping, pointers, idle stages with + order clamping, parking, and caffeine bypass); the wallpaper channels + model (mint/edit/delete and alpha/recency sort for pairs, sets, and + colors; per-mode sources visibility; previews). +3. *GTK4/Blueprint UI* — port the instruments prototype 37 actually uses + to GTK widgets in the Dupre family chrome: console keys, the + programmable matrix (glyph + numbered heads, jewel verdicts, tape + wheels), drum roller, tripper dial (bottom legend, exact-minutes drag + counter, bypass stamp), encircled bench groups, iconic channel cards, + side-scroll gallery wells with themed scrollbars, the pair/set/color + presses. Architecture rule from the prototypes: instruments that own a + clock own their widget lifetime (never rebuilt on state refresh). + Layer-shell chassis under the bar, wallpaper sub-view navigation, + AT-SPI smoke per the family pattern. +4. *Integration and bar* — gear module + Super+Shift+G; bar consolidation + per decision A as amended (dim and caffeine modules dropped 2026-07-22; + airplane's bar indicator is gone with the control — the network panel + owns it; only touchpad keeps a bar indicator; wire refresh signals + both ways); the + watch/clock/world renderers as projected surfaces (the night watch as + the hyprlock face per the WATCH-is-the-lock-screen resolution); live + hypridle rewrite-and-reload; dunst pause-level configuration; end-to- + end verify-everything against the real machine. + +Test surface: fake-binary suites for every backing; presenter suites +covering program recall/edit semantics (including the active-scene- +survives-member-writes invariant), idle-stage clamping/parking/bypass, +and mint/edit/delete/sort round trips for all three minted stores; one +AT-SPI smoke; a scripted end-to-end pass mirroring prototype 37's +17-point suite. + +* Review findings [7/7] + +Reviewed 2026-07-22 (in-session, both roles — the annotation-batch method +kept author and reviewer in one loop; Craig's "Build it" sealed the +design). Rubric: *Ready with caveats* — the caveats are build-time picks, +not design gaps. + +** DONE Implementation phases reflected the pre-casting roster +CLOSED: [2026-07-22 Wed] +The four phases still described the original controls table (lock/ +suspend, per-toggle rows). Rewritten this pass to the settled design — +matrix, bench, wallpaper channels, projected surfaces — with the test +surface enumerated. +** DONE Stale architecture prose vs settled design +CLOSED: [2026-07-22 Wed] +The Design sketch accumulated its corrections through the iteration +entries (controls table amended in place; wallpaper mode table and +clock-channel backings recorded; Final prototype section added). The +iterations subsection is the authoritative design log. +** DONE Wallpaper setter tool selection :build: +CLOSED: [2026-07-22 Wed] +DECIDED (phase 1, 2026-07-22): awww 0.12.1 — the swww fork already +running as the wallpaper daemon (=swww= on PATH is a symlink to it; +=set-wallpaper= already drives it). Images and slashed pairs go through +awww =img=; solid colors through awww =clear =; video via +mpvpaper (not yet installed — install lands in phase 4 integration); +clock/world/watch are projected layer-shell surfaces, not setter jobs. +The engine's setter adapter takes the binary from env so the fake-binary +suite and a future setter swap both stay one seam. +** DONE Speedrun pre-flight picks (Craig, 2026-07-22) :build: +CLOSED: [2026-07-22 Wed] +Batched before the phases 2-4 autonomous run: +- Alarm sources for DND: wtimer only — its notifications get the + CRITICAL promotion in the phase-4 dunstrc rules; everything else + drops at unpause. +- Night-watch host: google-chrome-stable in kiosk mode, launched by the + hypridle watch stage, killed on resume. +- Sun-pair location: live whereami lookup at each transition (the + machine's own location is the right datum for its screen), with the + last good reading cached in state.json as the offline fallback. +- Factory scenes (Focus / Presentation / Battery-saver / Night): drafted + against the controls table during phase 2, corrected by Craig in the + panel afterward. +** DONE DND unpause variant — held, not punch-through :build: +CLOSED: [2026-07-22 Wed] +DECIDED (Craig, 2026-07-22, first live drive): the held-to-unpause +variant. While DND is on nothing shows, alarms included (pause level +100 holds everything). At release, ordinary notifications are dropped +(history keeps them) and the alarms that arrived during DND surface: +close-all → read history → unpause → history-pop each CRITICAL entry +newer than the engage stamp, oldest first. The engine records the +engage instant as CLOCK_MONOTONIC microseconds (dunst's history +timestamp base) in $XDG_RUNTIME_DIR. The controls-table note offering +punch-through-live is superseded by this finding. +** DONE Night-light tool selection :build: +CLOSED: [2026-07-22 Wed] +DECIDED (phase 1, 2026-07-22): gammastep (installed; hyprsunset is not). +On Wayland the gamma ramp only holds while a client owns the +wlr-gamma-control protocol, so gammastep stays resident while night +light is on and the backing mirrors caffeine's daemon model: engage = +spawn detached (=gammastep -O =, default 4500K), release = pkill, +pgrep as the one source of truth. Swapping to hyprsunset later is one +adapter change behind the same seam. +** DONE Persisted store location and format :build: +CLOSED: [2026-07-22 Wed] +DECIDED (phase 1, 2026-07-22): one JSON document at +=~/.config/desktop-settings/state.json= (root overridable via +=DESKTOP_SETTINGS_CONFIG_DIR= for tests) carrying programs (slots 1-4), +idle stages, and the wallpaper store (channel, pairs, sets, colors, +sources, interval). Factory presets ship as defaults in code; the store +writes atomically (temp file + rename) and tolerates a missing or +corrupt file by falling back to defaults. -- cgit v1.2.3