1+ import { useState , useRef , useEffect } from 'react'
2+ import { Sun , Moon , Monitor , Check } from 'lucide-react'
3+ import { useTheme } from '@/contexts/ThemeContext'
4+ import { clsx } from 'clsx'
5+
6+ export default function ThemeToggle ( ) {
7+ const { theme, setTheme, effectiveTheme } = useTheme ( )
8+ const [ isOpen , setIsOpen ] = useState ( false )
9+ const dropdownRef = useRef < HTMLDivElement > ( null )
10+
11+ const options = [
12+ { value : 'light' as const , label : 'Light' , icon : Sun } ,
13+ { value : 'dark' as const , label : 'Dark' , icon : Moon } ,
14+ { value : 'auto' as const , label : 'System' , icon : Monitor } ,
15+ ]
16+
17+ const current = options . find ( opt => opt . value === theme ) || options [ 2 ]
18+ const Icon = current . icon
19+
20+ useEffect ( ( ) => {
21+ if ( ! isOpen ) return
22+
23+ const handleClick = ( e : MouseEvent ) => {
24+ if ( ! dropdownRef . current ?. contains ( e . target as Node ) ) {
25+ setIsOpen ( false )
26+ }
27+ }
28+
29+ const handleEscape = ( e : KeyboardEvent ) => {
30+ if ( e . key === 'Escape' ) setIsOpen ( false )
31+ }
32+
33+ document . addEventListener ( 'mousedown' , handleClick )
34+ document . addEventListener ( 'keydown' , handleEscape )
35+
36+ return ( ) => {
37+ document . removeEventListener ( 'mousedown' , handleClick )
38+ document . removeEventListener ( 'keydown' , handleEscape )
39+ }
40+ } , [ isOpen ] )
41+
42+ return (
43+ < div className = "relative" ref = { dropdownRef } >
44+ < button
45+ onClick = { ( ) => setIsOpen ( ! isOpen ) }
46+ className = "p-2 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300"
47+ aria-label = { `Current theme: ${ current . label } ` }
48+ >
49+ < Icon className = "w-4 h-4" />
50+ </ button >
51+
52+ { isOpen && (
53+ < div className = "absolute right-0 mt-2 w-40 rounded-lg shadow-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 py-1 z-50" >
54+ { options . map ( ( option ) => {
55+ const OptionIcon = option . icon
56+ const selected = theme === option . value
57+
58+ return (
59+ < button
60+ key = { option . value }
61+ onClick = { ( ) => {
62+ setTheme ( option . value )
63+ setIsOpen ( false )
64+ } }
65+ className = { clsx (
66+ 'w-full px-3 py-2 text-sm text-left flex items-center justify-between' ,
67+ 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300'
68+ ) }
69+ >
70+ < div className = "flex items-center" >
71+ < OptionIcon className = "w-4 h-4 mr-2" />
72+ < span > { option . label } </ span >
73+ { option . value === 'auto' && theme === 'auto' && (
74+ < span className = "ml-1 text-xs text-gray-500 dark:text-gray-400" >
75+ ({ effectiveTheme } )
76+ </ span >
77+ ) }
78+ </ div >
79+ { selected && < Check className = "w-4 h-4 text-blue-600 dark:text-blue-400" /> }
80+ </ button >
81+ )
82+ } ) }
83+ </ div >
84+ ) }
85+ </ div >
86+ )
87+ }
0 commit comments