refactor(ui): move shared flow colors to the orchestration status tokens

FLOW_EDGE_COLORS and TokenHealthBadge were pinned to the fixed dark-mode
hexes in STATUS_HEX, so both rendered dark-theme green/amber/red on a light
background. They now read the theme-aware --orch-status-{success,warning,
error,muted} custom properties introduced in Fase 2. The dark values of
those tokens are exactly the old hexes, so dark mode is unchanged and only
light mode gains contrast. `idle` was already a CSS var, which is the
precedent proving a var() resolves in a ReactFlow edge stroke.

Five call-sites built translucent variants by concatenating an 8-bit alpha
suffix onto the palette hex (`${FLOW_EDGE_COLORS.error}40`), which cannot
work with a var(). They move to a new documented helper, flowColorAlpha(),
that wraps color-mix() — the same approach orchStateBadgeBg() already uses
in the orchestration model. Percentages mirror the old suffixes
(20 -> 13%, 30 -> 19%, 40 -> 25%).

STATUS_HEX stays exported as the dark-mode mirror; it now has no production
consumer. globals.css needed no change — all five tokens already existed in
both themes.

The colour assertions in the topology, combo-live and design-grid suites
were aligned to the tokens, never weakened: every hex equality became an
equality against the corresponding var(). design-grid additionally now
asserts each token is defined in BOTH themes.

Refs #12378
This commit is contained in:
diegosouzapw
2026-09-08 09:19:34 -03:00
parent fcc2dcd1a6
commit 4ee1c4309b
8 changed files with 79 additions and 46 deletions

View File

@@ -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) {
<span
className="text-[9px] font-semibold px-1.5 py-0.5 rounded"
style={{
backgroundColor: `${FLOW_EDGE_COLORS.error}20`,
backgroundColor: flowColorAlpha(FLOW_EDGE_COLORS.error, 13),
color: FLOW_EDGE_COLORS.error,
}}
data-testid="fail-kind-badge"

View File

@@ -1,7 +1,7 @@
"use client";
import { Handle, Position, type NodeProps } from "@xyflow/react";
import { FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles";
import { FLOW_EDGE_COLORS, flowColorAlpha } from "@/shared/components/flow/edgeStyles";
import type { ComboRunModel } from "../comboFlowModel";
// ── Node data shape ───────────────────────────────────────────────────────
@@ -53,7 +53,7 @@ export function ResponseNode({ data }: NodeProps) {
className="flex flex-col items-center gap-1.5 px-3 py-2 rounded-lg border-2 bg-bg transition-all duration-300"
style={{
borderColor: color,
boxShadow: `0 0 10px ${color}30`,
boxShadow: `0 0 10px ${flowColorAlpha(color, 19)}`,
minWidth: "100px",
}}
data-testid="response-node"

View File

@@ -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",
}}

View File

@@ -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() {

View File

@@ -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 `<path>`).
*
* `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;

View File

@@ -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)", () => {

View File

@@ -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

View File

@@ -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<typeof createRoot>;