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