[FEAT] theme: light/charcoal dark mode with a persistent switcher

This commit is contained in:
Twarner491
2026-06-05 16:34:08 -07:00
parent 4e43b5ee69
commit 4fb21fe01d
3 changed files with 61 additions and 5 deletions
+28 -1
View File
@@ -1517,6 +1517,20 @@
+ ' <div class="seg" data-key="' + key + '">' + btns + '</div>'
+ '</div>';
}
// Client-side theme switcher row. Reuses the .seg look but is tagged
// data-theme-seg so wireSettingsControls skips it - it applies instantly
// and is NOT part of the Pi config save flow.
function themeRow() {
var cur = currentTheme();
var btn = function (v, label) {
return '<button type="button" data-theme="' + v + '" aria-current="' + (cur === v ? 'true' : 'false') + '">' + label + '</button>';
};
return ''
+ '<div class="menu-row">'
+ ' <div><span class="label">Theme</span><span class="hint">saved on this device</span></div>'
+ ' <div class="seg" data-theme-seg>' + btn('light', 'light') + btn('dark', 'dark') + '</div>'
+ '</div>';
}
function wireSettingsControls(scope) {
scope = scope || document;
scope.querySelectorAll('.switch').forEach(function (sw) {
@@ -1537,7 +1551,7 @@
setSaveState('change pending');
});
});
scope.querySelectorAll('.seg').forEach(function (seg) {
scope.querySelectorAll('.seg:not([data-theme-seg])').forEach(function (seg) {
seg.querySelectorAll('button').forEach(function (b) {
b.addEventListener('click', function () {
seg.querySelectorAll('button').forEach(function (x) { x.setAttribute('aria-current', x === b ? 'true' : 'false'); });
@@ -2053,6 +2067,7 @@
var preserve = cfg.preserve;
adminBody.innerHTML =
'<div class="admin-settings">'
+ themeRow()
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
+ settingsSlider('CONFIDENCE', 'Confidence threshold', 'min score to log a detection', v.CONFIDENCE, 0.1, 0.95, 0.05, 2)
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
@@ -2067,6 +2082,18 @@
+ '</div>'
+ '</div>';
wireSettingsControls(adminBody);
adminBody.querySelectorAll('.seg').forEach(wireToggleAdvance); // open-space advance
// Theme switcher applies + persists immediately (separate from the
// Pi config save below).
var themeSeg = adminBody.querySelector('[data-theme-seg]');
if (themeSeg) themeSeg.addEventListener('click', function (ev) {
var b = ev.target.closest('button[data-theme]');
if (!b) return;
applyTheme(b.getAttribute('data-theme'));
[].forEach.call(themeSeg.querySelectorAll('button'), function (x) {
x.setAttribute('aria-current', x === b ? 'true' : 'false');
});
});
var saveBtn = document.getElementById('saveBtn');
if (saveBtn) saveBtn.addEventListener('click', saveSettings);
})
+3
View File
@@ -7,6 +7,9 @@
<meta name="description" content="A live bird collage from your window.">
<link rel="icon" type="image/png" href="./favicon.png">
<link rel="apple-touch-icon" href="./favicon.png">
<!-- Apply the saved theme before first paint so dark mode doesn't flash
light on load. The full switcher lives in Settings (writes bird:theme). -->
<script>try{if(localStorage.getItem('bird:theme')==='dark')document.documentElement.setAttribute('data-theme','dark');}catch(e){}</script>
<link rel="stylesheet" href="./styles.css">
</head>
<body class="av-local">
+30 -4
View File
@@ -8,12 +8,38 @@
--ink-soft: #908576;
--accent: #4a3f31; /* warm dark grey - matches ink-2, no longer red */
--accent-2: #1a1612; /* near-black ink - same hue as the title text */
--hairline: rgba(26,22,18,0.14); /* faint divider lines + chart gridlines */
--pill: #fcfcfb; /* active-toggle sliding pill */
/* Depth shadow recipes - replace flat 1px borders */
--edge: inset 0 0 0 1px rgba(255,255,255,0.55), 0 0 0 1px rgba(26,22,18,0.04);
--edge-lg: inset 0 0 0 1px rgba(255,255,255,0.6), 0 0 0 1px rgba(26,22,18,0.06), 0 8px 28px rgba(26,22,18,0.06);
--recess: inset 0 1px 2px rgba(26,22,18,0.06), inset 0 0 0 1px rgba(26,22,18,0.04);
--raised: inset 0 0 0 1px rgba(255,255,255,0.8), 0 0 0 1px rgba(26,22,18,0.05), 0 1px 2px rgba(26,22,18,0.07);
}
/* ---- Charcoal dark theme. Set via data-theme="dark" on <html> (an
inline script in index.html applies the saved choice before paint;
the switcher lives in Settings). The whole UI is variable-driven, so
flipping ink<->paper inverts cleanly: text reads light, the stats
squares become light marks on dark, the collage cutouts (transparent
PNGs) sit on the charcoal directly. Shadow recipes are re-cut for a
dark surface (faint light edges, deeper drop shadows). ---- */
:root[data-theme="dark"] {
color-scheme: dark;
--paper: #17181c; /* charcoal page bg */
--paper-2: #212329; /* recessed track */
--paper-3: #2c2f36; /* placeholder fill */
--ink: #ece8e1; /* warm off-white text / marks */
--ink-2: #b7afa2; /* secondary text */
--ink-soft: #837c70; /* muted text */
--accent: #d8d2c6; /* light warm - pills read light-on-dark */
--accent-2: #ece8e1;
--hairline: rgba(236,232,225,0.20); /* faint light divider on charcoal */
--pill: #3a3e48; /* raised, clearly lighter than the track */
--edge: inset 0 0 0 1px rgba(255,255,255,0.05), 0 0 0 1px rgba(0,0,0,0.4);
--edge-lg: inset 0 0 0 1px rgba(255,255,255,0.06), 0 0 0 1px rgba(0,0,0,0.5), 0 10px 30px rgba(0,0,0,0.5);
--recess: inset 0 1px 2px rgba(0,0,0,0.5), inset 0 0 0 1px rgba(255,255,255,0.04);
--raised: inset 0 0 0 1px rgba(255,255,255,0.07), 0 0 0 1px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.5);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }
body {
@@ -144,7 +170,7 @@
raised-paper look; buttons just toggle their text colour. */
.seg-pill {
position: absolute; top: 4px; bottom: 4px; left: 0;
background: var(--paper); border-radius: 999px;
background: var(--pill); border-radius: 999px;
box-shadow: var(--raised);
transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);
will-change: transform, width;
@@ -280,7 +306,7 @@
.switch::after {
content: ''; position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px;
background: var(--paper); border-radius: 999px;
background: var(--pill); border-radius: 999px;
box-shadow: var(--raised);
transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;
}
@@ -289,7 +315,7 @@
}
.switch[aria-checked="true"]::after {
transform: translateX(18px);
background: var(--paper);
background: var(--pill);
}
/* Slider - title row (label + value badge with breathing room) and
@@ -361,7 +387,7 @@
}
.seg button[aria-current="true"] {
color: var(--ink);
background: var(--paper);
background: var(--pill);
box-shadow: var(--raised);
}