import { computed, ref, watch, nextTick } from 'vue' const STORAGE_KEY = 'voicehub:theme' type ThemeName = string & {} const currentTheme = ref(null) const initialized = ref(false) function isBrowser() { return typeof window !== 'undefined' && typeof document !== 'undefined' } function getMetaThemeColor() { if (!isBrowser()) return null return document.querySelector('meta[name="theme-color"]') } function parseEnabledThemesRaw(raw: unknown): string[] { if (Array.isArray(raw)) return raw.filter(Boolean) as string[] if (typeof raw === 'string') { const s = raw.trim() if (!s) return [] if (s.startsWith('[') || s.startsWith('{')) { try { const parsed = JSON.parse(s) if (Array.isArray(parsed)) return parsed.filter(Boolean) as string[] if (parsed && Array.isArray((parsed as any).items)) return (parsed as any).items.filter(Boolean) as string[] } catch (_) { // fallback } } return s.split(',').map((x) => x.trim()).filter(Boolean) } return [] } function applyTheme(theme: string) { if (!isBrowser()) return const root = document.documentElement const appRoot = document.querySelector('div.app') root.setAttribute('data-theme', theme) if (appRoot) appRoot.setAttribute('data-theme', theme) const effective = resolveEffectiveTheme(theme) if (effective === 'dark') { root.classList.add('dark') root.classList.remove('light') root.style.colorScheme = 'dark' } else if (effective === 'light') { root.classList.add('light') root.classList.remove('dark') root.style.colorScheme = 'light' } else { root.classList.remove('dark', 'light') root.style.colorScheme = 'light dark' } const meta = getMetaThemeColor() if (meta) { meta.content = effective === 'light' ? '#f8fafc' : '#111111' } try { localStorage.setItem(STORAGE_KEY, theme) } catch { /* ignore storage errors */ } } function detectSystemTheme(): 'dark' | 'light' { if (!isBrowser()) return 'dark' const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches return prefersDark ? 'dark' : 'light' } function resolveEffectiveTheme(theme: string): 'dark' | 'light' | string { if (theme === 'System' || theme === 'system') { return detectSystemTheme() } if (theme === 'dark' || theme.endsWith('Dark')) return 'dark' if (theme === 'light' || theme.endsWith('Light')) return 'light' return theme } export function useTheme() { const config = useRuntimeConfig() const { isLoaded: siteConfigLoaded, defaultTheme: siteDefaultTheme, enabledThemes: siteEnabledThemes, initSiteConfig } = useSiteConfig() const enabledThemes = computed(() => { if (siteConfigLoaded.value && siteEnabledThemes.value.length > 0) { return [...new Set(siteEnabledThemes.value)] } const fromRuntime = parseEnabledThemesRaw(config.public?.enabledThemes) if (fromRuntime.length > 0) return fromRuntime return ['System', 'dark', 'light'] }) const defaultTheme = computed(() => { if (siteConfigLoaded.value && siteDefaultTheme.value) { return siteDefaultTheme.value } const fromRuntime = config.public?.defaultTheme if (typeof fromRuntime === 'string' && fromRuntime) { return fromRuntime } return 'System' }) const theme = computed({ get: () => currentTheme.value ?? defaultTheme.value, set: (next: string) => setTheme(next) }) const isDark = computed(() => { return resolveEffectiveTheme(theme.value) === 'dark' }) function isThemeEnabled(name: string) { return enabledThemes.value.includes(name) } function firstValidTheme(candidate: string): string { const list = enabledThemes.value if (list.includes(candidate)) return candidate if (list.includes(defaultTheme.value)) return defaultTheme.value return list[0] ?? 'System' } function setTheme(next: string, { persist = true } = {}) { const candidate = firstValidTheme(next) if (currentTheme.value === candidate && initialized.value) return currentTheme.value = candidate applyTheme(candidate) if (!persist && isBrowser()) { try { localStorage.removeItem(STORAGE_KEY) } catch { /* ignore */ } } } function toggleTheme() { const order = enabledThemes.value.length >= 2 ? enabledThemes.value : ['System', 'dark', 'light'] const current = theme.value const idx = order.indexOf(current) const next = order[(idx + 1) % order.length] setTheme(next) } function resetToDefault() { setTheme(defaultTheme.value, { persist: false }) } function onSystemThemeChange() { const stored = (() => { try { return localStorage.getItem(STORAGE_KEY) } catch { return null } })() if (!stored) { applyTheme(theme.value) } } async function init() { if (initialized.value || !isBrowser()) return if (!siteConfigLoaded.value) { try { await initSiteConfig() await nextTick() } catch (_) { /* ignore */ } } initialized.value = true let stored: string | null = null try { stored = localStorage.getItem(STORAGE_KEY) } catch { stored = null } if (stored) { const valid = firstValidTheme(stored) if (valid === stored || enabledThemes.value.includes(stored)) { setTheme(stored) watchSystemTheme() return } } setTheme(defaultTheme.value, { persist: false }) watchSystemTheme() } function watchSystemTheme() { if (!isBrowser() || !window.matchMedia) return const mql = window.matchMedia('(prefers-color-scheme: dark)') if (typeof mql.addEventListener === 'function') { mql.addEventListener('change', onSystemThemeChange) } else if (typeof (mql as any).addListener === 'function') { ;(mql as any).addListener(onSystemThemeChange) } } watch(defaultTheme, () => { if (!initialized.value) return const stored = (() => { try { return localStorage.getItem(STORAGE_KEY) } catch { return null } })() if (!stored) { setTheme(defaultTheme.value, { persist: false }) } }) return { theme, currentTheme, defaultTheme, enabledThemes, isDark, isThemeEnabled, setTheme, toggleTheme, resetToDefault, init } }