diff options
10 files changed, 1440 insertions, 46 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, + }; +})(); diff --git a/docs/specs/2026-07-02-desktop-settings-panel-spec.org b/docs/specs/2026-07-02-desktop-settings-panel-spec.org index 17fa0e6..3aef647 100644 --- a/docs/specs/2026-07-02-desktop-settings-panel-spec.org +++ b/docs/specs/2026-07-02-desktop-settings-panel-spec.org @@ -14,6 +14,7 @@ :PROPERTIES: :ID: fb7eec22-a214-4568-82c4-903612f4832f :END: +- [2026-07-19 Sun] DRAFT — folded Craig's control-set decisions (home->archsetup handoff): added night-light, DND, lock/suspend, power profile, and scenes to the controls table; recorded volume and theme as out-of-scope with reasons; pulled the format pickers into a separate display-format sibling spec (stub filed). The "few other things" scoping decision and the scenes decision are now closed. One decision stays open — the wallpaper manager's shape (row-that-opens-a-sub-view vs its own sub-spec), to resolve at spec-review. UI prototyping (ui-prototyping rule: research -> 5 directions -> iterate) is not done yet and is the gate before READY. - [2026-07-04 Sat] DRAFT — all four decisions resolved by Craig (dim + airplane collapse into the panel, touchpad + caffeine stay on the bar; Super+Shift+G keybind; code in dotfiles settings/ beside net/; 5% brightness floor). The four collapse/keybind/location/floor decisions are closed; one open scoping question remains ("few other things" — see Decisions) before a spec-review can flip it READY. - [2026-07-03 Fri] DRAFT update — from the waybar/panel-family design discussion: adopt the instrument-console faceplate aesthetic net + bt @@ -52,7 +53,8 @@ gathers them. 1. One panel with every desktop toggle + slider: auto-dim, idle/caffeine, touchpad, mouse, airplane (laptop-only), screen brightness, keyboard - backlight. + backlight, night-light, Do Not Disturb, power profile, lock/suspend + actions, scenes, and a wallpaper manager (see the controls table). 2. Conditional rows appear only when the hardware/context applies (mouse present, trackpad present, battery present) — reuse the detection the airplane/touchpad indicators already do. @@ -84,27 +86,47 @@ gathers them. ** Controls and their backings -| Control | Backing | -|--------------------+----------------------------------------------| -| Auto-dim toggle | hyprctl decoration:dim_inactive (dim-toggle) | -|--------------------+----------------------------------------------| -| Idle / caffeine | hypridle start/stop (caffeine-toggle) | -|--------------------+----------------------------------------------| -| Touchpad toggle | toggle-touchpad + touchpad-state file | -|--------------------+----------------------------------------------| -| Mouse toggle | same mechanism, mouse-state file | -|--------------------+----------------------------------------------| -| Airplane mode | airplane-mode script (laptop-only row) | -|--------------------+----------------------------------------------| -| Screen brightness | brightnessctl (backlight class), slider + % | -|--------------------+----------------------------------------------| -| Keyboard backlight | brightnessctl (kbd_backlight class), slider | -|--------------------+----------------------------------------------| +| Control | Kind | Backing | +|--------------------+----------+----------------------------------------------| +| Auto-dim toggle | toggle | hyprctl decoration:dim_inactive (dim-toggle) | +|--------------------+----------+----------------------------------------------| +| Idle / caffeine | toggle | hypridle start/stop (caffeine-toggle) | +|--------------------+----------+----------------------------------------------| +| Touchpad toggle | toggle | toggle-touchpad + touchpad-state file | +|--------------------+----------+----------------------------------------------| +| Mouse toggle | toggle | same mechanism, mouse-state file | +|--------------------+----------+----------------------------------------------| +| Airplane mode | toggle | airplane-mode script (laptop-only row) | +|--------------------+----------+----------------------------------------------| +| Screen brightness | slider | brightnessctl (backlight class), slider + % | +|--------------------+----------+----------------------------------------------| +| Keyboard backlight | slider | brightnessctl (kbd_backlight class), slider | +|--------------------+----------+----------------------------------------------| +| Night light / | toggle | color-temperature shift (candidate: | +| color temperature | | hyprsunset; confirm the tool at build) | +|--------------------+----------+----------------------------------------------| +| Do Not Disturb | toggle | dunstctl set-paused (pause notifications) | +|--------------------+----------+----------------------------------------------| +| Power profile | selector | powerprofilesctl (performance / balanced / | +| | | power-saver, three-way) | +|--------------------+----------+----------------------------------------------| +| Lock / suspend | actions | hyprlock / systemctl suspend (momentary | +| | | buttons, not stateful toggles) | +|--------------------+----------+----------------------------------------------| +| Scenes / profiles | composite| one control that flips several toggles at | +| | | once: Focus, Presentation, Battery-saver, | +| | | Night (see the Scenes decision below) | +|--------------------+----------+----------------------------------------------| +| Wallpaper manager | sub-view | still-open shape (row-that-opens-a-sub-view | +| | | vs its own sub-spec) — resolve at spec-review| +|--------------------+----------+----------------------------------------------| Slider changes apply live (throttled) and read back the actual level after apply — verify-everything. Toggles re-read their source of truth after firing, same as the bar indicators do, and the bar modules get their refresh -signals so both surfaces agree. +signals so both surfaces agree. The lock/suspend buttons are momentary +actions with no persistent state to reflect. A scene reads back each toggle it +sets and reports the composite as applied only when every member verified. ** Open/close behavior @@ -130,23 +152,50 @@ Resolved (Craig, 2026-07-04): dotfiles =settings/= sibling to =net/= (same src-l CLOSED: [2026-07-04 Sat] Resolved (Craig, 2026-07-04): 5% floor on the brightness slider, so a dark-room drag can't black the screen out and lock you out. brightnessctl's 0-100% range clamps to a 5% minimum. -** TODO What are the "few other things" beyond the toggles? -The 2026-07-03 discussion named consolidating the toggle buttons "and a few -other things" into this panel, but the extras weren't enumerated. Current -control list (above): auto-dim, idle/caffeine, touchpad, mouse, airplane, -screen brightness, keyboard backlight. Candidates raised or adjacent — -confirm which belong here vs the audio panel vs the bar: night-light / color -temperature, volume or a -master-mute mirror (or leave all audio to the audio panel), a -notifications/do-not-disturb toggle (dunst), lock/suspend actions. Craig to -name the set. +** DONE What are the "few other things" beyond the toggles? +CLOSED: [2026-07-19 Sun] +Resolved (Craig, 2026-07-19, via the home->archsetup handoff). Beyond the seven +base toggles/sliders, the panel adds: night-light / color temperature; Do Not +Disturb (dunst); lock / suspend quick actions; power profile +(performance/balanced/saver); and scenes/profiles (see the Scenes decision). +Plus the wallpaper manager already confirmed 2026-07-04 (its shape stays open, +see the Wallpaper decision). The controls table above now carries all of them. + +OUT of this panel, with reasons: volume / master-mute stays with the audio +panel (this panel does not mirror it — one owner per control); theme light/dark +goes to the separate theme-studio task, not here. + +Format pickers (time/date/weather display format) are pulled out entirely into +their own future sibling spec. The real problem there is a single source of +truth — many programs store their own format, so the design question is where +the canonical time/date/weather format lives and how every consumer (clock +module, weather chip) reads it without drift. Tracked as a stub: +[[file:2026-07-19-display-format-single-source-of-truth-spec.org][display-format single-source-of-truth]]. *** 2026-07-04 Sat — Craig's input (roam capture): the set includes a wallpaper manager Confirmed the panel gathers the mouse/trackpad toggle, a no-sleep (idle-inhibit) toggle, and the auto-dim toggle, and adds a *wallpaper manager* (this is where the displaced waypaper functionality lands — see the media/keybind change that freed Super+Shift+P). The wallpaper manager needs its own depth: - take a number of directories to look in; - switch the wallpaper with the change persisting across sessions; - switch between two pictures at sunup / sundown (a day/night pair). -That last one implies a sun-time source (a lat/long or a sunrise/sunset lookup). The wallpaper manager is sizable enough it may want its own sub-spec rather than a single panel row; decide during the spec-review whether it's a row that opens a sub-view or a separate panel. Remaining "few other things" candidates above (night-light, theme switch, DND, lock/suspend) still await Craig's yes/no. +That last one implies a sun-time source (a lat/long or a sunrise/sunset lookup). + +** DONE Scenes / profiles — one control that flips several toggles at once +CLOSED: [2026-07-19 Sun] +Resolved (Craig, 2026-07-19): the panel offers named scenes that set several +controls in one action — Focus, Presentation, Battery-saver, Night. Scenes are +the payoff of consolidating every desktop control in one panel. The exact +toggle set each scene applies is a build-time detail (design it against the +control table); the spec commitment is that scenes exist and each verifies +every member it sets (see Controls note above). + +** DONE Wallpaper manager — a panel row that opens a sub-view, or its own sub-spec? +CLOSED: [2026-07-19 Sun] +Resolved (Craig, 2026-07-19): a row in the panel that opens a sub-view, kept +inside the one panel (the "consolidate everything" thesis). The wallpaper +manager's depth (directories to scan, persistent selection, a day/night +sunup/sundown pair needing a sun-time source) lives in that sub-view, not a +separate surface. Sun-time source (lat/long or sunrise/sunset lookup) is a +build detail for the sub-view. * Implementation phases diff --git a/docs/specs/2026-07-19-display-format-single-source-of-truth-spec.org b/docs/specs/2026-07-19-display-format-single-source-of-truth-spec.org new file mode 100644 index 0000000..02e043c --- /dev/null +++ b/docs/specs/2026-07-19-display-format-single-source-of-truth-spec.org @@ -0,0 +1,63 @@ +#+TITLE: Display-Format Single Source of Truth +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-19 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* DRAFT Display-Format Single Source of Truth +:PROPERTIES: +:ID: ae247f1a-e48f-4d5e-8556-dd1f0aa34a69 +:END: +- [2026-07-19 Sun] DRAFT — stub. Split out of the desktop-settings panel spec (Craig, 2026-07-19): format pickers do not belong in that panel. Filed to hold the problem until it gets its own design pass. Not started. + +* Status +:PROPERTIES: +:ID: a76a41df-85c2-4137-9ba5-eff04db529a5 +:END: +- [2026-07-19 Sun] DRAFT — stub only. Problem framed below; goals, decisions, and phases to be developed when Craig picks this up. + +* Metadata + +| Field | Value | +|--------+-----------------------------------------| +| Status | draft | +|--------+-----------------------------------------| +| Owner | Craig Jennings | +|--------+-----------------------------------------| +| Repo | dotfiles | +|--------+-----------------------------------------| +| Kin | desktop-settings panel (parent), clock | +| | module, weather chip (consumers) | +|--------+-----------------------------------------| + +* Problem + +Display-format settings for time, date, and weather are stored independently by +each program that renders them. The clock module has its own format ring, the +weather chip its own, and any future consumer would carry a third. There is no +canonical place a user sets "this is how I want the time shown" that every +surface reads. + +The design problem is a single source of truth: where the canonical +time/date/weather format lives, and how every consumer reads it without drift. +Solve that first; the pickers (the UI that edits the format) are downstream of +where the value is stored. + +This was pulled out of the desktop-settings panel spec +([[file:2026-07-02-desktop-settings-panel-spec.org][desktop-settings panel]]) on +2026-07-19 — format editing does not belong in that panel, and the real work is +the storage/consumption model, not a picker widget. + +* Goals + +(To be developed. At minimum: a canonical store for time/date/weather format; a +read path every consumer uses; a migration for the existing per-consumer +formats.) + +* Decisions + +(None yet — Craig has not started this.) + +* Implementation phases + +(To be developed once the goals and the storage model are settled.) @@ -23,20 +23,31 @@ The vocabulary is open — topic tags are coined as needed — so these are conv * Next Session Focus -On 2026-07-19, Craig selected the following autonomous work for the next -session. Complete implementation and automated verification independently; -bring only the final live/manual checks back for review. - -- Waybar and panels: restore date-format scrolling; dismiss the clock panel on - right click; show the active wired interface; order network connections by - availability; indicate hotspot/metered WiFi in amber; and connect the best - available saved WiFi profile when WiFi is enabled. -- Audio: reduce notification playback loudness by 40%; and make the audio - doctor assess microphone/input health as well as output health. +On 2026-07-19, Craig selected the following autonomous work. Status after the +2026-07-19 evening session: 5 shipped (dotfiles, pushed), 3 held for a design +decision. Each shipped item has its own DONE task below. + +Shipped: notification loudness -40% (808ca23); clock panel right-click dismiss +(fc9a2b7, live-verified); date-format scrolling as a date-only ring (9dfe082); +show the active wired interface (22867f9); connect the best saved WiFi profile +on enable (9105361). + +Held for a design decision (not solo — each needs Craig's call, kept as TODO +below): +- Order network connections by availability — the task wants one tiered list + (available saved -> available unsaved -> unavailable saved), but the panel + spec says "three labelled groups, never one merged list" with Saved MRU-first. + Reorder within Saved only, or merge into one list (overriding the spec)? +- Indicate hotspot/metered WiFi in amber — "hotspot" is ambiguous (connected-to + a phone hotspot vs the machine running an AP), and metered detection needs new + nmcli reads on the status fast path (contract is "one nmcli call"). +- Audio doctor mic/input health — points at docs/specs/2026-07-10-audio-doctor- + input-side-spec.org (DRAFT, four decisions open). * Archsetup Open Work -** TODO [#A] Velox boot recovery — no kernel in BE :bug:velox:zfs: -SCHEDULED: <2026-07-16> +** DONE [#A] Velox boot recovery — no kernel in BE :bug:velox:zfs: +CLOSED: [2026-07-19 Sun] +Recovered. Velox boots linux-lts 6.18.38 and is back on the tailnet (up 1d+, /boot holds initramfs-linux-lts.img). The pre-pacman ZFS snapshot rollback restored the kernel from the ZBM recovery shell. Velox won't boot: ZBM prompts for the passphrase, unlocks, then reports no bootable environment with a kernel. Cause: an interrupted kernel =-Syu= removed the old kernel and never installed the new one — /mnt/be/boot (from zroot/ROOT/default) holds ONLY intel-ucode.img; vmlinuz-linux + both initramfs are gone. /boot lives inside zroot/ROOT/default (no separate boot dataset), so root-dataset snapshots capture it. Status 2026-07-15: a first rollback attempt did NOT fix it (square zero after reboot) — suspected typo in the snapshot name, so the rollback likely errored and did nothing. NOT verified. Next session: verify state in the ZBM recovery shell BEFORE any reboot. @@ -83,7 +94,9 @@ Post-mortem for the 2026-07-15 velox no-kernel boot failure, from the archsetup/ ** TODO [#C] Add inetutils to install base :feature:solo:quick:network: TRAMP's /ftp: method (ange-ftp) shells out to a command-line ftp client; Arch ships none by default. GNU inetutils provides =/usr/bin/ftp=. Craig's dirvish config has an FTP quick-access entry (phone FTP server), so it's a config dependency. Installed manually on ratio 2026-07-14; velox needs it once it boots. Add to the install base so future machines get it for free; verify via VM test. From .emacs.d handoff 2026-07-14-1751. -** TODO [#C] Restore date-format scrolling on the waybar date module :feature:waybar:dotfiles:quick: +** DONE [#C] Restore date-format scrolling on the waybar date module :feature:waybar:dotfiles:quick: +CLOSED: [2026-07-19 Sun] +Shipped dotfiles 9dfe082: date-only ring (ordinal/full/longdate), on-scroll rewired, layout guard flipped. UTC/time stay on the time module. Date and time are separate fixed-position controls. The time display cycles its own formats, including UTC; the date/calendar control cycles date-only formats and never displays a second time. Implement the dedicated format rings, @@ -96,11 +109,17 @@ The clock-panel handoff settled the prior open question: UTC belongs only to the time ring, while the date ring is date-only. The existing task is therefore a focused follow-up, not a two-line restoration of the old combined ring. -** TODO [#C] Notification sound loudness :chore:audio:quick:solo: +** DONE [#C] Notification sound loudness :chore:audio:quick:solo: +CLOSED: [2026-07-19 Sun] +Shipped dotfiles 808ca23: NOTIFY_VOLUME default 65536->39322 (0.6 gain) in both notify copies. Reduce notification-sound playback loudness by 40% (0.6 gain, approximately -4.4 dB). Change the =NOTIFY_VOLUME= playback control rather than re-encoding the normalized sound files; verify each notification type still plays clearly. +** TODO [#C] Dupre theme waybar.css drifted from live style.css :bug:dotfiles:waybar: +Grading: Minor severity (cosmetic, reverts only on a theme switch) × rare edge case (dupre is already the active theme) = P4 = [#D] on user impact, bumped to [#C] because the dotfiles =make test= stays RED until synced, poisoning the green baseline for every future commit. +The weather-kit work added =#custom-weather= selectors to =hyprland/.config/waybar/style.css= but never mirrored them into =hyprland/.config/themes/dupre/waybar.css=. =tests/theme-css= asserts the two files are identical (set-theme copies the theme file over the live one), so switching to dupre would silently revert the weather chip styling. Fix: sync the theme file to live. Pre-existing; found 2026-07-19 during an unrelated commit's green-baseline run. + ** TODO [#B] Assess a Hyprland left-drag window gesture :feature:hyprland: Evaluate whether a global left-click drag can move ordinary windows without breaking application selection, text interaction, or Wayland security @@ -111,7 +130,9 @@ any binding. Offer a period-appropriate selector for timer duration, likely drawing on the tape-counter idiom, while preserving the existing direct-entry path. -** TODO [#C] Show the active wired interface in the Waybar network module :feature:waybar:network: +** DONE [#C] Show the active wired interface in the Waybar network module :feature:waybar:network: +CLOSED: [2026-07-19 Sun] +Shipped dotfiles 22867f9: select_device prefers connected wifi -> connected ethernet -> wifi fallback, so a live cable shows the wired glyph+iface instead of Offline. When Ethernet is active, replace the offline-WiFi presentation with the wired interface glyph and interface name. @@ -119,7 +140,9 @@ interface glyph and interface name. Present saved and currently available networks in this order: available saved profiles, available unsaved networks, then saved profiles that are unavailable. -** TODO [#C] Let the clock panel dismiss itself on right click :feature:clock:waybar: +** DONE [#C] Let the clock panel dismiss itself on right click :feature:clock:waybar: +CLOSED: [2026-07-19 Sun] +Shipped dotfiles fc9a2b7: secondary-button gesture -> ClockApplication._dismiss hides the open panel. Live-verified with Craig 2026-07-19. Make a right click inside the open clock panel toggle it closed. Preserve left click for its established interaction; the Waybar time module remains the explicit way to reopen the panel. @@ -128,7 +151,9 @@ explicit way to reopen the panel. Detect hotspot/metered connectivity and render the WiFi icon plus SSID amber, while ordinary WiFi stays white. -** TODO [#C] Make the WiFi toggle connect the best available profile :feature:network: +** DONE [#C] Make the WiFi toggle connect the best available profile :feature:network: +CLOSED: [2026-07-19 Sun] +Shipped dotfiles 9105361: manage.wifi_radio -> _connect_best_saved activates the strongest in-range saved profile on enable; nothing in range falls back to NM autoconnect. When enabling WiFi, automatically connect to the highest-priority available saved network instead of requiring a panel selection first. @@ -666,6 +691,12 @@ Design / open questions (propose before building): Implementation notes: a small GTK layer-shell app (mirror pocketbook's structure: src-layout Python package, pytest, Makefile) talking to brightnessctl / hyprctl / the touchpad + airplane helpers. Lives in the dotfiles repo or in-tree like pocketbook. TDD the backing toggle/slider logic. Sizable — worth a design doc first. +Home handoff 2026-07-19 (inbox, resolving the open "few other things" decision — fold into the spec, close the open decision, extend the controls table, then run spec-review, may flip DRAFT→READY). Ownership: home drives the build (dotfiles settings/), archsetup keeps the canonical spec. Full reconciliation in home docs/design/2026-07-19-desktop-settings-module-brainstorm.org. +- ADD controls: night-light / color temperature; Do Not Disturb / notifications (dunst); lock / suspend quick actions; power profile (performance/balanced/saver); scenes/profiles — one control flipping several toggles at once (Focus, Presentation, Battery-saver, Night). Scenes are the payoff of consolidating everything. +- OUT (record reasons): volume / master-mute stays with the audio panel (no mirror here); theme light/dark goes to the theme-studio task. +- FORMAT PICKERS pulled to their own future sibling spec — time/date/weather format is out of THIS panel. Rationale: format settings live in many programs, so the design problem is a single source of truth for the canonical format. Track a future sibling-spec stub in docs/specs (time/date/weather format single-source-of-truth); Craig thinking it through separately, not started. +- STILL OPEN (spec already flags): wallpaper manager confirmed in scope, but row-that-opens-a-sub-view vs its own sub-spec undecided — resolve at spec-review. + ** TODO [#B] Local offline LLM runtime + per-host model cache :tooling:llm: :PROPERTIES: :LAST_REVIEWED: 2026-07-06 |
