11import type { ReactNode } from "react" ;
2- import { createContext , useContext , useEffect , useState } from "react" ;
2+ import {
3+ createContext ,
4+ useContext ,
5+ useEffect ,
6+ useState ,
7+ useSyncExternalStore ,
8+ } from "react" ;
39import { THEME_STORAGE_KEY } from "./shell-constants" ;
410
511export type ThemeConfig = {
@@ -65,22 +71,14 @@ function isValidTheme(value: string | null): value is Theme {
6571 return value === "light" || value === "dark" || value === "system" ;
6672}
6773
68- function getEffectiveTheme ( theme : Theme ) : "light" | "dark" {
69- if ( theme === "system" ) {
70- return window . matchMedia ( "(prefers-color-scheme: dark)" ) . matches
71- ? "dark"
72- : "light" ;
73- }
74- return theme ;
75- }
76-
77- function applyThemeClass ( theme : Theme , disableTransitions ?: boolean ) {
74+ function applyThemeClass (
75+ resolved : "light" | "dark" ,
76+ disableTransitions ?: boolean ,
77+ ) {
7878 const root = window . document . documentElement ;
79- const resolved = getEffectiveTheme ( theme ) ;
8079
81- let styleEl : HTMLStyleElement | null = null ;
82- if ( disableTransitions ) {
83- styleEl = document . createElement ( "style" ) ;
80+ const styleEl = disableTransitions ? document . createElement ( "style" ) : null ;
81+ if ( styleEl ) {
8482 styleEl . textContent =
8583 "*, *::before, *::after { transition: none !important }" ;
8684 document . head . append ( styleEl ) ;
@@ -96,9 +94,9 @@ function applyThemeClass(theme: Theme, disableTransitions?: boolean) {
9694 }
9795}
9896
99- function applyThemeConfig ( config : ThemeConfig , theme : Theme ) {
97+ function applyThemeConfig ( config : ThemeConfig , resolved : "light" | "dark" ) {
10098 const root = document . documentElement ;
101- const isDark = getEffectiveTheme ( theme ) === "dark" ;
99+ const isDark = resolved === "dark" ;
102100
103101 for ( const cssVar of Object . values ( cssVarMap ) ) {
104102 root . style . removeProperty ( cssVar ) ;
@@ -135,34 +133,28 @@ export function ThemeProvider({
135133 return isValidTheme ( stored ) ? stored : defaultTheme ;
136134 } ) ;
137135
138- const [ resolvedTheme , setResolvedTheme ] = useState < "light" | "dark" > ( ( ) =>
139- typeof window === "undefined" ? "light" : getEffectiveTheme ( theme ) ,
136+ const systemDark = useSyncExternalStore (
137+ ( cb ) => {
138+ const mq = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
139+ mq . addEventListener ( "change" , cb ) ;
140+ return ( ) => mq . removeEventListener ( "change" , cb ) ;
141+ } ,
142+ ( ) => window . matchMedia ( "(prefers-color-scheme: dark)" ) . matches ,
143+ ( ) => false ,
140144 ) ;
141145
146+ const resolvedTheme =
147+ theme === "system" ? ( systemDark ? "dark" : "light" ) : theme ;
148+
142149 const setTheme = ( newTheme : Theme ) => {
143150 localStorage . setItem ( storageKey , newTheme ) ;
144151 setThemeState ( newTheme ) ;
145152 } ;
146153
147154 // Apply light/dark class to document root
148155 useEffect ( ( ) => {
149- applyThemeClass ( theme , disableTransitionOnChange ) ;
150- setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
151- } , [ theme , disableTransitionOnChange ] ) ;
152-
153- // Listen for system theme changes when in system mode
154- useEffect ( ( ) => {
155- if ( theme !== "system" ) return ;
156-
157- const mediaQuery = window . matchMedia ( "(prefers-color-scheme: dark)" ) ;
158- const handleChange = ( ) => {
159- applyThemeClass ( theme , disableTransitionOnChange ) ;
160- setResolvedTheme ( getEffectiveTheme ( theme ) ) ;
161- } ;
162-
163- mediaQuery . addEventListener ( "change" , handleChange ) ;
164- return ( ) => mediaQuery . removeEventListener ( "change" , handleChange ) ;
165- } , [ theme , disableTransitionOnChange ] ) ;
156+ applyThemeClass ( resolvedTheme , disableTransitionOnChange ) ;
157+ } , [ resolvedTheme , disableTransitionOnChange ] ) ;
166158
167159 // Cross-tab sync: update React state when theme changes in another tab
168160 useEffect ( ( ) => {
@@ -180,9 +172,9 @@ export function ThemeProvider({
180172 useEffect ( ( ) => {
181173 if ( ! themeConfig ) return ;
182174
183- applyThemeConfig ( themeConfig , theme ) ;
175+ applyThemeConfig ( themeConfig , resolvedTheme ) ;
184176 return ( ) => clearThemeConfig ( ) ;
185- } , [ themeConfig , theme ] ) ;
177+ } , [ themeConfig , resolvedTheme ] ) ;
186178
187179 return (
188180 < ThemeProviderContext . Provider value = { { theme, resolvedTheme, setTheme } } >
0 commit comments