67 lines
1.7 KiB
TypeScript
67 lines
1.7 KiB
TypeScript
type Theme = 'light' | 'dark';
|
|
|
|
const STORAGE_KEY = 'theme';
|
|
|
|
const getLocalStorage = (): Storage | null => {
|
|
try {
|
|
return window.localStorage;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getPreferredTheme = (): Theme => {
|
|
const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ?? false;
|
|
return prefersDark ? 'dark' : 'light';
|
|
};
|
|
|
|
const normalizeTheme = (raw: string | null): Theme | null => {
|
|
if (raw === 'light' || raw === 'dark') return raw;
|
|
return null;
|
|
};
|
|
|
|
const getSavedTheme = (): Theme => {
|
|
const storage = getLocalStorage();
|
|
const fromStorage = normalizeTheme(storage?.getItem(STORAGE_KEY) ?? null);
|
|
if (fromStorage) return fromStorage;
|
|
|
|
return getPreferredTheme();
|
|
};
|
|
|
|
const applyTheme = (theme: Theme): void => {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
document.documentElement.style.colorScheme = theme;
|
|
const storage = getLocalStorage();
|
|
try {
|
|
storage?.setItem(STORAGE_KEY, theme);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
const cycleTheme = (): void => {
|
|
const current = getSavedTheme();
|
|
const next: Theme = current === 'light' ? 'dark' : 'light';
|
|
applyTheme(next);
|
|
};
|
|
|
|
const initTheme = (): void => {
|
|
const saved = getSavedTheme();
|
|
applyTheme(saved);
|
|
|
|
// delegated click handler on theme buttons
|
|
document.addEventListener('click', e => {
|
|
const target = e.target;
|
|
if (!(target instanceof Element)) return;
|
|
const btn = target.closest('#theme-toggle, #footer-theme-toggle');
|
|
if (!(btn instanceof HTMLButtonElement)) return;
|
|
cycleTheme();
|
|
});
|
|
};
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initTheme);
|
|
} else {
|
|
initTheme();
|
|
}
|