[FEAT] theme: light/charcoal dark mode with a persistent switcher
This commit is contained in:
+28
-1
@@ -1517,6 +1517,20 @@
|
|||||||
+ ' <div class="seg" data-key="' + key + '">' + btns + '</div>'
|
+ ' <div class="seg" data-key="' + key + '">' + btns + '</div>'
|
||||||
+ '</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) {
|
function wireSettingsControls(scope) {
|
||||||
scope = scope || document;
|
scope = scope || document;
|
||||||
scope.querySelectorAll('.switch').forEach(function (sw) {
|
scope.querySelectorAll('.switch').forEach(function (sw) {
|
||||||
@@ -1537,7 +1551,7 @@
|
|||||||
setSaveState('change pending');
|
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) {
|
seg.querySelectorAll('button').forEach(function (b) {
|
||||||
b.addEventListener('click', function () {
|
b.addEventListener('click', function () {
|
||||||
seg.querySelectorAll('button').forEach(function (x) { x.setAttribute('aria-current', x === b ? 'true' : 'false'); });
|
seg.querySelectorAll('button').forEach(function (x) { x.setAttribute('aria-current', x === b ? 'true' : 'false'); });
|
||||||
@@ -2053,6 +2067,7 @@
|
|||||||
var preserve = cfg.preserve;
|
var preserve = cfg.preserve;
|
||||||
adminBody.innerHTML =
|
adminBody.innerHTML =
|
||||||
'<div class="admin-settings">'
|
'<div class="admin-settings">'
|
||||||
|
+ themeRow()
|
||||||
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
|
+ 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('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)
|
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
|
||||||
@@ -2067,6 +2082,18 @@
|
|||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
wireSettingsControls(adminBody);
|
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');
|
var saveBtn = document.getElementById('saveBtn');
|
||||||
if (saveBtn) saveBtn.addEventListener('click', saveSettings);
|
if (saveBtn) saveBtn.addEventListener('click', saveSettings);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
<meta name="description" content="A live bird collage from your window.">
|
<meta name="description" content="A live bird collage from your window.">
|
||||||
<link rel="icon" type="image/png" href="./favicon.png">
|
<link rel="icon" type="image/png" href="./favicon.png">
|
||||||
<link rel="apple-touch-icon" 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">
|
<link rel="stylesheet" href="./styles.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="av-local">
|
<body class="av-local">
|
||||||
|
|||||||
@@ -8,12 +8,38 @@
|
|||||||
--ink-soft: #908576;
|
--ink-soft: #908576;
|
||||||
--accent: #4a3f31; /* warm dark grey - matches ink-2, no longer red */
|
--accent: #4a3f31; /* warm dark grey - matches ink-2, no longer red */
|
||||||
--accent-2: #1a1612; /* near-black ink - same hue as the title text */
|
--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 */
|
/* 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: 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);
|
--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);
|
--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);
|
--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; }
|
* { box-sizing: border-box; }
|
||||||
html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }
|
html, body { margin: 0; padding: 0; height: 100%; background: var(--paper); color: var(--ink); }
|
||||||
body {
|
body {
|
||||||
@@ -144,7 +170,7 @@
|
|||||||
raised-paper look; buttons just toggle their text colour. */
|
raised-paper look; buttons just toggle their text colour. */
|
||||||
.seg-pill {
|
.seg-pill {
|
||||||
position: absolute; top: 4px; bottom: 4px; left: 0;
|
position: absolute; top: 4px; bottom: 4px; left: 0;
|
||||||
background: var(--paper); border-radius: 999px;
|
background: var(--pill); border-radius: 999px;
|
||||||
box-shadow: var(--raised);
|
box-shadow: var(--raised);
|
||||||
transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);
|
transition: transform 320ms cubic-bezier(.7,.05,.2,1), width 320ms cubic-bezier(.7,.05,.2,1);
|
||||||
will-change: transform, width;
|
will-change: transform, width;
|
||||||
@@ -280,7 +306,7 @@
|
|||||||
.switch::after {
|
.switch::after {
|
||||||
content: ''; position: absolute; top: 2px; left: 2px;
|
content: ''; position: absolute; top: 2px; left: 2px;
|
||||||
width: 16px; height: 16px;
|
width: 16px; height: 16px;
|
||||||
background: var(--paper); border-radius: 999px;
|
background: var(--pill); border-radius: 999px;
|
||||||
box-shadow: var(--raised);
|
box-shadow: var(--raised);
|
||||||
transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;
|
transition: transform 220ms cubic-bezier(.7,.05,.2,1), background 200ms ease;
|
||||||
}
|
}
|
||||||
@@ -289,7 +315,7 @@
|
|||||||
}
|
}
|
||||||
.switch[aria-checked="true"]::after {
|
.switch[aria-checked="true"]::after {
|
||||||
transform: translateX(18px);
|
transform: translateX(18px);
|
||||||
background: var(--paper);
|
background: var(--pill);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Slider - title row (label + value badge with breathing room) and
|
/* Slider - title row (label + value badge with breathing room) and
|
||||||
@@ -361,7 +387,7 @@
|
|||||||
}
|
}
|
||||||
.seg button[aria-current="true"] {
|
.seg button[aria-current="true"] {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
background: var(--paper);
|
background: var(--pill);
|
||||||
box-shadow: var(--raised);
|
box-shadow: var(--raised);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user