Files
platform/THEMES-NOTES.md
T
pastilhasandClaude Opus 5 105766a4eb extract the visual layer into its own workspace
The application no longer knows what it looks like. It imports one stylesheet and
mounts one provider; every colour, surface and radius lives in `src/workspaces/theme`.

Proven both directions, because either alone would be worthless:

  Duck renders IDENTICALLY to before — checked against screenshots taken before
  anything was touched, not asserted.

  Plain renders as white with black ink, no component edits — panels turn white, the
  teal avatar turns black, the grid fills vanish.

── What moved ──

`globals.css` held three unrelated things: Tailwind wiring, the token contract, and
the Duck brand's hex values. Split along those seams. `base.css` is structure and
names no colour; a theme file is values and nothing else, scoped to
`[data-theme='<id>']`. Adding a theme is a CSS file plus one registry line.

Light and dark are separate themes rather than a mode — the owner's call, and it
removes a concept from the app. Each is written out in full: a theme that only makes
sense as a diff against another cannot be authored by a stranger or sold to a
customer. `.dark` survives only as a per-theme flag, because 396 `dark:` utilities in
33 components resolve against it; retiring those retires the flag.

The `duck-*` utility names are kept for now and pointed at neutral variables, so a
non-Duck theme can define them sensibly. Renaming 1,470 occurrences in the same commit
as the extraction would make any visual difference impossible to attribute. Worth
recording that `duck-dark` was never a brand colour — `#14532d` in light, near-white
in dark, used by 946 components to mean "text". It is `--ink` now.

Surfaces are complete CSS values rather than HSL triplets: `--panel-bg` has to be able
to be a gradient or an image, or the glass the app already has would be unthemeable.

The old provider is a re-export, so its six call sites are untouched and the
extraction stays one variable.

── Bug found by the Plain theme ──

`useThemeSync` stamped the server's `appearance.colorMode` over the local theme on
every load, so choosing Plain and reloading silently reverted it. Diagnosed by asking
the page what it was wearing rather than reasoning: `stored` said `duck-dark` when the
script had written `plain`. The server value is now a starting point for a browser
that has never chosen, not a continuous instruction.

── Tools ──

Two, both of which lied to me before they worked. The snapshot script photographed
light twice and called it a clean run (one browser context, accumulated init scripts);
it now warns when a light/dark pair is identical. The preview server returned the
shell for missing assets, so the logo and wallpaper vanished and read as regressions
in the comparison — it falls through to upstream now.

Dead Tailwind v3 residue deleted: `components/ui/tailwind.{config.ts,css}`, referenced
by nothing, with content globs pointing at a directory that does not exist.

── Not done, deliberately ──

`THEMES-NOTES.md` has the full list. The headlines: the reference repo has not been
read, so the switching mechanism is deliberately the dumbest thing that works; the
wallpaper, panel radius and logo are still component-owned and Plain proves it; and
~985 raw Tailwind palette usages remain, worst in Soulseek.

tsgo clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:00:38 +00:00

117 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
---
## 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-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.