Spaces:
Sleeping
Sleeping
| /** 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; | |
| } | |