diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-19 23:45:24 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-19 23:45:24 -0500 |
| commit | 0468fedb23ae34a140b6a5a903adedb4e9873ca5 (patch) | |
| tree | 188151ac2e0e9a27b205cd523c7872028795e5ca /docs/prototypes/desktop-settings-shared.js | |
| parent | c4b3710b14a47d4c8297322297f217be264349ea (diff) | |
| download | archsetup-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.js | 261 |
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, + }; +})(); |
