// Theming: a small map of palette-var overrides persisted in app_settings and // applied to :root on boot. The whole UI is CSS-custom-property driven, so // setting these vars recolours everything live. (Canvas-drawn colours — the // blackflame card — and a few inline rgba() literals don't follow the theme.) import { api } from './api.js'; export const THEME_VARS = [ { key: 'accent', css: '--accent', label: 'Accent (flame)' }, { key: 'accent-dim', css: '--accent-dim', label: 'Accent · dim' }, { key: 'accent-soft', css: '--accent-soft', label: 'Accent · soft' }, { key: 'bg', css: '--bg', label: 'Background' }, { key: 'panel', css: '--panel', label: 'Panel' }, { key: 'panel-2', css: '--panel-2', label: 'Panel · raised' }, { key: 'border', css: '--border', label: 'Border' }, { key: 'text', css: '--text', label: 'Text' }, { key: 'muted', css: '--muted', label: 'Muted text' }, { key: 'ok', css: '--ok', label: 'OK / good' }, { key: 'warn', css: '--warn', label: 'Warning' }, { key: 'bad', css: '--bad', label: 'Bad / error' } ]; const BY_KEY = Object.fromEntries(THEME_VARS.map(v => [v.key, v])); // Named alternates. Blackflame = {} (clear overrides → CSS defaults). export const PRESETS = { Blackflame: {}, Ember: { accent: '#ff7a1a', 'accent-dim': '#8a3a10', 'accent-soft': '#3a1a0a', bg: '#0c0907', panel: '#171008', 'panel-2': '#20160c' }, Frost: { accent: '#4aa3ff', 'accent-dim': '#1e5a8a', 'accent-soft': '#0e2230', bg: '#070a0e', panel: '#0f141c', 'panel-2': '#161d28', ok: '#5fb0c4' }, Verdant: { accent: '#5fc46a', 'accent-dim': '#2a6a30', 'accent-soft': '#10240f', bg: '#070b08', panel: '#0f160f', 'panel-2': '#161f16' }, Amethyst: { accent: '#a86adf', 'accent-dim': '#5a2e8a', 'accent-soft': '#1e1030', bg: '#0a0810', panel: '#140f1c', 'panel-2': '#1c1528' } }; export function applyTheme(vars = {}) { const root = document.documentElement; for (const [k, val] of Object.entries(vars)) { const def = BY_KEY[k]; if (def && val) root.style.setProperty(def.css, val); } } export function clearTheme() { const root = document.documentElement; for (const v of THEME_VARS) root.style.removeProperty(v.css); } // Current effective value of a var (override or CSS default), normalised to #rrggbb. export function effectiveHex(key) { const def = BY_KEY[key]; if (!def) return '#000000'; const raw = getComputedStyle(document.documentElement).getPropertyValue(def.css).trim(); return toHex6(raw) || '#000000'; } export function toHex6(v) { if (!v) return ''; v = v.trim(); if (/^#[0-9a-fA-F]{6}$/.test(v)) return v.toLowerCase(); if (/^#[0-9a-fA-F]{8}$/.test(v)) return v.slice(0, 7).toLowerCase(); // drop alpha if (/^#[0-9a-fA-F]{3}$/.test(v)) return '#' + v.slice(1).split('').map(c => c + c).join('').toLowerCase(); const m = v.match(/rgba?\(\s*(\d+)\D+(\d+)\D+(\d+)/i); if (m) return '#' + [m[1], m[2], m[3]].map(n => (+n).toString(16).padStart(2, '0')).join(''); return ''; } let current = {}; export function currentTheme() { return { ...current }; } export async function loadTheme() { try { current = (await api.get('/api/theme')) || {}; applyTheme(current); } catch { /* defaults */ } return current; } export async function saveTheme(vars) { current = (await api.put('/api/theme', vars)) || {}; clearTheme(); applyTheme(current); return current; }