33import React , { createContext , useContext , useEffect , useReducer , useCallback } from 'react'
44
55export type ThemeColor = 'red' | 'amber' | 'yellow' | 'cyan' | 'green' | 'indigo' | 'purple' | 'pink' | 'blue' | 'slate'
6+ export type ThemeMode = 'light' | 'dark' | 'system'
67
78interface ThemeState {
89 themeColor : ThemeColor
10+ themeMode : ThemeMode
911 isDark : boolean
1012 isInitialized : boolean
1113}
1214
1315type ThemeAction =
1416 | { type : 'SET_THEME_COLOR' ; color : ThemeColor }
17+ | { type : 'SET_THEME_MODE' ; mode : ThemeMode }
1518 | { type : 'SET_DARK_MODE' ; isDark : boolean }
16- | { type : 'INITIALIZE' ; themeColor : ThemeColor ; isDark : boolean }
19+ | { type : 'INITIALIZE' ; themeColor : ThemeColor ; themeMode : ThemeMode ; isDark : boolean }
1720
1821interface ThemeContextType {
1922 themeColor : ThemeColor
23+ themeMode : ThemeMode
2024 setThemeColor : ( color : ThemeColor ) => void
25+ setThemeMode : ( mode : ThemeMode ) => void
2126}
2227
2328const ThemeContext = createContext < ThemeContextType | undefined > ( undefined )
@@ -38,11 +43,14 @@ function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
3843 switch ( action . type ) {
3944 case 'SET_THEME_COLOR' :
4045 return { ...state , themeColor : action . color }
46+ case 'SET_THEME_MODE' :
47+ return { ...state , themeMode : action . mode }
4148 case 'SET_DARK_MODE' :
4249 return { ...state , isDark : action . isDark }
4350 case 'INITIALIZE' :
4451 return {
4552 themeColor : action . themeColor ,
53+ themeMode : action . themeMode ,
4654 isDark : action . isDark ,
4755 isInitialized : true
4856 }
@@ -54,6 +62,7 @@ function themeReducer(state: ThemeState, action: ThemeAction): ThemeState {
5462export function ThemeProvider ( { children } : ThemeProviderProps ) {
5563 const [ state , dispatch ] = useReducer ( themeReducer , {
5664 themeColor : 'blue' ,
65+ themeMode : 'system' ,
5766 isDark : false ,
5867 isInitialized : false
5968 } )
@@ -62,6 +71,25 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
6271 dispatch ( { type : 'SET_THEME_COLOR' , color } )
6372 } , [ ] )
6473
74+ const setThemeMode = useCallback ( ( mode : ThemeMode ) => {
75+ dispatch ( { type : 'SET_THEME_MODE' , mode } )
76+
77+ // Apply the theme mode immediately
78+ if ( mode === 'light' ) {
79+ document . documentElement . classList . remove ( 'dark' )
80+ } else if ( mode === 'dark' ) {
81+ document . documentElement . classList . add ( 'dark' )
82+ } else {
83+ // System mode - check system preference
84+ const systemDark = window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches
85+ if ( systemDark ) {
86+ document . documentElement . classList . add ( 'dark' )
87+ } else {
88+ document . documentElement . classList . remove ( 'dark' )
89+ }
90+ }
91+ } , [ ] )
92+
6593 // Initialize theme from localStorage
6694 useEffect ( ( ) => {
6795 try {
@@ -70,18 +98,25 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
7098 ? savedTheme
7199 : 'blue'
72100
101+ const savedMode = localStorage . getItem ( 'theme-mode' ) as ThemeMode
102+ const validMode = savedMode && [ 'light' , 'dark' , 'system' ] . includes ( savedMode )
103+ ? savedMode
104+ : 'system'
105+
73106 const isDarkMode = document . documentElement . classList . contains ( 'dark' )
74107
75108 dispatch ( {
76109 type : 'INITIALIZE' ,
77- themeColor : validTheme ,
110+ themeColor : validTheme ,
111+ themeMode : validMode ,
78112 isDark : isDarkMode
79113 } )
80114 } catch ( error ) {
81115 console . warn ( 'Failed to load theme from localStorage:' , error )
82116 dispatch ( {
83117 type : 'INITIALIZE' ,
84- themeColor : 'blue' ,
118+ themeColor : 'blue' ,
119+ themeMode : 'system' ,
85120 isDark : false
86121 } )
87122 }
@@ -93,12 +128,13 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
93128
94129 try {
95130 localStorage . setItem ( 'theme-color' , state . themeColor )
131+ localStorage . setItem ( 'theme-mode' , state . themeMode )
96132 } catch ( error ) {
97133 console . warn ( 'Failed to save theme to localStorage:' , error )
98134 }
99135
100136 applyThemeVariables ( state . themeColor , state . isDark )
101- } , [ state . themeColor , state . isDark , state . isInitialized ] )
137+ } , [ state . themeColor , state . themeMode , state . isDark , state . isInitialized ] )
102138
103139 // Watch for dark mode changes with debouncing
104140 useEffect ( ( ) => {
@@ -131,10 +167,35 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
131167 }
132168 } , [ state . isDark , state . isInitialized ] )
133169
170+ // Watch for system theme preference changes
171+ useEffect ( ( ) => {
172+ if ( ! state . isInitialized || state . themeMode !== 'system' ) return
173+
174+ const mediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' )
175+
176+ const handleChange = ( e : MediaQueryListEvent ) => {
177+ if ( state . themeMode === 'system' ) {
178+ if ( e . matches ) {
179+ document . documentElement . classList . add ( 'dark' )
180+ } else {
181+ document . documentElement . classList . remove ( 'dark' )
182+ }
183+ }
184+ }
185+
186+ mediaQuery . addEventListener ( 'change' , handleChange )
187+
188+ return ( ) => {
189+ mediaQuery . removeEventListener ( 'change' , handleChange )
190+ }
191+ } , [ state . themeMode , state . isInitialized ] )
192+
134193 return (
135194 < ThemeContext . Provider value = { {
136- themeColor : state . themeColor ,
137- setThemeColor
195+ themeColor : state . themeColor ,
196+ themeMode : state . themeMode ,
197+ setThemeColor,
198+ setThemeMode
138199 } } >
139200 { children }
140201 </ ThemeContext . Provider >
0 commit comments