import type { Ref } from 'vue' import { usePreferredDark } from '@vueuse/core' import { computed, ref } from 'vue' // Type for a theme object export interface DaisyThemeMeta { theme: string cssVars?: string name?: string [key: string]: any } export type DaisyThemeInput = string | DaisyThemeMeta interface DaisyThemeOptions { themes: DaisyThemeInput[] defaultTheme?: string } function normalizeTheme(input: DaisyThemeInput): DaisyThemeMeta { if (typeof input === 'string') { return { theme: input } } return { ...input } } // Nuxt auto-imports useState globally; this declaration satisfies the // TypeScript compiler when the file is type-checked outside Nuxt. declare const useState: ((...args: any[]) => any) | undefined // Shared state factory — uses Nuxt useState when available, falls back to // module-level refs for standalone Vue usage. Nuxt's useState ensures // SSR-to-client hydration and per-request isolation on the server. const sharedRefs = new Map>() function useSharedState(key: string, init: () => T): Ref { // Try Nuxt's useState (auto-imported in Nuxt context) if (typeof useState === 'function') { try { return (useState as any)(key, init) as Ref } catch { // Not in Nuxt runtime — fall through to ref-based approach } } // Fallback for standalone Vue: module-level shared refs if (!sharedRefs.has(key)) { sharedRefs.set(key, ref(init()) as Ref) } return sharedRefs.get(key) as Ref } // Holds the canonical theme ref (cookie-backed or shared state) // so all callers of useDaisyTheme() read/write the same ref. let themeRef: Ref | null = null /** * useDaisyTheme composable * @param storage Optional. Ref factory for persistence (e.g., useCookie, useLocalStorage, or ref). Defaults to ref. * @param options Optional. Theme options (themes, defaultTheme, etc.). * * Calling with arguments (in app.vue/root): initializes global state. * Calling with no arguments (in any component): reuses global state. */ export function useDaisyTheme(storage?: (key: string, initial: T) => Ref, options?: DaisyThemeOptions) { // Shared themes list — SSR-safe via useState in Nuxt, shared ref otherwise const themes = useSharedState('daisy-themes', () => options?.themes?.map(normalizeTheme) ?? []) // If options provided, update themes (covers the app.vue initialization call) if (options?.themes) { themes.value = options.themes.map(normalizeTheme) } // Theme name — all callers must share the same ref. // When storage is provided (app.vue), it creates the canonical ref (e.g. cookie-backed). // Subsequent callers without storage reuse that same ref. let theme: Ref if (storage) { theme = storage('theme', options?.defaultTheme ?? themes.value[0]?.theme ?? 'light') themeRef = theme } else if (themeRef) { theme = themeRef } else { theme = useSharedState('daisy-theme', () => options?.defaultTheme ?? themes.value[0]?.theme ?? 'light') themeRef = theme } // System dark mode const preferredDark = usePreferredDark() // Compute the effective theme for UI/DOM const effectiveTheme = computed(() => { if (theme.value === 'system') { return preferredDark.value ? 'dark' : 'light' } return theme.value }) // Set theme by name function setTheme(name: string) { if (themes.value.some(t => t.theme === name) || name === 'system') { theme.value = name } } // Cycle to next theme function cycleTheme() { const names = themes.value.map(t => t.theme) if (!names.length) { return // Guard: no themes } const idx = names.indexOf(theme.value) const nextIdx = (idx + 1) % names.length // Only set if defined (TypeScript safety) if (typeof names[nextIdx] === 'string') { setTheme(names[nextIdx]!) } } // Register a new theme function registerTheme(newTheme: DaisyThemeInput) { const meta = normalizeTheme(newTheme) if (!themes.value.some(t => t.theme === meta.theme)) { themes.value.push(meta) } } // Remove a theme by name function removeTheme(name: string) { const idx = themes.value.findIndex(t => t.theme === name) if (idx !== -1) { themes.value.splice(idx, 1) // If current theme was removed, fallback to first if (theme.value === name) { theme.value = themes.value[0]?.theme ?? 'light' } } } // Get the current theme object const themeInfo = computed(() => themes.value.find(t => t.theme === theme.value)) return { themes, theme, effectiveTheme, themeInfo, setTheme, cycleTheme, registerTheme, removeTheme, } }