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 ThemeConfig = {
@@ -10,19 +17,21 @@ export type Theme = "light" | "dark" | "system";
1017
1118interface ThemeProviderProps {
1219 children : ReactNode ;
20+ defaultTheme ?: Theme ;
1321 themeConfig ?: ThemeConfig ;
1422 storageKey ?: string ;
23+ disableTransitionOnChange ?: boolean ;
1524}
1625
1726interface ThemeProviderState {
1827 theme : Theme ;
28+ resolvedTheme : "light" | "dark" ;
1929 setTheme : ( theme : Theme ) => void ;
2030}
2131
22- const ThemeProviderContext = createContext < ThemeProviderState > ( {
23- theme : "system" ,
24- setTheme : ( ) => null ,
25- } ) ;
32+ const ThemeProviderContext = createContext < ThemeProviderState | undefined > (
33+ undefined ,
34+ ) ;
2635
2736const cssVarMap : Record < string , string > = {
2837 background : "--background" ,
@@ -74,10 +83,26 @@ function getEffectiveTheme(theme: Theme): "light" | "dark" {
7483 return theme ;
7584}
7685
77- function applyThemeClass ( theme : Theme ) {
86+ function applyThemeClass ( theme : Theme , disableTransitions ?: boolean ) {
7887 const root = window . document . documentElement ;
88+ const resolved = getEffectiveTheme ( theme ) ;
89+
90+ let styleEl : HTMLStyleElement | null = null ;
91+ if ( disableTransitions ) {
92+ styleEl = document . createElement ( "style" ) ;
93+ styleEl . textContent =
94+ "*, *::before, *::after { transition: none !important }" ;
95+ document . head . append ( styleEl ) ;
96+ }
97+
7998 root . classList . remove ( "light" , "dark" ) ;
80- root . classList . add ( getEffectiveTheme ( theme ) ) ;
99+ root . classList . add ( resolved ) ;
100+
101+ if ( styleEl ) {
102+ // Force reflow, then re-enable transitions
103+ void root . offsetHeight ;
104+ styleEl . remove ( ) ;
105+ }
81106}
82107
83108function applyThemeConfig ( config : ThemeConfig , theme : Theme ) {
@@ -108,35 +133,48 @@ function clearThemeConfig() {
108133
109134export function ThemeProvider ( {
110135 children,
136+ defaultTheme = "system" ,
111137 themeConfig,
112138 storageKey = THEME_STORAGE_KEY ,
139+ disableTransitionOnChange,
113140} : ThemeProviderProps ) {
114141 const [ theme , setThemeState ] = useState < Theme > ( ( ) => {
115- if ( typeof window === "undefined" ) return "system" ;
142+ if ( typeof window === "undefined" ) return defaultTheme ;
116143 const stored = localStorage . getItem ( storageKey ) ;
117- return isValidTheme ( stored ) ? stored : "system" ;
144+ return isValidTheme ( stored ) ? stored : defaultTheme ;
118145 } ) ;
119146
120- const setTheme = ( newTheme : Theme ) => {
121- localStorage . setItem ( storageKey , newTheme ) ;
122- setThemeState ( newTheme ) ;
123- } ;
147+ const [ resolvedTheme , setResolvedTheme ] = useState < "light" | "dark" > ( ( ) =>
148+ typeof window === "undefined" ? "light" : getEffectiveTheme ( theme ) ,
149+ ) ;
150+
151+ const setTheme = useCallback (
152+ ( newTheme : Theme ) => {
153+ localStorage . setItem ( storageKey , newTheme ) ;
154+ setThemeState ( newTheme ) ;
155+ } ,
156+ [ storageKey ] ,
157+ ) ;
124158
125159 // Apply light/dark class to document root
126160 useEffect ( ( ) => {
127- applyThemeClass ( theme ) ;
128- } , [ theme ] ) ;
161+ applyThemeClass ( theme , disableTransitionOnChange ) ;
162+ setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
163+ } , [ theme , disableTransitionOnChange ] ) ;
129164
130165 // Listen for system theme changes when in system mode
131166 useEffect ( ( ) => {
132167 if ( theme !== "system" ) return ;
133168
134169 const mediaQuery = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
135- const handleChange = ( ) => applyThemeClass ( theme ) ;
170+ const handleChange = ( ) => {
171+ applyThemeClass ( theme , disableTransitionOnChange ) ;
172+ setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
173+ } ;
136174
137175 mediaQuery . addEventListener ( "change" , handleChange ) ;
138176 return ( ) => mediaQuery . removeEventListener ( "change" , handleChange ) ;
139- } , [ theme ] ) ;
177+ } , [ theme , disableTransitionOnChange ] ) ;
140178
141179 // Cross-tab sync: update React state when theme changes in another tab
142180 useEffect ( ( ) => {
@@ -158,8 +196,13 @@ export function ThemeProvider({
158196 return ( ) => clearThemeConfig ( ) ;
159197 } , [ themeConfig , theme ] ) ;
160198
199+ const value = useMemo (
200+ ( ) => ( { theme, resolvedTheme, setTheme } ) ,
201+ [ theme , resolvedTheme , setTheme ] ,
202+ ) ;
203+
161204 return (
162- < ThemeProviderContext . Provider value = { { theme , setTheme } } >
205+ < ThemeProviderContext . Provider value = { value } >
163206 { children }
164207 </ ThemeProviderContext . Provider >
165208 ) ;
0 commit comments