feat(e10): put the 1024x768 size contract inside the primitives

S1c, the last part of S1. The contract now lives in components/ui/ and
in one CSS recipe instead of in ~160 call sites:

- Minimum target 2.75rem, on coarse pointers only (.touch-target). A
  32px icon button is fine with a mouse -- precise pointer, hover to
  confirm the aim -- and not fine with a thumb, so it grows exactly where
  there is no precise pointer, which on this app's real second device is
  the tablet. It grows the real box rather than painting an invisible
  ::after hit area, because two expanded hit areas on icon buttons 4px
  apart would overlap and the overlap fires the WRONG button. `link` is
  excluded: it is text inside a sentence.
- 2.75rem rather than 44px so the target obeys the text-size slider like
  everything else; verified that the type/geometry ladder was already
  rem-derived off --font-scale, so nothing else was needed there.
- The ladder is documented as a closed set in Button.tsx, with the site
  counts it was derived from, and ui/README.md states the contract.

Measured in the browser: fine pointer 36x36 and 34px-tall buttons
(unchanged), coarse pointer the same buttons at 44x44 and 44px tall.
1024x768 renders identically. 3x tsc, eslint, knip, prettier and 690
vitest green.
This commit is contained in:
2026-09-05 18:49:48 +02:00
parent 721467ef69
commit 7e97559615
4 changed files with 64 additions and 0 deletions
+21
View File
@@ -37,6 +37,27 @@ domain-free.
| page & panel structure | `PageShell`, `PageScroller`, `PanelGroup`, `PanelGroups`, `Tabs`, `BackToTop` |
| state & messaging | `QueryState`, `Banner`, `InlineNotice` |
## The size contract (E10 S1c)
The app optimises for **1024x768 (tablet landscape) as the floor**; below it we owe an honest
message, above it correct rendering all the way up. That contract lives in these components, not in
the ~160 call sites, because a rule repeated in 160 places is a rule that is already broken
somewhere:
- **Geometry is a ladder, not a free choice.** `Button`'s `SIZES` are the rungs, derived from what
the app actually used. Pick a rung; do not hand-roll padding on a primitive.
- **Everything is rem-derived**, so the Settings text-size slider (`--font-scale` on the root
font-size) scales boxes and padding with the label. A `text-[Npx]` font size is an eslint error for
exactly this reason.
- **Minimum target: 2.75rem, on coarse pointers only** (`.touch-target`, index.css). A 32px icon
button is fine with a mouse and not with a thumb, so the box grows where there is no precise
pointer. It grows the real box rather than painting an invisible hit area: two expanded hit areas
on neighbouring icon buttons would overlap, and an overlap fires the _wrong_ button.
- `link` is excluded from the minimum: it is text inside a sentence.
Controls outside this directory (chips, list rows, the player's overlay buttons) are **not** covered
yet -- they are the next thing to bring under the contract, not evidence that there isn't one.
## Adding one
Move a component in when a second module needs it and it is domain-free — not on speculation. Adding