Six palettes, each with a light half and a dark one #209

Closed
opened 2026-09-02 07:15:21 +00:00 by jcoffey-dev · 0 comments
Owner

The theme was one enum — system | light | dark | ihasmail — where one value carried a whole palette and implied dark. That works for exactly one palette. The two questions now come apart: which palette, and which side.

Palette
Classic The plain light and dark this app has always had
ihasmail The project's own, now with a day version
Dracula with Alucard as its light half
Gruvbox
Rosé Pine with Dawn
Tokyo Night with Day

Sourcing

All four borrowed palettes are MIT and were fetched from each project's own repository, not from anyone's reimplementation. What was fetched is recorded verbatim in .palette-sources/palettes-upstream.md, and credited in a new NOTICE.

Worth flagging a correction to something I said earlier in planning: I believed Alucard was part of the commercial Dracula PRO and that Dracula would have to ship dark-only. It is not — the repository's section is titled "Color Palette (OSS)" and contains both.

The contrast pass is the substance

These projects publish twelve to twenty values; ihasmail needs about thirty. scripts/build-palettes.py derives the tiers between them, then measures every text colour against the surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts anything short towards white on a dark ground and towards black on a light one, so a lifted tier keeps its hue instead of washing out. It refuses to write a palette that would not pass.

Every one of the nine halves needed at least one lift. These palettes are built for code editors, not prose at this size:

  • Dracula's comment grey #6272a43.03:1 on its own background, lifted to #8390b7 at 4.50
  • Rosé Pine's gold on Dawn — 2.7:1, lifted
  • Almost every border-strong tier was under 3:1

Shipping them as published would have quietly ended the WCAG AA claim two sections down in FEATURES.md.

What giving ihasmail a light half removed

Nothing is one-sided any more, and a whole special case went with it: a palette can no longer override the mode, the toggle no longer sets a palette aside on the way to light and remembers it, and the greyed-out control that explained all that is gone. lastDarkTheme — which existed only for that — is gone too.

Two bugs caught while checking, not while writing

Choosing an accent over a new palette did nothing at all. The generated blocks were appended to the end of the stylesheet, putting them after the accent variants at equal specificity. Verified live: picking purple on Gruvbox left the accent at #076678. They now sit before those rules — which the existing ihasmail block's own comment says is exactly where they have to be.

The hand-written ihasmail block was unqualified (:root[data-palette="ihasmail"]), so it would have shadowed the new light half. It is now explicitly the dark one.

Migration

Settings written before this carry theme and no palette, and are read through the old enum — indefinitely, not for one release, since settings live in the account's Files and are opened by whatever version runs next. theme is still written back, derived, because a device on an older build reads it and would otherwise be stranded on a theme nobody chose.

Testing

16 new palette tests plus rewritten theme and settings-sync tests: migration of all four old values, the derived legacy write-back, the toggle keeping colours across every palette, and a file carrying both shapes preferring the new one.

Suite green: web 787 across 84 files, server 116, typecheck clean.

Verified in the running app

Gruvbox light paints upstream's light0 #fbf1c7 exactly; ihasmail day gives --bg #f4f9f9 with --fg #0d2430 (the dark palette's background as its text); theme-color follows --bg; and the accent override works again on a palette.

Merged 2026-09-02 as coffey-labs/ihasmail@0ac5f78789

Rebuilt from: git history, session transcript.

The theme was one enum — `system | light | dark | ihasmail` — where one value carried a whole palette and implied dark. That works for exactly one palette. The two questions now come apart: **which palette**, and **which side**. | Palette | | | --- | --- | | **Classic** | The plain light and dark this app has always had | | **ihasmail** | The project's own, now with a day version | | **Dracula** | with Alucard as its light half | | **Gruvbox** | | | **Rosé Pine** | with Dawn | | **Tokyo Night** | with Day | ## Sourcing All four borrowed palettes are MIT and were fetched from **each project's own repository**, not from anyone's reimplementation. What was fetched is recorded verbatim in `.palette-sources/palettes-upstream.md`, and credited in a new `NOTICE`. Worth flagging a correction to something I said earlier in planning: I believed Alucard was part of the commercial Dracula PRO and that Dracula would have to ship dark-only. It is not — the repository's section is titled *"Color Palette (OSS)"* and contains both. ## The contrast pass is the substance These projects publish twelve to twenty values; ihasmail needs about thirty. `scripts/build-palettes.py` derives the tiers between them, then measures every text colour against the surface it sits on — 4.5:1 for prose, 3:1 for borders and marks — and lifts anything short *towards white on a dark ground and towards black on a light one*, so a lifted tier keeps its hue instead of washing out. **It refuses to write a palette that would not pass.** **Every one of the nine halves needed at least one lift.** These palettes are built for code editors, not prose at this size: - Dracula's comment grey `#6272a4` — **3.03:1** on its own background, lifted to `#8390b7` at 4.50 - Rosé Pine's gold on Dawn — **2.7:1**, lifted - Almost every `border-strong` tier was under 3:1 Shipping them as published would have quietly ended the WCAG AA claim two sections down in FEATURES.md. ## What giving ihasmail a light half removed Nothing is one-sided any more, and a whole special case went with it: a palette can no longer override the mode, the toggle no longer sets a palette aside on the way to light and remembers it, and the greyed-out control that explained all that is gone. `lastDarkTheme` — which existed only for that — is gone too. ## Two bugs caught while checking, not while writing **Choosing an accent over a new palette did nothing at all.** The generated blocks were appended to the end of the stylesheet, putting them *after* the accent variants at equal specificity. Verified live: picking purple on Gruvbox left the accent at `#076678`. They now sit before those rules — which the existing ihasmail block's own comment says is exactly where they have to be. **The hand-written ihasmail block was unqualified** (`:root[data-palette="ihasmail"]`), so it would have shadowed the new light half. It is now explicitly the dark one. ## Migration Settings written before this carry `theme` and no palette, and are read through the old enum — indefinitely, not for one release, since settings live in the account's Files and are opened by whatever version runs next. `theme` is still **written back, derived**, because a device on an older build reads it and would otherwise be stranded on a theme nobody chose. ## Testing 16 new palette tests plus rewritten theme and settings-sync tests: migration of all four old values, the derived legacy write-back, the toggle keeping colours across every palette, and a file carrying both shapes preferring the new one. Suite green: web 787 across 84 files, server 116, typecheck clean. ## Verified in the running app Gruvbox light paints upstream's `light0` `#fbf1c7` exactly; ihasmail day gives `--bg #f4f9f9` with `--fg #0d2430` (the dark palette's background as its text); `theme-color` follows `--bg`; and the accent override works again on a palette. **Merged** 2026-09-02 as coffey-labs/ihasmail@0ac5f7878964 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.