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