Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

127 lines
2.9 KiB
TypeScript

/**
* Theme persistence utilities
* Handles light/dark theme with localStorage fallback and system preference detection
*/
export type Theme = "light" | "dark" | "glass" | "snow";
export const THEME_STORAGE_KEY = "deeptutor-theme";
type ThemeChangeListener = (theme: Theme) => void;
const themeListeners = new Set<ThemeChangeListener>();
/**
* Subscribe to theme changes
*/
export function subscribeToThemeChanges(
listener: ThemeChangeListener,
): () => void {
themeListeners.add(listener);
return () => themeListeners.delete(listener);
}
/**
* Notify all listeners of theme change
*/
function notifyThemeChange(theme: Theme): void {
themeListeners.forEach((listener) => listener(theme));
}
/**
* Get the stored theme from localStorage
*/
export function getStoredTheme(): Theme | null {
if (typeof window === "undefined") return null;
try {
const stored = localStorage.getItem(THEME_STORAGE_KEY);
if (
stored === "light" ||
stored === "dark" ||
stored === "glass" ||
stored === "snow"
) {
return stored;
}
} catch (e) {
// Silently fail - localStorage may be disabled
}
return null;
}
/**
* Save theme to localStorage
*/
export function saveThemeToStorage(theme: Theme): boolean {
if (typeof window === "undefined") return false;
try {
localStorage.setItem(THEME_STORAGE_KEY, theme);
return true;
} catch (e) {
// Silently fail - localStorage may be disabled or full
return false;
}
}
/**
* Get system preference for theme.
* Light systems get "snow" (the pure-white Default theme); dark systems
* get "dark". Must stay in sync with the inline ThemeScript fallback.
*/
export function getSystemTheme(): Theme {
if (typeof window === "undefined") return "snow";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "snow";
}
/**
* Apply theme to document
*/
export function applyThemeToDocument(theme: Theme): void {
if (typeof document === "undefined") return;
const html = document.documentElement;
html.classList.remove("dark", "theme-glass", "theme-snow");
if (theme === "dark") {
html.classList.add("dark");
} else if (theme === "glass") {
html.classList.add("dark", "theme-glass");
} else if (theme === "snow") {
html.classList.add("theme-snow");
}
}
/**
* Initialize theme on app startup
* Priority: localStorage > system preference (snow on light systems, dark on dark)
*/
export function initializeTheme(): Theme {
// Check localStorage first
const stored = getStoredTheme();
if (stored) {
applyThemeToDocument(stored);
return stored;
}
// Fall back to system preference
const systemTheme = getSystemTheme();
applyThemeToDocument(systemTheme);
saveThemeToStorage(systemTheme);
return systemTheme;
}
/**
* Set theme and persist it
*/
export function setTheme(theme: Theme): void {
applyThemeToDocument(theme);
saveThemeToStorage(theme);
notifyThemeChange(theme);
}