// #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; // #294: use the generic variant resolvers - midnight is dark-first // (dark preset = 'midnight', light = 'midnight-light'), so a raw // t.id + '-dark' build landed on nonexistent/forced-dark presets. document.documentElement.dataset.preset = dark ? darkPreset(t.id) : lightPreset(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); })();