diff --git a/src/app/(dashboard)/dashboard/combos/live/nodes/ProviderCascadeNode.tsx b/src/app/(dashboard)/dashboard/combos/live/nodes/ProviderCascadeNode.tsx index d2dc8ccf97..2838fc5cbc 100644 --- a/src/app/(dashboard)/dashboard/combos/live/nodes/ProviderCascadeNode.tsx +++ b/src/app/(dashboard)/dashboard/combos/live/nodes/ProviderCascadeNode.tsx @@ -3,7 +3,7 @@ import { Handle, Position, type NodeProps } from "@xyflow/react"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { StatusDot } from "@/shared/components/flow/StatusDot"; -import { FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles"; +import { FLOW_EDGE_COLORS, flowColorAlpha } from "@/shared/components/flow/edgeStyles"; import type { TargetState, FailKind, CbState } from "../comboFlowModel"; // ── State → visual mapping ──────────────────────────────────────────────── @@ -26,11 +26,11 @@ function getStateBorderColor(state: TargetState): string { function getStateGlow(state: TargetState): string { switch (state) { case "attempting": - return `0 0 12px ${FLOW_EDGE_COLORS.last}40`; + return `0 0 12px ${flowColorAlpha(FLOW_EDGE_COLORS.last, 25)}`; case "failed": - return `0 0 12px ${FLOW_EDGE_COLORS.error}40`; + return `0 0 12px ${flowColorAlpha(FLOW_EDGE_COLORS.error, 25)}`; case "succeeded": - return `0 0 12px ${FLOW_EDGE_COLORS.active}40`; + return `0 0 12px ${flowColorAlpha(FLOW_EDGE_COLORS.active, 25)}`; default: return "none"; } @@ -190,7 +190,7 @@ export function ProviderCascadeNode({ data }: NodeProps) { `). + * + * `STATUS_HEX` stays exported for the callers that genuinely need a resolved hex + * (canvas 2D, string math); it is no longer used here. + */ export const FLOW_EDGE_COLORS = { - active: STATUS_HEX.success, - error: STATUS_HEX.error, - last: STATUS_HEX.warning, + active: "var(--orch-status-success)", + error: "var(--orch-status-error)", + last: "var(--orch-status-warning)", idle: "var(--color-text-muted)", } as const; +/** + * Translucent variant of a flow color. The palette values are `var()` now, so the old + * `${hex}30` suffix trick no longer resolves; `color-mix` is the theme-aware equivalent + * (same precedent as `orchStateBadgeBg` in the orchestration model). Percentages mirror + * the previous 8-bit alpha suffixes: `20` -> 13%, `30` -> 19%, `40` -> 25%. + */ +export function flowColorAlpha(color: string, percent: number): string { + return `color-mix(in srgb, ${color} ${percent}%, transparent)`; +} + export interface FlowEdgeStyle { stroke: string; strokeWidth: number; diff --git a/tests/unit/design-grid-background.test.ts b/tests/unit/design-grid-background.test.ts index 7c12b99990..ba6f3ef7c8 100644 --- a/tests/unit/design-grid-background.test.ts +++ b/tests/unit/design-grid-background.test.ts @@ -103,16 +103,34 @@ test("status colors come from one canonical module", () => { assert.match(mod, /warning:\s*"#f59e0b"/); assert.match(mod, /error:\s*"#ef4444"/); + // Fase 3 (D1): the two shared flow surfaces moved from the fixed dark hex to the + // theme-aware `--orch-status-*` tokens. STATUS_HEX stays exported as the dark-mode + // mirror (and the canonical source of the token values in globals.css `.dark`). const edges = read("../../src/shared/components/flow/edgeStyles.ts"); const badge = read("../../src/shared/components/TokenHealthBadge.tsx"); assert.ok( - edges.includes('from "@/shared/constants/statusColors"'), - "edgeStyles imports the module" + edges.includes("var(--orch-status-success)"), + "edgeStyles uses the success token, not a literal" ); - assert.ok(edges.includes("STATUS_HEX.success"), "edgeStyles uses STATUS_HEX, not a literal"); + assert.ok(edges.includes("var(--orch-status-error)"), "edgeStyles uses the error token"); + assert.ok(edges.includes("var(--orch-status-warning)"), "edgeStyles uses the warning token"); assert.ok(!edges.includes('"#22c55e"'), "edgeStyles no longer hardcodes the success hex"); - assert.ok(badge.includes("STATUS_HEX.success"), "TokenHealthBadge uses STATUS_HEX"); + assert.ok( + badge.includes("var(--orch-status-success)"), + "TokenHealthBadge uses the success token" + ); + assert.ok(badge.includes("var(--orch-status-error)"), "TokenHealthBadge uses the error token"); + assert.ok( + badge.includes("var(--orch-status-warning)"), + "TokenHealthBadge uses the warning token" + ); assert.ok(!badge.includes('"#22c55e"'), "TokenHealthBadge no longer hardcodes the success hex"); + + // Both themes must define every token these surfaces read. + for (const token of ["success", "warning", "error", "muted"]) { + const hits = globalsCss.match(new RegExp(`--orch-status-${token}:`, "g")) ?? []; + assert.equal(hits.length, 2, `--orch-status-${token} is defined in light AND dark`); + } }); test("globals.css defines a monospace token (site parity)", () => { diff --git a/tests/unit/ui/edgeStyles.test.ts b/tests/unit/ui/edgeStyles.test.ts index a9305273a2..dbf7b7d25b 100644 --- a/tests/unit/ui/edgeStyles.test.ts +++ b/tests/unit/ui/edgeStyles.test.ts @@ -5,15 +5,15 @@ import { edgeStyle, FLOW_EDGE_COLORS } from "../../../src/shared/components/flow describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { it("exposes the shared flow palette", () => { - assert.equal(FLOW_EDGE_COLORS.active, "#22c55e"); - assert.equal(FLOW_EDGE_COLORS.error, "#ef4444"); - assert.equal(FLOW_EDGE_COLORS.last, "#f59e0b"); + assert.equal(FLOW_EDGE_COLORS.active, "var(--orch-status-success)"); + assert.equal(FLOW_EDGE_COLORS.error, "var(--orch-status-error)"); + assert.equal(FLOW_EDGE_COLORS.last, "var(--orch-status-warning)"); assert.equal(FLOW_EDGE_COLORS.idle, "var(--color-text-muted)"); }); it("styles an error edge", () => { assert.deepEqual(edgeStyle(false, false, true), { - stroke: "#ef4444", + stroke: "var(--orch-status-error)", strokeWidth: 2, opacity: 0.85, }); @@ -21,7 +21,7 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { it("styles an active edge", () => { assert.deepEqual(edgeStyle(true, false, false), { - stroke: "#22c55e", + stroke: "var(--orch-status-success)", strokeWidth: 2.5, opacity: 1, }); @@ -29,7 +29,7 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { it("styles a last-used edge", () => { assert.deepEqual(edgeStyle(false, true, false), { - stroke: "#f59e0b", + stroke: "var(--orch-status-warning)", strokeWidth: 1.5, opacity: 0.6, }); @@ -45,7 +45,7 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { it("styles a healthy (connected, no in-flight traffic) edge as static dim green", () => { assert.deepEqual(edgeStyle(false, false, false, true), { - stroke: "#22c55e", + stroke: "var(--orch-status-success)", strokeWidth: 1.5, opacity: 0.4, }); @@ -60,10 +60,10 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { }); it("applies precedence error > active > last > healthy", () => { - assert.equal(edgeStyle(true, true, true).stroke, "#ef4444"); // error wins - assert.equal(edgeStyle(true, true, false).stroke, "#22c55e"); // active beats last - assert.equal(edgeStyle(false, false, true, true).stroke, "#ef4444"); // error beats healthy - assert.equal(edgeStyle(false, true, false, true).stroke, "#f59e0b"); // last beats healthy + assert.equal(edgeStyle(true, true, true).stroke, "var(--orch-status-error)"); // error wins + assert.equal(edgeStyle(true, true, false).stroke, "var(--orch-status-success)"); // active beats last + assert.equal(edgeStyle(false, false, true, true).stroke, "var(--orch-status-error)"); // error beats healthy + assert.equal(edgeStyle(false, true, false, true).stroke, "var(--orch-status-warning)"); // last beats healthy // healthy green is dimmer/thinner than the active pulse green assert.equal(edgeStyle(false, false, false, true).opacity, 0.4); assert.equal(edgeStyle(true, false, false, true).opacity, 1); // active still wins diff --git a/tests/unit/ui/home-topology-last-used-node-color.test.tsx b/tests/unit/ui/home-topology-last-used-node-color.test.tsx index 2a7b8b82f4..5c97a66e16 100644 --- a/tests/unit/ui/home-topology-last-used-node-color.test.tsx +++ b/tests/unit/ui/home-topology-last-used-node-color.test.tsx @@ -52,18 +52,14 @@ vi.mock("@xyflow/react", () => ({ Position: { Top: "top", Bottom: "bottom", Left: "left", Right: "right" }, })); -const ProviderTopology = ( - await import("../../../src/app/(dashboard)/home/ProviderTopology") -).default; +const ProviderTopology = (await import("../../../src/app/(dashboard)/home/ProviderTopology")) + .default; -// jsdom normalises inline hex colours to `rgb(...)`, so compare in that space. -const rgb = (hex: string) => { - const n = parseInt(hex.slice(1), 16); - return `rgb(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255})`; -}; -const GREEN = rgb(FLOW_EDGE_COLORS.active); -const AMBER = rgb(FLOW_EDGE_COLORS.last); -const RED = rgb(FLOW_EDGE_COLORS.error); +// The flow palette is theme-aware CSS custom properties (Fase 3 D1). jsdom keeps +// `var(...)` verbatim in inline styles, so compare against the token itself. +const GREEN = FLOW_EDGE_COLORS.active; +const AMBER = FLOW_EDGE_COLORS.last; +const RED = FLOW_EDGE_COLORS.error; let container: HTMLDivElement; let root: ReturnType;