diff --git a/src/components/ToggleDarkMode/ToggleDarkMode.tsx b/src/components/ToggleDarkMode/ToggleDarkMode.tsx index e86bcc6..448ae42 100644 --- a/src/components/ToggleDarkMode/ToggleDarkMode.tsx +++ b/src/components/ToggleDarkMode/ToggleDarkMode.tsx @@ -1,7 +1,9 @@ import { useState, useEffect, useCallback } from 'react' -import { Moon, Sun } from 'lucide-react' +import { Moon, Sun, Monitor } from 'lucide-react' import { Button, type ButtonProps } from '../Button' +export type ThemePreference = 'light' | 'dark' | 'system' + export interface ToggleDarkModeProps { /** * Color mode for background context (passed to Button) @@ -35,15 +37,20 @@ export interface ToggleDarkModeProps { */ darkLabel?: string + /** + * Custom label for system mode + */ + systemLabel?: string + /** * localStorage key for persisting preference */ storageKey?: string /** - * Callback when dark mode changes + * Callback when theme preference changes */ - onChange?: (isDark: boolean) => void + onChange?: (theme: ThemePreference) => void /** * Additional CSS classes @@ -51,8 +58,14 @@ export interface ToggleDarkModeProps { className?: string } +const CYCLE: ThemePreference[] = ['light', 'dark', 'system'] + +function getSystemDark() { + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + /** - * Toggle button for switching between light and dark modes. + * Toggle button for cycling between light, dark, and system color modes. * Automatically persists preference to localStorage and applies * the 'dark' class to the document element. * @@ -64,11 +77,8 @@ export interface ToggleDarkModeProps { * // With label * * - * // Custom labels - * - * * // With change callback - * console.log('Dark mode:', isDark)} /> + * console.log('Theme:', theme)} /> * ``` */ export function ToggleDarkMode({ @@ -78,45 +88,68 @@ export function ToggleDarkMode({ showLabel = false, lightLabel = 'Light', darkLabel = 'Dark', + systemLabel = 'System', storageKey = 'darkMode', onChange, className, }: ToggleDarkModeProps) { - const [isDarkMode, setIsDarkMode] = useState(() => { - // Check for saved preference or system preference + const [theme, setTheme] = useState(() => { if (typeof window !== 'undefined') { const saved = localStorage.getItem(storageKey) - if (saved !== null) return saved === 'true' - return window.matchMedia('(prefers-color-scheme: dark)').matches + // Migrate legacy boolean values + if (saved === 'true') return 'dark' + if (saved === 'false') return 'light' + if (saved === 'light' || saved === 'dark' || saved === 'system') return saved } - return false + return 'system' }) - // Apply dark mode class to html element + // Apply dark mode class based on theme preference useEffect(() => { - document.documentElement.classList.toggle('dark', isDarkMode) - localStorage.setItem(storageKey, String(isDarkMode)) - }, [isDarkMode, storageKey]) + const apply = (isDark: boolean) => { + document.documentElement.classList.toggle('dark', isDark) + } + + if (theme === 'system') { + apply(getSystemDark()) + const mq = window.matchMedia('(prefers-color-scheme: dark)') + const handler = (e: MediaQueryListEvent) => apply(e.matches) + mq.addEventListener('change', handler) + return () => mq.removeEventListener('change', handler) + } + + apply(theme === 'dark') + }, [theme]) + + // Persist preference + useEffect(() => { + localStorage.setItem(storageKey, theme) + }, [theme, storageKey]) const toggle = useCallback(() => { - setIsDarkMode((prev) => { - const next = !prev + setTheme((prev) => { + const next = CYCLE[(CYCLE.indexOf(prev) + 1) % CYCLE.length] onChange?.(next) return next }) }, [onChange]) + const icon = { light: , dark: , system: }[theme] + const label = { light: lightLabel, dark: darkLabel, system: systemLabel }[theme] + const nextTheme = CYCLE[(CYCLE.indexOf(theme) + 1) % CYCLE.length] + const ariaLabel = `Switch to ${nextTheme} mode` + return ( ) } diff --git a/src/components/ToggleDarkMode/index.ts b/src/components/ToggleDarkMode/index.ts index 04212f3..615774f 100644 --- a/src/components/ToggleDarkMode/index.ts +++ b/src/components/ToggleDarkMode/index.ts @@ -1,2 +1,2 @@ export { ToggleDarkMode } from './ToggleDarkMode' -export type { ToggleDarkModeProps } from './ToggleDarkMode' +export type { ToggleDarkModeProps, ThemePreference } from './ToggleDarkMode'