design(decisions): mono type signal — icon + label, accent bar removed (1a)

Commit 2/6 of the Quiet Ledger series (Concept A, anatomy 1a).

- AttentionQueueRow: delete the left tone accent bar; source icon now
  renders in text-muted-foreground (inherited from the label span).
  Type identity reads through icon shape + text label alone.
- lib/attention: delete attentionTone/attentionToneStyle/TONE_STYLE —
  repo-wide grep found exactly one consumer (this row), so the five-tone
  sky/violet/rose/amber/neutral map goes rather than lingering unused.
- Tone unit tests deleted with the map they asserted (5 tests); the
  sourceMeta label/icon coverage still pins type identity per kind.
- DECISION-SHEET entry records the delete-vs-scope judgment call.

Verified: token gates 3/3 CLEAN, typecheck green, ui vitest 2532/2532.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
scotttongandPaperclip committed 2026-07-14 12:11:47 -07:00
1 parent b2a9e09dab
commit f833317588
4 files changed
+5 -142

No files matched your search

+4
View File
@@ -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 type signal → mono icon + label, accent bar removed (PAP-315 / PAP-313 anatomy 1a, Jul 14 2026)
The five-tone type→color system (sky/violet/rose/amber/neutral accent bar + tinted source icon) is retired from the Decisions feed: the left accent bar is deleted and the source icon renders in `text-muted-foreground`, so a row's type reads through icon shape + text label alone and color is freed up for severity (Critical `destructive`, per 2a) and CTAs. Judgment call in the same commit: `attentionTone`/`attentionToneStyle`/`TONE_STYLE` (lib/attention.ts) were deleted rather than kept as an unused map — a repo-wide grep found exactly one consumer (AttentionQueueRow itself), so no other surface loses the palette and no debt remains; the tone unit tests went with them (type identity is still covered by the `sourceMeta` label/icon tests).
## 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).
+1 -6
View File
@@ -21,7 +21,6 @@ import {
attentionDetailImages,
attentionDetailLine,
attentionImageUrl,
attentionToneStyle,
isInlineResolvable,
severityBadge,
sourceMeta,
@@ -107,7 +106,6 @@ export const AttentionQueueRow = memo(function AttentionQueueRow({
selected = false,
}: AttentionQueueRowProps) {
const meta = sourceMeta(item.sourceKind);
const tone = attentionToneStyle(item);
const sevBadge = severityBadge(item.severity);
const Icon = meta.icon;
const isHidden = variant === "hidden";
@@ -161,9 +159,6 @@ export const AttentionQueueRow = memo(function AttentionQueueRow({
data-attention-source={item.sourceKind}
data-attention-severity={item.severity}
>
{/* Type accent bar (canonical color map — never severity). */}
<span className={cn("absolute inset-y-0 left-0 w-1", tone.accent)} aria-hidden />
<div className="flex items-start gap-2 py-3 pl-4 pr-3">
{/* Expand affordance / spacer gutter — keeps headlines aligned across the list. */}
{expandable ? (
@@ -188,7 +183,7 @@ export const AttentionQueueRow = memo(function AttentionQueueRow({
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
<span className="inline-flex items-center gap-1 text-xs font-medium text-muted-foreground">
<Icon className={cn("h-3.5 w-3.5", tone.icon)} />
<Icon className="h-3.5 w-3.5" />
{meta.label}
</span>
{sevBadge && (
-53
View File
@@ -6,8 +6,6 @@ import {
attentionBadgeCount,
attentionDateBucket,
attentionDetailLine,
attentionTone,
attentionToneStyle,
buildAttentionFilterOptions,
countActiveAttentionFilters,
defaultAttentionFilterState,
@@ -131,57 +129,6 @@ describe("sourceMeta", () => {
});
describe("attentionTone + attentionToneStyle (canonical color map §4)", () => {
it("colors plan approvals violet regardless of source kind", () => {
const fromApproval = buildItem({
sourceKind: "approval",
detail: { kind: "plan_approval", issueTitle: "I", planTitle: "P", summaryExcerpt: null, images: [] },
});
const fromInteraction = buildItem({
sourceKind: "issue_thread_interaction",
detail: { kind: "plan_approval", issueTitle: "I", planTitle: "P", summaryExcerpt: null, images: [] },
});
expect(attentionTone(fromApproval)).toBe("violet");
expect(attentionTone(fromInteraction)).toBe("violet");
expect(attentionToneStyle(fromApproval).accent).toContain("violet");
});
it("colors confirmations / questions / verdicts in the sky family", () => {
expect(attentionTone(buildItem({ sourceKind: "approval" }))).toBe("sky");
expect(attentionTone(buildItem({ sourceKind: "issue_thread_interaction" }))).toBe("sky");
expect(
attentionTone(
buildItem({
sourceKind: "issue_thread_interaction",
detail: { kind: "questions", questionCount: 2, firstQuestionText: "?", images: [] },
}),
),
).toBe("sky");
});
it("colors failures rose and blocked/recovery/budget amber", () => {
expect(attentionTone(buildItem({ sourceKind: "failed_run" }))).toBe("rose");
expect(attentionTone(buildItem({ sourceKind: "agent_error_alert" }))).toBe("rose");
expect(attentionTone(buildItem({ sourceKind: "blocker_attention" }))).toBe("amber");
expect(attentionTone(buildItem({ sourceKind: "recovery_action" }))).toBe("amber");
expect(attentionTone(buildItem({ sourceKind: "budget_alert" }))).toBe("amber");
});
it("colors join requests neutral", () => {
expect(attentionTone(buildItem({ sourceKind: "join_request" }))).toBe("neutral");
});
it("gives every tone a distinct accent and never keys color off severity", () => {
const rose = buildItem({ sourceKind: "failed_run", severity: "low" });
const amber = buildItem({ sourceKind: "budget_alert", severity: "critical" });
// Same-source rows with opposite severities share one accent (color ≠ severity).
expect(attentionToneStyle(buildItem({ sourceKind: "failed_run", severity: "critical" })).accent).toBe(
attentionToneStyle(rose).accent,
);
expect(attentionToneStyle(rose).accent).not.toBe(attentionToneStyle(amber).accent);
});
});
describe("severityBadge (anatomy 2a — outline badges, red = Critical only)", () => {
it("only surfaces a badge for Critical/High", () => {
expect(severityBadge("critical")?.label).toBe("Critical");
-83
View File
@@ -60,89 +60,6 @@ export function sourceMeta(kind: AttentionSourceKind): SourceMeta {
return SOURCE_META[kind] ?? { label: kind.replaceAll("_", " "), icon: AlertTriangle };
}
// ---------------------------------------------------------------------------
// Canonical type → color map (PAP-13409 §4)
//
// The row color is driven by the *kind of decision*, never by severity — one
// map, sourced from `IssueThreadInteractionCard`'s palette so a plan approval or
// confirmation reads identically in the queue and on the issue thread:
// • confirmations / questions / suggested-tasks / verdicts / reviews → sky
// • plan approvals → violet
// • failures (failed run, agent error) → rose
// • blocked / recovery / budget → amber
// • join request → neutral
// Severity only ever surfaces as a small Critical/High badge (never the accent).
// ---------------------------------------------------------------------------
export type AttentionTone = "sky" | "violet" | "rose" | "amber" | "neutral";
export interface AttentionToneStyle {
/** Left accent bar background. */
accent: string;
/** Source-icon tint. */
icon: string;
/** Chip / badge border+bg+text (matches the interaction card badge palette). */
chip: string;
}
const TONE_STYLE: Record<AttentionTone, AttentionToneStyle> = {
sky: {
accent: "bg-sky-500",
icon: "text-sky-600 dark:text-sky-400",
chip: "border-sky-500/60 bg-sky-500/10 text-sky-900 dark:bg-sky-500/15 dark:text-sky-100",
},
violet: {
accent: "bg-violet-500",
icon: "text-violet-600 dark:text-violet-400",
chip: "border-violet-500/60 bg-violet-500/10 text-violet-900 dark:bg-violet-500/15 dark:text-violet-100",
},
rose: {
accent: "bg-rose-500",
icon: "text-rose-600 dark:text-rose-400",
chip: "border-rose-500/60 bg-rose-500/10 text-rose-900 dark:bg-rose-500/15 dark:text-rose-100",
},
amber: {
accent: "bg-amber-500",
icon: "text-amber-600 dark:text-amber-400",
chip: "border-amber-500/60 bg-amber-500/10 text-amber-900 dark:bg-amber-500/15 dark:text-amber-100",
},
neutral: {
accent: "bg-muted-foreground/40",
icon: "text-muted-foreground",
chip: "border-border/70 bg-muted/50 text-muted-foreground",
},
};
/**
* Resolve the canonical tone for a row. A plan approval is violet regardless of
* which surface tagged it (approval flow *or* issue-thread confirmation), so we
* check the T1 detail discriminant first, then fall back to the source kind.
*/
export function attentionTone(item: AttentionItem): AttentionTone {
if (item.detail?.kind === "plan_approval") return "violet";
switch (item.sourceKind) {
case "failed_run":
case "agent_error_alert":
return "rose";
case "blocker_attention":
case "recovery_action":
case "budget_alert":
return "amber";
case "join_request":
return "neutral";
case "approval":
case "issue_thread_interaction":
case "review":
case "productivity_review":
default:
return "sky";
}
}
export function attentionToneStyle(item: AttentionItem): AttentionToneStyle {
return TONE_STYLE[attentionTone(item)];
}
/**
* Severity is demoted to a small badge — and only when it is genuinely
* escalated (Critical/High). Medium/Low return `null` so most rows carry no