feat(design): batch A recorded, batch B in the design lab (E28 S2b)

DESIGN.md: the Controls chapter decided (A1-A6) with the decision-log rows;
density stays open as B1. The lab keeps only the chosen A renderings as
reference and adds batch B: density, the scheme set (measured accent hue
gaps and light contrast), state layers, the page-chrome model, surface per
role, the public watch page, the nav rail, the loading form and where the
header + fade edge goes.
This commit is contained in:
2026-09-24 02:21:20 +02:00
parent 50923bb684
commit 477271907e
7 changed files with 1481 additions and 274 deletions
+49 -22
View File
@@ -54,8 +54,8 @@ style guide.
`.reveal` class), or it is removed on touch because the same action is reachable another way
(`.reveal-hover`). [WCAG 1.4.13, Nielsen #6]
- **Targets:** at least 2.75rem (44px) on a coarse pointer, as a real box, not an invisible hit area:
overlapping hit areas fire the wrong control. [HIG, WCAG 2.5.8] _Which controls this covers beyond
`Button` is decision A3._
overlapping hit areas fire the wrong control. It covers every control primitive, not only `Button`
(A3, under Controls). [HIG, WCAG 2.5.8]
- **Text inputs** use at least 1rem text on a coarse pointer, so a tablet browser doesn't zoom the page
when an input gets focus.
- **Below 768px** (a narrow window, split screen): what replaces the side rails is **Pending (C)**.
@@ -63,23 +63,40 @@ style guide.
## Tokens
- **Colour:** only semantic tokens (`--bg --surface --card --fg --muted --accent --accent-fg --danger
--warn --success --info --border`, each with a `-fg` partner where text sits on it). Six schemes × dark
--warn --success --info --border`, each with a `-fg` partner where text sits on it). Six schemes × dark
and light. Normal text ≥ 4.5:1 and a control's edge ≥ 3:1 in **every** scheme and mode. [WCAG 1.4.3,
1.4.11] _The light-theme accent fix is decision B; the input edge token is decision A4._
1.4.11] _The scheme set and the light-theme accent are decision B2._ The input edge token is set under
Controls (A4).
- **Type and size:** everything is rem-derived off `--font-scale` (the Settings text-size slider), so a
px font size or a px-frozen box is an eslint error.
- **Radius:** chips and segmented controls `rounded-full`; tabs `rounded-lg`. _Buttons, inputs, triggers
and icon buttons: decisions A1, A2, A4, A5. Surfaces per role: decision B._
- **Radius:** chips and segmented controls `rounded-full`; tabs `rounded-lg`. Buttons, icon buttons and
form fields `rounded-lg`; toolbar triggers and search a pill (Controls). _Surfaces per role: decision B5._
- **Layers:** the fixed z-index scale in `index.css` (`base < menu < chrome < paneltab < panel < rail <
overlay < popover < tooltip`). No ad-hoc z-index. [maintainability]
overlay < popover < tooltip`). No ad-hoc z-index. [maintainability]
## Controls — **Pending (batch A)**
## Controls — _Decided_ (batch A; density **Pending**, B1)
Open in the lab: **A1** button shape and size rule · **A2** icon-button sizes and the on-media button ·
**A3** the touch size beyond `Button` · **A4** the input primitive · **A5** the dropdown trigger ·
**A6** checkbox, switch, slider.
The chosen rendering of each rule is in the lab under "Decided". The primitives are built to it in S3.
- **Buttons (A1):** `rounded-lg` (r8): a control's corner is smaller than the surface it sits on (card r12,
dialog r16). The rung follows the context: `md` in forms and dialog footers, `sm` in toolbars and panel
headers, `xs` inside a table row. Glass is a `Button` variant, not a hand-built glass button. _The rung
heights (standard 38/32/26 or compact 32/28/24) are decision B1._ [HIG, Material]
- **Icon buttons (A2):** two sizes, r8: the smaller in rows and dense toolbars, the larger on its own. On a
picture, a round, dark translucent button, readable on any thumbnail. Round means "on media" only.
- **Touch (A3):** on a coarse pointer every control primitive (`Button`, `SegmentedToggle`, `FilterSelect`,
`Tabs`, `Switch`, chips, the nav rail) grows to 2.75rem (44px) through one token, as a real box. Mouse
sizes don't change. [HIG, WCAG 2.5.8]
- **Input (A4):** one bordered input primitive. The edge is `color-mix(in srgb, var(--fg) 55%, var(--bg))`,
≥ 3:1 against the page in every scheme and mode. A form field is r8 at the `md` height; a toolbar search
is a pill. Text is 1rem on a coarse pointer. [WCAG 1.4.11]
- **Dropdown trigger (A5):** a pill in a toolbar (where the pill means "a filter or a choice"), r8 in a form
(it lines up with the inputs).
- **Checkbox, switch, slider (A6):** the browser's own controls tinted with `accent-color`; the whole
labelled row is the 44px target. Keyboard, screen reader and forced-colours support come from the browser.
Also in force:
Already in force:
- An action button is `ui/Button` with a `SIZES` rung. A hand-rolled `<button className="px-3 …">` is a
violation. A text-less button must have an accessible name, and the type enforces it. [WCAG 4.1.2]
- A choice between a few values is `SegmentedToggle`; a long list or a user's own list is `FilterSelect`.
@@ -90,11 +107,14 @@ Already in force:
## Surfaces and page chrome — **Pending (batch B)**
Open: the page-chrome model (a sticky toolbar vs controls in the content, collapsing, the page title), the
glass role per surface, the public watch page, the light-theme accent, menu items and popover surfaces, the
nav rail, and the loading form (spinner vs skeleton).
Open in the lab: **B1** button density · **B2** the scheme set (and the light-theme accent) · **B3** state
layers (hover, pressed) · **B4** the page-chrome model (a sticky band vs controls in the content, folding on
a short screen, the page `h1`) · **B5** the surface and radius per role (card, dialog, menu, tooltip) · **B6**
the public watch page · **B7** the nav rail · **B8** the loading form (spinner vs skeleton) · **B9** where
the header + fade edge goes.
Already in force:
- The glass role is load-bearing: a dropdown or menu is `.glass-menu` (it floats over undimmed content, so a
card alpha would bleed through), a dialog is `.glass`, and a grid card is `.glass-card`.
- Every overlay is a `Modal`/`Overlay` or an anchored popover from the one popover engine. Escape closes the
@@ -124,6 +144,7 @@ Which actions are visible and which live in a menu.
### Text stays selectable — _Decided_
Wherever there is text, the user can select it and copy it, and no click action overrides that.
- A title or a name is never the text content of a `<button>`. The play/open target is the thumbnail, an
icon, or a link-styled element.
- A click that ends a text selection does nothing: if the selection inside the element is non-empty, or
@@ -146,7 +167,7 @@ Wherever there is text, the user can select it and copy it, and no click action
An error sits next to what failed and says how to recover; a field error is under its field and linked
with `aria-describedby`. Colour is never the only signal. [Nielsen #9, WCAG 3.3.1, 1.4.1]
- **Loading:** anything slower than ~300ms shows a loading state; a near-instant load doesn't flash one.
Space is reserved for late content so the layout doesn't jump. _Spinner vs skeleton: batch B._
Space is reserved for late content so the layout doesn't jump. _Spinner vs skeleton: decision B8._
- **Reordering:** anything you can drag also has a button or menu way to move it (Top / Up / Down / Bottom),
and a drag handle carries `touch-action: none` so a finger drags instead of scrolling. [WCAG 2.5.7]
- **Motion:** 150–300ms micro-interactions, transform/opacity only; `prefers-reduced-motion` is respected.
@@ -169,9 +190,15 @@ Wherever there is text, the user can select it and copy it, and no click action
## Decision log
| Date | Decision | Chosen | Over | Why |
|---|---|---|---|---|
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
| Date | Decision | Chosen | Over | Why |
| ---------- | --------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------- | -------------------------------------------------------------------------------- |
| 2026-09-24 | Device contract | Tablet portrait + landscape and up; phones out | A 1024px floor with a "too small" message | Portrait tablet use is real, and the app already renders at 820 portrait |
| 2026-09-24 | Card actions on touch | Fewer visible, the rest in a "…" menu | 44px buttons that no longer fit the card | Six 44px targets need more width than a card has |
| 2026-09-24 | Action placement | One menu with several triggers, never gesture-only, a desktop background menu that mirrors the toolbar | Visible buttons for everything | Fewer non-critical buttons on screen, without hiding any function |
| 2026-09-24 | Text selection | Text stays selectable; a click that ends a selection does nothing | Click-to-play on titles | The user must be able to copy any text the app shows |
| 2026-09-24 | A1 Button shape | r8 + a rung per context; glass is a Button variant | r12; a pill | Nested corners (r8 in r12 in r16); the pill already means "a filter or a choice" |
| 2026-09-24 | A2 Icon buttons | Two sizes, r8; a round button only on media | One size; all round | A lone toolbar icon at the row size looks lost; round rows read as tags |
| 2026-09-24 | A3 Touch size | 44 for every control primitive | 36 + gaps; Button only | One rule (HIG 44) instead of two sizes nobody remembers |
| 2026-09-24 | A4 Input | Bordered, a 55% fg-into-bg edge (≥ 3:1) | Filled; today's look as a primitive | The only option that clears WCAG 1.4.11 without a heavy fill on glass |
| 2026-09-24 | A5 Dropdown trigger | Pill in a toolbar, r8 in a form | Pill everywhere; r8 everywhere | Matches the neighbours in each context |
| 2026-09-24 | A6 Checkbox, switch, slider | Native, accent-tinted, the row is the target | Drawn primitives | Free keyboard / screen-reader / forced-colours support; only 3 sites |