// Theme registry. The actual palettes live as CSS variables in src/index.css // (one `[data-theme='-']` block each); this module owns // the list of palettes and modes, how a stored choice resolves to one of those // blocks, and the browser chrome `theme-color`. // // A stored theme id is `-`, e.g. `confetti-system`. The // `system` mode follows the OS light/dark preference live. // // Persistence: the chosen id is stored per user in the backend (settings key // `theme`) so it syncs across devices. localStorage mirrors it only as a // fast-path cache so the inline script in index.html can apply it before the // settings request resolves (avoiding a flash of the default theme). export const PALETTES = [ { id: 'confetti', label: 'Confetti', description: 'Party colors on calm surfaces.', swatch: ['#6a48ee', '#ee4e8b', '#ff8a3d', '#10a593', '#ffc23c', '#3b82f6'], themeColor: { light: '#faf8ff', dark: '#110e1d' }, }, { id: 'meadow', label: 'Meadow', description: 'Soft pastels with a sage accent.', swatch: ['#7fa889', '#e79bb0', '#f1b784', '#a99bdc', '#efd474', '#93badf'], themeColor: { light: '#f7f6ef', dark: '#141612' }, }, ] export const MODES = [ { value: 'system', label: 'Auto' }, { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, ] export const DEFAULT_THEME = 'confetti-system' export const THEME_STORAGE_KEY = 'mykindlebox-theme' // Themes from before the Confetti redesign. Midnight was the only dark one; // everything else maps to the default (which follows the OS). const LEGACY_THEMES = { midnight: 'confetti-dark' } const PALETTE_IDS = new Set(PALETTES.map((p) => p.id)) const MODE_IDS = new Set(MODES.map((m) => m.value)) export function parseTheme(id) { const [palette, mode] = String(LEGACY_THEMES[id] ?? id ?? '').split('-') if (PALETTE_IDS.has(palette) && MODE_IDS.has(mode)) return { palette, mode } return parseTheme(DEFAULT_THEME) } export function composeTheme(palette, mode) { return `${palette}-${mode}` } // Falls back to the default for unknown/missing ids so a stray DB value or an // old cached id can never leave the app unstyled. export function normalizeTheme(id) { const { palette, mode } = parseTheme(id) return composeTheme(palette, mode) } const darkQuery = typeof window !== 'undefined' && window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null let current = DEFAULT_THEME function paint() { const { palette, mode } = parseTheme(current) const resolved = mode === 'system' ? (darkQuery?.matches ? 'dark' : 'light') : mode document.documentElement.dataset.theme = composeTheme(palette, resolved) const meta = document.querySelector('meta[name="theme-color"]') const entry = PALETTES.find((p) => p.id === palette) if (meta && entry) meta.setAttribute('content', entry.themeColor[resolved]) } // An OS light/dark flip repaints only while the choice is `system`. darkQuery?.addEventListener?.('change', () => { if (parseTheme(current).mode === 'system') paint() }) // Applies a theme choice to the document (swapping the CSS variables and the // browser-chrome color) and mirrors it to localStorage for next load's // flash-free start. export function applyTheme(id) { current = normalizeTheme(id) if (typeof document !== 'undefined') paint() try { localStorage.setItem(THEME_STORAGE_KEY, current) } catch { // localStorage can throw in private mode / when disabled — non-fatal, // the backend setting remains the source of truth. } }