Themes: five presets, chosen in admin, applied live to every dashboard

Server: one THEMES list; `theme` is read from the data directory's proddash.json like port/adminPasscode; GET /api/theme is public (dashboards need it before anyone logs in) and returns { theme, themes: [{ id, name, colors: { bg, panel, accent } }] }; POST /api/admin/theme { theme } validates against the list, writes the key into proddash.json keeping every other key, updates the in-memory value and broadcasts a `theme` shell event over /api/events.

style.css: four html[data-theme] variable sets appended at the end — Harbor (navy/cyan), Graphite (charcoal/blue), Ember (warm black/orange) and Daylight (light, for daytime FOH) — each swapping exactly the eight palette variables; :root stays Booth, the default.

shell.js: a self-contained theme section — the last-known theme (localStorage proddash:theme) goes on synchronously, then the server's answer, then live changes via the SSE event; one listener line added in subscribeEvents. Tile-configured colours are set on the tile and still win.

README: a Themes subsection under Configure and the `theme` row in the proddash.json key table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Waters Church
2026-09-13 12:55:36 -04:00
co-authored by Claude Fable 5.1
parent 5d55609fbe
commit f79e8abd92
4 changed files with 190 additions and 0 deletions
+38
View File
@@ -1381,6 +1381,7 @@ function subscribeEvents() {
try { eventsSource?.close(); } catch { /* not open */ }
eventsSource = new EventSource('/api/events');
eventsSource.addEventListener('modules-changed', () => handleModulesChanged());
eventsSource.addEventListener('theme', handleThemeEvent);
eventsSource.onerror = () => {
if (eventsSource.readyState === EventSource.CLOSED) {
clearTimeout(eventsRetry);
@@ -1417,6 +1418,43 @@ async function handleModulesChanged() {
}
}
/* ── theme ──────────────────────────────────────────────────────────── */
/* The dashboard's palette is a server-wide choice (Admin → Theme). Five
preset variable sets live in style.css as html[data-theme="…"]; applying
one is setting that attribute. The last-known theme goes on synchronously
from localStorage so a reload never flashes the default palette, then the
server's answer (GET /api/theme) wins, and a `theme` shell event on
/api/events switches every open dashboard the moment an admin picks
another. Modules style with the shell variables, so they follow; a colour
a tile is configured with is set on the tile itself and stays as chosen. */
const LS_THEME = 'proddash:theme';
function applyTheme(id) {
const theme = String(id || '');
if (!/^[a-z][a-z0-9-]{0,31}$/.test(theme)) return;
document.documentElement.dataset.theme = theme;
try { localStorage.setItem(LS_THEME, theme); } catch { /* private mode — fine */ }
}
/** `theme` shell event from /api/events (wired in subscribeEvents). */
function handleThemeEvent(e) {
try {
applyTheme(JSON.parse(e.data).theme);
} catch { /* a malformed frame changes nothing */ }
}
function initTheme() {
try { applyTheme(localStorage.getItem(LS_THEME)); } catch { /* fine */ }
fetch('/api/theme', { cache: 'no-store' })
.then((res) => (res.ok ? res.json() : null))
.then((body) => { if (body?.theme) applyTheme(body.theme); })
.catch(() => { /* offline: the remembered theme stands */ });
}
initTheme();
/* ── boot ───────────────────────────────────────────────────────────── */
async function boot() {
+60
View File
@@ -755,3 +755,63 @@ body.fullscreen #grid { padding-bottom: 0; }
@media (max-width: 560px) {
#topbar { padding: 0 8px; gap: 6px; }
}
/* ── themes ────────────────────────────────────────────────────────── */
/* Preset palettes, chosen in Admin → Theme and applied by shell.js (and the
admin page) as html[data-theme="…"]. Each swaps exactly the eight palette
variables the module guide names — nothing else — so every module and
every shell element follows, and a colour a tile is configured with (set
on the tile itself) still wins. The :root block at the top of this file is
"booth", the default; the list of ids and names lives in server.js
(GET /api/theme). All four are booth-grade: high contrast, the accent
nowhere near the danger red or the warning amber, muted still readable. */
/* Harbor — deep navy, cyan accent */
html[data-theme="harbor"] {
--bg: #06101f;
--panel: #0c1a30;
--border: #1a2b48;
--text: #e6eefb;
--muted: #8ea4c8;
--accent: #2ad4ee;
--danger: #ff5a5f;
--warn: #f7b53b;
}
/* Graphite — neutral charcoal, electric-blue accent */
html[data-theme="graphite"] {
--bg: #0f1012;
--panel: #17191d;
--border: #2a2d33;
--text: #f0f2f4;
--muted: #9ca3ad;
--accent: #6cb4ff;
--danger: #ff4d4f;
--warn: #ffb224;
}
/* Ember — warm broadcast black, orange accent (danger stays a cool red,
warn a pale yellow, so the three status colours never blur) */
html[data-theme="ember"] {
--bg: #120c0a;
--panel: #1d1411;
--border: #35251e;
--text: #f6ede6;
--muted: #b39b8c;
--accent: #ff8f3a;
--danger: #f03247;
--warn: #ffd86b;
}
/* Daylight — the light one, for a front-of-house desk in daytime */
html[data-theme="daylight"] {
--bg: #eef1f5;
--panel: #ffffff;
--border: #cfd6e0;
--text: #14181f;
--muted: #5b6675;
--accent: #0b8a5f;
--danger: #d1242f;
--warn: #b26a00;
}