topbar.js binds click handlers to every .dark-toggle, including the settings one; settings.js then also wired a delegation that re-clicked the topbar button, so each click flipped dark mode twice (net no-op). Mark buttons as wired in topbar.js and only add a fallback handler in settings.js when topbar.js did not run. CSS: center the toggle horizontally under the Settings title with auto margins, matching .settings-body padding. Closes #197
106 lines
4.4 KiB
JavaScript
106 lines
4.4 KiB
JavaScript
// #112/#127/#139: theme switcher. Swatch colors are derived from computed CSS vars per preset.
|
|
(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.
|
|
// #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' }
|
|
];
|
|
var SWATCH_VARS = ['--bg', '--surface', '--surface-2', '--muted', '--accent'];
|
|
|
|
function presetColors(id) {
|
|
var root = document.documentElement;
|
|
var prev = root.getAttribute('data-preset');
|
|
root.setAttribute('data-preset', id);
|
|
var cs = getComputedStyle(root);
|
|
var colors = SWATCH_VARS.map(function (v) { return cs.getPropertyValue(v).trim(); });
|
|
if (prev === null) root.removeAttribute('data-preset'); else root.setAttribute('data-preset', prev);
|
|
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 cards = {};
|
|
// #132: midnight is dark-first (root preset = midnight = dark; its light
|
|
// variant is midnight-light), the others are light-first. Resolve the
|
|
// LIGHT and DARK preset ids generically so the light swatches always
|
|
// render in the top row of every card.
|
|
function lightPreset(id) {
|
|
if (id === 'midnight') return 'midnight-light';
|
|
return id; // light-first bases use themselves as the light variant
|
|
}
|
|
function darkPreset(id) {
|
|
if (id === 'midnight') return 'midnight'; // root preset is midnight's dark
|
|
return id + '-dark';
|
|
}
|
|
pairs.forEach(function (t) {
|
|
var light = presetColors(lightPreset(t.id));
|
|
var dark = presetColors(darkPreset(t.id));
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'theme-card';
|
|
btn.setAttribute('aria-pressed', 'false');
|
|
btn.setAttribute('data-pair', t.id);
|
|
// #139: build the swatches via DOM APIs (CSSOM styles) instead of
|
|
// innerHTML strings: re-parsing serialized inline styles would trip
|
|
// style-src 'self'.
|
|
var label = document.createElement('strong');
|
|
label.textContent = t.name;
|
|
btn.appendChild(label);
|
|
[light, dark].forEach(function (colors) {
|
|
var row = document.createElement('span');
|
|
row.className = 'swatches';
|
|
colors.forEach(function (c) {
|
|
var sw = document.createElement('span');
|
|
sw.className = 'swatch';
|
|
sw.style.backgroundColor = c;
|
|
row.appendChild(sw);
|
|
});
|
|
btn.appendChild(row);
|
|
});
|
|
btn.addEventListener('click', function () {
|
|
var dark = state().dark;
|
|
document.documentElement.dataset.preset = dark ? t.id + '-dark' : t.id;
|
|
try { localStorage.setItem('palette-theme', t.id); } catch (e) {}
|
|
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();
|
|
// topbar.js already binds every .dark-toggle (deferred, so the settings
|
|
// button exists by then). Only wire here if it somehow did not run,
|
|
// otherwise the button would toggle twice per click and never change (#197).
|
|
var dt = document.getElementById('settings-dark-toggle');
|
|
if (dt && !dt.dataset.darkWired) {
|
|
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);
|
|
})();
|