From 629ff79a6a1e10da083aaf7c61ab88a8e2063194 Mon Sep 17 00:00:00 2001 From: Jeff Larson Date: Sun, 28 Jun 2026 21:44:20 -0700 Subject: [PATCH 1/3] docs: dashboard v3 design brief (light theme) + STYLEGUIDE Recover the synthesized design brief onto the build branch, switch the visual register to a LIGHT theme, and author docs/STYLEGUIDE.md as a light-theme token system (tokens + component->token map + the test-enforced accessibility gate). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP --- docs/STYLEGUIDE.md | 108 +++++++++++++++++++++++ docs/dashboard-v3-design.md | 171 ++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 docs/STYLEGUIDE.md create mode 100644 docs/dashboard-v3-design.md diff --git a/docs/STYLEGUIDE.md b/docs/STYLEGUIDE.md new file mode 100644 index 0000000..5f4544e --- /dev/null +++ b/docs/STYLEGUIDE.md @@ -0,0 +1,108 @@ +# Protector dashboard — style guide + +The dashboard is an operator's **situation board**, not a marketing page. This is the source +of truth for its visual system: the design tokens, the component→token map, and the +accessibility gate the tests assert. Pairs with [`dashboard-v3-design.md`](dashboard-v3-design.md) +(the IA/UX). **Light theme:** a clean, high-contrast light surface with dark ink — dense and +calm by default, loud only on a real breach. + +## Principles + +1. **Honesty over reassurance.** Green/calm is shown ONLY while the model is judging; unknown + and awaiting are never green. (Test-enforced — see the gate.) +2. **Posture ≠ severity.** The model's verdict is the loud channel; CVE severity is a cooler, + subordinate channel. A wall of critical CVEs must never look like a wall of breaches. +3. **Meaning never by colour alone.** Every status carries colour **+ glyph + word**. +4. **Density with discipline.** Monospace/tabular for all machine data so columns align; one + restrained type scale and a small palette so density never becomes noise. +5. **Calm by default, loud on breach.** Saturated colour is a scarce resource on the light + surface — spent only on a real breach. + +## Design tokens + +### Colour — surfaces & ink (light) +| Token | Value | Use | +|---|---|---| +| `--bg` | `#FCFCFD` | page background | +| `--surface` | `#FFFFFF` | cards, table | +| `--surface-raised` | `#F4F6F8` | expanded row, header strip | +| `--surface-hover` | `#F0F2F5` | row hover | +| `--border` | `#E2E6EB` | hairlines, grid | +| `--ink-1` | `#1A1F26` | primary text (≈15:1 on `--surface`) | +| `--ink-2` | `#5A6473` | secondary text (≈6:1) | +| `--ink-3` | `#6B7280` | tertiary / muted (≈4.6:1) | + +### Colour — posture (the model's `Verdict`; the loud channel) +| Token | Value | Glyph | Word | Rail | +|---|---|---|---|---| +| `--posture-breach` | `#D92D20` | ● filled | BREACH / EXPLOITABLE | 3px solid | +| `--posture-cleared` | `#067647` | ○ open | not exploitable | 2px solid | +| `--posture-uncertain` | `#B54708` | ◐ half | uncertain | 2px **dashed** | +| `--posture-awaiting` | `#667085` | ◌ dotted | awaiting judgement | 2px **dotted** | + +Uncertain & awaiting are amber-brown / slate — **never** the cleared green. Dashed/dotted +rails make "not decided" texturally distinct even in greyscale. + +### Colour — severity (CVE/`ScanFinding`; subordinate, muted) +| Token | Value | Cue | +|---|---|---| +| `--sev-critical` | `#B42318` | ▲▲ `CRIT` | +| `--sev-high` | `#B54708` | ▲ `HIGH` | +| `--sev-medium` | `#854A0E` | ◆ `MED` | +| `--sev-low` | `#6B7280` | · `LOW` | +| `--kev` | `#D92D20` (filled badge) | `KEV` — the one severity-side signal allowed to be loud | + +### Colour — recency Δ, coverage, mode +| Token | Value | | Token | Value | +|---|---|---|---|---| +| `--delta-new` | `#1570EF` | | `--cov-present` | `#067647` | +| `--delta-up` | `#D92D20` | | `--cov-degraded` | `#B54708` | +| `--delta-down` | `#067647` | | `--cov-absent` | `#6B7280` | +| `--delta-restored` | `#6B7280` | | `--mode-shadow` | `#667085` | +| | | | `--mode-enforce` | `#1570EF` | + +### Space (4px base) +`--space-1:4 · --space-2:8 · --space-3:12 · --space-4:16 · --space-6:24 · --space-8:32` + +### Type +``` +--font-ui: Inter, system-ui, sans-serif /* chrome, prose, labels */ +--font-data: "JetBrains Mono", ui-monospace, monospace /* ALL data: keys, CVEs, paths, counts */ +--text-display: 22/28 600 --text-h2: 16/24 600 --text-body: 14/20 400 +--text-data: 13/18 400 --text-data-strong: 13/18 600 +--text-micro: 11/16 500 /* uppercase, tracked: column headers, chip text */ +``` +Two weights only (400/600). Emphasis via weight + ink value, not a third weight. + +### Geometry +`--row-h:36 · --radius-chip:3 · --radius-panel:6 · --rail-strong:3px · --rail-soft:2px · --focus-ring:2px solid #1570EF` + +## Component → token map +| Component | Tokens | +|---|---| +| Page / app frame | `--bg`, `--font-ui` | +| Status strip | `--surface-raised`; posture-* count chips; honest banner uses `--posture-uncertain`/`--posture-awaiting`, **never** `--posture-cleared` | +| Finding row | `--surface`, `--row-h`, `--border` grid; entry/path `--font-data` `--text-data` | +| Posture cell | `--posture-*` (rail style: solid=decisive, dashed=uncertain, dotted=awaiting) + glyph + word | +| Severity chip | `--sev-*` border (muted); KEV → `--kev` fill | +| Δ cell | `--delta-*` glyph, or `--ink-3` age text for `Unchanged` | +| Detail panel | `--surface-raised`, `--space-4`; verdict prose `--text-body`, posture left-rule | +| CVE table | `--font-data` `--text-micro`, right-aligned numerics | +| Coverage row | `--cov-*` dot + glyph + label; `weakens_decisions` + absent → amber keyline | +| Reversion log | `--posture-cleared` toned (a self-revert is the system working) | +| Empty states | `--ink-2`; posture-coloured only when honestly earned (model judging) | + +## Accessibility gate (test-enforced) +1. **Contrast:** body/status text ≥ **4.5:1** on its surface; chips/rails/glyphs ≥ **3:1**. +2. **Meaning not by colour alone:** every posture / severity / Δ / coverage state renders a + non-empty **glyph + text label** in addition to colour. (Assert each enum→(glyph,label).) +3. **Honest-calm invariant:** `model_judging == false` OR `warming_up` ⇒ the status strip + never resolves to `--posture-cleared`/green and the honest banner renders. `Verdict::Uncertain` + and awaiting (`None`) never map to the cleared/green token. +4. **No implied-absent blanks:** any empty evidence/coverage field renders explicit + "none"/"unknown". +5. **Escaping:** all untrusted free-text (verdict prose, CVE/finding titles, model prompts, + node keys) is HTML-escaped at render (maud auto-escape; no `PreEscaped` outside an audited + allowlist). +6. **Keyboard/semantics:** findings are a real ``; row expanders are `