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 7b0c49c7..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,14 +144,17 @@ export function CommunicationOverlay() {
+ {COMM_TYPE_LABELS[c.type] ?? c.type} {c.sourceName} + to {c.targetName}
+ {c.status} {age}
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 998d88c0..52cab350 100644 --- a/canvas/src/components/EmptyState.tsx +++ b/canvas/src/components/EmptyState.tsx @@ -4,6 +4,8 @@ import { useState, useEffect } from "react"; import { api } from "@/lib/api"; import { useCanvasStore } from "@/store/canvas"; import { OrgTemplatesSection } from "./TemplatePalette"; +import { Spinner } from "./Spinner"; +import { TIER_CONFIG } from "@/lib/design-tokens"; interface Template { id: string; @@ -15,13 +17,6 @@ interface Template { skill_count: number; } -const TIER_COLORS: Record = { - 1: "text-zinc-400 border-zinc-700/60", - 2: "text-sky-400 border-sky-500/30", - 3: "text-violet-400 border-violet-500/30", - 4: "text-amber-400 border-amber-500/30", -}; - export function EmptyState() { const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); @@ -105,17 +100,20 @@ export function EmptyState() { {/* Template grid */} {loading ? ( -
Loading templates...
+
+ + 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 ( diff --git a/canvas/src/components/Legend.tsx b/canvas/src/components/Legend.tsx index e649b6d5..ad7ec8fa 100644 --- a/canvas/src/components/Legend.tsx +++ b/canvas/src/components/Legend.tsx @@ -1,5 +1,9 @@ "use client"; +import { STATUS_CONFIG } from "@/lib/design-tokens"; + +const LEGEND_STATUSES = ["online", "provisioning", "degraded", "failed", "paused", "offline"] as const; + export function Legend() { return (
@@ -9,12 +13,9 @@ export function Legend() {
Status
- - - - - - + {LEGEND_STATUSES.map((s) => ( + + ))}
diff --git a/canvas/src/components/SearchDialog.tsx b/canvas/src/components/SearchDialog.tsx index d6fe0f0f..f272dd17 100644 --- a/canvas/src/components/SearchDialog.tsx +++ b/canvas/src/components/SearchDialog.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useCanvasStore } from "@/store/canvas"; +import { statusDotClass } from "@/lib/design-tokens"; export function SearchDialog() { const open = useCanvasStore((s) => s.searchOpen); @@ -142,12 +143,7 @@ export function SearchDialog() { >