// composables/useTheme.ts import { ref, computed, watch } from 'vue' export interface ThemeOption { value: string class: string icon: string label: string isDark?: boolean } const themeOptions = ref([ { value: 'dark', class: 'bgl-dark-mode', icon: 'cloudy_night', label: 'Dark Mode', isDark: true, }, { value: 'light', class: 'bgl-light-mode', icon: 'brightness_7', label: 'Light Mode', isDark: false, }, { value: 'system', class: 'system', icon: 'settings_input_component', label: 'System Default', }, ]) const STORAGE_KEY = 'color-mode' const colorMode = ref('system') const isDark = ref(false) /** * Whether dark mode is enabled for this app. Defaults to `false` (light only), * so dark mode is opt-in per project. Call `configureTheme({ allowDark: true })` * once at app startup to enable it. When `false`, any dark/system-dark * preference falls back to light and `bgl-dark-mode` is never applied. */ const allowDark = ref(false) const isBrowser = typeof window !== 'undefined' function getSystemPrefersDark() { return allowDark.value && isBrowser && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches } function findTheme(value: string) { return themeOptions.value.find(t => t.value === value) } function applyTheme(themeValue: string) { if (!isBrowser) return const root = document.documentElement // Remove all theme classes themeOptions.value.forEach((t) => { if (t.class !== 'system') { root.classList.remove(t.class) } }) // Handle system theme if (themeValue === 'system') { const systemIsDark = getSystemPrefersDark() isDark.value = systemIsDark const darkTheme = themeOptions.value.find(t => t.value === 'dark') const lightTheme = themeOptions.value.find(t => t.value === 'light') const themeToApply = systemIsDark ? darkTheme : lightTheme if (themeToApply) { root.classList.add(themeToApply.class) } } else { // Apply the selected theme. If dark mode is disabled for this app, force // any dark selection back to light so projects can opt out entirely. const theme = findTheme(themeValue) const effectiveTheme = theme && theme.isDark && !allowDark.value ? findTheme('light') : theme if (effectiveTheme) { root.classList.add(effectiveTheme.class) isDark.value = effectiveTheme.isDark ?? false } } } function toggleTheme() { // Get all non-system themes const cyclableThemes = themeOptions.value.filter(t => t.value !== 'system') if (cyclableThemes.length === 0) { return } const currentIndex = cyclableThemes.findIndex(t => t.value === colorMode.value) const nextIndex = (currentIndex + 1) % cyclableThemes.length colorMode.value = cyclableThemes[nextIndex].value } /** * Configure theme behavior for the current app. Call once at startup. * * @param options.allowDark Enable dark mode for this project (default: false). * When false, dark / system-dark falls back to light. */ export function configureTheme(options: { allowDark?: boolean } = {}) { if (options.allowDark !== undefined) { allowDark.value = options.allowDark } // Re-apply with the new policy so a previously-saved dark choice is corrected. applyTheme(colorMode.value) } function addTheme(theme: ThemeOption) { // Check if theme with this value already exists const exists = themeOptions.value.some(t => t.value === theme.value) if (!exists) { // Add before 'system' option const systemIndex = themeOptions.value.findIndex(t => t.value === 'system') if (systemIndex > -1) { themeOptions.value.splice(systemIndex, 0, theme) } else { themeOptions.value.push(theme) } } } /** * Provides a way to manage the theme of the application. * * The theme is stored in localStorage and can be toggled between 'dark', 'light', and 'system'. * The theme is applied to the document root's classList when the theme is active. * the classes are 'bgl-dark-mode', 'bgl-light-mode', and 'system'. * * Example usage: * const { theme, isDark, toggleTheme, addTheme, setTheme } = useTheme() * theme.value = 'dark' * isDark.value = true * toggleTheme() * addTheme({ value: 'ocean', label: 'Ocean Blue', class: 'ocean-blue', isDark: false }) * setTheme('ocean') */ // Module-level one-time init: a single matchMedia listener and a single // colorMode watcher, no matter how many components call useTheme(). let initialized = false function initTheme() { if (initialized || !isBrowser) { return } initialized = true const saved = window.localStorage.getItem(STORAGE_KEY) if (themeOptions.value.some(t => t.value === saved)) { colorMode.value = saved! } applyTheme(colorMode.value) // React to system changes when in "system" mode window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { if (colorMode.value === 'system') { applyTheme('system') } }) // Persist + apply on mode changes watch(colorMode, (newMode) => { localStorage.setItem(STORAGE_KEY, newMode) applyTheme(newMode) }) } export function useTheme() { initTheme() const theme = computed(() => colorMode.value) return { isDark, theme, themeOptions, toggleTheme, addTheme, setTheme: (value: string) => { colorMode.value = value }, } }