diff --git a/docs/superpowers/specs/2026-09-02-design-foundation-design.md b/docs/superpowers/specs/2026-09-02-design-foundation-design.md new file mode 100644 index 0000000..5cb6f91 --- /dev/null +++ b/docs/superpowers/specs/2026-09-02-design-foundation-design.md @@ -0,0 +1,152 @@ +# Design foundation + +Sub-project 1 of the visual redesign. Owner's brief: *"right now it looks a bit +boring, simple and ugly. We want sleek, modern, and nice to use."* Owner's +answers to the four scoping questions: both themes with a toggle, a full +rethink of every screen, purchases compact and grouped by date, and a +self-hosted webfont. Owner then delegated the specifics: *"Override everything +you want for this step. This needs to look good."* + +## The diagnosis that shaped everything else + +A design system already existed and was never wired up. `src/index.css` +defined `--paper` (annotated "cool statement-paper ground, NOT cream"), +`--ink`, a muted teal, and surplus/overspend colours, with a design-contract +document behind them. The app painted itself with shadcn's defaults — +`bg-background text-foreground`, i.e. white and near-black. Those tokens +reached the screen in exactly two places: the carry meter, and the sign +colours on figures. + +So "boring, simple and ugly" was not a missing design. It was a design that was +specified and then bypassed. That is why the foundation comes first in the +decomposition, and why it re-themes shadcn rather than styling screens: doing +it screen by screen is how the bypass happened. + +## Decomposition + +The redesign is four sub-projects, each its own spec → plan → implementation: + +1. **Design foundation** (this document) — tokens, themes, type, shared furniture. +2. **Month view** — income and totals, post rows and carry meter, purchases grouped by date. +3. **Year, Summary, Post detail** — the reporting screens, including whether the 12-month matrix stays a scrolling table. +4. **Dialogs and fast entry** — purchase dialog, split editor, plan editor, bulk entry. + +A single spec covering all four would have meant dozens of layout decisions +made on the owner's behalf under one approval. + +## Token architecture + +Two layers, deliberately separate. + +**Layer 1, `--budget-*`**: the app's own vocabulary — paper, raised, ink, +ink-muted, rule, rule-strong, accent, accent-ink, tint — plus `--surplus` and +`--overspend`, which shadcn has no concept of. Defined once per theme. The only +colours app components may name. + +**Layer 2, shadcn**: its entire variable set points at layer 1. Every primitive +(dialog, select, badge, button) inherits both themes for free. + +### The brand teal is `--primary`, never `--accent` + +CLAUDE.md's hardest-won rule is "never define a bare `--accent`", paid for with +an app-wide contrast bug. The mechanism matters: shadcn uses `--accent` as a +hover **background** with `--accent-foreground` as the text on it. Setting one +without the other repaints the background and leaves the text behind. + +Two conclusions follow, and both are enforced rather than remembered: + +- Every shadcn pair is redefined **together**, in both themes. + `src/cssPairs.test.ts` fails the build if either half of a pair appears + without the other, in any block. +- The saturated teal belongs on `--primary`. Putting a brand colour on + `--accent` — which is what "override `--accent`" invites — turns every hover + into a coloured slab with dark text on it. `--accent` stays a tint of the + surface. + +### Contrast was computed, not chosen + +Weakest text pair: 5.50:1 (muted on paper, light) against a 4.5:1 requirement. +Field borders were iterated until they cleared the 3:1 non-text requirement on +**both** backgrounds they appear against — the first two candidates measured +2.17 and 2.55 and were rejected. The final palette was then read back out of +the live browser in both themes, so the check is on what actually rendered +rather than on the arithmetic. + +## Typography + +Inter for text, JetBrains Mono for figures, both variable, both self-hosted, +latin + latin-ext only. SIL OFL 1.1; licences committed beside the files. +Self-hosted rather than CDN-linked because PRODUCT.md's position is that the +data never leaves the browser and the app needs no third party — a font request +to another origin on every cold load quietly contradicts that. + +`.font-money` carries tabular figures and a slashed zero, so columns align on +the decimal and `0` cannot be misread as `O`. + +### Rejected: @font-face in the bundled stylesheet + +The obvious home. Bun's CSS bundler resolves every `url()` from disk and inlines +woff2 as base64 — **relative or absolute**, and `loader: { ".woff2": "file" }` +does not change it. Measured cost: `index.css` went from 70 KB to 303 KB of +render-blocking bytes, and the `unicode-range` subsetting became pointless, +since a subset embedded in the stylesheet downloads whether or not one +character needs it. + +Also rejected: a ``. It works in a +production build (`external: ["*fonts.css"]`), but the dev server's HTML +bundler resolves every `` from disk and has no `external` option, so +dev would serve a placeholder and screenshots would lie about typography. +Making the manifest and icon hrefs root-absolute for the same reason 500'd the +dev server outright. + +**Chosen**: `src/fontCss.ts` is the one definition, injected as a `