a ratchet on hardcoded colour, and the docs to match
The three previous attempts at this job died of endlessness, not difficulty. Nothing stopped the next component arriving with its own `text-emerald-400` while an old one was being fixed, so the number never fell for long. `bun run check:theme` counts palette classes per directory against a committed baseline. It does not care what the number is, only which way it moves. 942 across 21 directories today. Every directory can be paid down independently and none can quietly regrow — which turns an infinite job into a finite one. A baseline rather than a ban, deliberately: a rule forbidding palette classes outright fails on its first run, and a check nobody can make pass is a check everybody turns off. Proven by adding one `text-emerald-400` and watching it fail with the directory, the delta, and what to use instead. ── Documentation, which is part of the work rather than after it ── `src/workspaces/theme/README.md` is new and is the contract: how to write a theme, all 84 declarations by family, why colours are unwrapped HSL triplets while surfaces are complete CSS values, why assets are quoted paths, and the rules for components. Start from Plain, not from Duck — every value you change is then one you decided about. `plugins/EXTRACTING-A-PLUGIN.md` gains a rule beside the others: a plugin never hardcodes a colour, and pays its debt down DURING the extraction. The counts are named there — Soulseek 313, Invoices 95, Wallet 68 — because "some colours" does not convey that it is two thirds of the platform's total. The kickoff prompt says the same thing in the form a fresh agent will meet it. That is what makes the plugin exemption safe rather than a way of losing the work: the ratchet stops covering a directory the moment it leaves, so the rule has to travel with it. `THEMES-NOTES.md` rewritten to match reality — it still listed done things as open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
# Themes
|
||||
|
||||
Everything about how Officer looks lives here. The application imports one stylesheet and mounts one
|
||||
provider, and contains no colours of its own.
|
||||
|
||||
A theme is **one CSS file plus one line in a registry**. Nothing else.
|
||||
|
||||
---
|
||||
|
||||
## Writing one
|
||||
|
||||
1. Copy `css/themes/plain.css` — not a Duck theme. Plain is white, black and grey throughout, so every
|
||||
value you change is one you decided about, with nothing inherited from someone else's brand.
|
||||
2. Rename the selector: `[data-theme='plain']` → `[data-theme='your-theme']`.
|
||||
3. Add it to `css/index.css` and to `THEMES` in `src/themes.ts`.
|
||||
4. Reload. It appears in the avatar menu.
|
||||
|
||||
```ts
|
||||
{ id: 'your-theme', name: 'Your Theme', family: 'Yours', applyDarkClass: false }
|
||||
```
|
||||
|
||||
`applyDarkClass` puts `.dark` on the document. Set it for a dark theme: 396 `dark:` utilities are still
|
||||
written into components and they resolve against that class. It is a compatibility shim with an end date,
|
||||
not part of the design.
|
||||
|
||||
**Light and dark are separate themes, not modes.** `Duck Light` and `Duck Dark` are siblings, each 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.
|
||||
|
||||
---
|
||||
|
||||
## The contract
|
||||
|
||||
84 declarations. Copying Plain gives you all of them; these are the families and what they are for.
|
||||
|
||||
### Colour — HSL triplets, unwrapped
|
||||
|
||||
Written `0 0% 100%`, not `hsl(0 0% 100%)`, so `hsl(var(--x) / 0.5)` gives opacity for free. That is what
|
||||
makes `bg-card/50` work.
|
||||
|
||||
| Family | Tokens |
|
||||
|---|---|
|
||||
| Ground | `--background`, `--foreground` |
|
||||
| Surfaces | `--card(-foreground)`, `--popover(-foreground)`, `--muted(-foreground)`, `--accent(-foreground)` |
|
||||
| Actions | `--primary(-foreground)`, `--secondary(-foreground)`, `--cta(-foreground)`, `--ctahover` |
|
||||
| State | `--success`, `--warning`, `--destructive`, `--info`, `--busy` — each with `-foreground` |
|
||||
| Lines | `--border`, `--input`, `--ring`, `--radius` |
|
||||
| Data | `--chart-primary/secondary`, `--status-success/warning/in-progress` |
|
||||
|
||||
**`--busy-ink`** is the odd one and generalises: a fill bright enough to carry black text is far too light
|
||||
to *be* text. Busy needed it first because its fill is amber; the other tones are dark enough to do both
|
||||
jobs. Use `-ink` for text on a tint, `-foreground` for text on the solid fill.
|
||||
|
||||
### Surfaces — complete CSS values
|
||||
|
||||
Not triplets, because a panel background has to be able to be `rgba()`, a gradient, or `none`. A flat
|
||||
colour would rule out the glass Duck has.
|
||||
|
||||
| Token | What it is |
|
||||
|---|---|
|
||||
| `--page-bg` | the page itself |
|
||||
| `--panel-bg`, `--panel-border` | a tiled panel — the cornerstone of the layout |
|
||||
| `--panel-border-bare` | a panel with no header, which has nothing else separating it from what it sits on |
|
||||
| `--panel-raised-bg/-border` | a fullscreen panel: DENSER glass, not darker — panel chrome is dark text, and a heavy dark fill hides every icon |
|
||||
| `--panel-blur`, `--header-blur` | `0px` switches the backdrop blur off, which a flat theme should do — the compositor does that work for nothing on a solid surface |
|
||||
| `--card-bg`, `--glass-bg/-border`, `--dock-bg/-border/-tooltip-bg` | the other surfaces |
|
||||
| `--header-bg/-border/-fg` | the top bar |
|
||||
| `--pixel-grid-color`, `--card-grid-color`, `--panel-grid-color` | the graph-paper grid; `transparent` removes it |
|
||||
| `--scrollbar-*` | the app's own scrollbars |
|
||||
|
||||
### Assets — quoted paths
|
||||
|
||||
`--page-bg-src`, `--logo-src`.
|
||||
|
||||
A path in quotes, **not** `url(...)`. Bun's CSS bundler resolves `url()` at build time while these files
|
||||
are served from `public/` at runtime, so a `url()` token fails the build outright. `useThemeAsset` reads
|
||||
the path and wraps it. `''` means none.
|
||||
|
||||
Where a third party's assets live is not settled — the intent is a convention under `public/`.
|
||||
|
||||
### Brand slots
|
||||
|
||||
`--brand-primary`, `--brand-accent`, `--brand-warm`, `--brand-deep`, `--ink`, `--surface-warm`, and the
|
||||
legacy `--duck-*` aliases.
|
||||
|
||||
`--ink` is the app's text colour. It was called `duck-dark` and was never a brand green — `#14532d` in
|
||||
light, near-white in dark, used ~950 times to mean "text". The `duck-*` names are kept as aliases because
|
||||
`prose.css` and some plugins read them directly; they retire when the utility rename lands.
|
||||
|
||||
---
|
||||
|
||||
## Rules for components
|
||||
|
||||
**Never a palette colour.** `text-emerald-400`, `bg-zinc-800`, `border-red-500` do not follow the theme.
|
||||
Use the semantic token: `text-success`, `bg-muted`, `border-destructive`.
|
||||
|
||||
**Colour means state.** Five tones plus busy, and no others — see `components/Data/tone.ts` and
|
||||
`docs/design-language-interface.md`. When a view can reach for any of Tailwind's palettes, green stops
|
||||
meaning "open" and starts meaning "the author liked green".
|
||||
|
||||
**`bun scripts/check-theme-tokens.ts` enforces it.** Per-directory counts that may fall and never rise. Not
|
||||
a ban — hundreds exist, most in code that is leaving — but a ratchet. Pay some down, run `--update`, and
|
||||
the improvement is locked in.
|
||||
|
||||
**Plugins are held to the same rule.** See `plugins/EXTRACTING-A-PLUGIN.md`.
|
||||
|
||||
---
|
||||
|
||||
## Verifying a change
|
||||
|
||||
- `scripts/theme-snapshot.ts` — screenshots of 8 routes in both modes, or a pinned theme via `--theme`.
|
||||
- `scripts/theme-vardiff.ts` — computed variables of two running builds, side by side. **The instrument
|
||||
that matters**: screenshots are dominated by the wallpaper, which rotates per load, and a control run of
|
||||
one build against itself differs on 4 of 16 screens.
|
||||
- `scripts/theme-preview-server.ts` — this checkout's frontend against a running platform's API, so a build
|
||||
can be seen with real data without starting a second server beside a live one.
|
||||
|
||||
Render the app under **Plain** to find what is not themed yet. Anything still coloured is a component
|
||||
painting itself instead of asking the theme.
|
||||
Reference in New Issue
Block a user