| | import React, { createContext, useContext, useEffect, useMemo, useCallback, useRef } from 'react'; |
| | import { useAtom } from 'jotai'; |
| | import { IThemeRGB } from '../types'; |
| | import applyTheme from '../utils/applyTheme'; |
| | import { themeModeAtom, themeColorsAtom, themeNameAtom } from '../atoms/themeAtoms'; |
| |
|
| | type ThemeContextType = { |
| | theme: string; |
| | setTheme: (theme: string) => void; |
| | themeRGB?: IThemeRGB; |
| | setThemeRGB: (colors?: IThemeRGB) => void; |
| | themeName?: string; |
| | setThemeName: (name?: string) => void; |
| | resetTheme: () => void; |
| | }; |
| |
|
| | |
| | export const ThemeContext = createContext<ThemeContextType>({ |
| | theme: 'system', |
| | setTheme: () => undefined, |
| | setThemeRGB: () => undefined, |
| | setThemeName: () => undefined, |
| | resetTheme: () => undefined, |
| | }); |
| |
|
| | export interface ThemeProviderProps { |
| | children: React.ReactNode; |
| | themeRGB?: IThemeRGB; |
| | themeName?: string; |
| | initialTheme?: string; |
| | } |
| |
|
| | |
| | |
| | |
| | export const isDark = (theme: string): boolean => { |
| | if (theme === 'system') { |
| | return window.matchMedia('(prefers-color-scheme: dark)').matches; |
| | } |
| | return theme === 'dark'; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | export function ThemeProvider({ |
| | children, |
| | themeRGB: propThemeRGB, |
| | themeName: propThemeName, |
| | initialTheme, |
| | }: ThemeProviderProps) { |
| | |
| | const [theme, setTheme] = useAtom(themeModeAtom); |
| | const [storedThemeRGB, setStoredThemeRGB] = useAtom(themeColorsAtom); |
| | const [storedThemeName, setStoredThemeName] = useAtom(themeNameAtom); |
| |
|
| | |
| | const propsInitialized = useRef(false); |
| |
|
| | |
| | useEffect(() => { |
| | if (!propsInitialized.current) { |
| | propsInitialized.current = true; |
| |
|
| | |
| | if (initialTheme) { |
| | setTheme(initialTheme); |
| | } |
| |
|
| | |
| | if (propThemeRGB) { |
| | setStoredThemeRGB(propThemeRGB); |
| | } |
| |
|
| | |
| | if (propThemeName) { |
| | setStoredThemeName(propThemeName); |
| | } |
| | } |
| | }, [initialTheme, propThemeRGB, propThemeName, setTheme, setStoredThemeRGB, setStoredThemeName]); |
| |
|
| | |
| | const applyThemeMode = useCallback((rawTheme: string) => { |
| | const root = window.document.documentElement; |
| | const darkMode = isDark(rawTheme); |
| |
|
| | root.classList.remove(darkMode ? 'light' : 'dark'); |
| | root.classList.add(darkMode ? 'dark' : 'light'); |
| | }, []); |
| |
|
| | |
| | useEffect(() => { |
| | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); |
| | const changeThemeOnSystemChange = () => { |
| | if (theme === 'system') { |
| | applyThemeMode('system'); |
| | } |
| | }; |
| |
|
| | mediaQuery.addEventListener('change', changeThemeOnSystemChange); |
| | return () => { |
| | mediaQuery.removeEventListener('change', changeThemeOnSystemChange); |
| | }; |
| | }, [theme, applyThemeMode]); |
| |
|
| | |
| | useEffect(() => { |
| | applyThemeMode(theme); |
| | }, [theme, applyThemeMode]); |
| |
|
| | |
| | useEffect(() => { |
| | if (storedThemeRGB) { |
| | applyTheme(storedThemeRGB); |
| | } |
| | }, [storedThemeRGB]); |
| |
|
| | |
| | const resetTheme = useCallback(() => { |
| | setTheme('system'); |
| | setStoredThemeRGB(undefined); |
| | setStoredThemeName(undefined); |
| | |
| | const root = document.documentElement; |
| | const customProps = Array.from(root.style).filter((prop) => prop.startsWith('--')); |
| | customProps.forEach((prop) => root.style.removeProperty(prop)); |
| | }, [setTheme, setStoredThemeRGB, setStoredThemeName]); |
| |
|
| | const value = useMemo( |
| | () => ({ |
| | theme, |
| | setTheme, |
| | themeRGB: storedThemeRGB, |
| | setThemeRGB: setStoredThemeRGB, |
| | themeName: storedThemeName, |
| | setThemeName: setStoredThemeName, |
| | resetTheme, |
| | }), |
| | [ |
| | theme, |
| | setTheme, |
| | storedThemeRGB, |
| | setStoredThemeRGB, |
| | storedThemeName, |
| | setStoredThemeName, |
| | resetTheme, |
| | ], |
| | ); |
| |
|
| | return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>; |
| | } |
| |
|
| | |
| | |
| | |
| | export function useTheme() { |
| | const context = useContext(ThemeContext); |
| | if (!context) { |
| | throw new Error('useTheme must be used within a ThemeProvider'); |
| | } |
| | return context; |
| | } |
| |
|
| | export default ThemeProvider; |
| |
|