Files
restic-manager/design/v1-components.html
T
steve 136e1a1d8f
CI / Test (linux/amd64) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Build (windows/amd64) (push) Has been cancelled
CI / Build (linux/amd64) (push) Has been cancelled
CI / Build (linux/arm64) (push) Has been cancelled
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>
2026-05-01 19:05:39 +01:00

704 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>