diff --git a/ui/src/components/StatusGlyph.test.tsx b/ui/src/components/StatusGlyph.test.tsx index da76ccf542..a7ff3fbbcd 100644 --- a/ui/src/components/StatusGlyph.test.tsx +++ b/ui/src/components/StatusGlyph.test.tsx @@ -6,11 +6,24 @@ import { StatusGlyph } from "./StatusGlyph"; import { taskStatusIconVar } from "../lib/status-colors"; /** - * PAP-238 3b: the unified status glyph renders every status from ONE - * `viewBox="0 0 24 24"` SVG at a `sm 14 / md 16 / lg 20` scale, coloured from - * the AA-tuned `--status-task-icon-*` vars. These tests lock the geometry (the - * rev-4 spec hexes/paths), the size scale, the colour wiring and `in_queue`. + * The unified status glyph renders every status from ONE Lucide icon per status + * (all on a shared `viewBox="0 0 24 24"`, at a `sm 14 / md 16 / lg 20` scale), + * coloured from the AA-tuned `--status-task-icon-*` vars. These tests lock the + * icon mapping, the size scale, the colour wiring and `in_queue`. */ + +/** Status → the Lucide class token its icon renders (`lucide-`). */ +const STATUS_ICON_CLASS: Record = { + backlog: "lucide-circle-dashed", + todo: "lucide-circle", + in_progress: "lucide-rotate-cw", + in_review: "lucide-circle-dot", + done: "lucide-circle-check", + blocked: "lucide-circle-minus", + cancelled: "lucide-ban", + in_queue: "lucide-circle-minus", +}; + describe("StatusGlyph", () => { it("renders one 24-unit viewBox for every status (proportional scaling)", () => { for (const status of Object.keys(taskStatusIconVar)) { @@ -35,59 +48,33 @@ describe("StatusGlyph", () => { } }); - it("falls back to the backlog icon var for unknown statuses", () => { + it("falls back to the backlog icon + var for unknown statuses", () => { const html = renderToStaticMarkup(); expect(html).toContain("var(--status-task-icon-backlog)"); + expect(html).toContain("lucide-circle-dashed"); }); - it("gives backlog a uniform dashed ring (pathLength=100)", () => { - const html = renderToStaticMarkup(); - expect(html).toContain('pathLength="100"'); - expect(html).toContain('stroke-dasharray="6.25 6.25"'); + it("maps each status to its Lucide icon", () => { + for (const [status, iconClass] of Object.entries(STATUS_ICON_CLASS)) { + const html = renderToStaticMarkup(); + expect(html).toContain(iconClass); + } }); - it("gives todo a bare open ring (no inner shape)", () => { + it("gives todo the plain circle (not a compound circle icon)", () => { const html = renderToStaticMarkup(); - expect(html).toContain('r="8.5"'); - expect(html).not.toContain(" { - const html = renderToStaticMarkup(); - expect(html).toContain("M12 3.5 A8.5 8.5 0 0 1 12 20.5 Z"); - }); - - it("gives in_review a ring + centre dot", () => { - const html = renderToStaticMarkup(); - expect(html).toContain('r="3.6"'); - }); - - it("gives done a filled disc with a knocked-out check in the surface colour", () => { - const html = renderToStaticMarkup(); - expect(html).toContain('r="9.5"'); - expect(html).toContain("M7.5 12.2 10.6 15.2 16.5 8.8"); - expect(html).toContain("stroke-background"); - }); - - it("gives blocked a ring + bar", () => { - const html = renderToStaticMarkup(); - expect(html).toContain(" { - const html = renderToStaticMarkup(); - expect(html).toContain("M6.5 17.5 17.5 6.5"); - }); - - it("renders in_queue as the blocked shape recoloured blue (in_progress var)", () => { + it("renders in_queue as the blocked icon recoloured blue (in_queue var)", () => { const queue = renderToStaticMarkup(); const blocked = renderToStaticMarkup(); - // Same geometry as blocked (ring + bar)… - expect(queue).toContain(" = { sm: 14, md: 16, lg: 20 }; -/** Proportional stroke for the open-ring family (24-unit viewBox). */ -const SW = 2.4; - export type StatusGlyphStatus = | "backlog" | "todo" @@ -39,6 +44,21 @@ export type StatusGlyphStatus = | "cancelled" | "in_queue"; +/** Status → Lucide icon. `in_queue` borrows the blocked icon; its colour var resolves to blue. */ +const STATUS_ICON: Record = { + backlog: CircleDashed, + todo: Circle, + in_progress: RotateCw, + in_review: CircleDot, + done: CircleCheck, + blocked: CircleMinus, + cancelled: Ban, + in_queue: CircleMinus, +}; + +/** Unknown statuses fall back to the backlog icon (matches the colour-var fallback). */ +const STATUS_ICON_DEFAULT = CircleDashed; + interface StatusGlyphProps { status: string; /** sm 14 / md 16 / lg 20. Default `md`. */ @@ -48,92 +68,21 @@ interface StatusGlyphProps { title?: string; } -/** Inner geometry per status (viewBox 0 0 24 24). `in_queue` reuses `blocked`. */ -function glyphBody(status: string) { - // in_queue borrows the blocked shape; its colour var resolves to the blue. - const shape = status === "in_queue" ? "blocked" : status; - switch (shape) { - case "todo": - return ; - case "in_progress": - return ( - <> - - - - ); - case "in_review": - return ( - <> - - - - ); - case "done": - return ( - <> - - {/* Check knocked out in the surface colour so the disc reads at any size. */} - - - ); - case "blocked": - return ( - <> - - - - ); - case "cancelled": - return ( - <> - - - - ); - case "backlog": - default: - // pathLength=100 makes the dash pattern resolution-independent: 100/12.5 = - // 8 exact dashes, so the ring is uniform with no overlap at the seam. - return ( - - ); - } -} - export function StatusGlyph({ status, size = "md", className, title }: StatusGlyphProps) { const px = SIZE_PX[size]; + const Icon = STATUS_ICON[status] ?? STATUS_ICON_DEFAULT; const cssVar = taskStatusIconVar[status] ?? taskStatusIconVarDefault; const a11y = title ? ({ role: "img", "aria-label": title } as const) : ({ "aria-hidden": true } as const); return ( - {title ? {title} : null} - {glyphBody(status)} - + ); }