11import * as React from "react" ;
22
3- type Theme = 'light' | 'dark' | 'system' ;
3+ export type ColorTheme = 'blue' | 'green' | 'purple' | 'amber' ;
4+ export type BaseTheme = 'light' | 'dark' | 'system' ;
5+ export type Theme = BaseTheme | ColorTheme ;
6+
47type ThemeContextType = {
58 theme : Theme ;
9+ colorTheme : ColorTheme | null ;
10+ baseTheme : BaseTheme ;
611 resolvedTheme : 'light' | 'dark' ;
712 setTheme : ( theme : Theme ) => void ;
13+ availableThemes : Theme [ ] ;
814} ;
915
16+ export const availableThemes : Theme [ ] = [ 'light' , 'dark' , 'system' , 'blue' , 'green' , 'purple' , 'amber' ] ;
17+
1018const ThemeContext = React . createContext < ThemeContextType | undefined > ( undefined ) ;
1119
12- export const ThemeProvider : React . FC < { children : React . ReactNode } > = ( { children } ) => {
20+ const COLOR_THEMES : ColorTheme [ ] = [ 'blue' , 'green' , 'purple' , 'amber' ] ;
21+ const BASE_THEMES : BaseTheme [ ] = [ 'light' , 'dark' , 'system' ] ;
22+
23+ export interface ThemeProviderProps {
24+ children : React . ReactNode ;
25+ defaultTheme ?: Theme ;
26+ }
27+
28+ export const ThemeProvider : React . FC < ThemeProviderProps > = ( {
29+ children,
30+ defaultTheme = 'system'
31+ } ) => {
1332 const [ theme , setThemeState ] = React . useState < Theme > ( ( ) => {
14- if ( typeof window === 'undefined' ) return 'system' ;
15- return ( localStorage . getItem ( 'theme' ) as Theme ) || 'system' ;
33+ if ( typeof window === 'undefined' ) return defaultTheme ;
34+ return ( localStorage . getItem ( 'theme' ) as Theme ) || defaultTheme ;
1635 } ) ;
1736
1837 const [ resolvedTheme , setResolvedTheme ] = React . useState < 'light' | 'dark' > ( ( ) => {
1938 if ( typeof window === 'undefined' ) return 'light' ;
2039 if ( theme === 'system' ) {
2140 return window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? 'dark' : 'light' ;
2241 }
23- return theme ;
42+ return BASE_THEMES . includes ( theme as BaseTheme ) ? ( theme as 'light' | 'dark' ) : 'light' ;
2443 } ) ;
2544
45+ const baseTheme = React . useMemo < BaseTheme > ( ( ) => {
46+ if ( BASE_THEMES . includes ( theme as BaseTheme ) ) {
47+ return theme as BaseTheme ;
48+ }
49+ return 'light' ;
50+ } , [ theme ] ) ;
51+
52+ const colorTheme = React . useMemo < ColorTheme | null > ( ( ) => {
53+ if ( COLOR_THEMES . includes ( theme as ColorTheme ) ) {
54+ return theme as ColorTheme ;
55+ }
56+ return null ;
57+ } , [ theme ] ) ;
58+
2659 // Handle system preference changes
2760 React . useEffect ( ( ) => {
2861 const mediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
@@ -31,45 +64,62 @@ export const ThemeProvider: React.FC<{children: React.ReactNode}> = ({ children
3164 if ( theme === 'system' ) {
3265 const newTheme = mediaQuery . matches ? 'dark' : 'light' ;
3366 setResolvedTheme ( newTheme ) ;
34- updateDocumentClass ( newTheme ) ;
67+ updateDocumentClass ( newTheme , colorTheme ) ;
3568 }
3669 } ;
3770
3871 mediaQuery . addEventListener ( 'change' , handleChange ) ;
3972 return ( ) => mediaQuery . removeEventListener ( 'change' , handleChange ) ;
40- } , [ theme ] ) ;
73+ } , [ theme , colorTheme ] ) ;
4174
4275 // Update the DOM and resolved theme when theme changes
4376 React . useEffect ( ( ) => {
4477 if ( theme === 'system' ) {
4578 const systemTheme = window . matchMedia ( '(prefers-color-scheme: dark)' ) . matches ? 'dark' : 'light' ;
4679 setResolvedTheme ( systemTheme ) ;
47- updateDocumentClass ( systemTheme ) ;
80+ updateDocumentClass ( systemTheme , colorTheme ) ;
81+ } else if ( BASE_THEMES . includes ( theme as BaseTheme ) ) {
82+ setResolvedTheme ( theme as 'light' | 'dark' ) ;
83+ updateDocumentClass ( theme as 'light' | 'dark' , colorTheme ) ;
4884 } else {
49- setResolvedTheme ( theme ) ;
50- updateDocumentClass ( theme ) ;
85+ // For color themes, we apply both the base theme (light) and the color theme
86+ updateDocumentClass ( 'light' , theme as ColorTheme ) ;
5187 }
52- } , [ theme ] ) ;
88+ } , [ theme , colorTheme ] ) ;
5389
54- const updateDocumentClass = ( resolvedTheme : string ) => {
55- if ( resolvedTheme === 'dark' ) {
90+ const updateDocumentClass = ( baseTheme : 'light' | 'dark' , colorTheme : ColorTheme | null ) => {
91+ // Handle dark/light mode
92+ if ( baseTheme === 'dark' ) {
5693 document . documentElement . classList . add ( 'dark' ) ;
5794 } else {
5895 document . documentElement . classList . remove ( 'dark' ) ;
5996 }
97+
98+ // Remove any existing color theme classes
99+ COLOR_THEMES . forEach ( ct => {
100+ document . documentElement . classList . remove ( `theme-${ ct } ` ) ;
101+ } ) ;
102+
103+ // Apply color theme if present
104+ if ( colorTheme ) {
105+ document . documentElement . classList . add ( `theme-${ colorTheme } ` ) ;
106+ }
60107 } ;
61108
62109 const setTheme = ( newTheme : Theme ) => {
63110 setThemeState ( newTheme ) ;
64- if ( newTheme === 'system' ) {
65- localStorage . removeItem ( 'theme' ) ;
66- } else {
67- localStorage . setItem ( 'theme' , newTheme ) ;
68- }
111+ localStorage . setItem ( 'theme' , newTheme ) ;
69112 } ;
70113
71114 return (
72- < ThemeContext . Provider value = { { theme, resolvedTheme, setTheme } } >
115+ < ThemeContext . Provider value = { {
116+ theme,
117+ colorTheme,
118+ baseTheme,
119+ resolvedTheme,
120+ setTheme,
121+ availableThemes
122+ } } >
73123 { children }
74124 </ ThemeContext . Provider >
75125 ) ;
0 commit comments