# 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='']` 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 ``) 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.