diff --git a/ui-concepts/console-euclid-live/app.js b/ui-concepts/console-euclid-live/app.js index c4554fe..87d1210 100644 --- a/ui-concepts/console-euclid-live/app.js +++ b/ui-concepts/console-euclid-live/app.js @@ -1339,11 +1339,141 @@ function renderAll() { engine.on("change", renderAll); engine.on("connection", renderLinkStatus); +/* --------------------------------------------------------------------- + Modules — collapsible + drag-reorderable main-view panels. + Every main-view block carries data-module + a .module-head (which is + both the drag handle and the collapse toggle host). Order and collapse + state persist in localStorage so a user's layout survives reloads. + Pure DOM sugar — no device wiring here; all element IDs are untouched + so the engine bindings keep working regardless of order. + -------------------------------------------------------------------- */ + +function initModules() { + const view = $("#viewMain"); + if (!view) return; + + const KEY_ORDER = "lume.console.moduleOrder"; + const KEY_COLLAPSED = "lume.console.moduleCollapsed"; + const store = { + get(k) { try { return JSON.parse(localStorage.getItem(k)); } catch (_) { return null; } }, + set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (_) {} }, + }; + + const modules = () => $$("#viewMain > [data-module]"); + + // Restore saved order. Saved modules are re-appended in order; any module + // not in the saved list (e.g. one added in a future version) is left where + // it is and so surfaces ahead of the restored ones until reordered. + const savedOrder = store.get(KEY_ORDER); + if (Array.isArray(savedOrder)) { + const byId = new Map(modules().map((m) => [m.dataset.module, m])); + savedOrder.forEach((id) => { const el = byId.get(id); if (el) view.appendChild(el); }); + } + const saveOrder = () => store.set(KEY_ORDER, modules().map((m) => m.dataset.module)); + + // Restore + wire collapse state. + const savedCollapsed = store.get(KEY_COLLAPSED); + const collapsed = new Set(Array.isArray(savedCollapsed) ? savedCollapsed : []); + const saveCollapsed = () => store.set(KEY_COLLAPSED, Array.from(collapsed)); + + let dragEl = null; + + modules().forEach((mod) => { + const id = mod.dataset.module; + const head = mod.querySelector(".module-head"); + const btn = mod.querySelector(".module-collapse"); + + if (collapsed.has(id)) { + mod.classList.add("is-collapsed"); + if (btn) btn.setAttribute("aria-expanded", "false"); + } + + if (btn) btn.addEventListener("click", (e) => { + e.stopPropagation(); + const nowCollapsed = mod.classList.toggle("is-collapsed"); + btn.setAttribute("aria-expanded", nowCollapsed ? "false" : "true"); + if (nowCollapsed) collapsed.add(id); else collapsed.delete(id); + saveCollapsed(); + }); + + if (head) { + head.setAttribute("draggable", "true"); + head.addEventListener("dragstart", (e) => { + dragEl = mod; + mod.classList.add("is-dragging"); + view.classList.add("modules-dragging"); + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = "move"; + try { e.dataTransfer.setData("text/plain", id); } catch (_) {} + } + }); + head.addEventListener("dragend", () => { + mod.classList.remove("is-dragging"); + view.classList.remove("modules-dragging"); + dragEl = null; + saveOrder(); + }); + } + }); + + view.addEventListener("dragover", (e) => { + if (!dragEl) return; + e.preventDefault(); + if (e.dataTransfer) e.dataTransfer.dropEffect = "move"; + const target = e.target.closest("[data-module]"); + if (!target || target === dragEl || target.parentElement !== view) return; + const rect = target.getBoundingClientRect(); + const before = (e.clientY - rect.top) < rect.height / 2; + view.insertBefore(dragEl, before ? target : target.nextElementSibling); + }); + view.addEventListener("drop", (e) => { if (dragEl) e.preventDefault(); }); +} + +/* --------------------------------------------------------------------- + Theme — Auto / Light / Dark. The inline script sets the initial + resolved data-theme before first paint (no flash); this wires the + Settings control, persists the preference, and re-resolves "auto" live + when the system scheme changes. + -------------------------------------------------------------------- */ + +function initTheme() { + const KEY = "lume.console.theme"; + const root = document.documentElement; + const mq = window.matchMedia("(prefers-color-scheme: light)"); + const store = { + get() { try { return localStorage.getItem(KEY); } catch (_) { return null; } }, + set(v) { try { localStorage.setItem(KEY, v); } catch (_) {} }, + }; + + let pref = store.get() || "auto"; // "auto" | "light" | "dark" + const btns = $$("#themeSeg .theme-seg-btn"); + + const resolve = (p) => (p === "auto" ? (mq.matches ? "light" : "dark") : p); + const apply = () => { + root.setAttribute("data-theme", resolve(pref)); + btns.forEach((b) => b.classList.toggle("active", b.dataset.themeChoice === pref)); + }; + + btns.forEach((b) => b.addEventListener("click", () => { + pref = b.dataset.themeChoice; + store.set(pref); + apply(); + })); + + const onSystemChange = () => { if (pref === "auto") apply(); }; + if (mq.addEventListener) mq.addEventListener("change", onSystemChange); + else if (mq.addListener) mq.addListener(onSystemChange); // older Safari + + apply(); +} + /* --------------------------------------------------------------------- Bootstrap -------------------------------------------------------------------- */ requestAnimationFrame(() => { + initTheme(); + initModules(); engine.start().then(() => { if (engine.state.demo) { showToast("Demo mode — no device found, using sample data"); diff --git a/ui-concepts/console-euclid-live/index.html b/ui-concepts/console-euclid-live/index.html index ea1a174..b55d4d2 100644 --- a/ui-concepts/console-euclid-live/index.html +++ b/ui-concepts/console-euclid-live/index.html @@ -7,8 +7,20 @@ - + + @@ -47,8 +59,16 @@
-
-
Master
+
+
+ Master + + + + +
@@ -102,39 +122,31 @@
- -
-
-
- -
-
- -
- -
-
- cozy fireplace - calm ocean - sunset glow - forest morning -
-
-
- -
- Channels - 300 LEDs total +
+
+ Channels + + 300 LEDs total + + +
+
-
- -
-
CH 1 — Fire
+
+
+ CH 1 — Fire + + + + +
@@ -148,7 +160,17 @@
-
+
+ +
+ Controls + + + + +
Segment Range @@ -183,6 +205,37 @@
+ +
+
+ AI Prompt + + + + +
+
+
+ +
+
+ +
+ +
+
+ cozy fireplace + calm ocean + sunset glow + forest morning +
+
+
+
@@ -256,12 +309,36 @@ + +
+
+
+ + 03 · Appearance +
+ +
+
+
+
+ Theme + Auto follows your system · Light uses the warm-paper ground +
+
+ + + +
+
+
+
+
- 03 · AI Prompt Engine + 04 · AI Prompt Engine
@@ -284,7 +361,7 @@
- 04 · MQTT Broker + 05 · MQTT Broker
@@ -320,7 +397,7 @@
- 05 · sACN / DMX + 06 · sACN / DMX
@@ -348,7 +425,7 @@
- 06 · Feel / Tuning + 07 · Feel / Tuning
@@ -403,7 +480,7 @@
- 07 · Firmware Update + 08 · Firmware Update
diff --git a/ui-concepts/console-euclid-live/styles.css b/ui-concepts/console-euclid-live/styles.css index ee72c24..c7905bb 100644 --- a/ui-concepts/console-euclid-live/styles.css +++ b/ui-concepts/console-euclid-live/styles.css @@ -57,6 +57,40 @@ --font-ui: "Source Serif 4", Georgia, serif; --font-mono: "Space Mono", ui-monospace, monospace; --font-display: "Fraunces", "Source Serif 4", Georgia, serif; + /* wordmark only — a clean grotesque that pairs with the Space Mono labels */ + --font-brand: "Space Grotesk", "Space Mono", ui-sans-serif, system-ui, sans-serif; + + color-scheme: dark; +} + +/* ========================================================================== + Light "paper" theme — the lit-instrument identity inverts to ink-on-paper + rather than going generic-white. app.js resolves the Auto / Light / Dark + preference to a concrete data-theme on ; only the grounds, ink and + hairlines flip here — the three flat accents (amber / red / blue / green) + are shared across both themes, and the many `background: var(--c-text)` + fills invert cream→ink automatically, staying high-contrast either way. + ========================================================================== */ +:root[data-theme="light"]{ + color-scheme: light; + + --c-void: #EBE2CE; + --c-chassis: #E4DAC2; + --c-chassis-hi: #F0E8D7; + --c-panel: #E4DAC2; + --c-panel-hi: #F0E8D7; + --c-panel-in: #DBCFB4; + --c-line: #23201750; + --c-line-soft: #23201730; + --c-line-hair: #2320171c; + + --c-text: #201C13; + --c-text-dim: #201C13ad; + --c-text-faint: #201C1370; + + --c-paper: #F2ECDC; + --c-paper-dim: #E4DAC2; + --c-on-ink: #F2ECDC; } *{ box-sizing: border-box; -webkit-tap-highlight-color: transparent; } @@ -165,10 +199,10 @@ input, button, textarea{ font-family: inherit; color: inherit; } } .brand-plate .mark{ - font-family: var(--font-display); + font-family: var(--font-brand); font-weight: 600; - font-size: 21px; - letter-spacing: 0.16em; + font-size: 20px; + letter-spacing: 0.28em; color: var(--c-text); position: relative; padding-left: 1px; @@ -341,6 +375,58 @@ input, button, textarea{ font-family: inherit; color: inherit; } box-shadow: none; } +/* ========================================================================== + Modules — collapsible + drag-reorderable panels + Every main-view block is a self-contained `.module` with a `.module-head` + that doubles as the drag handle and hosts the collapse chevron. + ========================================================================== */ + +.module{ + transition: box-shadow var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease); +} + +.module-head{ + cursor: grab; + user-select: none; + -webkit-user-select: none; +} +.module-head:active{ cursor: grabbing; } + +.module-tools{ + display: inline-flex; + align-items: center; + gap: 10px; +} + +.module-collapse{ + width: 22px; height: 22px; + flex-shrink: 0; + display: inline-flex; align-items: center; justify-content: center; + padding: 0; + border: none; + background: transparent; + color: var(--c-text-faint); + cursor: pointer; + border-radius: var(--radius-panel); + transition: color var(--t-fast) var(--ease), background var(--t-fast) var(--ease); +} +.module-collapse:hover{ color: var(--c-text); background: var(--c-line-soft); } +.module-collapse .chevron{ + width: 15px; height: 15px; + transition: transform var(--t-med) var(--ease); +} +.module.is-collapsed .module-collapse .chevron{ transform: rotate(-90deg); } + +/* Collapsed: keep only the head, hide the body */ +.module.is-collapsed > :not(.module-head){ display: none !important; } +.module.is-collapsed .module-head{ margin-bottom: 0; } + +/* Drag feedback */ +.module.is-dragging{ opacity: 0.5; } +.modules-dragging .module-head{ cursor: grabbing; } +.module.drop-before{ box-shadow: inset 0 2px 0 0 var(--c-red); } +.module.drop-after{ box-shadow: inset 0 -2px 0 0 var(--c-red); } + /* ========================================================================== Master rail: power, brightness fader, nightlight, AI prompt ========================================================================== */ @@ -1420,6 +1506,34 @@ input, button, textarea{ font-family: inherit; color: inherit; } .toggle-field-title{ font-size: 13px; color: var(--c-text); } .toggle-field-sub{ font-family: var(--font-mono); font-size: 9.5px; color: var(--c-text-faint); } +/* Theme segmented control (Auto / Light / Dark) — mirrors the top-rail nav */ +.theme-seg{ + display: inline-flex; + border: var(--hair) solid var(--c-line); + border-radius: 2px; + overflow: hidden; + flex-shrink: 0; +} +.theme-seg-btn{ + border: none; + background: transparent; + color: var(--c-text-faint); + padding: 7px 13px; + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: color var(--t-med) var(--ease), background var(--t-med) var(--ease); +} +.theme-seg-btn + .theme-seg-btn{ border-left: var(--hair) solid var(--c-line); } +.theme-seg-btn:hover{ color: var(--c-text-dim); } +.theme-seg-btn.active{ + color: var(--c-void); + background: var(--c-text); +} + .sub-body{ display: grid; gap: 12px;