aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-22 15:15:02 -0500
committerCraig Jennings <c@cjennings.net>2026-07-22 15:15:02 -0500
commitf1c661f50d6af2a115fc1430ae0478f2acef2dec (patch)
treeb894ea2e14327794bb75671b962f37185cff22ea /docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html
parentf594068bed75302aba50793ccf5e6ce9cf53c7f8 (diff)
downloadarchsetup-f1c661f50d6af2a115fc1430ae0478f2acef2dec.tar.gz
archsetup-f1c661f50d6af2a115fc1430ae0478f2acef2dec.zip
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.
Diffstat (limited to 'docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html')
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-35.html717
1 files changed, 717 insertions, 0 deletions
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 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 35 (FINAL candidate)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ :root{
+ --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917;
+ --silver:#bfc4d0; --cream:#f3e7c5; --steel:#969385; --dim:#7c838a;
+ --slate:#424f5e; --slate-hi:#54677d; --wash:#2c2f32;
+ --pass:#74932f; --fail:#cb6b4d;
+ --phos:#7fe0a0; --phos-dim:#1c3626; --vfd:#63e6c8;
+ --sevgrn:#57d357; --sevred:#e2543f; --sevoff:#1a1613;
+ --jewel-r:#ff5b45; --jewel-a:#ffb43a; --jewel-g:#6fce33;
+ --gold:#dab53d; --gold-hi:#ffd75f;
+ --amber-grad-top:#f0d879; --amber-grad-mid:#dab53d; --amber-grad-bot:#8f7a1f;
+ --amber-edge:#7d6a16; --amber-warn:#edc95d;
+ --glow-hi:255,215,95; --glow-lo:218,181,61;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ body.ds-stage {
+ background: radial-gradient(1200px 600px at 70% -10%, #1c1915 0%, transparent 60%), var(--ground);
+ }
+ .ds-panel { width: 540px; background-color: var(--panel); border-width: 1.6px; }
+ .ds-face {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #262320;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.04);
+ }
+ .ds-face .title { color: var(--gold); letter-spacing: .12em; font-size: 15px; }
+ .ds-caption { max-width: 520px; }
+ .bench { display: flex; align-items: stretch; justify-content: center; gap: 12px; }
+ .fcol { display: flex; flex-direction: column; gap: 12px; }
+ .fbox {
+ position: relative;
+ border: 1px solid #35302a;
+ border-radius: 10px;
+ padding: 14px 12px 10px;
+ display: flex; align-items: center; justify-content: center; gap: 10px;
+ }
+ .fbox .flbl {
+ position: absolute; top: -7px; left: 12px;
+ background: var(--raise);
+ padding: 0 7px;
+ color: var(--steel); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
+ }
+ .fbox .dupre-key.ptr { width: 96px; height: 24px; font-size: 8px; letter-spacing: .08em; padding: 0; }
+ .plate {
+ background: linear-gradient(180deg, var(--raise), var(--panel));
+ border: 1px solid #2c2823;
+ border-radius: 11px;
+ padding: 10px;
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.05), 0 2px 6px rgba(0,0,0,.35);
+ }
+ .plate .ds-engrave { margin: 0 2px 8px; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+ .tcell { position: relative; }
+ .pointing { display: flex; justify-content: center; gap: 24px; }
+ .pointing .dupre-rocker { width: 50px; height: 32px; }
+ .pointing .dupre-rocker .dupre-half { font-size: 8px; }
+ /* ---- wallpaper channel bank ---- */
+ .wp-chans { display: grid; grid-template-columns: repeat(7, 1fr); gap: 6px; }
+ .wp-chan {
+ border: 1px solid #33302b; border-radius: 8px; padding: 4px;
+ background: linear-gradient(180deg, var(--key-top, #23211e), var(--key-bot, #191715));
+ cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 3px;
+ }
+ .wp-chan:hover { border-color: var(--gold); }
+ .wp-chan.on { border-color: #a9c95f; box-shadow: 0 0 8px rgba(169,201,95,.25); }
+ .wp-chan .face { width: 100%; aspect-ratio: 16/10; border-radius: 4px; overflow: hidden; position: relative; background: #0c0a08; }
+ .wp-chan .nm { font-size: 7px; letter-spacing: .08em; color: var(--dim); text-transform: uppercase; }
+ .wp-chan.on .nm { color: #a9c95f; }
+ .half { position: absolute; inset: 0; }
+ .half.b { clip-path: polygon(100% 0, 100% 100%, 0 100%); }
+ .deck { position: absolute; inset: 0; }
+ .deck i { position: absolute; width: 68%; height: 68%; border-radius: 3px; border: 1px solid #0009; }
+ .mini-clocks { position: absolute; inset: 2px; display: grid; grid-template-columns: repeat(3,1fr); gap: 1px; }
+ .mini-clocks i { border-radius: 50%; border: 1px solid #4a4438; position: relative; background: #14110e; }
+ .mini-clocks i::after { content: ""; position: absolute; left: 50%; top: 18%; width: 1px; height: 34%; background: var(--gold); transform-origin: bottom; }
+ .mini-clocks i:nth-child(2)::after { transform: rotate(65deg); }
+ .mini-clocks i:nth-child(3)::after { transform: rotate(140deg); }
+ .mini-clocks i:nth-child(4)::after { transform: rotate(-50deg); }
+ .mini-clocks i:nth-child(5)::after { transform: rotate(110deg); }
+ .mini-clocks i:nth-child(6)::after { transform: rotate(-120deg); }
+ .clock-face { position: absolute; inset: 8% 25%; border-radius: 50%; border: 1.5px solid var(--cream); background: #14110e; }
+ .clock-face::before { content: ""; position: absolute; left: 50%; top: 12%; width: 1.5px; height: 40%; background: var(--cream); transform: rotate(40deg); transform-origin: bottom; }
+ .clock-face::after { content: ""; position: absolute; left: 50%; top: 22%; width: 1.5px; height: 30%; background: var(--gold); transform: rotate(-70deg); transform-origin: bottom; }
+ .watch-face { position: absolute; inset: 6% 22%; border-radius: 50%; border: 1px solid #3a5c2a; background: radial-gradient(circle, #0f1a0c, #060905); }
+ .watch-face::after { content: ""; position: absolute; left: 50%; top: 6%; width: 1px; height: 44%; background: #a9c95f; transform: rotate(55deg); transform-origin: bottom; box-shadow: 0 0 4px #a9c95f; }
+ .vid-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #fffc; font-size: 14px; text-shadow: 0 1px 3px #000; }
+
+ .wp-conf { margin-top: 10px; }
+ .wp-lib { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
+ .wp-th { aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .wp-th:hover { border-color: var(--gold); }
+ .wp-th.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .pair-bench { display: flex; align-items: center; gap: 10px; justify-content: center; }
+ .pair-slot { width: 104px; aspect-ratio: 16/10; border: 1.5px dashed #4a443a; border-radius: 6px; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-slot.filled { border-style: solid; }
+ .pair-slot.arm { border-color: var(--gold); }
+ .pair-slot .cap { position: absolute; bottom: 2px; left: 4px; font-size: 7px; letter-spacing: .12em; color: #fff9; text-shadow: 0 1px 2px #000; }
+ .pair-tray { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
+ .pair-chip { width: 64px; aspect-ratio: 16/10; border-radius: 5px; border: 1.5px solid #33302b; cursor: pointer; position: relative; overflow: hidden; }
+ .pair-chip.sel { border-color: var(--gold); box-shadow: 0 0 6px rgba(218,181,61,.4); }
+ .wp-note { color: var(--dim); font-size: 10px; margin-top: 6px; }
+ .wp-row2 { display: flex; align-items: center; gap: 10px; justify-content: center; margin-top: 8px; }
+ .int-wheel {
+ width: 46px; height: 30px; border-radius: 4px; background: #0a0908; border: 1px solid #3a352c;
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ color: #2b2418; font-weight: 700; font-size: 12px; font-family: var(--mono);
+ background: linear-gradient(90deg,#cfc6a8,#ece4c8 50%,#c6bd9e);
+ box-shadow: inset 0 4px 5px rgba(0,0,0,.35), inset 0 -4px 5px rgba(0,0,0,.35);
+ }
+ .ds-toast.empty { visibility: hidden; }
+ .wp-dirs { display: flex; flex-direction: column; gap: 4px; }
+ .wp-dir {
+ display: flex; align-items: center; gap: 8px;
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 6px;
+ padding: 5px 8px; font-size: 11px; color: var(--silver);
+ }
+ .wp-dir .path { color: var(--dim); flex: 1; }
+ .wp-dir .rm { color: var(--dim); cursor: pointer; font-size: 12px; padding: 0 3px; }
+ .wp-dir .rm:hover { color: var(--fail); }
+ .pair-chip .rm {
+ position: absolute; top: 1px; right: 2px; color: #fffa; cursor: pointer;
+ font-size: 10px; text-shadow: 0 1px 2px #000; display: none; padding: 0 2px;
+ }
+ .pair-chip:hover .rm { display: block; }
+ .pair-chip.armed { border-color: var(--fail); box-shadow: 0 0 6px rgba(203,107,77,.5); }
+ .pair-chip.armed .rm { display: block; color: var(--fail); }
+ .wp-scroll { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 5px; }
+ .wp-scroll .wp-th { flex: 0 0 62px; }
+ .wp-desc {
+ background: #0e0c0a; border: 1px solid #2c2823; border-radius: 8px;
+ color: var(--steel); font-size: 11px; line-height: 1.5; padding: 8px 10px; margin-top: 8px;
+ }
+ .wp-prev {
+ width: 100%; aspect-ratio: 16/9; border-radius: 8px; border: 1px solid #2c2823;
+ overflow: hidden; position: relative; background: #0c0a08;
+ }
+ .wp-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
+ .sortkeys { display: flex; gap: 6px; margin-bottom: 6px; }
+ .sortkeys .dupre-key.srt { width: 30px; height: 20px; font-size: 9px; padding: 0; }
+ .pair-scroll .pair-cell { flex: 0 0 66px; display: flex; flex-direction: column; }
+ .pair-cell .pair-chip { width: 66px; }
+ .pair-foot { display: flex; align-items: center; justify-content: space-between; height: 14px; }
+ .pair-lbl { font-size: 7.5px; color: var(--steel); letter-spacing: .04em; overflow: hidden; white-space: nowrap; }
+ .pbtn { color: var(--dim); cursor: pointer; font-size: 10px; padding: 0 2px; }
+ .pbtn:hover { color: var(--gold); }
+ .press-side { display: flex; flex-direction: column; gap: 6px; }
+ .wp-lblin {
+ background: #0a0908; border: 1px solid #3a352c; border-radius: 5px;
+ color: var(--cream); font-family: var(--mono); font-size: 10px; padding: 4px 7px; width: 120px;
+ }
+ .wp-lblin::placeholder { color: var(--dim); }
+ .wprow {
+ display: flex; align-items: center; gap: 8px;
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-radius: 8px;
+ padding: 8px 10px; cursor: pointer; color: var(--silver); font-size: 12px;
+ }
+ .wprow:hover { border-color: var(--gold); color: var(--gold); }
+ .wprow .k-state { margin-left: auto; color: var(--dim); font-size: 10px; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Prototype 35 — the final candidate.</b> 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.
+ </div>
+
+ <div class="ds-panel" id="panel">
+ <div class="ds-face">
+ <span class="glyph">⚙</span>
+ <span class="title">SETTINGS</span>
+ <span class="sub" id="scene-sub"></span>
+ <span class="x">✕</span>
+ </div>
+ <div id="body"></div>
+ <div class="ds-toast" id="toast"></div>
+ <div class="ds-subview" id="subview"></div>
+ </div>
+
+<script src="widgets.js"></script>
+<script src="dupre-kit-additions.js"></script>
+<script src="desktop-settings-shared.js"></script>
+<script>
+const h = DS.h;
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const SCENES = Object.keys(DS.SCENES);
+const SWITCH_ROWS = [["AUTO-DIM", "autodim"], ["NIGHT LIGHT", "nightlight"], ["DO NOT DISTURB", "dnd"], ["CAFFEINE", "caffeine"]];
+const PWR_ROW = "CPU POWER";
+const PWR_VALUES = ["\u2191", "=", "\u2193"]; // up / equals / down, ordered with DS.PROFILES
+const LETTER_OF = { performance: "\u2191", balanced: "=", saver: "\u2193" };
+const PWR_OF = { "\u2191": "performance", "=": "balanced", "\u2193": "saver" };
+let booting = true;
+// the idle policy the rail programs (mock persistence across renders)
+const IDLE_STAGES = [
+ { id: "dim", label: "DIM", color: "#d29638", minutes: 5 },
+ { id: "lock", label: "LOCK · WATCH", color: "#54677d", minutes: 7 },
+ { id: "dpms", label: "SCREEN OFF", color: "#969385", minutes: 10 },
+ { id: "suspend", label: "SUSPEND", color: "#cb6b4d", minutes: 30 },
+];
+
+// two unassigned program slots (blank keys, all-off programs)
+["Slot5", "Slot6", "Slot7", "Slot8"].forEach((n) => { if (!DS.SCENES[n]) DS.SCENES[n] = { glyph: "", sets: {} }; });
+
+// factory presets (Craig, 2026-07-22) — rows: AUTO-DIM, NIGHT LIGHT, DND, CAFFEINE + CPU
+DS.SCENES.Focus.sets = { autodim: true, nightlight: false, dnd: true, caffeine: true, powerprofile: "performance" };
+DS.SCENES.Presentation.sets = { autodim: false, nightlight: false, dnd: true, caffeine: true, powerprofile: "balanced" };
+DS.SCENES.Battery.sets = { autodim: true, nightlight: false, dnd: false, caffeine: false, powerprofile: "saver" };
+DS.SCENES.Night.sets = { autodim: true, nightlight: true, dnd: false, caffeine: true, powerprofile: "saver" };
+
+// normalize scene programs to COMPLETE definitions: recall = the column's
+// pin pattern becomes the truth (pin on, no pin off; one power pin each).
+// Iterate the live key set so the just-registered blank slots normalize too.
+Object.keys(DS.SCENES).forEach((n) => {
+ const sets = DS.SCENES[n].sets;
+ SWITCH_ROWS.forEach(([, key]) => { sets[key] = !!sets[key]; });
+ if (!sets.powerprofile) sets.powerprofile = "balanced";
+});
+
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+// ---- wallpaper: channels state (mock persistence) -------------------------
+const SWATCH = [
+ "linear-gradient(135deg,#2a3d5c,#101b2e)", "linear-gradient(135deg,#5c3a2a,#2e1810)",
+ "linear-gradient(135deg,#3a5c2a,#16240f)", "linear-gradient(135deg,#40364f,#1b1626)",
+ "linear-gradient(135deg,#1a1e2e,#05070d)", "linear-gradient(135deg,#5c5030,#2e2810)",
+ "linear-gradient(135deg,#6b4a5c,#2e1420)", "linear-gradient(135deg,#2a5c55,#0f2420)",
+ "linear-gradient(135deg,#7a6248,#3a2c1a)", "linear-gradient(135deg,#23364a,#0a1018)",
+ "linear-gradient(135deg,#4a5a3a,#1c2410)", "linear-gradient(135deg,#5a3a4a,#240f1c)",
+];
+const VIDEOS = ["linear-gradient(160deg,#38506b,#0d1520)", "linear-gradient(160deg,#5c4a2a,#241a08)", "linear-gradient(160deg,#2c4a3e,#0a1a12)"];
+const WP = {
+ channel: "single",
+ single: 2,
+ pairs: [{ day: 1, night: 4, label: "harbor", ts: 1 }, { day: 8, night: 9, label: "", ts: 2 }],
+ pairSel: 0,
+ video: 0,
+ interval: 30,
+ building: { day: null, night: null, label: "", arm: null },
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ sort: "time",
+ mintSeq: 3,
+};
+const INTERVALS = [5, 10, 15, 30, 60];
+const CHANNELS = [
+ { id: "watch", nm: "Watch" },
+ { id: "pair", nm: "Sun pair" },
+ { id: "single", nm: "Single" },
+ { id: "random", nm: "Random" },
+ { id: "video", nm: "Video" },
+ { id: "clock", nm: "Clock" },
+ { id: "world", nm: "World" },
+];
+const SOURCED = ["single", "pair", "random", "video"]; // generator modes hide sources
+const DESC = {
+ watch: "Plays the Dupre night watch: the instrument board keeps real time while the machine idles. The same board is the lock screen's face.",
+ pair: "Two pictures, one day. The day picture shows from sunrise, the night picture from sundown — sun-times from your location. Pick a minted pair from the gallery, or press a new one below: fill both frames from the pictures, name it if you like, and mint.",
+ single: "One picture, always. Pick it from the gallery; the preview shows what ships.",
+ random: "Draws a fresh picture from your sources on the wheel's interval. Click the wheel to change the minutes.",
+ video: "Plays a video loop as the wallpaper, aerial style. Pick the loop from the gallery.",
+ clock: "Fills the screen with the big clock — the same clock the time module opens when clicked.",
+ world: "Fills the screen with a matrix of world clocks — the cities and timezones from the time module's hover.",
+};
+const DIR_CANDIDATES = ["~/pictures/travel", "~/pictures/astro", "~/downloads/walls", "~/pictures/scans"];
+
+function chanFace(id) {
+ const f = h("div", "face");
+ if (id === "single") { const i = h("div", "half"); i.style.background = SWATCH[WP.single]; f.appendChild(i); }
+ else if (id === "pair") {
+ const p = WP.pairs[WP.pairSel] || WP.pairs[0];
+ const a = h("div", "half"); a.style.background = p ? SWATCH[p.day] : "#222";
+ const b = h("div", "half b"); b.style.background = p ? SWATCH[p.night] : "#111";
+ f.appendChild(a); f.appendChild(b);
+ } else if (id === "random") {
+ const d = h("div", "deck");
+ [[8, "18%", "4%", "-6deg"], [4, "10%", "16%", "3deg"], [0, "2%", "28%", "-2deg"]].forEach(([si, top, left, rot]) => {
+ const c = h("i"); c.style.cssText = `top:${top};left:${left};transform:rotate(${rot});background:${SWATCH[si]}`;
+ d.appendChild(c);
+ });
+ f.appendChild(d);
+ } else if (id === "video") {
+ const i = h("div", "half"); i.style.background = VIDEOS[WP.video]; f.appendChild(i);
+ f.appendChild(h("div", "vid-play", "\u25B6"));
+ } else if (id === "clock") f.appendChild(h("div", "clock-face"));
+ else if (id === "world") {
+ const m = h("div", "mini-clocks");
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ f.appendChild(m);
+ } else if (id === "watch") f.appendChild(h("div", "watch-face"));
+ return f;
+}
+
+function scrollGallery(cls) {
+ return h("div", "wp-scroll" + (cls ? " " + cls : ""));
+}
+
+function wallpaperView() {
+ const sv = h("div", "ds-col");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">\u2039 Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = () => subview.classList.remove("open");
+ sv.appendChild(head);
+ const body2 = h("div", "ds-col");
+ sv.appendChild(body2);
+
+ const paint = () => {
+ body2.innerHTML = "";
+
+ // modes — labeled and divided like every family section
+ body2.appendChild(DS.engrave("Modes"));
+ const bank = h("div", "wp-chans");
+ CHANNELS.forEach((c) => {
+ const card = h("div", "wp-chan" + (WP.channel === c.id ? " on" : ""));
+ card.appendChild(chanFace(c.id));
+ card.appendChild(h("div", "nm", c.nm));
+ card.onclick = () => { WP.channel = c.id; DS.toast("Wallpaper: " + c.nm, true); paint(); };
+ bank.appendChild(card);
+ });
+ body2.appendChild(bank);
+
+ // description — helpful text for every mode, never "no options"
+ body2.appendChild(h("div", "wp-desc", DESC[WP.channel]));
+
+ const conf = h("div", "wp-conf");
+ const previewBox = (bg, imgSrc) => {
+ const pv = h("div", "wp-prev");
+ if (imgSrc) { const img = h("img"); img.src = imgSrc; pv.appendChild(img); }
+ else if (bg) pv.style.background = bg;
+ return pv;
+ };
+
+ if (WP.channel === "single") {
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th" + (i === WP.single ? " sel" : ""));
+ t.style.background = sw;
+ t.onclick = () => { WP.single = i; DS.toast("Wallpaper set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(SWATCH[WP.single]));
+ } else if (WP.channel === "pair") {
+ // minted pair gallery: side-scroll, sortable, labeled, editable
+ conf.appendChild(DS.engrave("Minted pairs"));
+ const sortRow = h("div", "sortkeys");
+ [["alpha", "a"], ["time", "\u{f0150}"]].forEach(([mode, glyph]) => {
+ const k = h("button", "dupre-key srt" + (WP.sort === mode ? " dupre-on" : ""), glyph);
+ k.onclick = () => { k.blur(); WP.sort = mode; paint(); };
+ sortRow.appendChild(k);
+ });
+ conf.appendChild(sortRow);
+ const tray = scrollGallery("pair-scroll");
+ const order = WP.pairs.map((p, i) => ({ p, i }));
+ if (WP.sort === "alpha") order.sort((x, y) => (x.p.label || "\uffff").localeCompare(y.p.label || "\uffff"));
+ else order.sort((x, y) => y.p.ts - x.p.ts);
+ order.forEach(({ p, i }) => {
+ const cell = h("div", "pair-cell");
+ const chip = h("div", "pair-chip" + (i === WP.pairSel ? " sel" : ""));
+ const a = h("div", "half"); a.style.background = SWATCH[p.day];
+ const b = h("div", "half b"); b.style.background = SWATCH[p.night];
+ chip.appendChild(a); chip.appendChild(b);
+ chip.onclick = () => { WP.pairSel = i; DS.toast("Pair selected", true); paint(); };
+ cell.appendChild(chip);
+ const foot = h("div", "pair-foot");
+ const ed = h("span", "pbtn", "~");
+ ed.title = "edit — reopens in the press";
+ ed.onclick = () => {
+ WP.building = { day: p.day, night: p.night, label: p.label, arm: null };
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair opened in the press", true);
+ paint();
+ };
+ foot.appendChild(ed);
+ foot.appendChild(h("span", "pair-lbl", p.label || ""));
+ const del = h("span", "pbtn", "\u2212");
+ del.title = "delete — recreate to get it back";
+ del.onclick = () => {
+ WP.pairs.splice(i, 1);
+ if (WP.pairSel >= WP.pairs.length) WP.pairSel = WP.pairs.length - 1;
+ DS.toast("Pair deleted", true);
+ paint();
+ };
+ foot.appendChild(del);
+ cell.appendChild(foot);
+ tray.appendChild(cell);
+ });
+ conf.appendChild(tray);
+
+ // the pair press, with its label field
+ conf.appendChild(DS.engrave("Pair press"));
+ const bench = h("div", "pair-bench");
+ ["day", "night"].forEach((k) => {
+ const slot = h("div", "pair-slot" +
+ (WP.building[k] !== null ? " filled" : "") + (WP.building.arm === k ? " arm" : ""));
+ if (WP.building[k] !== null) slot.style.background = SWATCH[WP.building[k]];
+ slot.appendChild(h("span", "cap", k === "day" ? "\u2600 DAY" : "\u263E NIGHT"));
+ slot.onclick = () => { WP.building.arm = WP.building.arm === k ? null : k; paint(); };
+ bench.appendChild(slot);
+ });
+ const side = h("div", "press-side");
+ const lbl = h("input", "wp-lblin");
+ lbl.placeholder = "label (optional)";
+ lbl.value = WP.building.label || "";
+ lbl.oninput = () => { WP.building.label = lbl.value; };
+ side.appendChild(lbl);
+ const mint = h("button", "dupre-key", "MINT PAIR");
+ mint.style.cssText = "height:26px;font-size:8px;padding:0 10px";
+ mint.onclick = () => {
+ mint.blur();
+ if (WP.building.day === null || WP.building.night === null) { DS.toast("fill both frames first", false); return; }
+ WP.pairs.push({ day: WP.building.day, night: WP.building.night, label: WP.building.label || "", ts: WP.mintSeq++ });
+ WP.pairSel = WP.pairs.length - 1;
+ WP.building = { day: null, night: null, label: "", arm: null };
+ DS.toast("Pair minted", true);
+ paint();
+ };
+ side.appendChild(mint);
+ bench.appendChild(side);
+ conf.appendChild(bench);
+ if (WP.building.arm) conf.appendChild(h("div", "wp-note", "click a picture below to fill the " + WP.building.arm.toUpperCase() + " frame"));
+
+ // the picture gallery that feeds the press
+ conf.appendChild(DS.engrave("Pictures"));
+ const g = scrollGallery();
+ SWATCH.forEach((sw, i) => {
+ const t = h("div", "wp-th");
+ t.style.background = sw;
+ t.onclick = () => {
+ if (!WP.building.arm) { DS.toast("click a DAY or NIGHT frame first", false); return; }
+ WP.building[WP.building.arm] = i;
+ WP.building.arm = WP.building.arm === "day" && WP.building.night === null ? "night" : null;
+ paint();
+ };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(h("div", "wp-note", "Only picture types the lock screen can display appear here."));
+ } else if (WP.channel === "random") {
+ const row = h("div", "wp-row2");
+ row.appendChild(h("span", "wp-note", "next draw every"));
+ const wheel = h("div", "int-wheel", WP.interval);
+ wheel.onclick = () => {
+ WP.interval = INTERVALS[(INTERVALS.indexOf(WP.interval) + 1) % INTERVALS.length];
+ DS.toast("Random interval: " + WP.interval + "m", true);
+ paint();
+ };
+ row.appendChild(wheel);
+ row.appendChild(h("span", "wp-note", "minutes"));
+ conf.appendChild(row);
+ } else if (WP.channel === "video") {
+ conf.appendChild(DS.engrave("Videos"));
+ const g = scrollGallery();
+ VIDEOS.forEach((v, i) => {
+ const t = h("div", "wp-th" + (i === WP.video ? " sel" : ""));
+ t.style.background = v;
+ t.appendChild(h("div", "vid-play", "\u25B6"));
+ t.onclick = () => { WP.video = i; DS.toast("Video set", true); paint(); };
+ g.appendChild(t);
+ });
+ conf.appendChild(g);
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox(VIDEOS[WP.video]);
+ pv.appendChild(h("div", "vid-play", "\u25B6"));
+ conf.appendChild(pv);
+ } else if (WP.channel === "watch") {
+ conf.appendChild(DS.engrave("Preview"));
+ conf.appendChild(previewBox(null, "2026-07-22-watch-preview.png"));
+ } else if (WP.channel === "clock") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const cf = h("div", "clock-face"); cf.style.inset = "6% 34%";
+ pv.appendChild(cf);
+ conf.appendChild(pv);
+ } else if (WP.channel === "world") {
+ conf.appendChild(DS.engrave("Preview"));
+ const pv = previewBox("#0c0a08");
+ const m = h("div", "mini-clocks"); m.style.inset = "8%";
+ for (let i = 0; i < 6; i++) m.appendChild(h("i"));
+ pv.appendChild(m);
+ conf.appendChild(pv);
+ }
+ body2.appendChild(conf);
+
+ // sources: only for the modes that draw from directories
+ if (SOURCED.includes(WP.channel)) {
+ const src = h("div", "wp-conf");
+ src.appendChild(DS.engrave("Sources"));
+ const dirs = h("div", "wp-dirs");
+ WP.dirs.forEach((d, i) => {
+ const row = h("div", "wp-dir");
+ row.innerHTML = `<span>\u{f024b}</span>`;
+ row.appendChild(h("span", "path", d));
+ const rm = h("span", "rm", "\u2715");
+ rm.onclick = () => {
+ if (WP.dirs.length === 1) { DS.toast("keep at least one source", false); return; }
+ WP.dirs.splice(i, 1);
+ DS.toast("Source removed", true);
+ paint();
+ };
+ row.appendChild(rm);
+ dirs.appendChild(row);
+ });
+ src.appendChild(dirs);
+ const add = h("button", "dupre-key", "ADD DIRECTORY");
+ add.style.cssText = "height:24px;font-size:8px;padding:0 10px;margin-top:6px";
+ add.onclick = () => {
+ add.blur();
+ const next = DIR_CANDIDATES.find((c) => !WP.dirs.includes(c));
+ if (!next) { DS.toast("no more candidates in the mock", false); return; }
+ WP.dirs.push(next);
+ DS.toast("Source added: " + next, true);
+ paint();
+ };
+ src.appendChild(add);
+ body2.appendChild(src);
+ }
+ };
+ paint();
+ return sv;
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(wallpaperView());
+ subview.classList.add("open");
+}
+
+function scaleSvg(host, w) {
+ const svg = host.querySelector("svg");
+ const [, , vw, vh] = svg.getAttribute("viewBox").split(" ").map(Number);
+ svg.setAttribute("width", w);
+ svg.setAttribute("height", Math.round(w * vh / vw));
+}
+
+
+// current pin set for the whole matrix
+function currentPins() {
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ if (DS.state[key]) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets[key]) pins.push(lbl + ">" + n); });
+ });
+ PWR_ROWS.forEach(([lbl, id]) => {
+ if (DS.state.powerprofile === id) pins.push(lbl + ">NOW");
+ SCENES.forEach((n) => { if (DS.SCENES[n].sets.powerprofile === id) pins.push(lbl + ">" + n); });
+ });
+ return pins;
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- plate 1: the routing matrix ---------------------------------------
+ const plate = h("div", "plate");
+ plate.appendChild(DS.engrave("Programmable presets"));
+ const mxHost = h("div", "khost");
+ const PROG_DEFS = [
+ { id: "FOCUS", label: "FOCUS", glyph: "\u{f0210}", scene: "Focus" }, // crosshairs — focus
+ { id: "PRESENT", label: "PRESENT", glyph: "\u{f0379}", scene: "Presentation" }, // projector screen
+ { id: "BATTERY", label: "BATTERY", glyph: "\u{f007e}", scene: "Battery" }, // half battery
+ { id: "NIGHT", label: "NIGHT", glyph: "\u{f0594}", scene: "Night" }, // crescent moon
+ { id: "S5", label: "1", scene: "Slot5", slim: true },
+ { id: "S6", label: "2", scene: "Slot6", slim: true },
+ { id: "S7", label: "3", scene: "Slot7", slim: true },
+ { id: "S8", label: "4", scene: "Slot8", slim: true },
+ ];
+ const SCENE_OF = Object.fromEntries(PROG_DEFS.map((p) => [p.id, p.scene]));
+ const PROGS = PROG_DEFS.map(({ id, label, glyph, slim }) => ({ id, label, glyph, slim }));
+ const pins = [];
+ SWITCH_ROWS.forEach(([lbl, key]) => {
+ PROG_DEFS.forEach((p) => { if (DS.SCENES[p.scene].sets[key]) pins.push(lbl + ">" + p.id); });
+ });
+ const letters = {};
+ PROG_DEFS.forEach((p) => { letters[PWR_ROW + ">" + p.id] = LETTER_OF[DS.SCENES[p.scene].sets.powerprofile]; });
+ const stayActive = (scene) => { DS.state.scene = scene; render(); };
+ const mx = DUPRE.programMatrix(mxHost, {
+ rows: [...SWITCH_ROWS.map(([l]) => l), { label: PWR_ROW, values: PWR_VALUES }],
+ programs: PROGS,
+ pins,
+ letters,
+ active: DS.state.scene ? PROG_DEFS.findIndex((p) => p.scene === DS.state.scene) : -1,
+ width: 452,
+ x0: 128,
+ dy: 23,
+ engraved: true,
+ onActivate: (j) => {
+ if (booting) return;
+ DS.applyScene(PROG_DEFS[j].scene, mxHost);
+ },
+ onPin: (rowLbl, progId, seated, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ const sets = DS.SCENES[scene].sets;
+ const sw = SWITCH_ROWS.find(([l]) => l === rowLbl);
+ if (!sw) return;
+ sets[sw[1]] = seated;
+ if (isActive && seated !== DS.state[sw[1]]) {
+ DS.setToggle(sw[1], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ onLetter: (rowLbl, progId, v, isActive) => {
+ if (booting) return;
+ const scene = SCENE_OF[progId];
+ DS.SCENES[scene].sets.powerprofile = PWR_OF[v];
+ if (isActive && DS.state.powerprofile !== PWR_OF[v]) {
+ DS.setProfile(PWR_OF[v], mxHost);
+ stayActive(scene);
+ } else DS.toast((PROG_DEFS.find((p) => p.id === progId).label || "slot") + " program updated", true);
+ },
+ });
+ plate.appendChild(mxHost);
+ col.appendChild(plate);
+
+ // ---- plate 2: the bench — three encircled instrument groups -------------
+ const lv = h("div", "plate");
+ const bench = h("div", "bench");
+
+ const fbox = (label) => {
+ const b = h("div", "fbox");
+ b.appendChild(h("span", "flbl", label));
+ return b;
+ };
+
+ // BRIGHTNESS — the drum pair
+ const bright = fbox("Brightness");
+ const dhost = h("div", "khost");
+ const commitDrums = debounce((vals) => {
+ const b = Math.max(DS.SLIDERS.brightness.floor, Math.round(vals[0]) * 10);
+ const k = Math.round(vals[1]) * 10;
+ if (b !== DS.state.brightness) DS.setSlider("brightness", b, bright);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, bright);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "",
+ channels: [
+ { name: "SCREEN", v: Math.max(1, Math.round(DS.state.brightness / 10)) },
+ { name: "KBD", v: Math.round(DS.state.kbd / 10) },
+ ],
+ min: 0, max: 10,
+ height: 210,
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ bright.appendChild(dhost);
+ bench.appendChild(bright);
+
+ const rcol = h("div", "fcol");
+
+ // POINTERS — console keys, horizontal
+ const ptr = fbox("Pointers");
+ [["touchpad", "TOUCHPAD"], ["mouse", "MOUSE"]].forEach(([k, name]) => {
+ const b = h("button", "dupre-key ptr", name);
+ if (DS.state[k]) b.classList.add("dupre-green");
+ b.onclick = () => { b.blur(); DS.setToggle(k, ptr); };
+ ptr.appendChild(b);
+ });
+ rcol.appendChild(ptr);
+
+ // IDLE — the tripper dial (size unchanged)
+ const idle = fbox("Idle");
+ const railHost = h("div", "khost");
+ const rail = DUPRE.tripDial(railHost, {
+ stages: IDLE_STAGES.map((st) => ({ ...st })),
+ max: 60,
+ size: 148,
+ legend: true,
+ onChange: (id, minutes, snapshot) => {
+ snapshot.forEach((st) => { const m = IDLE_STAGES.find((o) => o.id === st.id); if (m) m.minutes = st.minutes; });
+ if (booting || !id) return;
+ const st = IDLE_STAGES.find((o) => o.id === id);
+ DS.toast(st.label + (minutes == null ? " stage disabled" : " → idle " + Math.round(minutes) + "m"), true);
+ },
+ });
+ rail.setBypassed(DS.state.caffeine);
+ idle.appendChild(railHost);
+ rcol.appendChild(idle);
+
+ bench.appendChild(rcol);
+ lv.appendChild(bench);
+ col.appendChild(lv);
+
+ // ---- wallpaper ----------------------------------------------------------
+ const wp = h("div", "wprow");
+ wp.innerHTML = `<span>\u{f021c}</span><span>Wallpaper</span><span class="k-state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+ booting = false;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>