Files
salam e687b4437b
linter / quality (push) Canceled after 0s
tests / ci (8.3) (push) Canceled after 0s
tests / ci (8.4) (push) Canceled after 0s
tests / ci (8.5) (push) Canceled after 0s
first commit
2026-10-06 18:53:03 +02:00

118 lines
3.0 KiB
TypeScript

import type { Appearance, ResolvedAppearance } from '@/types';
export type { Appearance, ResolvedAppearance };
export type ThemeState = {
appearance: {
value: Appearance;
};
resolvedAppearance: () => ResolvedAppearance;
updateAppearance: (value: Appearance) => void;
};
const appearance = $state<{ value: Appearance }>({ value: 'system' });
let themeChangeMediaQuery: MediaQueryList | null = null;
const prefersDark = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
const isDarkMode = (value: Appearance): boolean => {
return value === 'dark' || (value === 'system' && prefersDark());
};
const getResolvedAppearance = (): ResolvedAppearance => {
return isDarkMode(appearance.value) ? 'dark' : 'light';
};
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 applyTheme = (value: Appearance): void => {
if (typeof document === 'undefined') {
return;
}
const isDark = isDarkMode(value);
document.documentElement.classList.toggle('dark', isDark);
document.documentElement.style.colorScheme = isDark ? 'dark' : 'light';
};
const getStoredAppearance = (): Appearance => {
if (typeof window === 'undefined') {
return 'system';
}
const stored = localStorage.getItem('appearance');
return stored === 'light' || stored === 'dark' || stored === 'system'
? stored
: 'system';
};
const handleSystemThemeChange = (): void => {
applyTheme(appearance.value);
};
const detachThemeChangeListener = (): void => {
if (!themeChangeMediaQuery) {
return;
}
themeChangeMediaQuery.removeEventListener(
'change',
handleSystemThemeChange,
);
themeChangeMediaQuery = null;
};
export function initializeTheme(): () => void {
if (typeof window === 'undefined') {
return () => {};
}
if (!localStorage.getItem('appearance')) {
localStorage.setItem('appearance', 'system');
setCookie('appearance', 'system');
}
appearance.value = getStoredAppearance();
applyTheme(appearance.value);
detachThemeChangeListener();
themeChangeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
themeChangeMediaQuery.addEventListener('change', handleSystemThemeChange);
return detachThemeChangeListener;
}
export function updateAppearance(value: Appearance): void {
appearance.value = value;
if (typeof window !== 'undefined') {
localStorage.setItem('appearance', value);
}
setCookie('appearance', value);
applyTheme(value);
}
export function themeState(): ThemeState {
return {
appearance,
resolvedAppearance: getResolvedAppearance,
updateAppearance,
};
}