mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-18 04:42:30 +03:00
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:
committed by
GitHub
parent
590582711c
commit
a0c69ca25e
1
changelog.d/maintenance/orchestration-status-tokens.md
Normal file
1
changelog.d/maintenance/orchestration-status-tokens.md
Normal 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))
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" && (
|
||||
|
||||
@@ -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)}%`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}%`}
|
||||
|
||||
@@ -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,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)", () => {
|
||||
|
||||
@@ -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