Files
ihasmail-inbuxa/web/src/lib/palette.ts
T

134 lines
5.9 KiB
TypeScript

/**
* Palettes, and the two axes they replaced.
*
* The theme used to be one enum — `system | light | dark | ihasmail` — where
* "ihasmail" carried a whole palette and implied dark. That works for exactly
* one palette. With several, the two questions come apart: **which palette**
* (the colors) and **which mode** (light or dark), and they are chosen
* separately.
*
* Every palette here is taken from the project that publishes it, all MIT, and
* from that project's own repository rather than from anyone's reimplementation
* of it. The values are recorded in `.palette-sources/palettes-upstream.md` so
* the derivation can be checked rather than taken on trust.
*/
export type PaletteId =
| "default" | "ihasmail" | "dracula" | "gruvbox" | "rose-pine" | "tokyo-night"
| "catppuccin" | "solarized" | "ayu" | "kanagawa" | "everforest" | "primer";
export type Mode = "system" | "light" | "dark";
/** What a mode resolves to once the system has been asked. */
export type ResolvedMode = "light" | "dark";
export interface PaletteMeta {
id: PaletteId;
name: string;
/** Shown in Settings and in NOTICE; who to credit and under what. */
credit?: string;
/**
* Whether the name is a word rather than a name.
*
* All but one of these are proper names -- ihasmail, Dracula, Gruvbox and
* the rest -- and are rendered translate="no" so a page translator leaves
* them alone. "Classic" is not a name, it is an adjective describing the
* theme, and a German reader should see "Klassisch". Reported by a native
* speaker reviewing the German catalog (#247).
*/
translatable?: boolean;
}
export const PALETTES: PaletteMeta[] = [
{ id: "default", name: "Classic", translatable: true },
{ id: "ihasmail", name: "ihasmail" },
{ id: "dracula", name: "Dracula", credit: "Dracula Theme (MIT) — dark: Dracula, light: Alucard" },
{ id: "gruvbox", name: "Gruvbox", credit: "gruvbox by morhetz (MIT)" },
{ id: "rose-pine", name: "Rosé Pine", credit: "Rosé Pine (MIT) — light variant is Dawn" },
{ id: "tokyo-night", name: "Tokyo Night", credit: "Tokyo Night by enkia (MIT) — light variant is Day" },
{ id: "catppuccin", name: "Catppuccin", credit: "Catppuccin (MIT) — dark is Mocha, light is Latte" },
{ id: "solarized", name: "Solarized", credit: "Solarized by Ethan Schoonover (MIT) — light and dark are both original" },
{ id: "ayu", name: "Ayu", credit: "Ayu by Konstantin Pschera (MIT)" },
{ id: "kanagawa", name: "Kanagawa", credit: "Kanagawa by rebelot (MIT) — dark is Wave, light is Lotus" },
{ id: "everforest", name: "Everforest", credit: "Everforest by sainnhe (MIT)" },
// Named for the design system rather than for GitHub: the colors are MIT,
// the name and the logo are trademarks, and nothing here is endorsed.
{ id: "primer", name: "Primer", credit: "GitHub's Primer primitives (MIT); not affiliated with or endorsed by GitHub" },
];
const byId = new Map(PALETTES.map((p) => [p.id, p]));
export function paletteMeta(id: PaletteId | string | null | undefined): PaletteMeta {
return byId.get(id as PaletteId) ?? byId.get("default")!;
}
/**
* Which of light and dark is actually being drawn.
*
* Every palette has both halves, so this is only ever resolving "system"
* against the OS. That was not true while `ihasmail` was dark-only: the mode
* then had to be overridden by the palette, and the toggle had to remember
* which palette it had set aside on the way to light. Giving that palette a
* light half removed the override, the memory and the grayed-out control in
* one go.
*/
export function effectiveMode(mode: Mode, prefersDark: boolean): ResolvedMode {
if (mode === "system") return prefersDark ? "dark" : "light";
return mode;
}
export interface ThemeChoice {
palette: PaletteId;
mode: Mode;
}
/**
* The old enum, read as the two axes.
*
* Settings are stored in the account's own Files and are read by whatever
* version happens to open them next, so this has to keep working indefinitely
* rather than for one release.
*/
export function migrateTheme(theme: string | null | undefined): ThemeChoice {
switch (theme) {
case "ihasmail":
return { palette: "ihasmail", mode: "dark" };
case "light":
return { palette: "default", mode: "light" };
case "dark":
return { palette: "default", mode: "dark" };
case "system":
return { palette: "default", mode: "system" };
default:
// Unknown, absent, or written by something newer: the default is what a
// new account gets, and is never wrong in a way that hides mail.
return { palette: "ihasmail", mode: "dark" };
}
}
/**
* The old enum, written back alongside the new fields.
*
* A device still running an older build reads `theme` and ignores everything
* it does not know, so leaving it stale would show that device a theme nobody
* chose. It cannot express "Gruvbox", but it can express light or dark, which
* is the half that matters.
*/
export function legacyTheme(choice: ThemeChoice, prefersDark = false): "system" | "light" | "dark" | "ihasmail" {
// Only the dark half of ihasmail's own palette has an old name; its light
// half is new, and an older build has no word for it beyond "light".
if (choice.palette === "ihasmail" && effectiveMode(choice.mode, prefersDark) === "dark") return "ihasmail";
if (choice.palette === "default" && choice.mode === "system") return "system";
return effectiveMode(choice.mode, prefersDark);
}
/**
* Where the top-bar toggle goes.
*
* The palette never changes: only the mode flips. This used to be the awkward
* part -- leaving a dark-only palette for light meant changing palette too,
* and remembering which one to come back to -- and it stopped being awkward
* when every palette gained both halves.
*/
export function toggleTarget(current: ThemeChoice, prefersDark: boolean): ThemeChoice {
return { palette: current.palette, mode: effectiveMode(current.mode, prefersDark) === "dark" ? "light" : "dark" };
}