1
- import { useCallback , useEffect , useState } from 'react' ;
1
+ import { useCallback , useEffect , useMemo , useState } from 'react' ;
2
2
3
- export type Appearance = 'light' | 'dark' | 'system' ;
3
+ export type ResolvedAppearance = 'light' | 'dark' ;
4
+ export type Appearance = ResolvedAppearance | 'system' ;
4
5
5
- const prefersDark = ( ) => {
6
- if ( typeof window === 'undefined' ) {
7
- return false ;
8
- }
6
+ // Global state management
7
+ const listeners = new Set < ( ) => void > ( ) ;
8
+ let currentAppearance : Appearance = 'system' ;
9
9
10
+ // Utility functions
11
+ const prefersDark = ( ) : boolean => {
12
+ if ( typeof window === 'undefined' ) return false ;
10
13
return window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ;
11
14
} ;
12
15
13
- const setCookie = ( name : string , value : string , days = 365 ) => {
14
- if ( typeof document === 'undefined' ) {
15
- return ;
16
- }
17
-
16
+ const setCookie = ( name : string , value : string , days = 365 ) : void => {
17
+ if ( typeof document === 'undefined' ) return ;
18
18
const maxAge = days * 24 * 60 * 60 ;
19
19
document . cookie = `${ name } =${ value } ;path=/;max-age=${ maxAge } ;SameSite=Lax` ;
20
20
} ;
21
21
22
- const applyTheme = ( appearance : Appearance ) => {
23
- const isDark =
24
- appearance === 'dark' || ( appearance === 'system' && prefersDark ( ) ) ;
22
+ const getStoredAppearance = ( ) : Appearance => {
23
+ if ( typeof window === 'undefined' ) return 'system' ;
24
+ return ( localStorage . getItem ( 'appearance' ) as Appearance ) || 'system' ;
25
+ } ;
26
+
27
+ const isDarkMode = ( appearance : Appearance ) : boolean => {
28
+ return appearance === 'dark' || ( appearance === 'system' && prefersDark ( ) ) ;
29
+ } ;
25
30
31
+ const applyTheme = ( appearance : Appearance ) : void => {
32
+ if ( typeof document === 'undefined' ) return ;
33
+ const isDark = isDarkMode ( appearance ) ;
26
34
document . documentElement . classList . toggle ( 'dark' , isDark ) ;
27
35
document . documentElement . style . colorScheme = isDark ? 'dark' : 'light' ;
28
36
} ;
29
37
30
- const mediaQuery = ( ) => {
31
- if ( typeof window === 'undefined' ) {
32
- return null ;
33
- }
38
+ const notify = ( ) : void => listeners . forEach ( ( listener ) => listener ( ) ) ;
34
39
40
+ const mediaQuery = ( ) : MediaQueryList | null => {
41
+ if ( typeof window === 'undefined' ) return null ;
35
42
return window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
36
43
} ;
37
44
38
- const handleSystemThemeChange = ( ) => {
39
- const currentAppearance = localStorage . getItem ( 'appearance' ) as Appearance ;
40
- applyTheme ( currentAppearance || 'system' ) ;
45
+ const handleSystemThemeChange = ( ) : void => {
46
+ applyTheme ( currentAppearance ) ;
47
+ notify ( ) ;
41
48
} ;
42
49
43
- export function initializeTheme ( ) {
44
- const savedAppearance =
45
- ( localStorage . getItem ( 'appearance' ) as Appearance ) || 'system' ;
50
+ export function initializeTheme ( ) : void {
51
+ if ( typeof window === 'undefined' ) return ;
46
52
47
- applyTheme ( savedAppearance ) ;
53
+ const storedAppearance = getStoredAppearance ( ) ;
48
54
49
- // Add the event listener for system theme changes...
55
+ // Initialize default appearance if none exists
56
+ if ( ! localStorage . getItem ( 'appearance' ) ) {
57
+ localStorage . setItem ( 'appearance' , 'system' ) ;
58
+ setCookie ( 'appearance' , 'system' ) ;
59
+ }
60
+
61
+ currentAppearance = storedAppearance ;
62
+ applyTheme ( currentAppearance ) ;
63
+
64
+ // Set up system theme change listener
50
65
mediaQuery ( ) ?. addEventListener ( 'change' , handleSystemThemeChange ) ;
51
66
}
52
67
53
68
export function useAppearance ( ) {
54
- const [ appearance , setAppearance ] = useState < Appearance > ( 'system' ) ;
69
+ const [ appearance , setAppearance ] =
70
+ useState < Appearance > ( getStoredAppearance ) ;
55
71
56
- const updateAppearance = useCallback ( ( mode : Appearance ) => {
72
+ useEffect ( ( ) => {
73
+ const handleChange = ( ) : void => {
74
+ const newAppearance = getStoredAppearance ( ) ;
75
+ setAppearance ( newAppearance ) ;
76
+ } ;
77
+
78
+ listeners . add ( handleChange ) ;
79
+ mediaQuery ( ) ?. addEventListener ( 'change' , handleChange ) ;
80
+
81
+ return ( ) => {
82
+ listeners . delete ( handleChange ) ;
83
+ mediaQuery ( ) ?. removeEventListener ( 'change' , handleChange ) ;
84
+ } ;
85
+ } , [ ] ) ;
86
+
87
+ const resolvedAppearance : ResolvedAppearance = useMemo (
88
+ ( ) => ( isDarkMode ( appearance ) ? 'dark' : 'light' ) ,
89
+ [ appearance ] ,
90
+ ) ;
91
+
92
+ const updateAppearance = useCallback ( ( mode : Appearance ) : void => {
93
+ currentAppearance = mode ;
57
94
setAppearance ( mode ) ;
58
95
59
96
// Store in localStorage for client-side persistence...
@@ -63,20 +100,8 @@ export function useAppearance() {
63
100
setCookie ( 'appearance' , mode ) ;
64
101
65
102
applyTheme ( mode ) ;
103
+ notify ( ) ;
66
104
} , [ ] ) ;
67
105
68
- useEffect ( ( ) => {
69
- const savedAppearance = localStorage . getItem (
70
- 'appearance' ,
71
- ) as Appearance | null ;
72
- updateAppearance ( savedAppearance || 'system' ) ;
73
-
74
- return ( ) =>
75
- mediaQuery ( ) ?. removeEventListener (
76
- 'change' ,
77
- handleSystemThemeChange ,
78
- ) ;
79
- } , [ updateAppearance ] ) ;
80
-
81
- return { appearance, updateAppearance } as const ;
106
+ return { appearance, resolvedAppearance, updateAppearance } as const ;
82
107
}
0 commit comments