Files
AnthoLume/frontend/src/theme/ThemeProvider.tsx
2026-03-22 17:21:34 -04:00

127 lines
3.4 KiB
TypeScript

import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
type ReactNode,
} from 'react';
import { getThemeMode, setThemeMode, type ThemeMode } from '../utils/localSettings';
export type ResolvedThemeMode = 'light' | 'dark';
interface ThemeContextValue {
themeMode: ThemeMode;
resolvedThemeMode: ResolvedThemeMode;
setThemeMode: (themeMode: ThemeMode) => void;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
function getSystemThemeMode(): ResolvedThemeMode {
if (typeof window === 'undefined') {
return 'light';
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export function resolveThemeMode(themeMode: ThemeMode): ResolvedThemeMode {
return themeMode === 'system' ? getSystemThemeMode() : themeMode;
}
export function applyThemeMode(themeMode: ThemeMode): ResolvedThemeMode {
const resolvedThemeMode = resolveThemeMode(themeMode);
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', resolvedThemeMode === 'dark');
document.documentElement.dataset.themeMode = themeMode;
document.documentElement.style.colorScheme = resolvedThemeMode;
}
return resolvedThemeMode;
}
export function initializeThemeMode(): ResolvedThemeMode {
return applyThemeMode(getThemeMode());
}
export function ThemeProvider({ children }: { children: ReactNode }) {
const [themeModeState, setThemeModeState] = useState<ThemeMode>(() => getThemeMode());
const [resolvedThemeMode, setResolvedThemeMode] = useState<ResolvedThemeMode>(() =>
resolveThemeMode(getThemeMode())
);
useEffect(() => {
setResolvedThemeMode(applyThemeMode(themeModeState));
}, [themeModeState]);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = () => {
if (themeModeState === 'system') {
setResolvedThemeMode(applyThemeMode('system'));
}
};
mediaQueryList.addEventListener('change', handleSystemThemeChange);
return () => {
mediaQueryList.removeEventListener('change', handleSystemThemeChange);
};
}, [themeModeState]);
useEffect(() => {
if (typeof window === 'undefined') {
return undefined;
}
const handleStorage = (event: StorageEvent) => {
if (event.key && event.key !== 'antholume:settings') {
return;
}
const nextThemeMode = getThemeMode();
setThemeModeState(nextThemeMode);
setResolvedThemeMode(applyThemeMode(nextThemeMode));
};
window.addEventListener('storage', handleStorage);
return () => {
window.removeEventListener('storage', handleStorage);
};
}, []);
const updateThemeMode = useCallback((nextThemeMode: ThemeMode) => {
setThemeMode(nextThemeMode);
setThemeModeState(nextThemeMode);
setResolvedThemeMode(applyThemeMode(nextThemeMode));
}, []);
const value = useMemo(
() => ({
themeMode: themeModeState,
resolvedThemeMode,
setThemeMode: updateThemeMode,
}),
[resolvedThemeMode, themeModeState, updateThemeMode]
);
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;
}