diff --git a/app/globals.css b/app/globals.css
index f2735b5..48156a5 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -190,6 +190,11 @@
z-index: 50;
}
+ /* Reconnect-Anker immer pointer-interactive halten (Drag-Detach/Reconnect) */
+ .react-flow__edgeupdater {
+ pointer-events: all;
+ }
+
/* Proximity-Vorschaukante (temp) */
.react-flow__edge.temp {
opacity: 0.9;
diff --git a/components/canvas/__tests__/canvas-handle.test.tsx b/components/canvas/__tests__/canvas-handle.test.tsx
index 62c8c3a..da8033b 100644
--- a/components/canvas/__tests__/canvas-handle.test.tsx
+++ b/components/canvas/__tests__/canvas-handle.test.tsx
@@ -4,7 +4,10 @@ import React, { act, useEffect } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { HANDLE_SNAP_RADIUS_PX } from "@/components/canvas/canvas-connection-magnetism";
+import {
+ HANDLE_GLOW_RADIUS_PX,
+ HANDLE_SNAP_RADIUS_PX,
+} from "@/components/canvas/canvas-connection-magnetism";
import {
CanvasConnectionMagnetismProvider,
useCanvasConnectionMagnetism,
@@ -15,6 +18,9 @@ const connectionStateRef: {
inProgress?: boolean;
fromNode?: { id: string };
fromHandle?: { id?: string; type?: "source" | "target" };
+ toNode?: { id: string } | null;
+ toHandle?: { id?: string | null; type?: "source" | "target" } | null;
+ isValid?: boolean | null;
};
} = {
current: { inProgress: false },
@@ -78,6 +84,7 @@ describe("CanvasHandle", () => {
});
}
container?.remove();
+ document.documentElement.classList.remove("dark");
container = null;
root = null;
});
@@ -87,6 +94,9 @@ describe("CanvasHandle", () => {
inProgress?: boolean;
fromNode?: { id: string };
fromHandle?: { id?: string; type?: "source" | "target" };
+ toNode?: { id: string } | null;
+ toHandle?: { id?: string | null; type?: "source" | "target" } | null;
+ isValid?: boolean | null;
};
activeTarget?: {
nodeId: string;
@@ -186,6 +196,42 @@ describe("CanvasHandle", () => {
expect(snappedHandle.style.boxShadow).not.toBe(nearGlow);
});
+ it("ramps up glow intensity as pointer gets closer within glow radius", async () => {
+ await renderHandle({
+ connectionState: { inProgress: true },
+ activeTarget: {
+ nodeId: "node-1",
+ handleId: "image-in",
+ handleType: "target",
+ centerX: 120,
+ centerY: 80,
+ distancePx: HANDLE_GLOW_RADIUS_PX - 1,
+ },
+ });
+
+ const farHandle = getHandleElement();
+ const farStrength = Number(farHandle.getAttribute("data-glow-strength") ?? "0");
+
+ await renderHandle({
+ connectionState: { inProgress: true },
+ activeTarget: {
+ nodeId: "node-1",
+ handleId: "image-in",
+ handleType: "target",
+ centerX: 120,
+ centerY: 80,
+ distancePx: HANDLE_SNAP_RADIUS_PX + 1,
+ },
+ });
+
+ const nearHandle = getHandleElement();
+ const nearStrength = Number(nearHandle.getAttribute("data-glow-strength") ?? "0");
+
+ expect(farHandle.getAttribute("data-glow-state")).toBe("near");
+ expect(nearHandle.getAttribute("data-glow-state")).toBe("near");
+ expect(nearStrength).toBeGreaterThan(farStrength);
+ });
+
it("does not glow for non-target handles during the same drag", async () => {
await renderHandle({
connectionState: { inProgress: true },
@@ -223,6 +269,61 @@ describe("CanvasHandle", () => {
expect(handle.getAttribute("data-glow-state")).toBe("near");
});
+ it("shows glow from native connection hover target even without custom magnet target", async () => {
+ await renderHandle({
+ connectionState: {
+ inProgress: true,
+ isValid: true,
+ toNode: { id: "node-1" },
+ toHandle: { id: "image-in", type: "target" },
+ },
+ activeTarget: null,
+ });
+
+ const handle = getHandleElement();
+ expect(handle.getAttribute("data-glow-state")).toBe("snapped");
+ });
+
+ it("adapts glow rendering between light and dark modes", async () => {
+ await renderHandle({
+ connectionState: { inProgress: true },
+ activeTarget: {
+ nodeId: "node-1",
+ handleId: "image-in",
+ handleType: "target",
+ centerX: 120,
+ centerY: 80,
+ distancePx: HANDLE_SNAP_RADIUS_PX + 1,
+ },
+ });
+
+ const lightHandle = getHandleElement();
+ const lightShadow = lightHandle.style.boxShadow;
+ const lightMode = lightHandle.getAttribute("data-glow-mode");
+
+ document.documentElement.classList.add("dark");
+
+ await renderHandle({
+ connectionState: { inProgress: true },
+ activeTarget: {
+ nodeId: "node-1",
+ handleId: "image-in",
+ handleType: "target",
+ centerX: 120,
+ centerY: 80,
+ distancePx: HANDLE_SNAP_RADIUS_PX + 1,
+ },
+ });
+
+ const darkHandle = getHandleElement();
+ const darkShadow = darkHandle.style.boxShadow;
+ const darkMode = darkHandle.getAttribute("data-glow-mode");
+
+ expect(lightMode).toBe("light");
+ expect(darkMode).toBe("dark");
+ expect(darkShadow).not.toBe(lightShadow);
+ });
+
it("emits stable handle geometry data attributes", async () => {
await renderHandle({
props: {
diff --git a/components/canvas/__tests__/custom-connection-line.test.tsx b/components/canvas/__tests__/custom-connection-line.test.tsx
index 379b438..bb583ce 100644
--- a/components/canvas/__tests__/custom-connection-line.test.tsx
+++ b/components/canvas/__tests__/custom-connection-line.test.tsx
@@ -19,11 +19,13 @@ const reactFlowStateRef: {
current: {
nodes: Array<{ id: string; type: string; position: { x: number; y: number }; data: object }>;
edges: Array<{ id: string; source: string; target: string; targetHandle?: string | null }>;
+ screenToFlowPosition: ({ x, y }: { x: number; y: number }) => { x: number; y: number };
};
} = {
current: {
nodes: [],
edges: [],
+ screenToFlowPosition: ({ x, y }) => ({ x, y }),
},
};
@@ -45,6 +47,7 @@ vi.mock("@xyflow/react", async () => {
useReactFlow: () => ({
getNodes: () => reactFlowStateRef.current.nodes,
getEdges: () => reactFlowStateRef.current.edges,
+ screenToFlowPosition: reactFlowStateRef.current.screenToFlowPosition,
}),
useConnection: () => connectionStateRef.current,
};
@@ -97,6 +100,7 @@ describe("CustomConnectionLine", () => {
document
.querySelectorAll("[data-testid='custom-line-magnet-handle']")
.forEach((element) => element.remove());
+ document.documentElement.classList.remove("dark");
container = null;
root = null;
});
@@ -105,6 +109,9 @@ describe("CustomConnectionLine", () => {
withMagnetHandle?: boolean;
connectionStatus?: ConnectionLineComponentProps["connectionStatus"];
omitFromHandleType?: boolean;
+ toX?: number;
+ toY?: number;
+ pointer?: { x: number; y: number };
}) {
document
.querySelectorAll("[data-testid='custom-line-magnet-handle']")
@@ -116,6 +123,7 @@ describe("CustomConnectionLine", () => {
{ id: "target-node", type: "render", position: { x: 0, y: 0 }, data: {} },
],
edges: [],
+ screenToFlowPosition: ({ x, y }) => ({ x, y }),
};
connectionStateRef.current = {
@@ -144,11 +152,14 @@ describe("CustomConnectionLine", () => {
}
act(() => {
- const lineProps = {
- ...baseProps,
- fromHandle: {
- ...baseProps.fromHandle,
- ...(args?.omitFromHandleType ? { type: undefined } : null),
+ const lineProps = {
+ ...baseProps,
+ ...(args?.toX !== undefined ? { toX: args.toX } : null),
+ ...(args?.toY !== undefined ? { toY: args.toY } : null),
+ ...(args?.pointer ? { pointer: args.pointer } : null),
+ fromHandle: {
+ ...baseProps.fromHandle,
+ ...(args?.omitFromHandleType ? { type: undefined } : null),
},
} as ConnectionLineComponentProps;
@@ -220,6 +231,29 @@ describe("CustomConnectionLine", () => {
expect(snappedPath.style.filter).not.toBe(idleFilter);
});
+ it("ramps stroke feedback up as pointer gets closer before snap", () => {
+ renderLine({
+ withMagnetHandle: true,
+ toX: 252,
+ toY: 220,
+ pointer: { x: 252, y: 220 },
+ });
+ const farNearPath = getPath();
+ const farNearWidth = Number(farNearPath.style.strokeWidth || "0");
+
+ renderLine({
+ withMagnetHandle: true,
+ toX: 266,
+ toY: 220,
+ pointer: { x: 266, y: 220 },
+ });
+ const closeNearPath = getPath();
+ const closeNearWidth = Number(closeNearPath.style.strokeWidth || "0");
+
+ expect(farNearWidth).toBeGreaterThan(2.5);
+ expect(closeNearWidth).toBeGreaterThan(farNearWidth);
+ });
+
it("keeps invalid connection opacity behavior while snapped", () => {
renderLine({
withMagnetHandle: true,
@@ -229,4 +263,48 @@ describe("CustomConnectionLine", () => {
const path = getPath();
expect(path.style.opacity).toBe("0.45");
});
+
+ it("uses client pointer coordinates for magnet lookup and converts snapped endpoint back to flow space", () => {
+ reactFlowStateRef.current.screenToFlowPosition = ({ x, y }) => ({
+ x: Math.round(x / 10),
+ y: Math.round(y / 10),
+ });
+
+ renderLine({
+ withMagnetHandle: true,
+ toX: 29,
+ toY: 21,
+ pointer: { x: 300, y: 220 },
+ });
+
+ const path = getPath();
+ expect(path.getAttribute("d")).toContain("30");
+ expect(path.getAttribute("d")).toContain("22");
+ });
+
+ it("adjusts glow filter between light and dark mode", () => {
+ renderLine({
+ withMagnetHandle: true,
+ toX: 266,
+ toY: 220,
+ pointer: { x: 266, y: 220 },
+ });
+ const lightPath = getPath();
+ const lightFilter = lightPath.style.filter;
+
+ document.documentElement.classList.add("dark");
+
+ renderLine({
+ withMagnetHandle: true,
+ toX: 266,
+ toY: 220,
+ pointer: { x: 266, y: 220 },
+ });
+ const darkPath = getPath();
+ const darkFilter = darkPath.style.filter;
+
+ expect(lightFilter).not.toBe("");
+ expect(darkFilter).not.toBe("");
+ expect(darkFilter).not.toBe(lightFilter);
+ });
});
diff --git a/components/canvas/canvas-connection-magnetism.ts b/components/canvas/canvas-connection-magnetism.ts
index 468049b..949b3f7 100644
--- a/components/canvas/canvas-connection-magnetism.ts
+++ b/components/canvas/canvas-connection-magnetism.ts
@@ -5,6 +5,52 @@ import { validateCanvasConnectionPolicy } from "@/lib/canvas-connection-policy";
export const HANDLE_GLOW_RADIUS_PX = 56;
export const HANDLE_SNAP_RADIUS_PX = 40;
+function clamp01(value: number): number {
+ if (!Number.isFinite(value)) {
+ return 0;
+ }
+ if (value <= 0) {
+ return 0;
+ }
+ if (value >= 1) {
+ return 1;
+ }
+ return value;
+}
+
+function smoothstep(value: number): number {
+ const v = clamp01(value);
+ return v * v * (3 - 2 * v);
+}
+
+export function resolveCanvasGlowStrength(args: {
+ distancePx: number;
+ glowRadiusPx?: number;
+ snapRadiusPx?: number;
+}): number {
+ const glowRadius = args.glowRadiusPx ?? HANDLE_GLOW_RADIUS_PX;
+ const snapRadius = args.snapRadiusPx ?? HANDLE_SNAP_RADIUS_PX;
+
+ if (!Number.isFinite(args.distancePx)) {
+ return 0;
+ }
+ if (args.distancePx <= 0) {
+ return 1;
+ }
+ if (args.distancePx >= glowRadius) {
+ return 0;
+ }
+ if (args.distancePx <= snapRadius) {
+ return 1;
+ }
+
+ const preSnapRange = Math.max(1, glowRadius - snapRadius);
+ const progressToSnap = (glowRadius - args.distancePx) / preSnapRange;
+ const eased = smoothstep(progressToSnap);
+
+ return 0.22 + eased * 0.68;
+}
+
export type CanvasMagnetTarget = {
nodeId: string;
handleId?: string;
diff --git a/components/canvas/canvas-handle.tsx b/components/canvas/canvas-handle.tsx
index bc03931..0fbd7b7 100644
--- a/components/canvas/canvas-handle.tsx
+++ b/components/canvas/canvas-handle.tsx
@@ -2,11 +2,14 @@
import { Handle, useConnection } from "@xyflow/react";
-import { HANDLE_SNAP_RADIUS_PX } from "@/components/canvas/canvas-connection-magnetism";
+import {
+ resolveCanvasGlowStrength,
+} from "@/components/canvas/canvas-connection-magnetism";
import { useCanvasConnectionMagnetism } from "@/components/canvas/canvas-connection-magnetism-context";
import {
canvasHandleAccentColor,
- canvasHandleAccentColorWithAlpha,
+ canvasHandleGlowShadow,
+ type EdgeGlowColorMode,
} from "@/lib/canvas-utils";
import { cn } from "@/lib/utils";
@@ -36,6 +39,7 @@ export default function CanvasHandle({
const connectionState = connection as {
inProgress?: boolean;
+ isValid?: boolean | null;
fromNode?: unknown;
toNode?: unknown;
fromHandle?: unknown;
@@ -52,6 +56,32 @@ export default function CanvasHandle({
const handleId = normalizeHandleId(id);
const targetHandleId = normalizeHandleId(activeTarget?.handleId);
+
+ const toNodeId =
+ connectionState.toNode &&
+ typeof connectionState.toNode === "object" &&
+ "id" in connectionState.toNode &&
+ typeof (connectionState.toNode as { id?: unknown }).id === "string"
+ ? ((connectionState.toNode as { id: string }).id ?? null)
+ : null;
+
+ const toHandleMeta =
+ connectionState.toHandle && typeof connectionState.toHandle === "object"
+ ? (connectionState.toHandle as { id?: string | null; type?: "source" | "target" })
+ : null;
+ const toHandleId = normalizeHandleId(
+ toHandleMeta?.id === null ? undefined : toHandleMeta?.id,
+ );
+ const toHandleType =
+ toHandleMeta?.type === "source" || toHandleMeta?.type === "target"
+ ? toHandleMeta.type
+ : null;
+
+ const colorMode: EdgeGlowColorMode =
+ typeof document !== "undefined" && document.documentElement.classList.contains("dark")
+ ? "dark"
+ : "light";
+
const isActiveTarget =
isConnectionDragActive &&
activeTarget !== null &&
@@ -59,21 +89,37 @@ export default function CanvasHandle({
activeTarget.handleType === type &&
targetHandleId === handleId;
- const glowState: "idle" | "near" | "snapped" = isActiveTarget
- ? activeTarget.distancePx <= HANDLE_SNAP_RADIUS_PX
- ? "snapped"
- : "near"
- : "idle";
+ const isNativeHoverTarget =
+ connectionState.inProgress === true &&
+ toNodeId === nodeId &&
+ toHandleType === type &&
+ toHandleId === handleId;
+
+ let glowStrength = 0;
+
+ if (isActiveTarget) {
+ glowStrength = resolveCanvasGlowStrength({
+ distancePx: activeTarget.distancePx,
+ });
+ } else if (isNativeHoverTarget) {
+ glowStrength = connectionState.isValid === true ? 1 : 0.68;
+ }
+
+ const glowState: "idle" | "near" | "snapped" =
+ glowStrength <= 0 ? "idle" : glowStrength >= 0.96 ? "snapped" : "near";
const accentColor = canvasHandleAccentColor({
nodeType,
handleId,
handleType: type,
});
- const glowAlpha = glowState === "snapped" ? 0.62 : glowState === "near" ? 0.4 : 0;
- const ringAlpha = glowState === "snapped" ? 0.34 : glowState === "near" ? 0.2 : 0;
- const glowSize = glowState === "snapped" ? 14 : glowState === "near" ? 10 : 0;
- const ringSize = glowState === "snapped" ? 6 : glowState === "near" ? 4 : 0;
+ const boxShadow = canvasHandleGlowShadow({
+ nodeType,
+ handleId,
+ handleType: type,
+ strength: glowStrength,
+ colorMode,
+ });
return (
);
}
diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx
index 13ec262..6ab38ab 100644
--- a/components/canvas/canvas.tsx
+++ b/components/canvas/canvas.tsx
@@ -78,6 +78,7 @@ import { useCanvasEdgeTypes } from "./use-canvas-edge-types";
import { useCanvasFlowReconciliation } from "./use-canvas-flow-reconciliation";
import { useCanvasLocalSnapshotPersistence } from "./use-canvas-local-snapshot-persistence";
import { useCanvasSyncEngine } from "./use-canvas-sync-engine";
+import { HANDLE_GLOW_RADIUS_PX } from "./canvas-connection-magnetism";
import { CanvasConnectionMagnetismProvider } from "./canvas-connection-magnetism-context";
interface CanvasInnerProps {
@@ -676,6 +677,9 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
panOnDrag={flowPanOnDrag}
selectionOnDrag={flowSelectionOnDrag}
panActivationKeyCode="Space"
+ connectionRadius={HANDLE_GLOW_RADIUS_PX}
+ reconnectRadius={24}
+ edgesReconnectable
proOptions={{ hideAttribution: true }}
colorMode={resolvedTheme === "dark" ? "dark" : "light"}
className={cn(
diff --git a/components/canvas/custom-connection-line.tsx b/components/canvas/custom-connection-line.tsx
index 8dbf683..746ca5f 100644
--- a/components/canvas/custom-connection-line.tsx
+++ b/components/canvas/custom-connection-line.tsx
@@ -14,10 +14,15 @@ import { useEffect, useMemo } from "react";
import {
HANDLE_SNAP_RADIUS_PX,
+ resolveCanvasGlowStrength,
resolveCanvasMagnetTarget,
} from "@/components/canvas/canvas-connection-magnetism";
import { useCanvasConnectionMagnetism } from "@/components/canvas/canvas-connection-magnetism-context";
-import { connectionLineAccentRgb } from "@/lib/canvas-utils";
+import {
+ connectionLineAccentRgb,
+ connectionLineGlowFilter,
+ type EdgeGlowColorMode,
+} from "@/lib/canvas-utils";
function hasSameMagnetTarget(
a: Parameters["setActiveTarget"]>[0],
@@ -51,8 +56,9 @@ export default function CustomConnectionLine({
fromPosition,
toPosition,
connectionStatus,
+ pointer,
}: ConnectionLineComponentProps) {
- const { getNodes, getEdges } = useReactFlow();
+ const { getNodes, getEdges, screenToFlowPosition } = useReactFlow();
const connection = useConnection();
const { activeTarget, setActiveTarget } = useCanvasConnectionMagnetism();
const fromHandleId = fromHandle?.id;
@@ -73,15 +79,20 @@ export default function CustomConnectionLine({
return null;
}
+ const magnetPoint =
+ pointer && Number.isFinite(pointer.x) && Number.isFinite(pointer.y)
+ ? { x: pointer.x, y: pointer.y }
+ : { x: toX, y: toY };
+
return resolveCanvasMagnetTarget({
- point: { x: toX, y: toY },
+ point: magnetPoint,
fromNodeId,
fromHandleId: fromHandleId ?? undefined,
fromHandleType,
nodes: getNodes(),
edges: getEdges(),
});
- }, [fromHandleId, fromHandleType, fromNodeId, getEdges, getNodes, toX, toY]);
+ }, [fromHandleId, fromHandleType, fromNodeId, getEdges, getNodes, pointer, toX, toY]);
useEffect(() => {
if (hasSameMagnetTarget(activeTarget, resolvedMagnetTarget)) {
@@ -91,12 +102,21 @@ export default function CustomConnectionLine({
}, [activeTarget, resolvedMagnetTarget, setActiveTarget]);
const magnetTarget = activeTarget ?? resolvedMagnetTarget;
+ const glowStrength = magnetTarget
+ ? resolveCanvasGlowStrength({
+ distancePx: magnetTarget.distancePx,
+ })
+ : 0;
const snappedTarget =
magnetTarget && magnetTarget.distancePx <= HANDLE_SNAP_RADIUS_PX
? magnetTarget
: null;
- const targetX = snappedTarget?.centerX ?? toX;
- const targetY = snappedTarget?.centerY ?? toY;
+ const snappedFlowPoint =
+ snappedTarget === null
+ ? null
+ : screenToFlowPosition({ x: snappedTarget.centerX, y: snappedTarget.centerY });
+ const targetX = snappedFlowPoint?.x ?? toX;
+ const targetY = snappedFlowPoint?.y ?? toY;
const pathParams = {
sourceX: fromX,
@@ -130,10 +150,17 @@ export default function CustomConnectionLine({
const [r, g, b] = connectionLineAccentRgb(fromNode.type, fromHandleId);
const opacity = connectionStatus === "invalid" ? 0.45 : 1;
- const strokeWidth = snappedTarget ? 3.25 : 2.5;
- const filter = snappedTarget
- ? `drop-shadow(0 0 3px rgba(${r}, ${g}, ${b}, 0.7)) drop-shadow(0 0 8px rgba(${r}, ${g}, ${b}, 0.48))`
- : undefined;
+ const colorMode: EdgeGlowColorMode =
+ typeof document !== "undefined" && document.documentElement.classList.contains("dark")
+ ? "dark"
+ : "light";
+ const strokeWidth = 2.5 + glowStrength * 0.75;
+ const filter = connectionLineGlowFilter({
+ nodeType: fromNode.type,
+ handleId: fromHandleId,
+ strength: glowStrength,
+ colorMode,
+ });
return (
= 1) {
+ return 1;
+ }
+ return value;
+}
+
+function lerp(min: number, max: number, t: number): number {
+ return min + (max - min) * t;
+}
+
+export function canvasHandleGlowShadow(args: {
+ nodeType: string | undefined;
+ handleId?: string | null;
+ handleType: "source" | "target";
+ strength: number;
+ colorMode: EdgeGlowColorMode;
+}): string | undefined {
+ const strength = clampUnit(args.strength);
+ if (strength <= 0) {
+ return undefined;
+ }
+
+ const [r, g, b] = canvasHandleAccentRgb(args);
+ const isDark = args.colorMode === "dark";
+
+ const ringAlpha = isDark
+ ? lerp(0.08, 0.3, strength)
+ : lerp(0.06, 0.2, strength);
+ const glowAlpha = isDark
+ ? lerp(0.12, 0.58, strength)
+ : lerp(0.08, 0.34, strength);
+ const ringSize = isDark
+ ? lerp(1.8, 6.4, strength)
+ : lerp(1.5, 5.2, strength);
+ const glowSize = isDark
+ ? lerp(4.5, 15, strength)
+ : lerp(3.5, 12, strength);
+
+ return `0 0 0 ${ringSize.toFixed(2)}px rgba(${r}, ${g}, ${b}, ${ringAlpha.toFixed(3)}), 0 0 ${glowSize.toFixed(2)}px rgba(${r}, ${g}, ${b}, ${glowAlpha.toFixed(3)})`;
+}
+
+export function connectionLineGlowFilter(args: {
+ nodeType: string | undefined;
+ handleId: string | null | undefined;
+ strength: number;
+ colorMode: EdgeGlowColorMode;
+}): string | undefined {
+ const strength = clampUnit(args.strength);
+ if (strength <= 0) {
+ return undefined;
+ }
+
+ const [r, g, b] = connectionLineAccentRgb(args.nodeType, args.handleId);
+ const isDark = args.colorMode === "dark";
+
+ const innerAlpha = isDark
+ ? lerp(0.22, 0.72, strength)
+ : lerp(0.12, 0.42, strength);
+ const outerAlpha = isDark
+ ? lerp(0.12, 0.38, strength)
+ : lerp(0.06, 0.2, strength);
+ const innerBlur = isDark
+ ? lerp(2.4, 4.2, strength)
+ : lerp(2, 3.4, strength);
+ const outerBlur = isDark
+ ? lerp(5.4, 9.8, strength)
+ : lerp(4.6, 7.8, strength);
+
+ return `drop-shadow(0 0 ${innerBlur.toFixed(2)}px rgba(${r}, ${g}, ${b}, ${innerAlpha.toFixed(3)})) drop-shadow(0 0 ${outerBlur.toFixed(2)}px rgba(${r}, ${g}, ${b}, ${outerAlpha.toFixed(3)}))`;
+}
+
/**
* RGB für die temporäre Verbindungslinie (Quell-Node + optional Handle, z. B. Reconnect).
*/