From a4b1886c356e6524ada014dab638773ab49a4214 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 16 Apr 2026 07:48:47 -0700 Subject: [PATCH] fix(canvas): address all code review findings on PR #482 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reconcile TIER_CONFIG/TIER_COLORS into single TIER_CONFIG with both `color` (pill style) and `border` (bordered badge style) fields - Remove TemplatePalette alias indirection (TIER_LABELS_SHARED → direct import) - Extract inline spinner SVGs to shared Spinner component (3 copies → 1) - Migrate status dot colors from 6 remaining files to shared tokens: SearchDialog, StatusDot, Legend, ContextMenu, Toolbar + add statusDotClass() - Add COMM_TYPE_LABELS to design-tokens, used by CommunicationOverlay sr-only - Update reduced-motion tests: components that delegate to design-tokens pass the guard check via import detection; add design-tokens.ts own test - 507/507 tests pass, build clean Co-Authored-By: Claude Opus 4.6 (1M context) --- canvas/src/__tests__/reduced-motion.test.ts | 24 ++++++++++++++----- .../src/components/CommunicationOverlay.tsx | 3 ++- canvas/src/components/ContextMenu.tsx | 5 ++-- canvas/src/components/EmptyState.tsx | 10 ++++---- canvas/src/components/Legend.tsx | 13 +++++----- canvas/src/components/SearchDialog.tsx | 8 ++----- canvas/src/components/Spinner.tsx | 12 ++++++++++ canvas/src/components/StatusDot.tsx | 13 +++------- canvas/src/components/TemplatePalette.tsx | 15 ++++-------- canvas/src/components/Toolbar.tsx | 13 +++++----- canvas/src/lib/design-tokens.ts | 23 ++++++++++-------- 11 files changed, 75 insertions(+), 64 deletions(-) create mode 100644 canvas/src/components/Spinner.tsx diff --git a/canvas/src/__tests__/reduced-motion.test.ts b/canvas/src/__tests__/reduced-motion.test.ts index 3f7eee05..328eb7df 100644 --- a/canvas/src/__tests__/reduced-motion.test.ts +++ b/canvas/src/__tests__/reduced-motion.test.ts @@ -13,6 +13,12 @@ function readSrc(rel: string) { return readFileSync(join(root, "src", rel), "utf8"); } +function usesGuardedPulse(src: string): boolean { + if (src.includes("motion-safe:animate-pulse")) return true; + if (src.includes("from \"@/lib/design-tokens\"") || src.includes("from '@/lib/design-tokens'")) return true; + return false; +} + describe("prefers-reduced-motion compliance", () => { it("globals.css contains @media (prefers-reduced-motion: reduce) block", () => { const css = readSrc("app/globals.css"); @@ -34,10 +40,10 @@ describe("prefers-reduced-motion compliance", () => { expect(src).toContain("motion-safe:animate-pulse"); }); - it("StatusDot.tsx uses motion-safe:animate-pulse", () => { + it("StatusDot.tsx uses motion-safe:animate-pulse (inline or via shared tokens)", () => { const src = readSrc("components/StatusDot.tsx"); expect(src.includes("animate-pulse") && !src.includes("motion-safe:animate-pulse")).toBe(false); - expect(src).toContain("motion-safe:animate-pulse"); + expect(usesGuardedPulse(src)).toBe(true); }); it("Toolbar.tsx uses motion-safe:animate-pulse", () => { @@ -52,16 +58,16 @@ describe("prefers-reduced-motion compliance", () => { expect(src).toContain("motion-safe:animate-pulse"); }); - it("Legend.tsx uses motion-safe:animate-pulse", () => { + it("Legend.tsx uses motion-safe:animate-pulse (inline or via shared tokens)", () => { const src = readSrc("components/Legend.tsx"); expect(src.includes("animate-pulse") && !src.includes("motion-safe:animate-pulse")).toBe(false); - expect(src).toContain("motion-safe:animate-pulse"); + expect(usesGuardedPulse(src)).toBe(true); }); - it("SearchDialog.tsx uses motion-safe:animate-pulse", () => { + it("SearchDialog.tsx uses motion-safe:animate-pulse (inline or via shared tokens)", () => { const src = readSrc("components/SearchDialog.tsx"); expect(src.includes("animate-pulse") && !src.includes("motion-safe:animate-pulse")).toBe(false); - expect(src).toContain("motion-safe:animate-pulse"); + expect(usesGuardedPulse(src)).toBe(true); }); it("TerminalTab.tsx uses motion-safe:animate-pulse", () => { @@ -76,6 +82,12 @@ describe("prefers-reduced-motion compliance", () => { expect(src).toContain("motion-safe:animate-pulse"); }); + it("design-tokens.ts uses motion-safe:animate-pulse, not bare animate-pulse", () => { + const src = readSrc("lib/design-tokens.ts"); + expect(src.includes("animate-pulse") && !src.includes("motion-safe:animate-pulse")).toBe(false); + expect(src).toContain("motion-safe:animate-pulse"); + }); + it("globals.css disables animate-in and slide-in classes under reduced-motion", () => { const css = readSrc("app/globals.css"); expect(css).toContain(".animate-in"); diff --git a/canvas/src/components/CommunicationOverlay.tsx b/canvas/src/components/CommunicationOverlay.tsx index 76e50157..ebc2c177 100644 --- a/canvas/src/components/CommunicationOverlay.tsx +++ b/canvas/src/components/CommunicationOverlay.tsx @@ -3,6 +3,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { useCanvasStore } from "@/store/canvas"; import { api } from "@/lib/api"; +import { COMM_TYPE_LABELS } from "@/lib/design-tokens"; interface Communication { id: string; @@ -143,7 +144,7 @@ export function CommunicationOverlay() {
- {c.type === "a2a_send" ? "sent" : c.type === "a2a_receive" ? "received" : "task update"} + {COMM_TYPE_LABELS[c.type] ?? c.type} {c.sourceName} diff --git a/canvas/src/components/ContextMenu.tsx b/canvas/src/components/ContextMenu.tsx index d4be6ec5..5e1d2f4f 100644 --- a/canvas/src/components/ContextMenu.tsx +++ b/canvas/src/components/ContextMenu.tsx @@ -5,6 +5,7 @@ import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas"; import { api } from "@/lib/api"; import { showToast } from "./Toaster"; import { ConfirmDialog } from "./ConfirmDialog"; +import { statusDotClass } from "@/lib/design-tokens"; interface MenuItem { label: string; @@ -277,9 +278,7 @@ export function ContextMenu() {
diff --git a/canvas/src/components/EmptyState.tsx b/canvas/src/components/EmptyState.tsx index 9f12f257..52cab350 100644 --- a/canvas/src/components/EmptyState.tsx +++ b/canvas/src/components/EmptyState.tsx @@ -4,7 +4,8 @@ import { useState, useEffect } from "react"; import { api } from "@/lib/api"; import { useCanvasStore } from "@/store/canvas"; import { OrgTemplatesSection } from "./TemplatePalette"; -import { TIER_COLORS } from "@/lib/design-tokens"; +import { Spinner } from "./Spinner"; +import { TIER_CONFIG } from "@/lib/design-tokens"; interface Template { id: string; @@ -100,16 +101,13 @@ export function EmptyState() { {/* Template grid */} {loading ? (
- + Loading templates...
) : templates.length > 0 ? (
{templates.map((t) => { - const tierColor = TIER_COLORS[t.tier] || TIER_COLORS[1]; + const tierColor = TIER_CONFIG[t.tier]?.border || TIER_CONFIG[1].border; return (