A theme references its wallpaper and logo by path because the CSS bundler cannot resolve a runtime `public/` file at build time. That is fine for themes we ship and says nothing about where a third party's assets live. The owner's intent is a convention under `public/` — recorded so the workaround is not mistaken for a design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
7.9 KiB
Markdown
144 lines
7.9 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. Theme ASSETS have no packaging story yet — deferred deliberately.** A theme publishes an asset as a
|
||
quoted path (`--logo-src: '/officer-logo.svg'`) and `useThemeAsset` wraps it, because Bun's CSS bundler
|
||
resolves `url()` at build time while these files are served from `public/` at runtime. That works for
|
||
themes we ship. It does not answer where a THIRD PARTY's wallpaper and logo live. The owner's intent is a
|
||
convention under `public/` — something like `public/themes/<name>/` — so an author drops files in a known
|
||
structure and references them by path. Not designed yet; the token shape above already fits it.
|
||
|
||
**7. 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.
|