diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx
index ad7d5717..6d28cd68 100644
--- a/src/components/WorkflowCanvas.tsx
+++ b/src/components/WorkflowCanvas.tsx
@@ -49,7 +49,7 @@ import {
// Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes
const GLBViewerNode = dynamic(() => import("./nodes/GLBViewerNode").then(mod => ({ default: mod.GLBViewerNode })), { ssr: false });
-import { EditableEdge, ReferenceEdge } from "./edges";
+import { EditableEdge, ReferenceEdge, SharedEdgeGradients } from "./edges";
import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
import { MultiSelectToolbar } from "./MultiSelectToolbar";
import { EdgeToolbar } from "./EdgeToolbar";
@@ -2008,6 +2008,7 @@ export function WorkflowCanvas() {
animated: false,
}}
>
+
diff --git a/src/components/__tests__/EditableEdge.test.tsx b/src/components/__tests__/EditableEdge.test.tsx
index 6344933c..8c12a45e 100644
--- a/src/components/__tests__/EditableEdge.test.tsx
+++ b/src/components/__tests__/EditableEdge.test.tsx
@@ -133,9 +133,10 @@ describe("EditableEdge", () => {
);
- // Check that gradient is defined
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
+ // Edges now reference shared gradient IDs instead of per-edge defs
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("edge-grad-image-");
});
it("should use blue color for prompt handle type", () => {
@@ -145,8 +146,9 @@ describe("EditableEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("edge-grad-prompt-");
});
it("should use orange color when edge is paused", () => {
@@ -160,8 +162,9 @@ describe("EditableEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("edge-grad-pause-");
});
});
@@ -278,13 +281,10 @@ describe("EditableEdge", () => {
);
- // Gradient should exist with active opacity values
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // Check first stop has opacity 1 (active state)
- const stops = gradient?.querySelectorAll("stop");
- expect(stops?.[0]?.getAttribute("stop-opacity")).toBe("1");
+ // Should reference the "active" shared gradient
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-active");
});
it("should have dimmed opacity when not connected to selected node", () => {
@@ -300,13 +300,10 @@ describe("EditableEdge", () => {
);
- // Gradient should exist with dimmed opacity values
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // Check first stop has lower opacity (dimmed state)
- const stops = gradient?.querySelectorAll("stop");
- expect(stops?.[0]?.getAttribute("stop-opacity")).toBe("0.25");
+ // Should reference the "dimmed" shared gradient
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-dimmed");
});
});
diff --git a/src/components/__tests__/ReferenceEdge.test.tsx b/src/components/__tests__/ReferenceEdge.test.tsx
index 633c38e7..c2db0824 100644
--- a/src/components/__tests__/ReferenceEdge.test.tsx
+++ b/src/components/__tests__/ReferenceEdge.test.tsx
@@ -115,13 +115,10 @@ describe("ReferenceEdge", () => {
);
- // Check that gradient is defined with reference color
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // Gradient ID should contain "reference"
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("reference");
+ // Edges now reference shared gradient IDs instead of per-edge defs
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("edge-grad-reference-");
});
});
@@ -169,12 +166,10 @@ describe("ReferenceEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // Check that gradient ID contains "active"
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("active");
+ // Should reference the "active" shared gradient
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-active");
});
it("should have dimmed opacity when not connected to selected node", () => {
@@ -190,12 +185,10 @@ describe("ReferenceEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // Check that gradient ID contains "dimmed"
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("dimmed");
+ // Should reference the "dimmed" shared gradient
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-dimmed");
});
it("should be dimmed when no nodes are selected", () => {
@@ -211,12 +204,10 @@ describe("ReferenceEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- expect(gradient).toBeInTheDocument();
-
- // When no nodes selected, edge should be dimmed
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("dimmed");
+ // Should reference the "dimmed" shared gradient
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-dimmed");
});
});
@@ -249,9 +240,9 @@ describe("ReferenceEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("active");
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-active");
});
it("should highlight when target node is selected", () => {
@@ -267,9 +258,9 @@ describe("ReferenceEdge", () => {
);
- const gradient = container.querySelector("linearGradient");
- const gradientId = gradient?.getAttribute("id");
- expect(gradientId).toContain("active");
+ const basePath = container.querySelector(".react-flow__edge-path");
+ const stroke = basePath?.getAttribute("style") ?? "";
+ expect(stroke).toContain("-active");
});
});
});
diff --git a/src/components/edges/EditableEdge.tsx b/src/components/edges/EditableEdge.tsx
index 8e7d0477..d93ad92f 100644
--- a/src/components/edges/EditableEdge.tsx
+++ b/src/components/edges/EditableEdge.tsx
@@ -10,6 +10,7 @@ import {
} from "@xyflow/react";
import { useWorkflowStore } from "@/store/workflowStore";
import { NanoBananaNodeData, WorkflowEdgeData } from "@/types";
+import { getSharedGradientId } from "./SharedEdgeGradients";
interface EdgeData extends WorkflowEdgeData {
offsetX?: number;
@@ -43,33 +44,26 @@ export function EditableEdge({
}: EdgeProps) {
const { setEdges } = useReactFlow();
const edgeStyle = useWorkflowStore((state) => state.edgeStyle);
- // Subscribe to nodes array to get updates when node data changes
- const nodes = useWorkflowStore((state) => state.nodes);
const [isDragging, setIsDragging] = useState(false);
- // Check if any node is selected and if this edge is connected to it
- const isConnectedToSelection = useMemo(() => {
- const selectedNodes = nodes.filter((n) => n.selected);
- if (selectedNodes.length === 0) return false; // No selection, show all dimmed
-
- // Check if this edge connects to any selected node
- return selectedNodes.some((n) => n.id === source || n.id === target);
- }, [nodes, source, target]);
+ // Narrow selector: returns boolean, only re-renders when selection relevance changes
+ const isConnectedToSelection = useWorkflowStore((state) => {
+ const selected = state.nodes.filter((n) => n.selected);
+ if (selected.length === 0) return false;
+ return selected.some((n) => n.id === source || n.id === target);
+ });
const edgeData = data as EdgeData | undefined;
const offsetX = edgeData?.offsetX ?? 0;
const offsetY = edgeData?.offsetY ?? 0;
const hasPause = edgeData?.hasPause ?? false;
- // Check if target node is a Generate node that's currently loading
- const isTargetLoading = useMemo(() => {
- const targetNode = nodes.find((n) => n.id === target);
- if (targetNode?.type === "nanoBanana") {
- const nodeData = targetNode.data as NanoBananaNodeData;
- return nodeData.status === "loading";
- }
- return false;
- }, [target, nodes]);
+ // Narrow selector: only re-renders when target loading status changes
+ const isTargetLoading = useWorkflowStore((state) => {
+ const targetNode = state.nodes.find((n) => n.id === target);
+ if (targetNode?.type !== "nanoBanana") return false;
+ return (targetNode.data as NanoBananaNodeData).status === "loading";
+ });
// Determine edge color based on handle type (orange if paused)
const edgeColor = useMemo(() => {
@@ -81,12 +75,12 @@ export function EditableEdge({
return EDGE_COLORS.default;
}, [hasPause, sourceHandleId, targetHandleId]);
- // Generate a unique gradient ID based on edge color and selection state
+ // Reference shared gradient by color key + selection state
const gradientId = useMemo(() => {
const colorKey = hasPause ? "pause" : (sourceHandleId || targetHandleId || "default");
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
- return `edge-gradient-${colorKey}-${selectionKey}-${id}`;
- }, [hasPause, sourceHandleId, targetHandleId, isConnectedToSelection, id]);
+ return getSharedGradientId(colorKey, selectionKey);
+ }, [hasPause, sourceHandleId, targetHandleId, isConnectedToSelection]);
// Calculate the path based on edge style
const [edgePath, labelX, labelY] = useMemo(() => {
@@ -181,15 +175,6 @@ export function EditableEdge({
return (
<>
- {/* SVG gradient definition for bright-dim-bright effect */}
-
-
-
-
-
-
-
-
- {/* Glow effect behind the pulse */}
+ {/* Outer glow — replaces blur(6px) filter for better perf on Windows */}
+ {/* Inner glow */}
+
{/* Animated flowing pulse using stroke-dasharray */}
state.edgeStyle);
- const nodes = useWorkflowStore((state) => state.nodes);
-
- // Check if any node is selected and if this edge is connected to it
- const isConnectedToSelection = useMemo(() => {
- const selectedNodes = nodes.filter((n) => n.selected);
- if (selectedNodes.length === 0) return false; // No selection, show all dimmed
- // Check if this edge connects to any selected node
- return selectedNodes.some((n) => n.id === source || n.id === target);
- }, [nodes, source, target]);
+ // Narrow selector: returns boolean, only re-renders when selection relevance changes
+ const isConnectedToSelection = useWorkflowStore((state) => {
+ const selected = state.nodes.filter((n) => n.selected);
+ if (selected.length === 0) return false;
+ return selected.some((n) => n.id === source || n.id === target);
+ });
// Calculate the path - always use curved for reference edges for softer look
const [edgePath] = useMemo(() => {
@@ -49,23 +44,14 @@ export function ReferenceEdge({
});
}, [sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition]);
- // Generate a unique gradient ID based on selection state
+ // Reference shared gradient by selection state
const gradientId = useMemo(() => {
const selectionKey = isConnectedToSelection ? "active" : "dimmed";
- return `reference-gradient-${selectionKey}-${id}`;
- }, [isConnectedToSelection, id]);
+ return getSharedGradientId("reference", selectionKey);
+ }, [isConnectedToSelection]);
return (
<>
- {/* SVG gradient definition for bright-dim-bright effect */}
-
-
-
-
-
-
-
-
/ in every edge component.
+
+const EDGE_COLORS: Record = {
+ image: "#0d9668",
+ prompt: "#2563eb",
+ default: "#64748b",
+ pause: "#ea580c",
+ reference: "#52525b",
+};
+
+const SELECTION_STATES = ["active", "dimmed"] as const;
+
+function gradientStops(color: string, active: boolean) {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+export function getSharedGradientId(colorKey: string, selectionKey: "active" | "dimmed") {
+ return `edge-grad-${colorKey}-${selectionKey}`;
+}
+
+export function SharedEdgeGradients() {
+ return (
+
+ );
+}
diff --git a/src/components/edges/index.ts b/src/components/edges/index.ts
index d5a5471a..b782c0d5 100644
--- a/src/components/edges/index.ts
+++ b/src/components/edges/index.ts
@@ -1,2 +1,3 @@
export { EditableEdge } from "./EditableEdge";
export { ReferenceEdge } from "./ReferenceEdge";
+export { SharedEdgeGradients } from "./SharedEdgeGradients";