fix(canvas): wire ReactFlow colorMode to resolvedTheme

PR #2555 (Tailwind v4 + warm-paper) migrated all canvas chrome (toolbar,
side panel, modal layer) to semantic tokens, but missed the React Flow
viewport's `colorMode="dark"` literal — and two paired hardcoded dark
literals on the Background dot color and MiniMap mask. Net result on
prod: the user picked light mode, the toolbar flipped warm-paper, but
the canvas backplate, edges, dots, controls, and minimap stayed black —
visibly half-themed.

Three coordinated fixes inside the canvas viewport:
- ReactFlow `colorMode={resolvedTheme}` so the library's own dark/light
  styles flip with the user's choice.
- Background dot color picks the line-soft tone in light mode (zinc-800
  was invisible-on-cream).
- MiniMap maskColor warm-tints the off-viewport dim so the unselected
  region doesn't render as a hard black bar over warm-paper.

Verification:
- `npx tsc --noEmit` clean
- `npx vitest run` 188/188 pass
- (will browser-verify post-redeploy on hongming.moleculesai.app)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-05-03 04:11:35 -07:00
parent 88ef70431e
commit df7edfcd3f

View File

@ -13,6 +13,7 @@ import {
import "@xyflow/react/dist/style.css";
import { useCanvasStore } from "@/store/canvas";
import { useTheme } from "@/lib/theme-provider";
import { A2ATopologyOverlay } from "./A2ATopologyOverlay";
import { WorkspaceNode } from "./WorkspaceNode";
import { SidePanel } from "./SidePanel";
@ -69,6 +70,14 @@ export function Canvas() {
}
function CanvasInner() {
// ReactFlow's `colorMode` prop drives the styling of every viewport
// primitive it renders directly (background dots, edge defaults,
// selection rings, controls, minimap mask). Pre-fix this was hard-pinned
// to "dark" — so on light theme the chrome (toolbar, side panel) flipped
// to warm-paper but the canvas backplate + edges stayed black, leaving a
// half-themed page. Pull resolvedTheme so the canvas matches the user's
// selected mode (and the system preference when they pick "system").
const { resolvedTheme } = useTheme();
const rawNodes = useCanvasStore((s) => s.nodes);
const edges = useCanvasStore((s) => s.edges);
const a2aEdges = useCanvasStore((s) => s.a2aEdges);
@ -250,7 +259,7 @@ function CanvasInner() {
</a>
<main id="canvas-main" className="w-screen h-screen bg-surface">
<ReactFlow
colorMode="dark"
colorMode={resolvedTheme}
nodes={nodes}
edges={allEdges}
onNodesChange={onNodesChange}
@ -273,7 +282,9 @@ function CanvasInner() {
variant={BackgroundVariant.Dots}
gap={24}
size={1}
color="#27272a"
// Match the line token so dots fade with the surface.
// Hard-coded zinc-800 was invisible on warm-paper.
color={resolvedTheme === "dark" ? "#27272a" : "#d4d0c4"}
/>
<Controls
className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20 [&>button]:!bg-surface-card [&>button]:!border-line/50 [&>button]:!text-ink-mid [&>button:hover]:!bg-surface-card [&>button:hover]:!text-ink"
@ -281,7 +292,9 @@ function CanvasInner() {
/>
<MiniMap
className="!bg-surface-sunken/90 !border-line/50 !rounded-lg !shadow-xl !shadow-black/20"
maskColor="rgba(0, 0, 0, 0.7)"
// Mask dims off-viewport areas; tint matches the surface so
// the dimming doesn't show as a black bar in light mode.
maskColor={resolvedTheme === "dark" ? "rgba(0, 0, 0, 0.7)" : "rgba(232, 226, 211, 0.7)"}
nodeColor={(node) => {
// Parents show as a filled region — hierarchy visible at
// a glance in the minimap without needing to zoom.