a ratchet on hardcoded colour, and the docs to match

The three previous attempts at this job died of endlessness, not difficulty. Nothing
stopped the next component arriving with its own `text-emerald-400` while an old one
was being fixed, so the number never fell for long.

`bun run check:theme` counts palette classes per directory against a committed
baseline. It does not care what the number is, only which way it moves. 942 across 21
directories today. Every directory can be paid down independently and none can quietly
regrow — which turns an infinite job into a finite one.

A baseline rather than a ban, deliberately: a rule forbidding palette classes outright
fails on its first run, and a check nobody can make pass is a check everybody turns
off.

Proven by adding one `text-emerald-400` and watching it fail with the directory, the
delta, and what to use instead.

── Documentation, which is part of the work rather than after it ──

`src/workspaces/theme/README.md` is new and is the contract: how to write a theme, all
84 declarations by family, why colours are unwrapped HSL triplets while surfaces are
complete CSS values, why assets are quoted paths, and the rules for components. Start
from Plain, not from Duck — every value you change is then one you decided about.

`plugins/EXTRACTING-A-PLUGIN.md` gains a rule beside the others: a plugin never
hardcodes a colour, and pays its debt down DURING the extraction. The counts are named
there — Soulseek 313, Invoices 95, Wallet 68 — because "some colours" does not convey
that it is two thirds of the platform's total. The kickoff prompt says the same thing
in the form a fresh agent will meet it.

That is what makes the plugin exemption safe rather than a way of losing the work: the
ratchet stops covering a directory the moment it leaves, so the rule has to travel with
it.

`THEMES-NOTES.md` rewritten to match reality — it still listed done things as open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 23:42:56 +00:00
co-authored by Claude Opus 5
parent df92a985d2
commit f977d26fef
7 changed files with 375 additions and 116 deletions
+14
View File
@@ -82,3 +82,17 @@ still open.
Then update `plugins/EXTRACTING-A-PLUGIN.md`: add what this extraction taught, and correct anything it
told you that turned out to be wrong. The runbook is the thing that makes the next one cheaper, and it is
only worth that if each extraction leaves it more accurate than it found it.
## Colour is part of the job
The feature you are extracting almost certainly hardcodes Tailwind palette classes — `text-emerald-400`,
`bg-zinc-800`, `border-red-500/30`. Replace them with semantic tokens as you go: `text-success`,
`bg-muted`, `border-destructive`, `bg-busy/15`. They follow the theme; a palette number does not.
Read `src/workspaces/theme/README.md` for the contract and `src/workspaces/components/Data/tone.ts` for the
closed set of state tones. `bun run check:theme` counts what is left.
Do it during the extraction rather than after. You are already rewriting every import in these files, and
once the directory leaves the platform the ratchet stops covering it.
+16
View File
@@ -37,6 +37,22 @@ file is **refused at discovery, by name**. Non-compliance is unrepresentable, no
of any sort. The platform's answer is uniform; what a grant _means_ — whose rows a member sees, whether a
resource is shared or per-user — is the plugin's own job, in its own queries.
**A plugin never hardcodes a colour.** Every colour comes from a semantic token — `bg-destructive`,
`text-muted-foreground`, `bg-busy/15`, `border-border` — and never from Tailwind's palette
(`text-emerald-400`, `bg-zinc-800`, `border-red-500/30`). Palette classes do not follow the theme, so a
plugin using them looks wrong under every theme but the one its author happened to have. The contract is
`src/workspaces/theme/README.md`; the closed set of state tones is `components/Data/tone.ts`.
Pay this down **as part of the extraction**, not afterwards. You are already rewriting the imports, and the
counts are not small: Soulseek carries 313 palette classes, Invoices 95, Wallet 68, Jellyfin 48, Photos 47,
Transmission 41. Left alone, an extraction exports its colour debt into a repository nobody will revisit —
and the platform's own ratchet (`bun run check:theme`) stops covering it the moment the directory leaves.
Two shortcuts that are not shortcuts. `text-white` and `bg-black/60` are fine ONLY over media — a video, a
photograph — where they sit on someone's content rather than on a themed surface; everywhere else they are
palette classes like any other. And a colour that carries no state (a brand accent, a chart series) belongs
in the plugin's own variables, declared once, not sprinkled as literals.
**Say `permissions`, never the other word.** It already means three things in this codebase.
**The manifest holds only what a directory listing cannot say.** Identity facts and human choices: