/** Numeric VITE_UI_THEME index → semantic data-theme id (UI layer only; 3D unchanged). */ export const THEME_MAP = { '1': 'obsidian_slate', '2': 'amber_charcoal', '3': 'nordic_forest', '4': 'titanium_oxide', '5': 'midnight_ink', '6': 'basalt_copper', '7': 'swiss_brutalist', '8': 'ibm_as400', '9': 'amiga_os13', '10': 'blueprint_heliography', }; /** Human-readable labels (design-system/README.md catalog). */ export const THEME_LABELS = { '1': 'Obsidian Slate', '2': 'Amber & Warm Charcoal', '3': 'Nordic Forest', '4': 'Titanium Oxide', '5': 'Midnight Ink', '6': 'Basalt & Raw Copper', '7': 'Swiss Brutalist', '8': 'IBM AS/400 Terminal', '9': 'Amiga Workbench 1.3', '10': 'Blue-Line Blueprint', }; export const THEME_STORAGE_KEY = 'ui_theme_index'; const DEFAULT_THEME_INDEX = 1; const MIN_THEME_INDEX = 1; const MAX_THEME_INDEX = 10; /** @type {number} */ let currentThemeIndex = DEFAULT_THEME_INDEX; /** * @param {string | number | undefined | null} rawIndex * @returns {number} */ export function normalizeThemeIndex(rawIndex) { const n = Number.parseInt(String(rawIndex ?? '').trim(), 10); if (!Number.isFinite(n) || n < MIN_THEME_INDEX || n > MAX_THEME_INDEX) { return DEFAULT_THEME_INDEX; } return n; } /** * Resolve env index to a theme id. Invalid or missing values fall back to obsidian_slate. * @param {string | number | undefined | null} rawIndex * @returns {string} */ export function resolveThemeName(rawIndex) { const key = String(normalizeThemeIndex(rawIndex)); return THEME_MAP[key] ?? THEME_MAP[String(DEFAULT_THEME_INDEX)]; } /** * @returns {number | null} */ export function readStoredThemeIndex() { if (typeof localStorage === 'undefined') return null; try { const raw = localStorage.getItem(THEME_STORAGE_KEY); if (raw == null || raw === '') return null; const n = Number.parseInt(raw, 10); if (!Number.isFinite(n) || n < MIN_THEME_INDEX || n > MAX_THEME_INDEX) return null; return n; } catch { return null; } } /** * Boot priority: localStorage → VITE_UI_THEME → default 1. * @returns {number} */ export function resolveInitialThemeIndex() { const stored = readStoredThemeIndex(); if (stored != null) return stored; const fromEnv = import.meta.env.VITE_UI_THEME; if (fromEnv != null && String(fromEnv).trim() !== '') { return normalizeThemeIndex(fromEnv); } return DEFAULT_THEME_INDEX; } /** * @param {number} index * @param {{ persist?: boolean }} [options] * @returns {{ index: number, id: string, label: string }} */ export function applyUiThemeByIndex(index, options = {}) { const { persist = true } = options; const normalized = normalizeThemeIndex(index); const id = resolveThemeName(normalized); const label = THEME_LABELS[String(normalized)] ?? THEME_LABELS[String(DEFAULT_THEME_INDEX)]; currentThemeIndex = normalized; if (typeof document !== 'undefined' && document.body) { document.body.setAttribute('data-theme', id); } if (persist && typeof localStorage !== 'undefined') { try { localStorage.setItem(THEME_STORAGE_KEY, String(normalized)); } catch { /* quota / private mode — theme still applied for this session */ } } return { index: normalized, id, label }; } /** Apply saved/env/default theme at boot without overwriting localStorage. */ export function bootstrapUiTheme() { return applyUiThemeByIndex(resolveInitialThemeIndex(), { persist: false }); } /** * Advance 1→10→1 and persist. * @returns {{ index: number, id: string, label: string }} */ export function cycleUiTheme() { const next = currentThemeIndex >= MAX_THEME_INDEX ? MIN_THEME_INDEX : currentThemeIndex + 1; return applyUiThemeByIndex(next, { persist: true }); } /** * @returns {{ index: number, id: string, label: string }} */ export function getUiThemeSummary() { const normalized = normalizeThemeIndex(currentThemeIndex); return { index: normalized, id: resolveThemeName(normalized), label: THEME_LABELS[String(normalized)] ?? THEME_LABELS[String(DEFAULT_THEME_INDEX)], }; } /** * Read VITE_UI_THEME and set body[data-theme]. Safe in SSR/test when document is absent. * @param {string | number | undefined | null} [rawIndex] * @returns {string} Applied theme id */ export function applyUiThemeFromEnv(rawIndex = import.meta.env.VITE_UI_THEME) { return applyUiThemeByIndex(rawIndex, { persist: false }).id; }