aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/desktop-settings-shared.js
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/desktop-settings-shared.js
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/desktop-settings-shared.js')
-rw-r--r--docs/prototypes/desktop-settings-shared.js261
1 files changed, 261 insertions, 0 deletions
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,
+ };
+})();