fix(design): the S3g conformance sweep — primitives at their rung (E28 S3g)
The probe re-run over the 15 pages at 1920x1080 found every button, icon button, chip, field, tab, segment and menu row drawn by its primitive except the S3h Plex picture, a div with the button role. It is a Pressable now, and the button-primitive lint rule refuses an element with role="button" outside the control primitives. - SegmentedToggle defaults to the toolbar rung (28, text-xs): the old small default had put the pager's page sizes and the YouTube result count on 10px type beside 28px buttons. The Plex match toggle asks for the small rung explicitly. - The language badge on the rail uses text-3xs (was an 8px one-off); the Plex playlist's watched mark and the Home hero's length use MEDIA_BADGE instead of hand-copied classes. - A media picture shows the pointer whether it is a link or a button. - DESIGN.md: a Conformance chapter (the four-step check before a UI change is done, with the S3g baseline), the segmented rung, two log rows. CLAUDE.md points at the check.
This commit is contained in:
+26
-1
@@ -18,6 +18,27 @@ owner settles it.
|
||||
- A rule stated here should also be enforced in code (an eslint rule, a primitive's type, a test). A rule
|
||||
that lives only in prose drifts. That is the finding that started this document.
|
||||
|
||||
## Conformance — the check before a UI change is done
|
||||
|
||||
A screen is measured against this document, not against taste. Every change that renders something new
|
||||
runs this check before it is called done (E28 S3g):
|
||||
|
||||
1. **Primitives at a named rung.** Each control goes through its primitive (lint refuses the raw element,
|
||||
and a `<div role="button">` with it), at a rung or role this document names: a `Button` size, an
|
||||
`IconButton` size, a `Chip` size, the `SegmentedToggle` rung, a `SURFACE` role. The lint holds the
|
||||
element, not the rung, so the rung is checked by eye and by measure.
|
||||
2. **The pattern's chapter, re-read.** The change matches the chapter of every pattern it touches, or it
|
||||
opens a **Pending** item and a log row. It never quietly adds a second look.
|
||||
3. **Measured.** On the changed pages, the rendered elements are grouped by kind (button, icon button,
|
||||
chip, field, tab, segment, menu row, surface) and by their computed look (radius, height, type, fill,
|
||||
edge). Each distinct look maps to a rung or role named here; a look that maps to none is a violation
|
||||
or a new decision. The S3g baseline across the 15 pages at 1920 × 1080: every button, icon button,
|
||||
chip, field, tab, segment and menu row is drawn by its primitive. What the measure still counts as
|
||||
hand-drawn is by design: text links (no primitive), surfaces painted by their `SURFACE` role (a class,
|
||||
which the measure cannot trace to a component) and the playlist table's own rows.
|
||||
4. **Seen.** A screenshot of every changed state, thin edges at 1:1; the tablet widths (1024 × 768, a
|
||||
narrow split screen) when the layout can reflow. Phones are the E29 epic's, not this check's.
|
||||
|
||||
## The design lab
|
||||
|
||||
`npm run dev`, then open **`http://localhost:5173/design-lab`**. The lab renders each open decision's variants
|
||||
@@ -189,7 +210,9 @@ Also in force:
|
||||
- **Unavailable with a reason** stays focusable: `aria-disabled` plus the reason in the label or tooltip,
|
||||
never native `disabled`, which drops it from the tab order with its explanation. [WCAG 2.1.1]
|
||||
- A choice between a few values is `SegmentedToggle`; a long list or a user's own list is `FilterSelect`.
|
||||
Never a native `<select>` (lint-enforced).
|
||||
Never a native `<select>` (lint-enforced). Its rung is the toolbar's, 28 with `text-xs`, so it lines up
|
||||
with the triggers and the pager's buttons; the 24 rung with the smallest type is only a side panel's
|
||||
captioned match toggle (S3g: the old small default had put a pager and a toolbar count on 10px type).
|
||||
- A single-select row of filter chips goes through `FilterChipGroup` (its options are `Chip`s).
|
||||
- A button that starts slow work shows `loading` (a spinner, `aria-busy`) and can't be pressed twice.
|
||||
[Nielsen #1]
|
||||
@@ -564,3 +587,5 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
|
||||
| 2026-09-27 | C2 as built | The side panel's drawer is `ui/Modal` `drawer-start`; drawer = a coarse pointer or below 768; the pin offered in its header ≥ 1024 | A drawer of its own inside `SidePanel` | Esc, Back, the scrim, the trap and the scroll lock are the dialog's; a second implementation is how two shells drift |
|
||||
| 2026-09-27 | Narrow rows by container | The download row, the playlist table and the Channels tag row stack by a container query; a fact line never wraps | Breakpoints on the window | The iPad zoomed in, split screen and a column beside a rail are narrow while the window is not; the zoomed download row broke "18.9 / MB" apart |
|
||||
| 2026-09-27 | Phones deferred | A phone-only layout (the hamburger nav, a folded feed band) waits for its own epic; E28 stays desktop + tablet | Fix the phone views inside E28 | Principle 2; the first phone attempts made the feed worse, and the epic's review grows with every sprint |
|
||||
| 2026-09-27 | Conformance check (S3g) | A four-step check before a UI change is done: primitive at a named rung, the chapter re-read, the rendered page measured by kind and look, every state seen | A reviewer's eye per sprint | The S1 probe found 69 button looks; after S3 every control kind is drawn by its primitive, and the check keeps a new look from arriving unnoticed |
|
||||
| 2026-09-27 | Segmented rung | `SegmentedToggle` defaults to the toolbar rung (28, `text-xs`); 24 only for a panel's captioned match toggle | The small default | Two toolbars had inherited 10px type from the default beside 28px buttons |
|
||||
|
||||
Reference in New Issue
Block a user