11import type { ReactNode } from "react" ;
2- import { createContext , useContext , useEffect , useState } from "react" ;
2+ import {
3+ createContext ,
4+ useCallback ,
5+ useContext ,
6+ useEffect ,
7+ useMemo ,
8+ useState ,
9+ } from "react" ;
310import { THEME_STORAGE_KEY } from "./shell-constants" ;
411
512export type Theme = "light" | "dark" | "system" ;
@@ -82,17 +89,21 @@ export interface ThemeConfig {
8289
8390interface ThemeProviderProps {
8491 children : ReactNode ;
92+ defaultTheme ?: Theme ;
8593 themeConfig ?: ThemeConfig ;
8694 storageKey ?: string ;
95+ disableTransitionOnChange ?: boolean ;
8796}
8897
8998interface ThemeProviderState {
9099 theme : Theme ;
100+ resolvedTheme : "light" | "dark" ;
91101 setTheme : ( theme : Theme ) => void ;
92102}
93103
94104const ThemeProviderContext = createContext < ThemeProviderState > ( {
95105 theme : "system" ,
106+ resolvedTheme : "light" ,
96107 setTheme : ( ) => null ,
97108} ) ;
98109
@@ -146,10 +157,26 @@ function getEffectiveTheme(theme: Theme): "light" | "dark" {
146157 return theme ;
147158}
148159
149- function applyThemeClass ( theme : Theme ) {
160+ function applyThemeClass ( theme : Theme , disableTransitions ?: boolean ) {
150161 const root = window . document . documentElement ;
162+ const resolved = getEffectiveTheme ( theme ) ;
163+
164+ let styleEl : HTMLStyleElement | null = null ;
165+ if ( disableTransitions ) {
166+ styleEl = document . createElement ( "style" ) ;
167+ styleEl . textContent =
168+ "*, *::before, *::after { transition: none !important }" ;
169+ document . head . append ( styleEl ) ;
170+ }
171+
151172 root . classList . remove ( "light" , "dark" ) ;
152- root . classList . add ( getEffectiveTheme ( theme ) ) ;
173+ root . classList . add ( resolved ) ;
174+
175+ if ( styleEl ) {
176+ // Force reflow, then re-enable transitions
177+ const _ = root . offsetHeight ;
178+ styleEl . remove ( ) ;
179+ }
153180}
154181
155182function applyThemeConfig ( config : ThemeConfig , theme : Theme ) {
@@ -180,35 +207,48 @@ function clearThemeConfig() {
180207
181208export function ThemeProvider ( {
182209 children,
210+ defaultTheme = "system" ,
183211 themeConfig,
184212 storageKey = THEME_STORAGE_KEY ,
213+ disableTransitionOnChange,
185214} : ThemeProviderProps ) {
186215 const [ theme , setThemeState ] = useState < Theme > ( ( ) => {
187- if ( typeof window === "undefined" ) return "system" ;
216+ if ( typeof window === "undefined" ) return defaultTheme ;
188217 const stored = localStorage . getItem ( storageKey ) ;
189- return isValidTheme ( stored ) ? stored : "system" ;
218+ return isValidTheme ( stored ) ? stored : defaultTheme ;
190219 } ) ;
191220
192- const setTheme = ( newTheme : Theme ) => {
193- localStorage . setItem ( storageKey , newTheme ) ;
194- setThemeState ( newTheme ) ;
195- } ;
221+ const [ resolvedTheme , setResolvedTheme ] = useState < "light" | "dark" > ( ( ) =>
222+ typeof window === "undefined" ? "light" : getEffectiveTheme ( theme ) ,
223+ ) ;
224+
225+ const setTheme = useCallback (
226+ ( newTheme : Theme ) => {
227+ localStorage . setItem ( storageKey , newTheme ) ;
228+ setThemeState ( newTheme ) ;
229+ } ,
230+ [ storageKey ] ,
231+ ) ;
196232
197233 // Apply light/dark class to document root
198234 useEffect ( ( ) => {
199- applyThemeClass ( theme ) ;
200- } , [ theme ] ) ;
235+ applyThemeClass ( theme , disableTransitionOnChange ) ;
236+ setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
237+ } , [ theme , disableTransitionOnChange ] ) ;
201238
202239 // Listen for system theme changes when in system mode
203240 useEffect ( ( ) => {
204241 if ( theme !== "system" ) return ;
205242
206243 const mediaQuery = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
207- const handleChange = ( ) => applyThemeClass ( theme ) ;
244+ const handleChange = ( ) => {
245+ applyThemeClass ( theme , disableTransitionOnChange ) ;
246+ setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
247+ } ;
208248
209249 mediaQuery . addEventListener ( "change" , handleChange ) ;
210250 return ( ) => mediaQuery . removeEventListener ( "change" , handleChange ) ;
211- } , [ theme ] ) ;
251+ } , [ theme , disableTransitionOnChange ] ) ;
212252
213253 // Cross-tab sync: update React state when theme changes in another tab
214254 useEffect ( ( ) => {
@@ -230,8 +270,13 @@ export function ThemeProvider({
230270 return ( ) => clearThemeConfig ( ) ;
231271 } , [ themeConfig , theme ] ) ;
232272
273+ const value = useMemo (
274+ ( ) => ( { theme, resolvedTheme, setTheme } ) ,
275+ [ theme , resolvedTheme , setTheme ] ,
276+ ) ;
277+
233278 return (
234- < ThemeProviderContext . Provider value = { { theme , setTheme } } >
279+ < ThemeProviderContext . Provider value = { value } >
235280 { children }
236281 </ ThemeProviderContext . Provider >
237282 ) ;
0 commit comments