1- import { createContext , useContext , useEffect , useState } from 'react' ;
1+ import { createContext , useCallback , useContext , useEffect , useMemo , useState } from 'react' ;
22import { storage } from '../utils/storage' ;
33import { BUILT_IN_THEMES , type ThemeInfo } from '../utils/themeRegistry' ;
44
5- type Mode = 'dark' | 'light' | 'system' ;
5+ export type Mode = 'dark' | 'light' | 'system' ;
66
77type ThemeProviderState = {
88 // Mode (dark/light/system) — backward-compatible with old "theme" API
99 theme : Mode ;
1010 setTheme : ( mode : Mode ) => void ;
1111 mode : Mode ;
1212 setMode : ( mode : Mode ) => void ;
13+ resolvedMode : 'dark' | 'light' ;
1314 // Color theme (palette)
1415 colorTheme : string ;
1516 setColorTheme : ( theme : string ) => void ;
@@ -21,6 +22,7 @@ const ThemeProviderContext = createContext<ThemeProviderState>({
2122 setTheme : ( ) => null ,
2223 mode : 'dark' ,
2324 setMode : ( ) => null ,
25+ resolvedMode : 'dark' ,
2426 colorTheme : 'plannotator' ,
2527 setColorTheme : ( ) => null ,
2628 availableThemes : BUILT_IN_THEMES ,
@@ -49,8 +51,15 @@ export function ThemeProvider({
4951 ( ) => storage . getItem ( colorThemeStorageKey ) || defaultColorTheme
5052 ) ;
5153
52- // Resolve whether the .light class should be applied, respecting theme's modeSupport
53- const resolveClasses = ( effectiveMode : 'dark' | 'light' ) => {
54+ const [ systemIsLight , setSystemIsLight ] = useState (
55+ ( ) => typeof window !== 'undefined' && window . matchMedia ( '(prefers-color-scheme: light)' ) . matches
56+ ) ;
57+
58+ // Compute resolved mode once — consumers use this instead of re-querying matchMedia
59+ const resolvedMode : 'dark' | 'light' = mode === 'system' ? ( systemIsLight ? 'light' : 'dark' ) : mode ;
60+
61+ // Resolve classes from resolved mode + theme's modeSupport
62+ const resolveClasses = useCallback ( ( effectiveMode : 'dark' | 'light' ) => {
5463 const themeInfo = BUILT_IN_THEMES . find ( t => t . id === colorTheme ) ;
5564 const modeSupport = themeInfo ?. modeSupport ?? 'both' ;
5665
@@ -59,57 +68,44 @@ export function ThemeProvider({
5968 if ( modeSupport === 'light-only' ) applyLight = true ;
6069
6170 return `theme-${ colorTheme } ${ applyLight ? ' light' : '' } ` ;
62- } ;
71+ } , [ colorTheme ] ) ;
6372
6473 // Apply theme class + mode class to document element
6574 useEffect ( ( ) => {
66- const root = window . document . documentElement ;
67-
68- let effectiveMode : 'dark' | 'light' = 'dark' ;
69- if ( mode === 'system' ) {
70- effectiveMode = window . matchMedia ( '(prefers-color-scheme: light)' ) . matches
71- ? 'light'
72- : 'dark' ;
73- } else {
74- effectiveMode = mode ;
75- }
76-
77- root . className = resolveClasses ( effectiveMode ) ;
78- } , [ mode , colorTheme ] ) ;
75+ window . document . documentElement . className = resolveClasses ( resolvedMode ) ;
76+ } , [ resolvedMode , resolveClasses ] ) ;
7977
8078 // Listen for system theme changes
8179 useEffect ( ( ) => {
8280 if ( mode !== 'system' ) return ;
8381
8482 const mediaQuery = window . matchMedia ( '(prefers-color-scheme: light)' ) ;
85- const handleChange = ( ) => {
86- const root = window . document . documentElement ;
87- root . className = resolveClasses ( mediaQuery . matches ? 'light' : 'dark' ) ;
88- } ;
83+ const handleChange = ( ) => setSystemIsLight ( mediaQuery . matches ) ;
8984
9085 mediaQuery . addEventListener ( 'change' , handleChange ) ;
9186 return ( ) => mediaQuery . removeEventListener ( 'change' , handleChange ) ;
92- } , [ mode , colorTheme ] ) ;
87+ } , [ mode ] ) ;
9388
94- const setMode = ( newMode : Mode ) => {
89+ const setMode = useCallback ( ( newMode : Mode ) => {
9590 storage . setItem ( storageKey , newMode ) ;
9691 setModeState ( newMode ) ;
97- } ;
92+ } , [ storageKey ] ) ;
9893
99- const setColorTheme = ( newTheme : string ) => {
94+ const setColorTheme = useCallback ( ( newTheme : string ) => {
10095 storage . setItem ( colorThemeStorageKey , newTheme ) ;
10196 setColorThemeState ( newTheme ) ;
102- } ;
97+ } , [ colorThemeStorageKey ] ) ;
10398
104- const value : ThemeProviderState = {
99+ const value = useMemo < ThemeProviderState > ( ( ) => ( {
105100 theme : mode ,
106101 setTheme : setMode ,
107102 mode,
108103 setMode,
104+ resolvedMode,
109105 colorTheme,
110106 setColorTheme,
111107 availableThemes : BUILT_IN_THEMES ,
112- } ;
108+ } ) , [ mode , resolvedMode , colorTheme , setMode , setColorTheme ] ) ;
113109
114110 return (
115111 < ThemeProviderContext . Provider value = { value } >
0 commit comments