Add the ihasmail theme, and make it the default
A dark theme carrying ihasmail.org's palette: a teal-navy ground rather
than the blue-slate of the plain dark theme, with the orange the logo's
cat is drawn in doing the work of the star and the warning colour. The
values are the site's own, read from its stylesheet rather than picked
by eye.
It is a theme rather than an accent because it changes backgrounds,
borders and text as well as the highlight -- an accent could not.
It rides on data-theme="dark" and adds data-palette="ihasmail" on top,
so the eleven dark-only rules further down the stylesheet keep applying
without being duplicated for a second dark theme. Specificity then does
something deliberate: the palette block is 0,2,0 and the accent variants
are 0,3,0, so a chosen accent still wins over it -- and because the
default accent ("teal") has no rule of its own, ihasmail.org's accent is
what shows until someone picks another. Verified both ways in a browser.
It is now what a new account starts on, so the app looks like itself
before anyone has chosen anything. Only a default: a stored theme always
wins, which leaves everyone already using ihasmail where they are, since
the setting is saved whether or not they deliberately picked it.
While here, the theme-color meta tag was fixed. There were two, both
carrying media attributes, and applyTheme looks for
:not([media]) -- so it matched neither and the browser chrome had never
followed the chosen theme at all, only what the OS preferred. One tag
now, updated from JS, starting at the default theme's background so the
first paint is right too.
Contrast measured rather than assumed, against the theme's own
background: text 14.5:1, muted 8.6:1, faint 6.4:1, accent 8.0:1, link
9.8:1, star 7.9:1, accent-on-accent 8.4:1. All AA or better.
This commit is contained in:
@@ -4,7 +4,12 @@ import { loadJson, saveJson } from "@/lib/storage";
|
||||
import { queueSettingsPush } from "@/lib/settingsSync";
|
||||
import { setDateTimePrefs, type DateFormat, type TimeFormat } from "@/lib/datetime";
|
||||
|
||||
export type Theme = "system" | "light" | "dark";
|
||||
/**
|
||||
* "ihasmail" is a dark theme carrying the palette from ihasmail.org. It is a
|
||||
* theme rather than an accent because it changes the backgrounds, borders and
|
||||
* text as well as the highlight colour — an accent could not.
|
||||
*/
|
||||
export type Theme = "system" | "light" | "dark" | "ihasmail";
|
||||
export type Density = "comfortable" | "cozy" | "compact";
|
||||
export type ReadingPane = "right" | "bottom" | "off";
|
||||
export type ImagePolicy = "ask" | "always" | "contacts";
|
||||
@@ -86,7 +91,14 @@ export interface Settings {
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
theme: "system",
|
||||
/**
|
||||
* ihasmail's own palette is what a new account gets, so the app looks like
|
||||
* itself before anyone has chosen anything. It is only a default: a stored
|
||||
* theme always wins, so nobody who has picked one — including everyone
|
||||
* already using ihasmail, whose choice is saved even if they never changed
|
||||
* it — is moved off it.
|
||||
*/
|
||||
theme: "ihasmail",
|
||||
accent: "teal",
|
||||
density: "cozy",
|
||||
readingPane: "right",
|
||||
@@ -247,16 +259,28 @@ function applyDateTimePrefs(s: Settings): void {
|
||||
setDateTimePrefs({ locale: s.locale, dateFormat: s.dateFormat, timeFormat: s.timeFormat });
|
||||
}
|
||||
|
||||
/** Background of each theme, for the browser chrome (`theme-color`). */
|
||||
const THEME_COLOR = { light: "#ffffff", dark: "#0b1220", ihasmail: "#0d2430" } as const;
|
||||
|
||||
export function applyTheme(s: Settings = useSettings.getState().settings): void {
|
||||
const root = document.documentElement;
|
||||
const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
|
||||
const dark = s.theme === "dark" || (s.theme === "system" && prefersDark);
|
||||
const dark = isDarkTheme(s.theme, prefersDark);
|
||||
// ihasmail keeps data-theme="dark" and adds a palette on top, so every
|
||||
// dark-only rule in the stylesheet applies to it without being repeated.
|
||||
root.dataset.theme = dark ? "dark" : "light";
|
||||
if (s.theme === "ihasmail") root.dataset.palette = "ihasmail";
|
||||
else delete root.dataset.palette;
|
||||
root.dataset.density = s.density;
|
||||
root.dataset.accent = s.accent;
|
||||
root.dataset.fontsize = s.fontSize;
|
||||
const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]:not([media])');
|
||||
if (meta) meta.content = dark ? "#0b1220" : "#ffffff";
|
||||
if (meta) meta.content = s.theme === "ihasmail" ? THEME_COLOR.ihasmail : dark ? THEME_COLOR.dark : THEME_COLOR.light;
|
||||
}
|
||||
|
||||
/** Whether a theme paints dark, resolving "system" against the OS. */
|
||||
export function isDarkTheme(theme: Theme, prefersDark = false): boolean {
|
||||
return theme === "dark" || theme === "ihasmail" || (theme === "system" && prefersDark);
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
@@ -278,7 +302,7 @@ export function useEffectiveTheme(): "light" | "dark" {
|
||||
mq.addEventListener("change", onChange);
|
||||
return () => mq.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
return theme === "dark" || (theme === "system" && systemDark) ? "dark" : "light";
|
||||
return isDarkTheme(theme, systemDark) ? "dark" : "light";
|
||||
}
|
||||
|
||||
export const settings = () => useSettings.getState().settings;
|
||||
|
||||
Reference in New Issue
Block a user