@elabs-ai/components-flow 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/index.d.ts +634 -0
- package/dist/index.js +1577 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/canvas-shell/canvas-shell.stories.tsx +77 -0
- package/src/canvas-shell/canvas-shell.test.tsx +106 -0
- package/src/canvas-shell/canvas-shell.tsx +128 -0
- package/src/canvas-shell/index.ts +1 -0
- package/src/flow-button-edge/flow-button-edge.stories.tsx +151 -0
- package/src/flow-button-edge/flow-button-edge.test.tsx +99 -0
- package/src/flow-button-edge/flow-button-edge.tsx +80 -0
- package/src/flow-button-edge/index.ts +5 -0
- package/src/flow-edge/flow-edge.stories.tsx +79 -0
- package/src/flow-edge/flow-edge.test.tsx +76 -0
- package/src/flow-edge/flow-edge.tsx +34 -0
- package/src/flow-edge/index.ts +1 -0
- package/src/flow-floating-edge/floating-edge-geometry.test.ts +100 -0
- package/src/flow-floating-edge/floating-edge-geometry.ts +128 -0
- package/src/flow-floating-edge/flow-floating-edge.stories.tsx +81 -0
- package/src/flow-floating-edge/flow-floating-edge.tsx +82 -0
- package/src/flow-floating-edge/index.ts +6 -0
- package/src/flow-group-node/flow-group-node.stories.tsx +283 -0
- package/src/flow-group-node/flow-group-node.test.tsx +100 -0
- package/src/flow-group-node/flow-group-node.tsx +134 -0
- package/src/flow-group-node/index.ts +6 -0
- package/src/flow-layout/flow-layout.stories.tsx +215 -0
- package/src/flow-layout/flow-layout.test.tsx +133 -0
- package/src/flow-layout/flow-layout.ts +104 -0
- package/src/flow-layout/index.ts +14 -0
- package/src/flow-layout/layout-graph.test.ts +257 -0
- package/src/flow-layout/layout-graph.ts +302 -0
- package/src/flow-layout/use-auto-layout.ts +26 -0
- package/src/flow-layout/use-flow-layout.ts +60 -0
- package/src/flow-mini-map/flow-mini-map.stories.tsx +74 -0
- package/src/flow-mini-map/flow-mini-map.test.tsx +56 -0
- package/src/flow-mini-map/flow-mini-map.tsx +29 -0
- package/src/flow-mini-map/index.ts +1 -0
- package/src/flow-node/flow-node.stories.tsx +143 -0
- package/src/flow-node/flow-node.test.tsx +156 -0
- package/src/flow-node/flow-node.tsx +190 -0
- package/src/flow-node/index.ts +8 -0
- package/src/flow-placeholder-node/flow-placeholder-node.stories.tsx +260 -0
- package/src/flow-placeholder-node/flow-placeholder-node.test.tsx +101 -0
- package/src/flow-placeholder-node/flow-placeholder-node.tsx +49 -0
- package/src/flow-placeholder-node/index.ts +5 -0
- package/src/flow-smart-edge/flow-smart-edge.stories.tsx +75 -0
- package/src/flow-smart-edge/flow-smart-edge.tsx +75 -0
- package/src/flow-smart-edge/index.ts +9 -0
- package/src/flow-smart-edge/smart-edge-geometry.test.ts +120 -0
- package/src/flow-smart-edge/smart-edge-geometry.ts +116 -0
- package/src/helper-lines/get-helper-lines.ts +98 -0
- package/src/helper-lines/helper-lines.stories.tsx +107 -0
- package/src/helper-lines/helper-lines.test.tsx +78 -0
- package/src/helper-lines/helper-lines.tsx +67 -0
- package/src/helper-lines/index.ts +7 -0
- package/src/helper-lines/use-helper-lines.ts +109 -0
- package/src/index.ts +37 -0
- package/src/inspector-panel/index.ts +1 -0
- package/src/inspector-panel/inspector-panel.stories.tsx +161 -0
- package/src/inspector-panel/inspector-panel.test.tsx +82 -0
- package/src/inspector-panel/inspector-panel.tsx +129 -0
- package/src/legend/index.ts +1 -0
- package/src/legend/legend.stories.tsx +56 -0
- package/src/legend/legend.test.tsx +46 -0
- package/src/legend/legend.tsx +39 -0
- package/src/templates-flow-workspace.stories.tsx +169 -0
- package/src/use-flow-groups/group-operations.test.ts +318 -0
- package/src/use-flow-groups/group-operations.ts +349 -0
- package/src/use-flow-groups/index.ts +13 -0
- package/src/use-flow-groups/use-flow-groups.ts +113 -0
- package/src/zoom-controls/index.ts +1 -0
- package/src/zoom-controls/zoom-controls.stories.tsx +68 -0
- package/src/zoom-controls/zoom-controls.test.tsx +80 -0
- package/src/zoom-controls/zoom-controls.tsx +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/canvas-shell/canvas-shell.tsx
|
|
4
|
+
import "react";
|
|
5
|
+
import {
|
|
6
|
+
Background,
|
|
7
|
+
ReactFlow,
|
|
8
|
+
ReactFlowProvider
|
|
9
|
+
} from "@xyflow/react";
|
|
10
|
+
import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
|
|
11
|
+
|
|
12
|
+
// src/helper-lines/helper-lines.tsx
|
|
13
|
+
import { forwardRef } from "react";
|
|
14
|
+
import { useViewport } from "@xyflow/react";
|
|
15
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
var HelperLines = forwardRef(function HelperLines2({ horizontal, vertical, className, ...props }, ref) {
|
|
18
|
+
const { x, y, zoom } = useViewport();
|
|
19
|
+
if (horizontal == null && vertical == null) return null;
|
|
20
|
+
const screenY = horizontal == null ? null : y + horizontal * zoom;
|
|
21
|
+
const screenX = vertical == null ? null : x + vertical * zoom;
|
|
22
|
+
return /* @__PURE__ */ jsxs(
|
|
23
|
+
"svg",
|
|
24
|
+
{
|
|
25
|
+
ref,
|
|
26
|
+
"aria-hidden": "true",
|
|
27
|
+
width: "100%",
|
|
28
|
+
height: "100%",
|
|
29
|
+
className: cn("pointer-events-none absolute inset-0 size-full", className),
|
|
30
|
+
...props,
|
|
31
|
+
children: [
|
|
32
|
+
screenY != null ? /* @__PURE__ */ jsx(
|
|
33
|
+
"line",
|
|
34
|
+
{
|
|
35
|
+
x1: 0,
|
|
36
|
+
x2: "100%",
|
|
37
|
+
y1: screenY,
|
|
38
|
+
y2: screenY,
|
|
39
|
+
stroke: "var(--flow-helper-line, var(--primary))",
|
|
40
|
+
strokeWidth: 1
|
|
41
|
+
}
|
|
42
|
+
) : null,
|
|
43
|
+
screenX != null ? /* @__PURE__ */ jsx(
|
|
44
|
+
"line",
|
|
45
|
+
{
|
|
46
|
+
x1: screenX,
|
|
47
|
+
x2: screenX,
|
|
48
|
+
y1: 0,
|
|
49
|
+
y2: "100%",
|
|
50
|
+
stroke: "var(--flow-helper-line, var(--primary))",
|
|
51
|
+
strokeWidth: 1
|
|
52
|
+
}
|
|
53
|
+
) : null
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// src/helper-lines/use-helper-lines.ts
|
|
60
|
+
import { useCallback, useState } from "react";
|
|
61
|
+
import { useReactFlow } from "@xyflow/react";
|
|
62
|
+
|
|
63
|
+
// src/helper-lines/get-helper-lines.ts
|
|
64
|
+
var ANCHORS = [0, 0.5, 1];
|
|
65
|
+
function getHelperLines(dragged, others, threshold = 5) {
|
|
66
|
+
const result = {};
|
|
67
|
+
let bestVerticalDist = threshold;
|
|
68
|
+
let bestHorizontalDist = threshold;
|
|
69
|
+
const draggedX = ANCHORS.map((a) => ({ a, pos: dragged.x + dragged.width * a }));
|
|
70
|
+
const draggedY = ANCHORS.map((a) => ({ a, pos: dragged.y + dragged.height * a }));
|
|
71
|
+
for (const other of others) {
|
|
72
|
+
const otherX = ANCHORS.map((a) => other.x + other.width * a);
|
|
73
|
+
const otherY = ANCHORS.map((a) => other.y + other.height * a);
|
|
74
|
+
for (const d of draggedX) {
|
|
75
|
+
for (const oPos of otherX) {
|
|
76
|
+
const dx = Math.abs(d.pos - oPos);
|
|
77
|
+
if (dx < bestVerticalDist) {
|
|
78
|
+
bestVerticalDist = dx;
|
|
79
|
+
result.vertical = oPos;
|
|
80
|
+
result.snapX = oPos - dragged.width * d.a;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const d of draggedY) {
|
|
85
|
+
for (const oPos of otherY) {
|
|
86
|
+
const dy = Math.abs(d.pos - oPos);
|
|
87
|
+
if (dy < bestHorizontalDist) {
|
|
88
|
+
bestHorizontalDist = dy;
|
|
89
|
+
result.horizontal = oPos;
|
|
90
|
+
result.snapY = oPos - dragged.height * d.a;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/helper-lines/use-helper-lines.ts
|
|
99
|
+
function nodeRect(node) {
|
|
100
|
+
return {
|
|
101
|
+
x: node.position.x,
|
|
102
|
+
y: node.position.y,
|
|
103
|
+
width: node.measured?.width ?? node.width ?? 0,
|
|
104
|
+
height: node.measured?.height ?? node.height ?? 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function useHelperLines(onNodesChange, options = {}) {
|
|
108
|
+
const { threshold = 5 } = options;
|
|
109
|
+
const { getNodes } = useReactFlow();
|
|
110
|
+
const [helperLineHorizontal, setHelperLineHorizontal] = useState();
|
|
111
|
+
const [helperLineVertical, setHelperLineVertical] = useState();
|
|
112
|
+
const handleNodesChange = useCallback(
|
|
113
|
+
(changes) => {
|
|
114
|
+
let horizontal;
|
|
115
|
+
let vertical;
|
|
116
|
+
const dragChanges = changes.filter(
|
|
117
|
+
(c) => c.type === "position" && c.dragging === true && c.position != null
|
|
118
|
+
);
|
|
119
|
+
if (dragChanges.length === 1) {
|
|
120
|
+
const change = dragChanges[0];
|
|
121
|
+
const nodes = getNodes();
|
|
122
|
+
const dragged = change ? nodes.find((n) => n.id === change.id) : void 0;
|
|
123
|
+
if (change && change.position && dragged) {
|
|
124
|
+
const draggedRect = {
|
|
125
|
+
x: change.position.x,
|
|
126
|
+
y: change.position.y,
|
|
127
|
+
width: dragged.measured?.width ?? dragged.width ?? 0,
|
|
128
|
+
height: dragged.measured?.height ?? dragged.height ?? 0
|
|
129
|
+
};
|
|
130
|
+
const others = nodes.filter((n) => n.id !== change.id).map(nodeRect);
|
|
131
|
+
const {
|
|
132
|
+
snapX,
|
|
133
|
+
snapY,
|
|
134
|
+
vertical: v,
|
|
135
|
+
horizontal: h
|
|
136
|
+
} = getHelperLines(draggedRect, others, threshold);
|
|
137
|
+
if (snapX !== void 0) change.position.x = snapX;
|
|
138
|
+
if (snapY !== void 0) change.position.y = snapY;
|
|
139
|
+
vertical = v;
|
|
140
|
+
horizontal = h;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
setHelperLineHorizontal(horizontal);
|
|
144
|
+
setHelperLineVertical(vertical);
|
|
145
|
+
onNodesChange?.(changes);
|
|
146
|
+
},
|
|
147
|
+
[getNodes, onNodesChange, threshold]
|
|
148
|
+
);
|
|
149
|
+
return { onNodesChange: handleNodesChange, helperLineHorizontal, helperLineVertical };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/canvas-shell/canvas-shell.tsx
|
|
153
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
154
|
+
var DEFAULT_ARIA_LABEL_CONFIG = {
|
|
155
|
+
"node.a11yDescription.default": "Press Enter or Space to select this node. Press Delete to remove it, Escape to cancel.",
|
|
156
|
+
"node.a11yDescription.keyboardDisabled": "Press Enter or Space to select this node. Then use the arrow keys to move it. Press Delete to remove it, Escape to cancel.",
|
|
157
|
+
"edge.a11yDescription.default": "Press Enter or Space to select this edge. Press Delete to remove it, Escape to cancel.",
|
|
158
|
+
"controls.ariaLabel": "Canvas controls",
|
|
159
|
+
"controls.zoomIn.ariaLabel": "Zoom in",
|
|
160
|
+
"controls.zoomOut.ariaLabel": "Zoom out",
|
|
161
|
+
"controls.fitView.ariaLabel": "Fit view",
|
|
162
|
+
"controls.interactive.ariaLabel": "Toggle interactivity",
|
|
163
|
+
"minimap.ariaLabel": "Minimap",
|
|
164
|
+
"handle.ariaLabel": "Connection handle"
|
|
165
|
+
};
|
|
166
|
+
function CanvasShell({
|
|
167
|
+
helperLines = false,
|
|
168
|
+
...props
|
|
169
|
+
}) {
|
|
170
|
+
if (helperLines) {
|
|
171
|
+
return /* @__PURE__ */ jsx2(ReactFlowProvider, { children: /* @__PURE__ */ jsx2(CanvasShellWithHelperLines, { ...props }) });
|
|
172
|
+
}
|
|
173
|
+
return /* @__PURE__ */ jsx2(CanvasShellBase, { ...props });
|
|
174
|
+
}
|
|
175
|
+
function CanvasShellBase({
|
|
176
|
+
background = true,
|
|
177
|
+
children,
|
|
178
|
+
className,
|
|
179
|
+
ariaLabelConfig,
|
|
180
|
+
...props
|
|
181
|
+
}) {
|
|
182
|
+
return /* @__PURE__ */ jsx2("div", { className: cn2("h-full w-full bg-canvas", className), children: /* @__PURE__ */ jsxs2(
|
|
183
|
+
ReactFlow,
|
|
184
|
+
{
|
|
185
|
+
fitView: true,
|
|
186
|
+
proOptions: { hideAttribution: true },
|
|
187
|
+
ariaLabelConfig: { ...DEFAULT_ARIA_LABEL_CONFIG, ...ariaLabelConfig },
|
|
188
|
+
...props,
|
|
189
|
+
children: [
|
|
190
|
+
background ? /* @__PURE__ */ jsx2(Background, { gap: 20, size: 1, color: "var(--canvas-grid)" }) : null,
|
|
191
|
+
children
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
) });
|
|
195
|
+
}
|
|
196
|
+
function CanvasShellWithHelperLines({
|
|
197
|
+
onNodesChange,
|
|
198
|
+
children,
|
|
199
|
+
...props
|
|
200
|
+
}) {
|
|
201
|
+
const {
|
|
202
|
+
onNodesChange: wrappedOnNodesChange,
|
|
203
|
+
helperLineHorizontal,
|
|
204
|
+
helperLineVertical
|
|
205
|
+
} = useHelperLines(onNodesChange);
|
|
206
|
+
return /* @__PURE__ */ jsxs2(CanvasShellBase, { ...props, onNodesChange: wrappedOnNodesChange, children: [
|
|
207
|
+
children,
|
|
208
|
+
/* @__PURE__ */ jsx2(HelperLines, { horizontal: helperLineHorizontal, vertical: helperLineVertical })
|
|
209
|
+
] });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/flow-node/flow-node.tsx
|
|
213
|
+
import "react";
|
|
214
|
+
import { Handle, Position } from "@xyflow/react";
|
|
215
|
+
import { Star } from "lucide-react";
|
|
216
|
+
import { STATUS_TONE_ICONS } from "@elabs-ai/components-ui";
|
|
217
|
+
import { cn as cn3 } from "@elabs-ai/components-ui/lib/cn";
|
|
218
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
219
|
+
var FLOW_ALL_SIDE_HANDLES = {
|
|
220
|
+
source: ["top", "right", "bottom", "left"],
|
|
221
|
+
target: ["top", "right", "bottom", "left"]
|
|
222
|
+
};
|
|
223
|
+
var toneRing = {
|
|
224
|
+
default: "border-border",
|
|
225
|
+
accent: "border-primary",
|
|
226
|
+
success: "border-success",
|
|
227
|
+
warning: "border-warning",
|
|
228
|
+
destructive: "border-destructive"
|
|
229
|
+
};
|
|
230
|
+
var toneIcon = {
|
|
231
|
+
accent: Star,
|
|
232
|
+
success: STATUS_TONE_ICONS.success,
|
|
233
|
+
warning: STATUS_TONE_ICONS.warning,
|
|
234
|
+
destructive: STATUS_TONE_ICONS.destructive
|
|
235
|
+
};
|
|
236
|
+
var toneIconColor = {
|
|
237
|
+
accent: "text-primary",
|
|
238
|
+
success: "text-success-text",
|
|
239
|
+
warning: "text-warning-text",
|
|
240
|
+
destructive: "text-destructive-text"
|
|
241
|
+
};
|
|
242
|
+
var toneLabel = {
|
|
243
|
+
accent: "Highlighted",
|
|
244
|
+
success: "Success",
|
|
245
|
+
warning: "Warning",
|
|
246
|
+
destructive: "Destructive"
|
|
247
|
+
};
|
|
248
|
+
var sidePosition = {
|
|
249
|
+
top: Position.Top,
|
|
250
|
+
right: Position.Right,
|
|
251
|
+
bottom: Position.Bottom,
|
|
252
|
+
left: Position.Left
|
|
253
|
+
};
|
|
254
|
+
var handleClassName = "!size-2 !border-2 !border-flow-edge !bg-flow-node";
|
|
255
|
+
function FlowNode({
|
|
256
|
+
data,
|
|
257
|
+
selected,
|
|
258
|
+
sourcePosition,
|
|
259
|
+
targetPosition
|
|
260
|
+
}) {
|
|
261
|
+
const tone = data.tone ?? "default";
|
|
262
|
+
const ToneIcon = toneIcon[tone];
|
|
263
|
+
return /* @__PURE__ */ jsxs3(
|
|
264
|
+
"div",
|
|
265
|
+
{
|
|
266
|
+
"data-tone": tone,
|
|
267
|
+
className: cn3(
|
|
268
|
+
"min-w-44 rounded-lg border bg-flow-node px-3 py-2 text-flow-node-foreground shadow-sm transition-[box-shadow,border-color] duration-fast ease-standard",
|
|
269
|
+
toneRing[tone],
|
|
270
|
+
selected && "ring-2 ring-ring"
|
|
271
|
+
),
|
|
272
|
+
children: [
|
|
273
|
+
data.handles ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
274
|
+
(data.handles.target ?? []).map((side) => /* @__PURE__ */ jsx3(
|
|
275
|
+
Handle,
|
|
276
|
+
{
|
|
277
|
+
id: side,
|
|
278
|
+
type: "target",
|
|
279
|
+
position: sidePosition[side],
|
|
280
|
+
className: handleClassName
|
|
281
|
+
},
|
|
282
|
+
`target-${side}`
|
|
283
|
+
)),
|
|
284
|
+
(data.handles.source ?? []).map((side) => /* @__PURE__ */ jsx3(
|
|
285
|
+
Handle,
|
|
286
|
+
{
|
|
287
|
+
id: side,
|
|
288
|
+
type: "source",
|
|
289
|
+
position: sidePosition[side],
|
|
290
|
+
className: handleClassName
|
|
291
|
+
},
|
|
292
|
+
`source-${side}`
|
|
293
|
+
))
|
|
294
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
295
|
+
/* @__PURE__ */ jsx3(
|
|
296
|
+
Handle,
|
|
297
|
+
{
|
|
298
|
+
type: "target",
|
|
299
|
+
position: targetPosition ?? Position.Top,
|
|
300
|
+
className: handleClassName
|
|
301
|
+
}
|
|
302
|
+
),
|
|
303
|
+
/* @__PURE__ */ jsx3(
|
|
304
|
+
Handle,
|
|
305
|
+
{
|
|
306
|
+
type: "source",
|
|
307
|
+
position: sourcePosition ?? Position.Bottom,
|
|
308
|
+
className: handleClassName
|
|
309
|
+
}
|
|
310
|
+
)
|
|
311
|
+
] }),
|
|
312
|
+
/* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
|
|
313
|
+
data.icon ? /* @__PURE__ */ jsx3("span", { className: "[&_svg]:size-4 text-muted-foreground", children: data.icon }) : null,
|
|
314
|
+
/* @__PURE__ */ jsxs3("div", { className: "min-w-0 flex-1", children: [
|
|
315
|
+
data.kind ? /* @__PURE__ */ jsx3("div", { className: "text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: data.kind }) : null,
|
|
316
|
+
/* @__PURE__ */ jsx3("div", { className: "truncate text-sm font-medium", children: data.title }),
|
|
317
|
+
data.subtitle ? /* @__PURE__ */ jsx3("div", { className: "truncate text-xs text-muted-foreground", children: data.subtitle }) : null
|
|
318
|
+
] }),
|
|
319
|
+
ToneIcon ? /* @__PURE__ */ jsx3(ToneIcon, { "aria-hidden": "true", className: cn3("size-3.5 shrink-0", toneIconColor[tone]) }) : null
|
|
320
|
+
] }),
|
|
321
|
+
toneLabel[tone] ? /* @__PURE__ */ jsx3("span", { className: "sr-only", children: toneLabel[tone] }) : null
|
|
322
|
+
]
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/flow-edge/flow-edge.tsx
|
|
328
|
+
import { BaseEdge, getBezierPath } from "@xyflow/react";
|
|
329
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
330
|
+
function FlowEdge({
|
|
331
|
+
id,
|
|
332
|
+
sourceX,
|
|
333
|
+
sourceY,
|
|
334
|
+
targetX,
|
|
335
|
+
targetY,
|
|
336
|
+
sourcePosition,
|
|
337
|
+
targetPosition,
|
|
338
|
+
markerEnd,
|
|
339
|
+
style
|
|
340
|
+
}) {
|
|
341
|
+
const [edgePath] = getBezierPath({
|
|
342
|
+
sourceX,
|
|
343
|
+
sourceY,
|
|
344
|
+
targetX,
|
|
345
|
+
targetY,
|
|
346
|
+
sourcePosition,
|
|
347
|
+
targetPosition
|
|
348
|
+
});
|
|
349
|
+
return /* @__PURE__ */ jsx4(
|
|
350
|
+
BaseEdge,
|
|
351
|
+
{
|
|
352
|
+
id,
|
|
353
|
+
path: edgePath,
|
|
354
|
+
markerEnd,
|
|
355
|
+
style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/flow-smart-edge/flow-smart-edge.tsx
|
|
361
|
+
import {
|
|
362
|
+
BaseEdge as BaseEdge2,
|
|
363
|
+
getBezierPath as getBezierPath2,
|
|
364
|
+
useInternalNode
|
|
365
|
+
} from "@xyflow/react";
|
|
366
|
+
|
|
367
|
+
// src/flow-smart-edge/smart-edge-geometry.ts
|
|
368
|
+
import { Position as Position2 } from "@xyflow/react";
|
|
369
|
+
var HANDLE_SIDES = ["top", "right", "bottom", "left"];
|
|
370
|
+
var sideToPosition = {
|
|
371
|
+
top: Position2.Top,
|
|
372
|
+
right: Position2.Right,
|
|
373
|
+
bottom: Position2.Bottom,
|
|
374
|
+
left: Position2.Left
|
|
375
|
+
};
|
|
376
|
+
function rectCenter(rect) {
|
|
377
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
|
|
378
|
+
}
|
|
379
|
+
function slideAnchor(rect, side, toward, inset = 12) {
|
|
380
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
381
|
+
const ix = Math.min(inset, rect.width / 2);
|
|
382
|
+
const iy = Math.min(inset, rect.height / 2);
|
|
383
|
+
switch (side) {
|
|
384
|
+
case "top":
|
|
385
|
+
return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y };
|
|
386
|
+
case "bottom":
|
|
387
|
+
return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y + rect.height };
|
|
388
|
+
case "left":
|
|
389
|
+
return { x: rect.x, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };
|
|
390
|
+
case "right":
|
|
391
|
+
return { x: rect.x + rect.width, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function handlePoint(rect, side) {
|
|
395
|
+
switch (side) {
|
|
396
|
+
case "top":
|
|
397
|
+
return { x: rect.x + rect.width / 2, y: rect.y };
|
|
398
|
+
case "bottom":
|
|
399
|
+
return { x: rect.x + rect.width / 2, y: rect.y + rect.height };
|
|
400
|
+
case "left":
|
|
401
|
+
return { x: rect.x, y: rect.y + rect.height / 2 };
|
|
402
|
+
case "right":
|
|
403
|
+
return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function pickClosestHandles(source, sourceSides, target, targetSides) {
|
|
407
|
+
const srcSides = sourceSides.length ? sourceSides : HANDLE_SIDES;
|
|
408
|
+
const tgtSides = targetSides.length ? targetSides : HANDLE_SIDES;
|
|
409
|
+
let best;
|
|
410
|
+
let bestDist = Infinity;
|
|
411
|
+
for (const s of srcSides) {
|
|
412
|
+
const sp = handlePoint(source, s);
|
|
413
|
+
for (const t of tgtSides) {
|
|
414
|
+
const tp = handlePoint(target, t);
|
|
415
|
+
const dx = tp.x - sp.x;
|
|
416
|
+
const dy = tp.y - sp.y;
|
|
417
|
+
const dist = dx * dx + dy * dy;
|
|
418
|
+
if (dist < bestDist) {
|
|
419
|
+
bestDist = dist;
|
|
420
|
+
best = { sourceSide: s, targetSide: t, sx: sp.x, sy: sp.y, tx: tp.x, ty: tp.y };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return best;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// src/flow-smart-edge/flow-smart-edge.tsx
|
|
428
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
429
|
+
function toRect(node) {
|
|
430
|
+
return {
|
|
431
|
+
x: node.internals.positionAbsolute.x,
|
|
432
|
+
y: node.internals.positionAbsolute.y,
|
|
433
|
+
width: node.measured.width ?? 0,
|
|
434
|
+
height: node.measured.height ?? 0
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function FlowSmartEdge({ id, source, target, markerEnd, style }) {
|
|
438
|
+
const sourceNode = useInternalNode(source);
|
|
439
|
+
const targetNode = useInternalNode(target);
|
|
440
|
+
if (!sourceNode || !targetNode) return null;
|
|
441
|
+
const sourceData = sourceNode.internals.userNode.data;
|
|
442
|
+
const targetData = targetNode.internals.userNode.data;
|
|
443
|
+
const sourceRect = toRect(sourceNode);
|
|
444
|
+
const targetRect = toRect(targetNode);
|
|
445
|
+
const { sourceSide, targetSide } = pickClosestHandles(
|
|
446
|
+
sourceRect,
|
|
447
|
+
sourceData?.handles?.source ?? [],
|
|
448
|
+
targetRect,
|
|
449
|
+
targetData?.handles?.target ?? []
|
|
450
|
+
);
|
|
451
|
+
const sourceAnchor = slideAnchor(sourceRect, sourceSide, rectCenter(targetRect));
|
|
452
|
+
const targetAnchor = slideAnchor(targetRect, targetSide, rectCenter(sourceRect));
|
|
453
|
+
const [edgePath] = getBezierPath2({
|
|
454
|
+
sourceX: sourceAnchor.x,
|
|
455
|
+
sourceY: sourceAnchor.y,
|
|
456
|
+
sourcePosition: sideToPosition[sourceSide],
|
|
457
|
+
targetX: targetAnchor.x,
|
|
458
|
+
targetY: targetAnchor.y,
|
|
459
|
+
targetPosition: sideToPosition[targetSide]
|
|
460
|
+
});
|
|
461
|
+
return /* @__PURE__ */ jsx5(
|
|
462
|
+
BaseEdge2,
|
|
463
|
+
{
|
|
464
|
+
id,
|
|
465
|
+
path: edgePath,
|
|
466
|
+
markerEnd,
|
|
467
|
+
style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
|
|
468
|
+
}
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/flow-floating-edge/flow-floating-edge.tsx
|
|
473
|
+
import { BaseEdge as BaseEdge3, getBezierPath as getBezierPath3, useInternalNode as useInternalNode2 } from "@xyflow/react";
|
|
474
|
+
|
|
475
|
+
// src/flow-floating-edge/floating-edge-geometry.ts
|
|
476
|
+
import { Position as Position3 } from "@xyflow/react";
|
|
477
|
+
function getNodeIntersection(intersectionNode, targetNode) {
|
|
478
|
+
const w = (intersectionNode.measured.width ?? 0) / 2;
|
|
479
|
+
const h = (intersectionNode.measured.height ?? 0) / 2;
|
|
480
|
+
const pos = intersectionNode.internals.positionAbsolute;
|
|
481
|
+
const targetPos = targetNode.internals.positionAbsolute;
|
|
482
|
+
const x2 = pos.x + w;
|
|
483
|
+
const y2 = pos.y + h;
|
|
484
|
+
const x1 = targetPos.x + (targetNode.measured.width ?? 0) / 2;
|
|
485
|
+
const y1 = targetPos.y + (targetNode.measured.height ?? 0) / 2;
|
|
486
|
+
const safeW = w || 1;
|
|
487
|
+
const safeH = h || 1;
|
|
488
|
+
const xx1 = (x1 - x2) / (2 * safeW) - (y1 - y2) / (2 * safeH);
|
|
489
|
+
const yy1 = (x1 - x2) / (2 * safeW) + (y1 - y2) / (2 * safeH);
|
|
490
|
+
const a = 1 / (Math.abs(xx1) + Math.abs(yy1) || 1);
|
|
491
|
+
const xx3 = a * xx1;
|
|
492
|
+
const yy3 = a * yy1;
|
|
493
|
+
const x = safeW * (xx3 + yy3) + x2;
|
|
494
|
+
const y = safeH * (-xx3 + yy3) + y2;
|
|
495
|
+
return { x, y };
|
|
496
|
+
}
|
|
497
|
+
function clampToBorder(point, node) {
|
|
498
|
+
const x0 = node.internals.positionAbsolute.x;
|
|
499
|
+
const y0 = node.internals.positionAbsolute.y;
|
|
500
|
+
const w = node.measured.width ?? 0;
|
|
501
|
+
const h = node.measured.height ?? 0;
|
|
502
|
+
if (w <= 0 || h <= 0) return point;
|
|
503
|
+
const cx = Math.min(Math.max(point.x, x0), x0 + w);
|
|
504
|
+
const cy = Math.min(Math.max(point.y, y0), y0 + h);
|
|
505
|
+
const dLeft = cx - x0;
|
|
506
|
+
const dRight = x0 + w - cx;
|
|
507
|
+
const dTop = cy - y0;
|
|
508
|
+
const dBottom = y0 + h - cy;
|
|
509
|
+
const nearest = Math.min(dLeft, dRight, dTop, dBottom);
|
|
510
|
+
if (nearest === dLeft) return { x: x0, y: cy };
|
|
511
|
+
if (nearest === dRight) return { x: x0 + w, y: cy };
|
|
512
|
+
if (nearest === dTop) return { x: cx, y: y0 };
|
|
513
|
+
return { x: cx, y: y0 + h };
|
|
514
|
+
}
|
|
515
|
+
function getEdgeBorderPosition(node, point) {
|
|
516
|
+
const nx = Math.round(node.internals.positionAbsolute.x);
|
|
517
|
+
const ny = Math.round(node.internals.positionAbsolute.y);
|
|
518
|
+
const px = Math.round(point.x);
|
|
519
|
+
const py = Math.round(point.y);
|
|
520
|
+
const w = node.measured.width ?? 0;
|
|
521
|
+
const h = node.measured.height ?? 0;
|
|
522
|
+
if (px <= nx + 1) return Position3.Left;
|
|
523
|
+
if (px >= nx + w - 1) return Position3.Right;
|
|
524
|
+
if (py <= ny + 1) return Position3.Top;
|
|
525
|
+
if (py >= ny + h - 1) return Position3.Bottom;
|
|
526
|
+
return Position3.Top;
|
|
527
|
+
}
|
|
528
|
+
function getEdgeParams(source, target) {
|
|
529
|
+
const sourceIntersection = clampToBorder(getNodeIntersection(source, target), source);
|
|
530
|
+
const targetIntersection = clampToBorder(getNodeIntersection(target, source), target);
|
|
531
|
+
return {
|
|
532
|
+
sx: sourceIntersection.x,
|
|
533
|
+
sy: sourceIntersection.y,
|
|
534
|
+
tx: targetIntersection.x,
|
|
535
|
+
ty: targetIntersection.y,
|
|
536
|
+
sourcePos: getEdgeBorderPosition(source, sourceIntersection),
|
|
537
|
+
targetPos: getEdgeBorderPosition(target, targetIntersection)
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/flow-floating-edge/flow-floating-edge.tsx
|
|
542
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
543
|
+
var ANCHOR_RADIUS = 4;
|
|
544
|
+
function FlowFloatingEdge({ id, source, target, markerEnd, style, data }) {
|
|
545
|
+
const sourceNode = useInternalNode2(source);
|
|
546
|
+
const targetNode = useInternalNode2(target);
|
|
547
|
+
if (!sourceNode || !targetNode) return null;
|
|
548
|
+
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
|
549
|
+
const [edgePath] = getBezierPath3({
|
|
550
|
+
sourceX: sx,
|
|
551
|
+
sourceY: sy,
|
|
552
|
+
sourcePosition: sourcePos,
|
|
553
|
+
targetX: tx,
|
|
554
|
+
targetY: ty,
|
|
555
|
+
targetPosition: targetPos
|
|
556
|
+
});
|
|
557
|
+
const showAnchors = data?.anchors !== false;
|
|
558
|
+
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
559
|
+
/* @__PURE__ */ jsx6(
|
|
560
|
+
BaseEdge3,
|
|
561
|
+
{
|
|
562
|
+
id,
|
|
563
|
+
path: edgePath,
|
|
564
|
+
markerEnd,
|
|
565
|
+
style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
|
|
566
|
+
}
|
|
567
|
+
),
|
|
568
|
+
showAnchors ? /* @__PURE__ */ jsxs4(
|
|
569
|
+
"g",
|
|
570
|
+
{
|
|
571
|
+
className: "brand-floating-edge__anchors",
|
|
572
|
+
fill: "var(--flow-node)",
|
|
573
|
+
stroke: "var(--flow-edge)",
|
|
574
|
+
children: [
|
|
575
|
+
/* @__PURE__ */ jsx6(
|
|
576
|
+
"circle",
|
|
577
|
+
{
|
|
578
|
+
cx: sx,
|
|
579
|
+
cy: sy,
|
|
580
|
+
r: ANCHOR_RADIUS,
|
|
581
|
+
strokeWidth: 2,
|
|
582
|
+
style: { pointerEvents: "none" },
|
|
583
|
+
"aria-hidden": "true"
|
|
584
|
+
}
|
|
585
|
+
),
|
|
586
|
+
/* @__PURE__ */ jsx6(
|
|
587
|
+
"circle",
|
|
588
|
+
{
|
|
589
|
+
cx: tx,
|
|
590
|
+
cy: ty,
|
|
591
|
+
r: ANCHOR_RADIUS,
|
|
592
|
+
strokeWidth: 2,
|
|
593
|
+
style: { pointerEvents: "none" },
|
|
594
|
+
"aria-hidden": "true"
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
]
|
|
598
|
+
}
|
|
599
|
+
) : null
|
|
600
|
+
] });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/flow-placeholder-node/flow-placeholder-node.tsx
|
|
604
|
+
import { Handle as Handle2, Position as Position4 } from "@xyflow/react";
|
|
605
|
+
import { Plus } from "lucide-react";
|
|
606
|
+
import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
|
|
607
|
+
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
608
|
+
function FlowPlaceholderNode({ data }) {
|
|
609
|
+
const label = data.label ?? "Add node";
|
|
610
|
+
return /* @__PURE__ */ jsxs5("div", { className: "relative", children: [
|
|
611
|
+
/* @__PURE__ */ jsx7(
|
|
612
|
+
Handle2,
|
|
613
|
+
{
|
|
614
|
+
type: "target",
|
|
615
|
+
position: Position4.Top,
|
|
616
|
+
className: "!size-2 !border-2 !border-flow-edge !bg-flow-node"
|
|
617
|
+
}
|
|
618
|
+
),
|
|
619
|
+
/* @__PURE__ */ jsxs5(
|
|
620
|
+
"button",
|
|
621
|
+
{
|
|
622
|
+
type: "button",
|
|
623
|
+
"aria-label": label,
|
|
624
|
+
onClick: () => data.onActivate?.(),
|
|
625
|
+
className: cn4(
|
|
626
|
+
"flex min-w-44 items-center justify-center gap-1.5 rounded-lg border border-dashed border-flow-group-border bg-flow-group px-3 py-2 text-muted-foreground",
|
|
627
|
+
"transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground",
|
|
628
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
629
|
+
),
|
|
630
|
+
children: [
|
|
631
|
+
/* @__PURE__ */ jsx7(Plus, { className: "size-4", "aria-hidden": "true" }),
|
|
632
|
+
/* @__PURE__ */ jsx7("span", { className: "text-body", children: label })
|
|
633
|
+
]
|
|
634
|
+
}
|
|
635
|
+
)
|
|
636
|
+
] });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/flow-button-edge/flow-button-edge.tsx
|
|
640
|
+
import {
|
|
641
|
+
BaseEdge as BaseEdge4,
|
|
642
|
+
EdgeLabelRenderer,
|
|
643
|
+
getBezierPath as getBezierPath4
|
|
644
|
+
} from "@xyflow/react";
|
|
645
|
+
import { Plus as Plus2 } from "lucide-react";
|
|
646
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
647
|
+
function FlowButtonEdge({
|
|
648
|
+
id,
|
|
649
|
+
sourceX,
|
|
650
|
+
sourceY,
|
|
651
|
+
targetX,
|
|
652
|
+
targetY,
|
|
653
|
+
sourcePosition,
|
|
654
|
+
targetPosition,
|
|
655
|
+
markerEnd,
|
|
656
|
+
style,
|
|
657
|
+
data
|
|
658
|
+
}) {
|
|
659
|
+
const [edgePath, labelX, labelY] = getBezierPath4({
|
|
660
|
+
sourceX,
|
|
661
|
+
sourceY,
|
|
662
|
+
sourcePosition,
|
|
663
|
+
targetX,
|
|
664
|
+
targetY,
|
|
665
|
+
targetPosition
|
|
666
|
+
});
|
|
667
|
+
const label = data?.label ?? "Insert node on edge";
|
|
668
|
+
return /* @__PURE__ */ jsxs6(Fragment3, { children: [
|
|
669
|
+
/* @__PURE__ */ jsx8(
|
|
670
|
+
BaseEdge4,
|
|
671
|
+
{
|
|
672
|
+
id,
|
|
673
|
+
path: edgePath,
|
|
674
|
+
markerEnd,
|
|
675
|
+
style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
|
|
676
|
+
}
|
|
677
|
+
),
|
|
678
|
+
/* @__PURE__ */ jsx8(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx8(
|
|
679
|
+
"div",
|
|
680
|
+
{
|
|
681
|
+
style: {
|
|
682
|
+
position: "absolute",
|
|
683
|
+
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`
|
|
684
|
+
},
|
|
685
|
+
className: "nodrag nopan pointer-events-auto",
|
|
686
|
+
children: /* @__PURE__ */ jsx8(
|
|
687
|
+
"button",
|
|
688
|
+
{
|
|
689
|
+
type: "button",
|
|
690
|
+
"aria-label": label,
|
|
691
|
+
onClick: () => data?.onInsert?.(),
|
|
692
|
+
className: "flex size-5 items-center justify-center rounded-full border border-flow-group-border bg-flow-node text-flow-node-foreground shadow-sm transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
693
|
+
children: /* @__PURE__ */ jsx8(Plus2, { className: "size-3", "aria-hidden": "true" })
|
|
694
|
+
}
|
|
695
|
+
)
|
|
696
|
+
}
|
|
697
|
+
) })
|
|
698
|
+
] });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// src/flow-layout/flow-layout.ts
|
|
702
|
+
import dagre from "@dagrejs/dagre";
|
|
703
|
+
import { Position as Position5 } from "@xyflow/react";
|
|
704
|
+
var HANDLE_BY_DIRECTION = {
|
|
705
|
+
TB: { source: Position5.Bottom, target: Position5.Top },
|
|
706
|
+
BT: { source: Position5.Top, target: Position5.Bottom },
|
|
707
|
+
LR: { source: Position5.Right, target: Position5.Left },
|
|
708
|
+
RL: { source: Position5.Left, target: Position5.Right }
|
|
709
|
+
};
|
|
710
|
+
var DEFAULT_NODE_WIDTH = 172;
|
|
711
|
+
var DEFAULT_NODE_HEIGHT = 40;
|
|
712
|
+
function nodeSize(node) {
|
|
713
|
+
return {
|
|
714
|
+
width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH,
|
|
715
|
+
height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
function layoutFlow(nodes, edges, options = {}) {
|
|
719
|
+
const { direction = "TB", nodeSpacing = 48, rankSpacing = 72 } = options;
|
|
720
|
+
const graph = new dagre.graphlib.Graph();
|
|
721
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
722
|
+
graph.setGraph({ rankdir: direction, nodesep: nodeSpacing, ranksep: rankSpacing });
|
|
723
|
+
for (const node of nodes) {
|
|
724
|
+
const { width, height } = nodeSize(node);
|
|
725
|
+
graph.setNode(node.id, { width, height });
|
|
726
|
+
}
|
|
727
|
+
for (const edge of edges) {
|
|
728
|
+
graph.setEdge(edge.source, edge.target);
|
|
729
|
+
}
|
|
730
|
+
dagre.layout(graph);
|
|
731
|
+
const handles = HANDLE_BY_DIRECTION[direction];
|
|
732
|
+
const layoutedNodes = nodes.map((node) => {
|
|
733
|
+
const dagreNode = graph.node(node.id);
|
|
734
|
+
const { width, height } = nodeSize(node);
|
|
735
|
+
return {
|
|
736
|
+
...node,
|
|
737
|
+
// Move the anchors with the layout direction so `FlowNode` puts its
|
|
738
|
+
// handles on the sides the edges actually leave/enter.
|
|
739
|
+
sourcePosition: handles.source,
|
|
740
|
+
targetPosition: handles.target,
|
|
741
|
+
position: {
|
|
742
|
+
x: dagreNode.x - width / 2,
|
|
743
|
+
y: dagreNode.y - height / 2
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
});
|
|
747
|
+
return { nodes: layoutedNodes, edges };
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/flow-layout/use-flow-layout.ts
|
|
751
|
+
import { useCallback as useCallback2, useRef, useState as useState2 } from "react";
|
|
752
|
+
import { useNodesInitialized, useReactFlow as useReactFlow2 } from "@xyflow/react";
|
|
753
|
+
function useFlowLayout() {
|
|
754
|
+
const { getNodes, getEdges, setNodes, fitView } = useReactFlow2();
|
|
755
|
+
const nodesInitialized = useNodesInitialized();
|
|
756
|
+
const [layouting, setLayouting] = useState2(false);
|
|
757
|
+
const runningRef = useRef(false);
|
|
758
|
+
const layout = useCallback2(
|
|
759
|
+
(direction, options) => {
|
|
760
|
+
if (!nodesInitialized || runningRef.current) return;
|
|
761
|
+
runningRef.current = true;
|
|
762
|
+
setLayouting(true);
|
|
763
|
+
try {
|
|
764
|
+
const { nodes: layoutedNodes } = layoutFlow(getNodes(), getEdges(), {
|
|
765
|
+
...options,
|
|
766
|
+
direction
|
|
767
|
+
});
|
|
768
|
+
setNodes(layoutedNodes);
|
|
769
|
+
requestAnimationFrame(() => fitView());
|
|
770
|
+
} finally {
|
|
771
|
+
runningRef.current = false;
|
|
772
|
+
setLayouting(false);
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
[nodesInitialized, getNodes, getEdges, setNodes, fitView]
|
|
776
|
+
);
|
|
777
|
+
return { layout, ready: nodesInitialized, layouting };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/flow-layout/layout-graph.ts
|
|
781
|
+
import {
|
|
782
|
+
forceCenter,
|
|
783
|
+
forceCollide,
|
|
784
|
+
forceLink,
|
|
785
|
+
forceManyBody,
|
|
786
|
+
forceSimulation
|
|
787
|
+
} from "d3-force";
|
|
788
|
+
var DEFAULT_NODE_WIDTH2 = 172;
|
|
789
|
+
var DEFAULT_NODE_HEIGHT2 = 40;
|
|
790
|
+
var DEFAULT_RING_RADIUS = 180;
|
|
791
|
+
var DEFAULT_FORCE_ITERATIONS = 300;
|
|
792
|
+
var DEFAULT_GRID_SPACING = { x: 200, y: 120 };
|
|
793
|
+
var FORCE_CHARGE_STRENGTH = -300;
|
|
794
|
+
var FORCE_LINK_DISTANCE = 150;
|
|
795
|
+
function resolveNodeSize(node, nodeSize2) {
|
|
796
|
+
if (nodeSize2) return nodeSize2(node.id);
|
|
797
|
+
return {
|
|
798
|
+
width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH2,
|
|
799
|
+
height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT2
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function layoutGraph(nodes, edges, options) {
|
|
803
|
+
if (nodes.length === 0) return [];
|
|
804
|
+
switch (options.algorithm) {
|
|
805
|
+
case "layered-lr":
|
|
806
|
+
return layoutLayeredLr(nodes, edges, options);
|
|
807
|
+
case "concentric":
|
|
808
|
+
return layoutConcentric(nodes, edges, options);
|
|
809
|
+
case "force":
|
|
810
|
+
return layoutForce(nodes, edges, options);
|
|
811
|
+
case "grid":
|
|
812
|
+
return layoutGrid(nodes, options);
|
|
813
|
+
default: {
|
|
814
|
+
const exhaustiveCheck = options.algorithm;
|
|
815
|
+
throw new Error(`layoutGraph: unknown algorithm "${String(exhaustiveCheck)}"`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function layoutLayeredLr(nodes, edges, options) {
|
|
820
|
+
const { spacing, nodeSize: nodeSize2 } = options;
|
|
821
|
+
const sizedNodes = nodeSize2 ? nodes.map((node) => {
|
|
822
|
+
const { width, height } = nodeSize2(node.id);
|
|
823
|
+
return { ...node, width, height, measured: { width, height } };
|
|
824
|
+
}) : nodes;
|
|
825
|
+
const { nodes: laidOut } = layoutFlow(sizedNodes, edges, {
|
|
826
|
+
direction: "LR",
|
|
827
|
+
// For an LR flow, ranks progress horizontally (x) and nodes within a rank
|
|
828
|
+
// stack vertically (y) — map spacing accordingly onto dagre's rank/node sep.
|
|
829
|
+
nodeSpacing: spacing?.y,
|
|
830
|
+
rankSpacing: spacing?.x
|
|
831
|
+
});
|
|
832
|
+
return nodes.map((node, i) => ({
|
|
833
|
+
...node,
|
|
834
|
+
sourcePosition: laidOut[i].sourcePosition,
|
|
835
|
+
targetPosition: laidOut[i].targetPosition,
|
|
836
|
+
position: laidOut[i].position
|
|
837
|
+
}));
|
|
838
|
+
}
|
|
839
|
+
function resolveCenterId(centerId, nodeIds, adjacency) {
|
|
840
|
+
if (centerId && nodeIds.includes(centerId)) return centerId;
|
|
841
|
+
let best = nodeIds[0];
|
|
842
|
+
let bestDegree = -1;
|
|
843
|
+
for (const id of nodeIds) {
|
|
844
|
+
const degree = adjacency.get(id)?.size ?? 0;
|
|
845
|
+
if (degree > bestDegree) {
|
|
846
|
+
bestDegree = degree;
|
|
847
|
+
best = id;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return best;
|
|
851
|
+
}
|
|
852
|
+
function layoutConcentric(nodes, edges, options) {
|
|
853
|
+
const ringRadius = options.ringRadius ?? DEFAULT_RING_RADIUS;
|
|
854
|
+
const nodeIds = nodes.map((node) => node.id);
|
|
855
|
+
const idSet = new Set(nodeIds);
|
|
856
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const id of nodeIds) adjacency.set(id, /* @__PURE__ */ new Set());
|
|
858
|
+
for (const edge of edges) {
|
|
859
|
+
if (!idSet.has(edge.source) || !idSet.has(edge.target)) continue;
|
|
860
|
+
adjacency.get(edge.source).add(edge.target);
|
|
861
|
+
adjacency.get(edge.target).add(edge.source);
|
|
862
|
+
}
|
|
863
|
+
const centerId = resolveCenterId(options.centerId, nodeIds, adjacency);
|
|
864
|
+
const hopOf = /* @__PURE__ */ new Map();
|
|
865
|
+
hopOf.set(centerId, 0);
|
|
866
|
+
const queue = [centerId];
|
|
867
|
+
let maxHop = 0;
|
|
868
|
+
while (queue.length > 0) {
|
|
869
|
+
const current = queue.shift();
|
|
870
|
+
const currentHop = hopOf.get(current);
|
|
871
|
+
for (const neighbor of adjacency.get(current) ?? []) {
|
|
872
|
+
if (hopOf.has(neighbor)) continue;
|
|
873
|
+
const hop = currentHop + 1;
|
|
874
|
+
hopOf.set(neighbor, hop);
|
|
875
|
+
maxHop = Math.max(maxHop, hop);
|
|
876
|
+
queue.push(neighbor);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const leftoverHop = maxHop + 1;
|
|
880
|
+
for (const id of nodeIds) {
|
|
881
|
+
if (!hopOf.has(id)) hopOf.set(id, leftoverHop);
|
|
882
|
+
}
|
|
883
|
+
const rings = /* @__PURE__ */ new Map();
|
|
884
|
+
for (const id of nodeIds) {
|
|
885
|
+
const hop = hopOf.get(id);
|
|
886
|
+
if (hop === 0) continue;
|
|
887
|
+
if (!rings.has(hop)) rings.set(hop, []);
|
|
888
|
+
rings.get(hop).push(id);
|
|
889
|
+
}
|
|
890
|
+
const positionById = /* @__PURE__ */ new Map();
|
|
891
|
+
positionById.set(centerId, { x: 0, y: 0 });
|
|
892
|
+
for (const [hop, ids] of rings) {
|
|
893
|
+
const count = ids.length;
|
|
894
|
+
const angleStep = 2 * Math.PI / count;
|
|
895
|
+
const ringOffset = (hop - 1) * (angleStep / 2);
|
|
896
|
+
const radius = hop * ringRadius;
|
|
897
|
+
ids.forEach((id, i) => {
|
|
898
|
+
const angle = ringOffset + i * angleStep;
|
|
899
|
+
positionById.set(id, { x: radius * Math.cos(angle), y: radius * Math.sin(angle) });
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
return nodes.map((node) => ({
|
|
903
|
+
...node,
|
|
904
|
+
position: positionById.get(node.id) ?? { x: 0, y: 0 }
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
907
|
+
function layoutForce(nodes, edges, options) {
|
|
908
|
+
const iterations = options.iterations ?? DEFAULT_FORCE_ITERATIONS;
|
|
909
|
+
const nodeIds = new Set(nodes.map((node) => node.id));
|
|
910
|
+
const seedRadius = Math.max(nodes.length * 20, 100);
|
|
911
|
+
const simNodes = nodes.map((node, i) => {
|
|
912
|
+
const angle = 2 * Math.PI * i / Math.max(nodes.length, 1);
|
|
913
|
+
return { id: node.id, x: seedRadius * Math.cos(angle), y: seedRadius * Math.sin(angle) };
|
|
914
|
+
});
|
|
915
|
+
const simLinks = edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge) => ({ source: edge.source, target: edge.target }));
|
|
916
|
+
const collisionById = new Map(
|
|
917
|
+
nodes.map((node) => {
|
|
918
|
+
const { width, height } = resolveNodeSize(node, options.nodeSize);
|
|
919
|
+
return [node.id, Math.max(width, height) / 2];
|
|
920
|
+
})
|
|
921
|
+
);
|
|
922
|
+
const simulation = forceSimulation(simNodes).force("charge", forceManyBody().strength(FORCE_CHARGE_STRENGTH)).force(
|
|
923
|
+
"link",
|
|
924
|
+
forceLink(simLinks).id((d) => d.id).distance(FORCE_LINK_DISTANCE)
|
|
925
|
+
).force("center", forceCenter(0, 0)).force(
|
|
926
|
+
"collide",
|
|
927
|
+
forceCollide((d) => collisionById.get(d.id) ?? DEFAULT_NODE_WIDTH2 / 2)
|
|
928
|
+
).stop();
|
|
929
|
+
for (let i = 0; i < iterations; i++) {
|
|
930
|
+
simulation.tick();
|
|
931
|
+
}
|
|
932
|
+
const positionById = new Map(simNodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));
|
|
933
|
+
return nodes.map((node) => ({
|
|
934
|
+
...node,
|
|
935
|
+
position: positionById.get(node.id) ?? { x: 0, y: 0 }
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
938
|
+
function layoutGrid(nodes, options) {
|
|
939
|
+
const spacing = options.spacing ?? DEFAULT_GRID_SPACING;
|
|
940
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(nodes.length)));
|
|
941
|
+
const handles = HANDLE_BY_DIRECTION.TB;
|
|
942
|
+
return nodes.map((node, i) => {
|
|
943
|
+
const row = Math.floor(i / cols);
|
|
944
|
+
const col = i % cols;
|
|
945
|
+
return {
|
|
946
|
+
...node,
|
|
947
|
+
sourcePosition: handles.source,
|
|
948
|
+
targetPosition: handles.target,
|
|
949
|
+
position: { x: col * spacing.x, y: row * spacing.y }
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/flow-layout/use-auto-layout.ts
|
|
955
|
+
import { useMemo } from "react";
|
|
956
|
+
function useAutoLayout(nodes, edges, options) {
|
|
957
|
+
return useMemo(
|
|
958
|
+
() => layoutGraph(nodes, edges, options),
|
|
959
|
+
[nodes, edges, options]
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/flow-mini-map/flow-mini-map.tsx
|
|
964
|
+
import { MiniMap } from "@xyflow/react";
|
|
965
|
+
import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
|
|
966
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
967
|
+
function FlowMiniMap({
|
|
968
|
+
className,
|
|
969
|
+
nodeColor = "var(--flow-minimap-node)",
|
|
970
|
+
nodeStrokeColor = "var(--flow-minimap-node)",
|
|
971
|
+
maskColor = "var(--flow-minimap-mask)",
|
|
972
|
+
...props
|
|
973
|
+
}) {
|
|
974
|
+
return /* @__PURE__ */ jsx9(
|
|
975
|
+
MiniMap,
|
|
976
|
+
{
|
|
977
|
+
className: cn5("!rounded-lg !bg-surface-elevated !shadow-ring-sm", className),
|
|
978
|
+
nodeColor,
|
|
979
|
+
nodeStrokeColor,
|
|
980
|
+
maskColor,
|
|
981
|
+
...props
|
|
982
|
+
}
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/inspector-panel/inspector-panel.tsx
|
|
987
|
+
import "react";
|
|
988
|
+
import { Reveal, useCollapsiblePanel } from "@elabs-ai/components-ui";
|
|
989
|
+
import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
|
|
990
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
991
|
+
var INSPECTOR_PANEL_WIDTH = "18rem";
|
|
992
|
+
function InspectorPanel({
|
|
993
|
+
title = "Inspector",
|
|
994
|
+
children,
|
|
995
|
+
onClose,
|
|
996
|
+
emptyMessage = "Select a node to see its details.",
|
|
997
|
+
hasSelection = true,
|
|
998
|
+
selectionKey,
|
|
999
|
+
open,
|
|
1000
|
+
defaultOpen = true,
|
|
1001
|
+
onOpenChange,
|
|
1002
|
+
side = "right",
|
|
1003
|
+
width = INSPECTOR_PANEL_WIDTH,
|
|
1004
|
+
className
|
|
1005
|
+
}) {
|
|
1006
|
+
const panel = useCollapsiblePanel({
|
|
1007
|
+
side,
|
|
1008
|
+
open,
|
|
1009
|
+
defaultOpen,
|
|
1010
|
+
onOpenChange,
|
|
1011
|
+
widthClassName: "w-(--inspector-panel-width)",
|
|
1012
|
+
spacerCollapsedClassName: "group-data-[state=collapsed]:w-0",
|
|
1013
|
+
containerSlideClassNames: {
|
|
1014
|
+
left: "left-0 group-data-[state=collapsed]:left-[calc(var(--inspector-panel-width)*-1)]",
|
|
1015
|
+
right: "right-0 group-data-[state=collapsed]:right-[calc(var(--inspector-panel-width)*-1)]"
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
return /* @__PURE__ */ jsxs7(
|
|
1019
|
+
"div",
|
|
1020
|
+
{
|
|
1021
|
+
className: cn6("group relative h-full overflow-hidden", className),
|
|
1022
|
+
style: { "--inspector-panel-width": width },
|
|
1023
|
+
inert: panel.open ? void 0 : true,
|
|
1024
|
+
...panel.attrs,
|
|
1025
|
+
children: [
|
|
1026
|
+
/* @__PURE__ */ jsx10("div", { "data-slot": "inspector-panel-gap", className: panel.spacerClassName }),
|
|
1027
|
+
/* @__PURE__ */ jsx10(
|
|
1028
|
+
"div",
|
|
1029
|
+
{
|
|
1030
|
+
"data-slot": "inspector-panel-container",
|
|
1031
|
+
className: cn6(panel.containerClassName, "absolute inset-y-0 flex h-full border-s"),
|
|
1032
|
+
children: /* @__PURE__ */ jsxs7("div", { className: "flex h-full w-full flex-col bg-card", children: [
|
|
1033
|
+
/* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
|
|
1034
|
+
/* @__PURE__ */ jsx10("h3", { className: "text-body font-semibold text-foreground", children: title }),
|
|
1035
|
+
onClose ? /* @__PURE__ */ jsx10(
|
|
1036
|
+
"button",
|
|
1037
|
+
{
|
|
1038
|
+
type: "button",
|
|
1039
|
+
"aria-label": "Close inspector",
|
|
1040
|
+
onClick: onClose,
|
|
1041
|
+
className: "rounded-sm p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
1042
|
+
children: /* @__PURE__ */ jsx10(
|
|
1043
|
+
"svg",
|
|
1044
|
+
{
|
|
1045
|
+
width: "14",
|
|
1046
|
+
height: "14",
|
|
1047
|
+
viewBox: "0 0 24 24",
|
|
1048
|
+
fill: "none",
|
|
1049
|
+
stroke: "currentColor",
|
|
1050
|
+
strokeWidth: "2",
|
|
1051
|
+
strokeLinecap: "round",
|
|
1052
|
+
strokeLinejoin: "round",
|
|
1053
|
+
"aria-hidden": "true",
|
|
1054
|
+
children: /* @__PURE__ */ jsx10("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
1055
|
+
}
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
1058
|
+
) : null
|
|
1059
|
+
] }),
|
|
1060
|
+
hasSelection ? /* @__PURE__ */ jsx10(
|
|
1061
|
+
Reveal,
|
|
1062
|
+
{
|
|
1063
|
+
appear: "fade",
|
|
1064
|
+
speed: "fast",
|
|
1065
|
+
className: "flex-1 overflow-y-auto p-4 text-body",
|
|
1066
|
+
children
|
|
1067
|
+
},
|
|
1068
|
+
selectionKey
|
|
1069
|
+
) : /* @__PURE__ */ jsx10("div", { className: "flex-1 overflow-y-auto p-4 text-body", children: /* @__PURE__ */ jsx10("p", { className: "text-muted-foreground", children: emptyMessage }) })
|
|
1070
|
+
] })
|
|
1071
|
+
}
|
|
1072
|
+
)
|
|
1073
|
+
]
|
|
1074
|
+
}
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// src/legend/legend.tsx
|
|
1079
|
+
import { cn as cn7 } from "@elabs-ai/components-ui/lib/cn";
|
|
1080
|
+
import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1081
|
+
function Legend({ items, title, className }) {
|
|
1082
|
+
return /* @__PURE__ */ jsxs8(
|
|
1083
|
+
"div",
|
|
1084
|
+
{
|
|
1085
|
+
className: cn7(
|
|
1086
|
+
"rounded-lg bg-surface-elevated/90 p-3 text-xs shadow-ring-sm backdrop-blur",
|
|
1087
|
+
className
|
|
1088
|
+
),
|
|
1089
|
+
children: [
|
|
1090
|
+
title ? /* @__PURE__ */ jsx11("div", { className: "mb-1.5 font-medium text-foreground", children: title }) : null,
|
|
1091
|
+
/* @__PURE__ */ jsx11("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxs8("li", { className: "flex items-center gap-2 text-muted-foreground", children: [
|
|
1092
|
+
/* @__PURE__ */ jsx11(
|
|
1093
|
+
"span",
|
|
1094
|
+
{
|
|
1095
|
+
"aria-hidden": "true",
|
|
1096
|
+
className: "size-2.5 shrink-0 rounded-full",
|
|
1097
|
+
style: { backgroundColor: item.color }
|
|
1098
|
+
}
|
|
1099
|
+
),
|
|
1100
|
+
item.label
|
|
1101
|
+
] }, item.label)) })
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/zoom-controls/zoom-controls.tsx
|
|
1108
|
+
import { Panel, useReactFlow as useReactFlow3 } from "@xyflow/react";
|
|
1109
|
+
import { cn as cn8 } from "@elabs-ai/components-ui/lib/cn";
|
|
1110
|
+
import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1111
|
+
function ControlButton({
|
|
1112
|
+
label,
|
|
1113
|
+
onClick,
|
|
1114
|
+
children
|
|
1115
|
+
}) {
|
|
1116
|
+
return /* @__PURE__ */ jsx12(
|
|
1117
|
+
"button",
|
|
1118
|
+
{
|
|
1119
|
+
type: "button",
|
|
1120
|
+
"aria-label": label,
|
|
1121
|
+
onClick,
|
|
1122
|
+
className: "flex size-8 items-center justify-center text-foreground transition-colors duration-fast hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
|
|
1123
|
+
children
|
|
1124
|
+
}
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
function ZoomControls({ position = "bottom-right", className }) {
|
|
1128
|
+
const { zoomIn, zoomOut, fitView } = useReactFlow3();
|
|
1129
|
+
return /* @__PURE__ */ jsx12(Panel, { position, children: /* @__PURE__ */ jsxs9(
|
|
1130
|
+
"div",
|
|
1131
|
+
{
|
|
1132
|
+
className: cn8(
|
|
1133
|
+
"flex divide-x overflow-hidden rounded-lg bg-surface-elevated shadow-ring-sm",
|
|
1134
|
+
className
|
|
1135
|
+
),
|
|
1136
|
+
children: [
|
|
1137
|
+
/* @__PURE__ */ jsx12(ControlButton, { label: "Zoom in", onClick: () => zoomIn(), children: /* @__PURE__ */ jsx12(
|
|
1138
|
+
"svg",
|
|
1139
|
+
{
|
|
1140
|
+
viewBox: "0 0 24 24",
|
|
1141
|
+
fill: "none",
|
|
1142
|
+
stroke: "currentColor",
|
|
1143
|
+
strokeWidth: "2",
|
|
1144
|
+
strokeLinecap: "round",
|
|
1145
|
+
children: /* @__PURE__ */ jsx12("path", { d: "M12 5v14M5 12h14" })
|
|
1146
|
+
}
|
|
1147
|
+
) }),
|
|
1148
|
+
/* @__PURE__ */ jsx12(ControlButton, { label: "Zoom out", onClick: () => zoomOut(), children: /* @__PURE__ */ jsx12(
|
|
1149
|
+
"svg",
|
|
1150
|
+
{
|
|
1151
|
+
viewBox: "0 0 24 24",
|
|
1152
|
+
fill: "none",
|
|
1153
|
+
stroke: "currentColor",
|
|
1154
|
+
strokeWidth: "2",
|
|
1155
|
+
strokeLinecap: "round",
|
|
1156
|
+
children: /* @__PURE__ */ jsx12("path", { d: "M5 12h14" })
|
|
1157
|
+
}
|
|
1158
|
+
) }),
|
|
1159
|
+
/* @__PURE__ */ jsx12(ControlButton, { label: "Fit view", onClick: () => fitView(), children: /* @__PURE__ */ jsx12(
|
|
1160
|
+
"svg",
|
|
1161
|
+
{
|
|
1162
|
+
viewBox: "0 0 24 24",
|
|
1163
|
+
fill: "none",
|
|
1164
|
+
stroke: "currentColor",
|
|
1165
|
+
strokeWidth: "2",
|
|
1166
|
+
strokeLinecap: "round",
|
|
1167
|
+
strokeLinejoin: "round",
|
|
1168
|
+
children: /* @__PURE__ */ jsx12("path", { d: "M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M16 21h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" })
|
|
1169
|
+
}
|
|
1170
|
+
) })
|
|
1171
|
+
]
|
|
1172
|
+
}
|
|
1173
|
+
) });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/flow-group-node/flow-group-node.tsx
|
|
1177
|
+
import "react";
|
|
1178
|
+
import { Handle as Handle3, NodeResizer, Position as Position6, useNodes } from "@xyflow/react";
|
|
1179
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
1180
|
+
import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
|
|
1181
|
+
|
|
1182
|
+
// src/use-flow-groups/use-flow-groups.ts
|
|
1183
|
+
import { useCallback as useCallback3 } from "react";
|
|
1184
|
+
import { useReactFlow as useReactFlow4 } from "@xyflow/react";
|
|
1185
|
+
|
|
1186
|
+
// src/use-flow-groups/group-operations.ts
|
|
1187
|
+
import { getNodesBounds } from "@xyflow/react";
|
|
1188
|
+
var FLOW_GROUP_NODE_TYPE = "group";
|
|
1189
|
+
var DEFAULT_GROUP_PADDING = 28;
|
|
1190
|
+
var GROUP_HEADER_OFFSET = 44;
|
|
1191
|
+
var OVERVIEW_WIDTH = 220;
|
|
1192
|
+
var OVERVIEW_HEIGHT = 48;
|
|
1193
|
+
var COLLAPSED_STATE_KEY = "__flowGroupCollapsedState";
|
|
1194
|
+
function isFlowGroupProxyEdge(edge) {
|
|
1195
|
+
return Boolean(edge.data?.__flowGroupProxy);
|
|
1196
|
+
}
|
|
1197
|
+
function proxyEdgeId(groupId, originalEdgeId) {
|
|
1198
|
+
return `flow-group-proxy__${groupId}__${originalEdgeId}`;
|
|
1199
|
+
}
|
|
1200
|
+
function readCollapsedState(node) {
|
|
1201
|
+
return node.data?.[COLLAPSED_STATE_KEY];
|
|
1202
|
+
}
|
|
1203
|
+
function collectDescendantIds(nodes, groupId) {
|
|
1204
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
1205
|
+
for (const n of nodes) {
|
|
1206
|
+
if (n.parentId) {
|
|
1207
|
+
const arr = childrenByParent.get(n.parentId);
|
|
1208
|
+
if (arr) arr.push(n);
|
|
1209
|
+
else childrenByParent.set(n.parentId, [n]);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const out = /* @__PURE__ */ new Set();
|
|
1213
|
+
const stack = [...childrenByParent.get(groupId) ?? []];
|
|
1214
|
+
while (stack.length > 0) {
|
|
1215
|
+
const n = stack.pop();
|
|
1216
|
+
if (out.has(n.id)) continue;
|
|
1217
|
+
out.add(n.id);
|
|
1218
|
+
const kids = childrenByParent.get(n.id);
|
|
1219
|
+
if (kids) stack.push(...kids);
|
|
1220
|
+
}
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
1223
|
+
function groupNodes(nodes, edges, nodeIds, options) {
|
|
1224
|
+
const idSet = new Set(nodeIds);
|
|
1225
|
+
const children = nodes.filter((n) => idSet.has(n.id));
|
|
1226
|
+
if (children.length === 0) return { nodes, edges };
|
|
1227
|
+
const bounds = getNodesBounds(children);
|
|
1228
|
+
const padding = options.padding ?? DEFAULT_GROUP_PADDING;
|
|
1229
|
+
const originX = bounds.x - padding;
|
|
1230
|
+
const originY = bounds.y - padding - GROUP_HEADER_OFFSET;
|
|
1231
|
+
const data = {
|
|
1232
|
+
title: options.title ?? "Group",
|
|
1233
|
+
childCount: children.length,
|
|
1234
|
+
...options.tone ? { tone: options.tone } : {}
|
|
1235
|
+
};
|
|
1236
|
+
const groupNode = {
|
|
1237
|
+
id: options.groupId,
|
|
1238
|
+
type: FLOW_GROUP_NODE_TYPE,
|
|
1239
|
+
position: { x: originX, y: originY },
|
|
1240
|
+
width: bounds.width + padding * 2,
|
|
1241
|
+
height: bounds.height + padding * 2 + GROUP_HEADER_OFFSET,
|
|
1242
|
+
data
|
|
1243
|
+
};
|
|
1244
|
+
const reparented = /* @__PURE__ */ new Map();
|
|
1245
|
+
for (const child of children) {
|
|
1246
|
+
reparented.set(child.id, {
|
|
1247
|
+
...child,
|
|
1248
|
+
parentId: options.groupId,
|
|
1249
|
+
extent: "parent",
|
|
1250
|
+
position: { x: child.position.x - originX, y: child.position.y - originY }
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
let earliest = nodes.length;
|
|
1254
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1255
|
+
if (idSet.has(nodes[i].id)) {
|
|
1256
|
+
earliest = i;
|
|
1257
|
+
break;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const mapped = nodes.map((n) => reparented.get(n.id) ?? n);
|
|
1261
|
+
const resultNodes = [...mapped.slice(0, earliest), groupNode, ...mapped.slice(earliest)];
|
|
1262
|
+
return { nodes: resultNodes, edges };
|
|
1263
|
+
}
|
|
1264
|
+
function ungroup(nodes, edges, groupId) {
|
|
1265
|
+
const group = nodes.find((n) => n.id === groupId);
|
|
1266
|
+
if (!group) return { nodes, edges };
|
|
1267
|
+
const base = group.data?.collapsed ? expandGroup(nodes, edges, groupId) : { nodes, edges };
|
|
1268
|
+
const liveGroup = base.nodes.find((n) => n.id === groupId);
|
|
1269
|
+
const gx = liveGroup.position.x;
|
|
1270
|
+
const gy = liveGroup.position.y;
|
|
1271
|
+
const resultNodes = base.nodes.filter((n) => n.id !== groupId).map((n) => {
|
|
1272
|
+
if (n.parentId === groupId) {
|
|
1273
|
+
const {
|
|
1274
|
+
parentId: _parentId,
|
|
1275
|
+
extent: _extent,
|
|
1276
|
+
...rest
|
|
1277
|
+
} = n;
|
|
1278
|
+
return {
|
|
1279
|
+
...rest,
|
|
1280
|
+
position: { x: n.position.x + gx, y: n.position.y + gy }
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
return n;
|
|
1284
|
+
});
|
|
1285
|
+
return { nodes: resultNodes, edges: base.edges };
|
|
1286
|
+
}
|
|
1287
|
+
function collapseGroup(nodes, edges, groupId) {
|
|
1288
|
+
const group = nodes.find((n) => n.id === groupId);
|
|
1289
|
+
if (!group) return { nodes, edges };
|
|
1290
|
+
if (group.data?.collapsed) return { nodes, edges };
|
|
1291
|
+
const descendantIds = collectDescendantIds(nodes, groupId);
|
|
1292
|
+
const descendantSnapshots = nodes.filter((n) => descendantIds.has(n.id));
|
|
1293
|
+
const directChildCount = nodes.filter((n) => n.parentId === groupId).length;
|
|
1294
|
+
const isInside = (id) => descendantIds.has(id);
|
|
1295
|
+
const reroutedEdges = [];
|
|
1296
|
+
const proxyEdges = [];
|
|
1297
|
+
for (const edge of edges) {
|
|
1298
|
+
const sourceInside = isInside(edge.source);
|
|
1299
|
+
const targetInside = isInside(edge.target);
|
|
1300
|
+
if (sourceInside === targetInside) continue;
|
|
1301
|
+
reroutedEdges.push(edge);
|
|
1302
|
+
const proxyData = {
|
|
1303
|
+
...edge.data ?? {},
|
|
1304
|
+
__flowGroupProxy: true,
|
|
1305
|
+
groupId
|
|
1306
|
+
};
|
|
1307
|
+
proxyEdges.push({
|
|
1308
|
+
...edge,
|
|
1309
|
+
id: proxyEdgeId(groupId, edge.id),
|
|
1310
|
+
source: sourceInside ? groupId : edge.source,
|
|
1311
|
+
target: targetInside ? groupId : edge.target,
|
|
1312
|
+
sourceHandle: void 0,
|
|
1313
|
+
targetHandle: void 0,
|
|
1314
|
+
data: proxyData
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
const collapsedState = {
|
|
1318
|
+
groupSnapshot: group,
|
|
1319
|
+
descendantSnapshots,
|
|
1320
|
+
reroutedEdges
|
|
1321
|
+
};
|
|
1322
|
+
const resultNodes = nodes.map((n) => {
|
|
1323
|
+
if (n.id === groupId) {
|
|
1324
|
+
return {
|
|
1325
|
+
...n,
|
|
1326
|
+
width: OVERVIEW_WIDTH,
|
|
1327
|
+
height: OVERVIEW_HEIGHT,
|
|
1328
|
+
data: {
|
|
1329
|
+
...n.data,
|
|
1330
|
+
collapsed: true,
|
|
1331
|
+
childCount: directChildCount,
|
|
1332
|
+
[COLLAPSED_STATE_KEY]: collapsedState
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
if (descendantIds.has(n.id)) {
|
|
1337
|
+
return { ...n, hidden: true };
|
|
1338
|
+
}
|
|
1339
|
+
return n;
|
|
1340
|
+
});
|
|
1341
|
+
const reroutedIds = new Set(reroutedEdges.map((e) => e.id));
|
|
1342
|
+
const resultEdges = edges.map(
|
|
1343
|
+
(e) => reroutedIds.has(e.id) ? { ...e, hidden: true } : e
|
|
1344
|
+
);
|
|
1345
|
+
resultEdges.push(...proxyEdges);
|
|
1346
|
+
return { nodes: resultNodes, edges: resultEdges };
|
|
1347
|
+
}
|
|
1348
|
+
function expandGroup(nodes, edges, groupId) {
|
|
1349
|
+
const group = nodes.find((n) => n.id === groupId);
|
|
1350
|
+
const state = group ? readCollapsedState(group) : void 0;
|
|
1351
|
+
if (!group || !state) return { nodes, edges };
|
|
1352
|
+
const snapshotById = new Map(state.descendantSnapshots.map((n) => [n.id, n]));
|
|
1353
|
+
const reroutedById = new Map(state.reroutedEdges.map((e) => [e.id, e]));
|
|
1354
|
+
const resultNodes = nodes.map((n) => {
|
|
1355
|
+
if (n.id === groupId) return state.groupSnapshot;
|
|
1356
|
+
const snap = snapshotById.get(n.id);
|
|
1357
|
+
return snap ? snap : n;
|
|
1358
|
+
});
|
|
1359
|
+
const resultEdges = edges.filter(
|
|
1360
|
+
(e) => !(isFlowGroupProxyEdge(e) && e.data?.groupId === groupId)
|
|
1361
|
+
).map((e) => {
|
|
1362
|
+
const original = reroutedById.get(e.id);
|
|
1363
|
+
return original ? original : e;
|
|
1364
|
+
});
|
|
1365
|
+
return { nodes: resultNodes, edges: resultEdges };
|
|
1366
|
+
}
|
|
1367
|
+
function toggleGroupCollapsed(nodes, edges, groupId) {
|
|
1368
|
+
const group = nodes.find((n) => n.id === groupId);
|
|
1369
|
+
if (!group) return { nodes, edges };
|
|
1370
|
+
return group.data?.collapsed ? expandGroup(nodes, edges, groupId) : collapseGroup(nodes, edges, groupId);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/use-flow-groups/use-flow-groups.ts
|
|
1374
|
+
function generateGroupId() {
|
|
1375
|
+
return `flow-group-${Math.random().toString(36).slice(2, 9)}`;
|
|
1376
|
+
}
|
|
1377
|
+
function useFlowGroups() {
|
|
1378
|
+
const { getNodes, getEdges, setNodes, setEdges } = useReactFlow4();
|
|
1379
|
+
const apply = useCallback3(
|
|
1380
|
+
(result) => {
|
|
1381
|
+
setNodes(result.nodes);
|
|
1382
|
+
setEdges(result.edges);
|
|
1383
|
+
},
|
|
1384
|
+
[setNodes, setEdges]
|
|
1385
|
+
);
|
|
1386
|
+
const groupNodes2 = useCallback3(
|
|
1387
|
+
(nodeIds, options) => {
|
|
1388
|
+
if (nodeIds.length === 0) return void 0;
|
|
1389
|
+
const groupId = options?.groupId ?? generateGroupId();
|
|
1390
|
+
const result = groupNodes(getNodes(), getEdges(), nodeIds, {
|
|
1391
|
+
...options,
|
|
1392
|
+
groupId
|
|
1393
|
+
});
|
|
1394
|
+
apply(result);
|
|
1395
|
+
return groupId;
|
|
1396
|
+
},
|
|
1397
|
+
[getNodes, getEdges, apply]
|
|
1398
|
+
);
|
|
1399
|
+
const groupSelection = useCallback3(
|
|
1400
|
+
(options) => {
|
|
1401
|
+
const selectedIds = getNodes().filter((n) => n.selected && !n.parentId).map((n) => n.id);
|
|
1402
|
+
return groupNodes2(selectedIds, options);
|
|
1403
|
+
},
|
|
1404
|
+
[getNodes, groupNodes2]
|
|
1405
|
+
);
|
|
1406
|
+
const ungroup2 = useCallback3(
|
|
1407
|
+
(groupId) => apply(ungroup(getNodes(), getEdges(), groupId)),
|
|
1408
|
+
[getNodes, getEdges, apply]
|
|
1409
|
+
);
|
|
1410
|
+
const collapseGroup2 = useCallback3(
|
|
1411
|
+
(groupId) => apply(collapseGroup(getNodes(), getEdges(), groupId)),
|
|
1412
|
+
[getNodes, getEdges, apply]
|
|
1413
|
+
);
|
|
1414
|
+
const expandGroup2 = useCallback3(
|
|
1415
|
+
(groupId) => apply(expandGroup(getNodes(), getEdges(), groupId)),
|
|
1416
|
+
[getNodes, getEdges, apply]
|
|
1417
|
+
);
|
|
1418
|
+
const toggleCollapse = useCallback3(
|
|
1419
|
+
(groupId) => apply(toggleGroupCollapsed(getNodes(), getEdges(), groupId)),
|
|
1420
|
+
[getNodes, getEdges, apply]
|
|
1421
|
+
);
|
|
1422
|
+
return {
|
|
1423
|
+
groupNodes: groupNodes2,
|
|
1424
|
+
groupSelection,
|
|
1425
|
+
ungroup: ungroup2,
|
|
1426
|
+
collapseGroup: collapseGroup2,
|
|
1427
|
+
expandGroup: expandGroup2,
|
|
1428
|
+
toggleCollapse
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/flow-group-node/flow-group-node.tsx
|
|
1433
|
+
import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1434
|
+
var toneAccent = {
|
|
1435
|
+
default: "text-muted-foreground",
|
|
1436
|
+
// #399 — the group LABEL is text on the canvas, so it takes the on-surface
|
|
1437
|
+
// `-text` rung. (The status rows below stay on their fill rungs; those are a
|
|
1438
|
+
// separate, still-open question, not part of #399's enumerated call sites.)
|
|
1439
|
+
accent: "text-primary-text",
|
|
1440
|
+
success: "text-success",
|
|
1441
|
+
warning: "text-warning",
|
|
1442
|
+
destructive: "text-destructive"
|
|
1443
|
+
};
|
|
1444
|
+
var handleClassName2 = "!size-2 !border-2 !border-flow-group-border !bg-flow-group";
|
|
1445
|
+
function FlowGroupNode({ id, data, selected }) {
|
|
1446
|
+
const { toggleCollapse } = useFlowGroups();
|
|
1447
|
+
const nodes = useNodes();
|
|
1448
|
+
const collapsed = data.collapsed ?? false;
|
|
1449
|
+
const tone = data.tone ?? "default";
|
|
1450
|
+
const liveChildCount = nodes.filter((n) => n.parentId === id).length;
|
|
1451
|
+
const childCount = liveChildCount || data.childCount || 0;
|
|
1452
|
+
return /* @__PURE__ */ jsxs10(
|
|
1453
|
+
"div",
|
|
1454
|
+
{
|
|
1455
|
+
className: cn9(
|
|
1456
|
+
"flex h-full w-full flex-col rounded-lg border bg-flow-group/60 text-foreground",
|
|
1457
|
+
"border-flow-group-border transition-[box-shadow,border-color] duration-fast ease-standard motion-reduce:transition-none",
|
|
1458
|
+
collapsed ? "shadow-sm" : "shadow-none",
|
|
1459
|
+
selected && "ring-2 ring-ring"
|
|
1460
|
+
),
|
|
1461
|
+
children: [
|
|
1462
|
+
!collapsed && selected ? /* @__PURE__ */ jsx13(
|
|
1463
|
+
NodeResizer,
|
|
1464
|
+
{
|
|
1465
|
+
minWidth: 160,
|
|
1466
|
+
minHeight: 96,
|
|
1467
|
+
lineClassName: "!border-flow-group-border",
|
|
1468
|
+
handleClassName: handleClassName2
|
|
1469
|
+
}
|
|
1470
|
+
) : null,
|
|
1471
|
+
/* @__PURE__ */ jsx13(Handle3, { type: "target", position: Position6.Top, className: handleClassName2 }),
|
|
1472
|
+
/* @__PURE__ */ jsx13(Handle3, { type: "source", position: Position6.Bottom, className: handleClassName2 }),
|
|
1473
|
+
/* @__PURE__ */ jsxs10(
|
|
1474
|
+
"div",
|
|
1475
|
+
{
|
|
1476
|
+
className: cn9(
|
|
1477
|
+
"flex items-center gap-2 px-3 py-2",
|
|
1478
|
+
!collapsed && "border-b border-flow-group-border"
|
|
1479
|
+
),
|
|
1480
|
+
children: [
|
|
1481
|
+
/* @__PURE__ */ jsx13(
|
|
1482
|
+
"button",
|
|
1483
|
+
{
|
|
1484
|
+
type: "button",
|
|
1485
|
+
onClick: () => toggleCollapse(id),
|
|
1486
|
+
"aria-label": collapsed ? `Expand group ${data.title}` : `Collapse group ${data.title}`,
|
|
1487
|
+
"aria-expanded": !collapsed,
|
|
1488
|
+
className: cn9(
|
|
1489
|
+
"-ml-1 grid size-5 shrink-0 place-items-center rounded",
|
|
1490
|
+
"text-muted-foreground hover:bg-muted hover:text-foreground",
|
|
1491
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
1492
|
+
),
|
|
1493
|
+
children: collapsed ? /* @__PURE__ */ jsx13(ChevronRight, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx13(ChevronDown, { "aria-hidden": "true", className: "size-4" })
|
|
1494
|
+
}
|
|
1495
|
+
),
|
|
1496
|
+
data.icon ? /* @__PURE__ */ jsx13("span", { className: cn9("[&_svg]:size-4", toneAccent[tone]), children: data.icon }) : null,
|
|
1497
|
+
/* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate text-body font-medium", children: data.title }),
|
|
1498
|
+
/* @__PURE__ */ jsx13(
|
|
1499
|
+
"span",
|
|
1500
|
+
{
|
|
1501
|
+
className: cn9(
|
|
1502
|
+
"shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-meta font-medium tabular-nums",
|
|
1503
|
+
toneAccent[tone]
|
|
1504
|
+
),
|
|
1505
|
+
"aria-label": `${childCount} ${childCount === 1 ? "node" : "nodes"}`,
|
|
1506
|
+
children: childCount
|
|
1507
|
+
}
|
|
1508
|
+
)
|
|
1509
|
+
]
|
|
1510
|
+
}
|
|
1511
|
+
)
|
|
1512
|
+
]
|
|
1513
|
+
}
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// src/index.ts
|
|
1518
|
+
import {
|
|
1519
|
+
Background as Background2,
|
|
1520
|
+
Controls,
|
|
1521
|
+
MiniMap as MiniMap2,
|
|
1522
|
+
Panel as Panel2,
|
|
1523
|
+
Position as Position7,
|
|
1524
|
+
ReactFlow as ReactFlow2,
|
|
1525
|
+
ReactFlowProvider as ReactFlowProvider2,
|
|
1526
|
+
useReactFlow as useReactFlow5,
|
|
1527
|
+
useNodesState,
|
|
1528
|
+
useEdgesState,
|
|
1529
|
+
addEdge
|
|
1530
|
+
} from "@xyflow/react";
|
|
1531
|
+
export {
|
|
1532
|
+
Background2 as Background,
|
|
1533
|
+
CanvasShell,
|
|
1534
|
+
Controls,
|
|
1535
|
+
FLOW_ALL_SIDE_HANDLES,
|
|
1536
|
+
FLOW_GROUP_NODE_TYPE,
|
|
1537
|
+
FlowButtonEdge,
|
|
1538
|
+
FlowEdge,
|
|
1539
|
+
FlowFloatingEdge,
|
|
1540
|
+
FlowGroupNode,
|
|
1541
|
+
FlowMiniMap,
|
|
1542
|
+
FlowNode,
|
|
1543
|
+
FlowPlaceholderNode,
|
|
1544
|
+
FlowSmartEdge,
|
|
1545
|
+
HANDLE_SIDES,
|
|
1546
|
+
HelperLines,
|
|
1547
|
+
InspectorPanel,
|
|
1548
|
+
Legend,
|
|
1549
|
+
MiniMap2 as MiniMap,
|
|
1550
|
+
Panel2 as Panel,
|
|
1551
|
+
Position7 as Position,
|
|
1552
|
+
ReactFlow2 as ReactFlow,
|
|
1553
|
+
ReactFlowProvider2 as ReactFlowProvider,
|
|
1554
|
+
ZoomControls,
|
|
1555
|
+
addEdge,
|
|
1556
|
+
collapseGroup,
|
|
1557
|
+
expandGroup,
|
|
1558
|
+
getEdgeParams,
|
|
1559
|
+
getHelperLines,
|
|
1560
|
+
groupNodes,
|
|
1561
|
+
handlePoint,
|
|
1562
|
+
isFlowGroupProxyEdge,
|
|
1563
|
+
layoutFlow,
|
|
1564
|
+
layoutGraph,
|
|
1565
|
+
pickClosestHandles,
|
|
1566
|
+
sideToPosition,
|
|
1567
|
+
toggleGroupCollapsed,
|
|
1568
|
+
ungroup,
|
|
1569
|
+
useAutoLayout,
|
|
1570
|
+
useEdgesState,
|
|
1571
|
+
useFlowGroups,
|
|
1572
|
+
useFlowLayout,
|
|
1573
|
+
useHelperLines,
|
|
1574
|
+
useNodesState,
|
|
1575
|
+
useReactFlow5 as useReactFlow
|
|
1576
|
+
};
|
|
1577
|
+
//# sourceMappingURL=index.js.map
|