/* ============================================================ GLASSGRID — THEME ENGINE Handles theme loading, switching, token overrides, and version rollback. Zero UI modification. ============================================================ */ 'use strict'; const STORAGE_KEY_THEME = 'gg_active_theme'; const STORAGE_KEY_HISTORY = 'gg_theme_history'; const STORAGE_KEY_OVERRIDES = 'gg_token_overrides'; class ThemeEngine { constructor() { this.activeTheme = 'midnight'; this.themeHistory = []; this.overrides = {}; this._styleEl = null; this._overrideEl = null; } /** Initialize theme engine — call on app start */ async init() { this._injectOverrideStyleEl(); this.themeHistory = this._loadHistory(); this.overrides = this._loadOverrides(); const savedTheme = localStorage.getItem(STORAGE_KEY_THEME) || 'midnight'; await this.applyTheme(savedTheme, false); this._applyTokenOverrides(this.overrides); } /** Switch to a named theme */ async applyTheme(themeId, pushHistory = true) { const themes = await this._loadThemeRegistry(); const theme = themes.find(t => t.id === themeId); if (!theme) { console.warn(`[ThemeEngine] Unknown theme: ${themeId}`); return; } document.documentElement.dataset.theme = themeId; this.activeTheme = themeId; localStorage.setItem(STORAGE_KEY_THEME, themeId); if (pushHistory) { this.themeHistory = [themeId, ...this.themeHistory.filter(t => t !== themeId)].slice(0, 10); this._saveHistory(this.themeHistory); } document.dispatchEvent(new CustomEvent('gg:theme:changed', { detail: { themeId } })); } /** Roll back to previous theme */ rollback() { const [_current, previous, ...rest] = this.themeHistory; if (!previous) { console.warn('[ThemeEngine] No previous theme to roll back to.'); return; } this.themeHistory = [previous, _current, ...rest]; this._saveHistory(this.themeHistory); this.applyTheme(previous, false); document.dispatchEvent(new CustomEvent('gg:theme:rollback', { detail: { themeId: previous } })); } /** Override a single CSS token */ setToken(tokenName, value) { if (!tokenName.startsWith('--')) tokenName = `--${tokenName}`; this.overrides[tokenName] = value; this._applyTokenOverrides(this.overrides); this._saveOverrides(this.overrides); } /** Override multiple tokens at once */ setTokens(tokenMap) { for (const [key, value] of Object.entries(tokenMap)) { const name = key.startsWith('--') ? key : `--${key}`; this.overrides[name] = value; } this._applyTokenOverrides(this.overrides); this._saveOverrides(this.overrides); } /** Remove a single token override (reverts to theme default) */ removeToken(tokenName) { if (!tokenName.startsWith('--')) tokenName = `--${tokenName}`; delete this.overrides[tokenName]; this._applyTokenOverrides(this.overrides); this._saveOverrides(this.overrides); } /** Reset all token overrides */ resetTokens() { this.overrides = {}; this._overrideEl.textContent = ''; this._saveOverrides({}); } /** Get all token overrides */ getOverrides() { return { ...this.overrides }; } /** Get theme history */ getHistory() { return [...this.themeHistory]; } // ── Private ────────────────────────── _injectOverrideStyleEl() { this._overrideEl = document.createElement('style'); this._overrideEl.id = 'gg-token-overrides'; document.head.appendChild(this._overrideEl); } _applyTokenOverrides(overrides) { if (!this._overrideEl) return; const rules = Object.entries(overrides) .map(([k, v]) => ` ${k}: ${v};`) .join('\n'); this._overrideEl.textContent = rules.length ? `:root {\n${rules}\n}` : ''; } async _loadThemeRegistry() { try { const res = await fetch('./data/themes.json'); const data = await res.json(); return data.themes || []; } catch { return [ { id: 'midnight', name: 'Midnight' }, { id: 'aurora', name: 'Aurora' }, { id: 'ember', name: 'Ember' }, { id: 'ocean', name: 'Ocean' }, ]; } } _loadHistory() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_HISTORY) || '["midnight"]'); } catch { return ['midnight']; } } _saveHistory(history) { localStorage.setItem(STORAGE_KEY_HISTORY, JSON.stringify(history)); } _loadOverrides() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_OVERRIDES) || '{}'); } catch { return {}; } } _saveOverrides(overrides) { localStorage.setItem(STORAGE_KEY_OVERRIDES, JSON.stringify(overrides)); } } export const themeEngine = new ThemeEngine();