Theme pairs with dark mode toggle, gear active state, PALETTE_DEFAULT_DARK (#127)
- Collapse the 10 theme cards into 5 pairs: light swatches top row, dark bottom row - Dark mode toggle in settings and topbar (sun/moon inline SVG) - Toggling dark mode switches to the other variant of the selected pair - Persist pair id in localStorage palette-theme, dark flag in palette-dark - Head script resolves base+dark to variant; URL ?theme= accepts both ids and wins - Settings gear highlights like nav tabs on /settings - PALETTE_DEFAULT_DARK env var sets server default dark state (default on) - Server-side resolution helpers + tests
This commit is contained in:
@@ -8,6 +8,11 @@
|
||||
<div class="settings-body">
|
||||
<h3>Theme</h3>
|
||||
<div class="theme-grid" id="theme-grid"></div>
|
||||
<button class="iconbtn dark-toggle" id="settings-dark-toggle" type="button" title="Toggle dark mode" aria-label="Toggle dark mode">
|
||||
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
<span>Dark mode</span>
|
||||
</button>
|
||||
<p class="admin-link-row"><a class="admin-link" href="/admin">Admin</a></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,17 +21,13 @@
|
||||
(function () {
|
||||
// #112: derive each preset's swatches from the real CSS variables in
|
||||
// app.css by temporarily applying data-preset, so they can never drift.
|
||||
var themeNames = [
|
||||
// #127: 5 theme pairs, light swatches top row, dark bottom row.
|
||||
var pairs = [
|
||||
{ id: 'midnight', name: 'Midnight' },
|
||||
{ id: 'smooth', name: 'Smooth' },
|
||||
{ id: 'pastel-lavender', name: 'Pastel Lavender' },
|
||||
{ id: 'pastel-peach', name: 'Pastel Peach' },
|
||||
{ id: 'pastel-cloud', name: 'Pastel Cloud' },
|
||||
{ id: 'midnight-light', name: 'Midnight Light' },
|
||||
{ id: 'smooth-dark', name: 'Smooth Dark' },
|
||||
{ id: 'pastel-lavender-dark', name: 'Pastel Lavender Dark' },
|
||||
{ id: 'pastel-peach-dark', name: 'Pastel Peach Dark' },
|
||||
{ id: 'pastel-cloud-dark', name: 'Pastel Cloud Dark' }
|
||||
{ id: 'pastel-cloud', name: 'Pastel Cloud' }
|
||||
];
|
||||
var SWATCH_VARS = ['--bg', '--surface', '--surface-2', '--muted', '--accent'];
|
||||
|
||||
@@ -40,26 +41,60 @@
|
||||
return colors;
|
||||
}
|
||||
|
||||
// current base pair + dark flag from the resolved data-preset.
|
||||
// "midnight" is itself the dark variant, so check dark ids first.
|
||||
function state() {
|
||||
var p = document.documentElement.dataset.preset || 'midnight';
|
||||
if (p === 'midnight' || /-dark$/.test(p)) {
|
||||
return { base: p === 'midnight' ? 'midnight' : p.replace(/-dark$/, ''), dark: true };
|
||||
}
|
||||
return { base: p === 'midnight-light' ? 'midnight' : p, dark: false };
|
||||
}
|
||||
|
||||
var grid = document.getElementById('theme-grid');
|
||||
var current = document.documentElement.dataset.preset || 'midnight';
|
||||
themeNames.forEach(function (t) {
|
||||
var colors = presetColors(t.id);
|
||||
var cards = {};
|
||||
pairs.forEach(function (t) {
|
||||
var light = presetColors(t.id);
|
||||
var dark = presetColors(t.id + '-dark');
|
||||
var btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'theme-card';
|
||||
btn.setAttribute('aria-pressed', current === t.id ? 'true' : 'false');
|
||||
btn.setAttribute('aria-pressed', 'false');
|
||||
btn.setAttribute('data-pair', t.id);
|
||||
btn.innerHTML = '<strong>' + t.name + '</strong>' +
|
||||
'<span class="swatches">' + colors.map(function (c) {
|
||||
'<span class="swatches">' + light.map(function (c) {
|
||||
return '<span class="swatch" style="background:' + c + '"></span>';
|
||||
}).join('') + '</span>' +
|
||||
'<span class="swatches">' + dark.map(function (c) {
|
||||
return '<span class="swatch" style="background:' + c + '"></span>';
|
||||
}).join('') + '</span>';
|
||||
btn.addEventListener('click', function () {
|
||||
document.documentElement.dataset.preset = t.id;
|
||||
var dark = state().dark;
|
||||
document.documentElement.dataset.preset = dark ? t.id + '-dark' : t.id;
|
||||
try { localStorage.setItem('palette-theme', t.id); } catch (e) {}
|
||||
grid.querySelectorAll('.theme-card').forEach(function (c) { c.setAttribute('aria-pressed', 'false'); });
|
||||
Object.keys(cards).forEach(function (k) { cards[k].setAttribute('aria-pressed', 'false'); });
|
||||
btn.setAttribute('aria-pressed', 'true');
|
||||
});
|
||||
cards[t.id] = btn;
|
||||
grid.appendChild(btn);
|
||||
});
|
||||
|
||||
function sync() {
|
||||
var s = state();
|
||||
Object.keys(cards).forEach(function (k) {
|
||||
cards[k].setAttribute('aria-pressed', k === s.base ? 'true' : 'false');
|
||||
});
|
||||
var dt = document.getElementById('settings-dark-toggle');
|
||||
if (dt) dt.setAttribute('aria-pressed', s.dark ? 'true' : 'false');
|
||||
}
|
||||
sync();
|
||||
// the topbar script runs before this button exists, so wire it here
|
||||
var dt = document.getElementById('settings-dark-toggle');
|
||||
dt.addEventListener('click', function () {
|
||||
var btns = document.querySelectorAll('.topbar .dark-toggle');
|
||||
if (btns.length) btns[0].click(); else document.dispatchEvent(new CustomEvent('palette-darkchange'));
|
||||
});
|
||||
document.addEventListener('palette-darkchange', sync);
|
||||
})();
|
||||
</script>
|
||||
{{template "foot" .}}
|
||||
|
||||
Reference in New Issue
Block a user