Files
arr/.impeccable/design.json
T
2026-08-24 14:38:03 +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 + 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; }"
}
],
"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."
]
}
}