84 lines
2.4 KiB
TypeScript
84 lines
2.4 KiB
TypeScript
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
|
|
|
export type Appearance = 'light' | 'dark' | 'system';
|
|
|
|
type AppearanceContextValue = {
|
|
appearance: Appearance;
|
|
resolved: 'light' | 'dark';
|
|
updateAppearance: (mode: Appearance) => void;
|
|
};
|
|
|
|
const AppearanceContext = createContext<AppearanceContextValue>({
|
|
appearance: 'system',
|
|
resolved: 'light',
|
|
updateAppearance: () => {},
|
|
});
|
|
|
|
function resolveTheme(mode: Appearance): 'light' | 'dark' {
|
|
if (mode === 'dark') return 'dark';
|
|
if (mode === 'light') return 'light';
|
|
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
}
|
|
|
|
function applyDocumentClass(theme: 'light' | 'dark') {
|
|
if (theme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}
|
|
|
|
export function AppearanceProvider({ children }: { children: React.ReactNode }) {
|
|
const [appearance, setAppearance] = useState<Appearance>(() => {
|
|
const stored = localStorage.getItem('theme') as Appearance | null;
|
|
return stored ?? 'system';
|
|
});
|
|
const [resolved, setResolved] = useState<'light' | 'dark'>(() => resolveTheme(appearance));
|
|
|
|
useEffect(() => {
|
|
const nextResolved = resolveTheme(appearance);
|
|
setResolved(nextResolved);
|
|
applyDocumentClass(nextResolved);
|
|
|
|
if (appearance === 'system') {
|
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
const listener = () => {
|
|
const resolvedTheme = mediaQuery.matches ? 'dark' : 'light';
|
|
setResolved(resolvedTheme);
|
|
applyDocumentClass(resolvedTheme);
|
|
};
|
|
mediaQuery.addEventListener('change', listener);
|
|
return () => mediaQuery.removeEventListener('change', listener);
|
|
}
|
|
|
|
return undefined;
|
|
}, [appearance]);
|
|
|
|
const updateAppearance = (mode: Appearance) => {
|
|
setAppearance(mode);
|
|
localStorage.setItem('theme', mode);
|
|
};
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
appearance,
|
|
resolved,
|
|
updateAppearance,
|
|
}),
|
|
[appearance, resolved]
|
|
);
|
|
|
|
return React.createElement(AppearanceContext.Provider, { value }, children);
|
|
}
|
|
|
|
export function useAppearance(): AppearanceContextValue {
|
|
return useContext(AppearanceContext);
|
|
}
|
|
|
|
export function initializeTheme() {
|
|
const stored = localStorage.getItem('theme') as Appearance | null;
|
|
const mode = stored ?? 'system';
|
|
const resolved = resolveTheme(mode);
|
|
applyDocumentClass(resolved);
|
|
}
|