docs(design): record Master Control revision 2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
52b11fda93
commit
1b6dae676a
+116
-33
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"generatedAt": "2026-08-26T00:00:00.000Z",
|
||||
"title": "Design System: arr — Master Control",
|
||||
"generatedAt": "2026-10-24T00:00:00.000Z",
|
||||
"title": "Design System: arr — Master Control, revision 2 (mobile first)",
|
||||
"extensions": {
|
||||
"colorMeta": {
|
||||
"ground": {
|
||||
@@ -129,7 +129,7 @@
|
||||
},
|
||||
"label": {
|
||||
"displayName": "Label (system sans)",
|
||||
"purpose": "system-ui 600 uppercase, 0.6875rem, 0.12em tracking — engraved micro-labels (module roles). Never a kicker above a heading."
|
||||
"purpose": "system-ui 600 uppercase, 0.6875rem, 0.12em tracking — group labels inside a section (eligible, below policy, no pt source). Never a kicker above a heading."
|
||||
},
|
||||
"readout": {
|
||||
"displayName": "Readout (mono)",
|
||||
@@ -137,7 +137,11 @@
|
||||
},
|
||||
"scale": {
|
||||
"displayName": "Type scale",
|
||||
"purpose": "Seven fixed steps as tokens: --text-2xs 0.6875rem (micro-labels), --text-xs 0.75rem (details, chips, controls), --text-sm 0.8125rem (readouts), --text-base 1rem (body), --text-md 1.0625rem (module names), --text-lg 1.3125rem (master module), --text-xl 1.625rem (wordmark). Components never carry a literal font-size. One control geometry: buttons and chips share --control-h 2rem and --text-xs, so a season line reads as one system."
|
||||
"purpose": "Seven fixed steps as tokens: --text-2xs 0.6875rem (micro-labels, chips), --text-xs 0.75rem (controls, readouts in rows), --text-sm 0.8125rem (row titles on a phone, readouts), --text-base 1rem (body, inputs on touch so iOS never zooms), --text-md 1.0625rem (section titles), --text-lg 1.3125rem (page titles on a phone), --text-xl 1.625rem (page titles from 40rem, wordmark). Components never carry a literal font-size. Controls and chips no longer share a height: --control-h 2.75rem (44px touch target; 2.25rem only under (pointer: fine) and (min-width: 64rem)) and --chip-h 1.5rem."
|
||||
},
|
||||
"roles": {
|
||||
"displayName": "Type roles",
|
||||
"purpose": "Five roles, one job each. Page title: display 700, text-lg (text-xl from 40rem). Section: display 600, text-md, uppercase 0.08em, ink-muted. Group label: label 600, text-2xs, uppercase 0.12em, with its count. Row title: label 600, text-sm (text-base from 40rem), one line with ellipsis, year in the readout face beside it. Readout: mono, tabular numerals, text-xs/2xs."
|
||||
}
|
||||
},
|
||||
"shadows": [
|
||||
@@ -171,12 +175,20 @@
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"name": "stack",
|
||||
"value": "46rem — the chain turns vertical, one junctioned trace"
|
||||
"name": "base",
|
||||
"value": "the phone (390px) is the base layout; every rule without a query is phone truth"
|
||||
},
|
||||
{
|
||||
"name": "deck-narrow",
|
||||
"value": "40rem — the release deck sheds hdr and audio columns (expand row carries them) and tightens the surviving five; headers never wrap"
|
||||
"name": "wide",
|
||||
"value": "min-width 40rem — the tab bar becomes rail links, the search field sits in the rail, rows show priority-2 chips and their wide-only actions, ⋯ trays retire"
|
||||
},
|
||||
{
|
||||
"name": "dense",
|
||||
"value": "(pointer: fine) and (min-width: 64rem) — --control-h 2.25rem, --row-min 3rem; the version readout and the / hint appear"
|
||||
},
|
||||
{
|
||||
"name": "row container",
|
||||
"value": "rows answer to their container (.deck, .buckets, .subs-panel, .chain-wrap): 40rem as above, 46rem turns release chips into the §9.3 fixed columns and the stacked chain into the five-column chain"
|
||||
}
|
||||
],
|
||||
"themes": {
|
||||
@@ -201,9 +213,9 @@
|
||||
"name": "Chip",
|
||||
"kind": "custom",
|
||||
"refersTo": "ground",
|
||||
"description": "The §9.3 column unit: a small machined panel holding one mono value (score, resolution, source, HDR, audio, size, seeders). Fixed rhythm so columns align; never a soft pill.",
|
||||
"description": "The §9.3 column unit and every row's meta: a small machined panel holding one mono value. A label, not a target — --chip-h (24px), one line, its text never wraps. A row's chips sit on one line; what does not fit fades at the edge.",
|
||||
"html": "<span class=\"chip readout\">2160p</span>",
|
||||
"css": ".chip { display: inline-flex; align-items: center; gap: var(--space-1); min-height: var(--control-h); padding: 0 var(--space-2); background: var(--panel-raised); border: 1px solid var(--line); border-radius: var(--radius); font-family: var(--font-readout); font-size: var(--text-xs); color: var(--ink-muted); }"
|
||||
"css": ".chip { display: inline-flex; align-items: center; gap: var(--space-1); height: var(--chip-h); padding: 0 var(--space-2); background: var(--panel-raised); border: 1px solid var(--line); border-radius: 3px; font-family: var(--font-readout); font-size: var(--text-2xs); white-space: nowrap; color: var(--ink-muted); }"
|
||||
},
|
||||
{
|
||||
"name": "Verdict chip",
|
||||
@@ -217,9 +229,9 @@
|
||||
"name": "Status chip",
|
||||
"kind": "custom",
|
||||
"refersTo": "status-airing",
|
||||
"description": "Library status (§4.2): airing / incomplete / waiting / complete / ended. Informational, not severity — colour marks activity in one channel-violet family (airing brightest, incomplete, waiting dimmest), satisfaction is neutral (complete bright ink, ended faint). The mono word is always present.",
|
||||
"description": "Library status (§4.2): airing / incomplete / waiting / complete / ended. Activity is channel violet (airing); satisfaction is signal green (complete, ended, a full N/M count); incomplete, waiting and a partial count are neutral ink. Amber never appears here — it is reserved for what needs the operator. The mono word is always present.",
|
||||
"html": "<span class=\"chip readout\" data-status=\"airing\">airing</span>",
|
||||
"css": ".chip[data-status=\"airing\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); } .chip[data-status=\"incomplete\"] { color: var(--signal-warn); border-color: oklch(from var(--signal-warn) l c h / 55%); } .chip[data-status=\"complete\"], .chip[data-status=\"ended\"] { color: var(--signal-ok); border-color: oklch(from var(--signal-ok) l c h / 45%); } .chip[data-counts=\"complete\"] { color: var(--signal-ok); } .chip[data-counts=\"partial\"] { color: var(--signal-warn); }"
|
||||
"css": ".chip[data-status=\"airing\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); } .chip[data-status=\"incomplete\"] { color: var(--ink); border-color: var(--line-strong); } .chip[data-status=\"complete\"], .chip[data-status=\"ended\"], .chip[data-counts=\"complete\"] { color: var(--signal-ok); border-color: oklch(from var(--signal-ok) l c h / 45%); } .chip[data-counts=\"partial\"] { color: var(--ink); }"
|
||||
},
|
||||
{
|
||||
"name": "Media state chip",
|
||||
@@ -227,15 +239,15 @@
|
||||
"refersTo": "signal-ok",
|
||||
"description": "§4.2's per-title state chip. Every state keeps the word, with one drawn exemption: `available` renders as a check glyph instead of the word `available` — a shape, not only a hue, so it survives a greyscale read the rule otherwise protects. The word moves to the chip's aria-label (exposed via role=\"img\", since a bare span's aria-label is not reliably read).",
|
||||
"html": "<span class=\"chip readout\" role=\"img\" aria-label=\"available\" data-movie-state=\"available\"><svg viewBox=\"0 0 16 16\" width=\"12\" height=\"12\"><path d=\"M3.2 8.4l3 3 6.6-6.8\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg></span>",
|
||||
"css": ".chip[data-movie-state=\"available\"] { color: var(--signal-ok); border-color: oklch(from var(--signal-ok) l c h / 45%); } .chip[data-movie-state=\"missing\"][data-wanted=\"true\"] { color: var(--signal-warn); border-color: oklch(from var(--signal-warn) l c h / 55%); } .chip[data-movie-state=\"downloading\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); }"
|
||||
"css": ".chip[data-movie-state=\"available\"] { color: var(--signal-ok); border-color: oklch(from var(--signal-ok) l c h / 45%); } .chip[data-movie-state=\"missing\"][data-wanted=\"true\"] { color: var(--ink); border-color: var(--line-strong); } .chip[data-movie-state=\"downloading\"] { color: var(--status-airing); border-color: oklch(from var(--status-airing) l c h / 45%); }"
|
||||
},
|
||||
{
|
||||
"name": "Score chip",
|
||||
"name": "Score lead",
|
||||
"kind": "custom",
|
||||
"refersTo": "score-heat",
|
||||
"description": "Issue 111: the release deck's score column (§9.3) reads on a list-relative heat scale — seven stops from the lowest-scoring candidate on screen (red) to the highest (green) — rather than a flat eligible-green fill. Every other column chip stays the plain neutral Chip.",
|
||||
"html": "<span class=\"chip readout cw cw-score\" data-score=\"6\" aria-label=\"score 128\">128</span>",
|
||||
"css": ".cw-score[data-score=\"0\"] { color: var(--score-0); border-color: oklch(from var(--score-0) l c h / 45%); } .cw-score[data-score=\"6\"] { color: var(--score-6); border-color: oklch(from var(--score-6) l c h / 45%); }"
|
||||
"description": "The release row's lead (§9.3): a 44px square carrying the score on the list-relative heat scale (issue 111) — seven stops from the lowest-scoring candidate on screen (red) to the highest (green).",
|
||||
"html": "<span class=\"score\" data-score=\"6\" aria-label=\"score 128\">128</span>",
|
||||
"css": ".score { display: inline-grid; place-items: center; width: var(--control-h); height: var(--control-h); font: 700 var(--text-sm) var(--font-readout); border: 1px solid var(--line); border-radius: var(--radius); } .score[data-score=\"6\"] { color: var(--score-6); border-color: oklch(from var(--score-6) l c h / 50%); }"
|
||||
},
|
||||
{
|
||||
"name": "Module (channel strip)",
|
||||
@@ -257,24 +269,72 @@
|
||||
"name": "Control",
|
||||
"kind": "button",
|
||||
"refersTo": "accent",
|
||||
"description": "The interactive element: mono uppercase, phosphor-cyan text, panel ground, ≥44px hit target. Cyan belongs to interaction only — never to status.",
|
||||
"description": "Five controls, one rule for words versus icons: icons in rows, words in headers and panels; a row may carry one word control only when that verb is why the row exists (GRAB, ALLOW ENGLISH). Primary: cyan mono uppercase on a raised plate, one per header or panel. Quiet (.control-quiet): ink on a hairline — cancel, add, retry; never a toggle. Icon (.control-icon): a 44px square glyph, aria-label is the name. Fixed (.control-fixed): a word whose label changes (GRAB → SENDING → SENT) keeps one width. Destructive ([data-kind=delete]): rests red, arms to an amber check in the same square, fires on the second tap, disarms after 4 s; the explanation goes in the row's status slot, never inside the button. One verb, one form: search is the search glyph in a row and \"search indexers\" in a header; open is the row itself; trailer is the play glyph (plus the word from 40rem).",
|
||||
"html": "<button type=\"button\" class=\"control\">probe</button>",
|
||||
"css": ".control { display: inline-flex; align-items: center; justify-content: center; min-height: var(--control-h); padding: 0 var(--space-4); font-family: var(--font-readout); font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); background: var(--panel-raised); border: 1px solid var(--line-strong); border-radius: var(--radius); }"
|
||||
"css": ".control { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-height: var(--control-h); padding: 0 var(--space-4); font-family: var(--font-readout); font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); background: var(--panel-raised); border: 1px solid var(--line-strong); border-radius: var(--radius); } .control-icon { width: var(--control-h); padding: 0; } .control[data-armed=\"true\"] { color: var(--ground); background: var(--signal-warn); border-color: var(--signal-warn); }"
|
||||
},
|
||||
{
|
||||
"name": "Rail",
|
||||
"kind": "nav",
|
||||
"refersTo": "ground",
|
||||
"description": "Full-width panel strip framing the board top (identity + controls) and bottom (readouts). Hairline-ruled, panel ground, inset highlight. Contents sit in a .rail-inner column capped at 68rem, centred — the panel stays full-bleed. The lamp beside the wordmark is the only header status signal.",
|
||||
"html": "<header class=\"rail\"><div class=\"rail-inner\"><div class=\"rail-id\"><span class=\"lamp\" data-state=\"ok\" aria-hidden=\"true\"></span><h1 class=\"wordmark\">arr</h1></div></div></header>",
|
||||
"css": ".rail { padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--line); background: var(--panel); box-shadow: inset 0 1px var(--highlight); } .rail-inner { display: flex; align-items: center; gap: var(--space-6); width: 100%; max-width: 68rem; margin-inline: auto; }"
|
||||
"description": "One sticky line (--rail-h) on a panel ground: master lamp and wordmark, then — from 40rem — the destination links (library, queues with its amber count, settings), the search field and, from 64rem, the version readout. Below 40rem the destinations live in the tab bar and the field opens over the rail from a search glyph. The lamp beside the wordmark is the only header status signal.",
|
||||
"html": "<header class=\"rail\"><div class=\"rail-inner\"><div class=\"rail-id\"><span class=\"lamp\" data-state=\"ok\" aria-hidden=\"true\"></span><a class=\"wordmark\" href=\"/\">arr</a></div><nav class=\"rail-nav\">…</nav><div class=\"rail-search\">…</div></div></header>",
|
||||
"css": ".rail { position: sticky; top: 0; padding-top: var(--safe-top); border-bottom: 1px solid var(--line); background: var(--panel); box-shadow: inset 0 1px var(--highlight); } .rail-inner { display: flex; align-items: center; gap: var(--space-3); max-width: 68rem; height: var(--rail-h); margin-inline: auto; padding-inline: var(--gutter); }"
|
||||
},
|
||||
{
|
||||
"name": "Row",
|
||||
"kind": "list",
|
||||
"refersTo": "ground",
|
||||
"description": "The one list primitive (web/src/ui.ts `row()`): a grid of lead | body | actions plus a full-width tray. The body is minmax(0, 1fr), so a long title or a run of chips is clipped inside it and can never push the actions onto a second line. The body holds at most a title line, a meta line of chips (priority-2 chips from 40rem) and a sub line (evidence, full on a tap). The action column has fixed slots per row kind — icon, toggle or word width — and an empty slot keeps its width, so a kind keeps one column. On a phone a row shows at most its primary action plus ⋯, which opens the other actions, labelled, in the tray. A row that opens something is a stretched link under the controls, marked by a chevron — never a fake button.",
|
||||
"html": "<li class=\"r\" data-row=\"episode\"><div class=\"r-lead\">E09</div><div class=\"r-body\"><div class=\"r-title\">The Last Signal</div><div class=\"r-meta\">…chips</div></div><div class=\"r-act\"><span class=\"r-slot\" data-width=\"icon\">…</span></div><div class=\"r-tray\">…</div></li>",
|
||||
"css": ".r { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; grid-template-areas: \"lead body act\" \"tray tray tray\"; column-gap: var(--space-3); min-height: var(--row-min); border-bottom: 1px solid oklch(from var(--line) l c h / 60%); }"
|
||||
},
|
||||
{
|
||||
"name": "Lamp toggle",
|
||||
"kind": "toggle",
|
||||
"refersTo": "accent",
|
||||
"description": "A rule that is on or off (wanted, tracked, targeted search, track future seasons): a small lamp, the label and a fixed-width on/off word. On is a lit cyan lamp and the word in cyan; off is a dark lamp and a faint word — never a grey plate that reads as disabled.",
|
||||
"html": "<button class=\"toggle\" aria-pressed=\"true\"><span class=\"toggle-lamp\"></span><span class=\"toggle-label\">wanted</span><span class=\"toggle-state\">on</span></button>",
|
||||
"css": ".toggle[aria-pressed=\"true\"] .toggle-lamp { background: var(--accent); box-shadow: 0 0 6px oklch(from var(--accent) l c h / 60%); } .toggle-state { min-width: 3ch; }"
|
||||
},
|
||||
{
|
||||
"name": "Disclosure",
|
||||
"kind": "button",
|
||||
"refersTo": "accent",
|
||||
"description": "Show / hide at full control height with a turning chevron: collapsed buckets (\"show 93\"), paged lists (\"show 103 more\"). Tabular digits, so the label never changes width. A season row's lead is the same idea as an icon control.",
|
||||
"html": "<button class=\"control control-quiet disclose\" aria-expanded=\"false\"><span class=\"control-word\">show 93</span><svg class=\"icon\">…</svg></button>",
|
||||
"css": ".disclose { justify-content: space-between; min-width: 9rem; }"
|
||||
},
|
||||
{
|
||||
"name": "Segmented",
|
||||
"kind": "toggle",
|
||||
"refersTo": "accent",
|
||||
"description": "One choice of a few, joined: library grid/list, the search kind filter, a subtitle language picker. The engaged side is lit with an inset cyan edge so joined borders never double.",
|
||||
"html": "<fieldset class=\"segmented\"><button class=\"seg\" aria-pressed=\"true\">all</button><button class=\"seg\" aria-pressed=\"false\">movies</button></fieldset>",
|
||||
"css": ".seg[aria-pressed=\"true\"] { color: var(--accent-bright); background: var(--panel-raised); box-shadow: inset 0 0 0 1px var(--accent); }"
|
||||
},
|
||||
{
|
||||
"name": "Tab bar",
|
||||
"kind": "nav",
|
||||
"refersTo": "ground",
|
||||
"description": "Below 40rem only: four destinations within thumb reach — library, search, queues (always present, amber count badge), settings — over the safe-area inset. The active destination is lit from the route, not from a tap, so deep links and back light the right one.",
|
||||
"html": "<nav class=\"tabbar\"><a class=\"tab\" href=\"/\" aria-current=\"page\">…library</a>…</nav>",
|
||||
"css": ".tabbar { position: fixed; bottom: 0; display: grid; grid-template-columns: repeat(4, 1fr); height: var(--tabbar-space); padding-bottom: var(--safe-bottom); background: var(--panel); border-top: 1px solid var(--line); }"
|
||||
},
|
||||
{
|
||||
"name": "Detail page",
|
||||
"kind": "layout",
|
||||
"refersTo": "ground",
|
||||
"description": "A back control and the title open every detail page. The hero is a fixed grid — poster box, chip line, rating and meta lines, three reserved lines of overview (a tap opens the rest) — that data fills without adding rows. Then one action bar: the primary word (search indexers), the trailer, and ⋯, which opens the page's settings: the external links, the rule toggles, the root and remove.",
|
||||
"html": "<header class=\"page-head\">…</header><div class=\"hero-body\">…</div><div class=\"actionbar\">…</div>",
|
||||
"css": ".hero-body { display: grid; grid-template-columns: 6rem minmax(0, 1fr); gap: var(--space-4); }"
|
||||
}
|
||||
],
|
||||
"narrative": {
|
||||
"northStar": "Master Control",
|
||||
"overview": "arr's UI is a broadcast master-control panel: the daemon is a signal chain (TMDB and Prowlarr feed in, Transmission is driven out) and every surface reads as lamps, readouts and traces on that chain. Near-black charcoal ground, machined panels with hairline borders and a 1px inset highlight, Rajdhani display names, monospace readouts, three semantic lamp hues plus one phosphor-cyan interactive hue. Committed dark. Built for the operator's one-glance question: can arr do its job, and if not, which upstream is down.",
|
||||
"keyCharacteristics": [
|
||||
"Signal chain, not dashboard: topology mirrors DESIGN.md §3; no sidebar, no stat cards, no hero metric.",
|
||||
"Signal chain, not dashboard: topology mirrors DESIGN.md §3 (now a settings section); no sidebar, no stat cards, no hero metric. Phone first: a sticky one-line rail and a bottom tab bar.",
|
||||
"Lamps carry state, words carry proof — every hue is paired with a mono status word.",
|
||||
"Rajdhani 700 uppercase for names, system sans for micro-labels, mono for every value.",
|
||||
"Panels are machined: hairline border, inset highlight, 4px radius; never soft shadows or glass.",
|
||||
@@ -283,7 +343,7 @@
|
||||
"rules": [
|
||||
{
|
||||
"name": "The Lamp-Hue Rule",
|
||||
"body": "Green/amber/red belong to state and to nothing else. Interaction is phosphor-cyan. A saturated color that is neither a state nor an interactive affordance is a bug.",
|
||||
"body": "Green, amber and red belong to state and to nothing else; amber means one thing — this needs you, or is armed. Interaction is phosphor-cyan. Wanted-but-missing, partial counts and rating stars are ink, not amber. A saturated colour that is neither a state nor an interactive affordance is a bug.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
@@ -293,7 +353,7 @@
|
||||
},
|
||||
{
|
||||
"name": "The Readout Rule",
|
||||
"body": "Monospace is for values the operator reads — statuses, sizes, latencies, versions. Prose and labels are never mono; names are never sans.",
|
||||
"body": "Monospace is for values the operator reads — statuses, sizes, latencies, versions, paths — always with tabular numerals. Prose and labels are never mono. Display (Rajdhani) carries page titles, section titles and module names; row titles are the label face, one line.",
|
||||
"section": "typography"
|
||||
},
|
||||
{
|
||||
@@ -308,7 +368,7 @@
|
||||
},
|
||||
{
|
||||
"name": "The Status-Is-Not-Severity Rule",
|
||||
"body": "The five library statuses are informational, never an error/warning/success ramp. Activity lives in one channel-violet family (airing > incomplete > waiting, by lightness and chroma); satisfaction is neutral (complete bright ink, ended faint). Complete is never green; incomplete is never amber.",
|
||||
"body": "The five library statuses are informational, never an error ramp. Activity is violet (airing, downloading); satisfaction is green (complete, ended, a full count); incomplete, waiting and a partial count are neutral ink. Amber never appears on a status.",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
@@ -328,21 +388,44 @@
|
||||
},
|
||||
{
|
||||
"name": "The List-Relative Heat Rule",
|
||||
"body": "The release deck's score chip (issue 111) is not an absolute quality scale — its seven stops are relative to the lowest and highest score on screen right now. Colour there answers \"which of these\", not \"how good is this release in general\".",
|
||||
"body": "The release row's score lead (issue 111) is not an absolute quality scale — its seven stops are relative to the lowest and highest score on screen right now. Colour there answers \"which of these\", not \"how good is this release in general\".",
|
||||
"section": "colors"
|
||||
},
|
||||
{
|
||||
"name": "The Row Rule",
|
||||
"body": "Every list is the one row primitive. Its action column never wraps and keeps fixed slots per row kind; the body clips, the actions do not move. A phone row shows its primary action plus ⋯; the rest are labelled in the tray.",
|
||||
"section": "layout"
|
||||
},
|
||||
{
|
||||
"name": "The Icons-In-Rows Rule",
|
||||
"body": "Rows repeat, so their actions are 44px glyph squares with an aria-label. Headers, panels and forms appear once, so they spell the action out. A row carries a word control only when that verb is why the row exists. One verb has one glyph everywhere.",
|
||||
"section": "components"
|
||||
},
|
||||
{
|
||||
"name": "The Reserve-The-Space Rule",
|
||||
"body": "Render the final layout empty, then fill it. Status lines are slots that keep their line; first loads paint skeleton rows at --row-min; the hero's poster box and text lines exist before their data; feedback changes a control's label at a fixed width; polled chips sit last on their line; long lists page at 25. Nothing is inserted above content after it paints.",
|
||||
"section": "layout"
|
||||
},
|
||||
{
|
||||
"name": "The Tap-To-Explain Rule",
|
||||
"body": "Touch has no hover, so no meaning lives only in a tooltip. A chip whose sentence will not fit (a waiver, a download's rate, an import failure) is a button that writes the sentence into its row's status slot; a group's long note opens from an info glyph.",
|
||||
"section": "components"
|
||||
}
|
||||
],
|
||||
"dos": [
|
||||
"Do lay future surfaces (manual search buckets, queues) out as panels and readouts consuming these tokens; chips in §9.3 are small panels with mono values.",
|
||||
"Do keep every control ≥44px, focus rings cyan and visible, and reduced-motion honored.",
|
||||
"Do build every list from the row primitive and every button from the five controls in web/src/ui.ts.",
|
||||
"Do keep every control at --control-h (44px on touch), focus rings cyan and visible, and reduced motion honoured.",
|
||||
"Do write the phone layout first and add from min-width 40rem / 64rem, or from container width for components.",
|
||||
"Do keep muted text at or above 4.5:1 on panel ground (--ink-muted floor).",
|
||||
"Do self-host every asset; the SPA is embedded in the binary and must work with no network.",
|
||||
"Do take every colour, spacing and font-size from the :root tokens; the token check (web/scripts/check-tokens.mjs) fails CI on literals.",
|
||||
"Do suppress a redundant status chip where another control on the same row already implies the exact same state one-to-one — the missing chip disappears from episode rows because the want button only ever renders for that state."
|
||||
"Do take every colour, spacing and font-size from the :root tokens; web/scripts/check-tokens.mjs fails CI on literals and on any var() that names a token nothing defines.",
|
||||
"Do suppress a redundant status chip where another control on the same row already implies the exact same state — the missing chip disappears from episode rows because the wanted toggle only ever renders for that state."
|
||||
],
|
||||
"donts": [
|
||||
"Colour state chips on the ramp — `complete` and `ended` green, `incomplete` and a partial count amber, `airing` and `downloading` violet, `waiting` and `untracked` neutral. The word stays in every chip, so state survives with colour removed.",
|
||||
"Don't use fault red for a rejected release; red means the daemon itself is broken.",
|
||||
"Don't let a row's actions wrap, or vary the action column between rows of one kind.",
|
||||
"Don't put a span styled as a control inside a clickable row; the row is the link.",
|
||||
"Don't toggle a status line's visibility above content, or change a button's width to report an outcome.",
|
||||
"Don't use amber for anything that does not need the operator; don't use fault red for a rejected release.",
|
||||
"Don't introduce a second interactive hue or use cyan for a status.",
|
||||
"Don't box content in soft-shadowed rounded cards or add glass/blur.",
|
||||
"Don't put a kicker/eyebrow label above a heading.",
|
||||
|
||||
@@ -720,8 +720,8 @@ the seasons you are deciding about now are at the top, not after nine rows of
|
||||
back catalogue.
|
||||
|
||||
**TMDB is the only metadata source.** The rating shown anywhere is TMDB's
|
||||
`vote_average` with its `vote_count`, rendered as amber stars (`--signal-warn`
|
||||
— the same state colour a wanted chip carries). No OMDb, no IMDb or Rotten
|
||||
`vote_average` with its `vote_count`, rendered as a star in ink — amber is
|
||||
kept for what needs the operator, and a rating never does. No OMDb, no IMDb or Rotten
|
||||
Tomatoes scores — each would need a second upstream, a second key and a second
|
||||
thing that can be down.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user