refactor(ui): orchestration canvas fase 3 — theme-aware status tokens across flow surfaces (#12378) (#13203)

* 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

* refactor(ui): finish the status-token migration across flow surfaces

Sweeps the five state hexes across the remaining flow surfaces, following D1:

- ComboLiveStudio: active/error provider pills and the run-outcome tri-state.
- CompressionCockpit / WaterfallInspector / IoNode: the savings readouts and the
  savings quality ramp (>=30 success, >=15 warning, else muted).
- EngineNode: the same ramp, plus the running state, whose glow moved to
  flowColorAlpha — the literal #f59e0b40 suffix is invalid once the value is a var().
- WaterfallInspector: a skipped step now reads as muted rather than a bare grey hex.

Deliberately NOT migrated, because they are categorical or brand palettes rather
than state: STRATEGY_COLORS (routing-strategy hues), LAYER_COLORS (compression
layer pills), the provider brand color in ProviderTopology, and IoNode's
indigo/green input-output identity pair. The new test asserts both halves — what
became a token AND what stays hex — so a later sweep cannot silently swallow a
categorical palette.

Refs #12378
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-09-10 10:29:55 -03:00
committed by GitHub
parent 590582711c
commit a0c69ca25e
14 changed files with 198 additions and 65 deletions

View File

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

View File

@@ -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) {
<span
key={p}
className="text-[11px] px-2 py-0.5 rounded-full font-medium"
style={{ backgroundColor: "#22c55e20", color: "#22c55e" }}
style={{
backgroundColor: flowColorAlpha(FLOW_EDGE_COLORS.active, 13),
color: FLOW_EDGE_COLORS.active,
}}
>
{p}
</span>
@@ -99,7 +103,10 @@ function FleetOverview({ comboEvents }: FleetOverviewProps) {
<span
key={p}
className="text-[11px] px-2 py-0.5 rounded-full font-medium"
style={{ backgroundColor: "#ef444420", color: "#ef4444" }}
style={{
backgroundColor: flowColorAlpha(FLOW_EDGE_COLORS.error, 13),
color: FLOW_EDGE_COLORS.error,
}}
>
{p}
</span>
@@ -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"
>

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

@@ -213,7 +213,7 @@ export function CompressionCockpit({ run: runProp }: CompressionCockpitProps) {
<span className="text-[11px] text-muted">
{fmt(run.originalTokens)} {fmt(run.compressedTokens)} {t("tokenShort")}
</span>
<span className="text-xs font-bold" style={{ color: "#22c55e" }}>
<span className="text-xs font-bold" style={{ color: "var(--orch-status-success)" }}>
{run.savingsPercent.toFixed(1)}%
</span>
{isComplete && view === "canvas" && (

View File

@@ -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 */}
<div className="w-28 shrink-0">
<span className="text-xs font-semibold" style={{ color: skipped ? "#6b7280" : color }}>
<span className="text-xs font-semibold" style={{ color: skipped ? SKIPPED_COLOR : color }}>
{step.engine}
</span>
{skipped && (
@@ -147,7 +151,7 @@ export function WaterfallInspector({ run, className = "" }: WaterfallInspectorPr
</div>
<div
className="text-[11px] font-bold"
style={{ color: "#22c55e" }}
style={{ color: "var(--orch-status-success)" }}
data-testid="waterfall-total-savings"
>
{`-${run.savingsPercent.toFixed(1)}%`}

View File

@@ -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<string, string> = {
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<string, string[]> = {
rtk: ["L3", "L4"],
@@ -28,10 +35,15 @@ const ENGINE_LAYER_MAP: Record<string, string[]> = {
"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 (
<div
className="rounded-lg border-2 bg-bg transition-all duration-300 min-w-[150px] max-w-[180px]"
style={{
borderColor,
boxShadow: running ? `0 0 14px #f59e0b40` : skipped ? "none" : `0 0 10px ${color}30`,
boxShadow: running
? `0 0 14px ${flowColorAlpha(RUNNING_COLOR, 25)}`
: skipped
? "none"
: `0 0 10px ${flowColorAlpha(color, 19)}`,
}}
>
<Handle
@@ -98,7 +114,7 @@ export function EngineNode({ data }: NodeProps) {
className="flex items-center gap-1.5 px-2.5 pt-2 pb-1"
style={{ borderBottom: "1px solid var(--color-border)" }}
>
{running && <StatusDot color="#f59e0b" sizeClass="size-1.5" />}
{running && <StatusDot color={RUNNING_COLOR} sizeClass="size-1.5" />}
<span className="text-xs font-semibold truncate flex-1" title={engine as string}>
{engine as string}
</span>

View File

@@ -55,7 +55,7 @@ export function IoNode({ data }: NodeProps) {
{!isInput && savingsPercent != null && (
<div
className="mt-1 text-[11px] font-semibold"
style={{ color: "#22c55e" }}
style={{ color: "var(--orch-status-success)" }}
data-testid="io-savings-percent"
>
{`-${(savingsPercent as number).toFixed(1)}%`}

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

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