mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
const THEME_KEY = "fleet-theme";
|
|
const TRANSITION_MS = 300;
|
|
|
|
export type ThemeMode = "system" | "light" | "dark";
|
|
|
|
const isThemeMode = (value: unknown): value is ThemeMode =>
|
|
value === "system" || value === "light" || value === "dark";
|
|
|
|
const systemPrefersDark = (): boolean => {
|
|
return (
|
|
typeof window !== "undefined" &&
|
|
typeof window.matchMedia === "function" &&
|
|
window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
);
|
|
};
|
|
|
|
export const getThemeMode = (): ThemeMode => {
|
|
let stored: string | null = null;
|
|
try {
|
|
stored = localStorage.getItem(THEME_KEY);
|
|
} catch {
|
|
// localStorage can throw in restricted environments; fall back to system.
|
|
}
|
|
return isThemeMode(stored) ? stored : "system";
|
|
};
|
|
|
|
const resolveDark = (mode: ThemeMode): boolean =>
|
|
mode === "system" ? systemPrefersDark() : mode === "dark";
|
|
|
|
export const isDarkMode = (): boolean => resolveDark(getThemeMode());
|
|
|
|
// Apply a theme change to the DOM and notify listeners. `animate` adds a
|
|
// blanket transition class so the whole UI cross-fades instead of snapping.
|
|
const applyDarkMode = (dark: boolean, animate: boolean): void => {
|
|
if (animate) {
|
|
document.body.classList.add("theme-transition");
|
|
setTimeout(() => {
|
|
document.body.classList.remove("theme-transition");
|
|
}, TRANSITION_MS);
|
|
}
|
|
document.body.classList.toggle("dark-mode", dark);
|
|
window.dispatchEvent(
|
|
new CustomEvent("fleet-theme-change", { detail: { dark } })
|
|
);
|
|
};
|
|
|
|
export const setThemeMode = (mode: ThemeMode): void => {
|
|
try {
|
|
if (mode === "system") {
|
|
localStorage.removeItem(THEME_KEY);
|
|
} else {
|
|
localStorage.setItem(THEME_KEY, mode);
|
|
}
|
|
} catch {
|
|
// localStorage can throw in restricted environments; the DOM still
|
|
// gets the updated theme below, the choice just won't persist.
|
|
}
|
|
applyDarkMode(resolveDark(mode), true);
|
|
};
|
|
|
|
export const initTheme = (): void => {
|
|
const mode = getThemeMode();
|
|
if (resolveDark(mode)) {
|
|
document.body.classList.add("dark-mode");
|
|
}
|
|
|
|
// Follow OS theme changes live — but only while the user is on "system".
|
|
// Once they've picked light or dark explicitly, their choice sticks
|
|
// regardless of what the OS does.
|
|
if (typeof window !== "undefined" && window.matchMedia) {
|
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
media.addEventListener("change", (e) => {
|
|
if (getThemeMode() !== "system") return;
|
|
applyDarkMode(e.matches, true);
|
|
});
|
|
}
|
|
};
|