@pyreon/flow 0.6.0 → 0.7.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/lib/types/index.d.ts +666 -2300
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/lib/types/chunk.d.ts +0 -2
- package/lib/types/elk.bundled.d.ts +0 -7
- package/lib/types/elk.bundled.d.ts.map +0 -1
- package/lib/types/index2.d.ts +0 -708
- package/lib/types/index2.d.ts.map +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -1,2342 +1,708 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { VNodeChild } from "@pyreon/core";
|
|
2
|
+
import { Computed, Signal } from "@pyreon/reactivity";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return {
|
|
9
|
-
type,
|
|
10
|
-
props: props ?? EMPTY_PROPS,
|
|
11
|
-
children: normalizeChildren(children),
|
|
12
|
-
key: props?.key ?? null
|
|
13
|
-
};
|
|
4
|
+
//#region src/types.d.ts
|
|
5
|
+
interface XYPosition {
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
14
8
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
interface Dimensions {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
interface Rect extends XYPosition, Dimensions {}
|
|
14
|
+
interface Viewport {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
zoom: number;
|
|
23
18
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*/
|
|
31
|
-
function jsx(type, props, key) {
|
|
32
|
-
const {
|
|
33
|
-
children,
|
|
34
|
-
...rest
|
|
35
|
-
} = props;
|
|
36
|
-
const propsWithKey = key != null ? {
|
|
37
|
-
...rest,
|
|
38
|
-
key
|
|
39
|
-
} : rest;
|
|
40
|
-
if (typeof type === "function") return h(type, children !== void 0 ? {
|
|
41
|
-
...propsWithKey,
|
|
42
|
-
children
|
|
43
|
-
} : propsWithKey);
|
|
44
|
-
return h(type, propsWithKey, ...(children === void 0 ? [] : Array.isArray(children) ? children : [children]));
|
|
19
|
+
type HandleType = 'source' | 'target';
|
|
20
|
+
declare enum Position {
|
|
21
|
+
Top = "top",
|
|
22
|
+
Right = "right",
|
|
23
|
+
Bottom = "bottom",
|
|
24
|
+
Left = "left"
|
|
45
25
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* Renders dots, lines, or cross patterns that move with the viewport.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```tsx
|
|
54
|
-
* <Flow instance={flow}>
|
|
55
|
-
* <Background variant="dots" gap={20} />
|
|
56
|
-
* </Flow>
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
function Background(props) {
|
|
60
|
-
const {
|
|
61
|
-
variant = "dots",
|
|
62
|
-
gap = 20,
|
|
63
|
-
size = 1,
|
|
64
|
-
color = "#ddd"
|
|
65
|
-
} = props;
|
|
66
|
-
const patternId = `flow-bg-${variant}`;
|
|
67
|
-
if (variant === "dots") return /* @__PURE__ */jsxs("svg", {
|
|
68
|
-
role: "img",
|
|
69
|
-
"aria-label": "background pattern",
|
|
70
|
-
class: "pyreon-flow-background",
|
|
71
|
-
style: "position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;",
|
|
72
|
-
children: [/* @__PURE__ */jsx("defs", {
|
|
73
|
-
children: /* @__PURE__ */jsx("pattern", {
|
|
74
|
-
id: patternId,
|
|
75
|
-
x: "0",
|
|
76
|
-
y: "0",
|
|
77
|
-
width: String(gap),
|
|
78
|
-
height: String(gap),
|
|
79
|
-
patternUnits: "userSpaceOnUse",
|
|
80
|
-
children: /* @__PURE__ */jsx("circle", {
|
|
81
|
-
cx: String(size),
|
|
82
|
-
cy: String(size),
|
|
83
|
-
r: String(size),
|
|
84
|
-
fill: color
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
}), /* @__PURE__ */jsx("rect", {
|
|
88
|
-
width: "100%",
|
|
89
|
-
height: "100%",
|
|
90
|
-
fill: `url(#${patternId})`
|
|
91
|
-
})]
|
|
92
|
-
});
|
|
93
|
-
if (variant === "lines") return /* @__PURE__ */jsxs("svg", {
|
|
94
|
-
role: "img",
|
|
95
|
-
"aria-label": "background pattern",
|
|
96
|
-
class: "pyreon-flow-background",
|
|
97
|
-
style: "position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;",
|
|
98
|
-
children: [/* @__PURE__ */jsx("defs", {
|
|
99
|
-
children: /* @__PURE__ */jsxs("pattern", {
|
|
100
|
-
id: patternId,
|
|
101
|
-
x: "0",
|
|
102
|
-
y: "0",
|
|
103
|
-
width: String(gap),
|
|
104
|
-
height: String(gap),
|
|
105
|
-
patternUnits: "userSpaceOnUse",
|
|
106
|
-
children: [/* @__PURE__ */jsx("line", {
|
|
107
|
-
x1: "0",
|
|
108
|
-
y1: String(gap),
|
|
109
|
-
x2: String(gap),
|
|
110
|
-
y2: String(gap),
|
|
111
|
-
stroke: color,
|
|
112
|
-
"stroke-width": String(size)
|
|
113
|
-
}), /* @__PURE__ */jsx("line", {
|
|
114
|
-
x1: String(gap),
|
|
115
|
-
y1: "0",
|
|
116
|
-
x2: String(gap),
|
|
117
|
-
y2: String(gap),
|
|
118
|
-
stroke: color,
|
|
119
|
-
"stroke-width": String(size)
|
|
120
|
-
})]
|
|
121
|
-
})
|
|
122
|
-
}), /* @__PURE__ */jsx("rect", {
|
|
123
|
-
width: "100%",
|
|
124
|
-
height: "100%",
|
|
125
|
-
fill: `url(#${patternId})`
|
|
126
|
-
})]
|
|
127
|
-
});
|
|
128
|
-
return /* @__PURE__ */jsxs("svg", {
|
|
129
|
-
role: "img",
|
|
130
|
-
"aria-label": "background pattern",
|
|
131
|
-
class: "pyreon-flow-background",
|
|
132
|
-
style: "position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;",
|
|
133
|
-
children: [/* @__PURE__ */jsx("defs", {
|
|
134
|
-
children: /* @__PURE__ */jsxs("pattern", {
|
|
135
|
-
id: patternId,
|
|
136
|
-
x: "0",
|
|
137
|
-
y: "0",
|
|
138
|
-
width: String(gap),
|
|
139
|
-
height: String(gap),
|
|
140
|
-
patternUnits: "userSpaceOnUse",
|
|
141
|
-
children: [/* @__PURE__ */jsx("line", {
|
|
142
|
-
x1: String(gap / 2 - size * 2),
|
|
143
|
-
y1: String(gap / 2),
|
|
144
|
-
x2: String(gap / 2 + size * 2),
|
|
145
|
-
y2: String(gap / 2),
|
|
146
|
-
stroke: color,
|
|
147
|
-
"stroke-width": String(size)
|
|
148
|
-
}), /* @__PURE__ */jsx("line", {
|
|
149
|
-
x1: String(gap / 2),
|
|
150
|
-
y1: String(gap / 2 - size * 2),
|
|
151
|
-
x2: String(gap / 2),
|
|
152
|
-
y2: String(gap / 2 + size * 2),
|
|
153
|
-
stroke: color,
|
|
154
|
-
"stroke-width": String(size)
|
|
155
|
-
})]
|
|
156
|
-
})
|
|
157
|
-
}), /* @__PURE__ */jsx("rect", {
|
|
158
|
-
width: "100%",
|
|
159
|
-
height: "100%",
|
|
160
|
-
fill: `url(#${patternId})`
|
|
161
|
-
})]
|
|
162
|
-
});
|
|
26
|
+
interface HandleConfig {
|
|
27
|
+
id?: string;
|
|
28
|
+
type: HandleType;
|
|
29
|
+
position: Position;
|
|
163
30
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const baseStyle = `position: absolute; ${positionStyles$2[position] ?? positionStyles$2["bottom-left"]} display: flex; flex-direction: column; gap: 2px; z-index: 5; background: white; border: 1px solid #ddd; border-radius: 6px; padding: 2px; box-shadow: 0 1px 4px rgba(0,0,0,0.08);`;
|
|
190
|
-
const btnStyle = "width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; border-radius: 4px; cursor: pointer; color: #555; padding: 0;";
|
|
191
|
-
return () => {
|
|
192
|
-
const zoomPercent = Math.round(instance.zoom() * 100);
|
|
193
|
-
return /* @__PURE__ */jsxs("div", {
|
|
194
|
-
class: "pyreon-flow-controls",
|
|
195
|
-
style: baseStyle,
|
|
196
|
-
children: [showZoomIn && /* @__PURE__ */jsx("button", {
|
|
197
|
-
type: "button",
|
|
198
|
-
style: btnStyle,
|
|
199
|
-
title: "Zoom in",
|
|
200
|
-
onClick: () => instance.zoomIn(),
|
|
201
|
-
children: /* @__PURE__ */jsx(ZoomInIcon, {})
|
|
202
|
-
}), showZoomOut && /* @__PURE__ */jsx("button", {
|
|
203
|
-
type: "button",
|
|
204
|
-
style: btnStyle,
|
|
205
|
-
title: "Zoom out",
|
|
206
|
-
onClick: () => instance.zoomOut(),
|
|
207
|
-
children: /* @__PURE__ */jsx(ZoomOutIcon, {})
|
|
208
|
-
}), showFitView && /* @__PURE__ */jsx("button", {
|
|
209
|
-
type: "button",
|
|
210
|
-
style: btnStyle,
|
|
211
|
-
title: "Fit view",
|
|
212
|
-
onClick: () => instance.fitView(),
|
|
213
|
-
children: /* @__PURE__ */jsx(FitViewIcon, {})
|
|
214
|
-
}), showLock && /* @__PURE__ */jsx("button", {
|
|
215
|
-
type: "button",
|
|
216
|
-
style: btnStyle,
|
|
217
|
-
title: "Lock/unlock",
|
|
218
|
-
onClick: () => {},
|
|
219
|
-
children: /* @__PURE__ */jsx(LockIcon, {})
|
|
220
|
-
}), /* @__PURE__ */jsxs("div", {
|
|
221
|
-
style: "font-size: 10px; text-align: center; color: #999; padding: 2px 0; user-select: none;",
|
|
222
|
-
title: "Current zoom level",
|
|
223
|
-
children: [zoomPercent, "%"]
|
|
224
|
-
})]
|
|
225
|
-
});
|
|
226
|
-
};
|
|
31
|
+
interface FlowNode<TData = Record<string, unknown>> {
|
|
32
|
+
id: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
position: XYPosition;
|
|
35
|
+
data: TData;
|
|
36
|
+
width?: number;
|
|
37
|
+
height?: number;
|
|
38
|
+
/** Whether the node can be dragged */
|
|
39
|
+
draggable?: boolean;
|
|
40
|
+
/** Whether the node can be selected */
|
|
41
|
+
selectable?: boolean;
|
|
42
|
+
/** Whether the node can be connected to */
|
|
43
|
+
connectable?: boolean;
|
|
44
|
+
/** Custom class name */
|
|
45
|
+
class?: string;
|
|
46
|
+
/** Custom style */
|
|
47
|
+
style?: string;
|
|
48
|
+
/** Source handles */
|
|
49
|
+
sourceHandles?: HandleConfig[];
|
|
50
|
+
/** Target handles */
|
|
51
|
+
targetHandles?: HandleConfig[];
|
|
52
|
+
/** Parent node id for grouping */
|
|
53
|
+
parentId?: string;
|
|
54
|
+
/** Whether this node is a group */
|
|
55
|
+
group?: boolean;
|
|
227
56
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const dx = targetNode.position.x + tw / 2 - (sourceNode.position.x + sw / 2);
|
|
245
|
-
const dy = targetNode.position.y + th / 2 - (sourceNode.position.y + sh / 2);
|
|
246
|
-
const sourceHandle = sourceNode.sourceHandles?.[0];
|
|
247
|
-
const targetHandle = targetNode.targetHandles?.[0];
|
|
248
|
-
return {
|
|
249
|
-
sourcePosition: sourceHandle ? sourceHandle.position : Math.abs(dx) > Math.abs(dy) ? dx > 0 ? Position.Right : Position.Left : dy > 0 ? Position.Bottom : Position.Top,
|
|
250
|
-
targetPosition: targetHandle ? targetHandle.position : Math.abs(dx) > Math.abs(dy) ? dx > 0 ? Position.Left : Position.Right : dy > 0 ? Position.Top : Position.Bottom
|
|
251
|
-
};
|
|
57
|
+
type EdgeType = 'bezier' | 'smoothstep' | 'straight' | 'step';
|
|
58
|
+
interface FlowEdge {
|
|
59
|
+
id?: string;
|
|
60
|
+
source: string;
|
|
61
|
+
target: string;
|
|
62
|
+
sourceHandle?: string;
|
|
63
|
+
targetHandle?: string;
|
|
64
|
+
type?: EdgeType;
|
|
65
|
+
label?: string;
|
|
66
|
+
animated?: boolean;
|
|
67
|
+
class?: string;
|
|
68
|
+
style?: string;
|
|
69
|
+
/** Custom data attached to the edge */
|
|
70
|
+
data?: Record<string, unknown>;
|
|
71
|
+
/** Waypoints — intermediate points the edge passes through */
|
|
72
|
+
waypoints?: XYPosition[];
|
|
252
73
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
x: (source.x + target.x) / 2,
|
|
259
|
-
y: (source.y + target.y) / 2
|
|
260
|
-
};
|
|
74
|
+
interface Connection {
|
|
75
|
+
source: string;
|
|
76
|
+
target: string;
|
|
77
|
+
sourceHandle?: string;
|
|
78
|
+
targetHandle?: string;
|
|
261
79
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
y: nodeY + nodeHeight / 2
|
|
286
|
-
};
|
|
287
|
-
}
|
|
80
|
+
type ConnectionRule = Record<string, {
|
|
81
|
+
outputs: string[];
|
|
82
|
+
}>;
|
|
83
|
+
type NodeChange = {
|
|
84
|
+
type: 'position';
|
|
85
|
+
id: string;
|
|
86
|
+
position: XYPosition;
|
|
87
|
+
} | {
|
|
88
|
+
type: 'dimensions';
|
|
89
|
+
id: string;
|
|
90
|
+
dimensions: Dimensions;
|
|
91
|
+
} | {
|
|
92
|
+
type: 'select';
|
|
93
|
+
id: string;
|
|
94
|
+
selected: boolean;
|
|
95
|
+
} | {
|
|
96
|
+
type: 'remove';
|
|
97
|
+
id: string;
|
|
98
|
+
};
|
|
99
|
+
interface EdgePathResult {
|
|
100
|
+
path: string;
|
|
101
|
+
labelX: number;
|
|
102
|
+
labelY: number;
|
|
288
103
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
*/
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
case Position.Bottom:
|
|
323
|
-
sourceControlY = sourceY + offset;
|
|
324
|
-
break;
|
|
325
|
-
case Position.Left:
|
|
326
|
-
sourceControlX = sourceX - offset;
|
|
327
|
-
break;
|
|
328
|
-
case Position.Right:
|
|
329
|
-
sourceControlX = sourceX + offset;
|
|
330
|
-
break;
|
|
331
|
-
}
|
|
332
|
-
switch (targetPosition) {
|
|
333
|
-
case Position.Top:
|
|
334
|
-
targetControlY = targetY - offset;
|
|
335
|
-
break;
|
|
336
|
-
case Position.Bottom:
|
|
337
|
-
targetControlY = targetY + offset;
|
|
338
|
-
break;
|
|
339
|
-
case Position.Left:
|
|
340
|
-
targetControlX = targetX - offset;
|
|
341
|
-
break;
|
|
342
|
-
case Position.Right:
|
|
343
|
-
targetControlX = targetX + offset;
|
|
344
|
-
break;
|
|
345
|
-
}
|
|
346
|
-
const center = getCenter({
|
|
347
|
-
x: sourceX,
|
|
348
|
-
y: sourceY
|
|
349
|
-
}, {
|
|
350
|
-
x: targetX,
|
|
351
|
-
y: targetY
|
|
352
|
-
});
|
|
353
|
-
return {
|
|
354
|
-
path: `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
|
|
355
|
-
labelX: center.x,
|
|
356
|
-
labelY: center.y
|
|
357
|
-
};
|
|
104
|
+
interface FlowConfig {
|
|
105
|
+
nodes?: FlowNode[];
|
|
106
|
+
edges?: FlowEdge[];
|
|
107
|
+
/** Default edge type */
|
|
108
|
+
defaultEdgeType?: EdgeType;
|
|
109
|
+
/** Min zoom level — default: 0.1 */
|
|
110
|
+
minZoom?: number;
|
|
111
|
+
/** Max zoom level — default: 4 */
|
|
112
|
+
maxZoom?: number;
|
|
113
|
+
/** Snap to grid */
|
|
114
|
+
snapToGrid?: boolean;
|
|
115
|
+
/** Grid size for snapping — default: 15 */
|
|
116
|
+
snapGrid?: number;
|
|
117
|
+
/** Connection rules — which node types can connect */
|
|
118
|
+
connectionRules?: ConnectionRule;
|
|
119
|
+
/** Whether nodes are draggable by default — default: true */
|
|
120
|
+
nodesDraggable?: boolean;
|
|
121
|
+
/** Whether nodes are connectable by default — default: true */
|
|
122
|
+
nodesConnectable?: boolean;
|
|
123
|
+
/** Whether nodes are selectable by default — default: true */
|
|
124
|
+
nodesSelectable?: boolean;
|
|
125
|
+
/** Whether to allow multi-selection — default: true */
|
|
126
|
+
multiSelect?: boolean;
|
|
127
|
+
/** Drag boundaries for nodes — [[minX, minY], [maxX, maxY]] */
|
|
128
|
+
nodeExtent?: [[number, number], [number, number]];
|
|
129
|
+
/** Whether panning is enabled — default: true */
|
|
130
|
+
pannable?: boolean;
|
|
131
|
+
/** Whether zooming is enabled — default: true */
|
|
132
|
+
zoomable?: boolean;
|
|
133
|
+
/** Fit view on initial render — default: false */
|
|
134
|
+
fitView?: boolean;
|
|
135
|
+
/** Padding for fitView — default: 0.1 */
|
|
136
|
+
fitViewPadding?: number;
|
|
358
137
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
138
|
+
interface FlowInstance {
|
|
139
|
+
/** All nodes — reactive */
|
|
140
|
+
nodes: Signal<FlowNode[]>;
|
|
141
|
+
/** All edges — reactive */
|
|
142
|
+
edges: Signal<FlowEdge[]>;
|
|
143
|
+
/** Viewport state — reactive */
|
|
144
|
+
viewport: Signal<Viewport>;
|
|
145
|
+
/** Current zoom level — computed */
|
|
146
|
+
zoom: Computed<number>;
|
|
147
|
+
/** Selected node ids — computed */
|
|
148
|
+
selectedNodes: Computed<string[]>;
|
|
149
|
+
/** Selected edge ids — computed */
|
|
150
|
+
selectedEdges: Computed<string[]>;
|
|
151
|
+
/** Container dimensions — updated by the Flow component via ResizeObserver */
|
|
152
|
+
containerSize: Signal<{
|
|
153
|
+
width: number;
|
|
154
|
+
height: number;
|
|
155
|
+
}>;
|
|
156
|
+
/** Get a single node by id */
|
|
157
|
+
getNode: (id: string) => FlowNode | undefined;
|
|
158
|
+
/** Add a node */
|
|
159
|
+
addNode: (node: FlowNode) => void;
|
|
160
|
+
/** Remove a node and its connected edges */
|
|
161
|
+
removeNode: (id: string) => void;
|
|
162
|
+
/** Update a node's properties */
|
|
163
|
+
updateNode: (id: string, update: Partial<FlowNode>) => void;
|
|
164
|
+
/** Update a node's position */
|
|
165
|
+
updateNodePosition: (id: string, position: XYPosition) => void;
|
|
166
|
+
/** Get a single edge by id */
|
|
167
|
+
getEdge: (id: string) => FlowEdge | undefined;
|
|
168
|
+
/** Add an edge */
|
|
169
|
+
addEdge: (edge: FlowEdge) => void;
|
|
170
|
+
/** Remove an edge */
|
|
171
|
+
removeEdge: (id: string) => void;
|
|
172
|
+
/** Check if a connection is valid (based on rules) */
|
|
173
|
+
isValidConnection: (connection: Connection) => boolean;
|
|
174
|
+
/** Select a node */
|
|
175
|
+
selectNode: (id: string, additive?: boolean) => void;
|
|
176
|
+
/** Deselect a node */
|
|
177
|
+
deselectNode: (id: string) => void;
|
|
178
|
+
/** Select an edge */
|
|
179
|
+
selectEdge: (id: string, additive?: boolean) => void;
|
|
180
|
+
/** Clear all selection */
|
|
181
|
+
clearSelection: () => void;
|
|
182
|
+
/** Select all nodes */
|
|
183
|
+
selectAll: () => void;
|
|
184
|
+
/** Delete selected nodes/edges */
|
|
185
|
+
deleteSelected: () => void;
|
|
186
|
+
/** Fit view to show all nodes */
|
|
187
|
+
fitView: (nodeIds?: string[], padding?: number) => void;
|
|
188
|
+
/** Set zoom level */
|
|
189
|
+
zoomTo: (zoom: number) => void;
|
|
190
|
+
/** Zoom in */
|
|
191
|
+
zoomIn: () => void;
|
|
192
|
+
/** Zoom out */
|
|
193
|
+
zoomOut: () => void;
|
|
194
|
+
/** Pan to position */
|
|
195
|
+
panTo: (position: XYPosition) => void;
|
|
196
|
+
/** Check if a node is visible in the current viewport */
|
|
197
|
+
isNodeVisible: (id: string) => boolean;
|
|
198
|
+
/** Apply auto-layout using elkjs */
|
|
199
|
+
layout: (algorithm?: LayoutAlgorithm, options?: LayoutOptions) => Promise<void>;
|
|
200
|
+
/** Batch multiple operations */
|
|
201
|
+
batch: (fn: () => void) => void;
|
|
202
|
+
/** Get edges connected to a node */
|
|
203
|
+
getConnectedEdges: (nodeId: string) => FlowEdge[];
|
|
204
|
+
/** Get incoming edges for a node */
|
|
205
|
+
getIncomers: (nodeId: string) => FlowNode[];
|
|
206
|
+
/** Get outgoing edges from a node */
|
|
207
|
+
getOutgoers: (nodeId: string) => FlowNode[];
|
|
208
|
+
/** Called when a connection is made */
|
|
209
|
+
onConnect: (callback: (connection: Connection) => void) => () => void;
|
|
210
|
+
/** Called when nodes change */
|
|
211
|
+
onNodesChange: (callback: (changes: NodeChange[]) => void) => () => void;
|
|
212
|
+
/** Called when a node is clicked */
|
|
213
|
+
onNodeClick: (callback: (node: FlowNode) => void) => () => void;
|
|
214
|
+
/** Called when an edge is clicked */
|
|
215
|
+
onEdgeClick: (callback: (edge: FlowEdge) => void) => () => void;
|
|
216
|
+
/** Called when a node starts being dragged */
|
|
217
|
+
onNodeDragStart: (callback: (node: FlowNode) => void) => () => void;
|
|
218
|
+
/** Called when a node stops being dragged */
|
|
219
|
+
onNodeDragEnd: (callback: (node: FlowNode) => void) => () => void;
|
|
220
|
+
/** Called when a node is double-clicked */
|
|
221
|
+
onNodeDoubleClick: (callback: (node: FlowNode) => void) => () => void;
|
|
222
|
+
/** Copy selected nodes and their edges to clipboard */
|
|
223
|
+
copySelected: () => void;
|
|
224
|
+
/** Paste clipboard contents with offset */
|
|
225
|
+
paste: (offset?: XYPosition) => void;
|
|
226
|
+
/** Save current state to undo history */
|
|
227
|
+
pushHistory: () => void;
|
|
228
|
+
/** Undo last change */
|
|
229
|
+
undo: () => void;
|
|
230
|
+
/** Redo last undone change */
|
|
231
|
+
redo: () => void;
|
|
232
|
+
/** Move all selected nodes by dx/dy */
|
|
233
|
+
moveSelectedNodes: (dx: number, dy: number) => void;
|
|
234
|
+
/** Get snap guide lines for a dragged node */
|
|
235
|
+
getSnapLines: (dragNodeId: string, position: XYPosition, threshold?: number) => {
|
|
236
|
+
x: number | null;
|
|
237
|
+
y: number | null;
|
|
238
|
+
snappedPosition: XYPosition;
|
|
405
239
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
240
|
+
/** Get child nodes of a group node */
|
|
241
|
+
getChildNodes: (parentId: string) => FlowNode[];
|
|
242
|
+
/** Get absolute position of a node (accounting for parent offsets) */
|
|
243
|
+
getAbsolutePosition: (nodeId: string) => XYPosition;
|
|
244
|
+
/** Add a waypoint (bend point) to an edge */
|
|
245
|
+
addEdgeWaypoint: (edgeId: string, point: XYPosition, index?: number) => void;
|
|
246
|
+
/** Remove a waypoint from an edge */
|
|
247
|
+
removeEdgeWaypoint: (edgeId: string, index: number) => void;
|
|
248
|
+
/** Update a waypoint position */
|
|
249
|
+
updateEdgeWaypoint: (edgeId: string, index: number, point: XYPosition) => void;
|
|
250
|
+
/** Reconnect an edge to a new source/target */
|
|
251
|
+
reconnectEdge: (edgeId: string, newConnection: {
|
|
252
|
+
source?: string;
|
|
253
|
+
target?: string;
|
|
254
|
+
sourceHandle?: string;
|
|
255
|
+
targetHandle?: string;
|
|
256
|
+
}) => void;
|
|
257
|
+
/** Find the nearest unconnected node within threshold distance */
|
|
258
|
+
getProximityConnection: (nodeId: string, threshold?: number) => Connection | null;
|
|
259
|
+
/** Get nodes that overlap with the given node */
|
|
260
|
+
getOverlappingNodes: (nodeId: string) => FlowNode[];
|
|
261
|
+
/** Push overlapping nodes apart */
|
|
262
|
+
resolveCollisions: (nodeId: string, spacing?: number) => void;
|
|
263
|
+
/** Set drag boundaries for all nodes — [[minX, minY], [maxX, maxY]] or null to remove */
|
|
264
|
+
setNodeExtent: (extent: [[number, number], [number, number]] | null) => void;
|
|
265
|
+
/** Clamp a position to the current node extent */
|
|
266
|
+
clampToExtent: (position: XYPosition, nodeWidth?: number, nodeHeight?: number) => XYPosition;
|
|
267
|
+
/** Find nodes matching a predicate */
|
|
268
|
+
findNodes: (predicate: (node: FlowNode) => boolean) => FlowNode[];
|
|
269
|
+
/** Find nodes by label text (case-insensitive) */
|
|
270
|
+
searchNodes: (query: string) => FlowNode[];
|
|
271
|
+
/** Focus viewport on a specific node (pan + optional zoom) */
|
|
272
|
+
focusNode: (nodeId: string, zoom?: number) => void;
|
|
273
|
+
/** Export the flow as a JSON-serializable object */
|
|
274
|
+
toJSON: () => {
|
|
275
|
+
nodes: FlowNode[];
|
|
276
|
+
edges: FlowEdge[];
|
|
277
|
+
viewport: Viewport;
|
|
278
|
+
};
|
|
279
|
+
/** Import flow state from a JSON object */
|
|
280
|
+
fromJSON: (data: {
|
|
281
|
+
nodes: FlowNode[];
|
|
282
|
+
edges: FlowEdge[];
|
|
283
|
+
viewport?: Viewport;
|
|
284
|
+
}) => void;
|
|
285
|
+
/** Animate viewport to a new position/zoom */
|
|
286
|
+
animateViewport: (target: Partial<Viewport>, duration?: number) => void;
|
|
287
|
+
/** @internal */
|
|
288
|
+
_emit: {
|
|
289
|
+
nodeDragStart: (node: FlowNode) => void;
|
|
290
|
+
nodeDragEnd: (node: FlowNode) => void;
|
|
291
|
+
nodeDoubleClick: (node: FlowNode) => void;
|
|
292
|
+
nodeClick: (node: FlowNode) => void;
|
|
293
|
+
edgeClick: (edge: FlowEdge) => void;
|
|
428
294
|
};
|
|
295
|
+
/** The flow configuration */
|
|
296
|
+
config: FlowConfig;
|
|
297
|
+
/** Dispose all listeners and clean up */
|
|
298
|
+
dispose: () => void;
|
|
429
299
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
*/
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
300
|
+
type LayoutAlgorithm = 'layered' | 'force' | 'stress' | 'tree' | 'radial' | 'box' | 'rectpacking';
|
|
301
|
+
interface LayoutOptions {
|
|
302
|
+
/** Layout direction — default: 'DOWN' */
|
|
303
|
+
direction?: 'UP' | 'DOWN' | 'LEFT' | 'RIGHT';
|
|
304
|
+
/** Spacing between nodes — default: 50 */
|
|
305
|
+
nodeSpacing?: number;
|
|
306
|
+
/** Spacing between layers — default: 80 */
|
|
307
|
+
layerSpacing?: number;
|
|
308
|
+
/** Edge routing — default: 'orthogonal' */
|
|
309
|
+
edgeRouting?: 'orthogonal' | 'splines' | 'polyline';
|
|
310
|
+
/** Whether to animate the layout transition — default: true */
|
|
311
|
+
animate?: boolean;
|
|
312
|
+
/** Animation duration in ms — default: 300 */
|
|
313
|
+
animationDuration?: number;
|
|
438
314
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const {
|
|
445
|
-
sourceX,
|
|
446
|
-
sourceY,
|
|
447
|
-
targetX,
|
|
448
|
-
targetY,
|
|
449
|
-
waypoints
|
|
450
|
-
} = params;
|
|
451
|
-
if (waypoints.length === 0) return getStraightPath({
|
|
452
|
-
sourceX,
|
|
453
|
-
sourceY,
|
|
454
|
-
targetX,
|
|
455
|
-
targetY
|
|
456
|
-
});
|
|
457
|
-
const path = `M${[{
|
|
458
|
-
x: sourceX,
|
|
459
|
-
y: sourceY
|
|
460
|
-
}, ...waypoints, {
|
|
461
|
-
x: targetX,
|
|
462
|
-
y: targetY
|
|
463
|
-
}].map(p => `${p.x},${p.y}`).join(" L")}`;
|
|
464
|
-
const midPoint = waypoints[Math.floor(waypoints.length / 2)] ?? {
|
|
465
|
-
x: (sourceX + targetX) / 2,
|
|
466
|
-
y: (sourceY + targetY) / 2
|
|
467
|
-
};
|
|
468
|
-
return {
|
|
469
|
-
path,
|
|
470
|
-
labelX: midPoint.x,
|
|
471
|
-
labelY: midPoint.y
|
|
472
|
-
};
|
|
315
|
+
interface FlowProps {
|
|
316
|
+
instance: FlowInstance;
|
|
317
|
+
style?: string;
|
|
318
|
+
class?: string;
|
|
319
|
+
children?: VNodeChild;
|
|
473
320
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
case "smoothstep":
|
|
480
|
-
return getSmoothStepPath({
|
|
481
|
-
sourceX,
|
|
482
|
-
sourceY,
|
|
483
|
-
sourcePosition,
|
|
484
|
-
targetX,
|
|
485
|
-
targetY,
|
|
486
|
-
targetPosition
|
|
487
|
-
});
|
|
488
|
-
case "straight":
|
|
489
|
-
return getStraightPath({
|
|
490
|
-
sourceX,
|
|
491
|
-
sourceY,
|
|
492
|
-
targetX,
|
|
493
|
-
targetY
|
|
494
|
-
});
|
|
495
|
-
case "step":
|
|
496
|
-
return getStepPath({
|
|
497
|
-
sourceX,
|
|
498
|
-
sourceY,
|
|
499
|
-
sourcePosition,
|
|
500
|
-
targetX,
|
|
501
|
-
targetY,
|
|
502
|
-
targetPosition
|
|
503
|
-
});
|
|
504
|
-
default:
|
|
505
|
-
return getBezierPath({
|
|
506
|
-
sourceX,
|
|
507
|
-
sourceY,
|
|
508
|
-
sourcePosition,
|
|
509
|
-
targetX,
|
|
510
|
-
targetY,
|
|
511
|
-
targetPosition
|
|
512
|
-
});
|
|
513
|
-
}
|
|
321
|
+
interface BackgroundProps {
|
|
322
|
+
variant?: 'dots' | 'lines' | 'cross';
|
|
323
|
+
gap?: number;
|
|
324
|
+
size?: number;
|
|
325
|
+
color?: string;
|
|
514
326
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
return /* @__PURE__ */jsx("div", {
|
|
523
|
-
style: `padding: 8px 16px; background: white; border: 2px solid ${props.selected ? "#3b82f6" : "#ddd"}; border-radius: 6px; font-size: 13px; min-width: 80px; text-align: center; cursor: ${props.dragging ? "grabbing" : "grab"}; user-select: none;`,
|
|
524
|
-
children: props.data?.label ?? props.id
|
|
525
|
-
});
|
|
327
|
+
interface MiniMapProps {
|
|
328
|
+
style?: string;
|
|
329
|
+
class?: string;
|
|
330
|
+
nodeColor?: string | ((node: FlowNode) => string);
|
|
331
|
+
maskColor?: string;
|
|
332
|
+
width?: number;
|
|
333
|
+
height?: number;
|
|
526
334
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
return () => {
|
|
534
|
-
const nodes = instance.nodes();
|
|
535
|
-
const edges = instance.edges();
|
|
536
|
-
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
537
|
-
const conn = connectionState();
|
|
538
|
-
return /* @__PURE__ */jsxs("svg", {
|
|
539
|
-
role: "img",
|
|
540
|
-
"aria-label": "flow edges",
|
|
541
|
-
class: "pyreon-flow-edges",
|
|
542
|
-
style: "position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible;",
|
|
543
|
-
children: [/* @__PURE__ */jsx("defs", {
|
|
544
|
-
children: /* @__PURE__ */jsx("marker", {
|
|
545
|
-
id: "flow-arrowhead",
|
|
546
|
-
markerWidth: "10",
|
|
547
|
-
markerHeight: "7",
|
|
548
|
-
refX: "10",
|
|
549
|
-
refY: "3.5",
|
|
550
|
-
orient: "auto",
|
|
551
|
-
children: /* @__PURE__ */jsx("polygon", {
|
|
552
|
-
points: "0 0, 10 3.5, 0 7",
|
|
553
|
-
fill: "#999"
|
|
554
|
-
})
|
|
555
|
-
})
|
|
556
|
-
}), edges.map(edge => {
|
|
557
|
-
const sourceNode = nodeMap.get(edge.source);
|
|
558
|
-
const targetNode = nodeMap.get(edge.target);
|
|
559
|
-
if (!sourceNode || !targetNode) return /* @__PURE__ */jsx("g", {}, edge.id);
|
|
560
|
-
const sourceW = sourceNode.width ?? 150;
|
|
561
|
-
const sourceH = sourceNode.height ?? 40;
|
|
562
|
-
const targetW = targetNode.width ?? 150;
|
|
563
|
-
const targetH = targetNode.height ?? 40;
|
|
564
|
-
const {
|
|
565
|
-
sourcePosition,
|
|
566
|
-
targetPosition
|
|
567
|
-
} = getSmartHandlePositions(sourceNode, targetNode);
|
|
568
|
-
const sourcePos = getHandlePosition(sourcePosition, sourceNode.position.x, sourceNode.position.y, sourceW, sourceH);
|
|
569
|
-
const targetPos = getHandlePosition(targetPosition, targetNode.position.x, targetNode.position.y, targetW, targetH);
|
|
570
|
-
const {
|
|
571
|
-
path,
|
|
572
|
-
labelX,
|
|
573
|
-
labelY
|
|
574
|
-
} = edge.waypoints?.length ? getWaypointPath({
|
|
575
|
-
sourceX: sourcePos.x,
|
|
576
|
-
sourceY: sourcePos.y,
|
|
577
|
-
targetX: targetPos.x,
|
|
578
|
-
targetY: targetPos.y,
|
|
579
|
-
waypoints: edge.waypoints
|
|
580
|
-
}) : getEdgePath(edge.type ?? "bezier", sourcePos.x, sourcePos.y, sourcePosition, targetPos.x, targetPos.y, targetPosition);
|
|
581
|
-
const selectedEdges = instance.selectedEdges();
|
|
582
|
-
const isSelected = edge.id ? selectedEdges.includes(edge.id) : false;
|
|
583
|
-
const CustomEdge = edge.type && edgeTypes?.[edge.type];
|
|
584
|
-
if (CustomEdge) return /* @__PURE__ */jsx("g", {
|
|
585
|
-
onClick: () => edge.id && instance.selectEdge(edge.id),
|
|
586
|
-
children: /* @__PURE__ */jsx(CustomEdge, {
|
|
587
|
-
edge,
|
|
588
|
-
sourceX: sourcePos.x,
|
|
589
|
-
sourceY: sourcePos.y,
|
|
590
|
-
targetX: targetPos.x,
|
|
591
|
-
targetY: targetPos.y,
|
|
592
|
-
selected: isSelected
|
|
593
|
-
})
|
|
594
|
-
}, edge.id);
|
|
595
|
-
return /* @__PURE__ */jsxs("g", {
|
|
596
|
-
children: [/* @__PURE__ */jsx("path", {
|
|
597
|
-
d: path,
|
|
598
|
-
fill: "none",
|
|
599
|
-
stroke: isSelected ? "#3b82f6" : "#999",
|
|
600
|
-
"stroke-width": isSelected ? "2" : "1.5",
|
|
601
|
-
"marker-end": "url(#flow-arrowhead)",
|
|
602
|
-
class: edge.animated ? "pyreon-flow-edge-animated" : "",
|
|
603
|
-
style: `pointer-events: stroke; cursor: pointer; ${edge.style ?? ""}`,
|
|
604
|
-
onClick: () => {
|
|
605
|
-
if (edge.id) instance.selectEdge(edge.id);
|
|
606
|
-
instance._emit.edgeClick(edge);
|
|
607
|
-
}
|
|
608
|
-
}), edge.label && /* @__PURE__ */jsx("text", {
|
|
609
|
-
x: String(labelX),
|
|
610
|
-
y: String(labelY),
|
|
611
|
-
"text-anchor": "middle",
|
|
612
|
-
"dominant-baseline": "central",
|
|
613
|
-
style: "font-size: 11px; fill: #666; pointer-events: none;",
|
|
614
|
-
children: edge.label
|
|
615
|
-
})]
|
|
616
|
-
}, edge.id);
|
|
617
|
-
}), conn.active && /* @__PURE__ */jsx("path", {
|
|
618
|
-
d: getEdgePath("bezier", conn.sourceX, conn.sourceY, conn.sourcePosition, conn.currentX, conn.currentY, Position.Left).path,
|
|
619
|
-
fill: "none",
|
|
620
|
-
stroke: "#3b82f6",
|
|
621
|
-
"stroke-width": "2",
|
|
622
|
-
"stroke-dasharray": "5,5"
|
|
623
|
-
})]
|
|
624
|
-
});
|
|
625
|
-
};
|
|
335
|
+
interface ControlsProps {
|
|
336
|
+
showZoomIn?: boolean;
|
|
337
|
+
showZoomOut?: boolean;
|
|
338
|
+
showFitView?: boolean;
|
|
339
|
+
showLock?: boolean;
|
|
340
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
626
341
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
onNodePointerDown,
|
|
633
|
-
onHandlePointerDown
|
|
634
|
-
} = props;
|
|
635
|
-
return () => {
|
|
636
|
-
const nodes = instance.nodes();
|
|
637
|
-
const selectedIds = instance.selectedNodes();
|
|
638
|
-
const dragId = draggingNodeId();
|
|
639
|
-
return /* @__PURE__ */jsx(Fragment, {
|
|
640
|
-
children: nodes.map(node => {
|
|
641
|
-
const isSelected = selectedIds.includes(node.id);
|
|
642
|
-
const isDragging = dragId === node.id;
|
|
643
|
-
const NodeComponent = node.type && nodeTypes[node.type] || nodeTypes.default;
|
|
644
|
-
return /* @__PURE__ */jsx("div", {
|
|
645
|
-
class: `pyreon-flow-node ${node.class ?? ""} ${isSelected ? "selected" : ""} ${isDragging ? "dragging" : ""}`,
|
|
646
|
-
style: `position: absolute; transform: translate(${node.position.x}px, ${node.position.y}px); z-index: ${isDragging ? 1e3 : isSelected ? 100 : 0}; ${node.style ?? ""}`,
|
|
647
|
-
"data-nodeid": node.id,
|
|
648
|
-
onClick: e => {
|
|
649
|
-
e.stopPropagation();
|
|
650
|
-
instance.selectNode(node.id, e.shiftKey);
|
|
651
|
-
instance._emit.nodeClick(node);
|
|
652
|
-
},
|
|
653
|
-
onDblclick: e => {
|
|
654
|
-
e.stopPropagation();
|
|
655
|
-
instance._emit.nodeDoubleClick(node);
|
|
656
|
-
},
|
|
657
|
-
onPointerdown: e => {
|
|
658
|
-
const handle = e.target.closest(".pyreon-flow-handle");
|
|
659
|
-
if (handle) {
|
|
660
|
-
const hType = handle.getAttribute("data-handletype") ?? "source";
|
|
661
|
-
const hId = handle.getAttribute("data-handleid") ?? "source";
|
|
662
|
-
const hPos = handle.getAttribute("data-handleposition") ?? Position.Right;
|
|
663
|
-
onHandlePointerDown(e, node.id, hType, hId, hPos);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (node.draggable !== false && instance.config.nodesDraggable !== false) onNodePointerDown(e, node);
|
|
667
|
-
},
|
|
668
|
-
children: /* @__PURE__ */jsx(NodeComponent, {
|
|
669
|
-
id: node.id,
|
|
670
|
-
data: node.data,
|
|
671
|
-
selected: isSelected,
|
|
672
|
-
dragging: isDragging
|
|
673
|
-
})
|
|
674
|
-
}, node.id);
|
|
675
|
-
})
|
|
676
|
-
});
|
|
677
|
-
};
|
|
342
|
+
interface PanelProps {
|
|
343
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
344
|
+
style?: string;
|
|
345
|
+
class?: string;
|
|
346
|
+
children?: VNodeChild;
|
|
678
347
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
* @example
|
|
686
|
-
* ```tsx
|
|
687
|
-
* const flow = createFlow({
|
|
688
|
-
* nodes: [...],
|
|
689
|
-
* edges: [...],
|
|
690
|
-
* })
|
|
691
|
-
*
|
|
692
|
-
* <Flow instance={flow} nodeTypes={{ custom: CustomNode }}>
|
|
693
|
-
* <Background />
|
|
694
|
-
* <MiniMap />
|
|
695
|
-
* <Controls />
|
|
696
|
-
* </Flow>
|
|
697
|
-
* ```
|
|
698
|
-
*/
|
|
699
|
-
function Flow(props) {
|
|
700
|
-
const {
|
|
701
|
-
instance,
|
|
702
|
-
children,
|
|
703
|
-
edgeTypes
|
|
704
|
-
} = props;
|
|
705
|
-
const nodeTypes = {
|
|
706
|
-
default: DefaultNode,
|
|
707
|
-
input: DefaultNode,
|
|
708
|
-
output: DefaultNode,
|
|
709
|
-
...props.nodeTypes
|
|
710
|
-
};
|
|
711
|
-
const dragState = signal({
|
|
712
|
-
...emptyDrag
|
|
713
|
-
});
|
|
714
|
-
const connectionState = signal({
|
|
715
|
-
...emptyConnection
|
|
716
|
-
});
|
|
717
|
-
const selectionBox = signal({
|
|
718
|
-
...emptySelectionBox
|
|
719
|
-
});
|
|
720
|
-
const helperLines = signal({
|
|
721
|
-
x: null,
|
|
722
|
-
y: null
|
|
723
|
-
});
|
|
724
|
-
const draggingNodeId = () => dragState().active ? dragState().nodeId : "";
|
|
725
|
-
const handleNodePointerDown = (e, node) => {
|
|
726
|
-
e.stopPropagation();
|
|
727
|
-
const selected = instance.selectedNodes();
|
|
728
|
-
const startPositions = /* @__PURE__ */new Map();
|
|
729
|
-
startPositions.set(node.id, {
|
|
730
|
-
...node.position
|
|
731
|
-
});
|
|
732
|
-
if (selected.includes(node.id)) for (const nid of selected) {
|
|
733
|
-
if (nid === node.id) continue;
|
|
734
|
-
const n = instance.getNode(nid);
|
|
735
|
-
if (n) startPositions.set(nid, {
|
|
736
|
-
...n.position
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
instance.pushHistory();
|
|
740
|
-
dragState.set({
|
|
741
|
-
active: true,
|
|
742
|
-
nodeId: node.id,
|
|
743
|
-
startX: e.clientX,
|
|
744
|
-
startY: e.clientY,
|
|
745
|
-
startPositions
|
|
746
|
-
});
|
|
747
|
-
instance.selectNode(node.id, e.shiftKey);
|
|
748
|
-
instance._emit.nodeDragStart(node);
|
|
749
|
-
const container = e.currentTarget.closest(".pyreon-flow");
|
|
750
|
-
if (container) container.setPointerCapture(e.pointerId);
|
|
751
|
-
};
|
|
752
|
-
const handleHandlePointerDown = (e, nodeId, _handleType, handleId, position) => {
|
|
753
|
-
e.stopPropagation();
|
|
754
|
-
e.preventDefault();
|
|
755
|
-
const node = instance.getNode(nodeId);
|
|
756
|
-
if (!node) return;
|
|
757
|
-
const w = node.width ?? 150;
|
|
758
|
-
const h = node.height ?? 40;
|
|
759
|
-
const handlePos = getHandlePosition(position, node.position.x, node.position.y, w, h);
|
|
760
|
-
connectionState.set({
|
|
761
|
-
active: true,
|
|
762
|
-
sourceNodeId: nodeId,
|
|
763
|
-
sourceHandleId: handleId,
|
|
764
|
-
sourcePosition: position,
|
|
765
|
-
sourceX: handlePos.x,
|
|
766
|
-
sourceY: handlePos.y,
|
|
767
|
-
currentX: handlePos.x,
|
|
768
|
-
currentY: handlePos.y
|
|
769
|
-
});
|
|
770
|
-
const container = e.target.closest(".pyreon-flow");
|
|
771
|
-
if (container) container.setPointerCapture(e.pointerId);
|
|
772
|
-
};
|
|
773
|
-
const handleWheel = e => {
|
|
774
|
-
if (instance.config.zoomable === false) return;
|
|
775
|
-
e.preventDefault();
|
|
776
|
-
const delta = -e.deltaY * .001;
|
|
777
|
-
const newZoom = Math.min(Math.max(instance.viewport.peek().zoom * (1 + delta), instance.config.minZoom ?? .1), instance.config.maxZoom ?? 4);
|
|
778
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
779
|
-
const mouseX = e.clientX - rect.left;
|
|
780
|
-
const mouseY = e.clientY - rect.top;
|
|
781
|
-
const vp = instance.viewport.peek();
|
|
782
|
-
const scale = newZoom / vp.zoom;
|
|
783
|
-
instance.viewport.set({
|
|
784
|
-
x: mouseX - (mouseX - vp.x) * scale,
|
|
785
|
-
y: mouseY - (mouseY - vp.y) * scale,
|
|
786
|
-
zoom: newZoom
|
|
787
|
-
});
|
|
788
|
-
};
|
|
789
|
-
let isPanning = false;
|
|
790
|
-
let panStartX = 0;
|
|
791
|
-
let panStartY = 0;
|
|
792
|
-
let panStartVpX = 0;
|
|
793
|
-
let panStartVpY = 0;
|
|
794
|
-
const handlePointerDown = e => {
|
|
795
|
-
if (instance.config.pannable === false) return;
|
|
796
|
-
const target = e.target;
|
|
797
|
-
if (target.closest(".pyreon-flow-node")) return;
|
|
798
|
-
if (target.closest(".pyreon-flow-handle")) return;
|
|
799
|
-
if (e.shiftKey && instance.config.multiSelect !== false) {
|
|
800
|
-
const container = e.currentTarget;
|
|
801
|
-
const rect = container.getBoundingClientRect();
|
|
802
|
-
const vp = instance.viewport.peek();
|
|
803
|
-
const flowX = (e.clientX - rect.left - vp.x) / vp.zoom;
|
|
804
|
-
const flowY = (e.clientY - rect.top - vp.y) / vp.zoom;
|
|
805
|
-
selectionBox.set({
|
|
806
|
-
active: true,
|
|
807
|
-
startX: flowX,
|
|
808
|
-
startY: flowY,
|
|
809
|
-
currentX: flowX,
|
|
810
|
-
currentY: flowY
|
|
811
|
-
});
|
|
812
|
-
container.setPointerCapture(e.pointerId);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
isPanning = true;
|
|
816
|
-
panStartX = e.clientX;
|
|
817
|
-
panStartY = e.clientY;
|
|
818
|
-
const vp = instance.viewport.peek();
|
|
819
|
-
panStartVpX = vp.x;
|
|
820
|
-
panStartVpY = vp.y;
|
|
821
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
822
|
-
instance.clearSelection();
|
|
823
|
-
};
|
|
824
|
-
const handlePointerMove = e => {
|
|
825
|
-
const drag = dragState.peek();
|
|
826
|
-
const conn = connectionState.peek();
|
|
827
|
-
const sel = selectionBox.peek();
|
|
828
|
-
if (sel.active) {
|
|
829
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
830
|
-
const vp = instance.viewport.peek();
|
|
831
|
-
const flowX = (e.clientX - rect.left - vp.x) / vp.zoom;
|
|
832
|
-
const flowY = (e.clientY - rect.top - vp.y) / vp.zoom;
|
|
833
|
-
selectionBox.set({
|
|
834
|
-
...sel,
|
|
835
|
-
currentX: flowX,
|
|
836
|
-
currentY: flowY
|
|
837
|
-
});
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
if (drag.active) {
|
|
841
|
-
const vp = instance.viewport.peek();
|
|
842
|
-
const dx = (e.clientX - drag.startX) / vp.zoom;
|
|
843
|
-
const dy = (e.clientY - drag.startY) / vp.zoom;
|
|
844
|
-
const primaryStart = drag.startPositions.get(drag.nodeId);
|
|
845
|
-
if (!primaryStart) return;
|
|
846
|
-
const rawPos = {
|
|
847
|
-
x: primaryStart.x + dx,
|
|
848
|
-
y: primaryStart.y + dy
|
|
849
|
-
};
|
|
850
|
-
const snap = instance.getSnapLines(drag.nodeId, rawPos);
|
|
851
|
-
helperLines.set({
|
|
852
|
-
x: snap.x,
|
|
853
|
-
y: snap.y
|
|
854
|
-
});
|
|
855
|
-
const actualDx = snap.snappedPosition.x - primaryStart.x;
|
|
856
|
-
const actualDy = snap.snappedPosition.y - primaryStart.y;
|
|
857
|
-
instance.nodes.update(nds => nds.map(n => {
|
|
858
|
-
const start = drag.startPositions.get(n.id);
|
|
859
|
-
if (!start) return n;
|
|
860
|
-
return {
|
|
861
|
-
...n,
|
|
862
|
-
position: {
|
|
863
|
-
x: start.x + actualDx,
|
|
864
|
-
y: start.y + actualDy
|
|
865
|
-
}
|
|
866
|
-
};
|
|
867
|
-
}));
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
if (conn.active) {
|
|
871
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
872
|
-
const vp = instance.viewport.peek();
|
|
873
|
-
const flowX = (e.clientX - rect.left - vp.x) / vp.zoom;
|
|
874
|
-
const flowY = (e.clientY - rect.top - vp.y) / vp.zoom;
|
|
875
|
-
connectionState.set({
|
|
876
|
-
...conn,
|
|
877
|
-
currentX: flowX,
|
|
878
|
-
currentY: flowY
|
|
879
|
-
});
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
if (isPanning) {
|
|
883
|
-
const dx = e.clientX - panStartX;
|
|
884
|
-
const dy = e.clientY - panStartY;
|
|
885
|
-
instance.viewport.set({
|
|
886
|
-
...instance.viewport.peek(),
|
|
887
|
-
x: panStartVpX + dx,
|
|
888
|
-
y: panStartVpY + dy
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
const handlePointerUp = e => {
|
|
893
|
-
const drag = dragState.peek();
|
|
894
|
-
const conn = connectionState.peek();
|
|
895
|
-
const sel = selectionBox.peek();
|
|
896
|
-
if (sel.active) {
|
|
897
|
-
const minX = Math.min(sel.startX, sel.currentX);
|
|
898
|
-
const minY = Math.min(sel.startY, sel.currentY);
|
|
899
|
-
const maxX = Math.max(sel.startX, sel.currentX);
|
|
900
|
-
const maxY = Math.max(sel.startY, sel.currentY);
|
|
901
|
-
instance.clearSelection();
|
|
902
|
-
for (const node of instance.nodes.peek()) {
|
|
903
|
-
const w = node.width ?? 150;
|
|
904
|
-
const h = node.height ?? 40;
|
|
905
|
-
const nx = node.position.x;
|
|
906
|
-
const ny = node.position.y;
|
|
907
|
-
if (nx + w > minX && nx < maxX && ny + h > minY && ny < maxY) instance.selectNode(node.id, true);
|
|
908
|
-
}
|
|
909
|
-
selectionBox.set({
|
|
910
|
-
...emptySelectionBox
|
|
911
|
-
});
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
if (drag.active) {
|
|
915
|
-
const node = instance.getNode(drag.nodeId);
|
|
916
|
-
if (node) instance._emit.nodeDragEnd(node);
|
|
917
|
-
dragState.set({
|
|
918
|
-
...emptyDrag
|
|
919
|
-
});
|
|
920
|
-
helperLines.set({
|
|
921
|
-
x: null,
|
|
922
|
-
y: null
|
|
923
|
-
});
|
|
924
|
-
}
|
|
925
|
-
if (conn.active) {
|
|
926
|
-
const handle = e.target.closest(".pyreon-flow-handle");
|
|
927
|
-
if (handle) {
|
|
928
|
-
const targetNodeId = handle.closest(".pyreon-flow-node")?.getAttribute("data-nodeid") ?? "";
|
|
929
|
-
const targetHandleId = handle.getAttribute("data-handleid") ?? "target";
|
|
930
|
-
if (targetNodeId && targetNodeId !== conn.sourceNodeId) {
|
|
931
|
-
const connection = {
|
|
932
|
-
source: conn.sourceNodeId,
|
|
933
|
-
target: targetNodeId,
|
|
934
|
-
sourceHandle: conn.sourceHandleId,
|
|
935
|
-
targetHandle: targetHandleId
|
|
936
|
-
};
|
|
937
|
-
if (instance.isValidConnection(connection)) instance.addEdge({
|
|
938
|
-
source: connection.source,
|
|
939
|
-
target: connection.target,
|
|
940
|
-
sourceHandle: connection.sourceHandle,
|
|
941
|
-
targetHandle: connection.targetHandle
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
connectionState.set({
|
|
946
|
-
...emptyConnection
|
|
947
|
-
});
|
|
948
|
-
}
|
|
949
|
-
isPanning = false;
|
|
950
|
-
};
|
|
951
|
-
const handleKeyDown = e => {
|
|
952
|
-
if (e.key === "Delete" || e.key === "Backspace") {
|
|
953
|
-
const target = e.target;
|
|
954
|
-
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") return;
|
|
955
|
-
instance.pushHistory();
|
|
956
|
-
instance.deleteSelected();
|
|
957
|
-
}
|
|
958
|
-
if (e.key === "Escape") {
|
|
959
|
-
instance.clearSelection();
|
|
960
|
-
connectionState.set({
|
|
961
|
-
...emptyConnection
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
|
|
965
|
-
e.preventDefault();
|
|
966
|
-
instance.selectAll();
|
|
967
|
-
}
|
|
968
|
-
if (e.key === "c" && (e.metaKey || e.ctrlKey)) instance.copySelected();
|
|
969
|
-
if (e.key === "v" && (e.metaKey || e.ctrlKey)) instance.paste();
|
|
970
|
-
if (e.key === "z" && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
|
971
|
-
e.preventDefault();
|
|
972
|
-
instance.undo();
|
|
973
|
-
}
|
|
974
|
-
if (e.key === "z" && (e.metaKey || e.ctrlKey) && e.shiftKey) {
|
|
975
|
-
e.preventDefault();
|
|
976
|
-
instance.redo();
|
|
977
|
-
}
|
|
978
|
-
};
|
|
979
|
-
let lastTouchDist = 0;
|
|
980
|
-
let lastTouchCenter = {
|
|
981
|
-
x: 0,
|
|
982
|
-
y: 0
|
|
983
|
-
};
|
|
984
|
-
const handleTouchStart = e => {
|
|
985
|
-
if (e.touches.length === 2) {
|
|
986
|
-
e.preventDefault();
|
|
987
|
-
const t1 = e.touches[0];
|
|
988
|
-
const t2 = e.touches[1];
|
|
989
|
-
lastTouchDist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
|
990
|
-
lastTouchCenter = {
|
|
991
|
-
x: (t1.clientX + t2.clientX) / 2,
|
|
992
|
-
y: (t1.clientY + t2.clientY) / 2
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
};
|
|
996
|
-
const handleTouchMove = e => {
|
|
997
|
-
if (e.touches.length === 2 && instance.config.zoomable !== false) {
|
|
998
|
-
e.preventDefault();
|
|
999
|
-
const t1 = e.touches[0];
|
|
1000
|
-
const t2 = e.touches[1];
|
|
1001
|
-
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
|
|
1002
|
-
const center = {
|
|
1003
|
-
x: (t1.clientX + t2.clientX) / 2,
|
|
1004
|
-
y: (t1.clientY + t2.clientY) / 2
|
|
1005
|
-
};
|
|
1006
|
-
const vp = instance.viewport.peek();
|
|
1007
|
-
const scaleFactor = dist / lastTouchDist;
|
|
1008
|
-
const newZoom = Math.min(Math.max(vp.zoom * scaleFactor, instance.config.minZoom ?? .1), instance.config.maxZoom ?? 4);
|
|
1009
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
1010
|
-
const mouseX = center.x - rect.left;
|
|
1011
|
-
const mouseY = center.y - rect.top;
|
|
1012
|
-
const scale = newZoom / vp.zoom;
|
|
1013
|
-
const panDx = center.x - lastTouchCenter.x;
|
|
1014
|
-
const panDy = center.y - lastTouchCenter.y;
|
|
1015
|
-
instance.viewport.set({
|
|
1016
|
-
x: mouseX - (mouseX - vp.x) * scale + panDx,
|
|
1017
|
-
y: mouseY - (mouseY - vp.y) * scale + panDy,
|
|
1018
|
-
zoom: newZoom
|
|
1019
|
-
});
|
|
1020
|
-
lastTouchDist = dist;
|
|
1021
|
-
lastTouchCenter = center;
|
|
1022
|
-
}
|
|
1023
|
-
};
|
|
1024
|
-
let resizeObserver = null;
|
|
1025
|
-
const containerRef = el => {
|
|
1026
|
-
if (resizeObserver) {
|
|
1027
|
-
resizeObserver.disconnect();
|
|
1028
|
-
resizeObserver = null;
|
|
1029
|
-
}
|
|
1030
|
-
if (!el) return;
|
|
1031
|
-
const updateSize = () => {
|
|
1032
|
-
const rect = el.getBoundingClientRect();
|
|
1033
|
-
instance.containerSize.set({
|
|
1034
|
-
width: rect.width,
|
|
1035
|
-
height: rect.height
|
|
1036
|
-
});
|
|
1037
|
-
};
|
|
1038
|
-
updateSize();
|
|
1039
|
-
resizeObserver = new ResizeObserver(updateSize);
|
|
1040
|
-
resizeObserver.observe(el);
|
|
1041
|
-
};
|
|
1042
|
-
const containerStyle = `position: relative; width: 100%; height: 100%; overflow: hidden; outline: none; touch-action: none; ${props.style ?? ""}`;
|
|
1043
|
-
return /* @__PURE__ */jsxs("div", {
|
|
1044
|
-
ref: containerRef,
|
|
1045
|
-
class: `pyreon-flow ${props.class ?? ""}`,
|
|
1046
|
-
style: containerStyle,
|
|
1047
|
-
tabIndex: 0,
|
|
1048
|
-
onWheel: handleWheel,
|
|
1049
|
-
onPointerdown: handlePointerDown,
|
|
1050
|
-
onPointermove: handlePointerMove,
|
|
1051
|
-
onPointerup: handlePointerUp,
|
|
1052
|
-
onTouchstart: handleTouchStart,
|
|
1053
|
-
onTouchmove: handleTouchMove,
|
|
1054
|
-
onKeydown: handleKeyDown,
|
|
1055
|
-
children: [children, () => {
|
|
1056
|
-
const vp = instance.viewport();
|
|
1057
|
-
return /* @__PURE__ */jsxs("div", {
|
|
1058
|
-
class: "pyreon-flow-viewport",
|
|
1059
|
-
style: `position: absolute; transform-origin: 0 0; transform: translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom});`,
|
|
1060
|
-
children: [/* @__PURE__ */jsx(EdgeLayer, {
|
|
1061
|
-
instance,
|
|
1062
|
-
connectionState: () => connectionState(),
|
|
1063
|
-
edgeTypes
|
|
1064
|
-
}), () => {
|
|
1065
|
-
const sel = selectionBox();
|
|
1066
|
-
if (!sel.active) return null;
|
|
1067
|
-
return /* @__PURE__ */jsx("div", {
|
|
1068
|
-
class: "pyreon-flow-selection-box",
|
|
1069
|
-
style: `position: absolute; left: ${Math.min(sel.startX, sel.currentX)}px; top: ${Math.min(sel.startY, sel.currentY)}px; width: ${Math.abs(sel.currentX - sel.startX)}px; height: ${Math.abs(sel.currentY - sel.startY)}px; border: 1px dashed #3b82f6; background: rgba(59, 130, 246, 0.08); pointer-events: none; z-index: 10;`
|
|
1070
|
-
});
|
|
1071
|
-
}, () => {
|
|
1072
|
-
const lines = helperLines();
|
|
1073
|
-
if (!lines.x && !lines.y) return null;
|
|
1074
|
-
return /* @__PURE__ */jsxs("svg", {
|
|
1075
|
-
role: "img",
|
|
1076
|
-
"aria-label": "helper lines",
|
|
1077
|
-
style: "position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; z-index: 5;",
|
|
1078
|
-
children: [lines.x !== null && /* @__PURE__ */jsx("line", {
|
|
1079
|
-
x1: String(lines.x),
|
|
1080
|
-
y1: "-10000",
|
|
1081
|
-
x2: String(lines.x),
|
|
1082
|
-
y2: "10000",
|
|
1083
|
-
stroke: "#3b82f6",
|
|
1084
|
-
"stroke-width": "0.5",
|
|
1085
|
-
"stroke-dasharray": "4,4"
|
|
1086
|
-
}), lines.y !== null && /* @__PURE__ */jsx("line", {
|
|
1087
|
-
x1: "-10000",
|
|
1088
|
-
y1: String(lines.y),
|
|
1089
|
-
x2: "10000",
|
|
1090
|
-
y2: String(lines.y),
|
|
1091
|
-
stroke: "#3b82f6",
|
|
1092
|
-
"stroke-width": "0.5",
|
|
1093
|
-
"stroke-dasharray": "4,4"
|
|
1094
|
-
})]
|
|
1095
|
-
});
|
|
1096
|
-
}, /* @__PURE__ */jsx(NodeLayer, {
|
|
1097
|
-
instance,
|
|
1098
|
-
nodeTypes,
|
|
1099
|
-
draggingNodeId,
|
|
1100
|
-
onNodePointerDown: handleNodePointerDown,
|
|
1101
|
-
onHandlePointerDown: handleHandlePointerDown
|
|
1102
|
-
})]
|
|
1103
|
-
});
|
|
1104
|
-
}]
|
|
1105
|
-
});
|
|
348
|
+
interface HandleProps {
|
|
349
|
+
type: HandleType;
|
|
350
|
+
position: Position;
|
|
351
|
+
id?: string;
|
|
352
|
+
style?: string;
|
|
353
|
+
class?: string;
|
|
1106
354
|
}
|
|
1107
|
-
|
|
355
|
+
type NodeComponentProps<TData = Record<string, unknown>> = {
|
|
356
|
+
id: string;
|
|
357
|
+
data: TData;
|
|
358
|
+
selected: boolean;
|
|
359
|
+
dragging: boolean;
|
|
360
|
+
};
|
|
1108
361
|
//#endregion
|
|
1109
|
-
//#region src/components/
|
|
1110
|
-
|
|
362
|
+
//#region src/components/background.d.ts
|
|
1111
363
|
/**
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
*
|
|
1115
|
-
* @example
|
|
1116
|
-
* ```tsx
|
|
1117
|
-
*
|
|
1118
|
-
*
|
|
1119
|
-
*
|
|
1120
|
-
*
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
* </div>
|
|
1124
|
-
* )
|
|
1125
|
-
* }
|
|
1126
|
-
* ```
|
|
1127
|
-
*/
|
|
1128
|
-
function Handle(props) {
|
|
1129
|
-
const {
|
|
1130
|
-
type,
|
|
1131
|
-
position,
|
|
1132
|
-
id,
|
|
1133
|
-
style = ""
|
|
1134
|
-
} = props;
|
|
1135
|
-
const baseStyle = `position: absolute; ${positionOffset[position] ?? positionOffset.bottom} width: 8px; height: 8px; background: #555; border: 2px solid white; border-radius: 50%; cursor: crosshair; z-index: 1; ${style}`;
|
|
1136
|
-
return /* @__PURE__ */jsx("div", {
|
|
1137
|
-
class: `pyreon-flow-handle pyreon-flow-handle-${type} ${props.class ?? ""}`,
|
|
1138
|
-
style: baseStyle,
|
|
1139
|
-
"data-handletype": type,
|
|
1140
|
-
"data-handleid": id ?? type,
|
|
1141
|
-
"data-handleposition": position
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
|
|
364
|
+
* Background pattern for the flow canvas.
|
|
365
|
+
* Renders dots, lines, or cross patterns that move with the viewport.
|
|
366
|
+
*
|
|
367
|
+
* @example
|
|
368
|
+
* ```tsx
|
|
369
|
+
* <Flow instance={flow}>
|
|
370
|
+
* <Background variant="dots" gap={20} />
|
|
371
|
+
* </Flow>
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
declare function Background(props: BackgroundProps): VNodeChild;
|
|
1145
375
|
//#endregion
|
|
1146
|
-
//#region src/components/
|
|
376
|
+
//#region src/components/controls.d.ts
|
|
1147
377
|
/**
|
|
1148
|
-
*
|
|
1149
|
-
*
|
|
1150
|
-
*
|
|
1151
|
-
* @example
|
|
1152
|
-
* ```tsx
|
|
1153
|
-
* <Flow instance={flow}>
|
|
1154
|
-
* <
|
|
1155
|
-
* </Flow>
|
|
1156
|
-
* ```
|
|
1157
|
-
*/
|
|
1158
|
-
function
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
height = 150,
|
|
1162
|
-
nodeColor = "#e2e8f0",
|
|
1163
|
-
maskColor = "rgba(0, 0, 0, 0.08)",
|
|
1164
|
-
instance
|
|
1165
|
-
} = props;
|
|
1166
|
-
if (!instance) return null;
|
|
1167
|
-
const containerStyle = `position: absolute; bottom: 10px; right: 10px; width: ${width}px; height: ${height}px; border: 1px solid #ddd; background: white; border-radius: 4px; overflow: hidden; z-index: 5; cursor: pointer;`;
|
|
1168
|
-
return () => {
|
|
1169
|
-
const nodes = instance.nodes();
|
|
1170
|
-
if (nodes.length === 0) return /* @__PURE__ */jsx("div", {
|
|
1171
|
-
class: "pyreon-flow-minimap",
|
|
1172
|
-
style: containerStyle
|
|
1173
|
-
});
|
|
1174
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
1175
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
1176
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
1177
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
1178
|
-
for (const node of nodes) {
|
|
1179
|
-
const w = node.width ?? 150;
|
|
1180
|
-
const h = node.height ?? 40;
|
|
1181
|
-
minX = Math.min(minX, node.position.x);
|
|
1182
|
-
minY = Math.min(minY, node.position.y);
|
|
1183
|
-
maxX = Math.max(maxX, node.position.x + w);
|
|
1184
|
-
maxY = Math.max(maxY, node.position.y + h);
|
|
1185
|
-
}
|
|
1186
|
-
const padding = 40;
|
|
1187
|
-
const graphW = maxX - minX + padding * 2;
|
|
1188
|
-
const graphH = maxY - minY + padding * 2;
|
|
1189
|
-
const scale = Math.min(width / graphW, height / graphH);
|
|
1190
|
-
const vp = instance.viewport();
|
|
1191
|
-
const cs = instance.containerSize();
|
|
1192
|
-
const vpLeft = (-vp.x / vp.zoom - minX + padding) * scale;
|
|
1193
|
-
const vpTop = (-vp.y / vp.zoom - minY + padding) * scale;
|
|
1194
|
-
const vpWidth = cs.width / vp.zoom * scale;
|
|
1195
|
-
const vpHeight = cs.height / vp.zoom * scale;
|
|
1196
|
-
const handleClick = e => {
|
|
1197
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
1198
|
-
const clickX = e.clientX - rect.left;
|
|
1199
|
-
const clickY = e.clientY - rect.top;
|
|
1200
|
-
const flowX = clickX / scale + minX - padding;
|
|
1201
|
-
const flowY = clickY / scale + minY - padding;
|
|
1202
|
-
instance.viewport.set({
|
|
1203
|
-
...vp,
|
|
1204
|
-
x: -(flowX * vp.zoom) + cs.width / 2,
|
|
1205
|
-
y: -(flowY * vp.zoom) + cs.height / 2
|
|
1206
|
-
});
|
|
1207
|
-
};
|
|
1208
|
-
return /* @__PURE__ */jsx("div", {
|
|
1209
|
-
class: "pyreon-flow-minimap",
|
|
1210
|
-
style: containerStyle,
|
|
1211
|
-
onClick: handleClick,
|
|
1212
|
-
children: /* @__PURE__ */jsxs("svg", {
|
|
1213
|
-
role: "img",
|
|
1214
|
-
"aria-label": "minimap",
|
|
1215
|
-
width: String(width),
|
|
1216
|
-
height: String(height),
|
|
1217
|
-
children: [/* @__PURE__ */jsx("rect", {
|
|
1218
|
-
width: String(width),
|
|
1219
|
-
height: String(height),
|
|
1220
|
-
fill: maskColor
|
|
1221
|
-
}), nodes.map(node => {
|
|
1222
|
-
const w = (node.width ?? 150) * scale;
|
|
1223
|
-
const h = (node.height ?? 40) * scale;
|
|
1224
|
-
const x = (node.position.x - minX + padding) * scale;
|
|
1225
|
-
const y = (node.position.y - minY + padding) * scale;
|
|
1226
|
-
const color = typeof nodeColor === "function" ? nodeColor(node) : nodeColor;
|
|
1227
|
-
return /* @__PURE__ */jsx("rect", {
|
|
1228
|
-
x: String(x),
|
|
1229
|
-
y: String(y),
|
|
1230
|
-
width: String(w),
|
|
1231
|
-
height: String(h),
|
|
1232
|
-
fill: color,
|
|
1233
|
-
rx: "2"
|
|
1234
|
-
}, node.id);
|
|
1235
|
-
}), /* @__PURE__ */jsx("rect", {
|
|
1236
|
-
x: String(Math.max(0, vpLeft)),
|
|
1237
|
-
y: String(Math.max(0, vpTop)),
|
|
1238
|
-
width: String(Math.min(vpWidth, width)),
|
|
1239
|
-
height: String(Math.min(vpHeight, height)),
|
|
1240
|
-
fill: "none",
|
|
1241
|
-
stroke: "#3b82f6",
|
|
1242
|
-
"stroke-width": "1.5",
|
|
1243
|
-
rx: "2"
|
|
1244
|
-
})]
|
|
1245
|
-
})
|
|
1246
|
-
});
|
|
1247
|
-
};
|
|
1248
|
-
}
|
|
1249
|
-
|
|
378
|
+
* Zoom and viewport controls for the flow canvas.
|
|
379
|
+
* Shows zoom in, zoom out, fit view, and optional lock button.
|
|
380
|
+
*
|
|
381
|
+
* @example
|
|
382
|
+
* ```tsx
|
|
383
|
+
* <Flow instance={flow}>
|
|
384
|
+
* <Controls />
|
|
385
|
+
* </Flow>
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
declare function Controls(props: ControlsProps & {
|
|
389
|
+
instance?: FlowInstance;
|
|
390
|
+
}): VNodeChild;
|
|
1250
391
|
//#endregion
|
|
1251
|
-
//#region src/components/
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
*/
|
|
1271
|
-
function NodeResizer(props) {
|
|
1272
|
-
const {
|
|
1273
|
-
nodeId,
|
|
1274
|
-
instance,
|
|
1275
|
-
minWidth = 50,
|
|
1276
|
-
minHeight = 30,
|
|
1277
|
-
handleSize = 8,
|
|
1278
|
-
showEdgeHandles = false
|
|
1279
|
-
} = props;
|
|
1280
|
-
const directions = showEdgeHandles ? ["nw", "ne", "sw", "se", "n", "s", "e", "w"] : ["nw", "ne", "sw", "se"];
|
|
1281
|
-
const createHandler = dir => {
|
|
1282
|
-
let startX = 0;
|
|
1283
|
-
let startY = 0;
|
|
1284
|
-
let startWidth = 0;
|
|
1285
|
-
let startHeight = 0;
|
|
1286
|
-
let startNodeX = 0;
|
|
1287
|
-
let startNodeY = 0;
|
|
1288
|
-
let zoomAtStart = 1;
|
|
1289
|
-
const onPointerDown = e => {
|
|
1290
|
-
e.stopPropagation();
|
|
1291
|
-
e.preventDefault();
|
|
1292
|
-
const node = instance.getNode(nodeId);
|
|
1293
|
-
if (!node) return;
|
|
1294
|
-
startX = e.clientX;
|
|
1295
|
-
startY = e.clientY;
|
|
1296
|
-
startWidth = node.width ?? 150;
|
|
1297
|
-
startHeight = node.height ?? 40;
|
|
1298
|
-
startNodeX = node.position.x;
|
|
1299
|
-
startNodeY = node.position.y;
|
|
1300
|
-
zoomAtStart = instance.viewport.peek().zoom;
|
|
1301
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1302
|
-
};
|
|
1303
|
-
const onPointerMove = e => {
|
|
1304
|
-
if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
|
|
1305
|
-
const dx = (e.clientX - startX) / zoomAtStart;
|
|
1306
|
-
const dy = (e.clientY - startY) / zoomAtStart;
|
|
1307
|
-
let newW = startWidth;
|
|
1308
|
-
let newH = startHeight;
|
|
1309
|
-
let newX = startNodeX;
|
|
1310
|
-
let newY = startNodeY;
|
|
1311
|
-
if (dir === "e" || dir === "se" || dir === "ne") newW = Math.max(minWidth, startWidth + dx);
|
|
1312
|
-
if (dir === "w" || dir === "sw" || dir === "nw") {
|
|
1313
|
-
newW = Math.max(minWidth, startWidth - dx);
|
|
1314
|
-
newX = startNodeX + startWidth - newW;
|
|
1315
|
-
}
|
|
1316
|
-
if (dir === "s" || dir === "se" || dir === "sw") newH = Math.max(minHeight, startHeight + dy);
|
|
1317
|
-
if (dir === "n" || dir === "ne" || dir === "nw") {
|
|
1318
|
-
newH = Math.max(minHeight, startHeight - dy);
|
|
1319
|
-
newY = startNodeY + startHeight - newH;
|
|
1320
|
-
}
|
|
1321
|
-
instance.updateNode(nodeId, {
|
|
1322
|
-
width: newW,
|
|
1323
|
-
height: newH,
|
|
1324
|
-
position: {
|
|
1325
|
-
x: newX,
|
|
1326
|
-
y: newY
|
|
1327
|
-
}
|
|
1328
|
-
});
|
|
1329
|
-
};
|
|
1330
|
-
const onPointerUp = e => {
|
|
1331
|
-
const el = e.currentTarget;
|
|
1332
|
-
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
|
|
1333
|
-
};
|
|
1334
|
-
return {
|
|
1335
|
-
onPointerDown,
|
|
1336
|
-
onPointerMove,
|
|
1337
|
-
onPointerUp
|
|
1338
|
-
};
|
|
1339
|
-
};
|
|
1340
|
-
const size = `${handleSize}px`;
|
|
1341
|
-
const baseStyle = `position: absolute; width: ${size}; height: ${size}; background: white; border: 1.5px solid #3b82f6; border-radius: 2px; z-index: 2;`;
|
|
1342
|
-
return /* @__PURE__ */jsx(Fragment, {
|
|
1343
|
-
children: directions.map(dir => {
|
|
1344
|
-
const handler = createHandler(dir);
|
|
1345
|
-
return /* @__PURE__ */jsx("div", {
|
|
1346
|
-
class: `pyreon-flow-resizer pyreon-flow-resizer-${dir}`,
|
|
1347
|
-
style: `${baseStyle} ${directionPositions[dir]} cursor: ${directionCursors[dir]};`,
|
|
1348
|
-
onPointerdown: handler.onPointerDown,
|
|
1349
|
-
onPointermove: handler.onPointerMove,
|
|
1350
|
-
onPointerup: handler.onPointerUp
|
|
1351
|
-
}, dir);
|
|
1352
|
-
})
|
|
1353
|
-
});
|
|
392
|
+
//#region src/components/flow-component.d.ts
|
|
393
|
+
type NodeTypeMap = Record<string, (props: NodeComponentProps<any>) => VNodeChild>;
|
|
394
|
+
type EdgeTypeMap = Record<string, (props: {
|
|
395
|
+
edge: FlowEdge;
|
|
396
|
+
sourceX: number;
|
|
397
|
+
sourceY: number;
|
|
398
|
+
targetX: number;
|
|
399
|
+
targetY: number;
|
|
400
|
+
selected: boolean;
|
|
401
|
+
}) => VNodeChild>;
|
|
402
|
+
interface FlowComponentProps {
|
|
403
|
+
instance: FlowInstance;
|
|
404
|
+
/** Custom node type renderers */
|
|
405
|
+
nodeTypes?: NodeTypeMap;
|
|
406
|
+
/** Custom edge type renderers */
|
|
407
|
+
edgeTypes?: EdgeTypeMap;
|
|
408
|
+
style?: string;
|
|
409
|
+
class?: string;
|
|
410
|
+
children?: VNodeChild;
|
|
1354
411
|
}
|
|
1355
|
-
|
|
412
|
+
/**
|
|
413
|
+
* The main Flow component — renders the interactive flow diagram.
|
|
414
|
+
*
|
|
415
|
+
* Supports node dragging, connection drawing, custom node types,
|
|
416
|
+
* pan/zoom, and all standard flow interactions.
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```tsx
|
|
420
|
+
* const flow = createFlow({
|
|
421
|
+
* nodes: [...],
|
|
422
|
+
* edges: [...],
|
|
423
|
+
* })
|
|
424
|
+
*
|
|
425
|
+
* <Flow instance={flow} nodeTypes={{ custom: CustomNode }}>
|
|
426
|
+
* <Background />
|
|
427
|
+
* <MiniMap />
|
|
428
|
+
* <Controls />
|
|
429
|
+
* </Flow>
|
|
430
|
+
* ```
|
|
431
|
+
*/
|
|
432
|
+
declare function Flow(props: FlowComponentProps): VNodeChild;
|
|
1356
433
|
//#endregion
|
|
1357
|
-
//#region src/components/
|
|
1358
|
-
|
|
434
|
+
//#region src/components/handle.d.ts
|
|
1359
435
|
/**
|
|
1360
|
-
*
|
|
1361
|
-
* Place inside
|
|
1362
|
-
*
|
|
1363
|
-
* @example
|
|
1364
|
-
* ```tsx
|
|
1365
|
-
* function
|
|
1366
|
-
* return (
|
|
1367
|
-
* <div class="node">
|
|
1368
|
-
* {
|
|
1369
|
-
* <
|
|
1370
|
-
*
|
|
1371
|
-
*
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
*/
|
|
1378
|
-
function NodeToolbar(props) {
|
|
1379
|
-
const {
|
|
1380
|
-
position = "top",
|
|
1381
|
-
offset = 8,
|
|
1382
|
-
showOnSelect = true,
|
|
1383
|
-
selected = false,
|
|
1384
|
-
children
|
|
1385
|
-
} = props;
|
|
1386
|
-
if (showOnSelect && !selected) return null;
|
|
1387
|
-
const baseStyle = `position: absolute; ${positionStyles$1[position] ?? positionStyles$1.top} ${position === "top" ? `margin-bottom: ${offset}px;` : position === "bottom" ? `margin-top: ${offset}px;` : position === "left" ? `margin-right: ${offset}px;` : `margin-left: ${offset}px;`} z-index: 10; display: flex; gap: 4px; background: white; border: 1px solid #ddd; border-radius: 6px; padding: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); ${props.style ?? ""}`;
|
|
1388
|
-
return /* @__PURE__ */jsx("div", {
|
|
1389
|
-
class: `pyreon-flow-node-toolbar ${props.class ?? ""}`,
|
|
1390
|
-
style: baseStyle,
|
|
1391
|
-
children
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
|
|
436
|
+
* Connection handle — attachment point on a node where edges connect.
|
|
437
|
+
* Place inside custom node components.
|
|
438
|
+
*
|
|
439
|
+
* @example
|
|
440
|
+
* ```tsx
|
|
441
|
+
* function CustomNode({ data }: NodeComponentProps) {
|
|
442
|
+
* return (
|
|
443
|
+
* <div class="custom-node">
|
|
444
|
+
* <Handle type="target" position={Position.Left} />
|
|
445
|
+
* <span>{data.label}</span>
|
|
446
|
+
* <Handle type="source" position={Position.Right} />
|
|
447
|
+
* </div>
|
|
448
|
+
* )
|
|
449
|
+
* }
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
declare function Handle(props: HandleProps): VNodeChild;
|
|
1395
453
|
//#endregion
|
|
1396
|
-
//#region src/components/
|
|
1397
|
-
|
|
454
|
+
//#region src/components/minimap.d.ts
|
|
1398
455
|
/**
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1403
|
-
*
|
|
1404
|
-
*
|
|
1405
|
-
*
|
|
1406
|
-
*
|
|
1407
|
-
*
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
position = "top-left",
|
|
1413
|
-
style = "",
|
|
1414
|
-
children
|
|
1415
|
-
} = props;
|
|
1416
|
-
const baseStyle = `position: absolute; ${positionStyles[position] ?? positionStyles["top-left"]} z-index: 5; ${style}`;
|
|
1417
|
-
return /* @__PURE__ */jsx("div", {
|
|
1418
|
-
class: `pyreon-flow-panel ${props.class ?? ""}`,
|
|
1419
|
-
style: baseStyle,
|
|
1420
|
-
children
|
|
1421
|
-
});
|
|
1422
|
-
}
|
|
1423
|
-
|
|
456
|
+
* Miniature overview of the flow diagram showing all nodes
|
|
457
|
+
* and the current viewport position. Click to navigate.
|
|
458
|
+
*
|
|
459
|
+
* @example
|
|
460
|
+
* ```tsx
|
|
461
|
+
* <Flow instance={flow}>
|
|
462
|
+
* <MiniMap nodeColor={(n) => n.type === 'error' ? 'red' : '#ddd'} />
|
|
463
|
+
* </Flow>
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
declare function MiniMap(props: MiniMapProps & {
|
|
467
|
+
instance?: FlowInstance;
|
|
468
|
+
}): VNodeChild;
|
|
1424
469
|
//#endregion
|
|
1425
|
-
//#region src/
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const layoutOptions = {
|
|
1438
|
-
"elk.algorithm": ELK_ALGORITHMS[algorithm] ?? ELK_ALGORITHMS.layered
|
|
1439
|
-
};
|
|
1440
|
-
if (options.direction) layoutOptions["elk.direction"] = ELK_DIRECTIONS[options.direction] ?? "DOWN";
|
|
1441
|
-
if (options.nodeSpacing !== void 0) layoutOptions["elk.spacing.nodeNode"] = String(options.nodeSpacing);
|
|
1442
|
-
if (options.layerSpacing !== void 0) layoutOptions["elk.layered.spacing.nodeNodeBetweenLayers"] = String(options.layerSpacing);
|
|
1443
|
-
if (options.edgeRouting) layoutOptions["elk.edgeRouting"] = {
|
|
1444
|
-
orthogonal: "ORTHOGONAL",
|
|
1445
|
-
splines: "SPLINES",
|
|
1446
|
-
polyline: "POLYLINE"
|
|
1447
|
-
}[options.edgeRouting] ?? "ORTHOGONAL";
|
|
1448
|
-
return {
|
|
1449
|
-
id: "root",
|
|
1450
|
-
layoutOptions,
|
|
1451
|
-
children: nodes.map(node => ({
|
|
1452
|
-
id: node.id,
|
|
1453
|
-
width: node.width ?? 150,
|
|
1454
|
-
height: node.height ?? 40
|
|
1455
|
-
})),
|
|
1456
|
-
edges: edges.map((edge, i) => ({
|
|
1457
|
-
id: edge.id ?? `e-${i}`,
|
|
1458
|
-
sources: [edge.source],
|
|
1459
|
-
targets: [edge.target]
|
|
1460
|
-
}))
|
|
1461
|
-
};
|
|
470
|
+
//#region src/components/node-resizer.d.ts
|
|
471
|
+
interface NodeResizerProps {
|
|
472
|
+
nodeId: string;
|
|
473
|
+
instance: FlowInstance;
|
|
474
|
+
/** Minimum width — default: 50 */
|
|
475
|
+
minWidth?: number;
|
|
476
|
+
/** Minimum height — default: 30 */
|
|
477
|
+
minHeight?: number;
|
|
478
|
+
/** Handle size in px — default: 8 */
|
|
479
|
+
handleSize?: number;
|
|
480
|
+
/** Also show edge (non-corner) resize handles — default: false */
|
|
481
|
+
showEdgeHandles?: boolean;
|
|
1462
482
|
}
|
|
1463
483
|
/**
|
|
1464
|
-
*
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1468
|
-
*
|
|
1469
|
-
* @example
|
|
1470
|
-
* ```
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
* }
|
|
1476
|
-
*
|
|
1477
|
-
*
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
484
|
+
* Node resize handles. Place inside a custom node component
|
|
485
|
+
* to allow users to resize the node by dragging corners or edges.
|
|
486
|
+
*
|
|
487
|
+
* Uses pointer capture for clean event handling — no document listener leaks.
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```tsx
|
|
491
|
+
* function ResizableNode({ id, data, selected }: NodeComponentProps) {
|
|
492
|
+
* return (
|
|
493
|
+
* <div style="min-width: 100px; min-height: 50px; position: relative;">
|
|
494
|
+
* {data.label}
|
|
495
|
+
* <NodeResizer nodeId={id} instance={flow} />
|
|
496
|
+
* </div>
|
|
497
|
+
* )
|
|
498
|
+
* }
|
|
499
|
+
* ```
|
|
500
|
+
*/
|
|
501
|
+
declare function NodeResizer(props: NodeResizerProps): VNodeChild;
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/components/node-toolbar.d.ts
|
|
504
|
+
interface NodeToolbarProps {
|
|
505
|
+
/** Position relative to node — default: 'top' */
|
|
506
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
507
|
+
/** Offset from node in px — default: 8 */
|
|
508
|
+
offset?: number;
|
|
509
|
+
/** Only show when node is selected — default: true */
|
|
510
|
+
showOnSelect?: boolean;
|
|
511
|
+
/** Whether the node is currently selected */
|
|
512
|
+
selected?: boolean;
|
|
513
|
+
style?: string;
|
|
514
|
+
class?: string;
|
|
515
|
+
children?: VNodeChild;
|
|
1489
516
|
}
|
|
1490
|
-
|
|
517
|
+
/**
|
|
518
|
+
* Floating toolbar that appears near a node, typically when selected.
|
|
519
|
+
* Place inside a custom node component.
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```tsx
|
|
523
|
+
* function EditableNode({ id, data, selected }: NodeComponentProps) {
|
|
524
|
+
* return (
|
|
525
|
+
* <div class="node">
|
|
526
|
+
* {data.label}
|
|
527
|
+
* <NodeToolbar selected={selected}>
|
|
528
|
+
* <button onClick={() => duplicate(id)}>Duplicate</button>
|
|
529
|
+
* <button onClick={() => remove(id)}>Delete</button>
|
|
530
|
+
* </NodeToolbar>
|
|
531
|
+
* </div>
|
|
532
|
+
* )
|
|
533
|
+
* }
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
declare function NodeToolbar(props: NodeToolbarProps): VNodeChild;
|
|
1491
537
|
//#endregion
|
|
1492
|
-
//#region src/
|
|
538
|
+
//#region src/components/panel.d.ts
|
|
1493
539
|
/**
|
|
1494
|
-
*
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
540
|
+
* Positioned overlay panel for custom content inside the flow canvas.
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* ```tsx
|
|
544
|
+
* <Flow instance={flow}>
|
|
545
|
+
* <Panel position="top-right">
|
|
546
|
+
* <SearchBar />
|
|
547
|
+
* </Panel>
|
|
548
|
+
* </Flow>
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
declare function Panel(props: PanelProps): VNodeChild;
|
|
552
|
+
//#endregion
|
|
553
|
+
//#region src/edges.d.ts
|
|
554
|
+
/**
|
|
555
|
+
* Auto-detect the best handle position based on relative node positions.
|
|
556
|
+
* If the node has configured handles, uses those. Otherwise picks the
|
|
557
|
+
* closest edge (top/right/bottom/left) based on direction to the other node.
|
|
558
|
+
*/
|
|
559
|
+
declare function getSmartHandlePositions(sourceNode: FlowNode, targetNode: FlowNode): {
|
|
560
|
+
sourcePosition: Position;
|
|
561
|
+
targetPosition: Position;
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* Get the handle position offset for a given position (top/right/bottom/left).
|
|
565
|
+
*/
|
|
566
|
+
declare function getHandlePosition(position: Position, nodeX: number, nodeY: number, nodeWidth: number, nodeHeight: number, _handleId?: string): XYPosition;
|
|
567
|
+
/**
|
|
568
|
+
* Calculate a cubic bezier edge path between two points.
|
|
569
|
+
*
|
|
570
|
+
* @example
|
|
571
|
+
* ```ts
|
|
572
|
+
* const { path, labelX, labelY } = getBezierPath({
|
|
573
|
+
* sourceX: 0, sourceY: 0, sourcePosition: Position.Right,
|
|
574
|
+
* targetX: 200, targetY: 100, targetPosition: Position.Left,
|
|
575
|
+
* })
|
|
576
|
+
* // path = "M0,0 C100,0 100,100 200,100"
|
|
577
|
+
* ```
|
|
578
|
+
*/
|
|
579
|
+
declare function getBezierPath(params: {
|
|
580
|
+
sourceX: number;
|
|
581
|
+
sourceY: number;
|
|
582
|
+
sourcePosition?: Position;
|
|
583
|
+
targetX: number;
|
|
584
|
+
targetY: number;
|
|
585
|
+
targetPosition?: Position;
|
|
586
|
+
curvature?: number;
|
|
587
|
+
}): EdgePathResult;
|
|
1502
588
|
/**
|
|
1503
|
-
*
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
*
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
*
|
|
1526
|
-
*/
|
|
1527
|
-
function
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
const node = getNode(id);
|
|
1602
|
-
pos = clampToExtent(pos, node?.width, node?.height);
|
|
1603
|
-
nodes.update(nds => nds.map(n => n.id === id ? {
|
|
1604
|
-
...n,
|
|
1605
|
-
position: pos
|
|
1606
|
-
} : n));
|
|
1607
|
-
emitNodeChanges([{
|
|
1608
|
-
type: "position",
|
|
1609
|
-
id,
|
|
1610
|
-
position: pos
|
|
1611
|
-
}]);
|
|
1612
|
-
}
|
|
1613
|
-
function getEdge(id) {
|
|
1614
|
-
return edges.peek().find(e => e.id === id);
|
|
1615
|
-
}
|
|
1616
|
-
function addEdge(edge) {
|
|
1617
|
-
const newEdge = {
|
|
1618
|
-
...edge,
|
|
1619
|
-
id: edgeId(edge),
|
|
1620
|
-
type: edge.type ?? defaultEdgeType
|
|
1621
|
-
};
|
|
1622
|
-
if (edges.peek().some(e => e.id === newEdge.id)) return;
|
|
1623
|
-
edges.update(eds => [...eds, newEdge]);
|
|
1624
|
-
const connection = {
|
|
1625
|
-
source: edge.source,
|
|
1626
|
-
target: edge.target,
|
|
1627
|
-
sourceHandle: edge.sourceHandle,
|
|
1628
|
-
targetHandle: edge.targetHandle
|
|
1629
|
-
};
|
|
1630
|
-
for (const cb of connectListeners) cb(connection);
|
|
1631
|
-
}
|
|
1632
|
-
function removeEdge(id) {
|
|
1633
|
-
edges.update(eds => eds.filter(e => e.id !== id));
|
|
1634
|
-
selectedEdgeIds.update(set => {
|
|
1635
|
-
const next = new Set(set);
|
|
1636
|
-
next.delete(id);
|
|
1637
|
-
return next;
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
function isValidConnection(connection) {
|
|
1641
|
-
if (!connectionRules) return true;
|
|
1642
|
-
const sourceNode = getNode(connection.source);
|
|
1643
|
-
if (!sourceNode) return false;
|
|
1644
|
-
const rule = connectionRules[sourceNode.type ?? "default"];
|
|
1645
|
-
if (!rule) return true;
|
|
1646
|
-
const targetNode = getNode(connection.target);
|
|
1647
|
-
if (!targetNode) return false;
|
|
1648
|
-
const targetType = targetNode.type ?? "default";
|
|
1649
|
-
return rule.outputs.includes(targetType);
|
|
1650
|
-
}
|
|
1651
|
-
function selectNode(id, additive = false) {
|
|
1652
|
-
selectedNodeIds.update(set => {
|
|
1653
|
-
const next = additive ? new Set(set) : /* @__PURE__ */new Set();
|
|
1654
|
-
next.add(id);
|
|
1655
|
-
return next;
|
|
1656
|
-
});
|
|
1657
|
-
if (!additive) selectedEdgeIds.set(/* @__PURE__ */new Set());
|
|
1658
|
-
}
|
|
1659
|
-
function deselectNode(id) {
|
|
1660
|
-
selectedNodeIds.update(set => {
|
|
1661
|
-
const next = new Set(set);
|
|
1662
|
-
next.delete(id);
|
|
1663
|
-
return next;
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
function selectEdge(id, additive = false) {
|
|
1667
|
-
selectedEdgeIds.update(set => {
|
|
1668
|
-
const next = additive ? new Set(set) : /* @__PURE__ */new Set();
|
|
1669
|
-
next.add(id);
|
|
1670
|
-
return next;
|
|
1671
|
-
});
|
|
1672
|
-
if (!additive) selectedNodeIds.set(/* @__PURE__ */new Set());
|
|
1673
|
-
}
|
|
1674
|
-
function clearSelection() {
|
|
1675
|
-
batch(() => {
|
|
1676
|
-
selectedNodeIds.set(/* @__PURE__ */new Set());
|
|
1677
|
-
selectedEdgeIds.set(/* @__PURE__ */new Set());
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
function selectAll() {
|
|
1681
|
-
selectedNodeIds.set(new Set(nodes.peek().map(n => n.id)));
|
|
1682
|
-
}
|
|
1683
|
-
function deleteSelected() {
|
|
1684
|
-
batch(() => {
|
|
1685
|
-
const nodeIdsToRemove = selectedNodeIds.peek();
|
|
1686
|
-
const edgeIdsToRemove = selectedEdgeIds.peek();
|
|
1687
|
-
if (nodeIdsToRemove.size > 0) {
|
|
1688
|
-
nodes.update(nds => nds.filter(n => !nodeIdsToRemove.has(n.id)));
|
|
1689
|
-
edges.update(eds => eds.filter(e => !nodeIdsToRemove.has(e.source) && !nodeIdsToRemove.has(e.target) && !edgeIdsToRemove.has(e.id)));
|
|
1690
|
-
} else if (edgeIdsToRemove.size > 0) edges.update(eds => eds.filter(e => !edgeIdsToRemove.has(e.id)));
|
|
1691
|
-
selectedNodeIds.set(/* @__PURE__ */new Set());
|
|
1692
|
-
selectedEdgeIds.set(/* @__PURE__ */new Set());
|
|
1693
|
-
});
|
|
1694
|
-
}
|
|
1695
|
-
function fitView(nodeIds, padding = config.fitViewPadding ?? .1) {
|
|
1696
|
-
const targetNodes = nodeIds ? nodes.peek().filter(n => nodeIds.includes(n.id)) : nodes.peek();
|
|
1697
|
-
if (targetNodes.length === 0) return;
|
|
1698
|
-
let minX = Number.POSITIVE_INFINITY;
|
|
1699
|
-
let minY = Number.POSITIVE_INFINITY;
|
|
1700
|
-
let maxX = Number.NEGATIVE_INFINITY;
|
|
1701
|
-
let maxY = Number.NEGATIVE_INFINITY;
|
|
1702
|
-
for (const node of targetNodes) {
|
|
1703
|
-
const w = node.width ?? 150;
|
|
1704
|
-
const h = node.height ?? 40;
|
|
1705
|
-
minX = Math.min(minX, node.position.x);
|
|
1706
|
-
minY = Math.min(minY, node.position.y);
|
|
1707
|
-
maxX = Math.max(maxX, node.position.x + w);
|
|
1708
|
-
maxY = Math.max(maxY, node.position.y + h);
|
|
1709
|
-
}
|
|
1710
|
-
const graphWidth = maxX - minX;
|
|
1711
|
-
const graphHeight = maxY - minY;
|
|
1712
|
-
const {
|
|
1713
|
-
width: containerWidth,
|
|
1714
|
-
height: containerHeight
|
|
1715
|
-
} = containerSize.peek();
|
|
1716
|
-
const zoomX = containerWidth / (graphWidth * (1 + padding * 2));
|
|
1717
|
-
const zoomY = containerHeight / (graphHeight * (1 + padding * 2));
|
|
1718
|
-
const newZoom = Math.min(Math.max(Math.min(zoomX, zoomY), minZoom), maxZoom);
|
|
1719
|
-
const centerX = (minX + maxX) / 2;
|
|
1720
|
-
const centerY = (minY + maxY) / 2;
|
|
1721
|
-
viewport.set({
|
|
1722
|
-
x: containerWidth / 2 - centerX * newZoom,
|
|
1723
|
-
y: containerHeight / 2 - centerY * newZoom,
|
|
1724
|
-
zoom: newZoom
|
|
1725
|
-
});
|
|
1726
|
-
}
|
|
1727
|
-
function zoomTo(z) {
|
|
1728
|
-
viewport.update(v => ({
|
|
1729
|
-
...v,
|
|
1730
|
-
zoom: Math.min(Math.max(z, minZoom), maxZoom)
|
|
1731
|
-
}));
|
|
1732
|
-
}
|
|
1733
|
-
function zoomIn() {
|
|
1734
|
-
viewport.update(v => ({
|
|
1735
|
-
...v,
|
|
1736
|
-
zoom: Math.min(v.zoom * 1.2, maxZoom)
|
|
1737
|
-
}));
|
|
1738
|
-
}
|
|
1739
|
-
function zoomOut() {
|
|
1740
|
-
viewport.update(v => ({
|
|
1741
|
-
...v,
|
|
1742
|
-
zoom: Math.max(v.zoom / 1.2, minZoom)
|
|
1743
|
-
}));
|
|
1744
|
-
}
|
|
1745
|
-
function panTo(position) {
|
|
1746
|
-
viewport.update(v => ({
|
|
1747
|
-
...v,
|
|
1748
|
-
x: -position.x * v.zoom,
|
|
1749
|
-
y: -position.y * v.zoom
|
|
1750
|
-
}));
|
|
1751
|
-
}
|
|
1752
|
-
function isNodeVisible(id) {
|
|
1753
|
-
const node = getNode(id);
|
|
1754
|
-
if (!node) return false;
|
|
1755
|
-
const v = viewport.peek();
|
|
1756
|
-
const w = node.width ?? 150;
|
|
1757
|
-
const h = node.height ?? 40;
|
|
1758
|
-
const screenX = node.position.x * v.zoom + v.x;
|
|
1759
|
-
const screenY = node.position.y * v.zoom + v.y;
|
|
1760
|
-
const screenW = w * v.zoom;
|
|
1761
|
-
const screenH = h * v.zoom;
|
|
1762
|
-
const {
|
|
1763
|
-
width: cw,
|
|
1764
|
-
height: ch
|
|
1765
|
-
} = containerSize.peek();
|
|
1766
|
-
return screenX + screenW > 0 && screenX < cw && screenY + screenH > 0 && screenY < ch;
|
|
1767
|
-
}
|
|
1768
|
-
async function layout(algorithm = "layered", options = {}) {
|
|
1769
|
-
const currentNodes = nodes.peek();
|
|
1770
|
-
const positions = await computeLayout(currentNodes, edges.peek(), algorithm, options);
|
|
1771
|
-
const animate = options.animate !== false;
|
|
1772
|
-
const duration = options.animationDuration ?? 300;
|
|
1773
|
-
if (!animate) {
|
|
1774
|
-
batch(() => {
|
|
1775
|
-
nodes.update(nds => nds.map(node => {
|
|
1776
|
-
const pos = positions.find(p => p.id === node.id);
|
|
1777
|
-
return pos ? {
|
|
1778
|
-
...node,
|
|
1779
|
-
position: pos.position
|
|
1780
|
-
} : node;
|
|
1781
|
-
}));
|
|
1782
|
-
});
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
const startPositions = new Map(currentNodes.map(n => [n.id, {
|
|
1786
|
-
...n.position
|
|
1787
|
-
}]));
|
|
1788
|
-
const targetPositions = new Map(positions.map(p => [p.id, p.position]));
|
|
1789
|
-
const startTime = performance.now();
|
|
1790
|
-
const animateFrame = () => {
|
|
1791
|
-
const elapsed = performance.now() - startTime;
|
|
1792
|
-
const t = Math.min(elapsed / duration, 1);
|
|
1793
|
-
const eased = 1 - (1 - t) ** 3;
|
|
1794
|
-
batch(() => {
|
|
1795
|
-
nodes.update(nds => nds.map(node => {
|
|
1796
|
-
const start = startPositions.get(node.id);
|
|
1797
|
-
const end = targetPositions.get(node.id);
|
|
1798
|
-
if (!start || !end) return node;
|
|
1799
|
-
return {
|
|
1800
|
-
...node,
|
|
1801
|
-
position: {
|
|
1802
|
-
x: start.x + (end.x - start.x) * eased,
|
|
1803
|
-
y: start.y + (end.y - start.y) * eased
|
|
1804
|
-
}
|
|
1805
|
-
};
|
|
1806
|
-
}));
|
|
1807
|
-
});
|
|
1808
|
-
if (t < 1) requestAnimationFrame(animateFrame);
|
|
1809
|
-
};
|
|
1810
|
-
requestAnimationFrame(animateFrame);
|
|
1811
|
-
}
|
|
1812
|
-
function batchOp(fn) {
|
|
1813
|
-
batch(fn);
|
|
1814
|
-
}
|
|
1815
|
-
function getConnectedEdges(nodeId) {
|
|
1816
|
-
return edges.peek().filter(e => e.source === nodeId || e.target === nodeId);
|
|
1817
|
-
}
|
|
1818
|
-
function getIncomers(nodeId) {
|
|
1819
|
-
const incomingEdges = edges.peek().filter(e => e.target === nodeId);
|
|
1820
|
-
const sourceIds = new Set(incomingEdges.map(e => e.source));
|
|
1821
|
-
return nodes.peek().filter(n => sourceIds.has(n.id));
|
|
1822
|
-
}
|
|
1823
|
-
function getOutgoers(nodeId) {
|
|
1824
|
-
const outgoingEdges = edges.peek().filter(e => e.source === nodeId);
|
|
1825
|
-
const targetIds = new Set(outgoingEdges.map(e => e.target));
|
|
1826
|
-
return nodes.peek().filter(n => targetIds.has(n.id));
|
|
1827
|
-
}
|
|
1828
|
-
function onConnect(callback) {
|
|
1829
|
-
connectListeners.add(callback);
|
|
1830
|
-
return () => connectListeners.delete(callback);
|
|
1831
|
-
}
|
|
1832
|
-
function onNodesChange(callback) {
|
|
1833
|
-
nodesChangeListeners.add(callback);
|
|
1834
|
-
return () => nodesChangeListeners.delete(callback);
|
|
1835
|
-
}
|
|
1836
|
-
function onNodeClick(callback) {
|
|
1837
|
-
nodeClickListeners.add(callback);
|
|
1838
|
-
return () => nodeClickListeners.delete(callback);
|
|
1839
|
-
}
|
|
1840
|
-
function onEdgeClick(callback) {
|
|
1841
|
-
edgeClickListeners.add(callback);
|
|
1842
|
-
return () => edgeClickListeners.delete(callback);
|
|
1843
|
-
}
|
|
1844
|
-
function onNodeDragStart(callback) {
|
|
1845
|
-
nodeDragStartListeners.add(callback);
|
|
1846
|
-
return () => nodeDragStartListeners.delete(callback);
|
|
1847
|
-
}
|
|
1848
|
-
function onNodeDragEnd(callback) {
|
|
1849
|
-
nodeDragEndListeners.add(callback);
|
|
1850
|
-
return () => nodeDragEndListeners.delete(callback);
|
|
1851
|
-
}
|
|
1852
|
-
function onNodeDoubleClick(callback) {
|
|
1853
|
-
nodeDoubleClickListeners.add(callback);
|
|
1854
|
-
return () => nodeDoubleClickListeners.delete(callback);
|
|
1855
|
-
}
|
|
1856
|
-
let clipboard = null;
|
|
1857
|
-
function copySelected() {
|
|
1858
|
-
const selectedNodeSet = selectedNodeIds.peek();
|
|
1859
|
-
if (selectedNodeSet.size === 0) return;
|
|
1860
|
-
const copiedNodes = nodes.peek().filter(n => selectedNodeSet.has(n.id));
|
|
1861
|
-
const nodeIdSet = new Set(copiedNodes.map(n => n.id));
|
|
1862
|
-
clipboard = {
|
|
1863
|
-
nodes: copiedNodes,
|
|
1864
|
-
edges: edges.peek().filter(e => nodeIdSet.has(e.source) && nodeIdSet.has(e.target))
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
function paste(offset = {
|
|
1868
|
-
x: 50,
|
|
1869
|
-
y: 50
|
|
1870
|
-
}) {
|
|
1871
|
-
if (!clipboard) return;
|
|
1872
|
-
const idMap = /* @__PURE__ */new Map();
|
|
1873
|
-
const newNodes = [];
|
|
1874
|
-
for (const node of clipboard.nodes) {
|
|
1875
|
-
const newId = `${node.id}-copy-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1876
|
-
idMap.set(node.id, newId);
|
|
1877
|
-
newNodes.push({
|
|
1878
|
-
...node,
|
|
1879
|
-
id: newId,
|
|
1880
|
-
position: {
|
|
1881
|
-
x: node.position.x + offset.x,
|
|
1882
|
-
y: node.position.y + offset.y
|
|
1883
|
-
}
|
|
1884
|
-
});
|
|
1885
|
-
}
|
|
1886
|
-
const newEdges = clipboard.edges.map(e => ({
|
|
1887
|
-
...e,
|
|
1888
|
-
id: void 0,
|
|
1889
|
-
source: idMap.get(e.source) ?? e.source,
|
|
1890
|
-
target: idMap.get(e.target) ?? e.target
|
|
1891
|
-
}));
|
|
1892
|
-
batch(() => {
|
|
1893
|
-
for (const node of newNodes) addNode(node);
|
|
1894
|
-
for (const edge of newEdges) addEdge(edge);
|
|
1895
|
-
selectedNodeIds.set(new Set(newNodes.map(n => n.id)));
|
|
1896
|
-
selectedEdgeIds.set(/* @__PURE__ */new Set());
|
|
1897
|
-
});
|
|
1898
|
-
}
|
|
1899
|
-
const undoStack = [];
|
|
1900
|
-
const redoStack = [];
|
|
1901
|
-
const maxHistory = 50;
|
|
1902
|
-
function pushHistory() {
|
|
1903
|
-
undoStack.push({
|
|
1904
|
-
nodes: structuredClone(nodes.peek()),
|
|
1905
|
-
edges: structuredClone(edges.peek())
|
|
1906
|
-
});
|
|
1907
|
-
if (undoStack.length > maxHistory) undoStack.shift();
|
|
1908
|
-
redoStack.length = 0;
|
|
1909
|
-
}
|
|
1910
|
-
function undo() {
|
|
1911
|
-
const prev = undoStack.pop();
|
|
1912
|
-
if (!prev) return;
|
|
1913
|
-
redoStack.push({
|
|
1914
|
-
nodes: structuredClone(nodes.peek()),
|
|
1915
|
-
edges: structuredClone(edges.peek())
|
|
1916
|
-
});
|
|
1917
|
-
batch(() => {
|
|
1918
|
-
nodes.set(prev.nodes);
|
|
1919
|
-
edges.set(prev.edges);
|
|
1920
|
-
clearSelection();
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
function redo() {
|
|
1924
|
-
const next = redoStack.pop();
|
|
1925
|
-
if (!next) return;
|
|
1926
|
-
undoStack.push({
|
|
1927
|
-
nodes: structuredClone(nodes.peek()),
|
|
1928
|
-
edges: structuredClone(edges.peek())
|
|
1929
|
-
});
|
|
1930
|
-
batch(() => {
|
|
1931
|
-
nodes.set(next.nodes);
|
|
1932
|
-
edges.set(next.edges);
|
|
1933
|
-
clearSelection();
|
|
1934
|
-
});
|
|
1935
|
-
}
|
|
1936
|
-
function moveSelectedNodes(dx, dy) {
|
|
1937
|
-
const selected = selectedNodeIds.peek();
|
|
1938
|
-
if (selected.size === 0) return;
|
|
1939
|
-
nodes.update(nds => nds.map(n => {
|
|
1940
|
-
if (!selected.has(n.id)) return n;
|
|
1941
|
-
return {
|
|
1942
|
-
...n,
|
|
1943
|
-
position: {
|
|
1944
|
-
x: n.position.x + dx,
|
|
1945
|
-
y: n.position.y + dy
|
|
1946
|
-
}
|
|
1947
|
-
};
|
|
1948
|
-
}));
|
|
1949
|
-
}
|
|
1950
|
-
function getSnapLines(dragNodeId, position, threshold = 5) {
|
|
1951
|
-
const dragNode = getNode(dragNodeId);
|
|
1952
|
-
if (!dragNode) return {
|
|
1953
|
-
x: null,
|
|
1954
|
-
y: null,
|
|
1955
|
-
snappedPosition: position
|
|
1956
|
-
};
|
|
1957
|
-
const w = dragNode.width ?? 150;
|
|
1958
|
-
const h = dragNode.height ?? 40;
|
|
1959
|
-
const dragCenterX = position.x + w / 2;
|
|
1960
|
-
const dragCenterY = position.y + h / 2;
|
|
1961
|
-
let snapX = null;
|
|
1962
|
-
let snapY = null;
|
|
1963
|
-
let snappedX = position.x;
|
|
1964
|
-
let snappedY = position.y;
|
|
1965
|
-
for (const node of nodes.peek()) {
|
|
1966
|
-
if (node.id === dragNodeId) continue;
|
|
1967
|
-
const nw = node.width ?? 150;
|
|
1968
|
-
const nh = node.height ?? 40;
|
|
1969
|
-
const nodeCenterX = node.position.x + nw / 2;
|
|
1970
|
-
const nodeCenterY = node.position.y + nh / 2;
|
|
1971
|
-
if (Math.abs(dragCenterX - nodeCenterX) < threshold) {
|
|
1972
|
-
snapX = nodeCenterX;
|
|
1973
|
-
snappedX = nodeCenterX - w / 2;
|
|
1974
|
-
}
|
|
1975
|
-
if (Math.abs(position.x - node.position.x) < threshold) {
|
|
1976
|
-
snapX = node.position.x;
|
|
1977
|
-
snappedX = node.position.x;
|
|
1978
|
-
}
|
|
1979
|
-
if (Math.abs(position.x + w - (node.position.x + nw)) < threshold) {
|
|
1980
|
-
snapX = node.position.x + nw;
|
|
1981
|
-
snappedX = node.position.x + nw - w;
|
|
1982
|
-
}
|
|
1983
|
-
if (Math.abs(dragCenterY - nodeCenterY) < threshold) {
|
|
1984
|
-
snapY = nodeCenterY;
|
|
1985
|
-
snappedY = nodeCenterY - h / 2;
|
|
1986
|
-
}
|
|
1987
|
-
if (Math.abs(position.y - node.position.y) < threshold) {
|
|
1988
|
-
snapY = node.position.y;
|
|
1989
|
-
snappedY = node.position.y;
|
|
1990
|
-
}
|
|
1991
|
-
if (Math.abs(position.y + h - (node.position.y + nh)) < threshold) {
|
|
1992
|
-
snapY = node.position.y + nh;
|
|
1993
|
-
snappedY = node.position.y + nh - h;
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
1996
|
-
return {
|
|
1997
|
-
x: snapX,
|
|
1998
|
-
y: snapY,
|
|
1999
|
-
snappedPosition: {
|
|
2000
|
-
x: snappedX,
|
|
2001
|
-
y: snappedY
|
|
2002
|
-
}
|
|
2003
|
-
};
|
|
2004
|
-
}
|
|
2005
|
-
function getChildNodes(parentId) {
|
|
2006
|
-
return nodes.peek().filter(n => n.parentId === parentId);
|
|
2007
|
-
}
|
|
2008
|
-
function getAbsolutePosition(nodeId) {
|
|
2009
|
-
const node = getNode(nodeId);
|
|
2010
|
-
if (!node) return {
|
|
2011
|
-
x: 0,
|
|
2012
|
-
y: 0
|
|
2013
|
-
};
|
|
2014
|
-
if (node.parentId) {
|
|
2015
|
-
const parentPos = getAbsolutePosition(node.parentId);
|
|
2016
|
-
return {
|
|
2017
|
-
x: parentPos.x + node.position.x,
|
|
2018
|
-
y: parentPos.y + node.position.y
|
|
2019
|
-
};
|
|
2020
|
-
}
|
|
2021
|
-
return node.position;
|
|
2022
|
-
}
|
|
2023
|
-
function reconnectEdge(targetEdgeId, newConnection) {
|
|
2024
|
-
edges.update(eds => eds.map(e => {
|
|
2025
|
-
if (e.id !== targetEdgeId) return e;
|
|
2026
|
-
return {
|
|
2027
|
-
...e,
|
|
2028
|
-
source: newConnection.source ?? e.source,
|
|
2029
|
-
target: newConnection.target ?? e.target,
|
|
2030
|
-
sourceHandle: newConnection.sourceHandle ?? e.sourceHandle,
|
|
2031
|
-
targetHandle: newConnection.targetHandle ?? e.targetHandle
|
|
2032
|
-
};
|
|
2033
|
-
}));
|
|
2034
|
-
}
|
|
2035
|
-
function addEdgeWaypoint(edgeIdentifier, point, index) {
|
|
2036
|
-
edges.update(eds => eds.map(e => {
|
|
2037
|
-
if (e.id !== edgeIdentifier) return e;
|
|
2038
|
-
const waypoints = [...(e.waypoints ?? [])];
|
|
2039
|
-
if (index !== void 0) waypoints.splice(index, 0, point);else waypoints.push(point);
|
|
2040
|
-
return {
|
|
2041
|
-
...e,
|
|
2042
|
-
waypoints
|
|
2043
|
-
};
|
|
2044
|
-
}));
|
|
2045
|
-
}
|
|
2046
|
-
function removeEdgeWaypoint(edgeIdentifier, index) {
|
|
2047
|
-
edges.update(eds => eds.map(e => {
|
|
2048
|
-
if (e.id !== edgeIdentifier) return e;
|
|
2049
|
-
const waypoints = [...(e.waypoints ?? [])];
|
|
2050
|
-
waypoints.splice(index, 1);
|
|
2051
|
-
return {
|
|
2052
|
-
...e,
|
|
2053
|
-
waypoints: waypoints.length > 0 ? waypoints : void 0
|
|
2054
|
-
};
|
|
2055
|
-
}));
|
|
2056
|
-
}
|
|
2057
|
-
function updateEdgeWaypoint(edgeIdentifier, index, point) {
|
|
2058
|
-
edges.update(eds => eds.map(e => {
|
|
2059
|
-
if (e.id !== edgeIdentifier) return e;
|
|
2060
|
-
const waypoints = [...(e.waypoints ?? [])];
|
|
2061
|
-
if (index >= 0 && index < waypoints.length) waypoints[index] = point;
|
|
2062
|
-
return {
|
|
2063
|
-
...e,
|
|
2064
|
-
waypoints
|
|
2065
|
-
};
|
|
2066
|
-
}));
|
|
2067
|
-
}
|
|
2068
|
-
function getProximityConnection(nodeId, threshold = 50) {
|
|
2069
|
-
const node = getNode(nodeId);
|
|
2070
|
-
if (!node) return null;
|
|
2071
|
-
const w = node.width ?? 150;
|
|
2072
|
-
const h = node.height ?? 40;
|
|
2073
|
-
const centerX = node.position.x + w / 2;
|
|
2074
|
-
const centerY = node.position.y + h / 2;
|
|
2075
|
-
let closest = null;
|
|
2076
|
-
for (const other of nodes.peek()) {
|
|
2077
|
-
if (other.id === nodeId) continue;
|
|
2078
|
-
if (edges.peek().some(e => e.source === nodeId && e.target === other.id || e.source === other.id && e.target === nodeId)) continue;
|
|
2079
|
-
const ow = other.width ?? 150;
|
|
2080
|
-
const oh = other.height ?? 40;
|
|
2081
|
-
const ocx = other.position.x + ow / 2;
|
|
2082
|
-
const ocy = other.position.y + oh / 2;
|
|
2083
|
-
const dist = Math.hypot(centerX - ocx, centerY - ocy);
|
|
2084
|
-
if (dist < threshold && (!closest || dist < closest.dist)) closest = {
|
|
2085
|
-
nodeId: other.id,
|
|
2086
|
-
dist
|
|
2087
|
-
};
|
|
2088
|
-
}
|
|
2089
|
-
if (!closest) return null;
|
|
2090
|
-
const connection = {
|
|
2091
|
-
source: nodeId,
|
|
2092
|
-
target: closest.nodeId
|
|
2093
|
-
};
|
|
2094
|
-
return isValidConnection(connection) ? connection : null;
|
|
2095
|
-
}
|
|
2096
|
-
function getOverlappingNodes(nodeId) {
|
|
2097
|
-
const node = getNode(nodeId);
|
|
2098
|
-
if (!node) return [];
|
|
2099
|
-
const w = node.width ?? 150;
|
|
2100
|
-
const h = node.height ?? 40;
|
|
2101
|
-
const ax1 = node.position.x;
|
|
2102
|
-
const ay1 = node.position.y;
|
|
2103
|
-
const ax2 = ax1 + w;
|
|
2104
|
-
const ay2 = ay1 + h;
|
|
2105
|
-
return nodes.peek().filter(other => {
|
|
2106
|
-
if (other.id === nodeId) return false;
|
|
2107
|
-
const ow = other.width ?? 150;
|
|
2108
|
-
const oh = other.height ?? 40;
|
|
2109
|
-
const bx1 = other.position.x;
|
|
2110
|
-
const by1 = other.position.y;
|
|
2111
|
-
const bx2 = bx1 + ow;
|
|
2112
|
-
const by2 = by1 + oh;
|
|
2113
|
-
return ax1 < bx2 && ax2 > bx1 && ay1 < by2 && ay2 > by1;
|
|
2114
|
-
});
|
|
2115
|
-
}
|
|
2116
|
-
function resolveCollisions(nodeId, spacing = 10) {
|
|
2117
|
-
const overlapping = getOverlappingNodes(nodeId);
|
|
2118
|
-
if (overlapping.length === 0) return;
|
|
2119
|
-
const node = getNode(nodeId);
|
|
2120
|
-
if (!node) return;
|
|
2121
|
-
const w = node.width ?? 150;
|
|
2122
|
-
const h = node.height ?? 40;
|
|
2123
|
-
for (const other of overlapping) {
|
|
2124
|
-
const ow = other.width ?? 150;
|
|
2125
|
-
const oh = other.height ?? 40;
|
|
2126
|
-
const overlapX = Math.min(node.position.x + w - other.position.x, other.position.x + ow - node.position.x);
|
|
2127
|
-
const overlapY = Math.min(node.position.y + h - other.position.y, other.position.y + oh - node.position.y);
|
|
2128
|
-
if (overlapX < overlapY) {
|
|
2129
|
-
const dx = node.position.x < other.position.x ? -(overlapX + spacing) / 2 : (overlapX + spacing) / 2;
|
|
2130
|
-
updateNodePosition(other.id, {
|
|
2131
|
-
x: other.position.x - dx,
|
|
2132
|
-
y: other.position.y
|
|
2133
|
-
});
|
|
2134
|
-
} else {
|
|
2135
|
-
const dy = node.position.y < other.position.y ? -(overlapY + spacing) / 2 : (overlapY + spacing) / 2;
|
|
2136
|
-
updateNodePosition(other.id, {
|
|
2137
|
-
x: other.position.x,
|
|
2138
|
-
y: other.position.y - dy
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
}
|
|
2143
|
-
function setNodeExtent(extent) {
|
|
2144
|
-
nodeExtent = extent;
|
|
2145
|
-
}
|
|
2146
|
-
let nodeExtent = config.nodeExtent ?? null;
|
|
2147
|
-
function clampToExtent(position, nodeWidth = 150, nodeHeight = 40) {
|
|
2148
|
-
if (!nodeExtent) return position;
|
|
2149
|
-
return {
|
|
2150
|
-
x: Math.min(Math.max(position.x, nodeExtent[0][0]), nodeExtent[1][0] - nodeWidth),
|
|
2151
|
-
y: Math.min(Math.max(position.y, nodeExtent[0][1]), nodeExtent[1][1] - nodeHeight)
|
|
2152
|
-
};
|
|
2153
|
-
}
|
|
2154
|
-
function findNodes(predicate) {
|
|
2155
|
-
return nodes.peek().filter(predicate);
|
|
2156
|
-
}
|
|
2157
|
-
function searchNodes(query) {
|
|
2158
|
-
const q = query.toLowerCase();
|
|
2159
|
-
return nodes.peek().filter(n => {
|
|
2160
|
-
return (n.data?.label ?? n.id).toLowerCase().includes(q);
|
|
2161
|
-
});
|
|
2162
|
-
}
|
|
2163
|
-
function focusNode(nodeId, focusZoom) {
|
|
2164
|
-
const node = getNode(nodeId);
|
|
2165
|
-
if (!node) return;
|
|
2166
|
-
const w = node.width ?? 150;
|
|
2167
|
-
const h = node.height ?? 40;
|
|
2168
|
-
const centerX = node.position.x + w / 2;
|
|
2169
|
-
const centerY = node.position.y + h / 2;
|
|
2170
|
-
const z = focusZoom ?? viewport.peek().zoom;
|
|
2171
|
-
const {
|
|
2172
|
-
width: cw,
|
|
2173
|
-
height: ch
|
|
2174
|
-
} = containerSize.peek();
|
|
2175
|
-
animateViewport({
|
|
2176
|
-
x: -centerX * z + cw / 2,
|
|
2177
|
-
y: -centerY * z + ch / 2,
|
|
2178
|
-
zoom: z
|
|
2179
|
-
});
|
|
2180
|
-
selectNode(nodeId);
|
|
2181
|
-
}
|
|
2182
|
-
function toJSON() {
|
|
2183
|
-
return {
|
|
2184
|
-
nodes: structuredClone(nodes.peek()),
|
|
2185
|
-
edges: structuredClone(edges.peek()),
|
|
2186
|
-
viewport: {
|
|
2187
|
-
...viewport.peek()
|
|
2188
|
-
}
|
|
2189
|
-
};
|
|
2190
|
-
}
|
|
2191
|
-
function fromJSON(data) {
|
|
2192
|
-
batch(() => {
|
|
2193
|
-
nodes.set(data.nodes);
|
|
2194
|
-
edges.set(data.edges.map(e => ({
|
|
2195
|
-
...e,
|
|
2196
|
-
id: e.id ?? edgeId(e),
|
|
2197
|
-
type: e.type ?? defaultEdgeType
|
|
2198
|
-
})));
|
|
2199
|
-
if (data.viewport) viewport.set(data.viewport);
|
|
2200
|
-
clearSelection();
|
|
2201
|
-
});
|
|
2202
|
-
}
|
|
2203
|
-
function animateViewport(target, duration = 300) {
|
|
2204
|
-
const start = {
|
|
2205
|
-
...viewport.peek()
|
|
2206
|
-
};
|
|
2207
|
-
const end = {
|
|
2208
|
-
x: target.x ?? start.x,
|
|
2209
|
-
y: target.y ?? start.y,
|
|
2210
|
-
zoom: target.zoom ?? start.zoom
|
|
2211
|
-
};
|
|
2212
|
-
const startTime = performance.now();
|
|
2213
|
-
const frame = () => {
|
|
2214
|
-
const elapsed = performance.now() - startTime;
|
|
2215
|
-
const t = Math.min(elapsed / duration, 1);
|
|
2216
|
-
const eased = 1 - (1 - t) ** 3;
|
|
2217
|
-
viewport.set({
|
|
2218
|
-
x: start.x + (end.x - start.x) * eased,
|
|
2219
|
-
y: start.y + (end.y - start.y) * eased,
|
|
2220
|
-
zoom: start.zoom + (end.zoom - start.zoom) * eased
|
|
2221
|
-
});
|
|
2222
|
-
if (t < 1) requestAnimationFrame(frame);
|
|
2223
|
-
};
|
|
2224
|
-
requestAnimationFrame(frame);
|
|
2225
|
-
}
|
|
2226
|
-
function dispose() {
|
|
2227
|
-
connectListeners.clear();
|
|
2228
|
-
nodesChangeListeners.clear();
|
|
2229
|
-
nodeClickListeners.clear();
|
|
2230
|
-
edgeClickListeners.clear();
|
|
2231
|
-
nodeDragStartListeners.clear();
|
|
2232
|
-
nodeDragEndListeners.clear();
|
|
2233
|
-
nodeDoubleClickListeners.clear();
|
|
2234
|
-
}
|
|
2235
|
-
if (config.fitView) fitView();
|
|
2236
|
-
return {
|
|
2237
|
-
nodes,
|
|
2238
|
-
edges,
|
|
2239
|
-
viewport,
|
|
2240
|
-
zoom,
|
|
2241
|
-
containerSize,
|
|
2242
|
-
selectedNodes,
|
|
2243
|
-
selectedEdges,
|
|
2244
|
-
getNode,
|
|
2245
|
-
addNode,
|
|
2246
|
-
removeNode,
|
|
2247
|
-
updateNode,
|
|
2248
|
-
updateNodePosition,
|
|
2249
|
-
getEdge,
|
|
2250
|
-
addEdge,
|
|
2251
|
-
removeEdge,
|
|
2252
|
-
isValidConnection,
|
|
2253
|
-
selectNode,
|
|
2254
|
-
deselectNode,
|
|
2255
|
-
selectEdge,
|
|
2256
|
-
clearSelection,
|
|
2257
|
-
selectAll,
|
|
2258
|
-
deleteSelected,
|
|
2259
|
-
fitView,
|
|
2260
|
-
zoomTo,
|
|
2261
|
-
zoomIn,
|
|
2262
|
-
zoomOut,
|
|
2263
|
-
panTo,
|
|
2264
|
-
isNodeVisible,
|
|
2265
|
-
layout,
|
|
2266
|
-
batch: batchOp,
|
|
2267
|
-
getConnectedEdges,
|
|
2268
|
-
getIncomers,
|
|
2269
|
-
getOutgoers,
|
|
2270
|
-
onConnect,
|
|
2271
|
-
onNodesChange,
|
|
2272
|
-
onNodeClick,
|
|
2273
|
-
onEdgeClick,
|
|
2274
|
-
onNodeDragStart,
|
|
2275
|
-
onNodeDragEnd,
|
|
2276
|
-
onNodeDoubleClick,
|
|
2277
|
-
_emit: {
|
|
2278
|
-
nodeDragStart: node => {
|
|
2279
|
-
for (const cb of nodeDragStartListeners) cb(node);
|
|
2280
|
-
},
|
|
2281
|
-
nodeDragEnd: node => {
|
|
2282
|
-
for (const cb of nodeDragEndListeners) cb(node);
|
|
2283
|
-
},
|
|
2284
|
-
nodeDoubleClick: node => {
|
|
2285
|
-
for (const cb of nodeDoubleClickListeners) cb(node);
|
|
2286
|
-
},
|
|
2287
|
-
nodeClick: node => {
|
|
2288
|
-
for (const cb of nodeClickListeners) cb(node);
|
|
2289
|
-
},
|
|
2290
|
-
edgeClick: edge => {
|
|
2291
|
-
for (const cb of edgeClickListeners) cb(edge);
|
|
2292
|
-
}
|
|
2293
|
-
},
|
|
2294
|
-
copySelected,
|
|
2295
|
-
paste,
|
|
2296
|
-
pushHistory,
|
|
2297
|
-
undo,
|
|
2298
|
-
redo,
|
|
2299
|
-
moveSelectedNodes,
|
|
2300
|
-
getSnapLines,
|
|
2301
|
-
getChildNodes,
|
|
2302
|
-
getAbsolutePosition,
|
|
2303
|
-
addEdgeWaypoint,
|
|
2304
|
-
removeEdgeWaypoint,
|
|
2305
|
-
updateEdgeWaypoint,
|
|
2306
|
-
reconnectEdge,
|
|
2307
|
-
getProximityConnection,
|
|
2308
|
-
getOverlappingNodes,
|
|
2309
|
-
resolveCollisions,
|
|
2310
|
-
setNodeExtent,
|
|
2311
|
-
clampToExtent,
|
|
2312
|
-
findNodes,
|
|
2313
|
-
searchNodes,
|
|
2314
|
-
focusNode,
|
|
2315
|
-
toJSON,
|
|
2316
|
-
fromJSON,
|
|
2317
|
-
animateViewport,
|
|
2318
|
-
config,
|
|
2319
|
-
dispose
|
|
589
|
+
* Calculate a smoothstep edge path — horizontal/vertical segments with rounded corners.
|
|
590
|
+
*/
|
|
591
|
+
declare function getSmoothStepPath(params: {
|
|
592
|
+
sourceX: number;
|
|
593
|
+
sourceY: number;
|
|
594
|
+
sourcePosition?: Position;
|
|
595
|
+
targetX: number;
|
|
596
|
+
targetY: number;
|
|
597
|
+
targetPosition?: Position;
|
|
598
|
+
borderRadius?: number;
|
|
599
|
+
offset?: number;
|
|
600
|
+
}): EdgePathResult;
|
|
601
|
+
/**
|
|
602
|
+
* Calculate a straight edge path — direct line between two points.
|
|
603
|
+
*/
|
|
604
|
+
declare function getStraightPath(params: {
|
|
605
|
+
sourceX: number;
|
|
606
|
+
sourceY: number;
|
|
607
|
+
targetX: number;
|
|
608
|
+
targetY: number;
|
|
609
|
+
}): EdgePathResult;
|
|
610
|
+
/**
|
|
611
|
+
* Calculate a step edge path — right-angle segments with no rounding.
|
|
612
|
+
*/
|
|
613
|
+
declare function getStepPath(params: {
|
|
614
|
+
sourceX: number;
|
|
615
|
+
sourceY: number;
|
|
616
|
+
sourcePosition?: Position;
|
|
617
|
+
targetX: number;
|
|
618
|
+
targetY: number;
|
|
619
|
+
targetPosition?: Position;
|
|
620
|
+
}): EdgePathResult;
|
|
621
|
+
/**
|
|
622
|
+
* Calculate an edge path that passes through waypoints.
|
|
623
|
+
* Uses line segments with optional smoothing.
|
|
624
|
+
*/
|
|
625
|
+
declare function getWaypointPath(params: {
|
|
626
|
+
sourceX: number;
|
|
627
|
+
sourceY: number;
|
|
628
|
+
targetX: number;
|
|
629
|
+
targetY: number;
|
|
630
|
+
waypoints: XYPosition[];
|
|
631
|
+
}): EdgePathResult;
|
|
632
|
+
/**
|
|
633
|
+
* Get the edge path for a given edge type.
|
|
634
|
+
*/
|
|
635
|
+
declare function getEdgePath(type: string, sourceX: number, sourceY: number, sourcePosition: Position, targetX: number, targetY: number, targetPosition: Position): EdgePathResult;
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/flow.d.ts
|
|
638
|
+
/**
|
|
639
|
+
* Create a reactive flow instance — the core state manager for flow diagrams.
|
|
640
|
+
*
|
|
641
|
+
* All state is signal-based. Nodes, edges, viewport, and selection are
|
|
642
|
+
* reactive and update the UI automatically when modified.
|
|
643
|
+
*
|
|
644
|
+
* @param config - Initial configuration with nodes, edges, and options
|
|
645
|
+
* @returns A FlowInstance with signals and methods for managing the diagram
|
|
646
|
+
*
|
|
647
|
+
* @example
|
|
648
|
+
* ```tsx
|
|
649
|
+
* const flow = createFlow({
|
|
650
|
+
* nodes: [
|
|
651
|
+
* { id: '1', position: { x: 0, y: 0 }, data: { label: 'Start' } },
|
|
652
|
+
* { id: '2', position: { x: 200, y: 100 }, data: { label: 'End' } },
|
|
653
|
+
* ],
|
|
654
|
+
* edges: [{ source: '1', target: '2' }],
|
|
655
|
+
* })
|
|
656
|
+
*
|
|
657
|
+
* flow.nodes() // reactive node list
|
|
658
|
+
* flow.viewport() // { x: 0, y: 0, zoom: 1 }
|
|
659
|
+
* flow.addNode({ id: '3', position: { x: 400, y: 0 }, data: { label: 'New' } })
|
|
660
|
+
* flow.layout('layered', { direction: 'RIGHT' })
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
663
|
+
declare function createFlow(config?: FlowConfig): FlowInstance;
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/layout.d.ts
|
|
666
|
+
/**
|
|
667
|
+
* Compute a layout for the given nodes and edges using elkjs.
|
|
668
|
+
* Returns an array of { id, position } for each node.
|
|
669
|
+
*
|
|
670
|
+
* elkjs is lazy-loaded — zero bundle cost until this function is called.
|
|
671
|
+
*
|
|
672
|
+
* @example
|
|
673
|
+
* ```ts
|
|
674
|
+
* const positions = await computeLayout(nodes, edges, 'layered', {
|
|
675
|
+
* direction: 'RIGHT',
|
|
676
|
+
* nodeSpacing: 50,
|
|
677
|
+
* layerSpacing: 100,
|
|
678
|
+
* })
|
|
679
|
+
* // positions: [{ id: '1', position: { x: 0, y: 0 } }, ...]
|
|
680
|
+
* ```
|
|
681
|
+
*/
|
|
682
|
+
declare function computeLayout(nodes: FlowNode[], edges: FlowEdge[], algorithm?: LayoutAlgorithm, options?: LayoutOptions): Promise<Array<{
|
|
683
|
+
id: string;
|
|
684
|
+
position: {
|
|
685
|
+
x: number;
|
|
686
|
+
y: number;
|
|
2320
687
|
};
|
|
2321
|
-
}
|
|
2322
|
-
|
|
688
|
+
}>>;
|
|
2323
689
|
//#endregion
|
|
2324
|
-
//#region src/styles.ts
|
|
690
|
+
//#region src/styles.d.ts
|
|
2325
691
|
/**
|
|
2326
|
-
* Default CSS styles for the flow diagram.
|
|
2327
|
-
* Inject via `<style>` tag or import in your CSS.
|
|
2328
|
-
*
|
|
2329
|
-
* @example
|
|
2330
|
-
* ```tsx
|
|
2331
|
-
* import { flowStyles } from '@pyreon/flow'
|
|
2332
|
-
*
|
|
2333
|
-
* // Inject once at app root
|
|
2334
|
-
* const style = document.createElement('style')
|
|
2335
|
-
* style.textContent = flowStyles
|
|
2336
|
-
* document.head.appendChild(style)
|
|
2337
|
-
* ```
|
|
2338
|
-
*/
|
|
2339
|
-
|
|
692
|
+
* Default CSS styles for the flow diagram.
|
|
693
|
+
* Inject via `<style>` tag or import in your CSS.
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* ```tsx
|
|
697
|
+
* import { flowStyles } from '@pyreon/flow'
|
|
698
|
+
*
|
|
699
|
+
* // Inject once at app root
|
|
700
|
+
* const style = document.createElement('style')
|
|
701
|
+
* style.textContent = flowStyles
|
|
702
|
+
* document.head.appendChild(style)
|
|
703
|
+
* ```
|
|
704
|
+
*/
|
|
705
|
+
declare const flowStyles = "\n/* \u2500\u2500 Animated edges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-edge-animated {\n stroke-dasharray: 5;\n animation: pyreon-flow-edge-dash 0.5s linear infinite;\n}\n\n@keyframes pyreon-flow-edge-dash {\n to {\n stroke-dashoffset: -10;\n }\n}\n\n/* \u2500\u2500 Node states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-node {\n transition: box-shadow 0.15s ease;\n}\n\n.pyreon-flow-node.dragging {\n opacity: 0.9;\n filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));\n cursor: grabbing;\n}\n\n.pyreon-flow-node.selected {\n filter: drop-shadow(0 0 0 2px rgba(59, 130, 246, 0.3));\n}\n\n/* \u2500\u2500 Handles \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-handle {\n transition: transform 0.1s ease, background 0.1s ease;\n}\n\n.pyreon-flow-handle:hover {\n transform: scale(1.4);\n background: #3b82f6 !important;\n}\n\n.pyreon-flow-handle-target:hover {\n background: #22c55e !important;\n border-color: #22c55e !important;\n}\n\n/* \u2500\u2500 Resizer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-resizer {\n transition: background 0.1s ease, transform 0.1s ease;\n}\n\n.pyreon-flow-resizer:hover {\n background: #3b82f6 !important;\n transform: scale(1.2);\n}\n\n/* \u2500\u2500 Selection box \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-selection-box {\n pointer-events: none;\n border-radius: 2px;\n}\n\n/* \u2500\u2500 MiniMap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-minimap {\n transition: opacity 0.2s ease;\n}\n\n.pyreon-flow-minimap:hover {\n opacity: 1 !important;\n}\n\n/* \u2500\u2500 Node toolbar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-node-toolbar {\n animation: pyreon-flow-toolbar-enter 0.15s ease;\n}\n\n@keyframes pyreon-flow-toolbar-enter {\n from {\n opacity: 0;\n transform: translateX(-50%) translateY(4px);\n }\n to {\n opacity: 1;\n transform: translateX(-50%) translateY(0);\n }\n}\n\n/* \u2500\u2500 Controls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.pyreon-flow-controls button:hover {\n background: #f3f4f6 !important;\n}\n\n.pyreon-flow-controls button:active {\n background: #e5e7eb !important;\n}\n";
|
|
2340
706
|
//#endregion
|
|
2341
|
-
export { Background, Controls, Flow, Handle, MiniMap, NodeResizer, NodeToolbar, Panel, Position, computeLayout, createFlow, flowStyles, getBezierPath, getEdgePath, getHandlePosition, getSmartHandlePositions, getSmoothStepPath, getStepPath, getStraightPath, getWaypointPath };
|
|
2342
|
-
//# sourceMappingURL=
|
|
707
|
+
export { Background, type BackgroundProps, type Connection, type ConnectionRule, Controls, type ControlsProps, type Dimensions, type EdgePathResult, type EdgeType, Flow, type FlowComponentProps, type FlowConfig, type FlowEdge, type FlowInstance, type FlowNode, type FlowProps, Handle, type HandleConfig, type HandleProps, type HandleType, type LayoutAlgorithm, type LayoutOptions, MiniMap, type MiniMapProps, type NodeChange, type NodeComponentProps, NodeResizer, type NodeResizerProps, NodeToolbar, type NodeToolbarProps, Panel, type PanelProps, Position, type Rect, type Viewport, type XYPosition, computeLayout, createFlow, flowStyles, getBezierPath, getEdgePath, getHandlePosition, getSmartHandlePositions, getSmoothStepPath, getStepPath, getStraightPath, getWaypointPath };
|
|
708
|
+
//# sourceMappingURL=index2.d.ts.map
|