@elabs-ai/components-flow 4.0.0 → 4.2.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 (68) hide show
  1. package/README.md +8 -8
  2. package/dist/index.d.ts +738 -20
  3. package/dist/index.js +985 -182
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -7
  6. package/src/__contract__/inspector-panel.contract.test.tsx +49 -0
  7. package/src/__contract__/legend.contract.test.tsx +49 -0
  8. package/src/canvas-shell/canvas-shell.tsx +116 -1
  9. package/src/canvas-shell/use-measured-nodes.ts +101 -0
  10. package/src/flow-button-edge/flow-button-edge.stories.tsx +13 -0
  11. package/src/flow-button-edge/flow-button-edge.tsx +8 -10
  12. package/src/flow-edge/flow-edge.stories.tsx +20 -0
  13. package/src/flow-edge/flow-edge.tsx +10 -3
  14. package/src/flow-edge-path/flow-edge-path.tsx +149 -0
  15. package/src/flow-edge-path/index.ts +1 -0
  16. package/src/flow-edge-path/no-raw-base-edge.test.ts +45 -0
  17. package/src/flow-floating-edge/flow-floating-edge.tsx +7 -3
  18. package/src/flow-group-node/flow-group-node.stories.tsx +1 -1
  19. package/src/flow-group-node/flow-group-node.tsx +24 -8
  20. package/src/flow-handle/flow-handle-anchor.test.tsx +97 -0
  21. package/src/flow-handle/flow-handle-anchor.ts +36 -0
  22. package/src/flow-handle/index.ts +1 -0
  23. package/src/flow-layout/flow-layout.stories.tsx +2 -2
  24. package/src/flow-layout/flow-layout.test.tsx +91 -0
  25. package/src/flow-layout/flow-layout.ts +77 -1
  26. package/src/flow-layout/layout-graph.test.ts +83 -2
  27. package/src/flow-layout/layout-graph.ts +23 -15
  28. package/src/flow-mini-map/flow-mini-map.stories.tsx +103 -0
  29. package/src/flow-node/flow-node.stories.tsx +151 -0
  30. package/src/flow-node/flow-node.tsx +56 -1
  31. package/src/flow-placeholder-node/flow-placeholder-node.tsx +5 -2
  32. package/src/flow-self-loop-edge/flow-self-loop-edge.stories.tsx +275 -0
  33. package/src/flow-self-loop-edge/flow-self-loop-edge.test.tsx +196 -0
  34. package/src/flow-self-loop-edge/flow-self-loop-edge.tsx +172 -0
  35. package/src/flow-self-loop-edge/index.ts +13 -0
  36. package/src/flow-self-loop-edge/self-loop-geometry.test.ts +128 -0
  37. package/src/flow-self-loop-edge/self-loop-geometry.ts +165 -0
  38. package/src/flow-smart-edge/flow-smart-edge.stories.tsx +55 -8
  39. package/src/flow-smart-edge/flow-smart-edge.tsx +125 -35
  40. package/src/flow-smart-edge/index.ts +5 -1
  41. package/src/flow-smart-edge/smart-edge-geometry.test.ts +88 -47
  42. package/src/flow-smart-edge/smart-edge-geometry.ts +69 -33
  43. package/src/flow-weighted-edge/back-edge-geometry.test.ts +54 -0
  44. package/src/flow-weighted-edge/back-edge-geometry.ts +60 -0
  45. package/src/flow-weighted-edge/edge-aria.test.ts +108 -0
  46. package/src/flow-weighted-edge/edge-aria.ts +117 -0
  47. package/src/flow-weighted-edge/edge-label-pill.test.tsx +65 -0
  48. package/src/flow-weighted-edge/edge-label-pill.tsx +82 -0
  49. package/src/flow-weighted-edge/flow-weighted-edge.stories.tsx +691 -0
  50. package/src/flow-weighted-edge/flow-weighted-edge.test.tsx +405 -0
  51. package/src/flow-weighted-edge/flow-weighted-edge.tsx +308 -0
  52. package/src/flow-weighted-edge/index.ts +18 -0
  53. package/src/flow-weighted-edge/weight-scale.test.ts +92 -0
  54. package/src/flow-weighted-edge/weight-scale.ts +86 -0
  55. package/src/index.ts +9 -0
  56. package/src/inspector-panel/inspector-panel.stories.tsx +1 -1
  57. package/src/inspector-panel/inspector-panel.test.tsx +20 -0
  58. package/src/inspector-panel/inspector-panel.tsx +22 -9
  59. package/src/legend/index.ts +7 -1
  60. package/src/legend/legend.stories.tsx +126 -0
  61. package/src/legend/legend.test.tsx +180 -0
  62. package/src/legend/legend.tsx +222 -3
  63. package/src/templates-flow-workspace.stories.tsx +1 -1
  64. package/src/testing/canvas-framing.test.ts +107 -0
  65. package/src/testing/canvas-framing.ts +396 -0
  66. package/src/testing/edge-anchors.ts +107 -0
  67. package/src/testing/index.ts +36 -0
  68. 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,
@@ -985,15 +1226,15 @@ function FlowMiniMap({
985
1226
 
986
1227
  // src/inspector-panel/inspector-panel.tsx
987
1228
  import "react";
988
- import { Reveal, useCollapsiblePanel } from "@elabs-ai/components-ui";
989
- import { cn as cn6 } from "@elabs-ai/components-ui/lib/cn";
990
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1229
+ import { Reveal, useCollapsiblePanel, useLocale } from "@elabs-ai/components-ui";
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
- title = "Inspector",
1234
+ title,
994
1235
  children,
995
1236
  onClose,
996
- emptyMessage = "Select a node to see its details.",
1237
+ emptyMessage,
997
1238
  hasSelection = true,
998
1239
  selectionKey,
999
1240
  open,
@@ -1003,6 +1244,9 @@ function InspectorPanel({
1003
1244
  width = INSPECTOR_PANEL_WIDTH,
1004
1245
  className
1005
1246
  }) {
1247
+ const { t } = useLocale();
1248
+ const resolvedTitle = title ?? t("flow.inspectorPanel.title");
1249
+ const resolvedEmptyMessage = emptyMessage ?? t("flow.inspectorPanel.emptyMessage");
1006
1250
  const panel = useCollapsiblePanel({
1007
1251
  side,
1008
1252
  open,
@@ -1015,31 +1259,31 @@ function InspectorPanel({
1015
1259
  right: "right-0 group-data-[state=collapsed]:right-[calc(var(--inspector-panel-width)*-1)]"
1016
1260
  }
1017
1261
  });
1018
- return /* @__PURE__ */ jsxs7(
1262
+ return /* @__PURE__ */ jsxs8(
1019
1263
  "div",
1020
1264
  {
1021
- className: cn6("group relative h-full overflow-hidden", className),
1265
+ className: cn7("group relative h-full overflow-hidden", className),
1022
1266
  style: { "--inspector-panel-width": width },
1023
1267
  inert: panel.open ? void 0 : true,
1024
1268
  ...panel.attrs,
1025
1269
  children: [
1026
- /* @__PURE__ */ jsx10("div", { "data-slot": "inspector-panel-gap", className: panel.spacerClassName }),
1027
- /* @__PURE__ */ jsx10(
1270
+ /* @__PURE__ */ jsx11("div", { "data-slot": "inspector-panel-gap", className: panel.spacerClassName }),
1271
+ /* @__PURE__ */ jsx11(
1028
1272
  "div",
1029
1273
  {
1030
1274
  "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(
1275
+ className: cn7(panel.containerClassName, "absolute inset-y-0 flex h-full border-s"),
1276
+ children: /* @__PURE__ */ jsxs8("div", { className: "flex h-full w-full flex-col bg-card", children: [
1277
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between border-b px-4 py-3", children: [
1278
+ /* @__PURE__ */ jsx11("h3", { className: "text-body font-semibold text-foreground", children: resolvedTitle }),
1279
+ onClose ? /* @__PURE__ */ jsx11(
1036
1280
  "button",
1037
1281
  {
1038
1282
  type: "button",
1039
- "aria-label": "Close inspector",
1283
+ "aria-label": t("flow.inspectorPanel.close"),
1040
1284
  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(
1285
+ className: "rounded-sm p-1 text-muted-foreground hover:text-foreground focus-ring",
1286
+ children: /* @__PURE__ */ jsx11(
1043
1287
  "svg",
1044
1288
  {
1045
1289
  width: "14",
@@ -1051,22 +1295,23 @@ function InspectorPanel({
1051
1295
  strokeLinecap: "round",
1052
1296
  strokeLinejoin: "round",
1053
1297
  "aria-hidden": "true",
1054
- children: /* @__PURE__ */ jsx10("path", { d: "M18 6 6 18M6 6l12 12" })
1298
+ children: /* @__PURE__ */ jsx11("path", { d: "M18 6 6 18M6 6l12 12" })
1055
1299
  }
1056
1300
  )
1057
1301
  }
1058
1302
  ) : null
1059
1303
  ] }),
1060
- hasSelection ? /* @__PURE__ */ jsx10(
1304
+ hasSelection ? /* @__PURE__ */ jsx11(
1061
1305
  Reveal,
1062
1306
  {
1063
1307
  appear: "fade",
1064
1308
  speed: "fast",
1065
- className: "flex-1 overflow-y-auto p-4 text-body",
1309
+ tabIndex: 0,
1310
+ className: "focus-ring flex-1 overflow-y-auto p-4 text-body",
1066
1311
  children
1067
1312
  },
1068
1313
  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 }) })
1314
+ ) : /* @__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: resolvedEmptyMessage }) })
1070
1315
  ] })
1071
1316
  }
1072
1317
  )
@@ -1076,20 +1321,64 @@ function InspectorPanel({
1076
1321
  }
1077
1322
 
1078
1323
  // 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(
1324
+ import { useLocale as useLocale2 } from "@elabs-ai/components-ui";
1325
+ import { cn as cn8 } from "@elabs-ai/components-ui/lib/cn";
1326
+
1327
+ // src/flow-weighted-edge/weight-scale.ts
1328
+ var DEFAULT_EDGE_WIDTH_RANGE = [1.5, 8];
1329
+ var DEFAULT_SCALE_GROUP = "__default__";
1330
+ function computeEdgeWeightScale(edges, opts = {}) {
1331
+ const [minWidth, maxWidth] = opts.widthRange ?? DEFAULT_EDGE_WIDTH_RANGE;
1332
+ const result = /* @__PURE__ */ new Map();
1333
+ const groups = /* @__PURE__ */ new Map();
1334
+ for (const edge of edges) {
1335
+ const weight = edge.data?.weight;
1336
+ if (weight === void 0) {
1337
+ result.set(edge.id, minWidth);
1338
+ continue;
1339
+ }
1340
+ const group = edge.data?.scaleGroup ?? DEFAULT_SCALE_GROUP;
1341
+ if (opts.scaleGroup !== void 0 && group !== opts.scaleGroup) continue;
1342
+ const list = groups.get(group);
1343
+ if (list) list.push({ id: edge.id, weight });
1344
+ else groups.set(group, [{ id: edge.id, weight }]);
1345
+ }
1346
+ for (const list of groups.values()) {
1347
+ let min = Infinity;
1348
+ let max = -Infinity;
1349
+ for (const { weight } of list) {
1350
+ if (weight < min) min = weight;
1351
+ if (weight > max) max = weight;
1352
+ }
1353
+ const span = max - min;
1354
+ for (const { id, weight } of list) {
1355
+ const width = span === 0 ? (minWidth + maxWidth) / 2 : minWidth + (weight - min) / span * (maxWidth - minWidth);
1356
+ result.set(id, width);
1357
+ }
1358
+ }
1359
+ return result;
1360
+ }
1361
+
1362
+ // src/legend/legend.tsx
1363
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1364
+ function Legend(props) {
1365
+ if (props.variant === "scale") {
1366
+ return /* @__PURE__ */ jsx12(LegendScale, { ...props });
1367
+ }
1368
+ return /* @__PURE__ */ jsx12(LegendCategorical, { ...props });
1369
+ }
1370
+ function LegendCategorical({ items, title, className }) {
1371
+ return /* @__PURE__ */ jsxs9(
1083
1372
  "div",
1084
1373
  {
1085
- className: cn7(
1374
+ className: cn8(
1086
1375
  "rounded-lg bg-surface-elevated/90 p-3 text-xs shadow-ring-sm backdrop-blur",
1087
1376
  className
1088
1377
  ),
1089
1378
  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(
1379
+ title ? /* @__PURE__ */ jsx12("div", { className: "mb-1.5 font-medium text-foreground", children: title }) : null,
1380
+ /* @__PURE__ */ jsx12("ul", { className: "space-y-1", children: items.map((item) => /* @__PURE__ */ jsxs9("li", { className: "flex items-center gap-2 text-muted-foreground", children: [
1381
+ /* @__PURE__ */ jsx12(
1093
1382
  "span",
1094
1383
  {
1095
1384
  "aria-hidden": "true",
@@ -1103,38 +1392,145 @@ function Legend({ items, title, className }) {
1103
1392
  }
1104
1393
  );
1105
1394
  }
1395
+ function computeWidthSamples(domain, ticks) {
1396
+ const [min, max] = domain;
1397
+ const values = ticks === "minmedmax" ? [min, (min + max) / 2, max] : [min, max];
1398
+ const likeEdges = values.map((weight, index) => ({
1399
+ id: String(index),
1400
+ data: { weight }
1401
+ }));
1402
+ const widths = computeEdgeWeightScale(likeEdges);
1403
+ return values.map((value, index) => ({ value, width: widths.get(String(index)) ?? 0 }));
1404
+ }
1405
+ var COLOR_STOP_FRACTIONS = [0, 0.25, 0.5, 0.75, 1];
1406
+ function scaleKindLabel(kind) {
1407
+ return kind === "width" ? "width" : "color";
1408
+ }
1409
+ function LegendScale({
1410
+ kind,
1411
+ domain,
1412
+ format,
1413
+ ticks = "minmax",
1414
+ title,
1415
+ className
1416
+ }) {
1417
+ const { formatNumber } = useLocale2();
1418
+ const format_ = format ?? formatNumber;
1419
+ const [min, max] = domain;
1420
+ const ariaLabel = `Edge ${scaleKindLabel(kind)} scale, ${format_(min)} to ${format_(max)}, minimum to maximum`;
1421
+ return /* @__PURE__ */ jsxs9(
1422
+ "div",
1423
+ {
1424
+ "aria-label": ariaLabel,
1425
+ className: cn8(
1426
+ "flex flex-col gap-2 rounded-lg bg-surface-elevated/90 p-3 shadow-ring-sm backdrop-blur",
1427
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
1428
+ className
1429
+ ),
1430
+ role: "group",
1431
+ tabIndex: 0,
1432
+ children: [
1433
+ title ? /* @__PURE__ */ jsx12("div", { className: "text-body font-medium text-foreground", children: title }) : null,
1434
+ kind === "width" ? /* @__PURE__ */ jsx12(LegendScaleWidth, { domain, format: format_, ticks }) : /* @__PURE__ */ jsx12(LegendScaleColor, { domain, format: format_ })
1435
+ ]
1436
+ }
1437
+ );
1438
+ }
1439
+ function LegendScaleWidth({
1440
+ domain,
1441
+ format,
1442
+ ticks
1443
+ }) {
1444
+ const samples = computeWidthSamples(domain, ticks);
1445
+ return /* @__PURE__ */ jsx12("div", { className: "flex flex-col gap-2", children: samples.map((sample, index) => (
1446
+ // Index disambiguates a zero-width domain, where every sample shares
1447
+ // the same value (and therefore the same width).
1448
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-3", children: [
1449
+ /* @__PURE__ */ jsx12("svg", { "aria-hidden": "true", className: "shrink-0", height: 16, overflow: "visible", width: 40, children: /* @__PURE__ */ jsx12(
1450
+ "line",
1451
+ {
1452
+ stroke: "var(--flow-edge)",
1453
+ strokeLinecap: "round",
1454
+ strokeWidth: sample.width,
1455
+ x1: 2,
1456
+ x2: 38,
1457
+ y1: 8,
1458
+ y2: 8
1459
+ }
1460
+ ) }),
1461
+ /* @__PURE__ */ jsx12("span", { className: "min-w-0 truncate text-meta tabular-nums text-muted-foreground", children: format(sample.value) })
1462
+ ] }, `${index}-${sample.value}`)
1463
+ )) });
1464
+ }
1465
+ function LegendScaleColor({
1466
+ domain,
1467
+ format
1468
+ }) {
1469
+ const [min, max] = domain;
1470
+ const span = max - min;
1471
+ const stops = COLOR_STOP_FRACTIONS.map((fraction) => ({
1472
+ fraction,
1473
+ value: min + fraction * span
1474
+ }));
1475
+ const gradient = `linear-gradient(to right, ${stops.map(({ fraction }) => {
1476
+ const percent = Math.round(fraction * 100);
1477
+ return `color-mix(in oklch, var(--flow-edge-strong) ${percent}%, var(--flow-edge-weak)) ${percent}%`;
1478
+ }).join(", ")})`;
1479
+ return /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1.5", children: [
1480
+ /* @__PURE__ */ jsx12(
1481
+ "div",
1482
+ {
1483
+ "aria-hidden": "true",
1484
+ className: "h-3 w-full rounded-full",
1485
+ style: { backgroundImage: gradient }
1486
+ }
1487
+ ),
1488
+ /* @__PURE__ */ jsx12("div", { className: "flex justify-between gap-1", children: stops.map((stop, index) => /* @__PURE__ */ jsx12(
1489
+ "span",
1490
+ {
1491
+ className: cn8(
1492
+ "min-w-0 truncate text-meta tabular-nums text-muted-foreground",
1493
+ index === 0 && "text-start",
1494
+ index === stops.length - 1 && "text-end"
1495
+ ),
1496
+ children: format(stop.value)
1497
+ },
1498
+ `${index}-${stop.value}`
1499
+ )) })
1500
+ ] });
1501
+ }
1106
1502
 
1107
1503
  // 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";
1504
+ import { Panel, useReactFlow as useReactFlow4 } from "@xyflow/react";
1505
+ import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
1506
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1111
1507
  function ControlButton({
1112
1508
  label,
1113
1509
  onClick,
1114
1510
  children
1115
1511
  }) {
1116
- return /* @__PURE__ */ jsx12(
1512
+ return /* @__PURE__ */ jsx13(
1117
1513
  "button",
1118
1514
  {
1119
1515
  type: "button",
1120
1516
  "aria-label": label,
1121
1517
  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",
1518
+ className: "flex size-8 items-center justify-center text-foreground transition-colors duration-fast hover:bg-surface-muted focus-ring [&_svg]:size-4",
1123
1519
  children
1124
1520
  }
1125
1521
  );
1126
1522
  }
1127
1523
  function ZoomControls({ position = "bottom-right", className }) {
1128
- const { zoomIn, zoomOut, fitView } = useReactFlow3();
1129
- return /* @__PURE__ */ jsx12(Panel, { position, children: /* @__PURE__ */ jsxs9(
1524
+ const { zoomIn, zoomOut, fitView } = useReactFlow4();
1525
+ return /* @__PURE__ */ jsx13(Panel, { position, children: /* @__PURE__ */ jsxs10(
1130
1526
  "div",
1131
1527
  {
1132
- className: cn8(
1528
+ className: cn9(
1133
1529
  "flex divide-x overflow-hidden rounded-lg bg-surface-elevated shadow-ring-sm",
1134
1530
  className
1135
1531
  ),
1136
1532
  children: [
1137
- /* @__PURE__ */ jsx12(ControlButton, { label: "Zoom in", onClick: () => zoomIn(), children: /* @__PURE__ */ jsx12(
1533
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Zoom in", onClick: () => zoomIn(), children: /* @__PURE__ */ jsx13(
1138
1534
  "svg",
1139
1535
  {
1140
1536
  viewBox: "0 0 24 24",
@@ -1142,10 +1538,10 @@ function ZoomControls({ position = "bottom-right", className }) {
1142
1538
  stroke: "currentColor",
1143
1539
  strokeWidth: "2",
1144
1540
  strokeLinecap: "round",
1145
- children: /* @__PURE__ */ jsx12("path", { d: "M12 5v14M5 12h14" })
1541
+ children: /* @__PURE__ */ jsx13("path", { d: "M12 5v14M5 12h14" })
1146
1542
  }
1147
1543
  ) }),
1148
- /* @__PURE__ */ jsx12(ControlButton, { label: "Zoom out", onClick: () => zoomOut(), children: /* @__PURE__ */ jsx12(
1544
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Zoom out", onClick: () => zoomOut(), children: /* @__PURE__ */ jsx13(
1149
1545
  "svg",
1150
1546
  {
1151
1547
  viewBox: "0 0 24 24",
@@ -1153,10 +1549,10 @@ function ZoomControls({ position = "bottom-right", className }) {
1153
1549
  stroke: "currentColor",
1154
1550
  strokeWidth: "2",
1155
1551
  strokeLinecap: "round",
1156
- children: /* @__PURE__ */ jsx12("path", { d: "M5 12h14" })
1552
+ children: /* @__PURE__ */ jsx13("path", { d: "M5 12h14" })
1157
1553
  }
1158
1554
  ) }),
1159
- /* @__PURE__ */ jsx12(ControlButton, { label: "Fit view", onClick: () => fitView(), children: /* @__PURE__ */ jsx12(
1555
+ /* @__PURE__ */ jsx13(ControlButton, { label: "Fit view", onClick: () => fitView(), children: /* @__PURE__ */ jsx13(
1160
1556
  "svg",
1161
1557
  {
1162
1558
  viewBox: "0 0 24 24",
@@ -1165,7 +1561,7 @@ function ZoomControls({ position = "bottom-right", className }) {
1165
1561
  strokeWidth: "2",
1166
1562
  strokeLinecap: "round",
1167
1563
  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" })
1564
+ 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
1565
  }
1170
1566
  ) })
1171
1567
  ]
@@ -1177,11 +1573,11 @@ function ZoomControls({ position = "bottom-right", className }) {
1177
1573
  import "react";
1178
1574
  import { Handle as Handle3, NodeResizer, Position as Position6, useNodes } from "@xyflow/react";
1179
1575
  import { ChevronDown, ChevronRight } from "lucide-react";
1180
- import { cn as cn9 } from "@elabs-ai/components-ui/lib/cn";
1576
+ import { cn as cn10 } from "@elabs-ai/components-ui/lib/cn";
1181
1577
 
1182
1578
  // src/use-flow-groups/use-flow-groups.ts
1183
- import { useCallback as useCallback3 } from "react";
1184
- import { useReactFlow as useReactFlow4 } from "@xyflow/react";
1579
+ import { useCallback as useCallback4 } from "react";
1580
+ import { useReactFlow as useReactFlow5 } from "@xyflow/react";
1185
1581
 
1186
1582
  // src/use-flow-groups/group-operations.ts
1187
1583
  import { getNodesBounds } from "@xyflow/react";
@@ -1375,15 +1771,15 @@ function generateGroupId() {
1375
1771
  return `flow-group-${Math.random().toString(36).slice(2, 9)}`;
1376
1772
  }
1377
1773
  function useFlowGroups() {
1378
- const { getNodes, getEdges, setNodes, setEdges } = useReactFlow4();
1379
- const apply = useCallback3(
1774
+ const { getNodes, getEdges, setNodes, setEdges } = useReactFlow5();
1775
+ const apply = useCallback4(
1380
1776
  (result) => {
1381
1777
  setNodes(result.nodes);
1382
1778
  setEdges(result.edges);
1383
1779
  },
1384
1780
  [setNodes, setEdges]
1385
1781
  );
1386
- const groupNodes2 = useCallback3(
1782
+ const groupNodes2 = useCallback4(
1387
1783
  (nodeIds, options) => {
1388
1784
  if (nodeIds.length === 0) return void 0;
1389
1785
  const groupId = options?.groupId ?? generateGroupId();
@@ -1396,26 +1792,26 @@ function useFlowGroups() {
1396
1792
  },
1397
1793
  [getNodes, getEdges, apply]
1398
1794
  );
1399
- const groupSelection = useCallback3(
1795
+ const groupSelection = useCallback4(
1400
1796
  (options) => {
1401
1797
  const selectedIds = getNodes().filter((n) => n.selected && !n.parentId).map((n) => n.id);
1402
1798
  return groupNodes2(selectedIds, options);
1403
1799
  },
1404
1800
  [getNodes, groupNodes2]
1405
1801
  );
1406
- const ungroup2 = useCallback3(
1802
+ const ungroup2 = useCallback4(
1407
1803
  (groupId) => apply(ungroup(getNodes(), getEdges(), groupId)),
1408
1804
  [getNodes, getEdges, apply]
1409
1805
  );
1410
- const collapseGroup2 = useCallback3(
1806
+ const collapseGroup2 = useCallback4(
1411
1807
  (groupId) => apply(collapseGroup(getNodes(), getEdges(), groupId)),
1412
1808
  [getNodes, getEdges, apply]
1413
1809
  );
1414
- const expandGroup2 = useCallback3(
1810
+ const expandGroup2 = useCallback4(
1415
1811
  (groupId) => apply(expandGroup(getNodes(), getEdges(), groupId)),
1416
1812
  [getNodes, getEdges, apply]
1417
1813
  );
1418
- const toggleCollapse = useCallback3(
1814
+ const toggleCollapse = useCallback4(
1419
1815
  (groupId) => apply(toggleGroupCollapsed(getNodes(), getEdges(), groupId)),
1420
1816
  [getNodes, getEdges, apply]
1421
1817
  );
@@ -1430,18 +1826,22 @@ function useFlowGroups() {
1430
1826
  }
1431
1827
 
1432
1828
  // src/flow-group-node/flow-group-node.tsx
1433
- import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1434
- var toneAccent = {
1829
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1830
+ var toneMark = {
1435
1831
  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
1832
  accent: "text-primary-text",
1440
1833
  success: "text-success",
1441
1834
  warning: "text-warning",
1442
1835
  destructive: "text-destructive"
1443
1836
  };
1444
- var handleClassName2 = "!size-2 !border-2 !border-flow-group-border !bg-flow-group";
1837
+ var toneInk = {
1838
+ default: "text-muted-foreground",
1839
+ accent: "text-primary-text",
1840
+ success: "text-success-text",
1841
+ warning: "text-warning-text",
1842
+ destructive: "text-destructive-text"
1843
+ };
1844
+ var handleClassName2 = `!size-2 !border-2 !border-flow-group-border !bg-flow-group ${FLOW_HANDLE_ANCHOR_CLASS}`;
1445
1845
  function FlowGroupNode({ id, data, selected }) {
1446
1846
  const { toggleCollapse } = useFlowGroups();
1447
1847
  const nodes = useNodes();
@@ -1449,17 +1849,17 @@ function FlowGroupNode({ id, data, selected }) {
1449
1849
  const tone = data.tone ?? "default";
1450
1850
  const liveChildCount = nodes.filter((n) => n.parentId === id).length;
1451
1851
  const childCount = liveChildCount || data.childCount || 0;
1452
- return /* @__PURE__ */ jsxs10(
1852
+ return /* @__PURE__ */ jsxs11(
1453
1853
  "div",
1454
1854
  {
1455
- className: cn9(
1855
+ className: cn10(
1456
1856
  "flex h-full w-full flex-col rounded-lg border bg-flow-group/60 text-foreground",
1457
1857
  "border-flow-group-border transition-[box-shadow,border-color] duration-fast ease-standard motion-reduce:transition-none",
1458
1858
  collapsed ? "shadow-sm" : "shadow-none",
1459
1859
  selected && "ring-2 ring-ring"
1460
1860
  ),
1461
1861
  children: [
1462
- !collapsed && selected ? /* @__PURE__ */ jsx13(
1862
+ !collapsed && selected ? /* @__PURE__ */ jsx14(
1463
1863
  NodeResizer,
1464
1864
  {
1465
1865
  minWidth: 160,
@@ -1468,39 +1868,39 @@ function FlowGroupNode({ id, data, selected }) {
1468
1868
  handleClassName: handleClassName2
1469
1869
  }
1470
1870
  ) : 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(
1871
+ /* @__PURE__ */ jsx14(Handle3, { type: "target", position: Position6.Top, className: handleClassName2 }),
1872
+ /* @__PURE__ */ jsx14(Handle3, { type: "source", position: Position6.Bottom, className: handleClassName2 }),
1873
+ /* @__PURE__ */ jsxs11(
1474
1874
  "div",
1475
1875
  {
1476
- className: cn9(
1876
+ className: cn10(
1477
1877
  "flex items-center gap-2 px-3 py-2",
1478
1878
  !collapsed && "border-b border-flow-group-border"
1479
1879
  ),
1480
1880
  children: [
1481
- /* @__PURE__ */ jsx13(
1881
+ /* @__PURE__ */ jsx14(
1482
1882
  "button",
1483
1883
  {
1484
1884
  type: "button",
1485
1885
  onClick: () => toggleCollapse(id),
1486
1886
  "aria-label": collapsed ? `Expand group ${data.title}` : `Collapse group ${data.title}`,
1487
1887
  "aria-expanded": !collapsed,
1488
- className: cn9(
1888
+ className: cn10(
1489
1889
  "-ml-1 grid size-5 shrink-0 place-items-center rounded",
1490
1890
  "text-muted-foreground hover:bg-muted hover:text-foreground",
1491
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
1891
+ "focus-ring"
1492
1892
  ),
1493
- children: collapsed ? /* @__PURE__ */ jsx13(ChevronRight, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx13(ChevronDown, { "aria-hidden": "true", className: "size-4" })
1893
+ children: collapsed ? /* @__PURE__ */ jsx14(ChevronRight, { "aria-hidden": "true", className: "size-4" }) : /* @__PURE__ */ jsx14(ChevronDown, { "aria-hidden": "true", className: "size-4" })
1494
1894
  }
1495
1895
  ),
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(
1896
+ data.icon ? /* @__PURE__ */ jsx14("span", { className: cn10("[&_svg]:size-4", toneMark[tone]), children: data.icon }) : null,
1897
+ /* @__PURE__ */ jsx14("span", { className: "min-w-0 flex-1 truncate text-body font-medium", children: data.title }),
1898
+ /* @__PURE__ */ jsx14(
1499
1899
  "span",
1500
1900
  {
1501
- className: cn9(
1901
+ className: cn10(
1502
1902
  "shrink-0 rounded-full bg-muted px-1.5 py-0.5 text-meta font-medium tabular-nums",
1503
- toneAccent[tone]
1903
+ toneInk[tone]
1504
1904
  ),
1505
1905
  "aria-label": `${childCount} ${childCount === 1 ? "node" : "nodes"}`,
1506
1906
  children: childCount
@@ -1520,40 +1920,438 @@ import {
1520
1920
  Controls,
1521
1921
  MiniMap as MiniMap2,
1522
1922
  Panel as Panel2,
1523
- Position as Position7,
1923
+ Position as Position8,
1524
1924
  ReactFlow as ReactFlow2,
1525
1925
  ReactFlowProvider as ReactFlowProvider2,
1526
- useReactFlow as useReactFlow5,
1926
+ useReactFlow as useReactFlow6,
1527
1927
  useNodesState,
1528
1928
  useEdgesState,
1529
1929
  addEdge
1530
1930
  } from "@xyflow/react";
1931
+
1932
+ // src/flow-weighted-edge/flow-weighted-edge.tsx
1933
+ import { useCallback as useCallback5, useMemo as useMemo3 } from "react";
1934
+ import {
1935
+ Position as Position7,
1936
+ getBezierPath as getBezierPath5,
1937
+ getSmoothStepPath,
1938
+ useEdges,
1939
+ useStore
1940
+ } from "@xyflow/react";
1941
+ import { resolveTokenColor } from "@elabs-ai/components-tokens";
1942
+
1943
+ // src/flow-weighted-edge/edge-label-pill.tsx
1944
+ import { EdgeLabelRenderer as EdgeLabelRenderer2 } from "@xyflow/react";
1945
+ import { cn as cn11 } from "@elabs-ai/components-ui/lib/cn";
1946
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1947
+ function EdgeLabelPill({
1948
+ label,
1949
+ secondaryLabel,
1950
+ x,
1951
+ y,
1952
+ selected,
1953
+ className,
1954
+ ...props
1955
+ }) {
1956
+ if (!label && !secondaryLabel) return null;
1957
+ const accessibleName = [label, secondaryLabel].filter(Boolean).join(" \xB7 ");
1958
+ return /* @__PURE__ */ jsx15(EdgeLabelRenderer2, { children: /* @__PURE__ */ jsx15(
1959
+ "div",
1960
+ {
1961
+ style: {
1962
+ position: "absolute",
1963
+ transform: `translate(-50%, -50%) translate(${x}px, ${y}px)`,
1964
+ pointerEvents: "none"
1965
+ },
1966
+ className: "nodrag nopan",
1967
+ "data-slot": "edge-label-pill-anchor",
1968
+ children: /* @__PURE__ */ jsxs12(
1969
+ "button",
1970
+ {
1971
+ type: "button",
1972
+ "aria-label": accessibleName,
1973
+ "data-slot": "edge-label-pill",
1974
+ ...props,
1975
+ className: cn11(
1976
+ "pointer-events-auto flex items-center gap-1 rounded-full border bg-flow-node px-2 py-0.5",
1977
+ "text-meta font-medium text-flow-node-foreground shadow-sm",
1978
+ "transition-colors duration-fast ease-standard",
1979
+ "focus-ring",
1980
+ selected ? "border-ring" : "border-flow-group-border",
1981
+ className
1982
+ ),
1983
+ children: [
1984
+ label ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", children: label }) : null,
1985
+ secondaryLabel ? /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", className: "text-flow-node-foreground/70 tabular-nums", children: secondaryLabel }) : null
1986
+ ]
1987
+ }
1988
+ )
1989
+ }
1990
+ ) });
1991
+ }
1992
+
1993
+ // src/flow-weighted-edge/back-edge-geometry.ts
1994
+ function backEdgeDetour(rects, axis, span, clearance) {
1995
+ const [lo, hi] = span[0] <= span[1] ? span : [span[1], span[0]];
1996
+ let far = Number.NEGATIVE_INFINITY;
1997
+ for (const rect of rects) {
1998
+ if (!(rect.width > 0) || !(rect.height > 0)) continue;
1999
+ const [near, beyond] = axis === "vertical" ? [rect.y, rect.y + rect.height] : [rect.x, rect.x + rect.width];
2000
+ if (beyond < lo || near > hi) continue;
2001
+ far = Math.max(far, axis === "vertical" ? rect.x + rect.width : rect.y + rect.height);
2002
+ }
2003
+ return Number.isFinite(far) ? far + clearance : null;
2004
+ }
2005
+
2006
+ // src/flow-weighted-edge/flow-weighted-edge.tsx
2007
+ import { Fragment as Fragment5, jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
2008
+ var NO_NODES = [];
2009
+ var FALLBACK_WEAK = "#6085a1";
2010
+ var FALLBACK_STRONG = "#496d89";
2011
+ var BACK_EDGE_DASHARRAY = "6 4";
2012
+ var BACK_EDGE_OPACITY = 0.7;
2013
+ var BACK_EDGE_CLEARANCE = 40;
2014
+ var DEFAULT_BACK_EDGE_LABEL = "Back edge \u2014 runs against the process direction";
2015
+ function clamp01(t) {
2016
+ return Math.min(1, Math.max(0, t));
2017
+ }
2018
+ function hexToRgb(hex) {
2019
+ const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(hex);
2020
+ if (!m) return [0, 0, 0];
2021
+ return [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)];
2022
+ }
2023
+ function toHexByte(n) {
2024
+ return Math.round(Math.min(255, Math.max(0, n))).toString(16).padStart(2, "0");
2025
+ }
2026
+ function mixHex(a, b, t) {
2027
+ const [ar, ag, ab] = hexToRgb(a);
2028
+ const [br, bg, bb] = hexToRgb(b);
2029
+ const r = ar + (br - ar) * t;
2030
+ const g = ag + (bg - ag) * t;
2031
+ const bl = ab + (bb - ab) * t;
2032
+ return `#${toHexByte(r)}${toHexByte(g)}${toHexByte(bl)}`;
2033
+ }
2034
+ function resolveValueStrokeColor(value, domain) {
2035
+ if (value === void 0 || !domain) return void 0;
2036
+ const [lo, hi] = domain;
2037
+ const t = hi === lo ? 1 : clamp01((value - lo) / (hi - lo));
2038
+ const weak = resolveTokenColor("--flow-edge-weak", { fallback: FALLBACK_WEAK });
2039
+ const strong = resolveTokenColor("--flow-edge-strong", { fallback: FALLBACK_STRONG });
2040
+ return mixHex(weak, strong, t);
2041
+ }
2042
+ function FlowWeightedEdge({
2043
+ id,
2044
+ sourceX,
2045
+ sourceY,
2046
+ targetX,
2047
+ targetY,
2048
+ sourcePosition,
2049
+ targetPosition,
2050
+ markerEnd,
2051
+ style,
2052
+ selected,
2053
+ data
2054
+ }) {
2055
+ const edges = useEdges();
2056
+ const widthByEdgeId = useMemo3(
2057
+ () => computeEdgeWeightScale(edges),
2058
+ [edges]
2059
+ );
2060
+ const variant = data?.variant ?? "forward";
2061
+ const isBack = variant === "back";
2062
+ const nodes = useStore(
2063
+ useCallback5((s) => isBack ? s.nodes : NO_NODES, [isBack])
2064
+ );
2065
+ const pathType = data?.path ?? "bezier";
2066
+ const axis = targetPosition === Position7.Top || targetPosition === Position7.Bottom ? "vertical" : "horizontal";
2067
+ const rects = useMemo3(() => {
2068
+ if (!isBack) return [];
2069
+ const out = [];
2070
+ for (const node of nodes) {
2071
+ if (node.parentId) continue;
2072
+ const width = node.measured?.width ?? node.width;
2073
+ const height = node.measured?.height ?? node.height;
2074
+ if (!width || !height) continue;
2075
+ out.push({ x: node.position.x, y: node.position.y, width, height });
2076
+ }
2077
+ return out;
2078
+ }, [isBack, nodes]);
2079
+ const detour = isBack ? backEdgeDetour(
2080
+ rects,
2081
+ axis,
2082
+ axis === "vertical" ? [sourceY, targetY] : [sourceX, targetX],
2083
+ BACK_EDGE_CLEARANCE
2084
+ ) : null;
2085
+ const [edgePath, labelX, labelY] = isBack ? getSmoothStepPath({
2086
+ sourceX,
2087
+ sourceY,
2088
+ sourcePosition,
2089
+ targetX,
2090
+ targetY,
2091
+ targetPosition,
2092
+ offset: BACK_EDGE_CLEARANCE,
2093
+ ...detour === null ? {} : axis === "vertical" ? { centerX: detour } : { centerY: detour }
2094
+ }) : pathType === "smoothstep" ? getSmoothStepPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }) : getBezierPath5({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
2095
+ const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];
2096
+ const valueColor = useMemo3(
2097
+ () => resolveValueStrokeColor(data?.value, data?.valueDomain),
2098
+ [data?.value, data?.valueDomain]
2099
+ );
2100
+ const stroke = selected ? "var(--ring)" : valueColor ?? "var(--flow-edge)";
2101
+ const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;
2102
+ const edge = /* @__PURE__ */ jsx16(
2103
+ FlowEdgePath,
2104
+ {
2105
+ id,
2106
+ path: edgePath,
2107
+ markerEnd,
2108
+ "data-slot": "flow-weighted-edge",
2109
+ "data-variant": variant,
2110
+ "data-weight": data?.weight,
2111
+ "data-value": data?.value,
2112
+ stroke,
2113
+ strokeWidth,
2114
+ strokeDasharray: isBack ? BACK_EDGE_DASHARRAY : void 0,
2115
+ strokeOpacity: isBack ? BACK_EDGE_OPACITY : void 0,
2116
+ style
2117
+ }
2118
+ );
2119
+ return /* @__PURE__ */ jsxs13(Fragment5, { children: [
2120
+ isBack ? (
2121
+ // A `data-variant` is invisible to assistive technology, so the back
2122
+ // edge's meaning is also published as a named graphic. Only the back
2123
+ // variant is wrapped — a forward edge's DOM is unchanged from before
2124
+ // this prop existed.
2125
+ /* @__PURE__ */ jsx16("g", { role: "img", "aria-label": data?.variantLabel ?? DEFAULT_BACK_EDGE_LABEL, children: edge })
2126
+ ) : edge,
2127
+ /* @__PURE__ */ jsx16(
2128
+ EdgeLabelPill,
2129
+ {
2130
+ label: data?.label,
2131
+ secondaryLabel: data?.secondaryLabel,
2132
+ x: labelX,
2133
+ y: labelY,
2134
+ selected,
2135
+ ...data?.labelProps
2136
+ }
2137
+ )
2138
+ ] });
2139
+ }
2140
+
2141
+ // src/flow-weighted-edge/edge-aria.ts
2142
+ var DEFAULT_WEIGHT_LABEL = "weight";
2143
+ var DEFAULT_VALUE_LABEL = "value";
2144
+ function defaultNameOf(nodeId) {
2145
+ return nodeId;
2146
+ }
2147
+ function defaultFormatNumber(n) {
2148
+ return String(n);
2149
+ }
2150
+ function buildWeightedEdgeAriaLabel(edge, opts = {}) {
2151
+ if (edge.ariaLabel) return edge.ariaLabel;
2152
+ const { weight, value, label, secondaryLabel } = edge.data ?? {};
2153
+ if (weight === void 0 && value === void 0 && !label && !secondaryLabel) {
2154
+ return void 0;
2155
+ }
2156
+ const {
2157
+ weightLabel = DEFAULT_WEIGHT_LABEL,
2158
+ valueLabel = DEFAULT_VALUE_LABEL,
2159
+ nameOf = defaultNameOf,
2160
+ formatNumber = defaultFormatNumber
2161
+ } = opts;
2162
+ const parts = [`Edge from ${nameOf(edge.source)} to ${nameOf(edge.target)}`];
2163
+ if (weight !== void 0) parts.push(`${weightLabel} ${formatNumber(weight)}`);
2164
+ if (value !== void 0) parts.push(`${valueLabel} ${formatNumber(value)}`);
2165
+ const pillText = [label, secondaryLabel].filter(Boolean).join(" ");
2166
+ if (pillText) parts.push(pillText);
2167
+ return parts.join(", ");
2168
+ }
2169
+ function withWeightedEdgeAria(edges, opts) {
2170
+ return edges.map((edge) => {
2171
+ if (edge.ariaLabel) return edge;
2172
+ const ariaLabel = buildWeightedEdgeAriaLabel(edge, opts);
2173
+ return ariaLabel === void 0 ? edge : { ...edge, ariaLabel };
2174
+ });
2175
+ }
2176
+
2177
+ // src/flow-self-loop-edge/flow-self-loop-edge.tsx
2178
+ import { useMemo as useMemo4 } from "react";
2179
+ import { useEdges as useEdges2, useInternalNode as useInternalNode3 } from "@xyflow/react";
2180
+
2181
+ // src/flow-self-loop-edge/self-loop-geometry.ts
2182
+ var DEFAULT_LOOP_RADIUS = 28;
2183
+ var CONTROL_REACH_X = 1.2;
2184
+ var CONTROL_REACH_Y = 2.4;
2185
+ var APEX_FACTOR = 3 / 4 * CONTROL_REACH_Y;
2186
+ function selfLoopPath(anchor, loopRadius) {
2187
+ const cx = Number.isFinite(anchor.centerX) ? anchor.centerX : 0;
2188
+ const ay = Number.isFinite(anchor.topY) ? anchor.topY : 0;
2189
+ const r = Number.isFinite(loopRadius) && loopRadius > 0 ? loopRadius : DEFAULT_LOOP_RADIUS;
2190
+ const startX = cx + r;
2191
+ const endX = cx - r;
2192
+ const controlY = ay - r * CONTROL_REACH_Y;
2193
+ return {
2194
+ path: `M ${startX},${ay} C ${startX + r * CONTROL_REACH_X},${controlY} ${endX - r * CONTROL_REACH_X},${controlY} ${endX},${ay}`,
2195
+ labelX: cx,
2196
+ labelY: ay - r * APEX_FACTOR
2197
+ };
2198
+ }
2199
+ function outward(x, y, centerX, centerY, fallback) {
2200
+ const dx = x - centerX;
2201
+ const dy = y - centerY;
2202
+ const length = Math.hypot(dx, dy);
2203
+ if (!Number.isFinite(length) || length < 1e-6) return fallback;
2204
+ return [dx / length, dy / length];
2205
+ }
2206
+ function selfLoopHandleArc(anchor, loopRadius) {
2207
+ const num = (value) => Number.isFinite(value) ? value : 0;
2208
+ const sx = num(anchor.sourceX);
2209
+ const sy = num(anchor.sourceY);
2210
+ const tx = num(anchor.targetX);
2211
+ const ty = num(anchor.targetY);
2212
+ const cx = num(anchor.centerX);
2213
+ const cy = num(anchor.centerY);
2214
+ const width = Math.max(0, num(anchor.width));
2215
+ const height = Math.max(0, num(anchor.height));
2216
+ const r = Number.isFinite(loopRadius) && loopRadius > 0 ? loopRadius : DEFAULT_LOOP_RADIUS;
2217
+ const [osx, osy] = outward(sx, sy, cx, cy, [0, 1]);
2218
+ const [otx, oty] = outward(tx, ty, cx, cy, [-osx, -osy]);
2219
+ const lx = osy;
2220
+ const ly = -osx;
2221
+ const clearance = Math.abs(lx) * (width / 2) + Math.abs(ly) * (height / 2) + r;
2222
+ const reach = 4 / 3 * clearance;
2223
+ const c1x = sx + osx * r + lx * reach;
2224
+ const c1y = sy + osy * r + ly * reach;
2225
+ const c2x = tx + otx * r + lx * reach;
2226
+ const c2y = ty + oty * r + ly * reach;
2227
+ return {
2228
+ path: `M ${sx},${sy} C ${c1x},${c1y} ${c2x},${c2y} ${tx},${ty}`,
2229
+ // A cubic's midpoint is (P0 + 3·C1 + 3·C2 + P3) / 8 — the arc's apex, which `reach`
2230
+ // above places `loopRadius` clear of the card, so the label sits off the node.
2231
+ labelX: (sx + 3 * c1x + 3 * c2x + tx) / 8,
2232
+ labelY: (sy + 3 * c1y + 3 * c2y + ty) / 8
2233
+ };
2234
+ }
2235
+
2236
+ // src/flow-self-loop-edge/flow-self-loop-edge.tsx
2237
+ import { Fragment as Fragment6, jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
2238
+ function nodeName(data, fallback) {
2239
+ if (data && typeof data === "object" && "title" in data) {
2240
+ const title = data.title;
2241
+ if (typeof title === "string" && title.length > 0) return title;
2242
+ }
2243
+ return fallback;
2244
+ }
2245
+ function FlowSelfLoopEdge({
2246
+ id,
2247
+ source,
2248
+ sourceX,
2249
+ sourceY,
2250
+ targetX,
2251
+ targetY,
2252
+ markerEnd,
2253
+ style,
2254
+ selected,
2255
+ data
2256
+ }) {
2257
+ const edges = useEdges2();
2258
+ const widthByEdgeId = useMemo4(
2259
+ () => computeEdgeWeightScale(edges),
2260
+ [edges]
2261
+ );
2262
+ const node = useInternalNode3(source);
2263
+ const measuredWidth = node?.measured?.width;
2264
+ const measuredHeight = node?.measured?.height;
2265
+ const loopRadius = data?.loopRadius ?? DEFAULT_LOOP_RADIUS;
2266
+ const { path, labelX, labelY } = useMemo4(() => {
2267
+ if (node && measuredWidth && measuredHeight) {
2268
+ return selfLoopHandleArc(
2269
+ {
2270
+ sourceX,
2271
+ sourceY,
2272
+ targetX,
2273
+ targetY,
2274
+ centerX: node.internals.positionAbsolute.x + measuredWidth / 2,
2275
+ centerY: node.internals.positionAbsolute.y + measuredHeight / 2,
2276
+ width: measuredWidth,
2277
+ height: measuredHeight
2278
+ },
2279
+ loopRadius
2280
+ );
2281
+ }
2282
+ return selfLoopPath(
2283
+ { centerX: (sourceX + targetX) / 2, topY: Math.min(sourceY, targetY) },
2284
+ loopRadius
2285
+ );
2286
+ }, [node, measuredWidth, measuredHeight, sourceX, sourceY, targetX, targetY, loopRadius]);
2287
+ const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];
2288
+ const stroke = selected ? "var(--ring)" : "var(--flow-edge)";
2289
+ const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;
2290
+ const accessibleName = data?.loopLabel ?? `Self-loop on ${nodeName(node?.data, source)} \u2014 this step repeats`;
2291
+ return /* @__PURE__ */ jsxs14(Fragment6, { children: [
2292
+ /* @__PURE__ */ jsx17("g", { role: "img", "aria-label": accessibleName, children: /* @__PURE__ */ jsx17(
2293
+ FlowEdgePath,
2294
+ {
2295
+ id,
2296
+ path,
2297
+ markerEnd,
2298
+ "data-slot": "flow-self-loop-edge",
2299
+ stroke,
2300
+ strokeWidth,
2301
+ style: { fill: "none", ...style }
2302
+ }
2303
+ ) }),
2304
+ /* @__PURE__ */ jsx17(
2305
+ EdgeLabelPill,
2306
+ {
2307
+ label: data?.label,
2308
+ secondaryLabel: data?.secondaryLabel,
2309
+ x: labelX,
2310
+ y: labelY,
2311
+ selected,
2312
+ ...data?.labelProps
2313
+ }
2314
+ )
2315
+ ] });
2316
+ }
1531
2317
  export {
1532
2318
  Background2 as Background,
1533
2319
  CanvasShell,
1534
2320
  Controls,
2321
+ DEFAULT_EDGE_WIDTH_RANGE,
2322
+ DEFAULT_LOOP_RADIUS,
2323
+ EdgeLabelPill,
1535
2324
  FLOW_ALL_SIDE_HANDLES,
2325
+ FLOW_EDGE_FOCUS_CONTOUR_WIDTH,
2326
+ FLOW_EDGE_FOCUS_RING_WIDTH,
1536
2327
  FLOW_GROUP_NODE_TYPE,
2328
+ FLOW_HANDLE_ANCHOR_CLASS,
1537
2329
  FlowButtonEdge,
1538
2330
  FlowEdge,
2331
+ FlowEdgePath,
1539
2332
  FlowFloatingEdge,
1540
2333
  FlowGroupNode,
1541
2334
  FlowMiniMap,
1542
2335
  FlowNode,
1543
2336
  FlowPlaceholderNode,
2337
+ FlowSelfLoopEdge,
1544
2338
  FlowSmartEdge,
2339
+ FlowWeightedEdge,
1545
2340
  HANDLE_SIDES,
1546
2341
  HelperLines,
1547
2342
  InspectorPanel,
1548
2343
  Legend,
1549
2344
  MiniMap2 as MiniMap,
1550
2345
  Panel2 as Panel,
1551
- Position7 as Position,
2346
+ Position8 as Position,
1552
2347
  ReactFlow2 as ReactFlow,
1553
2348
  ReactFlowProvider2 as ReactFlowProvider,
1554
2349
  ZoomControls,
1555
2350
  addEdge,
2351
+ backEdgeDetour,
2352
+ buildWeightedEdgeAriaLabel,
1556
2353
  collapseGroup,
2354
+ computeEdgeWeightScale,
1557
2355
  expandGroup,
1558
2356
  getEdgeParams,
1559
2357
  getHelperLines,
@@ -1562,7 +2360,11 @@ export {
1562
2360
  isFlowGroupProxyEdge,
1563
2361
  layoutFlow,
1564
2362
  layoutGraph,
2363
+ pickClosestAnchors,
1565
2364
  pickClosestHandles,
2365
+ positionToSide,
2366
+ selfLoopHandleArc,
2367
+ selfLoopPath,
1566
2368
  sideToPosition,
1567
2369
  toggleGroupCollapsed,
1568
2370
  ungroup,
@@ -1572,6 +2374,7 @@ export {
1572
2374
  useFlowLayout,
1573
2375
  useHelperLines,
1574
2376
  useNodesState,
1575
- useReactFlow5 as useReactFlow
2377
+ useReactFlow6 as useReactFlow,
2378
+ withWeightedEdgeAria
1576
2379
  };
1577
2380
  //# sourceMappingURL=index.js.map