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() {
{typeIcon}
+ {COMM_TYPE_LABELS[c.type] ?? c.type}
{c.sourceName}
→
+ to
{c.targetName}
{statusIcon}
+ {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() {
{contextMenu.nodeData.status}
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 (