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>
110 lines
5.9 KiB
Markdown
110 lines
5.9 KiB
Markdown
# Themes — where this got to
|
||
|
||
Merged to master. The reference implementation is `src/workspaces/theme/README.md`; this file is the state
|
||
of the work — what was done, what was decided and why, and what is still open.
|
||
|
||
---
|
||
|
||
## Done
|
||
|
||
**The visual layer is out of the application.** It imports one stylesheet and mounts one provider, and
|
||
contains no colours of its own. A theme is one CSS file scoped to `[data-theme='<id>']` plus one line in
|
||
`src/workspaces/theme/src/themes.ts`.
|
||
|
||
**Three themes ship**: Duck Light, Duck Dark, Plain. Switch from the avatar menu. Light and dark are
|
||
separate themes, not modes — the owner's call, and it removes a concept from the app.
|
||
|
||
**Themed and no longer component-owned**: the page wallpaper, the graph-paper grid, the header, the logo,
|
||
the panel surface (background, border, radius, blur, the fullscreen and header-less variants), the dock,
|
||
cards, glass, scrollbars.
|
||
|
||
**Zero raw palette classes in the five core apps** — FileBrowser, FileViewer, SystemMonitor, Chat, Plugins
|
||
— for state colour. `text-white`/`bg-black` remain where they sit over media, which is correct in every
|
||
theme.
|
||
|
||
**A sixth tone, `busy`.** The 38 ambers meant *running*, not *warning* — Chat's own comment said so. The
|
||
vocabulary had no word for it. `--busy-ink` came with it and generalises: a fill bright enough to carry
|
||
black text is too light to BE text.
|
||
|
||
**The account stores a theme id**, so the choice follows the account rather than one browser's
|
||
localStorage. Accounts predating themes migrate from `appearance.colorMode` on the server, which is the
|
||
only place that can tell "never chose" from "chose the default".
|
||
|
||
**A ratchet**: `bun run check:theme`. Per-directory counts of palette classes that may fall and never
|
||
rise. 942 across 21 directories today. This is what makes the job finite — the previous attempts died of
|
||
endlessness, not difficulty.
|
||
|
||
**Build failures name their cause.** `Bun.build` throws an `AggregateError` whose message is the string
|
||
"Bundle failed" with the real diagnostics in `.errors`, so the `result.success` branch was unreachable and
|
||
the catch reported the wrapper. `throw: false` fixed years of `Bundle failed`.
|
||
|
||
**The landing app is deleted** — built elsewhere now, and it carried a third copy of the palette.
|
||
|
||
---
|
||
|
||
## Decisions worth not relitigating
|
||
|
||
**`duck-*` utility names are kept, pointed at neutral variables.** ~1,470 occurrences. Renaming them in the
|
||
same change as the extraction would have made a visual difference impossible to attribute. `--duck-teal`
|
||
and friends survive as aliases because `prose.css` reads them directly, eleven times, for chat markdown.
|
||
|
||
**`duck-dark` is `--ink`.** It was never a brand colour: `#14532d` in light, near-white in dark, used ~950
|
||
times to mean "text". That misnaming is why it is the most-used token in the app.
|
||
|
||
**Surfaces are complete CSS values, not HSL triplets.** `--panel-bg` must be able to be a gradient or
|
||
`rgba()`; a flat colour would rule out the glass Duck already has.
|
||
|
||
**Assets are quoted paths, not `url()`.** Bun's CSS bundler resolves `url()` at build time while these
|
||
files are served from `public/` at runtime. `useThemeAsset` reads the path and wraps it.
|
||
|
||
**`.dark` survives as a per-theme flag.** 396 `dark:` utilities in 33 components resolve against it.
|
||
Removing them retires the flag.
|
||
|
||
**Fidelity stopped being the constraint partway through.** Duck's exact colours were preserved while the
|
||
extraction needed to be provably invisible. The owner then said the palette is not precious — the
|
||
wallpaper, the grid and the mark are what matter — so shades collapse rather than being approximated.
|
||
|
||
---
|
||
|
||
## Open
|
||
|
||
**1. `vimazing`, the reference implementation, has not been read.** The switching mechanism (`data-theme`
|
||
on `<html>`) and the packaging format are deliberately the dumbest thing that works, because that repo
|
||
will dictate them.
|
||
|
||
**2. The plugin candidates carry ~600 palette classes** — Soulseek 313, Invoices 95, Wallet 68, Jellyfin
|
||
48, Photos 47, Transmission 41. Deliberately untouched: they are leaving this repository, and their colour
|
||
belongs to their extraction, when the imports are being rewritten anyway. The rule is now written into
|
||
`plugins/EXTRACTING-A-PLUGIN.md` and the kickoff prompt so each extraction pays down its own.
|
||
|
||
**3. `src/apps/officer-web/Screens` holds 144**, and `src/workspaces/components` 47 — the latter is the
|
||
higher-leverage one, being shared. `MetricCard.tsx` is the single worst shared component.
|
||
|
||
**4. Theme asset packaging for third parties is not designed.** A theme references its wallpaper and logo
|
||
by path, which works for themes we ship. Where a stranger's assets live — the intent is a convention under
|
||
`public/` — is not settled.
|
||
|
||
**5. The marketplace app has its own palette copy.** A skeleton awaiting a rewrite, so not worth folding
|
||
in yet.
|
||
|
||
**6. An appearance settings screen does not exist.** The theme picker is a list in the avatar menu,
|
||
deliberately the crudest control that proves the point.
|
||
|
||
---
|
||
|
||
## Verifying theme work
|
||
|
||
- `scripts/theme-vardiff.ts` — computed variables of two running builds. **The instrument that matters.**
|
||
It found the one real regression of the whole refactor: six `--duck-*` variables resolving to EMPTY,
|
||
because the utilities were remapped while `prose.css` read the variables directly.
|
||
- `scripts/theme-snapshot.ts` — screenshots, 8 routes × 2 modes, or a pinned theme.
|
||
- `scripts/theme-preview-server.ts` — this checkout's frontend against a running platform's API.
|
||
|
||
**Screenshots are noisier than they look.** The wallpaper rotates per page load, so a control capture of
|
||
one build against itself differs on 4 of 16 screens — and a different 4 each time. Two rounds were spent
|
||
chasing that noise. Use them to find *which* screens changed, then look; use the variable diff to decide
|
||
whether anything actually did.
|
||
|
||
**Render under Plain to find what is not themed.** Anything still coloured is a component painting itself.
|
||
That is how the wallpaper, the header and the logo were found.
|