+ "content": "import { MediaMatcher } from '@angular/cdk/layout';\nimport { isPlatformBrowser, DOCUMENT } from '@angular/common';\nimport { DestroyRef, Injectable, PLATFORM_ID, computed, inject, signal } from '@angular/core';\n\nexport enum EDarkModes {\n LIGHT = 'light',\n DARK = 'dark',\n SYSTEM = 'system',\n}\nexport type DarkModeOptions = EDarkModes.LIGHT | EDarkModes.DARK | EDarkModes.SYSTEM;\n\n@Injectable({\n providedIn: 'root',\n})\nexport class ZardDarkMode {\n private readonly document = inject(DOCUMENT);\n private readonly destroyRef = inject(DestroyRef);\n private readonly query = inject(MediaMatcher).matchMedia('(prefers-color-scheme: dark)');\n\n private static readonly STORAGE_KEY = 'theme';\n private handleThemeChange = (event: MediaQueryListEvent) => this.updateThemeMode(event.matches);\n private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));\n private readonly themeSignal = signal<DarkModeOptions>(EDarkModes.SYSTEM);\n\n readonly themeMode = computed(() => {\n const currentTheme = this.themeSignal();\n if (currentTheme === EDarkModes.SYSTEM) {\n return this.isDarkModeActive(currentTheme) ? EDarkModes.DARK : EDarkModes.LIGHT;\n }\n return currentTheme;\n });\n\n constructor() {\n if (this.isBrowser) {\n this.destroyRef.onDestroy(() => this.handleSystemChanges(false));\n }\n }\n\n init() {\n if (this.isBrowser) {\n this.initializeTheme();\n }\n }\n\n toggleTheme(targetMode?: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n if (targetMode) {\n this.applyTheme(targetMode);\n } else {\n const next = this.themeMode() === EDarkModes.DARK ? EDarkModes.LIGHT : EDarkModes.DARK;\n this.applyTheme(next);\n }\n }\n\n /**\n * Returns a ReadonlySignal<\"light\" | \"dark\" | \"system\"> that cannot be mutated externally.\n * Call currentTheme() to access the value or use it directly in templates where signals are supported.\n * @example service.currentTheme() // returns \"light\", \"dark\", or \"system\"\n */\n get currentTheme() {\n return this.themeSignal.asReadonly();\n }\n\n private initializeTheme(): void {\n const storedTheme = this.getStoredTheme();\n if (storedTheme) {\n this.themeSignal.set(storedTheme);\n }\n\n if (!storedTheme || storedTheme === EDarkModes.SYSTEM) {\n this.updateThemeMode(this.isDarkModeActive(EDarkModes.SYSTEM));\n this.handleSystemChanges();\n } else {\n this.updateThemeMode(storedTheme === EDarkModes.DARK);\n }\n }\n\n private applyTheme(theme: DarkModeOptions): void {\n if (!this.isBrowser) {\n return;\n }\n\n localStorage.setItem(ZardDarkMode.STORAGE_KEY, theme);\n this.themeSignal.set(theme);\n\n this.updateThemeMode(this.isDarkModeActive(theme));\n\n if (theme === EDarkModes.SYSTEM) {\n this.handleSystemChanges();\n } else {\n this.handleSystemChanges(false);\n }\n }\n\n private getStoredTheme(): DarkModeOptions | undefined {\n if (!this.isBrowser) {\n return undefined;\n }\n\n const value = localStorage.getItem(ZardDarkMode.STORAGE_KEY);\n if (value === EDarkModes.LIGHT || value === EDarkModes.DARK || value === EDarkModes.SYSTEM) {\n return value;\n }\n return undefined;\n }\n\n private getThemeMode(isDarkMode: boolean): EDarkModes.LIGHT | EDarkModes.DARK {\n return isDarkMode ? EDarkModes.DARK : EDarkModes.LIGHT;\n }\n\n private updateThemeMode(isDarkMode: boolean): void {\n const themeMode = this.getThemeMode(isDarkMode);\n const html = this.document.documentElement;\n html.classList.toggle('dark', isDarkMode);\n html.classList.toggle('dark-theme', isDarkMode);\n html.setAttribute('data-theme', themeMode);\n html.style.colorScheme = themeMode;\n }\n\n private isDarkModeActive(currentTheme: DarkModeOptions): boolean {\n if (!this.isBrowser) {\n return false;\n }\n\n return currentTheme === EDarkModes.DARK || (currentTheme === EDarkModes.SYSTEM && this.query.matches);\n }\n\n private handleSystemChanges(addListener = true): void {\n if (addListener) {\n this.query.addEventListener('change', this.handleThemeChange);\n } else {\n this.query.removeEventListener('change', this.handleThemeChange);\n }\n }\n}\n"
0 commit comments