aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html')
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-14.html308
1 files changed, 308 insertions, 0 deletions
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 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 14 (timer bat, centered dial)</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:#e2a038; --gold-hi:#ffbe54;
+ --amber-grad-top:#f2c76a; --amber-grad-mid:#d29638; --amber-grad-bot:#8f671f;
+ --amber-edge:#7d5c16; --amber-warn:#f0b552;
+ --glow-hi:255,190,84; --glow-lo:226,160,56;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace;
+ --pulse-rate:1s;
+ }
+ .plate {
+ background:
+ repeating-linear-gradient(90deg, #ffffff05 0 1px, transparent 1px 3px),
+ linear-gradient(180deg, #1d1a16, #141210);
+ border: 1px solid #262320;
+ border-radius: 10px;
+ padding: 10px;
+ }
+ .row { display: flex; justify-content: center; align-items: center; gap: 10px; flex-wrap: wrap; }
+ .cellcap { color: var(--steel); font-size: 8.5px; letter-spacing: 0.14em; text-align: center; margin-top: 2px; text-transform: uppercase; }
+ .vcell { display: flex; flex-direction: column; align-items: center; }
+ .khost { display: flex; justify-content: center; }
+ .tcell { position: relative; }
+ .bat-oly { position: absolute; top: -8px; right: -10px; }
+ .bat-oly svg { width: 54px; height: 69px; }
+
+ /* top block: presets over the tuner, compact chicken-head upper right */
+ .topgrid { display: flex; gap: 8px; align-items: stretch; }
+ .topleft { flex: 1; display: flex; flex-direction: column; gap: 8px; }
+ .topright { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; }
+ .tuner-well { display: flex; justify-content: center; }
+
+ .presets { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 5px; }
+ .preset {
+ background-image: linear-gradient(180deg, var(--raise), #131110);
+ border: 1px solid #33302b; border-bottom-width: 3px; border-radius: 6px;
+ padding: 4px 1px; text-align: center; cursor: pointer; color: var(--silver);
+ overflow: hidden;
+ }
+ .preset:hover { border-color: var(--gold); color: var(--gold); }
+ .preset .num { font-size: 7.5px; color: var(--dim); display: block; letter-spacing: 0; }
+ .preset .nm { font-size: 8.5px; letter-spacing: 0; display: block; }
+ .preset.cur {
+ border-color: var(--gold); border-bottom-width: 1px; transform: translateY(2px);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12); color: var(--cream);
+ }
+ .preset.cur .num { color: var(--gold); }
+
+ /* levels + caffeine block: drums left, big timer right */
+ .midgrid { display: flex; gap: 8px; align-items: center; justify-content: center; }
+
+ /* switch wall: six cells, airplane's guarded lever among them */
+ .wall { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 6px; justify-items: center; align-items: end; }
+
+ .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 14 — P13 + timer refinements.</b> 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.
+ </div>
+
+ <div class="ds-bar"><span>19:24</span><span class="gear">⚙</span></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 STOPS = ["OFF", "FOCUS", "PRES", "BATT", "NIGHT"];
+const ROCKERS = [
+ ["autodim", "Auto-dim"], ["touchpad", "Touchpad"], ["mouse", "Mouse"],
+ ["nightlight", "Night light"], ["dnd", "DND"],
+];
+let booting = true;
+let caffMinutes = 0; // survives re-renders
+let caffHold = false; // the always-on rocker
+let timerHost = null; // the timerDial is PERSISTENT: it owns a live
+ // countdown interval, so re-renders reparent it
+ // instead of rebuilding (rebuilds leak ghost
+ // intervals and kill an in-progress wind drag)
+
+const sceneIdx = () => (DS.state.scene ? SCENES.indexOf(DS.state.scene) + 1 : 0);
+const debounce = (fn, ms) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function sceneOff() {
+ DS.state.scene = null;
+ DS.toast("Scene: off", true);
+ render();
+}
+
+// caffeine is on when held-on OR wound; keep DS in sync with that truth.
+// Debounced: the sync triggers a panel re-render, which must not happen
+// mid-wind (it would yank the dial out from under the drag).
+function syncCaffeine(el) {
+ const on = caffHold || caffMinutes > 0;
+ if (!booting && on !== DS.state.caffeine) DS.setToggle("caffeine", el);
+}
+const syncCaffeineSoon = debounce(() => syncCaffeine(timerHost), 450);
+
+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));
+}
+
+function render() {
+ booting = true;
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ // ---- top: presets over the tuner; compact chicken-head upper right -----
+ const top = h("div", "plate");
+ const pr = h("div", "presets");
+ SCENES.forEach((n, i) => {
+ const k = h("div", "preset" + (DS.state.scene === n ? " cur" : ""));
+ k.innerHTML = `<span class="num">PRESET ${i + 1}</span><span class="nm">${n}</span>`;
+ k.onclick = () => DS.applyScene(n, k);
+ pr.appendChild(k);
+ });
+ top.appendChild(pr);
+ const grid = h("div", "topgrid");
+ grid.style.marginTop = "8px";
+ const left = h("div", "topleft");
+
+ const tw = h("div", "tuner-well");
+ DUPRE.slideRule(tw, {
+ values: STOPS,
+ value: STOPS[sceneIdx()],
+ skin: "warm",
+ fmt: (v) => v,
+ onChange: (v) => {
+ if (booting) return;
+ if (v === "OFF") sceneOff();
+ else DS.applyScene(SCENES[STOPS.indexOf(v) - 1], tw);
+ },
+ });
+ tw.querySelectorAll(".dupre-mk").forEach((m) =>
+ m.classList.toggle("dupre-on", m.textContent === STOPS[sceneIdx()]));
+ // the kit tuner's fixed width clips the NIGHT stop's label; give it room
+ tw.querySelector(".dupre-tuner").style.width = "198px";
+ left.appendChild(tw);
+ grid.appendChild(left);
+
+ const right = h("div", "topright");
+ const phost = h("div", "khost");
+ const pIdx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ DUPRE.chickenHead(phost, {
+ items: [["PERF", -60], ["BAL", 0], ["SAVER", 60]],
+ index: pIdx,
+ onChange: (i) => { if (!booting && i !== pIdx) DS.setProfile(DS.PROFILES[i].id, right); },
+ });
+ scaleSvg(phost, 78);
+ right.appendChild(phost);
+ right.appendChild(h("div", "cellcap", "power"));
+ grid.appendChild(right);
+ top.appendChild(grid);
+ col.appendChild(top);
+
+ // ---- middle: drum partners left, larger caffeine timer right ------------
+ col.appendChild(DS.engrave("Levels · Caffeine"));
+ const mid = h("div", "midgrid");
+
+ const dcell = h("div", "vcell");
+ 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, dcell);
+ if (k !== DS.state.kbd) DS.setSlider("kbd", k, dcell);
+ }, 350);
+ DUPRE.drumRoller(dhost, {
+ title: "LEVELS ×10",
+ 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: 189, // tall drums; the timer centers against them
+ onChange: (vals) => { if (!booting) commitDrums(vals.slice()); },
+ });
+ dcell.appendChild(dhost);
+ mid.appendChild(dcell);
+
+ const tcell = h("div", "vcell tcell");
+ if (!timerHost) {
+ timerHost = h("div", "khost");
+ DUPRE.timerDial(timerHost, {
+ minutes: caffMinutes,
+ onChange: (min) => {
+ caffMinutes = min;
+ syncCaffeineSoon();
+ },
+ });
+ scaleSvg(timerHost, 178);
+ // the bat toggle takes the winding arrow's corner
+ const svg = timerHost.querySelector("svg");
+ [...svg.querySelectorAll("path")].find((p) => p.getAttribute("d").startsWith("M 122 14"))?.remove();
+ [...svg.querySelectorAll("polygon")].find((p) => p.getAttribute("points").startsWith("136,26"))?.remove();
+ }
+ tcell.appendChild(timerHost);
+ const batHost = h("div", "bat-oly");
+ DUPRE.batToggle(batHost, {
+ on: !caffHold, // up = TIMED, down = HOLD
+ onLabel: "TIMED",
+ offLabel: "HOLD",
+ onChange: (on) => {
+ if (booting) return;
+ caffHold = !on;
+ syncCaffeine(batHost);
+ },
+ });
+ tcell.appendChild(batHost);
+ mid.appendChild(tcell);
+ col.appendChild(mid);
+
+ // ---- switch wall: five rockers + the guarded airplane lever -------------
+ col.appendChild(DS.engrave("Switches"));
+ const wall = h("div", "wall");
+ ROCKERS.forEach(([k, name]) => {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.rocker(host, {
+ on: DS.state[k],
+ onChange: (on) => { if (!booting && on !== DS.state[k]) DS.setToggle(k, cell); },
+ });
+ cell.appendChild(host);
+ cell.appendChild(h("div", "cellcap", name));
+ wall.appendChild(cell);
+ });
+ if (DS.state.laptop) {
+ const cell = h("div", "vcell");
+ const host = h("div", "khost");
+ DUPRE.guardedToggle(host, {
+ on: DS.state.airplane,
+ onLabel: "AIRPLANE",
+ offLabel: "RADIO OK",
+ onChange: (on) => { if (!booting && on !== DS.state.airplane) DS.setToggle("airplane", cell); },
+ });
+ scaleSvg(host, 72);
+ cell.appendChild(host);
+ wall.appendChild(cell);
+ }
+ col.appendChild(wall);
+
+ // ---- wallpaper: where it was -------------------------------------------
+ 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>