Files
arr/.impeccable/design.json
T
Miguel Palhas fa5d876c09 chore(web): sync design.json to the state ramp
The token system still mirrored the pre-amendment palette and carried
the rule 'Don't colour complete green or incomplete amber', which now
contradicts DESIGN.md 4.2 and would have had the next session revert
the ramp. Drops the four --status-* tokens no rule consumes any more;
satisfaction reuses --signal-ok and a gap --signal-warn.

Committed by the blitz orchestrator: the authoring session wedged.
2026-08-24 14:37:52 +01:00

312 lines
17 KiB
JSON

{
"schemaVersion": 2,
"generatedAt": "2026-08-22T00:00:00.000Z",
"title": "Design System: arr — Master Control",
"extensions": {
"colorMeta": {
"ground": {
"role": "neutral",
"displayName": "Console Charcoal",
"canonical": "oklch(0.14 0.01 250)",
"tonalRamp": [
"oklch(0.14 0.01 250)",
"oklch(0.18 0.012 250)",
"oklch(0.21 0.014 250)",
"oklch(0.3 0.015 250)",
"oklch(0.42 0.02 250)",
"oklch(0.58 0.015 250)",
"oklch(0.72 0.015 250)",
"oklch(0.93 0.008 250)"
]
},
"signal-ok": {
"role": "secondary",
"displayName": "Signal Green",
"canonical": "oklch(0.8 0.17 150)",
"tonalRamp": [
"oklch(0.45 0.1 150)",
"oklch(0.62 0.14 150)",
"oklch(0.8 0.17 150)",
"oklch(0.88 0.12 150)"
]
},
"signal-warn": {
"role": "tertiary",
"displayName": "Caution Amber",
"canonical": "oklch(0.82 0.14 80)",
"tonalRamp": [
"oklch(0.5 0.1 80)",
"oklch(0.66 0.13 80)",
"oklch(0.82 0.14 80)",
"oklch(0.9 0.1 80)"
]
},
"signal-fault": {
"role": "secondary",
"displayName": "Fault Red",
"canonical": "oklch(0.68 0.21 25)",
"tonalRamp": [
"oklch(0.42 0.15 25)",
"oklch(0.55 0.19 25)",
"oklch(0.68 0.21 25)",
"oklch(0.8 0.14 25)"
]
},
"accent": {
"role": "primary",
"displayName": "Phosphor Cyan",
"canonical": "oklch(0.8 0.12 220)",
"tonalRamp": [
"oklch(0.5 0.1 220)",
"oklch(0.65 0.11 220)",
"oklch(0.8 0.12 220)",
"oklch(0.9 0.08 220)"
]
},
"verdict-eligible": {
"role": "secondary",
"displayName": "Eligible Green",
"canonical": "oklch(0.8 0.17 150)",
"tonalRamp": [
"oklch(0.45 0.1 150)",
"oklch(0.62 0.14 150)",
"oklch(0.8 0.17 150)",
"oklch(0.88 0.12 150)"
]
},
"verdict-waived": {
"role": "tertiary",
"displayName": "Waiver Amber",
"canonical": "oklch(0.82 0.14 80)",
"tonalRamp": [
"oklch(0.5 0.1 80)",
"oklch(0.66 0.13 80)",
"oklch(0.82 0.14 80)",
"oklch(0.9 0.1 80)"
]
},
"verdict-rejected": {
"role": "neutral",
"displayName": "Rejected Slate",
"canonical": "oklch(0.68 0.02 250)",
"tonalRamp": [
"oklch(0.42 0.02 250)",
"oklch(0.55 0.02 250)",
"oklch(0.68 0.02 250)",
"oklch(0.8 0.015 250)"
]
},
"status-channel": {
"role": "tertiary",
"displayName": "Channel Violet",
"canonical": "oklch(0.78 0.14 305)",
"tonalRamp": [
"oklch(0.64 0.045 305)",
"oklch(0.72 0.09 305)",
"oklch(0.78 0.14 305)",
"oklch(0.86 0.09 305)"
]
}
},
"typographyMeta": {
"display": {
"displayName": "Display (Rajdhani)",
"purpose": "Rajdhani 700 — wordmark and module names only. Machined, condensed, uppercase with 0.1-0.12em tracking. Self-hosted woff2 in web/public/fonts (no CDN: the SPA is embedded in the binary and must work offline)."
},
"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."
},
"readout": {
"displayName": "Readout (mono)",
"purpose": "ui-monospace stack — every value the operator reads: status words, details, version, rtt. Mono is for measurement, never costume."
},
"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."
}
},
"shadows": [
{
"name": "panel-highlight",
"value": "inset 0 1px oklch(1 0 0 / 4%)",
"purpose": "The machined top edge of every panel and control. The only non-lamp shadow in the system."
},
{
"name": "lamp-glow",
"value": "0 0 6px var(--lamp), 0 0 14px oklch(from var(--lamp) l c h / 35%)",
"purpose": "A lamp's own light. Reserved for .lamp; never decoration on panels or text."
}
],
"motion": [
{
"name": "ease-out",
"value": "cubic-bezier(0.25, 1, 0.5, 1)",
"purpose": "Default easing for state transitions."
},
{
"name": "duration-state",
"value": "150-300ms",
"purpose": "Hover/focus 150ms, lamp and trace state changes 300ms."
},
{
"name": "power-up strike",
"value": "lamp-strike 420ms, staggered 120ms upstream→downstream, once",
"purpose": "The one authored moment: the board lights in signal order on first verdict. Guarded by prefers-reduced-motion."
}
],
"breakpoints": [
{
"name": "stack",
"value": "46rem — the chain turns vertical, one junctioned trace"
},
{
"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"
}
],
"themes": {
"strategy": "committed dark only — a control room is lit by its lamps; the operator uses this on a homelab desk at night. color-scheme: dark is declared in the shell.",
"dark": {
"bg": "oklch(0.14 0.01 250)",
"ink": "oklch(0.93 0.008 250)",
"primary": "oklch(0.8 0.12 220)"
}
}
},
"components": [
{
"name": "Lamp",
"kind": "custom",
"refersTo": "signal-ok",
"description": "Status lamp. Hue by state (ok/degraded/unconfigured/unreachable), glow is its own light. data-state=\"off\" is dead air: dark, no glow, no motion. Always paired with a status word — remove color and the state must survive.",
"html": "<span class=\"lamp\" data-state=\"ok\" aria-hidden=\"true\"></span>",
"css": ".lamp { --lamp: var(--signal-idle); width: 0.625rem; height: 0.625rem; border-radius: 50%; background: var(--lamp); box-shadow: 0 0 6px var(--lamp), 0 0 14px oklch(from var(--lamp) l c h / 35%); } .lamp[data-state=\"ok\"] { --lamp: var(--signal-ok); } .lamp[data-state=\"off\"] { --lamp: var(--lamp-off); box-shadow: none; }"
},
{
"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.",
"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); }"
},
{
"name": "Verdict chip",
"kind": "custom",
"refersTo": "verdict-eligible",
"description": "Release classification (§9.3): eligible / waived / rejected. Hue + border treatment + the word, so the three buckets survive with colour removed — eligible is a solid tinted border, waived a dashed border (the visible mark of a relaxed rule), rejected is quiet slate and always carries the killing rule's name. Rejected is the policy working, never fault red.",
"html": "<span class=\"chip readout\" data-verdict=\"waived\">waived · pt missing</span>",
"css": ".chip[data-verdict=\"eligible\"] { color: var(--verdict-eligible); border-color: oklch(from var(--verdict-eligible) l c h / 45%); } .chip[data-verdict=\"waived\"] { color: var(--verdict-waived); border-style: dashed; border-color: oklch(from var(--verdict-waived) l c h / 55%); } .chip[data-verdict=\"rejected\"] { color: var(--verdict-rejected); }"
},
{
"name": "Status chip",
"kind": "custom",
"refersTo": "status-channel",
"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.",
"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); }"
},
{
"name": "Module (channel strip)",
"kind": "card",
"refersTo": "ground",
"description": "One unit on the chain: lamp + Rajdhani name + mono status word, role label, mono detail. Panel with hairline border and 1px inset highlight; 4px machined radius, never a soft card.",
"html": "<article class=\"module\"><header class=\"module-head\"><span class=\"lamp\" data-state=\"ok\" aria-hidden=\"true\"></span><h2 class=\"module-name\">prowlarr</h2><span class=\"module-status readout\" data-tone=\"ok\">ok</span></header><p class=\"module-role\">indexers in</p><p class=\"module-detail readout\">torznab search and rss</p></article>",
"css": ".module { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: inset 0 1px var(--highlight); padding: var(--space-4) var(--space-4) var(--space-3); }"
},
{
"name": "Bus trace",
"kind": "custom",
"refersTo": "ground",
"description": "Signal path between modules: hairline + arrowhead SVG, tethered to its source module's row. Lit with the source's signal hue via data-tone; inert gray when nothing flows.",
"html": "<svg viewBox=\"0 0 40 8\" preserveAspectRatio=\"none\" class=\"bus-trace\" data-tone=\"ok\" aria-hidden=\"true\"><line x1=\"0\" y1=\"4\" x2=\"32\" y2=\"4\" /><path d=\"M32 0.5 39 4 32 7.5 Z\" class=\"bus-arrow\" /></svg>",
"css": ".bus-trace { --trace: var(--line-strong); } .bus-trace line { stroke: var(--trace); } .bus-arrow { fill: var(--trace); } .bus-trace[data-tone=\"ok\"] { --trace: var(--signal-ok); }"
},
{
"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.",
"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); }"
},
{
"name": "Rail",
"kind": "nav",
"refersTo": "ground",
"description": "Full-width panel strip framing the board top (identity + verdict + controls) and bottom (readouts). Hairline-ruled, panel ground, inset highlight.",
"html": "<header class=\"rail\"><div class=\"rail-id\"><span class=\"lamp\" data-state=\"ok\" aria-hidden=\"true\"></span><h1 class=\"wordmark\">arr</h1></div><p class=\"rail-verdict readout\" data-tone=\"ok\">all signals nominal</p></header>",
"css": ".rail { display: flex; align-items: center; gap: var(--space-6); padding: var(--space-3) var(--space-6); border-bottom: 1px solid var(--line); background: var(--panel); box-shadow: inset 0 1px var(--highlight); }"
}
],
"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.",
"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.",
"One authored motion: the power-up strike, upstream to downstream, once."
],
"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.",
"section": "colors"
},
{
"name": "The Word-Beside-The-Lamp Rule",
"body": "Every lamp and lit trace is paired with a monospace status word. Remove all color and the board must still read.",
"section": "colors"
},
{
"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.",
"section": "typography"
},
{
"name": "The Machined-Panel Rule",
"body": "Depth is one inset 1px highlight on a hairline-bordered panel. No drop shadows, no glass, no gradients. The only glow on the board is a lamp's own light.",
"section": "elevation"
},
{
"name": "The Verdict Rule",
"body": "Release verdicts are eligible green, waiver amber, rejected slate. Rejected is the policy engine doing its job, so it is quiet neutral — fault red stays reserved for the daemon's own failures. Each bucket also carries a non-colour mark: solid tinted border (eligible), dashed border (waived), the killing rule's name (rejected).",
"section": "colors"
},
{
"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.",
"section": "colors"
},
{
"name": "The Token Rule",
"body": "Components consume the custom properties declared in :root of web/src/style.css — never a literal colour, margin, padding or gap. web/scripts/check-tokens.mjs enforces this in CI; relative-colour derivation from a token (oklch(from var(--x) …)) counts as consuming the token.",
"section": "colors"
},
{
"name": "The Dead-Air Rule",
"body": "When nothing is flowing, nothing animates. Pulse means probing; data-state=\"off\" is dark and still. Motion that misreports activity is a truth bug.",
"section": "motion"
}
],
"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 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."
],
"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 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.",
"Don't animate idle or dead elements; the strike runs once, on real data."
]
}
}