{`-${(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;