From b2a9e09dabf2e7393796e50d0656faaebe8f5cef Mon Sep 17 00:00:00 2001 From: scotttong Date: Tue, 14 Jul 2026 01:44:25 -0700 Subject: [PATCH] =?UTF-8?q?design(decisions):=20severity=20=E2=86=92=20out?= =?UTF-8?q?line=20badge,=20red=20=3D=20Critical=20only=20(2a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical renders as an outline badge on the destructive tokens (border-destructive/60 text-destructive; AA vs --card: 4.76:1 light, 4.69:1 dark — no dark override needed). High becomes a neutral outline (border-border text-muted-foreground). Medium/Low stay badge-less. Deletes the unconsumed severityStyle/SEVERITY_STYLE map (filled red/orange/yellow/blue accents; zero component call sites) — severity keys a badge, never card color, and the tone-vs-severity invariant test is preserved. Badge tests updated in lockstep: old literal assertions replaced with token-shaped ones (outline = no bg- class, no palette red/ orange). Part of the Quiet Ledger series (commit 1 of 6). Co-Authored-By: Paperclip --- doc/design/DECISION-SHEET.md | 4 ++++ ui/src/lib/attention.test.ts | 25 +++++++++++++++++++------ ui/src/lib/attention.ts | 24 ++++-------------------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/doc/design/DECISION-SHEET.md b/doc/design/DECISION-SHEET.md index 22ea4623ec4..367dc762a43 100644 --- a/doc/design/DECISION-SHEET.md +++ b/doc/design/DECISION-SHEET.md @@ -39,6 +39,10 @@ Every open question from TOKEN-AUDIT.md §8 + batch logs and COMPONENT-INVENTORY | C10 | FeatureGate wrapper pattern (3 near-identical gates) | Nice-to-have shared primitive; backlog, not a run | APPROVED — backlog nice-to-have, not a run | | C11 | Sidebar agents section: hand-rolled rows (own spacing/icon colors, palette-blue liveness dot) instead of `SidebarNavItem`; only section that is collapsible | Run 3 item: unify rows onto `SidebarNavItem`, settle collapsibility policy across sections, liveness dot → canonical status blue. Wholesale shadcn Sidebar adoption REJECTED for now (app already has equivalent machinery incl. icon-rail height trick; highest-regression chrome) — re-evaluate as a dedicated item after Run 3 only if its behaviors (kbd shortcut, persisted state, mobile sheet) are wanted | DONE (Run 3, Jul 7) — agent rows render via `SidebarNavItem` (new additive props: `iconNode`, `active`, `dense`, `trailing`, `liveAccessory`); all live dots (agents + nav rows, rail + expanded) now use `--status-agent-running` (#2563eb, was palette blue-500 #3b82f6 — small hue-darkening delta, per-C11 mandate; "N live" text keeps its palette classes for Run 4). **Collapsibility policy** (superseded Jul 8 by user review): EVERY labeled section (Work, Company, Projects, Agents) is collapsible, default-open, session-scoped state — one affordance everywhere. "See all agents" stays a plain muted Link on purpose (must not adopt active-route highlighting). 48 sidebar vitest assertions pass unchanged. | +## Decisions-page severity badges → outline, red = Critical only (PAP-314 / PAP-313 anatomy 2a, Jul 14 2026) + +Severity on the Decisions feed now renders as an *outline* badge — Critical on the `destructive` tokens (`border-destructive/60 text-destructive`; manual AA check: 4.76:1 light / 4.69:1 dark vs `--card`, WCAG 2.1 small-text pass with no dark override), High as a neutral outline (`border-border text-muted-foreground`), Medium/Low no badge (unchanged). Red is reserved for Critical alone so it reads as an alarm, not decoration. Judgment call in the same commit: the exported `severityStyle`/`SEVERITY_STYLE` map (filled red/orange/yellow/blue accents + dots) was deleted rather than retokenized — it had zero component consumers (only its own unit test) and its filled-accent model is exactly what the plan retires; severity keys a badge, never card color (invariant test preserved). + ## Gallery feedback round 1 (preset-tune session, Jul 6) — executed User rulings from the tune-session gallery review; all intentionally visible, snapshots NOT re-baselined (fresh before/after triplets regenerated in tests/storybook-visual/test-results/ against the old baseline): diff --git a/ui/src/lib/attention.test.ts b/ui/src/lib/attention.test.ts index a91b662e25b..9f9eeb650ee 100644 --- a/ui/src/lib/attention.test.ts +++ b/ui/src/lib/attention.test.ts @@ -19,7 +19,6 @@ import { planAttentionRenderRows, saveAttentionGroupBy, severityBadge, - severityStyle, sortAttentionItems, sourceMeta, } from "./attention"; @@ -110,7 +109,7 @@ describe("attentionBadgeCount", () => { }); }); -describe("sourceMeta + severityStyle", () => { +describe("sourceMeta", () => { it("labels every catalog source kind", () => { const kinds: AttentionSourceKind[] = [ "approval", @@ -130,9 +129,6 @@ describe("sourceMeta + severityStyle", () => { } }); - it("maps escalation severity to distinct accents", () => { - expect(severityStyle("critical").accent).not.toBe(severityStyle("low").accent); - }); }); describe("attentionTone + attentionToneStyle (canonical color map §4)", () => { @@ -186,13 +182,30 @@ describe("attentionTone + attentionToneStyle (canonical color map §4)", () => { }); }); -describe("severityBadge", () => { +describe("severityBadge (anatomy 2a — outline badges, red = Critical only)", () => { it("only surfaces a badge for Critical/High", () => { expect(severityBadge("critical")?.label).toBe("Critical"); expect(severityBadge("high")?.label).toBe("High"); expect(severityBadge("medium")).toBeNull(); expect(severityBadge("low")).toBeNull(); }); + + it("draws Critical as an outline on the destructive tokens — the page's only red", () => { + const badge = severityBadge("critical"); + expect(badge?.className).toContain("border-destructive"); + expect(badge?.className).toContain("text-destructive"); + // Outline, not filled: no background class of any kind. + expect(badge?.className).not.toMatch(/\bbg-/); + expect(badge?.className).not.toMatch(/red|orange/); + }); + + it("draws High as a neutral outline (no palette color)", () => { + const badge = severityBadge("high"); + expect(badge?.className).toContain("border-border"); + expect(badge?.className).toContain("text-muted-foreground"); + expect(badge?.className).not.toMatch(/\bbg-/); + expect(badge?.className).not.toMatch(/red|orange|destructive/); + }); }); describe("attentionDetailLine (§7)", () => { diff --git a/ui/src/lib/attention.ts b/ui/src/lib/attention.ts index f99665fe066..55513ef6e30 100644 --- a/ui/src/lib/attention.ts +++ b/ui/src/lib/attention.ts @@ -60,24 +60,6 @@ export function sourceMeta(kind: AttentionSourceKind): SourceMeta { return SOURCE_META[kind] ?? { label: kind.replaceAll("_", " "), icon: AlertTriangle }; } -interface SeverityStyle { - /** Left accent bar + dot color. */ - accent: string; - dot: string; - label: string; -} - -const SEVERITY_STYLE: Record = { - critical: { accent: "bg-red-500", dot: "bg-red-500", label: "Critical" }, - high: { accent: "bg-orange-500", dot: "bg-orange-500", label: "High" }, - medium: { accent: "bg-yellow-500", dot: "bg-yellow-500", label: "Medium" }, - low: { accent: "bg-blue-500", dot: "bg-blue-500", label: "Low" }, -}; - -export function severityStyle(severity: AttentionSeverity): SeverityStyle { - return SEVERITY_STYLE[severity] ?? SEVERITY_STYLE.low; -} - // --------------------------------------------------------------------------- // Canonical type → color map (PAP-13409 §4) // @@ -168,10 +150,12 @@ export function attentionToneStyle(item: AttentionItem): AttentionToneStyle { */ export function severityBadge(severity: AttentionSeverity): { label: string; className: string } | null { if (severity === "critical") { - return { label: "Critical", className: "border-red-500/60 bg-red-500/10 text-red-700 dark:text-red-300" }; + // Red is reserved for Critical, and only as an outline — no fill. AA on + // --card: 4.76:1 light, 4.69:1 dark (--destructive vs --card, WCAG 2.1). + return { label: "Critical", className: "border-destructive/60 text-destructive" }; } if (severity === "high") { - return { label: "High", className: "border-orange-500/60 bg-orange-500/10 text-orange-700 dark:text-orange-300" }; + return { label: "High", className: "border-border text-muted-foreground" }; } return null; }