Files
mailflare/src/components/theme-utils.ts
T
Vagner Rodrigues a133ecf3e4 feat: dark mode with a light/dark/system theme setting
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.
2026-09-30 01:13:00 -03:00

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 }));
}