import { computed, ref, watch } 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 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) if (theme === 'dark') { root.classList.add('dark') root.classList.remove('light') root.style.colorScheme = 'dark' } else if (theme === '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) { if (theme === 'light') { meta.content = '#f8fafc' } else { meta.content = '#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' } export function useTheme() { const config = useRuntimeConfig() const enabledThemes = computed(() => { const list = (config.public?.enabledThemes as unknown) as string[] | undefined return Array.isArray(list) && list.length > 0 ? list : ['dark', 'light'] }) const defaultTheme = computed(() => { const value = config.public?.defaultTheme as string | undefined if (value && enabledThemes.value.includes(value)) return value return enabledThemes.value.includes('dark') ? 'dark' : enabledThemes.value[0] || 'dark' }) const theme = computed({ get: () => currentTheme.value ?? defaultTheme.value, set: (next: string) => setTheme(next) }) const isDark = computed(() => { const t = theme.value if (t === 'dark') return true if (t === 'light') return false return detectSystemTheme() === 'dark' }) function isThemeEnabled(name: string) { return enabledThemes.value.includes(name) } function setTheme(next: string, { persist = true } = {}) { let candidate = next if (!enabledThemes.value.includes(candidate)) { candidate = defaultTheme.value } 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 : ['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 init() { if (initialized.value || !isBrowser()) return initialized.value = true let stored: string | null = null try { stored = localStorage.getItem(STORAGE_KEY) } catch { stored = null } if (stored && enabledThemes.value.includes(stored)) { setTheme(stored) return } setTheme(defaultTheme.value, { persist: false }) if (window.matchMedia) { const mql = window.matchMedia('(prefers-color-scheme: dark)') const onChange = () => { let s: string | null = null try { s = localStorage.getItem(STORAGE_KEY) } catch { s = null } if (!s) { setTheme(defaultTheme.value, { persist: false }) } } if (typeof mql.addEventListener === 'function') { mql.addEventListener('change', onChange) } else if (typeof mql.addListener === 'function') { mql.addListener(onChange) } } } return { theme, currentTheme, defaultTheme, enabledThemes, isDark, isThemeEnabled, setTheme, toggleTheme, resetToDefault, init } }