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>
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user