diff --git a/changelog.d/maintenance/orchestration-status-tokens.md b/changelog.d/maintenance/orchestration-status-tokens.md new file mode 100644 index 0000000000..f5c9197d5e --- /dev/null +++ b/changelog.d/maintenance/orchestration-status-tokens.md @@ -0,0 +1 @@ +- **refactor(ui):** flow surfaces (home topology, combo live studio, compression cockpit/waterfall/nodes, token-health badge) express state through the theme-aware `--orch-status-*` tokens instead of fixed dark-mode hex, so green/red/amber/grey stay legible in the light theme; dark mode is byte-identical. Categorical palettes (routing-strategy hues, compression-layer pills, provider brand colors) deliberately stay hex ([#12378](https://github.com/diegosouzapw/OmniRoute/issues/12378)) diff --git a/src/app/(dashboard)/dashboard/combos/live/ComboLiveStudio.tsx b/src/app/(dashboard)/dashboard/combos/live/ComboLiveStudio.tsx index 2f9022d37d..628fefa3d9 100644 --- a/src/app/(dashboard)/dashboard/combos/live/ComboLiveStudio.tsx +++ b/src/app/(dashboard)/dashboard/combos/live/ComboLiveStudio.tsx @@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import type { NodeTypes } from "@xyflow/react"; import { useTranslations } from "next-intl"; import { FlowCanvas } from "@/shared/components/flow/FlowCanvas"; +import { FLOW_EDGE_COLORS, flowColorAlpha } from "@/shared/components/flow/edgeStyles"; import { comboRunToFlow, reduceComboEvent, @@ -81,7 +82,10 @@ function FleetOverview({ comboEvents }: FleetOverviewProps) { {p} @@ -99,7 +103,10 @@ function FleetOverview({ comboEvents }: FleetOverviewProps) { {p} @@ -321,10 +328,10 @@ export function ComboLiveStudio({ style={{ color: displayRun.outcome === "succeeded" - ? "#22c55e" + ? "var(--orch-status-success)" : displayRun.outcome === "exhausted" - ? "#ef4444" - : "#f59e0b", + ? "var(--orch-status-error)" + : "var(--orch-status-warning)", }} data-testid="run-outcome" > 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) { {fmt(run.originalTokens)} → {fmt(run.compressedTokens)} {t("tokenShort")} - + −{run.savingsPercent.toFixed(1)}% {isComplete && view === "canvas" && ( diff --git a/src/app/(dashboard)/dashboard/compression/studio/WaterfallInspector.tsx b/src/app/(dashboard)/dashboard/compression/studio/WaterfallInspector.tsx index 6ccf8ae3a6..c87f72a19c 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/WaterfallInspector.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/WaterfallInspector.tsx @@ -5,12 +5,16 @@ import { useTranslations } from "next-intl"; // ── Helpers ─────────────────────────────────────────────────────────────── +/** Savings quality ramp — same thresholds/tokens as `EngineNode.getSavingsColor`. */ function savingsColor(pct: number): string { - if (pct >= 30) return "#22c55e"; - if (pct >= 15) return "#f59e0b"; - return "#6b7280"; + if (pct >= 30) return "var(--orch-status-success)"; + if (pct >= 15) return "var(--orch-status-warning)"; + return "var(--orch-status-muted)"; } +/** A skipped step reads as idle. */ +const SKIPPED_COLOR = "var(--orch-status-muted)"; + function pctWidth(tokIn: number, tokOut: number): string { if (tokIn === 0) return "100%"; return `${((tokOut / tokIn) * 100).toFixed(1)}%`; @@ -25,7 +29,7 @@ function fmt(n: number): string { function StepRow({ step, maxTokens }: { step: CompressionEngineStep; maxTokens: number }) { const t = useTranslations("compressionStudio"); const skipped = step.originalTokens === step.compressedTokens; - const color = skipped ? "#6b7280" : savingsColor(step.savingsPercent); + const color = skipped ? SKIPPED_COLOR : savingsColor(step.savingsPercent); const barWidthIn = maxTokens > 0 ? (step.originalTokens / maxTokens) * 100 : 100; const barWidthOut = maxTokens > 0 ? (step.compressedTokens / maxTokens) * 100 : 100; @@ -36,7 +40,7 @@ function StepRow({ step, maxTokens }: { step: CompressionEngineStep; maxTokens: > {/* Engine label */}
- + {step.engine} {skipped && ( @@ -147,7 +151,7 @@ export function WaterfallInspector({ run, className = "" }: WaterfallInspectorPr
{`-${run.savingsPercent.toFixed(1)}%`} diff --git a/src/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode.tsx b/src/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode.tsx index 265ce3e297..2896b7b84f 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode.tsx @@ -2,6 +2,7 @@ import { Handle, Position, type NodeProps } from "@xyflow/react"; import { StatusDot } from "@/shared/components/flow/StatusDot"; +import { flowColorAlpha } from "@/shared/components/flow/edgeStyles"; // ── Layer pill color map (10-layer model) ───────────────────────────────── @@ -14,6 +15,12 @@ const LAYER_COLORS: Record = { L9: "#ec4899", // pruning — pink }; +/** + * In-flight engine step. Mirrors the Phase-2 orchestration mapping `running -> + * var(--orch-status-warning)` (`orchestrationTypes.ts::STATE_VAR`). + */ +const RUNNING_COLOR = "var(--orch-status-warning)"; + /** Map engine name → layer tags for the pill display */ const ENGINE_LAYER_MAP: Record = { rtk: ["L3", "L4"], @@ -28,10 +35,15 @@ const ENGINE_LAYER_MAP: Record = { "rtk:standard": ["L3", "L4"], }; +/** + * Savings quality ramp — good / mediocre / none. Theme-aware `--orch-status-*` tokens + * (light values in `:root`, dark values in `.dark` of `src/app/globals.css`); the dark + * values are the previous hexes, so dark mode is byte-identical. + */ function getSavingsColor(savingsPercent: number): string { - if (savingsPercent >= 30) return "#22c55e"; - if (savingsPercent >= 15) return "#f59e0b"; - return "#6b7280"; + if (savingsPercent >= 30) return "var(--orch-status-success)"; + if (savingsPercent >= 15) return "var(--orch-status-warning)"; + return "var(--orch-status-muted)"; } // ── Node data shape ─────────────────────────────────────────────────────── @@ -72,14 +84,18 @@ export function EngineNode({ data }: NodeProps) { const tokOut = compressedTokens as number; const techniques = (techniquesUsed as string[]).slice(0, 2); - const borderColor = skipped ? "var(--color-border)" : running ? "#f59e0b" : color; + const borderColor = skipped ? "var(--color-border)" : running ? RUNNING_COLOR : color; return (
- {running && } + {running && } {engine as string} diff --git a/src/app/(dashboard)/dashboard/compression/studio/nodes/IoNode.tsx b/src/app/(dashboard)/dashboard/compression/studio/nodes/IoNode.tsx index 74b250c7fc..9a1508839b 100644 --- a/src/app/(dashboard)/dashboard/compression/studio/nodes/IoNode.tsx +++ b/src/app/(dashboard)/dashboard/compression/studio/nodes/IoNode.tsx @@ -55,7 +55,7 @@ export function IoNode({ data }: NodeProps) { {!isInput && savingsPercent != null && (
{`-${(savingsPercent as number).toFixed(1)}%`} diff --git a/src/app/(dashboard)/home/ProviderTopology.tsx b/src/app/(dashboard)/home/ProviderTopology.tsx index 413a7a7a7c..8f87e2ba26 100644 --- a/src/app/(dashboard)/home/ProviderTopology.tsx +++ b/src/app/(dashboard)/home/ProviderTopology.tsx @@ -8,7 +8,7 @@ import { AI_PROVIDERS } from "@/shared/constants/providers"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { FlowCanvas } from "@/shared/components/flow/FlowCanvas"; import { StatusDot } from "@/shared/components/flow/StatusDot"; -import { edgeStyle, FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles"; +import { edgeStyle, FLOW_EDGE_COLORS, flowColorAlpha } from "@/shared/components/flow/edgeStyles"; import { getFallbackProviderColor } from "@/shared/utils/providerFallbackColor"; import { resolveTopologyNodeLabel } from "./topologyLabel"; @@ -63,11 +63,11 @@ function ProviderNode({ data }: { data: ProviderNodeData }) { style={{ borderColor: error ? RED : active ? color : healthy ? GREEN : "var(--color-border)", boxShadow: error - ? `0 0 12px ${RED}30` + ? `0 0 12px ${flowColorAlpha(RED, 19)}` : active ? `0 0 12px ${color}30` : healthy - ? `0 0 10px ${GREEN}20` + ? `0 0 10px ${flowColorAlpha(GREEN, 13)}` : "none", minWidth: "136px", }} diff --git a/src/shared/components/TokenHealthBadge.tsx b/src/shared/components/TokenHealthBadge.tsx index 5d6aca0deb..b738d8f5d8 100644 --- a/src/shared/components/TokenHealthBadge.tsx +++ b/src/shared/components/TokenHealthBadge.tsx @@ -10,13 +10,14 @@ import { useTranslations } from "next-intl"; */ import { useState, useEffect } from "react"; -import { STATUS_HEX } from "@/shared/constants/statusColors"; +// Theme-aware status tokens (`--orch-status-*` in src/app/globals.css) instead of the +// fixed dark-mode hexes: the badge sits in the header, which is light in light mode. const STATUS_MAP = { - healthy: { icon: "check_circle", color: STATUS_HEX.success, tooltipKey: "allHealthy" }, - warning: { icon: "warning", color: STATUS_HEX.warning, tooltipKey: "needsAttention" }, - error: { icon: "error", color: STATUS_HEX.error, tooltipKey: "refreshFailures" }, - unknown: { icon: "help", color: STATUS_HEX.muted, tooltipKey: "unknown" }, + healthy: { icon: "check_circle", color: "var(--orch-status-success)", tooltipKey: "allHealthy" }, + warning: { icon: "warning", color: "var(--orch-status-warning)", tooltipKey: "needsAttention" }, + error: { icon: "error", color: "var(--orch-status-error)", tooltipKey: "refreshFailures" }, + unknown: { icon: "help", color: "var(--orch-status-muted)", tooltipKey: "unknown" }, }; export default function TokenHealthBadge() { diff --git a/src/shared/components/flow/edgeStyles.ts b/src/shared/components/flow/edgeStyles.ts index 6d20f2f8a4..8dd2a5998e 100644 --- a/src/shared/components/flow/edgeStyles.ts +++ b/src/shared/components/flow/edgeStyles.ts @@ -4,15 +4,33 @@ * the Compression Studio (Tela A) so all three flow graphs speak the same * color language: green = active, red = error, amber = last-used, muted = idle. */ -import { STATUS_HEX } from "@/shared/constants/statusColors"; - +/** + * Theme-aware CSS custom properties (light values in `:root`, dark values in `.dark` + * of `src/app/globals.css`) — the dark values are exactly the old `STATUS_HEX` hexes, + * so dark mode is byte-identical while light mode finally gets legible contrast. + * `idle` was already a var, which is the precedent that proves a `var()` resolves in + * an SVG `stroke` (ReactFlow renders edge `style` onto a real ``). + * + * `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..e0b08e6566 100644 --- a/tests/unit/design-grid-background.test.ts +++ b/tests/unit/design-grid-background.test.ts @@ -103,16 +103,106 @@ 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`); + } +}); + +// ── Phase 3 (D2): the remaining flow surfaces read the status tokens ── + +test("flow surfaces express state with --orch-status-* tokens, not fixed hex", () => { + // Every state-bearing color on the four flow surfaces (home topology, combo live + // studio, compression cockpit/waterfall, compression nodes) must be a theme-aware + // token. Decorative/categorical palettes (STRATEGY_COLORS, LAYER_COLORS, the + // provider brand color, the input/output identity pair) legitimately stay hex and + // are asserted below so a future migration does not silently swallow them. + const combo = read("../../src/app/(dashboard)/dashboard/combos/live/ComboLiveStudio.tsx"); + const engine = read( + "../../src/app/(dashboard)/dashboard/compression/studio/nodes/EngineNode.tsx" + ); + const waterfall = read( + "../../src/app/(dashboard)/dashboard/compression/studio/WaterfallInspector.tsx" + ); + const cockpit = read( + "../../src/app/(dashboard)/dashboard/compression/studio/CompressionCockpit.tsx" + ); + const io = read("../../src/app/(dashboard)/dashboard/compression/studio/nodes/IoNode.tsx"); + + // Combo live studio: active/error provider pills + the run outcome tri-state. + assert.ok(combo.includes("FLOW_EDGE_COLORS.active"), "combo active pill uses the flow palette"); + assert.ok(combo.includes("FLOW_EDGE_COLORS.error"), "combo error pill uses the flow palette"); + assert.ok( + combo.includes('"var(--orch-status-success)"'), + "combo outcome succeeded = success token" + ); + assert.ok(combo.includes('"var(--orch-status-error)"'), "combo outcome exhausted = error token"); + assert.ok( + combo.includes('"var(--orch-status-warning)"'), + "combo outcome pending = warning token" + ); + assert.ok(!combo.includes('"#22c55e"'), "combo studio hardcodes no success hex"); + assert.ok(!combo.includes('"#ef4444"'), "combo studio hardcodes no error hex"); + assert.ok(!combo.includes('"#f59e0b"'), "combo studio hardcodes no warning hex"); + + // Compression engine node: savings ramp + the running state. + assert.ok(engine.includes('"var(--orch-status-success)"'), "engine savings>=30 = success token"); + assert.ok( + engine.includes('"var(--orch-status-warning)"'), + "engine savings>=15 / running = warning token" + ); + assert.ok(engine.includes('"var(--orch-status-muted)"'), "engine no-savings = muted token"); + assert.ok( + engine.includes("flowColorAlpha("), + "engine glow uses color-mix, not a hex alpha suffix" + ); + assert.ok(!engine.includes('"#f59e0b"'), "engine hardcodes no warning hex"); + assert.ok(!engine.includes("#f59e0b40"), "the 8-bit alpha suffix is gone (invalid on a var())"); + + // Waterfall inspector: same ramp + the skipped/idle state + the total savings. + assert.ok(waterfall.includes('"var(--orch-status-success)"'), "waterfall success token"); + assert.ok(waterfall.includes('"var(--orch-status-warning)"'), "waterfall warning token"); + assert.ok(waterfall.includes('"var(--orch-status-muted)"'), "waterfall skipped = muted token"); + assert.ok(!waterfall.includes('"#22c55e"'), "waterfall hardcodes no success hex"); + assert.ok(!waterfall.includes('"#6b7280"'), "waterfall hardcodes no muted hex"); + + // Cockpit header + IoNode savings readout. + assert.ok(cockpit.includes('"var(--orch-status-success)"'), "cockpit savings = success token"); + assert.ok(!cockpit.includes('"#22c55e"'), "cockpit hardcodes no success hex"); + assert.ok(io.includes('"var(--orch-status-success)"'), "IoNode savings = success token"); + + // Deliberately NOT migrated — categorical/brand palettes, not state. + const strategy = read("../../src/app/(dashboard)/dashboard/combos/live/nodes/StrategyNode.tsx"); + assert.ok(strategy.includes("STRATEGY_COLORS"), "strategy hues stay a categorical palette"); + assert.ok(engine.includes("LAYER_COLORS"), "layer pills stay a categorical palette"); + assert.ok( + io.includes('isInput ? "#6366f1" : "#22c55e"'), + "IoNode keeps its indigo/green input-output identity pair" + ); }); 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;