mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
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:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user