docs(design): composition and the reader — a chapter and a Conformance step (E30 S6)

The rules covered each piece; nothing asked whether a screen works as a whole, and the person page passed
every rule with half its panel empty. New chapter: the space is used, one focal point, worst real data,
where am I, a click answers at once (no outside service on open), zoom and text size first-class (the
owner reads zoomed in). Conformance step 5 writes one composition line per screenshot and checks 200%
zoom and the largest text size. Anti-patterns and a decision-log row to match.

HU: the download worker setting reads "Párhuzamos letöltések" (a leftover from the S5 sweep).
This commit is contained in:
2026-09-30 02:19:54 +02:00
parent 63d01babe5
commit ca94a716d8
2 changed files with 41 additions and 2 deletions
+39
View File
@@ -38,6 +38,11 @@ runs this check before it is called done (E28 S3g):
which the measure cannot trace to a component) and the playlist table's own rows. 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 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. narrow split screen) when the layout can reflow. Phones are the E29 epic's, not this check's.
5. **Judged as a whole** (E30 S6). For each screenshot, one written line on its composition: where the empty
space is, what hugs one side, what the eye lands on first. Then the screen at 200% browser zoom and at
the largest text size. Both are held to [Composition and the reader](#composition-and-the-reader--decided-e30-s6).
A rule that holds is not the same as a screen that works: the person page passed every rule above with
half its panel empty.
## The design lab ## The design lab
@@ -435,6 +440,36 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
- **Reading measure.** Prose (a synopsis, a biography, a channel's description) is capped at ~65 - **Reading measure.** Prose (a synopsis, a biography, a channel's description) is capped at ~65
characters a line (the `measure` utility) wherever its container can be wider. [WCAG 1.4.8] characters a line (the `measure` utility) wherever its container can be wider. [WCAG 1.4.8]
## Composition and the reader — _Decided_ (E30 S6)
The rules above say how each piece looks; these say whether the screen works as a whole, for the one
reader it is built for. Found when the person page passed every rule with its biography in the left half
of a full-width panel and nothing on the right.
- **The space is used.** A narrow element in a wide container (prose at `measure`, a form, a short list)
comes with a decision about the rest of the width: a second column with related content (the person
page: facts and bio left, "Known for" right), a container sized to its content, or a layout that stacks.
A band of nothing wider than about a third of a panel, beside content that stops short, is a finding.
[Material layout, Gestalt proximity]
- **One focal point, then an order.** A screen has one thing the eye lands on first (the page heading, the
hero, the player), then the sections, then the metadata. A heading level keeps one look across screens;
a second focal point competing with the first is a finding. [Nielsen #8, HIG hierarchy]
- **Worst real data, not the demo row.** A screen is judged with the longest title, the Hungarian label,
zero items, the largest list, a missing picture and a missing field. None of them clips, overlaps or
leaves a hole where a value was. [Nielsen #9, WCAG 1.4.10]
- **Where am I.** The browser tab names the place and the object in it ("Nicolas Cage · Plex · Siftlode"),
the rail marks the place, and Back returns to the same view at the same scroll position with focus
where it was. [Nielsen #1, WCAG 2.4.2]
- **A click answers at once.** A control shows its pressed or changed state within ~100ms (an optimistic
update, a spinner in the control); what a screen opens with is the app's own data — an outside service
(TMDB, YouTube) is fetched ahead of time by a job, not on the click, so a page never waits on it.
Late content keeps its place (B8). [Nielsen #1, RAIL]
- **Zoom and text size are first-class.** The owner reads with the browser zoomed and a large text size.
Every screen holds at 200% browser zoom on a desktop window and at the largest Settings text size at
1024 wide: nothing clipped or overlapping, no sideways page scroll, every control reachable and at
least its rung, text wrapping rather than cut. Measured, not assumed, on every UI change. [WCAG 1.4.4,
1.4.10, 1.4.12]
## Interaction rules — _Decided_ ## Interaction rules — _Decided_
- **Keyboard:** everything works by keyboard, in visual order, with no traps. A shortcut is a row in - **Keyboard:** everything works by keyboard, in visual order, with no traps. A shortcut is a row in
@@ -561,6 +596,9 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
- A click handler on text that swallows the user's text selection. - A click handler on text that swallows the user's text selection.
- A colour literal, a px font size, a px-frozen box. - A colour literal, a px font size, a px-frozen box.
- An empty state shown for an error. - An empty state shown for an error.
- Half a panel left empty beside content that stops short.
- A page that waits on an outside service when the user opens it.
- A screen checked only at 100% zoom and the default text size.
## Decision log ## Decision log
@@ -635,6 +673,7 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen #
| 2026-09-27 | C4 as built | `initialFocus` required on every `Modal`; the commit row in `ModalFooter`, portalled under the body from any depth; the fades driven by the body's scroll timeline; `interactive-widget=resizes-content` | An optional prop defaulting to the ✕; a `footer` prop (the issue composer's buttons live three components down); always-on fades | A default is how 21 dialogs came to open on the ✕; lifting the composer's state to reach a prop is the larger change; a resting fade dimmed the first line | | 2026-09-27 | C4 as built | `initialFocus` required on every `Modal`; the commit row in `ModalFooter`, portalled under the body from any depth; the fades driven by the body's scroll timeline; `interactive-widget=resizes-content` | An optional prop defaulting to the ✕; a `footer` prop (the issue composer's buttons live three components down); always-on fades | A default is how 21 dialogs came to open on the ✕; lifting the composer's state to reach a prop is the larger change; a resting fade dimmed the first line |
| 2026-09-27 | C3 + C5 as built | A tap toggles the bar through `pointerType` from `pointerdown`; a double-tap takes back its first tap's toggle; the card's pair is the video card's `MediaResumeButtons` (Continue / Restart), a sibling over the picture; the YT description is an ⓘ click popover | A delayed single tap (wait out the double-tap window); new "Resume / From the start" words for Plex; the pair inside the picture's button | A delayed tap feels dead; one media card says one pair of words; a button inside a button is invalid and the hover row already sits beside the picture | | 2026-09-27 | C3 + C5 as built | A tap toggles the bar through `pointerType` from `pointerdown`; a double-tap takes back its first tap's toggle; the card's pair is the video card's `MediaResumeButtons` (Continue / Restart), a sibling over the picture; the YT description is an ⓘ click popover | A delayed single tap (wait out the double-tap window); new "Resume / From the start" words for Plex; the pair inside the picture's button | A delayed tap feels dead; one media card says one pair of words; a button inside a button is invalid and the hover row already sits beside the picture |
| 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 | 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-30 | Composition and the reader (E30 S6) | A chapter and a Conformance step: the space is used, one focal point, worst real data, where am I, a click answers at once, zoom and text size first-class; the person page as two columns (facts and bio left, "Known for" right) | Rules for pieces only; the bio in a full-width panel; a narrower panel | The person page passed every piece rule with half its panel empty; the owner reads zoomed in, so zoom is a requirement, not a nicety |
| 2026-09-30 | Hungarian words (E30 S5) | The D3 list: Feed, Shorts, Admin, Token, Port stay; stream, Prio, Job, Audit, Demo, Auto, Info, Frontend/Backend, Backfill, transcode get the Hungarian word; one name per thing (kérelem, behúzás, one "Test connection") | Keep every loanword; translate Feed/Shorts too | The owner chose the list (D3); the final sweep found the same feature under two names in three places | | 2026-09-30 | Hungarian words (E30 S5) | The D3 list: Feed, Shorts, Admin, Token, Port stay; stream, Prio, Job, Audit, Demo, Auto, Info, Frontend/Backend, Backfill, transcode get the Hungarian word; one name per thing (kérelem, behúzás, one "Test connection") | Keep every loanword; translate Feed/Shorts too | The owner chose the list (D3); the final sweep found the same feature under two names in three places |
| 2026-09-29 | Numbers and order in the UI language (E30 S3) | Every number through `lib/format.ts` in the UI language (decimal mark, grouping, compact, units); A→Z by the shown name in the UI language's alphabet, client and server alike; the language comes off `useTranslation`, never the singleton | `toFixed`, hand-built "1.2K", `localeCompare` in the browser's locale, byte-order server sorts | A Hungarian reader saw "8.3", "1.2K", "Életrajzi" after "Z"; the singleton read also left values in the old language after a switch (React Compiler) | | 2026-09-29 | Numbers and order in the UI language (E30 S3) | Every number through `lib/format.ts` in the UI language (decimal mark, grouping, compact, units); A→Z by the shown name in the UI language's alphabet, client and server alike; the language comes off `useTranslation`, never the singleton | `toFixed`, hand-built "1.2K", `localeCompare` in the browser's locale, byte-order server sorts | A Hungarian reader saw "8.3", "1.2K", "Életrajzi" after "Z"; the singleton read also left values in the old language after a switch (React Compiler) |
| 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 | 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 |
+2 -2
View File
@@ -149,8 +149,8 @@
"hint": "„Plex” = Csatorna/Évad ÉÉÉÉ/… fastruktúra .nfo + poszter fájlokkal (Plexben indexelhető); „Egy mappa” = minden egyetlen mappában." "hint": "„Plex” = Csatorna/Évad ÉÉÉÉ/… fastruktúra .nfo + poszter fájlokkal (Plexben indexelhető); „Egy mappa” = minden egyetlen mappában."
}, },
"download_worker_concurrency": { "download_worker_concurrency": {
"label": "Worker párhuzamosság", "label": "Párhuzamos letöltések",
"hint": "Hány letöltést dolgoz fel a worker párhuzamosan." "hint": "Hány letöltést dolgoz fel egyszerre a letöltő folyamat."
}, },
"sponsorblock_default": { "sponsorblock_default": {
"label": "SponsorBlock alapból", "label": "SponsorBlock alapból",