Screens use the Tailwind palette directly (bg-white, text-neutral-900, bg-blue-50...). Instead of touching every component, `.dark` remaps the palette variables in globals.css: neutrals follow Gmail's dark palette (#202124 page, #292a2d cards, #303134/#35363a panels and hover, #3c4043/#5f6368 borders, #9aa0a6/#e8eaed text) and accents are inverted (50<->950, 100<->900...), with a few explicit exceptions: white text on coloured buttons, dark chips with white text, fixed-hex page backgrounds (including their hover: variants), soft shadows, and rendered HTML emails, which stay on light "paper". The preference lives in localStorage (`mailflare-theme`), is applied before first paint by a bootstrap script in <head> (same approach as the sidebar width), follows the OS when set to "system", and is chosen in Settings > Inbox > Appearance. The `light` class hardcoded on <body> is gone; the theme class lives on <html>. The pre-existing `dark:` classes in the command palette, shortcuts dialog and badge assumed the stock palette and were removed.
55 lines
2.2 KiB
TypeScript
55 lines
2.2 KiB
TypeScript
export type ThemePreference = "light" | "dark" | "system";
|
|
|
|
export const THEME_STORAGE_KEY = "mailflare-theme";
|
|
export const THEME_CHANGED_EVENT = "mailflare:theme-changed";
|
|
|
|
// Runs in <head> before paint, so the first frame already has the right theme (same approach as the
|
|
// sidebar width). Keep it in sync with resolveTheme/applyTheme below.
|
|
export const themeBootstrapScript = `(() => {
|
|
try {
|
|
const saved = localStorage.getItem("mailflare-theme");
|
|
const preference = saved === "light" || saved === "dark" ? saved : "system";
|
|
const dark = preference === "dark" || (preference === "system" && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
document.documentElement.classList.toggle("dark", dark);
|
|
document.documentElement.classList.toggle("light", !dark);
|
|
} catch {}
|
|
})();`;
|
|
|
|
export function isThemePreference(value: unknown): value is ThemePreference {
|
|
return value === "light" || value === "dark" || value === "system";
|
|
}
|
|
|
|
export function readThemePreference(): ThemePreference {
|
|
if (typeof window === "undefined") return "system";
|
|
try {
|
|
const saved = localStorage.getItem(THEME_STORAGE_KEY);
|
|
return isThemePreference(saved) ? saved : "system";
|
|
} catch {
|
|
return "system";
|
|
}
|
|
}
|
|
|
|
export function resolveTheme(preference: ThemePreference): "light" | "dark" {
|
|
if (preference !== "system") return preference;
|
|
if (typeof window === "undefined") return "light";
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
}
|
|
|
|
export function applyTheme(preference: ThemePreference): void {
|
|
if (typeof document === "undefined") return;
|
|
const dark = resolveTheme(preference) === "dark";
|
|
document.documentElement.classList.toggle("dark", dark);
|
|
document.documentElement.classList.toggle("light", !dark);
|
|
}
|
|
|
|
export function saveThemePreference(preference: ThemePreference): void {
|
|
try {
|
|
if (preference === "system") localStorage.removeItem(THEME_STORAGE_KEY);
|
|
else localStorage.setItem(THEME_STORAGE_KEY, preference);
|
|
} catch {
|
|
// Storage can be unavailable in private windows; the theme still applies for this page.
|
|
}
|
|
applyTheme(preference);
|
|
if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(THEME_CHANGED_EVENT, { detail: preference }));
|
|
}
|