@elabs-ai/components-flow 4.0.0 → 4.1.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.
Files changed (66) hide show
  1. package/README.md +8 -8
  2. package/dist/index.d.ts +734 -20
  3. package/dist/index.js +976 -178
  4. package/dist/index.js.map +1 -1
  5. package/package.json +6 -6
  6. package/src/canvas-shell/canvas-shell.tsx +116 -1
  7. package/src/canvas-shell/use-measured-nodes.ts +101 -0
  8. package/src/flow-button-edge/flow-button-edge.stories.tsx +13 -0
  9. package/src/flow-button-edge/flow-button-edge.tsx +8 -10
  10. package/src/flow-edge/flow-edge.stories.tsx +20 -0
  11. package/src/flow-edge/flow-edge.tsx +10 -3
  12. package/src/flow-edge-path/flow-edge-path.tsx +149 -0
  13. package/src/flow-edge-path/index.ts +1 -0
  14. package/src/flow-edge-path/no-raw-base-edge.test.ts +45 -0
  15. package/src/flow-floating-edge/flow-floating-edge.tsx +7 -3
  16. package/src/flow-group-node/flow-group-node.stories.tsx +1 -1
  17. package/src/flow-group-node/flow-group-node.tsx +24 -8
  18. package/src/flow-handle/flow-handle-anchor.test.tsx +97 -0
  19. package/src/flow-handle/flow-handle-anchor.ts +36 -0
  20. package/src/flow-handle/index.ts +1 -0
  21. package/src/flow-layout/flow-layout.stories.tsx +2 -2
  22. package/src/flow-layout/flow-layout.test.tsx +91 -0
  23. package/src/flow-layout/flow-layout.ts +77 -1
  24. package/src/flow-layout/layout-graph.test.ts +83 -2
  25. package/src/flow-layout/layout-graph.ts +23 -15
  26. package/src/flow-mini-map/flow-mini-map.stories.tsx +86 -0
  27. package/src/flow-node/flow-node.stories.tsx +151 -0
  28. package/src/flow-node/flow-node.tsx +56 -1
  29. package/src/flow-placeholder-node/flow-placeholder-node.tsx +5 -2
  30. package/src/flow-self-loop-edge/flow-self-loop-edge.stories.tsx +275 -0
  31. package/src/flow-self-loop-edge/flow-self-loop-edge.test.tsx +196 -0
  32. package/src/flow-self-loop-edge/flow-self-loop-edge.tsx +172 -0
  33. package/src/flow-self-loop-edge/index.ts +13 -0
  34. package/src/flow-self-loop-edge/self-loop-geometry.test.ts +128 -0
  35. package/src/flow-self-loop-edge/self-loop-geometry.ts +165 -0
  36. package/src/flow-smart-edge/flow-smart-edge.stories.tsx +55 -8
  37. package/src/flow-smart-edge/flow-smart-edge.tsx +125 -35
  38. package/src/flow-smart-edge/index.ts +5 -1
  39. package/src/flow-smart-edge/smart-edge-geometry.test.ts +88 -47
  40. package/src/flow-smart-edge/smart-edge-geometry.ts +69 -33
  41. package/src/flow-weighted-edge/back-edge-geometry.test.ts +54 -0
  42. package/src/flow-weighted-edge/back-edge-geometry.ts +60 -0
  43. package/src/flow-weighted-edge/edge-aria.test.ts +108 -0
  44. package/src/flow-weighted-edge/edge-aria.ts +117 -0
  45. package/src/flow-weighted-edge/edge-label-pill.test.tsx +65 -0
  46. package/src/flow-weighted-edge/edge-label-pill.tsx +82 -0
  47. package/src/flow-weighted-edge/flow-weighted-edge.stories.tsx +691 -0
  48. package/src/flow-weighted-edge/flow-weighted-edge.test.tsx +405 -0
  49. package/src/flow-weighted-edge/flow-weighted-edge.tsx +308 -0
  50. package/src/flow-weighted-edge/index.ts +18 -0
  51. package/src/flow-weighted-edge/weight-scale.test.ts +92 -0
  52. package/src/flow-weighted-edge/weight-scale.ts +86 -0
  53. package/src/index.ts +9 -0
  54. package/src/inspector-panel/inspector-panel.stories.tsx +1 -1
  55. package/src/inspector-panel/inspector-panel.test.tsx +20 -0
  56. package/src/inspector-panel/inspector-panel.tsx +13 -3
  57. package/src/legend/index.ts +7 -1
  58. package/src/legend/legend.stories.tsx +126 -0
  59. package/src/legend/legend.test.tsx +169 -0
  60. package/src/legend/legend.tsx +214 -3
  61. package/src/templates-flow-workspace.stories.tsx +1 -1
  62. package/src/testing/canvas-framing.test.ts +107 -0
  63. package/src/testing/canvas-framing.ts +396 -0
  64. package/src/testing/edge-anchors.ts +107 -0
  65. package/src/testing/index.ts +36 -0
  66. package/src/zoom-controls/zoom-controls.tsx +1 -1
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  "use client";
2
2
 
3
3
  // src/canvas-shell/canvas-shell.tsx
4
- import "react";
4
+ import { useEffect } from "react";
5
5
  import {
6
6
  Background,
7
7
  ReactFlow,
8
- ReactFlowProvider
8
+ ReactFlowProvider,
9
+ useNodesInitialized,
10
+ useReactFlow as useReactFlow2,
11
+ useStoreApi
9
12
  } from "@xyflow/react";
10
13
  import { cn as cn2 } from "@elabs-ai/components-ui/lib/cn";
11
14
 
@@ -149,6 +152,51 @@ function useHelperLines(onNodesChange, options = {}) {
149
152
  return { onNodesChange: handleNodesChange, helperLineHorizontal, helperLineVertical };
150
153
  }
151
154
 
155
+ // src/canvas-shell/use-measured-nodes.ts
156
+ import { useCallback as useCallback2, useMemo, useRef, useState as useState2 } from "react";
157
+ function useMeasuredNodes(nodes, onNodesChange) {
158
+ const measured = useRef(/* @__PURE__ */ new Map());
159
+ const [version, setVersion] = useState2(0);
160
+ const handleNodesChange = useCallback2(
161
+ (changes) => {
162
+ let touched = false;
163
+ for (const change of changes) {
164
+ if (change.type !== "dimensions" || !change.dimensions) continue;
165
+ const previous = measured.current.get(change.id);
166
+ if (previous?.width === change.dimensions.width && previous.height === change.dimensions.height) {
167
+ continue;
168
+ }
169
+ measured.current.set(change.id, { ...change.dimensions });
170
+ touched = true;
171
+ }
172
+ onNodesChange?.(changes);
173
+ if (touched) setVersion((n) => n + 1);
174
+ },
175
+ [onNodesChange]
176
+ );
177
+ const mergedNodes = useMemo(() => {
178
+ if (!nodes) return nodes;
179
+ void version;
180
+ let changed = false;
181
+ const next = nodes.map((node) => {
182
+ const dimensions = measured.current.get(node.id);
183
+ if (!dimensions) return node;
184
+ if (node.measured?.width === dimensions.width && node.measured?.height === dimensions.height) {
185
+ return node;
186
+ }
187
+ changed = true;
188
+ return { ...node, measured: dimensions };
189
+ });
190
+ return changed ? next : nodes;
191
+ }, [nodes, version]);
192
+ return {
193
+ nodes: mergedNodes,
194
+ // Only take the handler over when there is something to measure INTO. An uncontrolled
195
+ // canvas is left exactly as it was.
196
+ onNodesChange: nodes ? handleNodesChange : onNodesChange
197
+ };
198
+ }
199
+
152
200
  // src/canvas-shell/canvas-shell.tsx
153
201
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
154
202
  var DEFAULT_ARIA_LABEL_CONFIG = {
@@ -177,22 +225,65 @@ function CanvasShellBase({
177
225
  children,
178
226
  className,
179
227
  ariaLabelConfig,
228
+ nodes,
229
+ onNodesChange,
230
+ fitViewKey,
231
+ fitViewKeyOptions,
180
232
  ...props
181
233
  }) {
234
+ const measured = useMeasuredNodes(nodes, onNodesChange);
182
235
  return /* @__PURE__ */ jsx2("div", { className: cn2("h-full w-full bg-canvas", className), children: /* @__PURE__ */ jsxs2(
183
236
  ReactFlow,
184
237
  {
185
238
  fitView: true,
186
239
  proOptions: { hideAttribution: true },
187
240
  ariaLabelConfig: { ...DEFAULT_ARIA_LABEL_CONFIG, ...ariaLabelConfig },
241
+ nodes: measured.nodes,
242
+ onNodesChange: measured.onNodesChange,
188
243
  ...props,
189
244
  children: [
190
245
  background ? /* @__PURE__ */ jsx2(Background, { gap: 20, size: 1, color: "var(--canvas-grid)" }) : null,
246
+ fitViewKey === void 0 ? null : /* @__PURE__ */ jsx2(FitViewOnKey, { fitViewKey, options: fitViewKeyOptions }),
191
247
  children
192
248
  ]
193
249
  }
194
250
  ) });
195
251
  }
252
+ function FitViewOnKey({
253
+ fitViewKey,
254
+ options
255
+ }) {
256
+ const { fitView, getNodes, getNodesBounds: getNodesBounds2, setViewport } = useReactFlow2();
257
+ const store = useStoreApi();
258
+ const nodesInitialized = useNodesInitialized();
259
+ useEffect(() => {
260
+ if (!nodesInitialized) return;
261
+ void fitView(options).then(
262
+ () => anchorToStartWhenClamped(store, setViewport, getNodesBounds2, getNodes(), options)
263
+ );
264
+ }, [fitViewKey, nodesInitialized, fitView, getNodes, getNodesBounds2, setViewport, store]);
265
+ return null;
266
+ }
267
+ function anchorToStartWhenClamped(store, setViewport, getNodesBounds2, nodes, options) {
268
+ if (nodes.length === 0) return;
269
+ const { width, height, transform, panZoom } = store.getState();
270
+ const [x, y, zoom] = transform;
271
+ if (!width || !height || !zoom) return;
272
+ const bounds = getNodesBounds2(nodes);
273
+ const padding = typeof options?.padding === "number" ? options.padding : 0.1;
274
+ const padX = width * padding;
275
+ const padY = height * padding;
276
+ const overflowsX = bounds.width * zoom > width - padX * 2 + 1;
277
+ const overflowsY = bounds.height * zoom > height - padY * 2 + 1;
278
+ if (!overflowsX && !overflowsY) return;
279
+ const next = {
280
+ x: overflowsX ? padX - bounds.x * zoom : x,
281
+ y: overflowsY ? padY - bounds.y * zoom : y,
282
+ zoom
283
+ };
284
+ if (!panZoom) return;
285
+ void setViewport(next);
286
+ }
196
287
  function CanvasShellWithHelperLines({
197
288
  onNodesChange,
198
289
  children,
@@ -209,6 +300,9 @@ function CanvasShellWithHelperLines({
209
300
  ] });
210
301
  }
211
302
 
303
+ // src/flow-handle/flow-handle-anchor.ts
304
+ var FLOW_HANDLE_ANCHOR_CLASS = "transition-none";
305
+
212
306
  // src/flow-node/flow-node.tsx
213
307
  import "react";
214
308
  import { Handle, Position } from "@xyflow/react";
@@ -251,7 +345,7 @@ var sidePosition = {
251
345
  bottom: Position.Bottom,
252
346
  left: Position.Left
253
347
  };
254
- var handleClassName = "!size-2 !border-2 !border-flow-edge !bg-flow-node";
348
+ var handleClassName = `!size-2 !border-2 !border-flow-edge !bg-flow-node ${FLOW_HANDLE_ANCHOR_CLASS}`;
255
349
  function FlowNode({
256
350
  data,
257
351
  selected,
@@ -263,11 +357,13 @@ function FlowNode({
263
357
  return /* @__PURE__ */ jsxs3(
264
358
  "div",
265
359
  {
360
+ "data-slot": "flow-node",
266
361
  "data-tone": tone,
267
362
  className: cn3(
268
363
  "min-w-44 rounded-lg border bg-flow-node px-3 py-2 text-flow-node-foreground shadow-sm transition-[box-shadow,border-color] duration-fast ease-standard",
269
364
  toneRing[tone],
270
- selected && "ring-2 ring-ring"
365
+ selected && "ring-2 ring-ring",
366
+ "[[data-id]:focus-visible_&]:focus-ring-static"
271
367
  ),
272
368
  children: [
273
369
  data.handles ? /* @__PURE__ */ jsxs3(Fragment, { children: [
@@ -318,6 +414,7 @@ function FlowNode({
318
414
  ] }),
319
415
  ToneIcon ? /* @__PURE__ */ jsx3(ToneIcon, { "aria-hidden": "true", className: cn3("size-3.5 shrink-0", toneIconColor[tone]) }) : null
320
416
  ] }),
417
+ data.footer ? /* @__PURE__ */ jsx3("div", { className: "mt-2", children: data.footer }) : null,
321
418
  toneLabel[tone] ? /* @__PURE__ */ jsx3("span", { className: "sr-only", children: toneLabel[tone] }) : null
322
419
  ]
323
420
  }
@@ -325,8 +422,81 @@ function FlowNode({
325
422
  }
326
423
 
327
424
  // src/flow-edge/flow-edge.tsx
328
- import { BaseEdge, getBezierPath } from "@xyflow/react";
329
- import { jsx as jsx4 } from "react/jsx-runtime";
425
+ import { getBezierPath } from "@xyflow/react";
426
+
427
+ // src/flow-edge-path/flow-edge-path.tsx
428
+ import { BaseEdge } from "@xyflow/react";
429
+ import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
430
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
431
+ var FLOW_EDGE_FOCUS_RING_WIDTH = 3;
432
+ var FLOW_EDGE_FOCUS_CONTOUR_WIDTH = 6;
433
+ function FlowEdgePath({
434
+ path,
435
+ stroke,
436
+ strokeWidth,
437
+ strokeDasharray,
438
+ strokeOpacity,
439
+ markerEnd,
440
+ markerStart,
441
+ interactionWidth,
442
+ className,
443
+ style,
444
+ ...props
445
+ }) {
446
+ const focusDash = strokeDasharray ?? "none";
447
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
448
+ /* @__PURE__ */ jsx4(
449
+ "path",
450
+ {
451
+ d: path,
452
+ fill: "none",
453
+ "aria-hidden": "true",
454
+ "data-slot": "flow-edge-focus-contour",
455
+ className: "pointer-events-none stroke-foreground opacity-0 [.react-flow\\_\\_edge:focus-visible_&]:opacity-100",
456
+ strokeWidth: strokeWidth + FLOW_EDGE_FOCUS_CONTOUR_WIDTH,
457
+ strokeDasharray: focusDash,
458
+ strokeLinecap: "round"
459
+ }
460
+ ),
461
+ /* @__PURE__ */ jsx4(
462
+ "path",
463
+ {
464
+ d: path,
465
+ fill: "none",
466
+ "aria-hidden": "true",
467
+ "data-slot": "flow-edge-focus-ring",
468
+ className: "pointer-events-none stroke-ring opacity-0 [.react-flow\\_\\_edge:focus-visible_&]:opacity-100",
469
+ strokeWidth: strokeWidth + FLOW_EDGE_FOCUS_RING_WIDTH,
470
+ strokeDasharray: focusDash,
471
+ strokeLinecap: "round"
472
+ }
473
+ ),
474
+ /* @__PURE__ */ jsx4(
475
+ BaseEdge,
476
+ {
477
+ ...props,
478
+ path,
479
+ markerEnd,
480
+ markerStart,
481
+ interactionWidth,
482
+ className: cn4(
483
+ "transition-[stroke,stroke-width] duration-fast ease-standard motion-reduce:transition-none",
484
+ className
485
+ ),
486
+ style: {
487
+ stroke,
488
+ strokeWidth,
489
+ ...strokeDasharray ? { strokeDasharray } : null,
490
+ ...strokeOpacity !== void 0 ? { strokeOpacity } : null,
491
+ ...style
492
+ }
493
+ }
494
+ )
495
+ ] });
496
+ }
497
+
498
+ // src/flow-edge/flow-edge.tsx
499
+ import { jsx as jsx5 } from "react/jsx-runtime";
330
500
  function FlowEdge({
331
501
  id,
332
502
  sourceX,
@@ -346,20 +516,22 @@ function FlowEdge({
346
516
  sourcePosition,
347
517
  targetPosition
348
518
  });
349
- return /* @__PURE__ */ jsx4(
350
- BaseEdge,
519
+ return /* @__PURE__ */ jsx5(
520
+ FlowEdgePath,
351
521
  {
352
522
  id,
353
523
  path: edgePath,
354
524
  markerEnd,
355
- style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
525
+ "data-slot": "flow-edge",
526
+ stroke: "var(--flow-edge)",
527
+ strokeWidth: 1.5,
528
+ style
356
529
  }
357
530
  );
358
531
  }
359
532
 
360
533
  // src/flow-smart-edge/flow-smart-edge.tsx
361
534
  import {
362
- BaseEdge as BaseEdge2,
363
535
  getBezierPath as getBezierPath2,
364
536
  useInternalNode
365
537
  } from "@xyflow/react";
@@ -373,23 +545,28 @@ var sideToPosition = {
373
545
  bottom: Position2.Bottom,
374
546
  left: Position2.Left
375
547
  };
376
- function rectCenter(rect) {
377
- return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
378
- }
379
- function slideAnchor(rect, side, toward, inset = 12) {
380
- const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
381
- const ix = Math.min(inset, rect.width / 2);
382
- const iy = Math.min(inset, rect.height / 2);
383
- switch (side) {
384
- case "top":
385
- return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y };
386
- case "bottom":
387
- return { x: clamp(toward.x, rect.x + ix, rect.x + rect.width - ix), y: rect.y + rect.height };
388
- case "left":
389
- return { x: rect.x, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };
390
- case "right":
391
- return { x: rect.x + rect.width, y: clamp(toward.y, rect.y + iy, rect.y + rect.height - iy) };
548
+ var positionToSide = {
549
+ [Position2.Top]: "top",
550
+ [Position2.Right]: "right",
551
+ [Position2.Bottom]: "bottom",
552
+ [Position2.Left]: "left"
553
+ };
554
+ function pickClosestAnchors(sources, targets) {
555
+ if (!sources.length || !targets.length) return void 0;
556
+ let best;
557
+ let bestDist = Infinity;
558
+ for (const source of sources) {
559
+ for (const target of targets) {
560
+ const dx = target.x - source.x;
561
+ const dy = target.y - source.y;
562
+ const dist = dx * dx + dy * dy;
563
+ if (dist < bestDist) {
564
+ bestDist = dist;
565
+ best = { source, target };
566
+ }
567
+ }
392
568
  }
569
+ return best;
393
570
  }
394
571
  function handlePoint(rect, side) {
395
572
  switch (side) {
@@ -425,7 +602,7 @@ function pickClosestHandles(source, sourceSides, target, targetSides) {
425
602
  }
426
603
 
427
604
  // src/flow-smart-edge/flow-smart-edge.tsx
428
- import { jsx as jsx5 } from "react/jsx-runtime";
605
+ import { jsx as jsx6 } from "react/jsx-runtime";
429
606
  function toRect(node) {
430
607
  return {
431
608
  x: node.internals.positionAbsolute.x,
@@ -434,43 +611,87 @@ function toRect(node) {
434
611
  height: node.measured.height ?? 0
435
612
  };
436
613
  }
437
- function FlowSmartEdge({ id, source, target, markerEnd, style }) {
614
+ function anchorsOf(node, kind, handleId) {
615
+ const bounds = node.internals.handleBounds?.[kind] ?? [];
616
+ const origin = node.internals.positionAbsolute;
617
+ return bounds.filter((handle) => handleId ? handle.id === handleId : true).map((handle) => ({
618
+ id: handle.id ?? null,
619
+ x: origin.x + handle.x + handle.width / 2,
620
+ y: origin.y + handle.y + handle.height / 2,
621
+ side: positionToSide[handle.position]
622
+ }));
623
+ }
624
+ function declaredSides(node, kind) {
625
+ const userNode = node.internals.userNode;
626
+ const data = userNode.data;
627
+ const configured = data?.handles?.[kind];
628
+ if (configured?.length) return [...configured];
629
+ const position = kind === "target" ? userNode.targetPosition : userNode.sourcePosition;
630
+ if (position) return [positionToSide[position]];
631
+ return [kind === "target" ? "top" : "bottom"];
632
+ }
633
+ function FlowSmartEdge({
634
+ id,
635
+ source,
636
+ target,
637
+ sourceHandleId,
638
+ targetHandleId,
639
+ markerEnd,
640
+ style
641
+ }) {
438
642
  const sourceNode = useInternalNode(source);
439
643
  const targetNode = useInternalNode(target);
440
644
  if (!sourceNode || !targetNode) return null;
441
- const sourceData = sourceNode.internals.userNode.data;
442
- const targetData = targetNode.internals.userNode.data;
443
- const sourceRect = toRect(sourceNode);
444
- const targetRect = toRect(targetNode);
445
- const { sourceSide, targetSide } = pickClosestHandles(
446
- sourceRect,
447
- sourceData?.handles?.source ?? [],
448
- targetRect,
449
- targetData?.handles?.target ?? []
645
+ const picked = pickClosestAnchors(
646
+ anchorsOf(sourceNode, "source", sourceHandleId),
647
+ anchorsOf(targetNode, "target", targetHandleId)
450
648
  );
451
- const sourceAnchor = slideAnchor(sourceRect, sourceSide, rectCenter(targetRect));
452
- const targetAnchor = slideAnchor(targetRect, targetSide, rectCenter(sourceRect));
649
+ let sx;
650
+ let sy;
651
+ let tx;
652
+ let ty;
653
+ let sourcePosition;
654
+ let targetPosition;
655
+ if (picked) {
656
+ ({ x: sx, y: sy } = picked.source);
657
+ ({ x: tx, y: ty } = picked.target);
658
+ sourcePosition = sideToPosition[picked.source.side];
659
+ targetPosition = sideToPosition[picked.target.side];
660
+ } else {
661
+ const fallback = pickClosestHandles(
662
+ toRect(sourceNode),
663
+ declaredSides(sourceNode, "source"),
664
+ toRect(targetNode),
665
+ declaredSides(targetNode, "target")
666
+ );
667
+ ({ sx, sy, tx, ty } = fallback);
668
+ sourcePosition = sideToPosition[fallback.sourceSide];
669
+ targetPosition = sideToPosition[fallback.targetSide];
670
+ }
453
671
  const [edgePath] = getBezierPath2({
454
- sourceX: sourceAnchor.x,
455
- sourceY: sourceAnchor.y,
456
- sourcePosition: sideToPosition[sourceSide],
457
- targetX: targetAnchor.x,
458
- targetY: targetAnchor.y,
459
- targetPosition: sideToPosition[targetSide]
672
+ sourceX: sx,
673
+ sourceY: sy,
674
+ sourcePosition,
675
+ targetX: tx,
676
+ targetY: ty,
677
+ targetPosition
460
678
  });
461
- return /* @__PURE__ */ jsx5(
462
- BaseEdge2,
679
+ return /* @__PURE__ */ jsx6(
680
+ FlowEdgePath,
463
681
  {
464
682
  id,
465
683
  path: edgePath,
466
684
  markerEnd,
467
- style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
685
+ "data-slot": "flow-smart-edge",
686
+ stroke: "var(--flow-edge)",
687
+ strokeWidth: 1.5,
688
+ style
468
689
  }
469
690
  );
470
691
  }
471
692
 
472
693
  // src/flow-floating-edge/flow-floating-edge.tsx
473
- import { BaseEdge as BaseEdge3, getBezierPath as getBezierPath3, useInternalNode as useInternalNode2 } from "@xyflow/react";
694
+ import { getBezierPath as getBezierPath3, useInternalNode as useInternalNode2 } from "@xyflow/react";
474
695
 
475
696
  // src/flow-floating-edge/floating-edge-geometry.ts
476
697
  import { Position as Position3 } from "@xyflow/react";
@@ -539,7 +760,7 @@ function getEdgeParams(source, target) {
539
760
  }
540
761
 
541
762
  // src/flow-floating-edge/flow-floating-edge.tsx
542
- import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
763
+ import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
543
764
  var ANCHOR_RADIUS = 4;
544
765
  function FlowFloatingEdge({ id, source, target, markerEnd, style, data }) {
545
766
  const sourceNode = useInternalNode2(source);
@@ -555,24 +776,27 @@ function FlowFloatingEdge({ id, source, target, markerEnd, style, data }) {
555
776
  targetPosition: targetPos
556
777
  });
557
778
  const showAnchors = data?.anchors !== false;
558
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
559
- /* @__PURE__ */ jsx6(
560
- BaseEdge3,
779
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
780
+ /* @__PURE__ */ jsx7(
781
+ FlowEdgePath,
561
782
  {
562
783
  id,
563
784
  path: edgePath,
564
785
  markerEnd,
565
- style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
786
+ "data-slot": "flow-floating-edge",
787
+ stroke: "var(--flow-edge)",
788
+ strokeWidth: 1.5,
789
+ style
566
790
  }
567
791
  ),
568
- showAnchors ? /* @__PURE__ */ jsxs4(
792
+ showAnchors ? /* @__PURE__ */ jsxs5(
569
793
  "g",
570
794
  {
571
795
  className: "brand-floating-edge__anchors",
572
796
  fill: "var(--flow-node)",
573
797
  stroke: "var(--flow-edge)",
574
798
  children: [
575
- /* @__PURE__ */ jsx6(
799
+ /* @__PURE__ */ jsx7(
576
800
  "circle",
577
801
  {
578
802
  cx: sx,
@@ -583,7 +807,7 @@ function FlowFloatingEdge({ id, source, target, markerEnd, style, data }) {
583
807
  "aria-hidden": "true"
584
808
  }
585
809
  ),
586
- /* @__PURE__ */ jsx6(
810
+ /* @__PURE__ */ jsx7(
587
811
  "circle",
588
812
  {
589
813
  cx: tx,
@@ -603,33 +827,33 @@ function FlowFloatingEdge({ id, source, target, markerEnd, style, data }) {
603
827
  // src/flow-placeholder-node/flow-placeholder-node.tsx
604
828
  import { Handle as Handle2, Position as Position4 } from "@xyflow/react";
605
829
  import { Plus } from "lucide-react";
606
- import { cn as cn4 } from "@elabs-ai/components-ui/lib/cn";
607
- import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
830
+ import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
831
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
608
832
  function FlowPlaceholderNode({ data }) {
609
833
  const label = data.label ?? "Add node";
610
- return /* @__PURE__ */ jsxs5("div", { className: "relative", children: [
611
- /* @__PURE__ */ jsx7(
834
+ return /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
835
+ /* @__PURE__ */ jsx8(
612
836
  Handle2,
613
837
  {
614
838
  type: "target",
615
839
  position: Position4.Top,
616
- className: "!size-2 !border-2 !border-flow-edge !bg-flow-node"
840
+ className: `!size-2 !border-2 !border-flow-edge !bg-flow-node ${FLOW_HANDLE_ANCHOR_CLASS}`
617
841
  }
618
842
  ),
619
- /* @__PURE__ */ jsxs5(
843
+ /* @__PURE__ */ jsxs6(
620
844
  "button",
621
845
  {
622
846
  type: "button",
623
847
  "aria-label": label,
624
848
  onClick: () => data.onActivate?.(),
625
- className: cn4(
849
+ className: cn5(
626
850
  "flex min-w-44 items-center justify-center gap-1.5 rounded-lg border border-dashed border-flow-group-border bg-flow-group px-3 py-2 text-muted-foreground",
627
851
  "transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground",
628
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
852
+ "focus-ring"
629
853
  ),
630
854
  children: [
631
- /* @__PURE__ */ jsx7(Plus, { className: "size-4", "aria-hidden": "true" }),
632
- /* @__PURE__ */ jsx7("span", { className: "text-body", children: label })
855
+ /* @__PURE__ */ jsx8(Plus, { className: "size-4", "aria-hidden": "true" }),
856
+ /* @__PURE__ */ jsx8("span", { className: "text-body", children: label })
633
857
  ]
634
858
  }
635
859
  )
@@ -637,13 +861,9 @@ function FlowPlaceholderNode({ data }) {
637
861
  }
638
862
 
639
863
  // src/flow-button-edge/flow-button-edge.tsx
640
- import {
641
- BaseEdge as BaseEdge4,
642
- EdgeLabelRenderer,
643
- getBezierPath as getBezierPath4
644
- } from "@xyflow/react";
864
+ import { EdgeLabelRenderer, getBezierPath as getBezierPath4 } from "@xyflow/react";
645
865
  import { Plus as Plus2 } from "lucide-react";
646
- import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
866
+ import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
647
867
  function FlowButtonEdge({
648
868
  id,
649
869
  sourceX,
@@ -665,17 +885,20 @@ function FlowButtonEdge({
665
885
  targetPosition
666
886
  });
667
887
  const label = data?.label ?? "Insert node on edge";
668
- return /* @__PURE__ */ jsxs6(Fragment3, { children: [
669
- /* @__PURE__ */ jsx8(
670
- BaseEdge4,
888
+ return /* @__PURE__ */ jsxs7(Fragment4, { children: [
889
+ /* @__PURE__ */ jsx9(
890
+ FlowEdgePath,
671
891
  {
672
892
  id,
673
893
  path: edgePath,
674
894
  markerEnd,
675
- style: { stroke: "var(--flow-edge)", strokeWidth: 1.5, ...style }
895
+ "data-slot": "flow-button-edge",
896
+ stroke: "var(--flow-edge)",
897
+ strokeWidth: 1.5,
898
+ style
676
899
  }
677
900
  ),
678
- /* @__PURE__ */ jsx8(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx8(
901
+ /* @__PURE__ */ jsx9(EdgeLabelRenderer, { children: /* @__PURE__ */ jsx9(
679
902
  "div",
680
903
  {
681
904
  style: {
@@ -683,14 +906,14 @@ function FlowButtonEdge({
683
906
  transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`
684
907
  },
685
908
  className: "nodrag nopan pointer-events-auto",
686
- children: /* @__PURE__ */ jsx8(
909
+ children: /* @__PURE__ */ jsx9(
687
910
  "button",
688
911
  {
689
912
  type: "button",
690
913
  "aria-label": label,
691
914
  onClick: () => data?.onInsert?.(),
692
- className: "flex size-5 items-center justify-center rounded-full border border-flow-group-border bg-flow-node text-flow-node-foreground shadow-sm transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
693
- children: /* @__PURE__ */ jsx8(Plus2, { className: "size-3", "aria-hidden": "true" })
915
+ className: "flex size-5 items-center justify-center rounded-full border border-flow-group-border bg-flow-node text-flow-node-foreground shadow-sm transition-colors duration-fast ease-standard hover:bg-accent hover:text-accent-foreground focus-ring",
916
+ children: /* @__PURE__ */ jsx9(Plus2, { className: "size-3", "aria-hidden": "true" })
694
917
  }
695
918
  )
696
919
  }
@@ -724,10 +947,16 @@ function layoutFlow(nodes, edges, options = {}) {
724
947
  const { width, height } = nodeSize(node);
725
948
  graph.setNode(node.id, { width, height });
726
949
  }
950
+ const selfLoops = [];
727
951
  for (const edge of edges) {
952
+ if (edge.source === edge.target) {
953
+ selfLoops.push(edge.id);
954
+ continue;
955
+ }
728
956
  graph.setEdge(edge.source, edge.target);
729
957
  }
730
958
  dagre.layout(graph);
959
+ const backEdges = collectBackEdges(graph, edges);
731
960
  const handles = HANDLE_BY_DIRECTION[direction];
732
961
  const layoutedNodes = nodes.map((node) => {
733
962
  const dagreNode = graph.node(node.id);
@@ -744,18 +973,29 @@ function layoutFlow(nodes, edges, options = {}) {
744
973
  }
745
974
  };
746
975
  });
747
- return { nodes: layoutedNodes, edges };
976
+ return { nodes: layoutedNodes, edges, backEdges, selfLoops };
977
+ }
978
+ function collectBackEdges(graph, edges) {
979
+ const backEdges = [];
980
+ for (const edge of edges) {
981
+ if (edge.source === edge.target) continue;
982
+ const sourceRank = graph.node(edge.source)?.rank;
983
+ const targetRank = graph.node(edge.target)?.rank;
984
+ if (typeof sourceRank !== "number" || typeof targetRank !== "number") continue;
985
+ if (sourceRank >= targetRank) backEdges.push(edge.id);
986
+ }
987
+ return backEdges;
748
988
  }
749
989
 
750
990
  // src/flow-layout/use-flow-layout.ts
751
- import { useCallback as useCallback2, useRef, useState as useState2 } from "react";
752
- import { useNodesInitialized, useReactFlow as useReactFlow2 } from "@xyflow/react";
991
+ import { useCallback as useCallback3, useRef as useRef2, useState as useState3 } from "react";
992
+ import { useNodesInitialized as useNodesInitialized2, useReactFlow as useReactFlow3 } from "@xyflow/react";
753
993
  function useFlowLayout() {
754
- const { getNodes, getEdges, setNodes, fitView } = useReactFlow2();
755
- const nodesInitialized = useNodesInitialized();
756
- const [layouting, setLayouting] = useState2(false);
757
- const runningRef = useRef(false);
758
- const layout = useCallback2(
994
+ const { getNodes, getEdges, setNodes, fitView } = useReactFlow3();
995
+ const nodesInitialized = useNodesInitialized2();
996
+ const [layouting, setLayouting] = useState3(false);
997
+ const runningRef = useRef2(false);
998
+ const layout = useCallback3(
759
999
  (direction, options) => {
760
1000
  if (!nodesInitialized || runningRef.current) return;
761
1001
  runningRef.current = true;
@@ -803,7 +1043,9 @@ function layoutGraph(nodes, edges, options) {
803
1043
  if (nodes.length === 0) return [];
804
1044
  switch (options.algorithm) {
805
1045
  case "layered-lr":
806
- return layoutLayeredLr(nodes, edges, options);
1046
+ return layoutLayered(nodes, edges, options, "LR");
1047
+ case "layered-tb":
1048
+ return layoutLayered(nodes, edges, options, "TB");
807
1049
  case "concentric":
808
1050
  return layoutConcentric(nodes, edges, options);
809
1051
  case "force":
@@ -816,18 +1058,17 @@ function layoutGraph(nodes, edges, options) {
816
1058
  }
817
1059
  }
818
1060
  }
819
- function layoutLayeredLr(nodes, edges, options) {
1061
+ function layoutLayered(nodes, edges, options, direction) {
820
1062
  const { spacing, nodeSize: nodeSize2 } = options;
821
1063
  const sizedNodes = nodeSize2 ? nodes.map((node) => {
822
1064
  const { width, height } = nodeSize2(node.id);
823
1065
  return { ...node, width, height, measured: { width, height } };
824
1066
  }) : nodes;
1067
+ const horizontalRanks = direction === "LR" || direction === "RL";
825
1068
  const { nodes: laidOut } = layoutFlow(sizedNodes, edges, {
826
- direction: "LR",
827
- // For an LR flow, ranks progress horizontally (x) and nodes within a rank
828
- // stack vertically (y) map spacing accordingly onto dagre's rank/node sep.
829
- nodeSpacing: spacing?.y,
830
- rankSpacing: spacing?.x
1069
+ direction,
1070
+ nodeSpacing: horizontalRanks ? spacing?.y : spacing?.x,
1071
+ rankSpacing: horizontalRanks ? spacing?.x : spacing?.y
831
1072
  });
832
1073
  return nodes.map((node, i) => ({
833
1074
  ...node,
@@ -952,9 +1193,9 @@ function layoutGrid(nodes, options) {
952
1193
  }
953
1194
 
954
1195
  // src/flow-layout/use-auto-layout.ts
955
- import { useMemo } from "react";
1196
+ import { useMemo as useMemo2 } from "react";
956
1197
  function useAutoLayout(nodes, edges, options) {
957
- return useMemo(
1198
+ return useMemo2(
958
1199
  () => layoutGraph(nodes, edges, options),
959
1200
  [nodes, edges, options]
960
1201
  );
@@ -962,8 +1203,8 @@ function useAutoLayout(nodes, edges, options) {
962
1203
 
963
1204
  // src/flow-mini-map/flow-mini-map.tsx
964
1205
  import { MiniMap } from "@xyflow/react";
965
- import { cn as cn5 } from "@elabs-ai/components-ui/lib/cn";
966
- import { jsx as jsx9 } from "react/jsx-runtime";
1206
+ import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
1207
+ import { jsx as jsx10 } from "react/jsx-runtime";
967
1208
  function FlowMiniMap({
968
1209
  className,
969
1210
  nodeColor = "var(--flow-minimap-node)",
@@ -971,10 +1212,10 @@ function FlowMiniMap({
971
1212
  maskColor = "var(--flow-minimap-mask)",
972
1213
  ...props
973
1214
  }) {
974
- return /* @__PURE__ */ jsx9(
1215
+ return /* @__PURE__ */ jsx10(
975
1216
  MiniMap,
976
1217
  {
977
- className: cn5("!rounded-lg !bg-surface-elevated !shadow-ring-sm", className),
1218
+ className: cn6("!rounded-lg !bg-surface-elevated !shadow-ring-sm", className),
978
1219
  nodeColor,
979
1220
  nodeStrokeColor,
980
1221
  maskColor,
@@ -986,8 +1227,8 @@ function FlowMiniMap({
986
1227
  // src/inspector-panel/inspector-panel.tsx
987
1228
  import "react";
988
1229
  import { Reveal, useCollapsiblePanel } from "@elabs-ai/components-ui";
989
- import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
990
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1230
+ import { cn as cn7 } from "@elabs-ai/components-ui/lib/cn";
1231
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
991
1232
  var INSPECTOR_PANEL_WIDTH = "18rem";
992
1233
  function InspectorPanel({
993
1234
  title = "Inspector",
@@ -1015,31 +1256,31 @@ function InspectorPanel({
1015
1256
  right: "right-0 group-data-[state=collapsed]:right-[calc(var(--inspector-panel-width)*-1)]"
1016
1257
  }
1017
1258
  });
1018
- return /* @__PURE__ */ jsxs7(
1259
+ return /* @__PURE__ */ jsxs8(
1019
1260
  "div",
1020
1261
  {
1021
- className: cn6("group relative h-full overflow-hidden", className),
1262
+ className: cn7("group relative h-full overflow-hidden", className),
1022
1263
  style: { "--inspector-panel-width": width },
1023
1264
  inert: panel.open ? void 0 : true,
1024
1265
  ...panel.attrs,
1025
1266
  children: [
1026
- /* @__PURE__ */ jsx10("div", { "data-slot": "inspector-panel-gap", className: panel.spacerClassName }),
1027
- /* @__PURE__ */ jsx10(
1267
+ /* @__PURE__ */ jsx11("div", { "data-slot": "inspector-panel-gap", className: panel.spacerClassName }),
1268
+ /* @__PURE__ */ jsx11(
1028
1269
  "div",
1029
1270
  {
1030
1271
  "data-slot": "inspector-panel-container",
1031
- className: cn6(panel.containerClassName, "absolute inset-y-0 flex h-full border-s"),
1032
- children: /* @__PURE__ */ jsxs7("div", { className: "flex h-full w-full flex-col bg-card", children: [
1033
- /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
1034
- /* @__PURE__ */ jsx10("h3", { className: "text-body font-semibold text-foreground", children: title }),
1035
- onClose ? /* @__PURE__ */ jsx10(
1272
+ className: cn7(panel.containerClassName, "absolute inset-y-0 flex h-full border-s"),
1273
+ children: /* @__PURE__ */ jsxs8("div", { className: "flex h-full w-full flex-col bg-card", children: [
1274
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
1275
+ /* @__PURE__ */ jsx11("h3", { className: "text-body font-semibold text-foreground", children: title }),
1276
+ onClose ? /* @__PURE__ */ jsx11(
1036
1277
  "button",
1037
1278
  {
1038
1279
  type: "button",
1039
1280
  "aria-label": "Close inspector",
1040
1281
  onClick: onClose,
1041
- className: "rounded-sm p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1042
- children: /* @__PURE__ */ jsx10(
1282
+ className: "rounded-sm p-1 text-muted-foreground hover:text-foreground focus-ring",
1283
+ children: /* @__PURE__ */ jsx11(
1043
1284
  "svg",
1044
1285
  {
1045
1286
  width: "14",
@@ -1051,22 +1292,23 @@ function InspectorPanel({
1051
1292
  strokeLinecap: "round",
1052
1293
  strokeLinejoin: "round",
1053
1294
  "aria-hidden": "true",
1054
- children: /* @__PURE__ */ jsx10("path", { d: "M18 6 6 18M6 6l12 12" })
1295
+ children: /* @__PURE__ */ jsx11("path", { d: "M18 6 6 18M6 6l12 12" })
1055
1296
  }
1056
1297
  )
1057
1298
  }
1058
1299
  ) : null
1059
1300
  ] }),
1060
- hasSelection ? /* @__PURE__ */ jsx10(
1301
+ hasSelection ? /* @__PURE__ */ jsx11(
1061
1302
  Reveal,
1062
1303
  {
1063
1304
  appear: "fade",
1064
1305
  speed: "fast",
1065
- className: "flex-1 overflow-y-auto p-4 text-body",
1306
+ tabIndex: 0,
1307
+ className: "focus-ring flex-1 overflow-y-auto p-4 text-body",
1066
1308
  children
1067
1309
  },
1068
1310
  selectionKey
1069
- ) : /* @__PURE__ */ jsx10("div", { className: "flex-1 overflow-y-auto p-4 text-body", children: /* @__PURE__ */ jsx10("p", { className: "text-muted-foreground", children: emptyMessage }) })
1311
+ ) : /* @__PURE__ */ jsx11("div", { tabIndex: 0, className: "focus-ring flex-1 overflow-y-auto p-4 text-body", children: /* @__PURE__ */ jsx11("p", { className: "text-muted-foreground", children: emptyMessage }) })
1070
1312
  ] })
1071
1313
  }
1072
1314
  )
@@ -1076,20 +1318,64 @@ function InspectorPanel({
1076
1318
  }
1077
1319
 
1078
1320
  // src/legend/legend.tsx
1079
- import { cn as cn7 } from "@elabs-ai/components-ui/lib/cn";
1080
- import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1081
- function Legend({ items, title, className }) {
1082
- return /* @__PURE__ */ jsxs8(
1321
+ import { cn as cn8 } from "@elabs-ai/components-ui/lib/cn";
1322
+
1323
+ // src/flow-weighted-edge/weight-scale.ts
1324
+ var DEFAULT_EDGE_WIDTH_RANGE = [1.5, 8];
1325
+ var DEFAULT_SCALE_GROUP = "__default__";
1326
+ function computeEdgeWeightScale(edges, opts = {}) {
1327
+ const [minWidth, maxWidth] = opts.widthRange ?? DEFAULT_EDGE_WIDTH_RANGE;
1328
+ const result = /* @__PURE__ */ new Map();
1329
+ const groups = /* @__PURE__ */ new Map();
1330
+ for (const edge of edges) {
1331
+ const weight = edge.data?.weight;
1332
+ if (weight === void 0) {
1333
+ result.set(edge.id, minWidth);
1334
+ continue;
1335
+ }
1336
+ const group = edge.data?.scaleGroup ?? DEFAULT_SCALE_GROUP;
1337
+ if (opts.scaleGroup !== void 0 && group !== opts.scaleGroup) continue;
1338
+ const list = groups.get(group);
1339
+ if (list) list.push({ id: edge.id, weight });
1340
+ else groups.set(group, [{ id: edge.id, weight }]);
1341
+ }
1342
+ for (const list of groups.values()) {
1343
+ let min = Infinity;
1344
+ let max = -Infinity;
1345
+ for (const { weight } of list) {
1346
+ if (weight < min) min = weight;
1347
+ if (weight > max) max = weight;
1348
+ }
1349
+ const span = max - min;
1350
+ for (const { id, weight } of list) {
1351
+ const width = span === 0 ? (minWidth + maxWidth) / 2 : minWidth + (weight - min) / span * (maxWidth - minWidth);
1352
+ result.set(id, width);
1353
+ }
1354
+ }
1355
+ return result;
1356
+ }
1357
+
1358
+ // src/legend/legend.tsx
1359
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1360
+ var defaultFormat = (value) => value.toLocaleString();
1361
+ function Legend(props) {
1362
+ if (props.variant === "scale") {
1363
+ return /* @__PURE__ */ jsx12(LegendScale, { ...props });
1364
+ }
1365
+ return /* @__PURE__ */ jsx12(LegendCategorical, { ...props });
1366
+ }
1367
+ function LegendCategorical({ items, title, className }) {
1368
+ return /* @__PURE__ */ jsxs9(
1083
1369
  "div",
1084
1370
  {
1085
- className: cn7(
1371
+ className: cn8(
1086
1372
  "rounded-lg bg-surface-elevated/90 p-3 text-xs shadow-ring-sm backdrop-blur",
1087
1373
  className
1088
1374
  ),
1089
1375
  children: [
1090
- title ? /* @__PURE__ */ jsx11("div", { className: "mb-1.5 font-medium text-foreground", children: title }) : null,
1091
- /* @__PURE__ */ jsx11("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxs8("li", { className: "flex items-center gap-2 text-muted-foreground", children: [
1092
- /* @__PURE__ */ jsx11(
1376
+ title ? /* @__PURE__ */ jsx12("div", { className: "mb-1.5 font-medium text-foreground", children: title }) : null,
1377
+ /* @__PURE__ */ jsx12("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxs9("li", { className: "flex items-center gap-2 text-muted-foreground", children: [
1378
+ /* @__PURE__ */ jsx12(
1093
1379
  "span",
1094
1380
  {
1095
1381
  "aria-hidden": "true",
@@ -1103,38 +1389,143 @@ function Legend({ items, title, className }) {
1103
1389
  }
1104
1390
  );
1105
1391
  }
1392
+ function computeWidthSamples(domain, ticks) {
1393
+ const [min, max] = domain;
1394
+ const values = ticks === "minmedmax" ? [min, (min + max) / 2, max] : [min, max];
1395
+ const likeEdges = values.map((weight, index) => ({
1396
+ id: String(index),
1397
+ data: { weight }
1398
+ }));
1399
+ const widths = computeEdgeWeightScale(likeEdges);
1400
+ return values.map((value, index) => ({ value, width: widths.get(String(index)) ?? 0 }));
1401
+ }
1402
+ var COLOR_STOP_FRACTIONS = [0, 0.25, 0.5, 0.75, 1];
1403
+ function scaleKindLabel(kind) {
1404
+ return kind === "width" ? "width" : "color";
1405
+ }
1406
+ function LegendScale({
1407
+ kind,
1408
+ domain,
1409
+ format = defaultFormat,
1410
+ ticks = "minmax",
1411
+ title,
1412
+ className
1413
+ }) {
1414
+ const [min, max] = domain;
1415
+ const ariaLabel = `Edge ${scaleKindLabel(kind)} scale, ${format(min)} to ${format(max)}, minimum to maximum`;
1416
+ return /* @__PURE__ */ jsxs9(
1417
+ "div",
1418
+ {
1419
+ "aria-label": ariaLabel,
1420
+ className: cn8(
1421
+ "flex flex-col gap-2 rounded-lg bg-surface-elevated/90 p-3 shadow-ring-sm backdrop-blur",
1422
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1423
+ className
1424
+ ),
1425
+ role: "group",
1426
+ tabIndex: 0,
1427
+ children: [
1428
+ title ? /* @__PURE__ */ jsx12("div", { className: "text-body font-medium text-foreground", children: title }) : null,
1429
+ kind === "width" ? /* @__PURE__ */ jsx12(LegendScaleWidth, { domain, format, ticks }) : /* @__PURE__ */ jsx12(LegendScaleColor, { domain, format })
1430
+ ]
1431
+ }
1432
+ );
1433
+ }
1434
+ function LegendScaleWidth({
1435
+ domain,
1436
+ format,
1437
+ ticks
1438
+ }) {
1439
+ const samples = computeWidthSamples(domain, ticks);
1440
+ return /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-2", children: samples.map((sample, index) => (
1441
+ // Index disambiguates a zero-width domain, where every sample shares
1442
+ // the same value (and therefore the same width).
1443
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
1444
+ /* @__PURE__ */ jsx12("svg", { "aria-hidden": "true", className: "shrink-0", height: 16, overflow: "visible", width: 40, children: /* @__PURE__ */ jsx12(
1445
+ "line",
1446
+ {
1447
+ stroke: "var(--flow-edge)",
1448
+ strokeLinecap: "round",
1449
+ strokeWidth: sample.width,
1450
+ x1: 2,
1451
+ x2: 38,
1452
+ y1: 8,
1453
+ y2: 8
1454
+ }
1455
+ ) }),
1456
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate text-meta tabular-nums text-muted-foreground", children: format(sample.value) })
1457
+ ] }, `${index}-${sample.value}`)
1458
+ )) });
1459
+ }
1460
+ function LegendScaleColor({
1461
+ domain,
1462
+ format
1463
+ }) {
1464
+ const [min, max] = domain;
1465
+ const span = max - min;
1466
+ const stops = COLOR_STOP_FRACTIONS.map((fraction) => ({
1467
+ fraction,
1468
+ value: min + fraction * span
1469
+ }));
1470
+ const gradient = `linear-gradient(to right, ${stops.map(({ fraction }) => {
1471
+ const percent = Math.round(fraction * 100);
1472
+ return `color-mix(in oklch, var(--flow-edge-strong) ${percent}%, var(--flow-edge-weak)) ${percent}%`;
1473
+ }).join(", ")})`;
1474
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1.5", children: [
1475
+ /* @__PURE__ */ jsx12(
1476
+ "div",
1477
+ {
1478
+ "aria-hidden": "true",
1479
+ className: "h-3 w-full rounded-full",
1480
+ style: { backgroundImage: gradient }
1481
+ }
1482
+ ),
1483
+ /* @__PURE__ */ jsx12("div", { className: "flex justify-between gap-1", children: stops.map((stop, index) => /* @__PURE__ */ jsx12(
1484
+ "span",
1485
+ {
1486
+ className: cn8(
1487
+ "min-w-0 truncate text-meta tabular-nums text-muted-foreground",
1488
+ index === 0 && "text-start",
1489
+ index === stops.length - 1 && "text-end"
1490
+ ),
1491
+ children: format(stop.value)
1492
+ },
1493
+ `${index}-${stop.value}`
1494
+ )) })
1495
+ ] });
1496
+ }
1106
1497
 
1107
1498
  // src/zoom-controls/zoom-controls.tsx
1108
- import { Panel, useReactFlow as useReactFlow3 } from "@xyflow/react";
1109
- import { cn as cn8 } from "@elabs-ai/components-ui/lib/cn";
1110
- import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1499
+ import { Panel, useReactFlow as useReactFlow4 } from "@xyflow/react";
1500
+ import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
1501
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1111
1502
  function ControlButton({
1112
1503
  label,
1113
1504
  onClick,
1114
1505
  children
1115
1506
  }) {
1116
- return /* @__PURE__ */ jsx12(
1507
+ return /* @__PURE__ */ jsx13(
1117
1508
  "button",
1118
1509
  {
1119
1510
  type: "button",
1120
1511
  "aria-label": label,
1121
1512
  onClick,
1122
- className: "flex size-8 items-center justify-center text-foreground transition-colors duration-fast hover:bg-surface-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring [&_svg]:size-4",
1513
+ className: "flex size-8 items-center justify-center text-foreground transition-colors duration-fast hover:bg-surface-muted focus-ring [&_svg]:size-4",
1123
1514
  children
1124
1515
  }
1125
1516
  );
1126
1517
  }
1127
1518
  function ZoomControls({ position = "bottom-right", className }) {
1128
- const { zoomIn, zoomOut, fitView } = useReactFlow3();
1129
- return /* @__PURE__ */ jsx12(Panel, { position, children: /* @__PURE__ */ jsxs9(
1519
+ const { zoomIn, zoomOut, fitView } = useReactFlow4();
1520
+ return /* @__PURE__ */ jsx13(Panel, { position, children: /* @__PURE__ */ jsxs10(
1130
1521
  "div",
1131
1522
  {
1132
- className: cn8(
1523
+ className: cn9(
1133
1524
  "flex divide-x overflow-hidden rounded-lg bg-surface-elevated shadow-ring-sm",
1134
1525
  className
1135
1526
  ),
1136
1527
  children: [
1137
- /* @__PURE__ */ jsx12(ControlButton, { label: "Zoom in", onClick: () => zoomIn(), children: /* @__PURE__ */ jsx12(
1528
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Zoom in", onClick: () => zoomIn(), children: /* @__PURE__ */ jsx13(
1138
1529
  "svg",
1139
1530
  {
1140
1531
  viewBox: "0 0 24 24",
@@ -1142,10 +1533,10 @@ function ZoomControls({ position = "bottom-right", className }) {
1142
1533
  stroke: "currentColor",
1143
1534
  strokeWidth: "2",
1144
1535
  strokeLinecap: "round",
1145
- children: /* @__PURE__ */ jsx12("path", { d: "M12 5v14M5 12h14" })
1536
+ children: /* @__PURE__ */ jsx13("path", { d: "M12 5v14M5 12h14" })
1146
1537
  }
1147
1538
  ) }),
1148
- /* @__PURE__ */ jsx12(ControlButton, { label: "Zoom out", onClick: () => zoomOut(), children: /* @__PURE__ */ jsx12(
1539
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Zoom out", onClick: () => zoomOut(), children: /* @__PURE__ */ jsx13(
1149
1540
  "svg",
1150
1541
  {
1151
1542
  viewBox: "0 0 24 24",
@@ -1153,10 +1544,10 @@ function ZoomControls({ position = "bottom-right", className }) {
1153
1544
  stroke: "currentColor",
1154
1545
  strokeWidth: "2",
1155
1546
  strokeLinecap: "round",
1156
- children: /* @__PURE__ */ jsx12("path", { d: "M5 12h14" })
1547
+ children: /* @__PURE__ */ jsx13("path", { d: "M5 12h14" })
1157
1548
  }
1158
1549
  ) }),
1159
- /* @__PURE__ */ jsx12(ControlButton, { label: "Fit view", onClick: () => fitView(), children: /* @__PURE__ */ jsx12(
1550
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Fit view", onClick: () => fitView(), children: /* @__PURE__ */ jsx13(
1160
1551
  "svg",
1161
1552
  {
1162
1553
  viewBox: "0 0 24 24",
@@ -1165,7 +1556,7 @@ function ZoomControls({ position = "bottom-right", className }) {
1165
1556
  strokeWidth: "2",
1166
1557
  strokeLinecap: "round",
1167
1558
  strokeLinejoin: "round",
1168
- children: /* @__PURE__ */ jsx12("path", { d: "M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M16 21h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" })
1559
+ children: /* @__PURE__ */ jsx13("path", { d: "M8 3H5a2 2 0 0 0-2 2v3M21 8V5a2 2 0 0 0-2-2h-3M16 21h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" })
1169
1560
  }
1170
1561
  ) })
1171
1562
  ]
@@ -1177,11 +1568,11 @@ function ZoomControls({ position = "bottom-right", className }) {
1177
1568
  import "react";
1178
1569
  import { Handle as Handle3, NodeResizer, Position as Position6, useNodes } from "@xyflow/react";
1179
1570
  import { ChevronDown, ChevronRight } from "lucide-react";
1180
- import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
1571
+ import { cn as cn10 } from "@elabs-ai/components-ui/lib/cn";
1181
1572
 
1182
1573
  // src/use-flow-groups/use-flow-groups.ts
1183
- import { useCallback as useCallback3 } from "react";
1184
- import { useReactFlow as useReactFlow4 } from "@xyflow/react";
1574
+ import { useCallback as useCallback4 } from "react";
1575
+ import { useReactFlow as useReactFlow5 } from "@xyflow/react";
1185
1576
 
1186
1577
  // src/use-flow-groups/group-operations.ts
1187
1578
  import { getNodesBounds } from "@xyflow/react";
@@ -1375,15 +1766,15 @@ function generateGroupId() {
1375
1766
  return `flow-group-${Math.random().toString(36).slice(2, 9)}`;
1376
1767
  }
1377
1768
  function useFlowGroups() {
1378
- const { getNodes, getEdges, setNodes, setEdges } = useReactFlow4();
1379
- const apply = useCallback3(
1769
+ const { getNodes, getEdges, setNodes, setEdges } = useReactFlow5();
1770
+ const apply = useCallback4(
1380
1771
  (result) => {
1381
1772
  setNodes(result.nodes);
1382
1773
  setEdges(result.edges);
1383
1774
  },
1384
1775
  [setNodes, setEdges]
1385
1776
  );
1386
- const groupNodes2 = useCallback3(
1777
+ const groupNodes2 = useCallback4(
1387
1778
  (nodeIds, options) => {
1388
1779
  if (nodeIds.length === 0) return void 0;
1389
1780
  const groupId = options?.groupId ?? generateGroupId();
@@ -1396,26 +1787,26 @@ function useFlowGroups() {
1396
1787
  },
1397
1788
  [getNodes, getEdges, apply]
1398
1789
  );
1399
- const groupSelection = useCallback3(
1790
+ const groupSelection = useCallback4(
1400
1791
  (options) => {
1401
1792
  const selectedIds = getNodes().filter((n) => n.selected && !n.parentId).map((n) => n.id);
1402
1793
  return groupNodes2(selectedIds, options);
1403
1794
  },
1404
1795
  [getNodes, groupNodes2]
1405
1796
  );
1406
- const ungroup2 = useCallback3(
1797
+ const ungroup2 = useCallback4(
1407
1798
  (groupId) => apply(ungroup(getNodes(), getEdges(), groupId)),
1408
1799
  [getNodes, getEdges, apply]
1409
1800
  );
1410
- const collapseGroup2 = useCallback3(
1801
+ const collapseGroup2 = useCallback4(
1411
1802
  (groupId) => apply(collapseGroup(getNodes(), getEdges(), groupId)),
1412
1803
  [getNodes, getEdges, apply]
1413
1804
  );
1414
- const expandGroup2 = useCallback3(
1805
+ const expandGroup2 = useCallback4(
1415
1806
  (groupId) => apply(expandGroup(getNodes(), getEdges(), groupId)),
1416
1807
  [getNodes, getEdges, apply]
1417
1808
  );
1418
- const toggleCollapse = useCallback3(
1809
+ const toggleCollapse = useCallback4(
1419
1810
  (groupId) => apply(toggleGroupCollapsed(getNodes(), getEdges(), groupId)),
1420
1811
  [getNodes, getEdges, apply]
1421
1812
  );
@@ -1430,18 +1821,22 @@ function useFlowGroups() {
1430
1821
  }
1431
1822
 
1432
1823
  // src/flow-group-node/flow-group-node.tsx
1433
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1434
- var toneAccent = {
1824
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1825
+ var toneMark = {
1435
1826
  default: "text-muted-foreground",
1436
- // #399 — the group LABEL is text on the canvas, so it takes the on-surface
1437
- // `-text` rung. (The status rows below stay on their fill rungs; those are a
1438
- // separate, still-open question, not part of #399's enumerated call sites.)
1439
1827
  accent: "text-primary-text",
1440
1828
  success: "text-success",
1441
1829
  warning: "text-warning",
1442
1830
  destructive: "text-destructive"
1443
1831
  };
1444
- var handleClassName2 = "!size-2 !border-2 !border-flow-group-border !bg-flow-group";
1832
+ var toneInk = {
1833
+ default: "text-muted-foreground",
1834
+ accent: "text-primary-text",
1835
+ success: "text-success-text",
1836
+ warning: "text-warning-text",
1837
+ destructive: "text-destructive-text"
1838
+ };
1839
+ var handleClassName2 = `!size-2 !border-2 !border-flow-group-border !bg-flow-group ${FLOW_HANDLE_ANCHOR_CLASS}`;
1445
1840
  function FlowGroupNode({ id, data, selected }) {
1446
1841
  const { toggleCollapse } = useFlowGroups();
1447
1842
  const nodes = useNodes();
@@ -1449,17 +1844,17 @@ function FlowGroupNode({ id, data, selected }) {
1449
1844
  const tone = data.tone ?? "default";
1450
1845
  const liveChildCount = nodes.filter((n) => n.parentId === id).length;
1451
1846
  const childCount = liveChildCount || data.childCount || 0;
1452
- return /* @__PURE__ */ jsxs10(
1847
+ return /* @__PURE__ */ jsxs11(
1453
1848
  "div",
1454
1849
  {
1455
- className: cn9(
1850
+ className: cn10(
1456
1851
  "flex h-full w-full flex-col rounded-lg border bg-flow-group/60 text-foreground",
1457
1852
  "border-flow-group-border transition-[box-shadow,border-color] duration-fast ease-standard motion-reduce:transition-none",
1458
1853
  collapsed ? "shadow-sm" : "shadow-none",
1459
1854
  selected && "ring-2 ring-ring"
1460
1855
  ),
1461
1856
  children: [
1462
- !collapsed && selected ? /* @__PURE__ */ jsx13(
1857
+ !collapsed && selected ? /* @__PURE__ */ jsx14(
1463
1858
  NodeResizer,
1464
1859
  {
1465
1860
  minWidth: 160,
@@ -1468,39 +1863,39 @@ function FlowGroupNode({ id, data, selected }) {
1468
1863
  handleClassName: handleClassName2
1469
1864
  }
1470
1865
  ) : null,
1471
- /* @__PURE__ */ jsx13(Handle3, { type: "target", position: Position6.Top, className: handleClassName2 }),
1472
- /* @__PURE__ */ jsx13(Handle3, { type: "source", position: Position6.Bottom, className: handleClassName2 }),
1473
- /* @__PURE__ */ jsxs10(
1866
+ /* @__PURE__ */ jsx14(Handle3, { type: "target", position: Position6.Top, className: handleClassName2 }),
1867
+ /* @__PURE__ */ jsx14(Handle3, { type: "source", position: Position6.Bottom, className: handleClassName2 }),
1868
+ /* @__PURE__ */ jsxs11(
1474
1869
  "div",
1475
1870
  {
1476
- className: cn9(
1871
+ className: cn10(
1477
1872
  "flex items-center gap-2 px-3 py-2",
1478
1873
  !collapsed && "border-b border-flow-group-border"
1479
1874
  ),
1480
1875
  children: [
1481
- /* @__PURE__ */ jsx13(
1876
+ /* @__PURE__ */ jsx14(
1482
1877
  "button",
1483
1878
  {
1484
1879
  type: "button",
1485
1880
  onClick: () => toggleCollapse(id),
1486
1881
  "aria-label": collapsed ? `Expand group ${data.title}` : `Collapse group ${data.title}`,
1487
1882
  "aria-expanded": !collapsed,
1488
- className: cn9(
1883
+ className: cn10(
1489
1884
  "-ml-1 grid size-5 shrink-0 place-items-center rounded",
1490
1885
  "text-muted-foreground hover:bg-muted hover:text-foreground",
1491
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1886
+ "focus-ring"
1492
1887
  ),
1493
- children: collapsed ? /* @__PURE__ */ jsx13(ChevronRight, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx13(ChevronDown, { "aria-hidden": "true", className: "size-4" })
1888
+ children: collapsed ? /* @__PURE__ */ jsx14(ChevronRight, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx14(ChevronDown, { "aria-hidden": "true", className: "size-4" })
1494
1889
  }
1495
1890
  ),
1496
- data.icon ? /* @__PURE__ */ jsx13("span", { className: cn9("[&_svg]:size-4", toneAccent[tone]), children: data.icon }) : null,
1497
- /* @__PURE__ */ jsx13("span", { className: "min-w-0 flex-1 truncate text-body font-medium", children: data.title }),
1498
- /* @__PURE__ */ jsx13(
1891
+ data.icon ? /* @__PURE__ */ jsx14("span", { className: cn10("[&_svg]:size-4", toneMark[tone]), children: data.icon }) : null,
1892
+ /* @__PURE__ */ jsx14("span", { className: "min-w-0 flex-1 truncate text-body font-medium", children: data.title }),
1893
+ /* @__PURE__ */ jsx14(
1499
1894
  "span",
1500
1895
  {
1501
- className: cn9(
1896
+ className: cn10(
1502
1897
  "shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-meta font-medium tabular-nums",
1503
- toneAccent[tone]
1898
+ toneInk[tone]
1504
1899
  ),
1505
1900
  "aria-label": `${childCount} ${childCount === 1 ? "node" : "nodes"}`,
1506
1901
  children: childCount
@@ -1520,40 +1915,438 @@ import {
1520
1915
  Controls,
1521
1916
  MiniMap as MiniMap2,
1522
1917
  Panel as Panel2,
1523
- Position as Position7,
1918
+ Position as Position8,
1524
1919
  ReactFlow as ReactFlow2,
1525
1920
  ReactFlowProvider as ReactFlowProvider2,
1526
- useReactFlow as useReactFlow5,
1921
+ useReactFlow as useReactFlow6,
1527
1922
  useNodesState,
1528
1923
  useEdgesState,
1529
1924
  addEdge
1530
1925
  } from "@xyflow/react";
1926
+
1927
+ // src/flow-weighted-edge/flow-weighted-edge.tsx
1928
+ import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
1929
+ import {
1930
+ Position as Position7,
1931
+ getBezierPath as getBezierPath5,
1932
+ getSmoothStepPath,
1933
+ useEdges,
1934
+ useStore
1935
+ } from "@xyflow/react";
1936
+ import { resolveTokenColor } from "@elabs-ai/components-tokens";
1937
+
1938
+ // src/flow-weighted-edge/edge-label-pill.tsx
1939
+ import { EdgeLabelRenderer as EdgeLabelRenderer2 } from "@xyflow/react";
1940
+ import { cn as cn11 } from "@elabs-ai/components-ui/lib/cn";
1941
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1942
+ function EdgeLabelPill({
1943
+ label,
1944
+ secondaryLabel,
1945
+ x,
1946
+ y,
1947
+ selected,
1948
+ className,
1949
+ ...props
1950
+ }) {
1951
+ if (!label && !secondaryLabel) return null;
1952
+ const accessibleName = [label, secondaryLabel].filter(Boolean).join(" \xB7 ");
1953
+ return /* @__PURE__ */ jsx15(EdgeLabelRenderer2, { children: /* @__PURE__ */ jsx15(
1954
+ "div",
1955
+ {
1956
+ style: {
1957
+ position: "absolute",
1958
+ transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
1959
+ pointerEvents: "none"
1960
+ },
1961
+ className: "nodrag nopan",
1962
+ "data-slot": "edge-label-pill-anchor",
1963
+ children: /* @__PURE__ */ jsxs12(
1964
+ "button",
1965
+ {
1966
+ type: "button",
1967
+ "aria-label": accessibleName,
1968
+ "data-slot": "edge-label-pill",
1969
+ ...props,
1970
+ className: cn11(
1971
+ "pointer-events-auto flex items-center gap-1 rounded-full border bg-flow-node px-2 py-0.5",
1972
+ "text-meta font-medium text-flow-node-foreground shadow-sm",
1973
+ "transition-colors duration-fast ease-standard",
1974
+ "focus-ring",
1975
+ selected ? "border-ring" : "border-flow-group-border",
1976
+ className
1977
+ ),
1978
+ children: [
1979
+ label ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", children: label }) : null,
1980
+ secondaryLabel ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", className: "text-flow-node-foreground/70 tabular-nums", children: secondaryLabel }) : null
1981
+ ]
1982
+ }
1983
+ )
1984
+ }
1985
+ ) });
1986
+ }
1987
+
1988
+ // src/flow-weighted-edge/back-edge-geometry.ts
1989
+ function backEdgeDetour(rects, axis, span, clearance) {
1990
+ const [lo, hi] = span[0] <= span[1] ? span : [span[1], span[0]];
1991
+ let far = Number.NEGATIVE_INFINITY;
1992
+ for (const rect of rects) {
1993
+ if (!(rect.width > 0) || !(rect.height > 0)) continue;
1994
+ const [near, beyond] = axis === "vertical" ? [rect.y, rect.y + rect.height] : [rect.x, rect.x + rect.width];
1995
+ if (beyond < lo || near > hi) continue;
1996
+ far = Math.max(far, axis === "vertical" ? rect.x + rect.width : rect.y + rect.height);
1997
+ }
1998
+ return Number.isFinite(far) ? far + clearance : null;
1999
+ }
2000
+
2001
+ // src/flow-weighted-edge/flow-weighted-edge.tsx
2002
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
2003
+ var NO_NODES = [];
2004
+ var FALLBACK_WEAK = "#6085a1";
2005
+ var FALLBACK_STRONG = "#496d89";
2006
+ var BACK_EDGE_DASHARRAY = "6 4";
2007
+ var BACK_EDGE_OPACITY = 0.7;
2008
+ var BACK_EDGE_CLEARANCE = 40;
2009
+ var DEFAULT_BACK_EDGE_LABEL = "Back edge \u2014 runs against the process direction";
2010
+ function clamp01(t) {
2011
+ return Math.min(1, Math.max(0, t));
2012
+ }
2013
+ function hexToRgb(hex) {
2014
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(hex);
2015
+ if (!m) return [0, 0, 0];
2016
+ return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
2017
+ }
2018
+ function toHexByte(n) {
2019
+ return Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, "0");
2020
+ }
2021
+ function mixHex(a, b, t) {
2022
+ const [ar, ag, ab] = hexToRgb(a);
2023
+ const [br, bg, bb] = hexToRgb(b);
2024
+ const r = ar + (br - ar) * t;
2025
+ const g = ag + (bg - ag) * t;
2026
+ const bl = ab + (bb - ab) * t;
2027
+ return `#${toHexByte(r)}${toHexByte(g)}${toHexByte(bl)}`;
2028
+ }
2029
+ function resolveValueStrokeColor(value, domain) {
2030
+ if (value === void 0 || !domain) return void 0;
2031
+ const [lo, hi] = domain;
2032
+ const t = hi === lo ? 1 : clamp01((value - lo) / (hi - lo));
2033
+ const weak = resolveTokenColor("--flow-edge-weak", { fallback: FALLBACK_WEAK });
2034
+ const strong = resolveTokenColor("--flow-edge-strong", { fallback: FALLBACK_STRONG });
2035
+ return mixHex(weak, strong, t);
2036
+ }
2037
+ function FlowWeightedEdge({
2038
+ id,
2039
+ sourceX,
2040
+ sourceY,
2041
+ targetX,
2042
+ targetY,
2043
+ sourcePosition,
2044
+ targetPosition,
2045
+ markerEnd,
2046
+ style,
2047
+ selected,
2048
+ data
2049
+ }) {
2050
+ const edges = useEdges();
2051
+ const widthByEdgeId = useMemo3(
2052
+ () => computeEdgeWeightScale(edges),
2053
+ [edges]
2054
+ );
2055
+ const variant = data?.variant ?? "forward";
2056
+ const isBack = variant === "back";
2057
+ const nodes = useStore(
2058
+ useCallback5((s) => isBack ? s.nodes : NO_NODES, [isBack])
2059
+ );
2060
+ const pathType = data?.path ?? "bezier";
2061
+ const axis = targetPosition === Position7.Top || targetPosition === Position7.Bottom ? "vertical" : "horizontal";
2062
+ const rects = useMemo3(() => {
2063
+ if (!isBack) return [];
2064
+ const out = [];
2065
+ for (const node of nodes) {
2066
+ if (node.parentId) continue;
2067
+ const width = node.measured?.width ?? node.width;
2068
+ const height = node.measured?.height ?? node.height;
2069
+ if (!width || !height) continue;
2070
+ out.push({ x: node.position.x, y: node.position.y, width, height });
2071
+ }
2072
+ return out;
2073
+ }, [isBack, nodes]);
2074
+ const detour = isBack ? backEdgeDetour(
2075
+ rects,
2076
+ axis,
2077
+ axis === "vertical" ? [sourceY, targetY] : [sourceX, targetX],
2078
+ BACK_EDGE_CLEARANCE
2079
+ ) : null;
2080
+ const [edgePath, labelX, labelY] = isBack ? getSmoothStepPath({
2081
+ sourceX,
2082
+ sourceY,
2083
+ sourcePosition,
2084
+ targetX,
2085
+ targetY,
2086
+ targetPosition,
2087
+ offset: BACK_EDGE_CLEARANCE,
2088
+ ...detour === null ? {} : axis === "vertical" ? { centerX: detour } : { centerY: detour }
2089
+ }) : pathType === "smoothstep" ? getSmoothStepPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }) : getBezierPath5({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
2090
+ const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];
2091
+ const valueColor = useMemo3(
2092
+ () => resolveValueStrokeColor(data?.value, data?.valueDomain),
2093
+ [data?.value, data?.valueDomain]
2094
+ );
2095
+ const stroke = selected ? "var(--ring)" : valueColor ?? "var(--flow-edge)";
2096
+ const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;
2097
+ const edge = /* @__PURE__ */ jsx16(
2098
+ FlowEdgePath,
2099
+ {
2100
+ id,
2101
+ path: edgePath,
2102
+ markerEnd,
2103
+ "data-slot": "flow-weighted-edge",
2104
+ "data-variant": variant,
2105
+ "data-weight": data?.weight,
2106
+ "data-value": data?.value,
2107
+ stroke,
2108
+ strokeWidth,
2109
+ strokeDasharray: isBack ? BACK_EDGE_DASHARRAY : void 0,
2110
+ strokeOpacity: isBack ? BACK_EDGE_OPACITY : void 0,
2111
+ style
2112
+ }
2113
+ );
2114
+ return /* @__PURE__ */ jsxs13(Fragment5, { children: [
2115
+ isBack ? (
2116
+ // A `data-variant` is invisible to assistive technology, so the back
2117
+ // edge's meaning is also published as a named graphic. Only the back
2118
+ // variant is wrapped — a forward edge's DOM is unchanged from before
2119
+ // this prop existed.
2120
+ /* @__PURE__ */ jsx16("g", { role: "img", "aria-label": data?.variantLabel ?? DEFAULT_BACK_EDGE_LABEL, children: edge })
2121
+ ) : edge,
2122
+ /* @__PURE__ */ jsx16(
2123
+ EdgeLabelPill,
2124
+ {
2125
+ label: data?.label,
2126
+ secondaryLabel: data?.secondaryLabel,
2127
+ x: labelX,
2128
+ y: labelY,
2129
+ selected,
2130
+ ...data?.labelProps
2131
+ }
2132
+ )
2133
+ ] });
2134
+ }
2135
+
2136
+ // src/flow-weighted-edge/edge-aria.ts
2137
+ var DEFAULT_WEIGHT_LABEL = "weight";
2138
+ var DEFAULT_VALUE_LABEL = "value";
2139
+ function defaultNameOf(nodeId) {
2140
+ return nodeId;
2141
+ }
2142
+ function defaultFormatNumber(n) {
2143
+ return String(n);
2144
+ }
2145
+ function buildWeightedEdgeAriaLabel(edge, opts = {}) {
2146
+ if (edge.ariaLabel) return edge.ariaLabel;
2147
+ const { weight, value, label, secondaryLabel } = edge.data ?? {};
2148
+ if (weight === void 0 && value === void 0 && !label && !secondaryLabel) {
2149
+ return void 0;
2150
+ }
2151
+ const {
2152
+ weightLabel = DEFAULT_WEIGHT_LABEL,
2153
+ valueLabel = DEFAULT_VALUE_LABEL,
2154
+ nameOf = defaultNameOf,
2155
+ formatNumber = defaultFormatNumber
2156
+ } = opts;
2157
+ const parts = [`Edge from ${nameOf(edge.source)} to ${nameOf(edge.target)}`];
2158
+ if (weight !== void 0) parts.push(`${weightLabel} ${formatNumber(weight)}`);
2159
+ if (value !== void 0) parts.push(`${valueLabel} ${formatNumber(value)}`);
2160
+ const pillText = [label, secondaryLabel].filter(Boolean).join(" ");
2161
+ if (pillText) parts.push(pillText);
2162
+ return parts.join(", ");
2163
+ }
2164
+ function withWeightedEdgeAria(edges, opts) {
2165
+ return edges.map((edge) => {
2166
+ if (edge.ariaLabel) return edge;
2167
+ const ariaLabel = buildWeightedEdgeAriaLabel(edge, opts);
2168
+ return ariaLabel === void 0 ? edge : { ...edge, ariaLabel };
2169
+ });
2170
+ }
2171
+
2172
+ // src/flow-self-loop-edge/flow-self-loop-edge.tsx
2173
+ import { useMemo as useMemo4 } from "react";
2174
+ import { useEdges as useEdges2, useInternalNode as useInternalNode3 } from "@xyflow/react";
2175
+
2176
+ // src/flow-self-loop-edge/self-loop-geometry.ts
2177
+ var DEFAULT_LOOP_RADIUS = 28;
2178
+ var CONTROL_REACH_X = 1.2;
2179
+ var CONTROL_REACH_Y = 2.4;
2180
+ var APEX_FACTOR = 3 / 4 * CONTROL_REACH_Y;
2181
+ function selfLoopPath(anchor, loopRadius) {
2182
+ const cx = Number.isFinite(anchor.centerX) ? anchor.centerX : 0;
2183
+ const ay = Number.isFinite(anchor.topY) ? anchor.topY : 0;
2184
+ const r = Number.isFinite(loopRadius) && loopRadius > 0 ? loopRadius : DEFAULT_LOOP_RADIUS;
2185
+ const startX = cx + r;
2186
+ const endX = cx - r;
2187
+ const controlY = ay - r * CONTROL_REACH_Y;
2188
+ return {
2189
+ path: `M ${startX},${ay} C ${startX + r * CONTROL_REACH_X},${controlY} ${endX - r * CONTROL_REACH_X},${controlY} ${endX},${ay}`,
2190
+ labelX: cx,
2191
+ labelY: ay - r * APEX_FACTOR
2192
+ };
2193
+ }
2194
+ function outward(x, y, centerX, centerY, fallback) {
2195
+ const dx = x - centerX;
2196
+ const dy = y - centerY;
2197
+ const length = Math.hypot(dx, dy);
2198
+ if (!Number.isFinite(length) || length < 1e-6) return fallback;
2199
+ return [dx / length, dy / length];
2200
+ }
2201
+ function selfLoopHandleArc(anchor, loopRadius) {
2202
+ const num = (value) => Number.isFinite(value) ? value : 0;
2203
+ const sx = num(anchor.sourceX);
2204
+ const sy = num(anchor.sourceY);
2205
+ const tx = num(anchor.targetX);
2206
+ const ty = num(anchor.targetY);
2207
+ const cx = num(anchor.centerX);
2208
+ const cy = num(anchor.centerY);
2209
+ const width = Math.max(0, num(anchor.width));
2210
+ const height = Math.max(0, num(anchor.height));
2211
+ const r = Number.isFinite(loopRadius) && loopRadius > 0 ? loopRadius : DEFAULT_LOOP_RADIUS;
2212
+ const [osx, osy] = outward(sx, sy, cx, cy, [0, 1]);
2213
+ const [otx, oty] = outward(tx, ty, cx, cy, [-osx, -osy]);
2214
+ const lx = osy;
2215
+ const ly = -osx;
2216
+ const clearance = Math.abs(lx) * (width / 2) + Math.abs(ly) * (height / 2) + r;
2217
+ const reach = 4 / 3 * clearance;
2218
+ const c1x = sx + osx * r + lx * reach;
2219
+ const c1y = sy + osy * r + ly * reach;
2220
+ const c2x = tx + otx * r + lx * reach;
2221
+ const c2y = ty + oty * r + ly * reach;
2222
+ return {
2223
+ path: `M ${sx},${sy} C ${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`,
2224
+ // A cubic's midpoint is (P0 + 3·C1 + 3·C2 + P3) / 8 — the arc's apex, which `reach`
2225
+ // above places `loopRadius` clear of the card, so the label sits off the node.
2226
+ labelX: (sx + 3 * c1x + 3 * c2x + tx) / 8,
2227
+ labelY: (sy + 3 * c1y + 3 * c2y + ty) / 8
2228
+ };
2229
+ }
2230
+
2231
+ // src/flow-self-loop-edge/flow-self-loop-edge.tsx
2232
+ import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
2233
+ function nodeName(data, fallback) {
2234
+ if (data && typeof data === "object" && "title" in data) {
2235
+ const title = data.title;
2236
+ if (typeof title === "string" && title.length > 0) return title;
2237
+ }
2238
+ return fallback;
2239
+ }
2240
+ function FlowSelfLoopEdge({
2241
+ id,
2242
+ source,
2243
+ sourceX,
2244
+ sourceY,
2245
+ targetX,
2246
+ targetY,
2247
+ markerEnd,
2248
+ style,
2249
+ selected,
2250
+ data
2251
+ }) {
2252
+ const edges = useEdges2();
2253
+ const widthByEdgeId = useMemo4(
2254
+ () => computeEdgeWeightScale(edges),
2255
+ [edges]
2256
+ );
2257
+ const node = useInternalNode3(source);
2258
+ const measuredWidth = node?.measured?.width;
2259
+ const measuredHeight = node?.measured?.height;
2260
+ const loopRadius = data?.loopRadius ?? DEFAULT_LOOP_RADIUS;
2261
+ const { path, labelX, labelY } = useMemo4(() => {
2262
+ if (node && measuredWidth && measuredHeight) {
2263
+ return selfLoopHandleArc(
2264
+ {
2265
+ sourceX,
2266
+ sourceY,
2267
+ targetX,
2268
+ targetY,
2269
+ centerX: node.internals.positionAbsolute.x + measuredWidth / 2,
2270
+ centerY: node.internals.positionAbsolute.y + measuredHeight / 2,
2271
+ width: measuredWidth,
2272
+ height: measuredHeight
2273
+ },
2274
+ loopRadius
2275
+ );
2276
+ }
2277
+ return selfLoopPath(
2278
+ { centerX: (sourceX + targetX) / 2, topY: Math.min(sourceY, targetY) },
2279
+ loopRadius
2280
+ );
2281
+ }, [node, measuredWidth, measuredHeight, sourceX, sourceY, targetX, targetY, loopRadius]);
2282
+ const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];
2283
+ const stroke = selected ? "var(--ring)" : "var(--flow-edge)";
2284
+ const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;
2285
+ const accessibleName = data?.loopLabel ?? `Self-loop on ${nodeName(node?.data, source)} \u2014 this step repeats`;
2286
+ return /* @__PURE__ */ jsxs14(Fragment6, { children: [
2287
+ /* @__PURE__ */ jsx17("g", { role: "img", "aria-label": accessibleName, children: /* @__PURE__ */ jsx17(
2288
+ FlowEdgePath,
2289
+ {
2290
+ id,
2291
+ path,
2292
+ markerEnd,
2293
+ "data-slot": "flow-self-loop-edge",
2294
+ stroke,
2295
+ strokeWidth,
2296
+ style: { fill: "none", ...style }
2297
+ }
2298
+ ) }),
2299
+ /* @__PURE__ */ jsx17(
2300
+ EdgeLabelPill,
2301
+ {
2302
+ label: data?.label,
2303
+ secondaryLabel: data?.secondaryLabel,
2304
+ x: labelX,
2305
+ y: labelY,
2306
+ selected,
2307
+ ...data?.labelProps
2308
+ }
2309
+ )
2310
+ ] });
2311
+ }
1531
2312
  export {
1532
2313
  Background2 as Background,
1533
2314
  CanvasShell,
1534
2315
  Controls,
2316
+ DEFAULT_EDGE_WIDTH_RANGE,
2317
+ DEFAULT_LOOP_RADIUS,
2318
+ EdgeLabelPill,
1535
2319
  FLOW_ALL_SIDE_HANDLES,
2320
+ FLOW_EDGE_FOCUS_CONTOUR_WIDTH,
2321
+ FLOW_EDGE_FOCUS_RING_WIDTH,
1536
2322
  FLOW_GROUP_NODE_TYPE,
2323
+ FLOW_HANDLE_ANCHOR_CLASS,
1537
2324
  FlowButtonEdge,
1538
2325
  FlowEdge,
2326
+ FlowEdgePath,
1539
2327
  FlowFloatingEdge,
1540
2328
  FlowGroupNode,
1541
2329
  FlowMiniMap,
1542
2330
  FlowNode,
1543
2331
  FlowPlaceholderNode,
2332
+ FlowSelfLoopEdge,
1544
2333
  FlowSmartEdge,
2334
+ FlowWeightedEdge,
1545
2335
  HANDLE_SIDES,
1546
2336
  HelperLines,
1547
2337
  InspectorPanel,
1548
2338
  Legend,
1549
2339
  MiniMap2 as MiniMap,
1550
2340
  Panel2 as Panel,
1551
- Position7 as Position,
2341
+ Position8 as Position,
1552
2342
  ReactFlow2 as ReactFlow,
1553
2343
  ReactFlowProvider2 as ReactFlowProvider,
1554
2344
  ZoomControls,
1555
2345
  addEdge,
2346
+ backEdgeDetour,
2347
+ buildWeightedEdgeAriaLabel,
1556
2348
  collapseGroup,
2349
+ computeEdgeWeightScale,
1557
2350
  expandGroup,
1558
2351
  getEdgeParams,
1559
2352
  getHelperLines,
@@ -1562,7 +2355,11 @@ export {
1562
2355
  isFlowGroupProxyEdge,
1563
2356
  layoutFlow,
1564
2357
  layoutGraph,
2358
+ pickClosestAnchors,
1565
2359
  pickClosestHandles,
2360
+ positionToSide,
2361
+ selfLoopHandleArc,
2362
+ selfLoopPath,
1566
2363
  sideToPosition,
1567
2364
  toggleGroupCollapsed,
1568
2365
  ungroup,
@@ -1572,6 +2369,7 @@ export {
1572
2369
  useFlowLayout,
1573
2370
  useHelperLines,
1574
2371
  useNodesState,
1575
- useReactFlow5 as useReactFlow
2372
+ useReactFlow6 as useReactFlow,
2373
+ withWeightedEdgeAria
1576
2374
  };
1577
2375
  //# sourceMappingURL=index.js.map