design: extend v1 to login / add-host / host-detail / job-log + lock components

Five hi-fi screens completing the Phase 1 surface, all in v1's dark
operator-console register.

  v1-login          Sparse centred card. Sign-in + first-error variant.
                    No marketing chrome; build version sits in footer
                    so a returning operator can spot agent drift.

  v1-add-host       Focused two-column page (form left, contextual
                    "what happens next" right) — not a modal. Two
                    states: form (state A) and minted-token result
                    with install command (state B). Backed by
                    POST /api/enrollment-tokens (P1-32).

  v1-host-detail    Persistent header (status dot, mono name, tags,
                    primary CTAs, vitals strip) over four sub-tabs
                    (Snapshots / Jobs / Repo / Settings). Snapshots
                    is the default — the thing 90% of operators
                    want when they click a host name. Right rail
                    holds Recent activity, run-now stack, and a
                    danger-zone panel.

  v1-job-log        WS-streamed log view. Three states: running (live
                    progress bar + auto-scroll cursor), succeeded
                    (summary stats + final lines), failed (error
                    panel + tail). Backed by WS /api/jobs/{id}/stream
                    (P1-21 remainder).

  v1-components     The load-bearing reference. 14 sections covering
                    tokens (colour + type scale), status, buttons,
                    form fields, tags, tabs, host row, log viewer,
                    progress bar, stat tile, modal, toast, install
                    snippet, empty-state pattern. Every CSS class is
                    real and copy-able into the Go template build.

This locks the visual register before P1-23 onwards. Each Phase 1
template gets a {{define}} matching a section in v1-components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 19:05:39 +01:00
parent cca525a04d
commit 8b7b1479a1
5 changed files with 1995 additions and 0 deletions
+703
View File
@@ -0,0 +1,703 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>restic-manager · v1 Components</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* ============================================================
v1 design tokens. Whatever the Go templates need, lives here.
Anything not in this file does not exist in v1. New components
get added here first, then templated.
============================================================ */
:root {
/* surface */
--bg: oklch(0.17 0.006 250);
--panel: oklch(0.20 0.007 250);
--panel-hi: oklch(0.23 0.008 250);
/* line */
--line: oklch(0.27 0.010 250);
--line-soft: oklch(0.23 0.008 250);
/* ink */
--ink: oklch(0.96 0.005 250);
--ink-mid: oklch(0.78 0.005 250);
--ink-mute: oklch(0.58 0.006 250);
--ink-fade: oklch(0.42 0.006 250);
/* state */
--ok: oklch(0.78 0.14 155);
--warn: oklch(0.82 0.13 80);
--bad: oklch(0.70 0.20 25);
--off: oklch(0.50 0.005 250);
/* one accent */
--accent: oklch(0.82 0.12 195);
--accent-dim:oklch(0.55 0.10 195);
}
html, body { background: var(--bg); color: var(--ink); }
body { font-family: 'Inter', system-ui, sans-serif; }
.mono { font-family: 'JetBrains Mono', monospace; font-variant-numeric: tabular-nums; }
::selection { background: color-mix(in oklch, var(--accent), transparent 70%); }
/* ---------- LAYOUT PRIMITIVES ---------- */
.doc { max-width: 1280px; margin: 0 auto; padding: 0 32px; }
.panel { background: var(--panel); border: 1px solid var(--line-soft); }
.hairline { box-shadow: inset 0 -1px 0 var(--line-soft); }
/* ---------- STATUS DOTS ---------- */
.dot { width: 7px; height: 7px; border-radius: 9999px; display: inline-block; }
.dot-online { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in oklch, var(--ok), transparent 80%); }
.dot-degraded { background: var(--warn); box-shadow: 0 0 0 3px color-mix(in oklch, var(--warn), transparent 80%); }
.dot-offline { background: var(--off); }
.dot-failed { background: var(--bad); box-shadow: 0 0 0 3px color-mix(in oklch, var(--bad), transparent 80%); }
.pulse { animation: pulse 2.4s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent), transparent 80%); }
50% { box-shadow: 0 0 0 6px color-mix(in oklch, var(--accent), transparent 92%); }
}
/* ---------- BUTTONS ---------- */
.btn {
font-size: 12px; font-weight: 500;
padding: 6px 11px; border-radius: 5px;
background: transparent; border: 1px solid var(--line);
color: var(--ink-mid);
transition: all 120ms ease; cursor: pointer;
}
.btn:hover { background: var(--panel-hi); color: var(--ink); }
.btn:disabled, .btn[disabled] { opacity: 0.4; cursor: not-allowed; pointer-events: none; }
.btn-primary { color: oklch(0.18 0.01 195); background: var(--accent); border-color: var(--accent); }
.btn-primary:hover { filter: brightness(1.08); }
.btn-ghost { border-color: transparent; }
.btn-ghost:hover { background: var(--panel-hi); border-color: transparent; }
.btn-danger { color: var(--bad); border-color: color-mix(in oklch, var(--bad), transparent 70%); }
.btn-danger:hover { background: color-mix(in oklch, var(--bad), transparent 88%); border-color: color-mix(in oklch, var(--bad), transparent 50%); color: oklch(0.85 0.10 25); }
.btn-lg { font-size: 13px; padding: 9px 14px; }
/* ---------- TAGS / CHIPS ---------- */
.tag {
display: inline-flex; align-items: center; gap: 5px;
font-size: 11px; line-height: 1; padding: 4px 7px;
border: 1px solid var(--line); color: var(--ink-mid);
border-radius: 3px; letter-spacing: 0.01em;
}
.tag-removable .x { color: var(--ink-fade); cursor: pointer; padding-left: 2px; }
.tag-status {
border: 1px solid color-mix(in oklch, var(--token), transparent 70%);
color: var(--token);
background: color-mix(in oklch, var(--token), transparent 88%);
}
/* ---------- FORM FIELDS ---------- */
.field-label { font-size: 12px; color: var(--ink-mid); margin-bottom: 6px; display: block; }
.field-help { font-size: 12px; color: var(--ink-mute); margin-top: 6px; line-height: 1.55; text-wrap: pretty; }
.field-error { font-size: 12px; color: oklch(0.85 0.10 25); margin-top: 6px; }
.field {
width: 100%; padding: 9px 12px;
background: var(--bg); border: 1px solid var(--line-soft);
color: var(--ink); border-radius: 5px;
font-size: 13px; font-family: inherit; outline: none;
transition: border-color 120ms ease;
}
.field:focus { border-color: var(--accent); }
.field.invalid { border-color: color-mix(in oklch, var(--bad), transparent 50%); }
.field.mono { font-family: 'JetBrains Mono', monospace; font-size: 12px; }
.field.with-prefix { padding-left: 64px; }
.field-prefix {
position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
font-family: 'JetBrains Mono', monospace; font-size: 12px;
color: var(--ink-mute); pointer-events: none;
}
/* ---------- TABS ---------- */
.nav-tab { font-size: 13px; padding: 18px 0; color: var(--ink-mute); border-bottom: 2px solid transparent; margin-right: 28px; cursor: pointer; }
.nav-tab.active { color: var(--ink); border-color: var(--accent); }
.nav-tab:hover { color: var(--ink); }
.sub-tab { font-size: 13px; padding: 12px 0; color: var(--ink-mute); border-bottom: 1.5px solid transparent; margin-right: 24px; cursor: pointer; }
.sub-tab.active { color: var(--ink); border-color: var(--ink); }
/* ---------- HOST ROW (the dashboard's load-bearing component) ---------- */
.host-row {
display: grid; align-items: center;
grid-template-columns: 24px 1.4fr 0.95fr 1.5fr 0.75fr 0.7fr 0.7fr 1.1fr 92px;
padding: 11px 16px; font-size: 13px;
border-left: 3px solid transparent;
}
.host-row.head { padding-top: 10px; padding-bottom: 10px; font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; border-left-width: 3px; }
.host-row.degraded { border-left-color: color-mix(in oklch, var(--warn), transparent 50%); }
.host-row.failed { border-left-color: color-mix(in oklch, var(--bad), transparent 50%); }
.host-row.offline { border-left-color: color-mix(in oklch, var(--off), transparent 70%); }
.host-row:hover { background: var(--panel-hi); }
/* ---------- LOG VIEWER ---------- */
.log {
background: var(--bg); border: 1px solid var(--line-soft);
border-radius: 7px;
font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7;
overflow: hidden;
}
.log-line { display: grid; grid-template-columns: 14ch 8ch 1fr; column-gap: 14px; padding: 1px 16px; align-items: baseline; }
.log-line:first-child { padding-top: 12px; }
.log-line:last-child { padding-bottom: 12px; }
.log-ts { color: var(--ink-fade); }
.log-tag { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-fade); }
.log-stream-stdout { color: var(--ink-mid); }
.log-stream-stderr { color: oklch(0.78 0.13 50); }
.log-stream-event { color: var(--accent); }
/* ---------- PROGRESS BAR ---------- */
.progress-track { background: var(--bg); border: 1px solid var(--line-soft); height: 6px; border-radius: 9999px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--accent); border-radius: 9999px; transition: width 250ms ease; }
.progress-fill.ok { background: var(--ok); }
.progress-fill.bad { background: var(--bad); }
/* ---------- TOAST ---------- */
.toast {
display: flex; align-items: flex-start; gap: 12px;
background: var(--panel); border: 1px solid var(--line-soft);
padding: 12px 14px; border-radius: 7px;
box-shadow: 0 8px 24px -12px rgba(0,0,0,0.4);
max-width: 420px;
}
.toast-ok { border-left: 3px solid var(--ok); }
.toast-bad { border-left: 3px solid var(--bad); }
.toast .x { color: var(--ink-fade); cursor: pointer; padding: 2px 4px; margin-left: auto; }
/* ---------- MODAL ---------- */
.modal-backdrop {
position: relative; background: rgba(0,0,0,0.45);
border: 1px dashed var(--line-soft); border-radius: 7px;
padding: 48px;
}
.modal {
background: var(--panel); border: 1px solid var(--line-soft);
border-radius: 8px; max-width: 480px; margin: 0 auto;
box-shadow: 0 24px 48px -16px rgba(0,0,0,0.6);
}
.modal-head { padding: 18px 22px; border-bottom: 1px solid var(--line-soft); }
.modal-body { padding: 18px 22px; font-size: 13px; line-height: 1.65; color: var(--ink-mid); text-wrap: pretty; }
.modal-foot { padding: 14px 22px; border-top: 1px solid var(--line-soft); display: flex; gap: 8px; justify-content: flex-end; }
/* ---------- INSTALL SNIPPET ---------- */
.snippet { border: 1px solid var(--line-soft); border-radius: 6px; overflow: hidden; }
.snippet-head { display: flex; justify-content: space-between; align-items: center; padding: 10px 14px; border-bottom: 1px solid var(--line-soft); font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.1em; }
.snippet pre {
margin: 0; padding: 14px; font-family: 'JetBrains Mono', monospace;
font-size: 12px; color: var(--ink-mid); line-height: 1.7;
white-space: pre-wrap; word-break: break-all;
}
.snippet pre .var { color: var(--accent); }
/* ---------- DOCUMENT SHELL FOR THIS REFERENCE PAGE ---------- */
.philosophy { padding: 56px 0 32px; border-bottom: 1px solid var(--line-soft); }
.philosophy h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; }
.philosophy p { color: var(--ink-mid); max-width: 680px; margin-top: 14px; line-height: 1.65; text-wrap: pretty; }
.philosophy .meta { color: var(--ink-fade); font-size: 12px; margin-top: 14px; }
.section { padding: 48px 0 24px; border-bottom: 1px solid var(--line-soft); }
.section h2 {
font-size: 11px; font-weight: 600; color: var(--ink-fade);
text-transform: uppercase; letter-spacing: 0.16em;
margin-bottom: 20px;
}
.section h2 .num { color: var(--ink); margin-right: 8px; }
.section h2 .name { color: var(--ink); letter-spacing: 0.04em; font-weight: 600; }
.section h2 .desc { color: var(--ink-fade); font-weight: 400; margin-left: 12px; }
.swatch-row { display: grid; grid-template-columns: repeat(8, 1fr); gap: 12px; }
.swatch { background: var(--panel); border: 1px solid var(--line-soft); border-radius: 6px; overflow: hidden; }
.swatch .chip { height: 56px; }
.swatch .meta { padding: 8px 10px; }
.swatch .name { font-size: 12px; color: var(--ink); font-weight: 500; }
.swatch .var-name { font-size: 11px; color: var(--ink-fade); font-family: 'JetBrains Mono', monospace; margin-top: 2px; }
</style>
</head>
<body>
<div class="doc">
<!-- Philosophy preamble -->
<header class="philosophy">
<div class="text-xs uppercase tracking-[0.18em] text-[color:var(--ink-fade)] mb-3">v1 · Component reference</div>
<h1>The system, in one place.</h1>
<p>
Every reusable piece across the v1 mockups, lifted out and shown beside its
states. If something appears in a screen but not here, it shouldnt — its
either drift or a candidate for promotion. The Go templates (P1-23
onwards) lean on this file: a partial gets a name in here before it gets
a <code class="mono" style="color: var(--ink); background: var(--panel); padding: 1px 6px; border-radius: 3px;">{{define}}</code> in the templates.
</p>
<p class="meta">
Every CSS class on this page is real and copy-able into the Tailwind
build. Anything inline is a one-off and shouldnt be.
</p>
</header>
<!-- ============================================================
1 · TOKENS
============================================================ -->
<section class="section">
<h2><span class="num">01</span><span class="name">Tokens</span><span class="desc">colours · type · spacing · the alphabet of v1</span></h2>
<!-- surface + ink + line -->
<div style="margin-bottom: 28px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px;">Surface, line, ink</div>
<div class="swatch-row">
<div class="swatch"><div class="chip" style="background: var(--bg);"></div><div class="meta"><div class="name">bg</div><div class="var-name">--bg</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--panel);"></div><div class="meta"><div class="name">panel</div><div class="var-name">--panel</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--panel-hi);"></div><div class="meta"><div class="name">panel-hi</div><div class="var-name">--panel-hi</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--line);"></div><div class="meta"><div class="name">line</div><div class="var-name">--line</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink);"></div><div class="meta"><div class="name">ink</div><div class="var-name">--ink</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-mid);"></div><div class="meta"><div class="name">ink-mid</div><div class="var-name">--ink-mid</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-mute);"></div><div class="meta"><div class="name">ink-mute</div><div class="var-name">--ink-mute</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--ink-fade);"></div><div class="meta"><div class="name">ink-fade</div><div class="var-name">--ink-fade</div></div></div>
</div>
</div>
<!-- state + accent -->
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 12px;">State + accent</div>
<div class="swatch-row" style="grid-template-columns: repeat(5, 1fr);">
<div class="swatch"><div class="chip" style="background: var(--ok);"></div><div class="meta"><div class="name">ok</div><div class="var-name">--ok · success / online</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--warn);"></div><div class="meta"><div class="name">warn</div><div class="var-name">--warn · degraded / cache-warning</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--bad);"></div><div class="meta"><div class="name">bad</div><div class="var-name">--bad · failed / alert</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--off);"></div><div class="meta"><div class="name">off</div><div class="var-name">--off · offline (neutral)</div></div></div>
<div class="swatch"><div class="chip" style="background: var(--accent);"></div><div class="meta"><div class="name">accent</div><div class="var-name">--accent · running, links</div></div></div>
</div>
</div>
<!-- type scale -->
<div style="margin-top: 32px;">
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 16px;">Type scale</div>
<div class="panel" style="border-radius: 7px; padding: 16px 20px;">
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">28 / 500</span>
<span style="font-size: 28px; font-weight: 500; letter-spacing: -0.018em;">Hero / numeric stat</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">stat tile values</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">22 / 500</span>
<span style="font-size: 22px; font-weight: 500; letter-spacing: -0.012em;">Page title</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">h1 in body</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">18 / 500</span>
<span style="font-size: 18px; font-weight: 500;">Section header</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">h2 above panels</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">13 / 400</span>
<span style="font-size: 13px;">Body & table cells</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">default for prose</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line-soft);">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">12 / 400</span>
<span style="font-size: 12px; color: var(--ink-mute);">Helper, captions, secondary detail</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">.field-help, meta lines</span>
</div>
<div style="display: grid; grid-template-columns: 70px 1fr 200px; align-items: baseline; padding: 6px 0;">
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">11 / 600 · 0.08em</span>
<span style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Section heading · column header</span>
<span class="mono" style="font-size: 11px; color: var(--ink-fade);">all caps tracking</span>
</div>
</div>
</div>
</section>
<!-- ============================================================
2 · STATUS
============================================================ -->
<section class="section">
<h2><span class="num">02</span><span class="name">Status</span><span class="desc">five states · only place colour is used as semantic</span></h2>
<div class="panel" style="border-radius: 7px;">
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-online"></span></span>
<span style="font-weight: 500;">online</span>
<span style="color: var(--ink-mute); font-size: 12px;">heartbeat received within last 90 seconds</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-online</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-online pulse"></span></span>
<span style="font-weight: 500;">online · running</span>
<span style="color: var(--ink-mute); font-size: 12px;">a job is in flight on this host — pulse only when active</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-online.pulse</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-degraded"></span></span>
<span style="font-weight: 500;">degraded</span>
<span style="color: var(--ink-mute); font-size: 12px;">online, but open alerts &gt; 0 — soft amber, not loud</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-degraded</span>
</div>
<div class="hairline" style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-offline"></span></span>
<span style="font-weight: 500;">offline</span>
<span style="color: var(--ink-mute); font-size: 12px;">no heartbeat for &gt; 90 seconds — neutral, not alarming</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-offline</span>
</div>
<div style="display: grid; grid-template-columns: 60px 200px 1fr 240px; padding: 14px 18px; align-items: center; font-size: 13px;">
<span><span class="dot dot-failed"></span></span>
<span style="font-weight: 500;">last job failed</span>
<span style="color: var(--ink-mute); font-size: 12px;">distinct from offline — host is up, but its last job did not succeed</span>
<span class="mono" style="color: var(--ink-fade); font-size: 11px;">.dot.dot-failed</span>
</div>
</div>
</section>
<!-- ============================================================
3 · BUTTONS
============================================================ -->
<section class="section">
<h2><span class="num">03</span><span class="name">Buttons</span><span class="desc">one primary per page · everything else is the neutral secondary</span></h2>
<div class="panel" style="border-radius: 7px; padding: 24px;">
<div class="flex flex-wrap items-center gap-3" style="margin-bottom: 24px;">
<button class="btn btn-primary btn-lg">+ Add host</button>
<button class="btn btn-primary">Run backup now</button>
<button class="btn">Run now</button>
<button class="btn btn-ghost">View →</button>
<button class="btn btn-danger">Cancel job</button>
<button class="btn" disabled>Disabled</button>
</div>
<div class="grid grid-cols-3 gap-6" style="font-size: 12px; color: var(--ink-mute); line-height: 1.55;">
<div><span style="color: var(--ink); font-weight: 500;">Primary</span> · the one verb the page is about. Use <span style="color: var(--ink);">at most once per page</span>.</div>
<div><span style="color: var(--ink); font-weight: 500;">Secondary</span> · everything else. Run-now per row, all panel actions.</div>
<div><span style="color: var(--ink); font-weight: 500;">Ghost</span> · inline / “View →” affordances inside cards and rows.</div>
<div><span style="color: var(--ink); font-weight: 500;">Danger</span> · destructive verbs only. Always pair with a confirmation modal for irreversible ones.</div>
<div><span style="color: var(--ink); font-weight: 500;">Disabled</span> · 0.4 opacity, not-allowed cursor, no hover.</div>
<div><span style="color: var(--ink); font-weight: 500;">Sizes</span> · default 12px. <span class="mono" style="color: var(--ink);">.btn-lg</span> for the page-level primary (slightly bigger affordance).</div>
</div>
</div>
</section>
<!-- ============================================================
4 · FORM FIELDS
============================================================ -->
<section class="section">
<h2><span class="num">04</span><span class="name">Form fields</span><span class="desc">labels above · helper below · 0.005em letter-spacing</span></h2>
<div class="grid grid-cols-2 gap-6">
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 18px;">
<label class="field-label">Default text</label>
<input class="field" type="text" value="prod-redis-01">
<div class="field-help">Helper text. Plain prose.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">Monospace text</label>
<input class="field mono" type="text" value="rest:https://restic.unraid.lab/host/">
<div class="field-help">Use <span class="mono" style="color: var(--ink);">.field.mono</span> for URLs, IDs, anything machine-shaped.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">With prefix</label>
<div style="position: relative;">
<input class="field mono with-prefix" type="text" value="https://restic.unraid.lab/host/">
<span class="field-prefix">repo:</span>
</div>
</div>
<div>
<label class="field-label">Password</label>
<input class="field" type="password" value="••••••••••••••••">
</div>
</div>
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 18px;">
<label class="field-label">Focus state</label>
<input class="field" type="text" value="prod-redis-01" style="border-color: var(--accent);">
<div class="field-help">Border becomes <span style="color: var(--accent);">accent</span> on focus.</div>
</div>
<div style="margin-bottom: 18px;">
<label class="field-label">Invalid</label>
<input class="field invalid" type="text" value="not a url">
<div class="field-error">repo_url must look like <span class="mono">rest:</span> / <span class="mono">s3:</span> / <span class="mono">b2:</span></div>
</div>
<div>
<label class="field-label">Tag chip input</label>
<div class="flex items-center gap-1.5 flex-wrap" style="padding: 8px; background: var(--bg); border: 1px solid var(--line-soft); border-radius: 5px;">
<span class="tag tag-removable">prod <span class="x">×</span></span>
<span class="tag tag-removable">cache <span class="x">×</span></span>
<input class="mono" placeholder="add tag…" style="flex: 1; min-width: 80px; background: transparent; border: none; color: var(--ink); font-size: 12px; padding: 4px 6px; outline: none;">
</div>
<div class="field-help">Free-form tags. Comma or Enter to commit.</div>
</div>
</div>
</div>
</section>
<!-- ============================================================
5 · TAGS / CHIPS
============================================================ -->
<section class="section">
<h2><span class="num">05</span><span class="name">Tags &amp; chips</span><span class="desc">labels for hosts · status pills · removable in form</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px;">
<div class="flex items-center gap-2 flex-wrap" style="margin-bottom: 16px;">
<span class="tag">prod</span>
<span class="tag">db</span>
<span class="tag">homelab</span>
<span class="tag">storage</span>
<span class="tag">edge</span>
<span class="tag">test</span>
<span class="tag tag-removable">prod <span class="x">×</span></span>
</div>
<div class="flex items-center gap-2 flex-wrap">
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--ok), transparent 88%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">succeeded</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--accent), transparent 88%); color: var(--accent); border: 1px solid color-mix(in oklch, var(--accent), transparent 70%); border-radius: 3px;">running</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--bad), transparent 88%); color: var(--bad); border: 1px solid color-mix(in oklch, var(--bad), transparent 70%); border-radius: 3px;">failed</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--warn), transparent 88%); color: var(--warn); border: 1px solid color-mix(in oklch, var(--warn), transparent 70%); border-radius: 3px;">cancelled</span>
<span class="mono" style="font-size: 11px; padding: 3px 8px; background: color-mix(in oklch, var(--ok), transparent 90%); color: var(--ok); border: 1px solid color-mix(in oklch, var(--ok), transparent 70%); border-radius: 3px;">expires in 59m</span>
</div>
</div>
</section>
<!-- ============================================================
6 · TABS
============================================================ -->
<section class="section">
<h2><span class="num">06</span><span class="name">Tabs</span><span class="desc">primary nav (accent underline) · sub-nav (ink underline)</span></h2>
<div class="panel" style="border-radius: 7px; padding: 0 24px;">
<nav class="flex items-end" style="border-bottom: 1px solid var(--line-soft);">
<div class="nav-tab active">Dashboard</div>
<div class="nav-tab">Repos</div>
<div class="nav-tab">Alerts <span class="mono ml-1.5" style="font-size:11px; color: var(--bad);">5</span></div>
<div class="nav-tab">Audit</div>
<div class="nav-tab">Settings</div>
</nav>
</div>
<div class="panel" style="border-radius: 7px; padding: 0 24px; margin-top: 16px;">
<nav class="flex items-end">
<div class="sub-tab active">Snapshots <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">1,847</span></div>
<div class="sub-tab">Jobs <span class="mono" style="color: var(--ink-fade); font-size: 11px; margin-left: 4px;">47 in 24h</span></div>
<div class="sub-tab">Repo</div>
<div class="sub-tab">Settings</div>
</nav>
</div>
</section>
<!-- ============================================================
7 · HOST ROW (3 states)
============================================================ -->
<section class="section">
<h2><span class="num">07</span><span class="name">Host row</span><span class="desc">the dashboard's load-bearing row · degraded/failed/offline get a left edge</span></h2>
<div class="panel" style="border-radius: 7px;">
<div class="host-row head hairline">
<div></div>
<div>Host</div>
<div>OS · arch</div>
<div>Last backup</div>
<div class="text-right">Repo size</div>
<div class="text-right">Snapshots</div>
<div class="text-right">Alerts</div>
<div>Tags</div>
<div></div>
</div>
<div class="host-row hairline">
<div><span class="dot dot-online"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">healthy-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--ok);">succeeded</span> · <span class="mono">5m ago</span></div>
<div class="text-right mono" style="color: var(--ink);">87 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">2,103</div>
<div class="text-right mono" style="color: var(--ink-mute);"></div>
<div class="flex gap-1.5"><span class="tag">prod</span></div>
<div class="text-right"><button class="btn">Run now</button></div>
</div>
<div class="host-row degraded hairline">
<div><span class="dot dot-degraded"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">degraded-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--ok);">succeeded</span> · <span class="mono">1h ago</span></div>
<div class="text-right mono" style="color: var(--ink);">128 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">1,402</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">3</div>
<div class="flex gap-1.5"><span class="tag">prod</span></div>
<div class="text-right"><button class="btn">Run now</button></div>
</div>
<div class="host-row failed hairline">
<div><span class="dot dot-online"></span></div>
<div class="mono" style="color: var(--ink); font-weight: 500;">failed-host</div>
<div class="mono" style="color: var(--ink-mid); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mid);"><span style="color: var(--bad); font-weight:500;">failed</span> · <span class="mono">47m ago</span></div>
<div class="text-right mono" style="color: var(--ink);">97 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">2,847</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">1</div>
<div class="flex gap-1.5"><span class="tag">ci</span></div>
<div class="text-right"><button class="btn">Retry</button></div>
</div>
<div class="host-row offline">
<div><span class="dot dot-offline"></span></div>
<div class="mono" style="color: var(--ink-mid); font-weight: 500;">offline-host</div>
<div class="mono" style="color: var(--ink-mute); font-size:12px;">linux/amd64</div>
<div class="text-xs" style="color: var(--ink-mute);">last seen <span class="mono">2d ago</span></div>
<div class="text-right mono" style="color: var(--ink-mid);">64 <span style="color: var(--ink-mute); font-size: 11px;">GB</span></div>
<div class="text-right mono" style="color: var(--ink-mute);">127</div>
<div class="text-right mono" style="color: var(--bad); font-weight: 500;">1</div>
<div class="flex gap-1.5"><span class="tag">dev</span></div>
<div class="text-right"><span class="mono text-xs" style="color: var(--ink-fade);">offline</span></div>
</div>
</div>
</section>
<!-- ============================================================
8 · LOG VIEWER
============================================================ -->
<section class="section">
<h2><span class="num">08</span><span class="name">Log viewer</span><span class="desc">live or replayed · ts · stream · payload · color reserved for events &amp; stderr</span></h2>
<div class="log">
<div class="log-line"><span class="log-ts">11:43:21.039</span><span class="log-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.000,"total_files":21402,"files_done":0}</span></div>
<div class="log-line"><span class="log-ts">11:43:21.412</span><span class="log-tag">OUT</span><span class="log-stream-stdout">scan finished in 0.371s</span></div>
<div class="log-line"><span class="log-ts">11:43:25.812</span><span class="log-tag">ERR</span><span class="log-stream-stderr">warn: file changed during read: /var/lib/postgresql/13/main/pg_wal/000000010000007800000042</span></div>
<div class="log-line"><span class="log-ts">11:43:31.625</span><span class="log-tag">EVENT</span><span class="log-stream-event">{"message_type":"status","percent_done":0.380,"files_done":8124,"bytes_done":1503870976}</span></div>
<div class="log-line"><span class="log-ts" style="color: var(--accent);">11:43:32.122</span><span class="log-tag" style="color: var(--accent);">···</span><span style="color: var(--accent);"></span></div>
</div>
</section>
<!-- ============================================================
9 · PROGRESS BAR
============================================================ -->
<section class="section">
<h2><span class="num">09</span><span class="name">Progress bar</span><span class="desc">running · complete · failed</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px 24px;">
<div style="margin-bottom: 22px;">
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--ink); font-weight:500;">running · 38%</span><span class="mono" style="color: var(--ink-mute);">42 MB/s · ETA 2m 14s</span></div>
<div class="progress-track"><div class="progress-fill" style="width: 38%;"></div></div>
</div>
<div style="margin-bottom: 22px;">
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--ok);">complete · 100%</span><span class="mono" style="color: var(--ink-mute);">finished in 5m 21s</span></div>
<div class="progress-track"><div class="progress-fill ok" style="width: 100%;"></div></div>
</div>
<div>
<div class="flex items-center justify-between mb-2 text-xs"><span style="color: var(--bad);">failed · 0%</span><span class="mono" style="color: var(--ink-mute);">repo locked</span></div>
<div class="progress-track"><div class="progress-fill bad" style="width: 4%;"></div></div>
</div>
</div>
</section>
<!-- ============================================================
10 · STAT TILE
============================================================ -->
<section class="section">
<h2><span class="num">10</span><span class="name">Stat tile</span><span class="desc">used in summary strips · fleet, host header, job summary</span></h2>
<div class="panel" style="border-radius: 7px; padding: 22px;">
<div class="grid grid-cols-4 gap-6">
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Hosts</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">12 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">total</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono" style="color: var(--ok);">10</span> online · <span class="mono" style="color: var(--warn);">1</span> degraded</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Backed up</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">4.9 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">TB</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono">23,649</span> snapshots</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Last 24h</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px;">147 <span style="font-size: 13px; color: var(--ink-mute); font-weight: 400;">jobs</span></div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;"><span class="mono" style="color: var(--ok);">144</span> ok · <span class="mono" style="color: var(--bad);">2</span> failed</div>
</div>
<div>
<div style="font-size: 11px; color: var(--ink-fade); text-transform: uppercase; letter-spacing: 0.08em;">Open alerts</div>
<div class="mono" style="font-size: 28px; font-weight: 500; letter-spacing: -0.02em; margin-top: 4px; color: var(--bad);">5</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 4px;">oldest <span class="mono" style="color: var(--ink-mid);">3h</span></div>
</div>
</div>
</div>
</section>
<!-- ============================================================
11 · MODAL
============================================================ -->
<section class="section">
<h2><span class="num">11</span><span class="name">Modal</span><span class="desc">used for confirms · destructive actions · short prompts</span></h2>
<div class="modal-backdrop">
<div class="modal">
<div class="modal-head">
<h3 style="font-size: 16px; font-weight: 500; letter-spacing: -0.005em;">Remove host?</h3>
</div>
<div class="modal-body">
<p>
Removes <span class="mono" style="color: var(--ink);">prod-cache-01</span> from
the dashboard and revokes its agent token. Backup data on the
rest-server is left intact — delete that yourself if you want it gone.
</p>
<p style="margin-top: 10px; color: var(--ink-mute); font-size: 12px;">This action is logged in the audit trail.</p>
</div>
<div class="modal-foot">
<button class="btn">Cancel</button>
<button class="btn btn-danger">Remove host</button>
</div>
</div>
</div>
</section>
<!-- ============================================================
12 · TOAST
============================================================ -->
<section class="section">
<h2><span class="num">12</span><span class="name">Toast</span><span class="desc">success and error variants · auto-dismiss after 4s</span></h2>
<div style="display: flex; flex-direction: column; gap: 12px; align-items: flex-end;">
<div class="toast toast-ok">
<span class="dot dot-online" style="margin-top: 4px;"></span>
<div style="flex: 1;">
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">Token minted</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 2px;">install command shown below — expires in 1h</div>
</div>
<span class="x">×</span>
</div>
<div class="toast toast-bad">
<span class="dot dot-failed" style="margin-top: 4px;"></span>
<div style="flex: 1;">
<div style="font-size: 13px; color: var(--ink); font-weight: 500;">Couldnt reach the agent</div>
<div style="font-size: 12px; color: var(--ink-mute); margin-top: 2px;"><span class="mono">prod-cache-01</span> didnt respond within 5s. Try again or check the WS connection.</div>
</div>
<span class="x">×</span>
</div>
</div>
</section>
<!-- ============================================================
13 · INSTALL SNIPPET
============================================================ -->
<section class="section">
<h2><span class="num">13</span><span class="name">Install snippet</span><span class="desc">load-bearing affordance on Add host · Empty state · Settings → Agents</span></h2>
<div class="snippet">
<div class="snippet-head">
<span>install command · 59m left</span>
<div class="flex gap-2">
<button class="btn btn-ghost mono" style="font-size: 11px;">Download .sh</button>
<button class="btn">Copy</button>
</div>
</div>
<pre>curl -fsSL <span class="var">https://restic.lab.example/install.sh</span> | sudo \
RM_SERVER=<span class="var">https://restic.lab.example</span> \
RM_TOKEN=<span class="var">HdqFbQh8U-I1fb52iP1M8qxvoYS5t9VZ-T-yghr_CzA</span> sh</pre>
</div>
</section>
<!-- ============================================================
14 · EMPTY STATE PATTERN
============================================================ -->
<section class="section" style="border-bottom: none;">
<h2><span class="num">14</span><span class="name">Empty-state pattern</span><span class="desc">nothing-yet screens · centred prompt + the affordance that fixes it</span></h2>
<div class="panel" style="border-radius: 7px; padding: 60px 40px; text-align: center; background: radial-gradient(ellipse at top, color-mix(in oklch, var(--accent), transparent 95%), transparent 60%), var(--panel);">
<h3 style="font-size: 18px; font-weight: 500; letter-spacing: -0.005em;">Nothing here yet.</h3>
<p style="color: var(--ink-mid); max-width: 480px; margin: 12px auto 22px; line-height: 1.65; font-size: 13px; text-wrap: pretty;">
Empty states always pair a one-sentence explanation with a single
primary affordance. Dont fill the space with stats or graphics —
the void <em>is</em> the message.
</p>
<button class="btn btn-primary">Take the action that fixes this</button>
</div>
</section>
<!-- final note -->
<div style="padding: 48px 0 96px; text-align: center; font-size: 12px; color: var(--ink-fade);">
end of v1 component reference · 14 sections · ~all the pieces the Phase 1 templates need
</div>
</div>
</body>
</html>