feat(web): SPA skeleton embedded in the binary (#60)
This commit was merged in pull request #60.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"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)"]
|
||||
}
|
||||
},
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"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" }],
|
||||
"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: oklch(0.3 0.012 250); box-shadow: none; }"
|
||||
},
|
||||
{
|
||||
"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 { min-height: 2.75rem; padding: 0 var(--space-4); font-family: var(--font-readout); 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 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."
|
||||
],
|
||||
"donts": [
|
||||
"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."
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user