From 12bf0ed0777d791944fcd6c2f8d4459256a4900b Mon Sep 17 00:00:00 2001 From: nguyenha935 Date: Sun, 19 Jul 2026 01:14:28 +0700 Subject: [PATCH] fix(ui): improve React Flow dark theme (#7553) * fix(ui): theme provider topology in dark mode * fix(i18n): isolate topology translation keys * refactor(i18n): reuse existing topology labels --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> --- .../fixes/pending-react-flow-dark-theme.md | 1 + .../dashboard/HomeProviderTopologySection.tsx | 18 ++++++++++----- src/app/globals.css | 22 ++++++++----------- src/shared/components/flow/FlowCanvas.tsx | 1 + src/shared/components/flow/edgeStyles.ts | 4 ++-- tests/unit/ui/edgeStyles.test.ts | 6 ++--- tests/unit/ui/flowCanvas.test.tsx | 13 ++++++++++- ...me-provider-topology-section-4606.test.tsx | 14 ++++++++---- 8 files changed, 51 insertions(+), 28 deletions(-) create mode 100644 changelog.d/fixes/pending-react-flow-dark-theme.md diff --git a/changelog.d/fixes/pending-react-flow-dark-theme.md b/changelog.d/fixes/pending-react-flow-dark-theme.md new file mode 100644 index 0000000000..1e177ee735 --- /dev/null +++ b/changelog.d/fixes/pending-react-flow-dark-theme.md @@ -0,0 +1 @@ +- **fix(ui):** Theme React Flow controls correctly in dark mode, improve idle connector contrast, and localize the provider topology legend. diff --git a/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx index 53abbbeed2..8aef005e07 100644 --- a/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx +++ b/src/app/(dashboard)/dashboard/HomeProviderTopologySection.tsx @@ -27,9 +27,14 @@ export function HomeProviderTopologySection({ enabled?: boolean; }) { const t = useTranslations("home"); + const tCommon = useTranslations("common"); + const tSettings = useTranslations("settings"); + const tAnalytics = useTranslations("analytics"); // #4596: gate the live-WS connection so it only opens while the topology // section is actually shown on the home page. const { activeRequests: liveActiveRequests } = useLiveRequests({ enabled }); + const activeRequests = selectActiveRequests(liveActiveRequests); + const activeProviderCount = new Set(activeRequests.map(({ provider }) => provider)).size; return ( @@ -37,24 +42,27 @@ export function HomeProviderTopologySection({

{t("providerTopology")}

- Connected providers routing through OmniRoute in real time + {t("activeError", { active: activeProviderCount, errors: errorProvider ? 1 : 0 })}

- Active + + {tCommon("active")} - Recent + + {tSettings("recent")} - Error + + {tAnalytics("modelStatusError")}
diff --git a/src/app/globals.css b/src/app/globals.css index 4df43dff07..566fafb51d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -482,19 +482,15 @@ select option { color: var(--color-text-main); } -/* ReactFlow Controls (zoom +/- buttons) dark mode theming */ -.react-flow__controls-button { - background-color: var(--color-surface); - border-bottom-color: var(--color-border); - fill: var(--color-text-main); -} - -.react-flow__controls-button:hover { - background-color: var(--color-bg-subtle); -} - -.react-flow__controls-button svg { - fill: var(--color-text-main); +/* React Flow loads its component stylesheet after this global file in some + bundles, so direct background/fill declarations can lose to its shorthand + rules. Override the supported theme variables on our canvas instead. */ +.react-flow.omniroute-flow { + --xy-controls-button-background-color: var(--color-surface); + --xy-controls-button-background-color-hover: var(--color-bg-subtle); + --xy-controls-button-color: var(--color-text-main); + --xy-controls-button-color-hover: var(--color-text-main); + --xy-controls-button-border-color: var(--color-border); } /* Desktop sidebar visibility must not depend on Tailwind's hidden/lg:flex cascade diff --git a/src/shared/components/flow/FlowCanvas.tsx b/src/shared/components/flow/FlowCanvas.tsx index d27c7ed923..f106e67d86 100644 --- a/src/shared/components/flow/FlowCanvas.tsx +++ b/src/shared/components/flow/FlowCanvas.tsx @@ -121,6 +121,7 @@ export function FlowCanvas({
{ 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.idle, "var(--color-border)"); + assert.equal(FLOW_EDGE_COLORS.idle, "var(--color-text-muted)"); }); it("styles an error edge", () => { @@ -37,9 +37,9 @@ describe("flow edgeStyles (U0 — extracted from ProviderTopology)", () => { it("styles an idle edge", () => { assert.deepEqual(edgeStyle(false, false, false), { - stroke: "var(--color-border)", + stroke: "var(--color-text-muted)", strokeWidth: 1, - opacity: 0.2, + opacity: 0.3, }); }); diff --git a/tests/unit/ui/flowCanvas.test.tsx b/tests/unit/ui/flowCanvas.test.tsx index 58b6d02fb0..362583f997 100644 --- a/tests/unit/ui/flowCanvas.test.tsx +++ b/tests/unit/ui/flowCanvas.test.tsx @@ -1,4 +1,6 @@ // @vitest-environment jsdom +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import React, { act } from "react"; import { createRoot } from "react-dom/client"; import { describe, it, expect, beforeAll, beforeEach, afterEach } from "vitest"; @@ -48,7 +50,7 @@ const edges = [{ id: "a-b", source: "a", target: "b" }]; describe("FlowCanvas (U0 — shared ReactFlow wrapper)", () => { it("renders the canvas with Controls and hides the attribution", () => { const container = mount(); - expect(container.querySelector(".react-flow")).toBeTruthy(); + expect(container.querySelector(".react-flow.omniroute-flow")).toBeTruthy(); expect(container.querySelector(".react-flow__controls")).toBeTruthy(); // proOptions.hideAttribution => the attribution element must not render. expect(container.querySelector(".react-flow__attribution")).toBeNull(); @@ -60,4 +62,13 @@ describe("FlowCanvas (U0 — shared ReactFlow wrapper)", () => { ); expect(container.querySelector(".omni-test-canvas")).toBeTruthy(); }); + + it("themes controls through React Flow variables so library shorthands cannot override them", () => { + const css = readFileSync(resolve("src/app/globals.css"), "utf8"); + + expect(css).toContain(".react-flow.omniroute-flow"); + expect(css).toContain("--xy-controls-button-background-color: var(--color-surface)"); + expect(css).toContain("--xy-controls-button-color: var(--color-text-main)"); + expect(css).toContain("--xy-controls-button-border-color: var(--color-border)"); + }); }); diff --git a/tests/unit/ui/home-provider-topology-section-4606.test.tsx b/tests/unit/ui/home-provider-topology-section-4606.test.tsx index 2cd2d3ccf2..cd9885a26d 100644 --- a/tests/unit/ui/home-provider-topology-section-4606.test.tsx +++ b/tests/unit/ui/home-provider-topology-section-4606.test.tsx @@ -15,7 +15,10 @@ vi.mock("next-intl", () => ({ })); vi.mock("next/dynamic", () => ({ default: () => (props: Record) => ( -
+
), })); vi.mock("@/shared/components", () => ({ @@ -26,9 +29,8 @@ vi.mock("@/hooks/useLiveDashboard", () => ({ useLiveRequests: () => liveRequestsMock(), })); -const { HomeProviderTopologySection } = await import( - "../../../src/app/(dashboard)/dashboard/HomeProviderTopologySection" -); +const { HomeProviderTopologySection } = + await import("../../../src/app/(dashboard)/dashboard/HomeProviderTopologySection"); let container: HTMLDivElement; let root: ReturnType; @@ -63,4 +65,8 @@ it("renders the topology card and forwards providers to ProviderTopology", () => const topology = container.querySelector("[data-testid='provider-topology']"); expect(topology).not.toBeNull(); expect(topology?.getAttribute("data-providers")).toBe("2"); + expect(container.textContent).toContain("activeError"); + expect(container.textContent).toContain("active"); + expect(container.textContent).toContain("recent"); + expect(container.textContent).toContain("modelStatusError"); });