Holded_Blog / lib /theme-engine.js
Shinhati2023's picture
Create lib/theme-engine.js
138e8b6 verified
/* ============================================================
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();