The screenshots said seven dark screens differed and could not say why. Comparing COMPUTED theme variables between the old build and the new one, in the same colour mode, answered it in a single run: 47 of 53 matched exactly, and the six that did not were the `--duck-*` variables — EMPTY in the new build. The utilities were remapped to `--brand-primary`, `--ink` and friends, which is correct. What that missed is code reading the VARIABLE rather than using the utility. `prose.css` does exactly that eleven times — chat markdown links, blockquotes, inline code, checkboxes — and `landing/styles.css` does it six more. All silently resolved to nothing. Fixed by keeping the legacy names as aliases in every theme, pointing at the new source of truth rather than duplicating values. That also covers any plugin or inline style doing the same thing, which I have not enumerated. They retire when the utility rename lands. Every variable now matches. ── What the screenshots are actually worth here ── Less than I assumed. The wallpaper rotates per page load, so a control capture of the same build against itself had 4 of 16 screens differ — and a different 4 each time. Between 5 and 6 come out byte-identical to the baseline on any run; the rest is noise I mistook for signal for two rounds, including one where I removed a `color-scheme` line to test a theory it turned out not to explain. The variable diff is the instrument that settles it, and it is cheap. Added as `scripts/theme-vardiff.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
7.3 KiB
Markdown
137 lines
7.3 KiB
Markdown
# Themes decoupling — working notes
|
||
|
||
Branch `themes-decoupling`, worktree `platform-themes`. Nothing here is on master.
|
||
|
||
Read this before the diff. It is the state of the work, the decisions taken and why, and the things
|
||
deliberately left alone.
|
||
|
||
---
|
||
|
||
## Where it got to
|
||
|
||
The visual layer is out of the app and into `src/workspaces/theme`. The application no longer contains any
|
||
colour: it imports one stylesheet and mounts one provider.
|
||
|
||
Proven by the two things that matter:
|
||
|
||
- **Duck renders identically to before.** Panels, wallpaper, grid fills, glass, logo — checked against
|
||
screenshots taken before anything was touched.
|
||
- **Plain renders as white with black ink**, with no component edits. Panels turn white, the teal avatar
|
||
turns black, grid fills disappear. That is the decoupling working.
|
||
|
||
Switch themes from the avatar menu, top right.
|
||
|
||
---
|
||
|
||
## The shape
|
||
|
||
```
|
||
src/workspaces/theme/
|
||
css/base.css structure — Tailwind, the @theme contract, element defaults. NO values.
|
||
css/themes/duck-light.css the app's original appearance, moved unchanged
|
||
css/themes/duck-dark.css ditto, the old .dark block
|
||
css/themes/plain.css white/black — an instrument, not a design
|
||
css/chrome.css scrollbars and overlays, now in theme variables
|
||
css/index.css the single import an app makes
|
||
src/themes.ts the registry: id, name, family, applyDarkClass. Nothing about appearance.
|
||
src/ThemeProvider.tsx sets data-theme, toggles .dark, persists the choice
|
||
```
|
||
|
||
A theme is a CSS file scoped to `[data-theme='<id>']` plus one line in the registry. Nothing else.
|
||
|
||
---
|
||
|
||
## Decisions, and why
|
||
|
||
**Light and dark are separate themes, not a mode.** The owner's call, and it removes a concept from the
|
||
app. `Duck Light` and `Duck Dark` are siblings. Written out in full rather than as a diff — a theme that
|
||
only makes sense in the presence of another cannot be authored by a third party or sold to a customer.
|
||
|
||
**`.dark` survives as a compatibility flag.** 396 `dark:` utilities are written into 33 components and they
|
||
resolve against that class. A theme sets `applyDarkClass` and they keep working. Shim with a known end
|
||
date; removing those 396 retires it.
|
||
|
||
**`duck-*` utility names are kept, pointed at neutral variables.** ~1,470 occurrences across 108 files.
|
||
Renaming them and extracting in the same change would make a visual difference impossible to attribute.
|
||
They now resolve through `--brand-primary`, `--ink`, and friends, so a non-Duck theme can define them
|
||
sensibly even while the spelling is wrong.
|
||
|
||
**`duck-dark` is `--ink`.** It was never a brand colour: `#14532d` in light, near-white in dark. 946
|
||
components use it 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` can be `rgba()`, a gradient or an
|
||
image. A panel background that could only be a flat colour would rule out the glass the app has now.
|
||
|
||
**The old provider file is a re-export.** Six call sites keep working untouched, so the extraction is one
|
||
variable.
|
||
|
||
---
|
||
|
||
## Blockers and open questions — for the morning
|
||
|
||
**1. The reference repo (`vimazing`) has not been read yet.** The switching mechanism (`data-theme` on
|
||
`<html>`) and the packaging format are deliberately the dumbest thing that works, because that repo will
|
||
dictate them. Everything above is independent of that choice.
|
||
|
||
**2. Leaks the Plain theme exposed.** These are components painting themselves rather than asking the
|
||
theme. The screenshots in `.snapshots/plain/` are the punch list:
|
||
|
||
- **The wallpaper.** Still renders under Plain, so it is set by a component, not `--page-bg`. Probably the
|
||
dashboard layout. Needs to become a theme-owned surface.
|
||
- **Panel radius.** `--panel-radius: 0` had no effect — panels hardcode `rounded-*`. Shape is themeable in
|
||
principle and is not yet.
|
||
- **The logo.** A fixed image. Fine for Duck, wrong for white-labelling: a customer's build needs its own.
|
||
- **~985 raw Tailwind palette usages** (`text-emerald-400`, `bg-black/10`) and **~119 hex literals**. The
|
||
worst concentration by far is `officerdev/src/apps/Soulseek/` (~285). `MetricCard.tsx` is the
|
||
highest-leverage single fix — a shared component hardcoding its palette.
|
||
|
||
**3. Server settings still store `appearance.colorMode`, not a theme id.** `useThemeSync` used to stamp the
|
||
server's colour mode over the local theme on every load — picking Plain and reloading silently reverted it.
|
||
Now the server value is only honoured when the browser has never chosen. The proper fix is storing a theme
|
||
id, which needs a settings migration.
|
||
|
||
**4. A pre-existing bug, unrelated but adjacent.** The server's default is `appearance: { theme: 'light' }`
|
||
while the client reads `appearance.colorMode` — so a fresh account's server default has never been read.
|
||
Left alone.
|
||
|
||
**5. `rebuildFrontend()` reports `"Bundle failed"` and discards the bundler's actual errors.** Cost twenty
|
||
minutes tonight. Same shape as the agent bugs from earlier — a failure that refuses to say why.
|
||
|
||
**6. Landing and marketplace are untouched.** They have their own copies of the palette; `duck-teal` has
|
||
three different definitions across the three apps. Folding them into this layer is a natural follow-up and
|
||
was not in tonight's scope.
|
||
|
||
---
|
||
|
||
## On verification, and what the screenshots can and cannot tell you
|
||
|
||
The byte comparison is **dominated by wallpaper noise**. The dashboard background rotates per page load, so
|
||
any screen showing it differs from itself between runs — a control capture of the same build against itself
|
||
had 4 of 16 differ, and which 4 changes each time. Between 5 and 6 screens come out byte-identical to the
|
||
pre-refactor baseline on any given run; the rest are the wallpaper.
|
||
|
||
So the authoritative instrument is `scripts/theme-vardiff.ts`, which compares the COMPUTED value of every
|
||
theme variable between the old and new builds in the same colour mode. It reports **all 53 identical**.
|
||
That is a much stronger statement than a screenshot: it says the token layer resolves to exactly the values
|
||
the old stylesheet produced.
|
||
|
||
It also found the one real regression, which no amount of looking would have: six `--duck-*` variables came
|
||
out EMPTY in the new build. The utilities were remapped, but `prose.css` reads `var(--duck-teal)` DIRECTLY
|
||
in eleven places for chat markdown — links, blockquotes, inline code, checkboxes — and `landing/styles.css`
|
||
does the same. Fixed by keeping the legacy names as aliases in every theme, which also protects any plugin
|
||
or inline style doing the same thing. They retire with the utility rename.
|
||
|
||
## Tools built
|
||
|
||
- `scripts/theme-snapshot.ts` — captures 8 routes × 2 modes, or a pinned theme with `--theme`. Compares two
|
||
directories byte-for-byte. Its first version silently photographed light twice; it now warns when a
|
||
light/dark pair is identical.
|
||
- `scripts/theme-vardiff.ts` — computed theme variables, old build vs new, side by side. The instrument
|
||
that actually proved the extraction faithful.
|
||
- `scripts/theme-preview-server.ts` — serves this worktree's build against the RUNNING platform's API, so
|
||
the new frontend can be seen with real data without starting a second platform beside the live one.
|
||
|
||
Byte comparison is too strict for before/after across two builds: this worktree has no plugins, so the dock
|
||
differs on every page, and live data moves between captures. Use it to find *which* screens changed, then
|
||
look at them.
|