feat: improve theme system with cookie and prefers-color-scheme
This commit is contained in:
@@ -1,18 +1,65 @@
|
|||||||
type Theme = 'light' | 'dark';
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
const STORAGE_KEY = 'theme';
|
const STORAGE_KEY = 'theme';
|
||||||
|
const COOKIE_KEY = 'theme';
|
||||||
|
|
||||||
|
const getLocalStorage = (): Storage | null => {
|
||||||
|
try {
|
||||||
|
return window.localStorage;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const readCookie = (key: string): string | null => {
|
||||||
|
const entries = document.cookie.split(';').map(part => part.trim());
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!entry) continue;
|
||||||
|
const eqIndex = entry.indexOf('=');
|
||||||
|
if (eqIndex === -1) continue;
|
||||||
|
const k = entry.slice(0, eqIndex).trim();
|
||||||
|
if (k !== key) continue;
|
||||||
|
return decodeURIComponent(entry.slice(eqIndex + 1));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeCookie = (key: string, value: string): void => {
|
||||||
|
const maxAgeSeconds = 60 * 60 * 24 * 365;
|
||||||
|
document.cookie = `${encodeURIComponent(key)}=${encodeURIComponent(value)}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 getSavedTheme = (): Theme => {
|
||||||
const raw = localStorage.getItem(STORAGE_KEY);
|
const storage = getLocalStorage();
|
||||||
if (raw === 'light' || raw === 'dark') {
|
const fromStorage = normalizeTheme(storage?.getItem(STORAGE_KEY) ?? null);
|
||||||
return raw;
|
if (fromStorage) return fromStorage;
|
||||||
}
|
|
||||||
return 'dark'; // default to dark
|
const fromCookie = normalizeTheme(readCookie(COOKIE_KEY));
|
||||||
|
if (fromCookie) return fromCookie;
|
||||||
|
|
||||||
|
return getPreferredTheme();
|
||||||
};
|
};
|
||||||
|
|
||||||
const applyTheme = (theme: Theme): void => {
|
const applyTheme = (theme: Theme): void => {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem(STORAGE_KEY, theme);
|
document.documentElement.style.colorScheme = theme;
|
||||||
|
const storage = getLocalStorage();
|
||||||
|
try {
|
||||||
|
storage?.setItem(STORAGE_KEY, theme);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
writeCookie(COOKIE_KEY, theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cycleTheme = (): void => {
|
const cycleTheme = (): void => {
|
||||||
@@ -27,11 +74,11 @@ const initTheme = (): void => {
|
|||||||
|
|
||||||
// delegated click handler on theme buttons
|
// delegated click handler on theme buttons
|
||||||
document.addEventListener('click', e => {
|
document.addEventListener('click', e => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target;
|
||||||
const btn = target.closest('#theme-toggle, #footer-theme-toggle') as HTMLButtonElement | null;
|
if (!(target instanceof Element)) return;
|
||||||
if (btn) {
|
const btn = target.closest('#theme-toggle, #footer-theme-toggle');
|
||||||
cycleTheme();
|
if (!(btn instanceof HTMLButtonElement)) return;
|
||||||
}
|
cycleTheme();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user