first commit
This commit is contained in:
106
laravel/resources/js/hooks/use-appearance.tsx
Normal file
106
laravel/resources/js/hooks/use-appearance.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
||||
|
||||
export type ResolvedAppearance = 'light' | 'dark';
|
||||
export type Appearance = ResolvedAppearance | 'system';
|
||||
|
||||
export type UseAppearanceReturn = {
|
||||
readonly appearance: Appearance;
|
||||
readonly resolvedAppearance: ResolvedAppearance;
|
||||
readonly updateAppearance: (mode: Appearance) => void;
|
||||
};
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
let currentAppearance: Appearance = 'system';
|
||||
|
||||
const prefersDark = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
};
|
||||
|
||||
const setCookie = (name: string, value: string, days = 365): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
const maxAge = days * 24 * 60 * 60;
|
||||
document.cookie = `${name}=${value};path=/;max-age=${maxAge};SameSite=Lax`;
|
||||
};
|
||||
|
||||
const getStoredAppearance = (): Appearance => {
|
||||
if (typeof window === 'undefined') return 'system';
|
||||
|
||||
return (localStorage.getItem('appearance') as Appearance) || 'system';
|
||||
};
|
||||
|
||||
const isDarkMode = (appearance: Appearance): boolean => {
|
||||
return appearance === 'dark' || (appearance === 'system' && prefersDark());
|
||||
};
|
||||
|
||||
const applyTheme = (appearance: Appearance): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const isDark = isDarkMode(appearance);
|
||||
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
const subscribe = (callback: () => void) => {
|
||||
listeners.add(callback);
|
||||
|
||||
return () => listeners.delete(callback);
|
||||
};
|
||||
|
||||
const notify = (): void => listeners.forEach((listener) => listener());
|
||||
|
||||
const mediaQuery = (): MediaQueryList | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)');
|
||||
};
|
||||
|
||||
const handleSystemThemeChange = (): void => {
|
||||
applyTheme(currentAppearance);
|
||||
notify();
|
||||
};
|
||||
|
||||
export function initializeTheme(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (!localStorage.getItem('appearance')) {
|
||||
localStorage.setItem('appearance', 'system');
|
||||
setCookie('appearance', 'system');
|
||||
}
|
||||
|
||||
currentAppearance = getStoredAppearance();
|
||||
applyTheme(currentAppearance);
|
||||
|
||||
// Set up system theme change listener
|
||||
mediaQuery()?.addEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
|
||||
export function useAppearance(): UseAppearanceReturn {
|
||||
const appearance: Appearance = useSyncExternalStore(
|
||||
subscribe,
|
||||
() => currentAppearance,
|
||||
() => 'system',
|
||||
);
|
||||
|
||||
const resolvedAppearance: ResolvedAppearance = useMemo(
|
||||
() => (isDarkMode(appearance) ? 'dark' : 'light'),
|
||||
[appearance],
|
||||
);
|
||||
|
||||
const updateAppearance = useCallback((mode: Appearance): void => {
|
||||
currentAppearance = mode;
|
||||
|
||||
// Store in localStorage for client-side persistence...
|
||||
localStorage.setItem('appearance', mode);
|
||||
|
||||
// Store in cookie for SSR...
|
||||
setCookie('appearance', mode);
|
||||
|
||||
applyTheme(mode);
|
||||
notify();
|
||||
}, []);
|
||||
|
||||
return { appearance, resolvedAppearance, updateAppearance } as const;
|
||||
}
|
||||
Reference in New Issue
Block a user