// Dark / light theme, interchangeable with web_template's lt.theme (devdesk#31): // same localStorage key (lt_theme), same data-theme attribute on , the OS // preference while nothing is saved, and tabs follow each other via `storage`. // The inline script in index.template.html applies it before first paint. const KEY = 'lt_theme'; const prefersLight = window.matchMedia('(prefers-color-scheme: light)'); const listeners = new Set(); function saved() { try { const value = localStorage.getItem(KEY); return value === 'light' || value === 'dark' ? value : null; } catch { return null; } } export function getTheme() { return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; } function apply(theme) { const root = document.documentElement; root.setAttribute('data-theme', theme); root.style.colorScheme = theme; document.querySelector('meta[name="theme-color"]')?.setAttribute('content', theme === 'light' ? '#edf0f5' : '#030508'); const next = theme === 'light' ? 'dark' : 'light'; for (const button of document.querySelectorAll('.lt-theme-btn')) { button.textContent = theme === 'light' ? '◐' : '☀'; button.setAttribute('aria-label', `Switch to ${next} mode`); button.title = `Switch to ${next} mode`; } for (const listener of listeners) listener(theme); } export function setTheme(theme) { try { localStorage.setItem(KEY, theme); } catch { // Private mode: the choice lasts until the tab closes. } apply(theme); } export function toggleTheme() { setTheme(getTheme() === 'light' ? 'dark' : 'light'); } export function onThemeChange(listener) { listeners.add(listener); } export function initTheme() { apply(saved() ?? (prefersLight.matches ? 'light' : 'dark')); prefersLight.addEventListener('change', (event) => { if (!saved()) apply(event.matches ? 'light' : 'dark'); }); window.addEventListener('storage', (event) => { if (event.key === KEY) apply(saved() ?? (prefersLight.matches ? 'light' : 'dark')); }); }