Spaces:
Running
Running
| /* ============================================================ | |
| GLASSGRID — THEME ENGINE | |
| Handles theme loading, switching, token overrides, | |
| and version rollback. Zero UI modification. | |
| ============================================================ */ | |
| ; | |
| 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(); | |