aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-19 23:45:24 -0500
committerCraig Jennings <c@cjennings.net>2026-07-19 23:45:24 -0500
commit0468fedb23ae34a140b6a5a903adedb4e9873ca5 (patch)
tree188151ac2e0e9a27b205cd523c7872028795e5ca /docs/prototypes
parentc4b3710b14a47d4c8297322297f217be264349ea (diff)
downloadarchsetup-0468fedb23ae34a140b6a5a903adedb4e9873ca5.tar.gz
archsetup-0468fedb23ae34a140b6a5a903adedb4e9873ca5.zip
docs: expand desktop-settings spec and add panel prototypes
I folded the home-handoff control-set decisions into the desktop-settings panel spec. The controls table gains night-light, Do Not Disturb, power profile, lock/suspend actions, scenes, and a wallpaper sub-view. Volume and theme are out of scope, with reasons, and the format pickers move to their own display-format sibling spec (stubbed). The scenes and wallpaper-shape decisions close. The spec stays DRAFT pending UI prototyping. I added five direction prototypes over a shared engine (tile grid, sectioned rack, scenes-first, instrument cluster, expandable rows) so the layout gets picked against working behavior. I closed the shipped Next Session Focus work in todo.org: notification loudness, clock right-click dismiss, date-only scrolling, the wired-interface indicator, and best-saved WiFi connect, plus velox boot recovery. The three design-blocked network and audio items stay TODO with their open question recorded.
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html88
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html99
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html130
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html166
-rw-r--r--docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html149
-rw-r--r--docs/prototypes/desktop-settings-shared.css358
-rw-r--r--docs/prototypes/desktop-settings-shared.js261
7 files changed, 1251 insertions, 0 deletions
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
new file mode 100644
index 0000000..e57d13e
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-1.html
@@ -0,0 +1,88 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 1 (tile grid)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 1 — Tile grid.</b> The quick-settings idiom (Android / GNOME 43):
+ scenes on top, a 2-column tile grid for toggles, sliders and power below.
+ Fast to scan, one tap per control, the wallpaper tile opens a sub-view.
+ </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="desktop-settings-shared.js"></script>
+<script>
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = DS.h("div", "ds-col");
+
+ // Scenes strip
+ col.appendChild(DS.engrave("Scenes"));
+ const scenes = DS.h("div", "ds-grid4");
+ Object.keys(DS.SCENES).forEach((n) => scenes.appendChild(DS.sceneKey(n)));
+ col.appendChild(scenes);
+
+ // Toggle tiles
+ col.appendChild(DS.engrave("Toggles"));
+ const grid = DS.h("div", "ds-grid2");
+ ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((k) =>
+ grid.appendChild(DS.toggleTile(k)));
+ if (DS.state.laptop) grid.appendChild(DS.toggleTile("airplane"));
+ col.appendChild(grid);
+
+ // Sliders
+ col.appendChild(DS.engrave("Levels"));
+ const levels = DS.h("div", "ds-well");
+ levels.appendChild(DS.slider("brightness"));
+ levels.appendChild(DS.slider("kbd"));
+ col.appendChild(levels);
+
+ // Power profile
+ col.appendChild(DS.engrave("Power"));
+ col.appendChild(DS.profileSeg());
+
+ // Actions + wallpaper entry
+ col.appendChild(DS.engrave("Actions"));
+ const acts = DS.h("div", "ds-grid3");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ const wp = DS.actionBtn("Wallpaper", "\u{f021c}", false, openWallpaper);
+ acts.appendChild(wp);
+ col.appendChild(acts);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
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
new file mode 100644
index 0000000..af2160c
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-2.html
@@ -0,0 +1,99 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 2 (sectioned rack)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 2 — Sectioned rack.</b> Full-width console keys stacked under
+ engraved section labels, closest to the net/bt panel idiom. Densest and most
+ scannable; every control is one row you read top to bottom.
+ </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="desktop-settings-shared.js"></script>
+<script>
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function section(label, nodes) {
+ const wrap = DS.h("div", "ds-col");
+ wrap.appendChild(DS.engrave(label));
+ nodes.forEach((n) => wrap.appendChild(n));
+ return wrap;
+}
+
+// A wallpaper entry row styled like a console key that opens the sub-view.
+function wallpaperRow() {
+ const el = DS.h("div", "ds-key");
+ el.innerHTML =
+ `<span class="ds-lamp off"></span><span class="k-glyph">\u{f021c}</span>` +
+ `<span class="k-name">Wallpaper</span><span class="k-state">manage ›</span>`;
+ el.onclick = openWallpaper;
+ return el;
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = DS.h("div", "ds-col");
+
+ col.appendChild(section("Display", [
+ DS.toggleKey("autodim"),
+ DS.toggleKey("nightlight"),
+ (() => { const w = DS.h("div", "ds-well"); w.appendChild(DS.slider("brightness")); w.appendChild(DS.slider("kbd")); return w; })(),
+ ]));
+
+ col.appendChild(section("Input", [
+ DS.toggleKey("touchpad"),
+ DS.toggleKey("mouse"),
+ ]));
+
+ const powerNodes = [];
+ if (DS.state.laptop) powerNodes.push(DS.toggleKey("airplane"));
+ powerNodes.push(DS.profileSeg());
+ const acts = DS.h("div", "ds-grid2");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ powerNodes.push(acts);
+ col.appendChild(section("Power", powerNodes));
+
+ col.appendChild(section("Notifications", [DS.toggleKey("dnd")]));
+
+ const scenes = DS.h("div", "ds-grid4");
+ Object.keys(DS.SCENES).forEach((n) => scenes.appendChild(DS.sceneKey(n)));
+ col.appendChild(section("Scenes", [scenes]));
+
+ col.appendChild(section("Wallpaper", [wallpaperRow()]));
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
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
new file mode 100644
index 0000000..8c383f1
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-3.html
@@ -0,0 +1,130 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 3 (scenes-first)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Scenes-first: the hero scene grid + the active-scene readout. */
+ .ds-scene.hero { min-height: 74px; padding: 12px 8px; }
+ .ds-scene.hero .sc-glyph { font-size: 24px; }
+ .ds-scene.hero .sc-name { font-size: 12.5px; }
+ .scene-readout {
+ background: var(--well); border: 1px solid var(--line); border-radius: 10px;
+ padding: 8px 11px; font-size: 11.5px; color: var(--steel); min-height: 20px;
+ }
+ .scene-readout b { color: var(--gold); font-weight: 700; }
+ .scene-readout .sets { color: var(--silver); }
+ .manual-head { display: flex; align-items: center; gap: 8px; }
+ .manual-head .hint { color: var(--dim); font-size: 10px; margin-left: auto; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 3 — Scenes-first.</b> The scene selector is the hero: one tap sets
+ the whole desktop (the iOS-Focus idea). Individual controls sit below as the
+ manual-override layer, and touching one drops you out of the active scene.
+ </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="desktop-settings-shared.js"></script>
+<script>
+const body = document.getElementById("body");
+const subview = document.getElementById("subview");
+DS.bindToast(document.getElementById("toast"));
+
+const NAMEOF = {
+ autodim: "auto-dim", caffeine: "caffeine", touchpad: "touchpad", mouse: "mouse",
+ airplane: "airplane", nightlight: "night light", dnd: "DND",
+};
+function sceneSummary(name) {
+ const sets = DS.SCENES[name].sets;
+ const parts = Object.entries(sets).map(([k, v]) => {
+ if (k === "powerprofile") return v;
+ return (v ? "" : "no ") + (NAMEOF[k] || k);
+ });
+ return parts.join(", ");
+}
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = DS.h("div", "ds-col");
+
+ // Hero scenes
+ col.appendChild(DS.engrave("Scene"));
+ const grid = DS.h("div", "ds-grid2");
+ Object.keys(DS.SCENES).forEach((n) => {
+ const k = DS.sceneKey(n);
+ k.classList.add("hero");
+ k.addEventListener("mouseenter", () => setReadout(n));
+ grid.appendChild(k);
+ });
+ col.appendChild(grid);
+
+ const readout = DS.h("div", "scene-readout");
+ readout.id = "readout";
+ col.appendChild(readout);
+
+ // Manual override layer
+ const mh = DS.h("div", "manual-head");
+ mh.appendChild(DS.engrave("Manual"));
+ const hint = DS.h("span", "hint", "overrides the scene");
+ mh.appendChild(hint);
+ col.appendChild(mh);
+
+ const grid2 = DS.h("div", "ds-grid2");
+ ["autodim", "caffeine", "touchpad", "mouse", "nightlight", "dnd"].forEach((key) =>
+ grid2.appendChild(DS.toggleTile(key)));
+ if (DS.state.laptop) grid2.appendChild(DS.toggleTile("airplane"));
+ col.appendChild(grid2);
+
+ const levels = DS.h("div", "ds-well");
+ levels.appendChild(DS.slider("brightness"));
+ levels.appendChild(DS.slider("kbd"));
+ col.appendChild(levels);
+
+ col.appendChild(DS.profileSeg());
+
+ const acts = DS.h("div", "ds-grid3");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ acts.appendChild(DS.actionBtn("Wallpaper", "\u{f021c}", false, openWallpaper));
+ col.appendChild(acts);
+
+ body.appendChild(col);
+ setReadout(DS.state.scene);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+function setReadout(name) {
+ const r = document.getElementById("readout");
+ if (!r) return;
+ if (!name) { r.innerHTML = `<span>No scene active — controls are manual.</span>`; return; }
+ r.innerHTML = `<b>${name}</b> — <span class="sets">${sceneSummary(name)}</span>`;
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
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
new file mode 100644
index 0000000..23d72fb
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-4.html
@@ -0,0 +1,166 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 4 (instrument cluster)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Instrument cluster: ring gauges for the levels, a rotary deck for power. */
+ .gauge-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
+ .gauge {
+ background: var(--well); border: 1px solid var(--line); border-radius: 12px;
+ padding: 10px; display: flex; flex-direction: column; align-items: center; gap: 6px;
+ }
+ .g-ring {
+ width: 92px; height: 92px; border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ position: relative;
+ }
+ .g-ring::before {
+ content: ""; position: absolute; inset: 9px; border-radius: 50%;
+ background: var(--ground); border: 1px solid var(--plate-edge);
+ }
+ .g-val { position: relative; z-index: 1; color: var(--cream); font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; }
+ .g-val .u { color: var(--dim); font-size: 11px; }
+ .g-name { color: var(--steel); font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; }
+ .g-fader { width: 100%; }
+ .g-fader input[type=range] {
+ -webkit-appearance:none; appearance:none; width: 100%; height: 5px; border-radius: 5px;
+ background: var(--ground); border: 1px solid var(--line); outline: none;
+ }
+ .g-fader input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance:none; width: 12px; height: 12px; border-radius: 50%;
+ background: var(--cream); border: 1px solid var(--gold); cursor: pointer;
+ }
+ .rotary { display: flex; align-items: center; gap: 10px; background: var(--well); border: 1px solid var(--line); border-radius: 12px; padding: 8px 12px; }
+ .rotary .dial { width: 40px; height: 40px; border-radius: 50%; border: 2px solid var(--key-edge); position: relative; background: linear-gradient(180deg,#23211e,#191715); }
+ .rotary .dial .tick { position: absolute; top: 3px; left: 50%; width: 2px; height: 12px; background: var(--gold); transform-origin: 50% 17px; }
+ .rotary .opts { display: flex; gap: 6px; flex: 1; }
+ .rotary .opt { flex: 1; text-align: center; font-size: 11px; color: var(--dim); cursor: pointer; padding: 4px 2px; border-radius: 6px; }
+ .rotary .opt.cur { color: var(--gold); font-weight: 700; }
+ .rotary .opt:hover { color: var(--silver); }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 4 — Instrument cluster.</b> The most literal control panel:
+ brightness and keyboard read as needle-ring gauges, power profile is a rotary
+ selector, toggles are lamp rows. Leans hardest into the faceplate aesthetic.
+ </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="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"));
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function gauge(key) {
+ const meta = DS.SLIDERS[key];
+ const v = DS.state[key];
+ const g = h("div", "gauge");
+ const ring = h("div", "g-ring");
+ ring.style.background = `conic-gradient(var(--gold) ${v * 3.6}deg, #1a1917 0)`;
+ ring.innerHTML = `<span class="g-val">${v}<span class="u">%</span></span>`;
+ g.appendChild(ring);
+ g.appendChild(h("div", "g-name", meta.name));
+ const fw = h("div", "g-fader");
+ const inp = h("input"); inp.type = "range"; inp.min = meta.floor; inp.max = 100; inp.value = v;
+ inp.oninput = () => { ring.style.background = `conic-gradient(var(--gold) ${inp.value*3.6}deg, #1a1917 0)`; ring.querySelector(".g-val").innerHTML = `${inp.value}<span class="u">%</span>`; };
+ inp.onchange = () => DS.setSlider(key, +inp.value, g);
+ fw.appendChild(inp); g.appendChild(fw);
+ return g;
+}
+
+function rotary() {
+ const r = h("div", "rotary");
+ const idx = DS.PROFILES.findIndex((p) => p.id === DS.state.powerprofile);
+ const dial = h("div", "dial");
+ const angle = -50 + idx * 50; // three positions
+ dial.innerHTML = `<span class="tick" style="transform:translateX(-50%) rotate(${angle}deg)"></span>`;
+ r.appendChild(dial);
+ const opts = h("div", "opts");
+ DS.PROFILES.forEach((p) => {
+ const o = h("div", "opt" + (p.id === DS.state.powerprofile ? " cur" : ""), p.label);
+ o.onclick = () => DS.setProfile(p.id, r);
+ opts.appendChild(o);
+ });
+ r.appendChild(opts);
+ return r;
+}
+
+function lampRow(key) {
+ const meta = DS.TOGGLES[key];
+ const on = DS.state[key];
+ const el = h("div", "ds-key" + (on ? " on" : ""));
+ el.innerHTML =
+ `<span class="ds-lamp ${on ? "gold" : "off"}"></span>` +
+ `<span class="k-glyph">${meta.glyph}</span>` +
+ `<span class="k-name">${meta.name}</span>` +
+ `<span class="k-state">${on ? meta.on : meta.off}</span>`;
+ el.onclick = () => DS.setToggle(key, el);
+ return el;
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = h("div", "ds-col");
+
+ col.appendChild(DS.engrave("Levels"));
+ const gr = h("div", "gauge-row");
+ gr.appendChild(gauge("brightness"));
+ gr.appendChild(gauge("kbd"));
+ col.appendChild(gr);
+
+ col.appendChild(DS.engrave("Power profile"));
+ col.appendChild(rotary());
+
+ col.appendChild(DS.engrave("Toggles"));
+ const well = h("div", "ds-well ds-col");
+ ["autodim", "nightlight", "touchpad", "mouse", "dnd"].forEach((k) => well.appendChild(lampRow(k)));
+ if (DS.state.laptop) well.appendChild(lampRow("airplane"));
+ well.appendChild(lampRow("caffeine"));
+ col.appendChild(well);
+
+ col.appendChild(DS.engrave("Scenes"));
+ const scenes = h("div", "ds-grid4");
+ Object.keys(DS.SCENES).forEach((n) => scenes.appendChild(DS.sceneKey(n)));
+ col.appendChild(scenes);
+
+ col.appendChild(DS.engrave("Actions"));
+ const acts = h("div", "ds-grid3");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ acts.appendChild(DS.actionBtn("Wallpaper", "\u{f021c}", false, openWallpaper));
+ col.appendChild(acts);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
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
new file mode 100644
index 0000000..662e9f6
--- /dev/null
+++ b/docs/prototypes/2026-07-02-desktop-settings-panel-prototype-5.html
@@ -0,0 +1,149 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Desktop Settings — Prototype 5 (compact expandable rows)</title>
+<link rel="stylesheet" href="desktop-settings-shared.css">
+<style>
+ /* Compact rows: one dense column; rows with depth expand inline. */
+ .crow {
+ display: flex; align-items: center; gap: 10px;
+ padding: 8px 8px; border-radius: 8px; cursor: pointer;
+ border: 1px solid transparent;
+ }
+ .crow:hover { background: #16191b; }
+ .crow .glyph { color: var(--steel); font-size: 15px; width: 18px; text-align: center; }
+ .crow.on .glyph { color: var(--gold); }
+ .crow .name { color: var(--silver); font-size: 13px; }
+ .crow.on .name { color: var(--cream); }
+ .crow .state { margin-left: auto; color: var(--dim); font-size: 11px; }
+ .crow.on .state { color: var(--gold); }
+ .crow .chev { color: var(--dim); font-size: 12px; margin-left: auto; transition: transform .15s; }
+ .crow.exp .chev { transform: rotate(90deg); color: var(--gold); }
+ .crow.verified { animation: dsVerify 0.9s ease-out; }
+ .reveal { overflow: hidden; max-height: 0; transition: max-height .2s ease; }
+ .reveal.open { max-height: 260px; }
+ .reveal-inner { padding: 6px 6px 10px 36px; }
+ .sep { height: 1px; background: var(--line); margin: 3px 6px; }
+ .inline-slider { padding: 4px 8px 4px 0; }
+</style>
+</head>
+<body class="ds-stage">
+ <div class="ds-caption">
+ <b>Direction 5 — Compact expandable rows.</b> Minimal by default: one slim row
+ per control in a single column. Rows with depth (power, scenes, wallpaper)
+ expand inline — progressive disclosure, the long-press pattern made visible.
+ </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="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"));
+
+function openWallpaper() {
+ subview.innerHTML = "";
+ subview.appendChild(DS.wallpaperSubview(() => subview.classList.remove("open")));
+ subview.classList.add("open");
+}
+
+function toggleRow(key) {
+ const meta = DS.TOGGLES[key];
+ const on = DS.state[key];
+ const el = h("div", "crow" + (on ? " on" : ""));
+ el.innerHTML =
+ `<span class="ds-lamp ${on ? "gold" : "off"}"></span>` +
+ `<span class="glyph">${meta.glyph}</span>` +
+ `<span class="name">${meta.name}</span>` +
+ `<span class="state">${on ? meta.on : meta.off}</span>`;
+ el.onclick = () => DS.setToggle(key, el);
+ return el;
+}
+
+function sliderRow(key) {
+ const meta = DS.SLIDERS[key];
+ const el = h("div", "crow inline-slider");
+ el.style.cursor = "default";
+ el.appendChild(Object.assign(h("span", "glyph"), { textContent: meta.glyph }));
+ el.appendChild(Object.assign(h("span", "name"), { textContent: meta.name }));
+ const s = DS.slider(key);
+ s.style.flex = "1";
+ // strip the redundant leading glyph/name from the shared slider
+ s.removeChild(s.children[0]); s.removeChild(s.children[0]);
+ el.appendChild(s);
+ return el;
+}
+
+// A row that expands an inline panel (built by contentFn) below it.
+function expandRow(glyph, name, stateText, contentFn) {
+ const wrap = h("div");
+ const row = h("div", "crow");
+ row.innerHTML =
+ `<span class="ds-lamp off"></span><span class="glyph">${glyph}</span>` +
+ `<span class="name">${name}</span><span class="state">${stateText || ""}</span><span class="chev">›</span>`;
+ const rev = h("div", "reveal");
+ const inner = h("div", "reveal-inner");
+ inner.appendChild(contentFn());
+ rev.appendChild(inner);
+ row.onclick = () => { row.classList.toggle("exp"); rev.classList.toggle("open"); };
+ wrap.appendChild(row); wrap.appendChild(rev);
+ return wrap;
+}
+
+function render() {
+ body.innerHTML = "";
+ const col = h("div");
+
+ ["autodim", "nightlight"].forEach((k) => col.appendChild(toggleRow(k)));
+ col.appendChild(sliderRow("brightness"));
+ col.appendChild(sliderRow("kbd"));
+ col.appendChild(h("div", "sep"));
+
+ ["touchpad", "mouse"].forEach((k) => col.appendChild(toggleRow(k)));
+ if (DS.state.laptop) col.appendChild(toggleRow("airplane"));
+ col.appendChild(toggleRow("dnd"));
+ col.appendChild(h("div", "sep"));
+
+ col.appendChild(expandRow("\u{f0241}", "Power profile", DS.state.powerprofile, () => DS.profileSeg()));
+ col.appendChild(expandRow("\u{f0210}", "Scenes", DS.state.scene || "none", () => {
+ const g = h("div", "ds-grid4");
+ Object.keys(DS.SCENES).forEach((n) => g.appendChild(DS.sceneKey(n)));
+ return g;
+ }));
+
+ const wp = h("div", "crow");
+ wp.innerHTML = `<span class="ds-lamp off"></span><span class="glyph">\u{f021c}</span><span class="name">Wallpaper</span><span class="state">manage ›</span>`;
+ wp.onclick = openWallpaper;
+ col.appendChild(wp);
+ col.appendChild(h("div", "sep"));
+
+ const acts = h("div", "ds-grid2");
+ acts.appendChild(DS.actionBtn("Lock", "\u{f033e}", false));
+ acts.appendChild(DS.actionBtn("Suspend", "\u{f04b2}", false));
+ col.appendChild(acts);
+
+ body.appendChild(col);
+ document.getElementById("scene-sub").textContent = DS.state.scene ? "▶ " + DS.state.scene : "";
+}
+
+DS.onChange(render);
+render();
+</script>
+</body>
+</html>
diff --git a/docs/prototypes/desktop-settings-shared.css b/docs/prototypes/desktop-settings-shared.css
new file mode 100644
index 0000000..95aec22
--- /dev/null
+++ b/docs/prototypes/desktop-settings-shared.css
@@ -0,0 +1,358 @@
+/* desktop-settings-shared.css — the shared engine styling for every
+ * desktop-settings panel prototype. Translates the dupre instrument-console
+ * palette (themes/dupre/panel.css) into browser-friendly component classes so
+ * all five direction prototypes read as one system and port near-1:1 to svg.el.
+ *
+ * Palette: warm near-black ground, gold border/accent, slate blue selection,
+ * silver/cream text, olive pass, terracotta fail. */
+
+:root {
+ --ground: #100f0f;
+ --well: #0a0c0d;
+ --well2: #101214;
+ --plate: #1a1917;
+ --plate-edge: #262320;
+ --gold: #dab53d;
+ --gold-bright: #f0d879;
+ --slate: #424f5e;
+ --slate-hi: #54677d;
+ --silver: #bfc4d0;
+ --cream: #f3e7c5;
+ --steel: #969385;
+ --dim: #7c838a;
+ --line: #2c2f32;
+ --pass: #74932f;
+ --fail: #cb6b4d;
+ --key-top: #23211e;
+ --key-bot: #191715;
+ --key-edge: #33302b;
+ --mono: "BerkeleyMono Nerd Font", "JetBrains Mono", "DejaVu Sans Mono", ui-monospace, monospace;
+}
+
+* { box-sizing: border-box; }
+
+body.ds-stage {
+ margin: 0;
+ min-height: 100vh;
+ background:
+ radial-gradient(1200px 600px at 80% -10%, #17151322, transparent),
+ #050505;
+ color: var(--silver);
+ font-family: var(--mono);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 14px;
+ padding: 28px 16px 48px;
+}
+
+/* A caption above each prototype naming the direction (prototype chrome only,
+ not part of the panel). */
+.ds-caption {
+ color: var(--dim);
+ font-size: 12px;
+ letter-spacing: 0.04em;
+ text-align: center;
+ max-width: 360px;
+}
+.ds-caption b { color: var(--gold); font-weight: 700; }
+
+/* The bar stub the panel drops from, so the prototype shows the anchor. */
+.ds-bar {
+ width: 360px;
+ height: 30px;
+ border-radius: 8px;
+ background: linear-gradient(180deg, #17151388, #0d0c0b);
+ border: 1px solid #201d17;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 10px;
+ padding: 0 12px;
+ color: var(--steel);
+ font-size: 13px;
+}
+.ds-bar .gear { color: var(--gold); }
+
+/* ---- the panel capsule --------------------------------------------------- */
+.ds-panel {
+ width: 360px;
+ background-color: var(--ground);
+ border: 1.5px solid var(--gold);
+ border-radius: 16px;
+ padding: 14px 16px 16px;
+ box-shadow: 0 18px 50px #000a, 0 0 0 1px #0006;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ position: relative;
+ overflow: hidden;
+}
+
+/* Faceplate header: raised ID plate with title, gear glyph, close. */
+.ds-face {
+ background-color: var(--plate);
+ border: 1px solid var(--plate-edge);
+ border-radius: 12px;
+ padding: 9px 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.ds-face .glyph { color: var(--gold); font-size: 18px; }
+.ds-face .title { color: var(--gold); font-weight: 700; font-size: 16px; letter-spacing: 0.02em; }
+.ds-face .sub { color: var(--steel); font-size: 11px; margin-left: auto; }
+.ds-face .x {
+ color: var(--dim); cursor: pointer; font-size: 15px; padding: 2px 5px;
+ border-radius: 50%; line-height: 1;
+}
+.ds-face .x:hover { color: var(--silver); background: var(--line); }
+
+/* Engraved section label: steel small-caps + a thin rule. */
+.ds-engrave {
+ color: var(--steel);
+ font-size: 10px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 4px 2px 2px;
+}
+.ds-engrave::after {
+ content: "";
+ flex: 1;
+ height: 1px;
+ background: linear-gradient(90deg, var(--line), transparent);
+}
+
+/* Status lamp: a colored dot with a soft glow; off is a dim wash. */
+.ds-lamp {
+ width: 9px; height: 9px; border-radius: 50%;
+ background-color: var(--pass);
+ box-shadow: 0 0 6px 1px rgba(116,147,47,.55);
+ flex: none;
+}
+.ds-lamp.gold { background-color: var(--gold); box-shadow: 0 0 6px 1px rgba(218,181,61,.55); }
+.ds-lamp.red { background-color: var(--fail); box-shadow: 0 0 6px 1px rgba(203,107,77,.55); }
+.ds-lamp.off { background-color: var(--line); box-shadow: none; }
+
+/* Console key: a raised mechanical button that lights gold on hover/active. */
+.ds-key {
+ color: var(--silver);
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ background-color: var(--key-bot);
+ border: 1px solid var(--key-edge);
+ border-radius: 8px;
+ padding: 8px 10px;
+ font-family: var(--mono);
+ font-size: 13px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ transition: border-color .12s, color .12s, background-color .12s;
+}
+.ds-key:hover { color: var(--gold); border-color: var(--gold); }
+.ds-key .k-glyph { font-size: 15px; color: var(--steel); }
+.ds-key:hover .k-glyph { color: var(--gold); }
+.ds-key .k-name { color: var(--silver); }
+.ds-key .k-state { margin-left: auto; color: var(--dim); font-size: 11px; }
+
+/* Console key in the ON state: gold cast + lit lamp glyph. */
+.ds-key.on {
+ border-color: var(--gold);
+ background-image: linear-gradient(180deg, #2a2618, #1d1a12);
+}
+.ds-key.on .k-glyph { color: var(--gold); }
+.ds-key.on .k-name { color: var(--cream); }
+.ds-key.on .k-state { color: var(--gold); }
+
+/* A brief verify flash: after a change, the control confirms the readback. */
+.ds-key.verified { animation: dsVerify 0.9s ease-out; }
+@keyframes dsVerify {
+ 0% { box-shadow: 0 0 0 0 rgba(116,147,47,.0); }
+ 20% { box-shadow: 0 0 0 2px rgba(116,147,47,.55); }
+ 100% { box-shadow: 0 0 0 0 rgba(116,147,47,0); }
+}
+
+/* Tile (grid layout): a squarer console key with the lamp top-left. */
+.ds-tile {
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ border-radius: 10px;
+ padding: 10px 11px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ min-height: 62px;
+ transition: border-color .12s, background-color .12s;
+}
+.ds-tile:hover { border-color: var(--gold); }
+.ds-tile .t-top { display: flex; align-items: center; gap: 8px; }
+.ds-tile .t-glyph { font-size: 17px; color: var(--steel); }
+.ds-tile .t-name { color: var(--silver); font-size: 12.5px; }
+.ds-tile .t-state { color: var(--dim); font-size: 11px; margin-top: auto; }
+.ds-tile.on { border-color: var(--gold); background-image: linear-gradient(180deg, #2a2618, #1d1a12); }
+.ds-tile.on .t-glyph { color: var(--gold); }
+.ds-tile.on .t-name { color: var(--cream); }
+.ds-tile.on .t-state { color: var(--gold); }
+.ds-tile.verified { animation: dsVerify 0.9s ease-out; }
+.ds-tile .t-detail {
+ margin-left: auto; color: var(--dim); font-size: 11px; padding: 0 3px;
+}
+.ds-tile .t-detail:hover { color: var(--gold); }
+
+/* Slider row: engraved name + machined fader + % readout. */
+.ds-slider {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 6px 4px;
+}
+.ds-slider .s-glyph { color: var(--steel); font-size: 15px; width: 18px; text-align: center; }
+.ds-slider .s-name { color: var(--silver); font-size: 12.5px; width: 92px; }
+.ds-slider input[type=range] {
+ -webkit-appearance: none; appearance: none;
+ flex: 1;
+ height: 6px;
+ border-radius: 6px;
+ background: var(--well);
+ border: 1px solid var(--line);
+ outline: none;
+}
+.ds-slider input[type=range]::-webkit-slider-thumb {
+ -webkit-appearance: none; appearance: none;
+ width: 14px; height: 14px; border-radius: 50%;
+ 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 */
+}
+.ds-slider input[type=range]::-moz-range-thumb {
+ width: 14px; height: 14px; border-radius: 50%;
+ background: var(--cream); border: 1px solid var(--gold); cursor: pointer;
+}
+.ds-slider input[type=range]::-moz-range-progress { background: var(--gold); height: 6px; border-radius: 6px; }
+.ds-slider .s-pct { color: var(--cream); font-size: 12px; width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
+
+/* Segmented selector (power profile): joined keys, the current one lit gold. */
+.ds-seg { display: flex; border-radius: 8px; overflow: hidden; border: 1px solid var(--key-edge); }
+.ds-seg button {
+ flex: 1;
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ color: var(--silver);
+ border: none;
+ border-right: 1px solid var(--key-edge);
+ padding: 7px 6px;
+ font-family: var(--mono);
+ font-size: 11.5px;
+ cursor: pointer;
+}
+.ds-seg button:last-child { border-right: none; }
+.ds-seg button:hover { color: var(--gold); }
+.ds-seg button.cur {
+ background-image: linear-gradient(180deg, var(--gold-bright), var(--gold));
+ color: var(--ground);
+ font-weight: 700;
+}
+
+/* Scene key: a wider console key with the scene name; active is gold-lit. */
+.ds-scene {
+ background-image: linear-gradient(180deg, var(--key-top), var(--key-bot));
+ border: 1px solid var(--key-edge);
+ border-radius: 9px;
+ padding: 9px 8px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 3px;
+ transition: border-color .12s, background-color .12s;
+}
+.ds-scene:hover { border-color: var(--gold); }
+.ds-scene .sc-glyph { font-size: 18px; color: var(--steel); }
+.ds-scene .sc-name { font-size: 11px; color: var(--silver); letter-spacing: 0.03em; }
+.ds-scene.cur { border-color: var(--gold); background-image: linear-gradient(180deg, #2a2618, #1d1a12); }
+.ds-scene.cur .sc-glyph { color: var(--gold); }
+.ds-scene.cur .sc-name { color: var(--cream); }
+
+/* Momentary action buttons (lock / suspend). */
+.ds-act {
+ background: transparent;
+ border: 1px solid var(--slate);
+ color: var(--silver);
+ border-radius: 8px;
+ padding: 8px 10px;
+ font-family: var(--mono);
+ font-size: 12.5px;
+ cursor: pointer;
+ display: flex; align-items: center; justify-content: center; gap: 7px;
+}
+.ds-act:hover { border-color: var(--gold); color: var(--gold); background: #dab53d12; }
+.ds-act.danger { border-color: var(--fail); color: var(--fail); }
+.ds-act.danger:hover { background: var(--fail); color: var(--ground); }
+
+/* Generic grid + row helpers. */
+.ds-grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+.ds-grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; }
+.ds-grid4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 8px; }
+.ds-col { display: flex; flex-direction: column; gap: 6px; }
+.ds-well { background: var(--well); border: 1px solid var(--line); border-radius: 10px; padding: 8px; }
+
+/* Toast: a slate feedback strip that names what was applied + verified. */
+.ds-toast {
+ color: var(--cream);
+ background-color: var(--slate);
+ border-radius: 8px;
+ padding: 6px 10px;
+ font-size: 12px;
+ min-height: 18px;
+ transition: opacity .3s;
+}
+.ds-toast .ok { color: #cfe08c; }
+.ds-toast.empty { background: transparent; color: var(--dim); }
+
+/* Wallpaper sub-view overlay: slides over the panel body, Back returns. */
+.ds-subview {
+ position: absolute;
+ inset: 0;
+ background: var(--ground);
+ border-radius: 16px;
+ padding: 14px 16px 16px;
+ display: none;
+ flex-direction: column;
+ gap: 10px;
+ z-index: 5;
+}
+.ds-subview.open { display: flex; }
+.ds-sub-head { display: flex; align-items: center; gap: 10px; }
+.ds-sub-head .back { color: var(--dim); cursor: pointer; font-size: 13px; }
+.ds-sub-head .back:hover { color: var(--gold); }
+.ds-sub-head .title { color: var(--gold); font-weight: 700; font-size: 14px; }
+.ds-thumbs { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; }
+.ds-thumb {
+ aspect-ratio: 16/10;
+ border-radius: 8px;
+ border: 1.5px solid var(--line);
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+}
+.ds-thumb.cur { border-color: var(--gold); }
+.ds-thumb .tag {
+ position: absolute; bottom: 3px; left: 4px;
+ font-size: 9px; color: #fff; text-shadow: 0 1px 2px #000;
+}
+.ds-daynight { display: flex; gap: 8px; }
+.ds-daynight .dn {
+ flex: 1; border-radius: 8px; border: 1px solid var(--line);
+ padding: 8px; display: flex; flex-direction: column; gap: 4px; align-items: center;
+}
+.ds-daynight .dn .lbl { font-size: 10px; color: var(--steel); text-transform: uppercase; letter-spacing: 0.08em; }
+.ds-daynight .dn .sw { width: 100%; aspect-ratio: 16/9; border-radius: 6px; }
+.ds-dirs { display: flex; flex-direction: column; gap: 4px; }
+.ds-dir { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--silver); padding: 3px 4px; }
+.ds-dir .path { color: var(--dim); }
+.ds-note { color: var(--dim); font-size: 11px; }
diff --git a/docs/prototypes/desktop-settings-shared.js b/docs/prototypes/desktop-settings-shared.js
new file mode 100644
index 0000000..39bc76a
--- /dev/null
+++ b/docs/prototypes/desktop-settings-shared.js
@@ -0,0 +1,261 @@
+/* desktop-settings-shared.js — the shared engine behind every direction
+ * prototype. One mock state model, one widget factory, and the net panel's
+ * verify-everything contract simulated: a control applies, then a mock backing
+ * "reads back" the real value and confirms (a green verify flash + a toast).
+ *
+ * Each prototype builds a different LAYOUT from these same widgets and binds to
+ * this same state, so the five directions are genuinely comparable. */
+
+const DS = (() => {
+ // ---- mock state (what the real settings.py engine would read/write) ------
+ const state = {
+ autodim: false,
+ caffeine: false,
+ touchpad: true,
+ mouse: true,
+ airplane: false,
+ nightlight: false,
+ dnd: false,
+ brightness: 70,
+ kbd: 40,
+ powerprofile: "balanced", // performance | balanced | saver
+ scene: null, // Focus | Presentation | Battery | Night | null
+ wallpaper: {
+ dirs: ["~/pictures/wallpaper", "~/pictures/wallpaper/day-night"],
+ current: 2,
+ day: 1,
+ night: 4,
+ },
+ laptop: true, // gates the airplane row
+ };
+
+ // control metadata: glyph (nerd-font), label, and the "on/off" wording.
+ const TOGGLES = {
+ autodim: { glyph: "\u{f0335}", name: "Auto-dim", on: "dimming", off: "off" },
+ caffeine: { glyph: "\u{f0176}", name: "Caffeine", on: "awake", off: "idle ok" },
+ touchpad: { glyph: "\u{f0168}", name: "Touchpad", on: "on", off: "off" },
+ mouse: { glyph: "\u{f037d}", name: "Mouse", on: "on", off: "off" },
+ 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" },
+ };
+ const SLIDERS = {
+ brightness: { glyph: "\u{f00df}", name: "Brightness", floor: 5 },
+ kbd: { glyph: "\u{f60f}", name: "Keyboard", floor: 0 },
+ };
+ const PROFILES = [
+ { id: "performance", label: "Perf" },
+ { id: "balanced", label: "Balanced" },
+ { id: "saver", label: "Saver" },
+ ];
+ // scenes flip several toggles + a profile at once (the iOS-Focus pattern).
+ const SCENES = {
+ Focus: { glyph: "\u{f0210}", sets: { dnd: true, autodim: false, nightlight: false, powerprofile: "balanced" } },
+ Presentation: { glyph: "\u{f0379}", sets: { caffeine: true, autodim: false, dnd: true, powerprofile: "performance" } },
+ Battery: { glyph: "\u{f0079}", sets: { autodim: true, powerprofile: "saver", nightlight: false } },
+ Night: { glyph: "\u{f0594}", sets: { nightlight: true, dnd: true, autodim: true, powerprofile: "saver" } },
+ };
+
+ let toastEl = null;
+ const listeners = [];
+ const onChange = (fn) => listeners.push(fn);
+ const emit = () => listeners.forEach((f) => f(state));
+
+ function toast(msg, ok) {
+ if (!toastEl) return;
+ toastEl.classList.remove("empty");
+ toastEl.innerHTML = ok ? `${msg} <span class="ok">✓ verified</span>` : msg;
+ clearTimeout(toast._t);
+ toast._t = setTimeout(() => {
+ toastEl.classList.add("empty");
+ toastEl.textContent = "waiting…";
+ }, 2600);
+ }
+
+ // The verify-everything contract: apply, then a mock backing reads back the
+ // actual value and confirms. cb runs on the confirmed readback.
+ function applyVerified(label, mutate, el, cb) {
+ mutate();
+ emit();
+ setTimeout(() => {
+ if (el) {
+ el.classList.remove("verified");
+ void el.offsetWidth;
+ el.classList.add("verified");
+ }
+ toast(label, true);
+ if (cb) cb();
+ }, 140);
+ }
+
+ function setToggle(key, el) {
+ const meta = TOGGLES[key];
+ applyVerified(
+ `${meta.name}: ${state[key] ? meta.off : meta.on}`,
+ () => { state[key] = !state[key]; state.scene = null; },
+ el
+ );
+ }
+ function setSlider(key, value, el) {
+ const meta = SLIDERS[key];
+ const v = Math.max(meta.floor, Math.min(100, Math.round(value)));
+ applyVerified(`${meta.name}: ${v}%`, () => { state[key] = v; }, el);
+ }
+ function setProfile(id, el) {
+ applyVerified(`Power profile: ${id}`, () => { state.powerprofile = id; state.scene = null; }, el);
+ }
+ function applyScene(name, el) {
+ const sc = SCENES[name];
+ applyVerified(`Scene: ${name}`, () => {
+ Object.assign(state, sc.sets);
+ state.scene = name;
+ }, el);
+ }
+ function momentary(label) { toast(`${label}…`, false); }
+
+ // ---- small render helpers the prototypes call ----------------------------
+ const h = (tag, cls, txt) => {
+ const e = document.createElement(tag);
+ if (cls) e.className = cls;
+ if (txt != null) e.textContent = txt;
+ return e;
+ };
+
+ function toggleKey(key) {
+ const meta = TOGGLES[key];
+ const on = state[key];
+ const el = h("div", "ds-key" + (on ? " on" : ""));
+ el.innerHTML =
+ `<span class="ds-lamp ${on ? "gold" : "off"}"></span>` +
+ `<span class="k-glyph">${meta.glyph}</span>` +
+ `<span class="k-name">${meta.name}</span>` +
+ `<span class="k-state">${on ? meta.on : meta.off}</span>`;
+ el.onclick = () => setToggle(key, el);
+ return el;
+ }
+
+ function toggleTile(key, detailCb) {
+ const meta = TOGGLES[key];
+ const on = state[key];
+ const el = h("div", "ds-tile" + (on ? " on" : ""));
+ el.innerHTML =
+ `<div class="t-top"><span class="ds-lamp ${on ? "gold" : "off"}"></span>` +
+ `<span class="t-glyph">${meta.glyph}</span>` +
+ (detailCb ? `<span class="t-detail" title="details">⋯</span>` : "") +
+ `</div>` +
+ `<div class="t-name">${meta.name}</div>` +
+ `<div class="t-state">${on ? meta.on : meta.off}</div>`;
+ el.onclick = (ev) => {
+ if (detailCb && ev.target.classList.contains("t-detail")) { detailCb(); return; }
+ setToggle(key, el);
+ };
+ return el;
+ }
+
+ function slider(key) {
+ const meta = SLIDERS[key];
+ const row = h("div", "ds-slider");
+ row.innerHTML =
+ `<span class="s-glyph">${meta.glyph}</span>` +
+ `<span class="s-name">${meta.name}</span>`;
+ 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 + "%"; };
+ inp.onchange = () => setSlider(key, +inp.value, row);
+ row.appendChild(inp); row.appendChild(pct);
+ return row;
+ }
+
+ function profileSeg() {
+ const seg = h("div", "ds-seg");
+ PROFILES.forEach((p) => {
+ const b = h("button", state.powerprofile === p.id ? "cur" : "", p.label);
+ b.onclick = () => setProfile(p.id, seg);
+ seg.appendChild(b);
+ });
+ return seg;
+ }
+
+ function sceneKey(name) {
+ const sc = SCENES[name];
+ const el = h("div", "ds-scene" + (state.scene === name ? " cur" : ""));
+ el.innerHTML = `<span class="sc-glyph">${sc.glyph}</span><span class="sc-name">${name}</span>`;
+ el.onclick = () => applyScene(name, el);
+ return el;
+ }
+
+ function actionBtn(label, glyph, danger, fn) {
+ const b = h("button", "ds-act" + (danger ? " danger" : ""));
+ b.innerHTML = `<span>${glyph}</span><span>${label}</span>`;
+ b.onclick = fn || (() => momentary(label));
+ return b;
+ }
+
+ // Wallpaper sub-view: directory list, thumbnails, day/night pair, sun-time.
+ const SWATCHES = [
+ "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)",
+ ];
+ function wallpaperSubview(closeCb) {
+ const sv = h("div", "ds-subview");
+ const head = h("div", "ds-sub-head");
+ head.innerHTML = `<span class="back">‹ Back</span><span class="title">Wallpaper</span>`;
+ head.querySelector(".back").onclick = closeCb;
+ sv.appendChild(head);
+
+ sv.appendChild(engrave("Current"));
+ const thumbs = h("div", "ds-thumbs");
+ SWATCHES.forEach((g, i) => {
+ const t = h("div", "ds-thumb" + (state.wallpaper.current === i ? " cur" : ""));
+ t.style.background = g;
+ if (i === state.wallpaper.day) t.appendChild(tag("day"));
+ if (i === state.wallpaper.night) t.appendChild(tag("night"));
+ t.onclick = () => {
+ state.wallpaper.current = i;
+ applyVerified(`Wallpaper #${i + 1}`, () => {}, t);
+ sv.querySelectorAll(".ds-thumb").forEach((x, j) => x.classList.toggle("cur", j === i));
+ };
+ thumbs.appendChild(t);
+ });
+ sv.appendChild(thumbs);
+
+ sv.appendChild(engrave("Day / night pair"));
+ const dn = h("div", "ds-daynight");
+ ["day", "night"].forEach((k) => {
+ const cell = h("div", "dn");
+ const sw = h("div", "sw"); sw.style.background = SWATCHES[state.wallpaper[k]];
+ cell.appendChild(sw);
+ cell.appendChild(h("div", "lbl", k === "day" ? "☀ sunup" : "☾ sundown"));
+ dn.appendChild(cell);
+ });
+ sv.appendChild(dn);
+ sv.appendChild(h("div", "ds-note", "Swaps at local sunrise/sundown (sun-time from lat/long)."));
+
+ sv.appendChild(engrave("Directories"));
+ const dirs = h("div", "ds-dirs");
+ state.wallpaper.dirs.forEach((d) => {
+ const r = h("div", "ds-dir");
+ r.innerHTML = `<span class="ds-lamp"></span><span class="path">${d}</span>`;
+ dirs.appendChild(r);
+ });
+ sv.appendChild(dirs);
+ return sv;
+ }
+ function tag(t) { const e = h("span", "tag", t); return e; }
+ function engrave(txt) { return h("div", "ds-engrave", txt); }
+
+ function bindToast(el) { toastEl = el; el.classList.add("empty"); el.textContent = "waiting…"; }
+
+ return {
+ state, TOGGLES, SLIDERS, PROFILES, SCENES,
+ onChange, toast, bindToast,
+ toggleKey, toggleTile, slider, profileSeg, sceneKey, actionBtn,
+ wallpaperSubview, engrave, h,
+ setToggle, setSlider, setProfile, applyScene, momentary,
+ };
+})();