From bd6aaa1e43e4b14fd32f1ccb34d9475267ac285b Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 27 Sep 2026 05:46:17 +0200 Subject: [PATCH] =?UTF-8?q?feat(design):=20the=20Copy=20rule=20=E2=80=94?= =?UTF-8?q?=20names,=20verbs,=20dates=20and=20units=20(E28=20S3g)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's picks (all the recommended options) become DESIGN.md's Copy chapter and a sweep of both languages (742 values, no key drift): - One name per feature: the rail's label, which may shorten it (Alerts -> Notifications, Fiókok -> Felhasználók, Download Center -> Downloads, Channel manager -> Channels); one "sync from YouTube" verb. - Four verbs, four meanings (Delete / Remove / Dismiss / Discard, and their Hungarian pairs), checked against what each action does. - Sentence case, the app's places capitalised; "…" on a command that asks first (a popover or form title got its own key without it); one-sentence messages without a stop or a tick, "Couldn't …"; curly quotes. - Dates only from lib/format.ts, in the UI language: formatDateTime names the month and stops at the minute (it printed "9/27/2026, 8:30:14 AM"), formatDateTimeSec for the log, the audit and the recorder; ui/RelativeTime pairs "3d ago" with the exact minute on hover. local/date-format refuses toLocale*String and Intl date formatters elsewhere. - Units from Intl, short (formatUnit): runtimes, totals, the Scheduler countdown, the stats range, the player's tap skip. "2h 13m" was English in both languages. - src/i18n/copyRule.test.ts holds the machine-readable part (quotes, "...", ticks, bare unit letters). --- docs/DESIGN.md | 180 +++++++----- frontend/eslint.config.js | 33 +++ frontend/src/components/AddToPlaylist.tsx | 4 +- frontend/src/components/AdminLogs.tsx | 6 +- frontend/src/components/ChannelPage.tsx | 9 +- frontend/src/components/ChatThread.tsx | 9 +- frontend/src/components/DateFilterSelect.tsx | 6 +- frontend/src/components/DebugRecorder.tsx | 8 +- .../DiagnosticsSettings.dom.test.tsx | 4 +- .../src/components/DiagnosticsSettings.tsx | 5 +- frontend/src/components/IssueComposer.tsx | 6 +- frontend/src/components/Messages.tsx | 9 +- .../src/components/NotificationsPanel.tsx | 7 +- .../src/components/Playlists.dom.test.tsx | 2 +- .../PlexPlayer.controls.dom.test.tsx | 9 +- frontend/src/components/PlexPlayer.tsx | 3 +- .../PlexPlaylistView.header.dom.test.tsx | 2 +- frontend/src/components/Scheduler.tsx | 14 +- frontend/src/components/SettingsPanel.tsx | 12 +- frontend/src/components/Stats.tsx | 6 +- .../components/VideoCard.actions.dom.test.tsx | 4 +- frontend/src/components/VideoCard.tsx | 13 +- frontend/src/components/auditColumns.tsx | 11 +- frontend/src/components/channelColumns.tsx | 5 +- frontend/src/components/plexBrowseLists.tsx | 2 +- .../src/components/plexCards.dom.test.tsx | 2 +- frontend/src/components/ui/README.md | 2 +- .../components/ui/RelativeTime.dom.test.tsx | 40 +++ frontend/src/components/ui/RelativeTime.tsx | 32 ++ frontend/src/i18n/copyRule.test.ts | 45 +++ frontend/src/i18n/locales/en/about.json | 2 +- frontend/src/i18n/locales/en/adminLogs.json | 6 +- frontend/src/i18n/locales/en/audit.json | 2 +- .../src/i18n/locales/en/auditActions.json | 2 +- frontend/src/i18n/locales/en/channel.json | 2 +- frontend/src/i18n/locales/en/channels.json | 22 +- frontend/src/i18n/locales/en/common.json | 8 +- frontend/src/i18n/locales/en/config.json | 16 +- frontend/src/i18n/locales/en/downloads.json | 64 ++-- frontend/src/i18n/locales/en/editor.json | 4 +- frontend/src/i18n/locales/en/errors.json | 274 ++++++++--------- frontend/src/i18n/locales/en/feed.json | 14 +- frontend/src/i18n/locales/en/header.json | 12 +- frontend/src/i18n/locales/en/home.json | 14 +- frontend/src/i18n/locales/en/inbox.json | 8 +- frontend/src/i18n/locales/en/messages.json | 8 +- frontend/src/i18n/locales/en/nav.json | 2 +- .../src/i18n/locales/en/notifications.json | 2 +- frontend/src/i18n/locales/en/onboarding.json | 4 +- frontend/src/i18n/locales/en/playlists.json | 59 ++-- frontend/src/i18n/locales/en/plex.json | 67 ++--- .../src/i18n/locales/en/quotaActions.json | 4 +- frontend/src/i18n/locales/en/recorder.json | 18 +- frontend/src/i18n/locales/en/scheduler.json | 10 +- frontend/src/i18n/locales/en/settings.json | 62 ++-- frontend/src/i18n/locales/en/setup.json | 12 +- frontend/src/i18n/locales/en/sidebar.json | 6 +- frontend/src/i18n/locales/en/stats.json | 10 +- frontend/src/i18n/locales/en/tagManager.json | 4 +- frontend/src/i18n/locales/en/users.json | 18 +- frontend/src/i18n/locales/en/views.json | 4 +- frontend/src/i18n/locales/en/welcome.json | 26 +- frontend/src/i18n/locales/hu/about.json | 2 +- frontend/src/i18n/locales/hu/adminLogs.json | 6 +- frontend/src/i18n/locales/hu/audit.json | 2 +- .../src/i18n/locales/hu/auditActions.json | 2 +- frontend/src/i18n/locales/hu/card.json | 2 +- frontend/src/i18n/locales/hu/channel.json | 2 +- frontend/src/i18n/locales/hu/channels.json | 24 +- frontend/src/i18n/locales/hu/common.json | 4 +- frontend/src/i18n/locales/hu/config.json | 8 +- frontend/src/i18n/locales/hu/downloads.json | 64 ++-- frontend/src/i18n/locales/hu/editor.json | 2 +- frontend/src/i18n/locales/hu/errors.json | 276 +++++++++--------- frontend/src/i18n/locales/hu/feed.json | 12 +- frontend/src/i18n/locales/hu/header.json | 14 +- frontend/src/i18n/locales/hu/home.json | 16 +- frontend/src/i18n/locales/hu/inbox.json | 10 +- frontend/src/i18n/locales/hu/keys.json | 2 +- frontend/src/i18n/locales/hu/messages.json | 12 +- frontend/src/i18n/locales/hu/nav.json | 4 +- .../src/i18n/locales/hu/notifications.json | 4 +- frontend/src/i18n/locales/hu/onboarding.json | 2 +- frontend/src/i18n/locales/hu/playlists.json | 73 ++--- frontend/src/i18n/locales/hu/plex.json | 55 ++-- .../src/i18n/locales/hu/quotaActions.json | 4 +- frontend/src/i18n/locales/hu/recorder.json | 14 +- frontend/src/i18n/locales/hu/scheduler.json | 10 +- frontend/src/i18n/locales/hu/settings.json | 60 ++-- frontend/src/i18n/locales/hu/setup.json | 16 +- frontend/src/i18n/locales/hu/sidebar.json | 4 +- frontend/src/i18n/locales/hu/stats.json | 8 +- frontend/src/i18n/locales/hu/tagManager.json | 4 +- frontend/src/i18n/locales/hu/users.json | 18 +- frontend/src/i18n/locales/hu/views.json | 4 +- frontend/src/i18n/locales/hu/welcome.json | 26 +- frontend/src/lib/dateFormat.contract.test.ts | 32 ++ frontend/src/lib/downloadFailure.test.ts | 2 +- frontend/src/lib/format.test.ts | 45 ++- frontend/src/lib/format.ts | 56 +++- 100 files changed, 1193 insertions(+), 964 deletions(-) create mode 100644 frontend/src/components/ui/RelativeTime.dom.test.tsx create mode 100644 frontend/src/components/ui/RelativeTime.tsx create mode 100644 frontend/src/i18n/copyRule.test.ts create mode 100644 frontend/src/lib/dateFormat.contract.test.ts diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 524ed61f..e6f4201b 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -497,11 +497,34 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen # placed with `ModalFooter` from any depth of the body. A section's own buttons (a sub-form inside a manager, a list row's Delete) stay in the body; a manager with no commit row has no footer. -## Copy +## Copy — _Decided_ (S3g) - Every user-facing string is in both Hungarian and English with zero key drift, and a user-facing error is - a structured `{code, reason}` translated on the client. -- A label says what happens ("Delete playlist"), not "OK". A destructive confirm names the object. + a structured `{code, reason}` translated on the client. Hungarian speaks to the user as "te". +- **One name per feature.** The nav rail's label is the feature's name; page titles, menus, shortcuts, + settings tabs and onboarding use the same words. The rail may shorten them ("Config" for + "Configuration", "Listák" for "Lejátszási listák"), never swap in another word. [Nielsen #4] +- **One verb per meaning.** Delete / Törlés: gone for good. Remove / Eltávolítás: out of a list, the + thing itself survives. Dismiss / Elrejtés: a notice leaves the view. Discard / Elvetés: unsaved edits + are thrown away. A confirm uses its action's verb. [Nielsen #4] +- **Sentence case.** Capitals only for brand names (YouTube, Plex, Google) and the app's places used as + places ("Hide from Feed", "Megnézés a Könyvtárban"). [Material, HIG] +- **"…" when the command asks first.** A label ends in "…" when it opens a dialog, form or picker before + anything happens ("Edit tags…"); not on a submenu (its chevron says it) or an immediate action. Progress + text ("Loading…") keeps its own. [HIG, Windows] +- **Messages.** A toast or error of one sentence has no full stop and no "✓": "Couldn't sync + subscriptions" / "Nem sikerült szinkronizálni". Two sentences keep their stops. Help text is prose. +- **Quotes** around a name are curly: “abc” in English, „abc” in Hungarian. +- **Dates**, in the UI language, only from `lib/format.ts` (lint: `local/date-format`): recency in a list + is relative ("3d ago" / "3 napja") through `ui/RelativeTime`, which carries the exact minute on hover; + a date is "Sep 27, 2026" / "2026. szept. 27."; a moment "Sep 27, 2026, 08:30 AM" / + "2026. szept. 27. 08:30"; seconds only in the server log, the audit and the recorder. +- **Units.** A figure with a unit (a length, a countdown, a range of days) is the browser's short form + (`Intl`), through `formatUnit` / `formatRuntime`: "90 min", "2 hr 13 min" / "90 p", "2 ó 13 p"; a fixed + label holding one is written in that same form ("Under 90 min"). Never a bare letter ("90m", "24h", + "7d"; test-held). In a sentence, and in a list of time spans named in words ("24 hours", "1 week"), the + unit is a word. A clock-style length on a picture stays "2:34:11". +- A label says what happens ("Delete playlist"), not "OK"; a destructive confirm names the object. ## Anti-patterns @@ -514,78 +537,79 @@ pick a density step; it never changes the anatomy or loses an action. [Nielsen # ## Decision log -| Date | Decision | Chosen | Over | Why | -| ---------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-09-24 | Floating button | Back to top: a round 48 glass FAB; "round" = floats over something (media or the page) | A Material 3 rounded square; a labelled pill; the old 64 circle | The common web convention for back to top; round tells it apart from every toolbar button; 64 was oversized | -| 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 | -| 2026-09-24 | B1 Density | Compact 32 / 28 / 24 by explicit height | Standard 38 / 32 / 26 | The owner's read of the lab; touch is covered separately (A3), and one toolbar row lines up at 28 | -| 2026-09-24 | B2 Scheme set | Midnight, Forest→cyan, Slate→fuchsia, Matrix→lime | Today's six | The accent collided with danger (YouTube), warn (Slate) and success (Forest, Matrix); Midnight and Starship were one scheme twice; 4 of 6 light accents failed 4.5:1 | -| 2026-09-24 | B3 State layers | Text colour at 10% / 16% over anything | Material's 8 / 12%; today's opacity + bg-card | Visible in every scheme and mode (≥ 1.2:1); today's row hover was 1.00:1 on a card | -| 2026-09-24 | B4 Page chrome | One sticky band + fade; filters fold when short | A floating glass bar; everything scrolls | Tabs and filters always in reach, in one place; a keyboard-up tablet keeps its content | -| 2026-09-24 | B5 Surfaces | One class + radius per role | Today's per-site mix | Nested corners and no per-site alpha drift | -| 2026-09-24 | B6 /watch | The app's surfaces, native video controls | Flat and minimal | The page strangers see should look like the product; native controls work on any device | -| 2026-09-24 | B7 Nav rail | Soft tint + edge bar, optional labels (a user setting), slim | Solid accent (recommended); a Material rail | The owner's combination: calmer than a solid fill, labels for who wants them, no width cost | -| 2026-09-24 | B8 Loading | Skeleton by shape, spinner in a control | A spinner everywhere | Nothing jumps when data lands; a long wait doesn't look stuck | -| 2026-09-24 | B9 Fade | Toolbars / headers, panel tabs, dialog edges, chip rows | Thumbnails; buttons | The north-star depth where content scrolls under chrome; not on content or small controls | -| 2026-09-24 | C1 Action classification | The table (lab C1): state + primary at rest, everyday toggles on hover, "…" on touch, destructive into the menu | Visible buttons per site, as today | One action list per object, nothing gesture-only, fewer buttons without losing a function | -| 2026-09-24 | C2 Side panels | A tap opens an overlay drawer; pin is a separate button ≥ 1024 | A tap pins and the content reflows | Below 768 the filters were unreachable; on touch a tap pinned and pushed the grid | -| 2026-09-24 | C3 Players on touch | Tap = show the controls; 44px seek hit area; 44 buttons; the YT description behind ⓘ | Today (tap also pauses; 7.5px seek bar) | Measured live: you couldn't reach the seek bar without pausing, and the hover-only description never opened on a tap | -| 2026-09-24 | C4 Dialogs | Focus by purpose; a pinned footer | Focus on Close; the footer scrolls | A form starts in its first field, and Save stays on screen with the keyboard up | -| 2026-09-24 | C5 Plex player controls | The owner's lean set: no Stop / Restart / ±10 s buttons, Back by position, Next only with a next item, Resume + From the start on the card, touch double-tap ±10 s | Every control in one row (it ran off-screen below 1024); Back by a press counter; ±10 s keyboard-only | The row fits a portrait tablet; the decision to restart is made on the card; a tablet keeps the 10 s skip | -| 2026-09-24 | Glass tuner | Retired; the schemes are the only colour choice | Keep it with ranges clamped to the rules | Free tuning is the drift the constitution exists to stop (owner) | -| 2026-09-24 | Matrix toned down | Moss #86a05a at Midnight's lightness, near-neutral greys; every white-on-accent part → `--accent-fg` | The B2 lime #a3e635 | The owner found the lime neon and tiring; it was the loudest accent (L 0.85, C 0.21), and fixed white parts sat on it at 1.5:1 | -| 2026-09-24 | Field primitives | `Input` / `Textarea` / `InnerInput` + `fieldClass`, `SelectTrigger`, the native controls; the lint rule "no raw ``" | `inputCls` and ~40 hand-styled boxes | Six edges, four radii and two focus looks for one control; the ≥ 3:1 edge held nowhere | -| 2026-09-25 | Surface roles | `SURFACE` roles in `ui/surfaces.ts`; the lint rule "no bare glass class"; menus p-1 with r8 rows | Three tiers paired ad hoc with five radii and three menu paddings | ~140 sites drifted; nested corners only hold if the radius comes with the role | -| 2026-09-25 | Static labels | Flat tonal `Badge`, no edge; a filtering tag is an edged chip | A glass pill with an edge, like the chips | Edged = pressable at a glance; a static tag that looks like a chip gets clicked and does nothing | -| 2026-09-25 | Header search | A 40 glass pill with the field edge (`lg`); what sits inside it is round | 44, r16, a glass box with a faint edge | The one rounded box among pills, and its edge under 3:1; the hero search is the YouTube/Gmail pill | -| 2026-09-25 | Switch row (F12) | The whole settings row toggles (`SwitchRow`; a hand-laid row points a label at the switch) | Only the 36×24 switch toggles | The iOS / Material settings row; the text was a dead click beside the control it names | -| 2026-09-25 | Edges and hovers (F14) | The ≥ 3:1 edge on the field family (fields, the form trigger); labelled pills keep the faint edge and the accent-edge hover | The ≥ 3:1 edge and a state-layer hover on every control | 1.4.11 asks an edge only where nothing else identifies the control; a heavy edge on every chip and tab weighs the whole UI down | -| 2026-09-25 | Rating and live (F16) | Own tokens: `--rating` (theme-scoped gold), `--live`/`--live-fg`, `--rating-media`, `--media-chrome`; palette classes lint-refused | Map them to `--warn` / `--danger` | A gold star and a red LIVE are what the eye expects; as warn/danger they would read as a status. The old amber score was 2:1 on light | -| 2026-09-25 | Fjord trial (B2 exception) | A fifth scheme, temporary: Midnight's neutrals, Playnite's steel accent (#337599 lifted to #5aa1cb) and steel rules; exit = fold into Midnight or remove | Retune Midnight straight away; borrow only the qualities | The owner wants to live with the calmer palette app-wide before it replaces the default; it breaks "own hue family" on purpose, and only until that call | -| 2026-09-25 | Whole-card row (owner) | A card row's B3 layer is the card's `::before` under its content; the open button's hit layer spans the card, the actions sit above it | The name button's own hover, ending at the first badge or button | A row that half-answers the pointer reads as broken; a badge click did nothing | -| 2026-09-25 | Badge tones (F29) | `Badge tone` neutral / accent / warn at 6%; light `--warn` #b45309 → #a14a07; edged label spans lint-refused | Keep edged status pills (a private Badge in Users, Settings "Granted", the suggestions count) | Edged = pressable at a glance; amber-700 was 4.58:1 as text and fell under 4.5 on its own tint | -| 2026-09-26 | Page band built (B4) | Controls only in the band (intros scroll); the fold shows the same row in place; one `h1` per page (`PageHeading` steps the hidden one aside); `SectionLabel` headings; Skip to content | A popover of the folded filters; the page title kept visible | A folded row is still the same controls, nothing duplicated; the rail and the tab title already name the page | -| 2026-09-26 | Fade built (B9) | `fade-below` on sticky toolbars and table headers; the scroller mask where the band sits on the backdrop | A painted band over the backdrop | The backdrop is the colour under the band, so fading the content into it is the same edge | -| 2026-09-26 | Viewport units | `dvh` everywhere (the Plex player stage with S3f) | `vh` / `h-screen` | `100vh` hides the last rows under a tablet browser's toolbar | -| 2026-09-26 | Video actions built (C1) | One list per video (menu, right-click, long-press, "…"); the card keeps watched / save / add to playlist on its picture on hover; download, details, copy, hide, reset in the menu | The six-button row under the card and the hover ⓘ | The menu is the one door every device has; the card loses a line of height and the touch card its row of 28px buttons | -| 2026-09-26 | Plex actions built (C1) | One list per Plex title (play/open, watched, add to playlist, media info, copy title, pin); the poster keeps watched / add / "…" on its picture on hover; seasons get open / watched / add / copy | The hover ⓘ, the pin-only menu, an episode's add button at rest, a collection's "Remove" X on the card | The same door as a video's; the collection grid goes to two columns on a phone so the touch "…" leaves the title its line | -| 2026-09-26 | Playlist actions built (C1) | A playlist row's "…" (play, pin, copy title / link, move, order, remove), Delete + an undo toast; one list per playlist (rename, pin, YouTube open / push / revert, delete) from its rail row and its header's "…" | The row's X column and its confirm; the header's pencil, YouTube link, push, revert and trash buttons | Remove is reversible, so it needs no confirm; the header keeps Play all; a narrow table drops "#" and Channel so the title keeps a width | -| 2026-09-26 | Channel + Plex list actions (C1) | A channel row's "…" (open, pin, YouTube, copy, full history, hide + undo, unsubscribe); a Plex playlist's title / show / season "…", Delete + undo; its header's "…" (rename, delete); an action toast lasts ≥ 8 s | The channel row's full-history, hide and unsubscribe buttons; the Plex list's red X on every row, show and season; the header's pencil and trash | Hiding and removing are reversible, so they need no confirm and the undo needs time; unsubscribe and deleting a playlist keep theirs | -| 2026-09-26 | Download / notification / Scheduler actions (C1) | A download row's "…" (play, share, save, edit, details, re-download, manage sharing, copies, cancel / delete / remove); a library row's Share stays in sight, ahead of save; a notification row's "…" and hover dismiss; the inbox band's and the Scheduler strip's "…" | The download row's five rare buttons and its cancel / delete / remove; the source link's copy button; an activity notice's Find / Undo links; the band's Clear everything; the strip's Purge discovery | The destructive rows keep their confirm, and Clear everything gains one; a row link keeps the browser's own menu; Find in feed drops the tag / search / age filters that could hide its video | -| 2026-09-26 | Channel menu: Tags + Priority (C1) | Tags A–Z as in-place checks; Priority as fixed steps 10 · 5 · 3 · 2 · 1 · 0, the current one ticked; a pick re-sorts at once and the page + focus follow the row; one drawn `MenuCheck` for every checkable row that cannot be an `` | Relative steps (+1 / +5); straight to the top; the stepper only; seven hand-drawn check marks (three box sizes, a tinted box, a × for "on", a trailing tick) | Bubbling a channel up is one pick; fixed levels keep the scale the reader already uses; the list moving where the reader can see it is the feedback | -| 2026-09-26 | Reorder without a drag (WCAG 2.5.7) | Row menus: Top / Up / Down / Bottom (YouTube + Plex playlists); saved views: four buttons with the place beside the selected view (owner: A); side panels in edit mode: an Up / Down pair per group (owner: A), the name wrapping there | A "…" on every saved-view row; a "…" on every panel group; drag only | A visible, one-click path where the list is edited; the edge moves shown off so the buttons never shift | -| 2026-09-26 | Titles in click surfaces | The surface stays a button, flagged `selectable`: its text selects, and the click that ends a selection does nothing; every plain `Pressable` pinned with its reason | Rebuilding each card and row so the title sits outside the button; exceptions for master-detail lists | Chrome selects inside a button once it is `user-select: text` (measured), so one flag on the primitive keeps the keyboard, focus and list walking every surface already has | -| 2026-09-26 | Right-click on a selection | The browser's own menu opens on selected words; beside them the object's menu | A "Copy selection" row in the app menu | The reader selected the words to do something with them; the native menu copies, searches and translates, and changes nothing else | -| 2026-09-26 | Keyboard ring on a flush row | Inside the row (a table row by its tag, a log line, a show header in its clipping card, with the card's corners) | The ring outside, as on a card | Outside it ran under the table head and past the table, and a clipping card cut it away entirely (invisible focus) | -| 2026-09-26 | Fade at rest | The sticky header's fade appears with the first 1rem of scroll (a scroll-driven animation; no fade where unsupported) | A fade that is always drawn | At rest it dimmed the first row and hid its ring's top edge; nothing is under the header yet | -| 2026-09-26 | Narrow playlist table | "#" and Channel hide by the table's own width (under 42rem), not the window's | Hiding below the `sm` window width | Beside the playlist rail a desktop window left the table 432 px and the title 0 px | -| 2026-09-26 | Background menu built | Five list pages (feed, Plex grid, channels, playlist, notifications); the toolbar's sort / view / filters and the band's actions; no destructive row | Every page with a toolbar; the "…" menus copied whole, delete included | The tab-only pages have nothing a menu would add; a blank-area right-click must not be one click from delete | -| 2026-09-26 | Media card anatomy (C) | One frame; the meta line pinned to its bottom; the length a badge on the picture; the Plex card on the same primitive as the video card | A: the year as an overline over the title; B: the year inline after it; a fixed two-line title slot | A one-line title left a hole the eye reads as a missing line; C puts nothing under a variable-height title | -| 2026-09-26 | One Play | `MediaPlayGlyph` (the media button variant) and only where the click plays | The Plex card's hand-painted accent circle; a playable dot on every card | One Play for every card; a dot that only a colour and a tooltip explain says nothing | -| 2026-09-26 | Filtering values on the info page | A value that filters (year, certificate, studio, director) is an edged chip; a fact is flat text | Everything flat, the clickable ones only hover-tinted | The S3d rule: edged = pressable, so the reader sees which values do something | -| 2026-09-26 | Shelf density | On any strip: one title line over one meta line | The grid's two lines and full meta on a strip | The tallest card set the strip's height and left holes in the cards with less to say | -| 2026-09-26 | Compact list row (B) | The title leads; a faint "…" at the end; the toggles float over the date on hover | A leading 128px action column | That column sat empty at rest, a hole at the start of every row | -| 2026-09-26 | Media-info layout (B) | A main column (collections first, crew, cast) beside a facts sidebar; an empty column is not drawn | Every section full width, stacked | The collection is the most used; the keywords' column left a void beside a short details box | -| 2026-09-26 | Mixed Home shelf (A) | Plex titles as wide art beside videos | Posters, with the videos not stretched | Stretching a video card to a poster's height left an ~80px gap | -| 2026-09-27 | Home hero | A card (hover row + Play on the picture, the whole menu, a touch "…"; owner: A); three quarters wide and centred on a wide window; one fact line; the summary at the measure | The hero's pin row alone; full width; an 18rem picture column | The one answer stands in focus, and it offers what the same card offers anywhere | -| 2026-09-27 | Home shelf tiles | A channel / playlist tile: Play, ↗ open, "…" with Open; a show acts on its next episode, ⓘ opens it; the shelf slot 232px | The tile's pin row alone; 200px | A tile without its object's actions read as broken; at 200 the Play touched the hover row | -| 2026-09-27 | Synopsis preview (C) | A ~0.9 s mouse rest opens a passive panel beside the card | The synopsis as the picture's `title` tooltip; an ⓘ to hover; none | One long tooltip line fired anywhere on the art and clashed with the cut title's own tooltip | -| 2026-09-27 | Reading measure | Prose capped at ~65 characters a line (`measure`) | The container's full width | At 110–130 characters a line the eye loses the next line's start | -| 2026-09-24 | Chip family | One `Chip` (filter / token / tag / suggest / ghost) + `DismissibleChip`; nothing under 24; the lint rule "no raw `