@elabs-ai/components-flow 4.2.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +2 -1
  2. package/dist/canvas-shell/canvas-shell.js +13 -0
  3. package/dist/canvas-shell/canvas-shell.js.map +1 -0
  4. package/dist/canvas-shell/clamped-fit-offset.js +8 -0
  5. package/dist/canvas-shell/clamped-fit-offset.js.map +1 -0
  6. package/dist/canvas-shell/index.js +14 -0
  7. package/dist/canvas-shell/index.js.map +1 -0
  8. package/dist/canvas-shell/use-measured-nodes.js +8 -0
  9. package/dist/canvas-shell/use-measured-nodes.js.map +1 -0
  10. package/dist/chunk-3BTU2VWZ.js +42 -0
  11. package/dist/chunk-3BTU2VWZ.js.map +1 -0
  12. package/dist/chunk-3LK6OOXE.js +2 -0
  13. package/dist/chunk-3LK6OOXE.js.map +1 -0
  14. package/dist/chunk-3M3FQAD4.js +105 -0
  15. package/dist/chunk-3M3FQAD4.js.map +1 -0
  16. package/dist/chunk-3Y5TCGC3.js +2 -0
  17. package/dist/chunk-3Y5TCGC3.js.map +1 -0
  18. package/dist/chunk-4VYHZM3S.js +108 -0
  19. package/dist/chunk-4VYHZM3S.js.map +1 -0
  20. package/dist/chunk-5TVVM4GR.js +45 -0
  21. package/dist/chunk-5TVVM4GR.js.map +1 -0
  22. package/dist/chunk-5XT73MQQ.js +2 -0
  23. package/dist/chunk-5XT73MQQ.js.map +1 -0
  24. package/dist/chunk-6LQD5Y5R.js +24 -0
  25. package/dist/chunk-6LQD5Y5R.js.map +1 -0
  26. package/dist/chunk-6SR26QY2.js +18 -0
  27. package/dist/chunk-6SR26QY2.js.map +1 -0
  28. package/dist/chunk-75JJK6BH.js +157 -0
  29. package/dist/chunk-75JJK6BH.js.map +1 -0
  30. package/dist/chunk-A66JBW4X.js +2 -0
  31. package/dist/chunk-A66JBW4X.js.map +1 -0
  32. package/dist/chunk-A7AFM3LI.js +72 -0
  33. package/dist/chunk-A7AFM3LI.js.map +1 -0
  34. package/dist/chunk-ADHLQF56.js +2 -0
  35. package/dist/chunk-ADHLQF56.js.map +1 -0
  36. package/dist/chunk-ATO4LXJF.js +75 -0
  37. package/dist/chunk-ATO4LXJF.js.map +1 -0
  38. package/dist/chunk-ATXVXTMZ.js +227 -0
  39. package/dist/chunk-ATXVXTMZ.js.map +1 -0
  40. package/dist/chunk-B6IUPZU7.js +2 -0
  41. package/dist/chunk-B6IUPZU7.js.map +1 -0
  42. package/dist/chunk-BJ7UMCD3.js +53 -0
  43. package/dist/chunk-BJ7UMCD3.js.map +1 -0
  44. package/dist/chunk-BWGYV4JZ.js +2 -0
  45. package/dist/chunk-BWGYV4JZ.js.map +1 -0
  46. package/dist/chunk-CWNO63LQ.js +109 -0
  47. package/dist/chunk-CWNO63LQ.js.map +1 -0
  48. package/dist/chunk-DHPUXKCZ.js +76 -0
  49. package/dist/chunk-DHPUXKCZ.js.map +1 -0
  50. package/dist/chunk-DNX7G3KA.js +185 -0
  51. package/dist/chunk-DNX7G3KA.js.map +1 -0
  52. package/dist/chunk-DNZRAJAQ.js +41 -0
  53. package/dist/chunk-DNZRAJAQ.js.map +1 -0
  54. package/dist/chunk-ELCLRVD4.js +2 -0
  55. package/dist/chunk-ELCLRVD4.js.map +1 -0
  56. package/dist/chunk-ELOADZSX.js +2 -0
  57. package/dist/chunk-ELOADZSX.js.map +1 -0
  58. package/dist/chunk-EUNV2MR5.js +21 -0
  59. package/dist/chunk-EUNV2MR5.js.map +1 -0
  60. package/dist/chunk-FAFQ5KLB.js +43 -0
  61. package/dist/chunk-FAFQ5KLB.js.map +1 -0
  62. package/dist/chunk-FIRO6Y3R.js +128 -0
  63. package/dist/chunk-FIRO6Y3R.js.map +1 -0
  64. package/dist/chunk-GE73WQUK.js +2 -0
  65. package/dist/chunk-GE73WQUK.js.map +1 -0
  66. package/dist/chunk-GIARAOU6.js +39 -0
  67. package/dist/chunk-GIARAOU6.js.map +1 -0
  68. package/dist/chunk-HTLTCCFA.js +73 -0
  69. package/dist/chunk-HTLTCCFA.js.map +1 -0
  70. package/dist/chunk-I53W3CFN.js +56 -0
  71. package/dist/chunk-I53W3CFN.js.map +1 -0
  72. package/dist/chunk-IJVFPUXO.js +2 -0
  73. package/dist/chunk-IJVFPUXO.js.map +1 -0
  74. package/dist/chunk-JUPMOISE.js +51 -0
  75. package/dist/chunk-JUPMOISE.js.map +1 -0
  76. package/dist/chunk-K5RPSVO6.js +167 -0
  77. package/dist/chunk-K5RPSVO6.js.map +1 -0
  78. package/dist/chunk-K6U27YJ6.js +78 -0
  79. package/dist/chunk-K6U27YJ6.js.map +1 -0
  80. package/dist/chunk-KDVMGMAK.js +2 -0
  81. package/dist/chunk-KDVMGMAK.js.map +1 -0
  82. package/dist/chunk-LQOZCOK3.js +74 -0
  83. package/dist/chunk-LQOZCOK3.js.map +1 -0
  84. package/dist/chunk-NAQWNX2A.js +102 -0
  85. package/dist/chunk-NAQWNX2A.js.map +1 -0
  86. package/dist/chunk-NVJVWGUS.js +152 -0
  87. package/dist/chunk-NVJVWGUS.js.map +1 -0
  88. package/dist/chunk-O2F3R3PV.js +2 -0
  89. package/dist/chunk-O2F3R3PV.js.map +1 -0
  90. package/dist/chunk-OMFQHMA6.js +2 -0
  91. package/dist/chunk-OMFQHMA6.js.map +1 -0
  92. package/dist/chunk-PE6JXVOX.js +2 -0
  93. package/dist/chunk-PE6JXVOX.js.map +1 -0
  94. package/dist/chunk-ROJEIIA4.js +52 -0
  95. package/dist/chunk-ROJEIIA4.js.map +1 -0
  96. package/dist/chunk-ROYAQKJA.js +9 -0
  97. package/dist/chunk-ROYAQKJA.js.map +1 -0
  98. package/dist/chunk-SHCZK4DO.js +45 -0
  99. package/dist/chunk-SHCZK4DO.js.map +1 -0
  100. package/dist/chunk-SXXJV6YJ.js +79 -0
  101. package/dist/chunk-SXXJV6YJ.js.map +1 -0
  102. package/dist/chunk-T7S7NJFD.js +287 -0
  103. package/dist/chunk-T7S7NJFD.js.map +1 -0
  104. package/dist/chunk-TEZ3OXAY.js +63 -0
  105. package/dist/chunk-TEZ3OXAY.js.map +1 -0
  106. package/dist/chunk-TGWPM2CG.js +199 -0
  107. package/dist/chunk-TGWPM2CG.js.map +1 -0
  108. package/dist/chunk-TSB6RE7L.js +2 -0
  109. package/dist/chunk-TSB6RE7L.js.map +1 -0
  110. package/dist/chunk-UPV2MT4O.js +2 -0
  111. package/dist/chunk-UPV2MT4O.js.map +1 -0
  112. package/dist/chunk-UZJZY4A5.js +2 -0
  113. package/dist/chunk-UZJZY4A5.js.map +1 -0
  114. package/dist/chunk-W3TQMUGX.js +2 -0
  115. package/dist/chunk-W3TQMUGX.js.map +1 -0
  116. package/dist/chunk-WN46RIOC.js +29 -0
  117. package/dist/chunk-WN46RIOC.js.map +1 -0
  118. package/dist/chunk-XZFE2YYH.js +70 -0
  119. package/dist/chunk-XZFE2YYH.js.map +1 -0
  120. package/dist/chunk-YG3KXTZW.js +2 -0
  121. package/dist/chunk-YG3KXTZW.js.map +1 -0
  122. package/dist/chunk-ZATVF42E.js +65 -0
  123. package/dist/chunk-ZATVF42E.js.map +1 -0
  124. package/dist/chunk-ZXZTLQIX.js +19 -0
  125. package/dist/chunk-ZXZTLQIX.js.map +1 -0
  126. package/dist/flow-button-edge/flow-button-edge.js +10 -0
  127. package/dist/flow-button-edge/flow-button-edge.js.map +1 -0
  128. package/dist/flow-button-edge/index.js +11 -0
  129. package/dist/flow-button-edge/index.js.map +1 -0
  130. package/dist/flow-edge/flow-edge.js +10 -0
  131. package/dist/flow-edge/flow-edge.js.map +1 -0
  132. package/dist/flow-edge/index.js +11 -0
  133. package/dist/flow-edge/index.js.map +1 -0
  134. package/dist/flow-edge-path/flow-edge-path.js +12 -0
  135. package/dist/flow-edge-path/flow-edge-path.js.map +1 -0
  136. package/dist/flow-edge-path/index.js +13 -0
  137. package/dist/flow-edge-path/index.js.map +1 -0
  138. package/dist/flow-edge-tokens/edge-path-sampler.js +10 -0
  139. package/dist/flow-edge-tokens/edge-path-sampler.js.map +1 -0
  140. package/dist/flow-edge-tokens/flow-edge-tokens.js +12 -0
  141. package/dist/flow-edge-tokens/flow-edge-tokens.js.map +1 -0
  142. package/dist/flow-edge-tokens/index.js +17 -0
  143. package/dist/flow-edge-tokens/index.js.map +1 -0
  144. package/dist/flow-floating-edge/floating-edge-geometry.js +8 -0
  145. package/dist/flow-floating-edge/floating-edge-geometry.js.map +1 -0
  146. package/dist/flow-floating-edge/flow-floating-edge.js +11 -0
  147. package/dist/flow-floating-edge/flow-floating-edge.js.map +1 -0
  148. package/dist/flow-floating-edge/index.js +15 -0
  149. package/dist/flow-floating-edge/index.js.map +1 -0
  150. package/dist/flow-group-node/flow-group-node.js +12 -0
  151. package/dist/flow-group-node/flow-group-node.js.map +1 -0
  152. package/dist/flow-group-node/index.js +13 -0
  153. package/dist/flow-group-node/index.js.map +1 -0
  154. package/dist/flow-handle/flow-handle-anchor.js +8 -0
  155. package/dist/flow-handle/flow-handle-anchor.js.map +1 -0
  156. package/dist/flow-handle/index.js +9 -0
  157. package/dist/flow-handle/index.js.map +1 -0
  158. package/dist/flow-layout/backbone.js +8 -0
  159. package/dist/flow-layout/backbone.js.map +1 -0
  160. package/dist/flow-layout/flow-layout.js +10 -0
  161. package/dist/flow-layout/flow-layout.js.map +1 -0
  162. package/dist/flow-layout/index.js +21 -0
  163. package/dist/flow-layout/index.js.map +1 -0
  164. package/dist/flow-layout/layout-flow-elk.js +9 -0
  165. package/dist/flow-layout/layout-flow-elk.js.map +1 -0
  166. package/dist/flow-layout/layout-graph.js +9 -0
  167. package/dist/flow-layout/layout-graph.js.map +1 -0
  168. package/dist/flow-layout/use-auto-layout.js +10 -0
  169. package/dist/flow-layout/use-auto-layout.js.map +1 -0
  170. package/dist/flow-layout/use-flow-layout.js +9 -0
  171. package/dist/flow-layout/use-flow-layout.js.map +1 -0
  172. package/dist/flow-mini-map/flow-mini-map.js +8 -0
  173. package/dist/flow-mini-map/flow-mini-map.js.map +1 -0
  174. package/dist/flow-mini-map/index.js +9 -0
  175. package/dist/flow-mini-map/index.js.map +1 -0
  176. package/dist/flow-node/flow-node.js +11 -0
  177. package/dist/flow-node/flow-node.js.map +1 -0
  178. package/dist/flow-node/index.js +12 -0
  179. package/dist/flow-node/index.js.map +1 -0
  180. package/dist/flow-placeholder-node/flow-placeholder-node.js +9 -0
  181. package/dist/flow-placeholder-node/flow-placeholder-node.js.map +1 -0
  182. package/dist/flow-placeholder-node/index.js +10 -0
  183. package/dist/flow-placeholder-node/index.js.map +1 -0
  184. package/dist/flow-self-loop-edge/flow-self-loop-edge.js +20 -0
  185. package/dist/flow-self-loop-edge/flow-self-loop-edge.js.map +1 -0
  186. package/dist/flow-self-loop-edge/index.js +28 -0
  187. package/dist/flow-self-loop-edge/index.js.map +1 -0
  188. package/dist/flow-self-loop-edge/self-loop-geometry.js +12 -0
  189. package/dist/flow-self-loop-edge/self-loop-geometry.js.map +1 -0
  190. package/dist/flow-smart-edge/flow-smart-edge.js +11 -0
  191. package/dist/flow-smart-edge/flow-smart-edge.js.map +1 -0
  192. package/dist/flow-smart-edge/index.js +25 -0
  193. package/dist/flow-smart-edge/index.js.map +1 -0
  194. package/dist/flow-smart-edge/smart-edge-geometry.js +18 -0
  195. package/dist/flow-smart-edge/smart-edge-geometry.js.map +1 -0
  196. package/dist/flow-weighted-edge/back-edge-geometry.js +8 -0
  197. package/dist/flow-weighted-edge/back-edge-geometry.js.map +1 -0
  198. package/dist/flow-weighted-edge/edge-aria.js +10 -0
  199. package/dist/flow-weighted-edge/edge-aria.js.map +1 -0
  200. package/dist/flow-weighted-edge/edge-label-pill.js +8 -0
  201. package/dist/flow-weighted-edge/edge-label-pill.js.map +1 -0
  202. package/dist/flow-weighted-edge/flow-weighted-edge.js +16 -0
  203. package/dist/flow-weighted-edge/flow-weighted-edge.js.map +1 -0
  204. package/dist/flow-weighted-edge/index.js +34 -0
  205. package/dist/flow-weighted-edge/index.js.map +1 -0
  206. package/dist/flow-weighted-edge/weight-scale.js +10 -0
  207. package/dist/flow-weighted-edge/weight-scale.js.map +1 -0
  208. package/dist/helper-lines/get-helper-lines.js +8 -0
  209. package/dist/helper-lines/get-helper-lines.js.map +1 -0
  210. package/dist/helper-lines/helper-lines.js +9 -0
  211. package/dist/helper-lines/helper-lines.js.map +1 -0
  212. package/dist/helper-lines/index.js +17 -0
  213. package/dist/helper-lines/index.js.map +1 -0
  214. package/dist/helper-lines/use-helper-lines.js +10 -0
  215. package/dist/helper-lines/use-helper-lines.js.map +1 -0
  216. package/dist/index.d.ts +236 -1
  217. package/dist/index.js +167 -2311
  218. package/dist/index.js.map +1 -1
  219. package/dist/inspector-panel/index.js +9 -0
  220. package/dist/inspector-panel/index.js.map +1 -0
  221. package/dist/inspector-panel/inspector-panel.js +8 -0
  222. package/dist/inspector-panel/inspector-panel.js.map +1 -0
  223. package/dist/legend/index.js +10 -0
  224. package/dist/legend/index.js.map +1 -0
  225. package/dist/legend/legend.js +9 -0
  226. package/dist/legend/legend.js.map +1 -0
  227. package/dist/use-flow-groups/group-operations.js +20 -0
  228. package/dist/use-flow-groups/group-operations.js.map +1 -0
  229. package/dist/use-flow-groups/index.js +25 -0
  230. package/dist/use-flow-groups/index.js.map +1 -0
  231. package/dist/use-flow-groups/use-flow-groups.js +9 -0
  232. package/dist/use-flow-groups/use-flow-groups.js.map +1 -0
  233. package/dist/zoom-controls/index.js +9 -0
  234. package/dist/zoom-controls/index.js.map +1 -0
  235. package/dist/zoom-controls/zoom-controls.js +8 -0
  236. package/dist/zoom-controls/zoom-controls.js.map +1 -0
  237. package/package.json +13 -6
  238. package/src/canvas-shell/canvas-shell.stories.tsx +1 -0
  239. package/src/canvas-shell/canvas-shell.tsx +51 -4
  240. package/src/canvas-shell/clamped-fit-offset.test.ts +25 -0
  241. package/src/canvas-shell/clamped-fit-offset.ts +30 -0
  242. package/src/custom-nodes/custom-nodes.stories.tsx +718 -0
  243. package/src/flow-edge-tokens/edge-path-sampler.test.ts +108 -0
  244. package/src/flow-edge-tokens/edge-path-sampler.ts +362 -0
  245. package/src/flow-edge-tokens/flow-edge-tokens.stories.tsx +312 -0
  246. package/src/flow-edge-tokens/flow-edge-tokens.test.tsx +115 -0
  247. package/src/flow-edge-tokens/flow-edge-tokens.tsx +94 -0
  248. package/src/flow-edge-tokens/index.ts +12 -0
  249. package/src/flow-layout/backbone.test.ts +88 -0
  250. package/src/flow-layout/backbone.ts +58 -0
  251. package/src/flow-layout/layout-flow-elk.stories.tsx +154 -0
  252. package/src/flow-layout/layout-flow-elk.test.ts +152 -0
  253. package/src/flow-layout/layout-flow-elk.ts +399 -0
  254. package/src/flow-mini-map/flow-mini-map.stories.tsx +10 -7
  255. package/src/flow-node/flow-node.stories.tsx +40 -1
  256. package/src/flow-self-loop-edge/flow-self-loop-edge.test.tsx +38 -0
  257. package/src/flow-self-loop-edge/flow-self-loop-edge.tsx +7 -0
  258. package/src/flow-weighted-edge/edge-label-pill.test.tsx +20 -0
  259. package/src/flow-weighted-edge/flow-weighted-edge.stories.tsx +1 -0
  260. package/src/flow-weighted-edge/flow-weighted-edge.test.tsx +40 -0
  261. package/src/flow-weighted-edge/flow-weighted-edge.tsx +7 -0
  262. package/src/index.ts +13 -0
  263. package/src/templates-flow-workspace.stories.tsx +7 -2
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-B6IUPZU7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ // src/helper-lines/helper-lines.tsx
4
+ import { forwardRef } from "react";
5
+ import { useViewport } from "@xyflow/react";
6
+ import { cn } from "@elabs-ai/components-ui/lib/cn";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var HelperLines = forwardRef(function HelperLines2({ horizontal, vertical, className, ...props }, ref) {
9
+ const { x, y, zoom } = useViewport();
10
+ if (horizontal == null && vertical == null) return null;
11
+ const screenY = horizontal == null ? null : y + horizontal * zoom;
12
+ const screenX = vertical == null ? null : x + vertical * zoom;
13
+ return /* @__PURE__ */ jsxs(
14
+ "svg",
15
+ {
16
+ ref,
17
+ "aria-hidden": "true",
18
+ width: "100%",
19
+ height: "100%",
20
+ className: cn("pointer-events-none absolute inset-0 size-full", className),
21
+ ...props,
22
+ children: [
23
+ screenY != null ? /* @__PURE__ */ jsx(
24
+ "line",
25
+ {
26
+ x1: 0,
27
+ x2: "100%",
28
+ y1: screenY,
29
+ y2: screenY,
30
+ stroke: "var(--flow-helper-line, var(--primary))",
31
+ strokeWidth: 1
32
+ }
33
+ ) : null,
34
+ screenX != null ? /* @__PURE__ */ jsx(
35
+ "line",
36
+ {
37
+ x1: screenX,
38
+ x2: screenX,
39
+ y1: 0,
40
+ y2: "100%",
41
+ stroke: "var(--flow-helper-line, var(--primary))",
42
+ strokeWidth: 1
43
+ }
44
+ ) : null
45
+ ]
46
+ }
47
+ );
48
+ });
49
+
50
+ export {
51
+ HelperLines
52
+ };
53
+ //# sourceMappingURL=chunk-BJ7UMCD3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helper-lines/helper-lines.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, type SVGAttributes } from \"react\";\nimport { useViewport } from \"@xyflow/react\";\nimport { cn } from \"@elabs-ai/components-ui/lib/cn\";\n\nexport interface HelperLinesProps extends SVGAttributes<SVGSVGElement> {\n /** Absolute flow-y of the horizontal guide (from `useHelperLines`). */\n horizontal?: number;\n /** Absolute flow-x of the vertical guide (from `useHelperLines`). */\n vertical?: number;\n}\n\n/**\n * Decorative alignment-guide overlay. Draws a full-width horizontal line at\n * `horizontal` and/or a full-height vertical line at `vertical`, coloured with\n * the `--flow-helper-line` token. It reads the live viewport transform\n * (zoom + pan) so the guides sit on the real alignment position when the canvas\n * is zoomed or panned.\n *\n * Render as a child of `<CanvasShell>` / `<ReactFlow>` (it must be inside the\n * flow context to read the viewport). Purely decorative — hidden from AT.\n */\nexport const HelperLines = forwardRef<SVGSVGElement, HelperLinesProps>(function HelperLines(\n { horizontal, vertical, className, ...props },\n ref,\n) {\n const { x, y, zoom } = useViewport();\n\n if (horizontal == null && vertical == null) return null;\n\n // flow coordinate → pane pixel: coord * zoom + pan offset.\n const screenY = horizontal == null ? null : y + horizontal * zoom;\n const screenX = vertical == null ? null : x + vertical * zoom;\n\n return (\n <svg\n ref={ref}\n aria-hidden=\"true\"\n width=\"100%\"\n height=\"100%\"\n className={cn(\"pointer-events-none absolute inset-0 size-full\", className)}\n {...props}\n >\n {screenY != null ? (\n <line\n x1={0}\n x2=\"100%\"\n y1={screenY}\n y2={screenY}\n stroke=\"var(--flow-helper-line, var(--primary))\"\n strokeWidth={1}\n />\n ) : null}\n {screenX != null ? (\n <line\n x1={screenX}\n x2={screenX}\n y1={0}\n y2=\"100%\"\n stroke=\"var(--flow-helper-line, var(--primary))\"\n strokeWidth={1}\n />\n ) : null}\n </svg>\n );\n});\n"],"mappings":";;;AAEA,SAAS,kBAAsC;AAC/C,SAAS,mBAAmB;AAC5B,SAAS,UAAU;AAgCf,SASI,KATJ;AAbG,IAAM,cAAc,WAA4C,SAASA,aAC9E,EAAE,YAAY,UAAU,WAAW,GAAG,MAAM,GAC5C,KACA;AACA,QAAM,EAAE,GAAG,GAAG,KAAK,IAAI,YAAY;AAEnC,MAAI,cAAc,QAAQ,YAAY,KAAM,QAAO;AAGnD,QAAM,UAAU,cAAc,OAAO,OAAO,IAAI,aAAa;AAC7D,QAAM,UAAU,YAAY,OAAO,OAAO,IAAI,WAAW;AAEzD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,eAAY;AAAA,MACZ,OAAM;AAAA,MACN,QAAO;AAAA,MACP,WAAW,GAAG,kDAAkD,SAAS;AAAA,MACxE,GAAG;AAAA,MAEH;AAAA,mBAAW,OACV;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,IAAG;AAAA,YACH,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,QAAO;AAAA,YACP,aAAa;AAAA;AAAA,QACf,IACE;AAAA,QACH,WAAW,OACV;AAAA,UAAC;AAAA;AAAA,YACC,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAI;AAAA,YACJ,IAAG;AAAA,YACH,QAAO;AAAA,YACP,aAAa;AAAA;AAAA,QACf,IACE;AAAA;AAAA;AAAA,EACN;AAEJ,CAAC;","names":["HelperLines"]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-BWGYV4JZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,109 @@
1
+ "use client";
2
+ import {
3
+ EdgeLabelPill
4
+ } from "./chunk-I53W3CFN.js";
5
+ import {
6
+ DEFAULT_EDGE_WIDTH_RANGE,
7
+ computeEdgeWeightScale
8
+ } from "./chunk-3BTU2VWZ.js";
9
+ import {
10
+ DEFAULT_LOOP_RADIUS,
11
+ selfLoopHandleArc,
12
+ selfLoopPath
13
+ } from "./chunk-TEZ3OXAY.js";
14
+ import {
15
+ FlowEdgeTokens
16
+ } from "./chunk-ROJEIIA4.js";
17
+ import {
18
+ FlowEdgePath
19
+ } from "./chunk-SXXJV6YJ.js";
20
+
21
+ // src/flow-self-loop-edge/flow-self-loop-edge.tsx
22
+ import { useMemo } from "react";
23
+ import { useEdges, useInternalNode } from "@xyflow/react";
24
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
25
+ function nodeName(data, fallback) {
26
+ if (data && typeof data === "object" && "title" in data) {
27
+ const title = data.title;
28
+ if (typeof title === "string" && title.length > 0) return title;
29
+ }
30
+ return fallback;
31
+ }
32
+ function FlowSelfLoopEdge({
33
+ id,
34
+ source,
35
+ sourceX,
36
+ sourceY,
37
+ targetX,
38
+ targetY,
39
+ markerEnd,
40
+ style,
41
+ selected,
42
+ data
43
+ }) {
44
+ const edges = useEdges();
45
+ const widthByEdgeId = useMemo(
46
+ () => computeEdgeWeightScale(edges),
47
+ [edges]
48
+ );
49
+ const node = useInternalNode(source);
50
+ const measuredWidth = node?.measured?.width;
51
+ const measuredHeight = node?.measured?.height;
52
+ const loopRadius = data?.loopRadius ?? DEFAULT_LOOP_RADIUS;
53
+ const { path, labelX, labelY } = useMemo(() => {
54
+ if (node && measuredWidth && measuredHeight) {
55
+ return selfLoopHandleArc(
56
+ {
57
+ sourceX,
58
+ sourceY,
59
+ targetX,
60
+ targetY,
61
+ centerX: node.internals.positionAbsolute.x + measuredWidth / 2,
62
+ centerY: node.internals.positionAbsolute.y + measuredHeight / 2,
63
+ width: measuredWidth,
64
+ height: measuredHeight
65
+ },
66
+ loopRadius
67
+ );
68
+ }
69
+ return selfLoopPath(
70
+ { centerX: (sourceX + targetX) / 2, topY: Math.min(sourceY, targetY) },
71
+ loopRadius
72
+ );
73
+ }, [node, measuredWidth, measuredHeight, sourceX, sourceY, targetX, targetY, loopRadius]);
74
+ const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];
75
+ const stroke = selected ? "var(--ring)" : "var(--flow-edge)";
76
+ const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;
77
+ const accessibleName = data?.loopLabel ?? `Self-loop on ${nodeName(node?.data, source)} \u2014 this step repeats`;
78
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
79
+ /* @__PURE__ */ jsx("g", { role: "img", "aria-label": accessibleName, children: /* @__PURE__ */ jsx(
80
+ FlowEdgePath,
81
+ {
82
+ id,
83
+ path,
84
+ markerEnd,
85
+ "data-slot": "flow-self-loop-edge",
86
+ stroke,
87
+ strokeWidth,
88
+ style: { fill: "none", ...style }
89
+ }
90
+ ) }),
91
+ data?.tokens?.length ? /* @__PURE__ */ jsx(FlowEdgeTokens, { path, tokens: data.tokens }) : null,
92
+ /* @__PURE__ */ jsx(
93
+ EdgeLabelPill,
94
+ {
95
+ label: data?.label,
96
+ secondaryLabel: data?.secondaryLabel,
97
+ x: labelX,
98
+ y: labelY,
99
+ selected,
100
+ ...data?.labelProps
101
+ }
102
+ )
103
+ ] });
104
+ }
105
+
106
+ export {
107
+ FlowSelfLoopEdge
108
+ };
109
+ //# sourceMappingURL=chunk-CWNO63LQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/flow-self-loop-edge/flow-self-loop-edge.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useEdges, useInternalNode, type Edge, type EdgeProps } from \"@xyflow/react\";\nimport { FlowEdgePath } from \"../flow-edge-path\";\nimport { FlowEdgeTokens, type FlowEdgeToken } from \"../flow-edge-tokens\";\nimport {\n computeEdgeWeightScale,\n DEFAULT_EDGE_WIDTH_RANGE,\n EdgeLabelPill,\n type EdgeLabelPillProps,\n type WeightedEdgeLike,\n} from \"../flow-weighted-edge\";\nimport { DEFAULT_LOOP_RADIUS, selfLoopHandleArc, selfLoopPath } from \"./self-loop-geometry\";\n\nexport interface FlowSelfLoopEdgeData extends Record<string, unknown> {\n /**\n * Frequency/volume this loop carries. Scaled into stroke width by the SAME\n * `computeEdgeWeightScale` domain as `FlowWeightedEdge`, so a loop's weight\n * is directly comparable with the forward edges around it.\n */\n weight?: number;\n /** Edges sharing a `scaleGroup` share one min-max width domain. @default all edges in the flow */\n scaleGroup?: string;\n /** Primary edge-label-pill text, e.g. a repeat count. */\n label?: string;\n /** Secondary edge-label-pill text, e.g. an average duration. */\n secondaryLabel?: string;\n /** Radius of the arc, in px. @default 28 */\n loopRadius?: number;\n /**\n * Overrides the accessible name given to the loop's graphic. Defaults to\n * \"Self-loop on <node> — this step repeats\".\n */\n loopLabel?: string;\n /**\n * Passed straight through to the rendered `EdgeLabelPill`'s `className`/`...props`\n * (see `EdgeLabelPillProps`) — the seam a composing package (e.g.\n * `@elabs-ai/components-process`'s `ProcessTransitionEdge`) uses to reach the pill's\n * own root button from outside this component, without a new semantic prop here.\n */\n labelProps?: Omit<EdgeLabelPillProps, \"label\" | \"secondaryLabel\" | \"x\" | \"y\" | \"selected\">;\n /**\n * Markers travelling along the loop's computed arc (see `FlowEdgeTokens`). The parent\n * drives each token's `progress`; omitted or empty renders exactly the edge without them.\n */\n tokens?: readonly FlowEdgeToken[];\n}\n\nexport type BrandFlowSelfLoopEdge = Edge<FlowSelfLoopEdgeData, \"self-loop\">;\n\n/** Node `data` shapes a title can be read from — `FlowNode`'s is `{ title }`. */\nfunction nodeName(data: unknown, fallback: string): string {\n if (data && typeof data === \"object\" && \"title\" in data) {\n const title = (data as { title?: unknown }).title;\n if (typeof title === \"string\" && title.length > 0) return title;\n }\n return fallback;\n}\n\n/**\n * Branded self-loop edge: an edge whose `source === target` — the \"this step\n * repeated\" signal of a process map. Register it in\n * `edgeTypes={{ \"self-loop\": FlowSelfLoopEdge }}` and create edges with\n * `type: \"self-loop\"` and `data: FlowSelfLoopEdgeData`.\n *\n * dagre cannot lay a self-loop out, so `layoutFlow` withholds them from the\n * graph entirely and reports their ids in `selfLoops` — this component draws\n * the arc itself, from the node's own live geometry (`useInternalNode`).\n *\n * ## It terminates ON the handle dots, like every other brand edge\n *\n * The two handle points of a self-loop sit on OPPOSITE sides of one node, so they\n * describe no useful straight line — which is why this edge used to ignore them and\n * arc over the node's top edge instead, with its feet on bare border a loop-radius\n * away from the nearest dot. Measured on the process map, that read as a detached\n * arc floating above the card: 24 px clear of any dot in a top-to-bottom layout, and\n * 69 px in a left-to-right one, where the loop stayed stubbornly on TOP while the\n * flow (and the handles) had moved to the sides. Both are the \"an edge terminates on\n * a handle dot\" rule being broken, just by a component that had declared itself\n * exempt.\n *\n * `selfLoopHandleArc` keeps the loop a SHAPE — it just bulges clear of the node on\n * the side a quarter turn from the source handle's own normal, so it lassos down the\n * right in a top-to-bottom layout and arcs over the top in a left-to-right one,\n * without this component knowing which direction is in force. Before the node is\n * measured there is nothing to clear, so it falls back to the node-box arc\n * (`selfLoopPath`) rather than to `NaN`.\n *\n * The loop is distinguished from a forward edge by its SHAPE, not by colour —\n * a closed arc above the node, legible in greyscale and in every theme — and\n * publishes that meaning as a real accessible name, because a `data-slot` is\n * invisible to assistive technology. Its label is an `EdgeLabelPill` at the\n * arc's apex, a genuine keyboard tab stop with a visible focus ring.\n *\n * Stroke width comes from the same `computeEdgeWeightScale` domain\n * `FlowWeightedEdge` uses, so a loop weighted 8 reads as thick as a forward\n * edge weighted 8. Nothing animates, so there is no motion to reduce.\n *\n * The arc is drawn through `FlowEdgePath`, so it inherits the shared keyboard\n * focus indicator (#286) rather than having to opt into it.\n */\nexport function FlowSelfLoopEdge({\n id,\n source,\n sourceX,\n sourceY,\n targetX,\n targetY,\n markerEnd,\n style,\n selected,\n data,\n}: EdgeProps<BrandFlowSelfLoopEdge>) {\n const edges = useEdges();\n const widthByEdgeId = useMemo(\n () => computeEdgeWeightScale(edges as unknown as WeightedEdgeLike[]),\n [edges],\n );\n const node = useInternalNode(source);\n\n const measuredWidth = node?.measured?.width;\n const measuredHeight = node?.measured?.height;\n const loopRadius = data?.loopRadius ?? DEFAULT_LOOP_RADIUS;\n\n const { path, labelX, labelY } = useMemo(() => {\n if (node && measuredWidth && measuredHeight) {\n return selfLoopHandleArc(\n {\n sourceX,\n sourceY,\n targetX,\n targetY,\n centerX: node.internals.positionAbsolute.x + measuredWidth / 2,\n centerY: node.internals.positionAbsolute.y + measuredHeight / 2,\n width: measuredWidth,\n height: measuredHeight,\n },\n loopRadius,\n );\n }\n // Before measurement lands there is no box to clear, and the two handle points\n // still bracket the node — draw the plain arc above them.\n return selfLoopPath(\n { centerX: (sourceX + targetX) / 2, topY: Math.min(sourceY, targetY) },\n loopRadius,\n );\n }, [node, measuredWidth, measuredHeight, sourceX, sourceY, targetX, targetY, loopRadius]);\n\n const scaledWidth = widthByEdgeId.get(id) ?? DEFAULT_EDGE_WIDTH_RANGE[0];\n const stroke = selected ? \"var(--ring)\" : \"var(--flow-edge)\";\n const strokeWidth = selected ? scaledWidth + 1.5 : scaledWidth;\n\n const accessibleName =\n data?.loopLabel ?? `Self-loop on ${nodeName(node?.data, source)} — this step repeats`;\n\n return (\n <>\n <g role=\"img\" aria-label={accessibleName}>\n <FlowEdgePath\n id={id}\n path={path}\n markerEnd={markerEnd}\n data-slot=\"flow-self-loop-edge\"\n stroke={stroke}\n strokeWidth={strokeWidth}\n style={{ fill: \"none\", ...style }}\n />\n </g>\n {data?.tokens?.length ? <FlowEdgeTokens path={path} tokens={data.tokens} /> : null}\n <EdgeLabelPill\n label={data?.label}\n secondaryLabel={data?.secondaryLabel}\n x={labelX}\n y={labelY}\n selected={selected}\n {...data?.labelProps}\n />\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,eAAe;AACxB,SAAS,UAAU,uBAAkD;AA0JjE,mBAEI,KAFJ;AAzGJ,SAAS,SAAS,MAAe,UAA0B;AACzD,MAAI,QAAQ,OAAO,SAAS,YAAY,WAAW,MAAM;AACvD,UAAM,QAAS,KAA6B;AAC5C,QAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;AAAA,EAC5D;AACA,SAAO;AACT;AA4CO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAqC;AACnC,QAAM,QAAQ,SAAS;AACvB,QAAM,gBAAgB;AAAA,IACpB,MAAM,uBAAuB,KAAsC;AAAA,IACnE,CAAC,KAAK;AAAA,EACR;AACA,QAAM,OAAO,gBAAgB,MAAM;AAEnC,QAAM,gBAAgB,MAAM,UAAU;AACtC,QAAM,iBAAiB,MAAM,UAAU;AACvC,QAAM,aAAa,MAAM,cAAc;AAEvC,QAAM,EAAE,MAAM,QAAQ,OAAO,IAAI,QAAQ,MAAM;AAC7C,QAAI,QAAQ,iBAAiB,gBAAgB;AAC3C,aAAO;AAAA,QACL;AAAA,UACE;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA,SAAS,KAAK,UAAU,iBAAiB,IAAI,gBAAgB;AAAA,UAC7D,SAAS,KAAK,UAAU,iBAAiB,IAAI,iBAAiB;AAAA,UAC9D,OAAO;AAAA,UACP,QAAQ;AAAA,QACV;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,WAAO;AAAA,MACL,EAAE,UAAU,UAAU,WAAW,GAAG,MAAM,KAAK,IAAI,SAAS,OAAO,EAAE;AAAA,MACrE;AAAA,IACF;AAAA,EACF,GAAG,CAAC,MAAM,eAAe,gBAAgB,SAAS,SAAS,SAAS,SAAS,UAAU,CAAC;AAExF,QAAM,cAAc,cAAc,IAAI,EAAE,KAAK,yBAAyB,CAAC;AACvE,QAAM,SAAS,WAAW,gBAAgB;AAC1C,QAAM,cAAc,WAAW,cAAc,MAAM;AAEnD,QAAM,iBACJ,MAAM,aAAa,gBAAgB,SAAS,MAAM,MAAM,MAAM,CAAC;AAEjE,SACE,iCACE;AAAA,wBAAC,OAAE,MAAK,OAAM,cAAY,gBACxB;AAAA,MAAC;AAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA,aAAU;AAAA,QACV;AAAA,QACA;AAAA,QACA,OAAO,EAAE,MAAM,QAAQ,GAAG,MAAM;AAAA;AAAA,IAClC,GACF;AAAA,IACC,MAAM,QAAQ,SAAS,oBAAC,kBAAe,MAAY,QAAQ,KAAK,QAAQ,IAAK;AAAA,IAC9E;AAAA,MAAC;AAAA;AAAA,QACC,OAAO,MAAM;AAAA,QACb,gBAAgB,MAAM;AAAA,QACtB,GAAG;AAAA,QACH,GAAG;AAAA,QACH;AAAA,QACC,GAAG,MAAM;AAAA;AAAA,IACZ;AAAA,KACF;AAEJ;","names":[]}
@@ -0,0 +1,76 @@
1
+ "use client";
2
+
3
+ // src/flow-smart-edge/smart-edge-geometry.ts
4
+ import { Position } from "@xyflow/react";
5
+ var HANDLE_SIDES = ["top", "right", "bottom", "left"];
6
+ var sideToPosition = {
7
+ top: Position.Top,
8
+ right: Position.Right,
9
+ bottom: Position.Bottom,
10
+ left: Position.Left
11
+ };
12
+ var positionToSide = {
13
+ [Position.Top]: "top",
14
+ [Position.Right]: "right",
15
+ [Position.Bottom]: "bottom",
16
+ [Position.Left]: "left"
17
+ };
18
+ function pickClosestAnchors(sources, targets) {
19
+ if (!sources.length || !targets.length) return void 0;
20
+ let best;
21
+ let bestDist = Infinity;
22
+ for (const source of sources) {
23
+ for (const target of targets) {
24
+ const dx = target.x - source.x;
25
+ const dy = target.y - source.y;
26
+ const dist = dx * dx + dy * dy;
27
+ if (dist < bestDist) {
28
+ bestDist = dist;
29
+ best = { source, target };
30
+ }
31
+ }
32
+ }
33
+ return best;
34
+ }
35
+ function handlePoint(rect, side) {
36
+ switch (side) {
37
+ case "top":
38
+ return { x: rect.x + rect.width / 2, y: rect.y };
39
+ case "bottom":
40
+ return { x: rect.x + rect.width / 2, y: rect.y + rect.height };
41
+ case "left":
42
+ return { x: rect.x, y: rect.y + rect.height / 2 };
43
+ case "right":
44
+ return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };
45
+ }
46
+ }
47
+ function pickClosestHandles(source, sourceSides, target, targetSides) {
48
+ const srcSides = sourceSides.length ? sourceSides : HANDLE_SIDES;
49
+ const tgtSides = targetSides.length ? targetSides : HANDLE_SIDES;
50
+ let best;
51
+ let bestDist = Infinity;
52
+ for (const s of srcSides) {
53
+ const sp = handlePoint(source, s);
54
+ for (const t of tgtSides) {
55
+ const tp = handlePoint(target, t);
56
+ const dx = tp.x - sp.x;
57
+ const dy = tp.y - sp.y;
58
+ const dist = dx * dx + dy * dy;
59
+ if (dist < bestDist) {
60
+ bestDist = dist;
61
+ best = { sourceSide: s, targetSide: t, sx: sp.x, sy: sp.y, tx: tp.x, ty: tp.y };
62
+ }
63
+ }
64
+ }
65
+ return best;
66
+ }
67
+
68
+ export {
69
+ HANDLE_SIDES,
70
+ sideToPosition,
71
+ positionToSide,
72
+ pickClosestAnchors,
73
+ handlePoint,
74
+ pickClosestHandles
75
+ };
76
+ //# sourceMappingURL=chunk-DHPUXKCZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/flow-smart-edge/smart-edge-geometry.ts"],"sourcesContent":["import { Position } from \"@xyflow/react\";\nimport type { FlowHandleSide } from \"../flow-node\";\n\n/** Axis-aligned node rectangle in absolute flow coordinates. */\nexport interface NodeRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/** All four candidate handle sides, in a stable order. */\nexport const HANDLE_SIDES: FlowHandleSide[] = [\"top\", \"right\", \"bottom\", \"left\"];\n\n/** Maps a handle side to the React Flow `Position` used for bezier control. */\nexport const sideToPosition: Record<FlowHandleSide, Position> = {\n top: Position.Top,\n right: Position.Right,\n bottom: Position.Bottom,\n left: Position.Left,\n};\n\n/** Maps a React Flow `Position` back to the handle side it names. */\nexport const positionToSide: Record<Position, FlowHandleSide> = {\n [Position.Top]: \"top\",\n [Position.Right]: \"right\",\n [Position.Bottom]: \"bottom\",\n [Position.Left]: \"left\",\n};\n\n/**\n * One candidate connection point: the **measured centre of a rendered handle\n * dot**, in absolute flow coordinates, plus the side it sits on.\n *\n * This is the unit `FlowSmartEdge` routes between. Anchoring on a measured\n * handle — rather than on a point derived from the node's rectangle — is what\n * guarantees the drawn path terminates exactly on the dot the user sees,\n * whatever the handle's size, offset or CSS. See {@link pickClosestAnchors}.\n */\nexport interface HandleAnchor {\n /** The handle's `id`, when it has one (`FlowNode` uses the side name). */\n id: string | null;\n /** Absolute x of the handle dot's centre. */\n x: number;\n /** Absolute y of the handle dot's centre. */\n y: number;\n /** The node side the handle sits on — the bezier's control direction. */\n side: FlowHandleSide;\n}\n\n/** The chosen source/target anchor pair. */\nexport interface ClosestAnchors {\n source: HandleAnchor;\n target: HandleAnchor;\n}\n\n/**\n * Picks the source/target pair of **rendered handles** with the shortest\n * straight-line distance between them.\n *\n * Returns `undefined` when either side has no candidates, so the caller can\n * fall back to rectangle geometry for a node whose handles have not been\n * measured yet (React Flow populates `handleBounds` on its first measurement\n * pass; before that there is nothing to anchor to).\n */\nexport function pickClosestAnchors(\n sources: HandleAnchor[],\n targets: HandleAnchor[],\n): ClosestAnchors | undefined {\n if (!sources.length || !targets.length) return undefined;\n\n let best: ClosestAnchors | undefined;\n let bestDist = Infinity;\n\n for (const source of sources) {\n for (const target of targets) {\n const dx = target.x - source.x;\n const dy = target.y - source.y;\n const dist = dx * dx + dy * dy;\n if (dist < bestDist) {\n bestDist = dist;\n best = { source, target };\n }\n }\n }\n\n return best;\n}\n\n/** Absolute coordinate of a handle on the given side (the side's midpoint). */\nexport function handlePoint(rect: NodeRect, side: FlowHandleSide): { x: number; y: number } {\n switch (side) {\n case \"top\":\n return { x: rect.x + rect.width / 2, y: rect.y };\n case \"bottom\":\n return { x: rect.x + rect.width / 2, y: rect.y + rect.height };\n case \"left\":\n return { x: rect.x, y: rect.y + rect.height / 2 };\n case \"right\":\n return { x: rect.x + rect.width, y: rect.y + rect.height / 2 };\n }\n}\n\n/** The side pair picked by {@link pickClosestHandles}, with both anchor points. */\nexport interface ClosestHandles {\n sourceSide: FlowHandleSide;\n targetSide: FlowHandleSide;\n sx: number;\n sy: number;\n tx: number;\n ty: number;\n}\n\n/**\n * Rectangle-only fallback: picks the closest pair of **side midpoints** from the\n * candidate sides of each node.\n *\n * `FlowSmartEdge` prefers {@link pickClosestAnchors} (measured handle centres)\n * and only reaches for this before React Flow has measured the nodes. Pass the\n * sides each node genuinely renders a handle on — passing sides that carry no\n * handle produces an anchor floating on a bare border, which is the defect this\n * module exists to avoid.\n */\nexport function pickClosestHandles(\n source: NodeRect,\n sourceSides: FlowHandleSide[],\n target: NodeRect,\n targetSides: FlowHandleSide[],\n): ClosestHandles {\n const srcSides = sourceSides.length ? sourceSides : HANDLE_SIDES;\n const tgtSides = targetSides.length ? targetSides : HANDLE_SIDES;\n\n let best: ClosestHandles | undefined;\n let bestDist = Infinity;\n\n for (const s of srcSides) {\n const sp = handlePoint(source, s);\n for (const t of tgtSides) {\n const tp = handlePoint(target, t);\n const dx = tp.x - sp.x;\n const dy = tp.y - sp.y;\n const dist = dx * dx + dy * dy;\n if (dist < bestDist) {\n bestDist = dist;\n best = { sourceSide: s, targetSide: t, sx: sp.x, sy: sp.y, tx: tp.x, ty: tp.y };\n }\n }\n }\n\n // srcSides/tgtSides are non-empty (fallback above), so best is always set.\n return best as ClosestHandles;\n}\n"],"mappings":";;;AAAA,SAAS,gBAAgB;AAYlB,IAAM,eAAiC,CAAC,OAAO,SAAS,UAAU,MAAM;AAGxE,IAAM,iBAAmD;AAAA,EAC9D,KAAK,SAAS;AAAA,EACd,OAAO,SAAS;AAAA,EAChB,QAAQ,SAAS;AAAA,EACjB,MAAM,SAAS;AACjB;AAGO,IAAM,iBAAmD;AAAA,EAC9D,CAAC,SAAS,GAAG,GAAG;AAAA,EAChB,CAAC,SAAS,KAAK,GAAG;AAAA,EAClB,CAAC,SAAS,MAAM,GAAG;AAAA,EACnB,CAAC,SAAS,IAAI,GAAG;AACnB;AAqCO,SAAS,mBACd,SACA,SAC4B;AAC5B,MAAI,CAAC,QAAQ,UAAU,CAAC,QAAQ,OAAQ,QAAO;AAE/C,MAAI;AACJ,MAAI,WAAW;AAEf,aAAW,UAAU,SAAS;AAC5B,eAAW,UAAU,SAAS;AAC5B,YAAM,KAAK,OAAO,IAAI,OAAO;AAC7B,YAAM,KAAK,OAAO,IAAI,OAAO;AAC7B,YAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,UAAI,OAAO,UAAU;AACnB,mBAAW;AACX,eAAO,EAAE,QAAQ,OAAO;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,YAAY,MAAgB,MAAgD;AAC1F,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,EAAE,GAAG,KAAK,IAAI,KAAK,QAAQ,GAAG,GAAG,KAAK,EAAE;AAAA,IACjD,KAAK;AACH,aAAO,EAAE,GAAG,KAAK,IAAI,KAAK,QAAQ,GAAG,GAAG,KAAK,IAAI,KAAK,OAAO;AAAA,IAC/D,KAAK;AACH,aAAO,EAAE,GAAG,KAAK,GAAG,GAAG,KAAK,IAAI,KAAK,SAAS,EAAE;AAAA,IAClD,KAAK;AACH,aAAO,EAAE,GAAG,KAAK,IAAI,KAAK,OAAO,GAAG,KAAK,IAAI,KAAK,SAAS,EAAE;AAAA,EACjE;AACF;AAsBO,SAAS,mBACd,QACA,aACA,QACA,aACgB;AAChB,QAAM,WAAW,YAAY,SAAS,cAAc;AACpD,QAAM,WAAW,YAAY,SAAS,cAAc;AAEpD,MAAI;AACJ,MAAI,WAAW;AAEf,aAAW,KAAK,UAAU;AACxB,UAAM,KAAK,YAAY,QAAQ,CAAC;AAChC,eAAW,KAAK,UAAU;AACxB,YAAM,KAAK,YAAY,QAAQ,CAAC;AAChC,YAAM,KAAK,GAAG,IAAI,GAAG;AACrB,YAAM,KAAK,GAAG,IAAI,GAAG;AACrB,YAAM,OAAO,KAAK,KAAK,KAAK;AAC5B,UAAI,OAAO,UAAU;AACnB,mBAAW;AACX,eAAO,EAAE,YAAY,GAAG,YAAY,GAAG,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,IAAI,GAAG,GAAG,IAAI,GAAG,EAAE;AAAA,MAChF;AAAA,IACF;AAAA,EACF;AAGA,SAAO;AACT;","names":[]}
@@ -0,0 +1,185 @@
1
+ "use client";
2
+ import {
3
+ HANDLE_BY_DIRECTION,
4
+ layoutFlow
5
+ } from "./chunk-HTLTCCFA.js";
6
+
7
+ // src/flow-layout/layout-graph.ts
8
+ import {
9
+ forceCenter,
10
+ forceCollide,
11
+ forceLink,
12
+ forceManyBody,
13
+ forceSimulation
14
+ } from "d3-force";
15
+ var DEFAULT_NODE_WIDTH = 172;
16
+ var DEFAULT_NODE_HEIGHT = 40;
17
+ var DEFAULT_RING_RADIUS = 180;
18
+ var DEFAULT_FORCE_ITERATIONS = 300;
19
+ var DEFAULT_GRID_SPACING = { x: 200, y: 120 };
20
+ var FORCE_CHARGE_STRENGTH = -300;
21
+ var FORCE_LINK_DISTANCE = 150;
22
+ function resolveNodeSize(node, nodeSize) {
23
+ if (nodeSize) return nodeSize(node.id);
24
+ return {
25
+ width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH,
26
+ height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT
27
+ };
28
+ }
29
+ function layoutGraph(nodes, edges, options) {
30
+ if (nodes.length === 0) return [];
31
+ switch (options.algorithm) {
32
+ case "layered-lr":
33
+ return layoutLayered(nodes, edges, options, "LR");
34
+ case "layered-tb":
35
+ return layoutLayered(nodes, edges, options, "TB");
36
+ case "concentric":
37
+ return layoutConcentric(nodes, edges, options);
38
+ case "force":
39
+ return layoutForce(nodes, edges, options);
40
+ case "grid":
41
+ return layoutGrid(nodes, options);
42
+ default: {
43
+ const exhaustiveCheck = options.algorithm;
44
+ throw new Error(`layoutGraph: unknown algorithm "${String(exhaustiveCheck)}"`);
45
+ }
46
+ }
47
+ }
48
+ function layoutLayered(nodes, edges, options, direction) {
49
+ const { spacing, nodeSize } = options;
50
+ const sizedNodes = nodeSize ? nodes.map((node) => {
51
+ const { width, height } = nodeSize(node.id);
52
+ return { ...node, width, height, measured: { width, height } };
53
+ }) : nodes;
54
+ const horizontalRanks = direction === "LR" || direction === "RL";
55
+ const { nodes: laidOut } = layoutFlow(sizedNodes, edges, {
56
+ direction,
57
+ nodeSpacing: horizontalRanks ? spacing?.y : spacing?.x,
58
+ rankSpacing: horizontalRanks ? spacing?.x : spacing?.y
59
+ });
60
+ return nodes.map((node, i) => ({
61
+ ...node,
62
+ sourcePosition: laidOut[i].sourcePosition,
63
+ targetPosition: laidOut[i].targetPosition,
64
+ position: laidOut[i].position
65
+ }));
66
+ }
67
+ function resolveCenterId(centerId, nodeIds, adjacency) {
68
+ if (centerId && nodeIds.includes(centerId)) return centerId;
69
+ let best = nodeIds[0];
70
+ let bestDegree = -1;
71
+ for (const id of nodeIds) {
72
+ const degree = adjacency.get(id)?.size ?? 0;
73
+ if (degree > bestDegree) {
74
+ bestDegree = degree;
75
+ best = id;
76
+ }
77
+ }
78
+ return best;
79
+ }
80
+ function layoutConcentric(nodes, edges, options) {
81
+ const ringRadius = options.ringRadius ?? DEFAULT_RING_RADIUS;
82
+ const nodeIds = nodes.map((node) => node.id);
83
+ const idSet = new Set(nodeIds);
84
+ const adjacency = /* @__PURE__ */ new Map();
85
+ for (const id of nodeIds) adjacency.set(id, /* @__PURE__ */ new Set());
86
+ for (const edge of edges) {
87
+ if (!idSet.has(edge.source) || !idSet.has(edge.target)) continue;
88
+ adjacency.get(edge.source).add(edge.target);
89
+ adjacency.get(edge.target).add(edge.source);
90
+ }
91
+ const centerId = resolveCenterId(options.centerId, nodeIds, adjacency);
92
+ const hopOf = /* @__PURE__ */ new Map();
93
+ hopOf.set(centerId, 0);
94
+ const queue = [centerId];
95
+ let maxHop = 0;
96
+ while (queue.length > 0) {
97
+ const current = queue.shift();
98
+ const currentHop = hopOf.get(current);
99
+ for (const neighbor of adjacency.get(current) ?? []) {
100
+ if (hopOf.has(neighbor)) continue;
101
+ const hop = currentHop + 1;
102
+ hopOf.set(neighbor, hop);
103
+ maxHop = Math.max(maxHop, hop);
104
+ queue.push(neighbor);
105
+ }
106
+ }
107
+ const leftoverHop = maxHop + 1;
108
+ for (const id of nodeIds) {
109
+ if (!hopOf.has(id)) hopOf.set(id, leftoverHop);
110
+ }
111
+ const rings = /* @__PURE__ */ new Map();
112
+ for (const id of nodeIds) {
113
+ const hop = hopOf.get(id);
114
+ if (hop === 0) continue;
115
+ if (!rings.has(hop)) rings.set(hop, []);
116
+ rings.get(hop).push(id);
117
+ }
118
+ const positionById = /* @__PURE__ */ new Map();
119
+ positionById.set(centerId, { x: 0, y: 0 });
120
+ for (const [hop, ids] of rings) {
121
+ const count = ids.length;
122
+ const angleStep = 2 * Math.PI / count;
123
+ const ringOffset = (hop - 1) * (angleStep / 2);
124
+ const radius = hop * ringRadius;
125
+ ids.forEach((id, i) => {
126
+ const angle = ringOffset + i * angleStep;
127
+ positionById.set(id, { x: radius * Math.cos(angle), y: radius * Math.sin(angle) });
128
+ });
129
+ }
130
+ return nodes.map((node) => ({
131
+ ...node,
132
+ position: positionById.get(node.id) ?? { x: 0, y: 0 }
133
+ }));
134
+ }
135
+ function layoutForce(nodes, edges, options) {
136
+ const iterations = options.iterations ?? DEFAULT_FORCE_ITERATIONS;
137
+ const nodeIds = new Set(nodes.map((node) => node.id));
138
+ const seedRadius = Math.max(nodes.length * 20, 100);
139
+ const simNodes = nodes.map((node, i) => {
140
+ const angle = 2 * Math.PI * i / Math.max(nodes.length, 1);
141
+ return { id: node.id, x: seedRadius * Math.cos(angle), y: seedRadius * Math.sin(angle) };
142
+ });
143
+ const simLinks = edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target)).map((edge) => ({ source: edge.source, target: edge.target }));
144
+ const collisionById = new Map(
145
+ nodes.map((node) => {
146
+ const { width, height } = resolveNodeSize(node, options.nodeSize);
147
+ return [node.id, Math.max(width, height) / 2];
148
+ })
149
+ );
150
+ const simulation = forceSimulation(simNodes).force("charge", forceManyBody().strength(FORCE_CHARGE_STRENGTH)).force(
151
+ "link",
152
+ forceLink(simLinks).id((d) => d.id).distance(FORCE_LINK_DISTANCE)
153
+ ).force("center", forceCenter(0, 0)).force(
154
+ "collide",
155
+ forceCollide((d) => collisionById.get(d.id) ?? DEFAULT_NODE_WIDTH / 2)
156
+ ).stop();
157
+ for (let i = 0; i < iterations; i++) {
158
+ simulation.tick();
159
+ }
160
+ const positionById = new Map(simNodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));
161
+ return nodes.map((node) => ({
162
+ ...node,
163
+ position: positionById.get(node.id) ?? { x: 0, y: 0 }
164
+ }));
165
+ }
166
+ function layoutGrid(nodes, options) {
167
+ const spacing = options.spacing ?? DEFAULT_GRID_SPACING;
168
+ const cols = Math.max(1, Math.ceil(Math.sqrt(nodes.length)));
169
+ const handles = HANDLE_BY_DIRECTION.TB;
170
+ return nodes.map((node, i) => {
171
+ const row = Math.floor(i / cols);
172
+ const col = i % cols;
173
+ return {
174
+ ...node,
175
+ sourcePosition: handles.source,
176
+ targetPosition: handles.target,
177
+ position: { x: col * spacing.x, y: row * spacing.y }
178
+ };
179
+ });
180
+ }
181
+
182
+ export {
183
+ layoutGraph
184
+ };
185
+ //# sourceMappingURL=chunk-DNX7G3KA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/flow-layout/layout-graph.ts"],"sourcesContent":["import {\n forceCenter,\n forceCollide,\n forceLink,\n forceManyBody,\n forceSimulation,\n type SimulationLinkDatum,\n type SimulationNodeDatum,\n} from \"d3-force\";\nimport type { Edge, Node } from \"@xyflow/react\";\nimport { HANDLE_BY_DIRECTION, layoutFlow, type FlowLayoutDirection } from \"./flow-layout\";\n\n/** Which generic graph-geometry algorithm `layoutGraph` should run. */\nexport type LayoutAlgorithm = \"concentric\" | \"force\" | \"layered-lr\" | \"layered-tb\" | \"grid\";\n\n/** Horizontal/vertical gap used by the `\"grid\"` and `\"layered-*\"` algorithms. */\nexport interface LayoutSpacing {\n x: number;\n y: number;\n}\n\nexport interface LayoutOptions {\n /** Which layout algorithm to run. */\n algorithm: LayoutAlgorithm;\n /**\n * `\"concentric\"` only — the focal node placed at the origin. Defaults to the\n * highest-degree node when omitted (or when the id doesn't match a node).\n */\n centerId?: string;\n /** `\"concentric\"` only — radius of ring 1; ring N sits at `N × ringRadius`. @default 180 */\n ringRadius?: number;\n /** `\"force\"` only — number of synchronous simulation ticks to run. @default 300 */\n iterations?: number;\n /** `\"grid\"` / `\"layered-lr\"` / `\"layered-tb\"` — gap between nodes. @default {x:200,y:120} for grid. */\n spacing?: LayoutSpacing;\n /** Resolve a node's size by id. Falls back to `measured`/`width`/`height`/a sensible default. */\n nodeSize?: (id: string) => { width: number; height: number };\n}\n\nconst DEFAULT_NODE_WIDTH = 172;\nconst DEFAULT_NODE_HEIGHT = 40;\nconst DEFAULT_RING_RADIUS = 180;\nconst DEFAULT_FORCE_ITERATIONS = 300;\nconst DEFAULT_GRID_SPACING: LayoutSpacing = { x: 200, y: 120 };\n/** Base charge/link/collide tuning — chosen to read well at brand-ui's default node size. */\nconst FORCE_CHARGE_STRENGTH = -300;\nconst FORCE_LINK_DISTANCE = 150;\n\nfunction resolveNodeSize(\n node: Node,\n nodeSize?: (id: string) => { width: number; height: number },\n): { width: number; height: number } {\n if (nodeSize) return nodeSize(node.id);\n return {\n width: node.measured?.width ?? node.width ?? DEFAULT_NODE_WIDTH,\n height: node.measured?.height ?? node.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/**\n * Pure graph-geometry auto layout. Computes new `position`s for `nodes` given\n * `edges` and a `LayoutAlgorithm` — no React, no DOM, no side effects, safe to\n * call anywhere (including outside a component or in a test). Deterministic:\n * identical `nodes`/`edges`/`options` always produce identical positions.\n *\n * Node identity and `data` are left untouched — only `position` changes.\n * Pair with `useAutoLayout` for a memoized hook form.\n *\n * - `\"layered-lr\"` / `\"layered-tb\"` delegate to the dagre-powered `layoutFlow`\n * (direction `\"LR\"` / `\"TB\"`). `spacing.x` is always the horizontal gap and\n * `spacing.y` the vertical one, so the two differ only in which axis carries\n * the ranks.\n * - `\"concentric\"` places `centerId` (or the highest-degree node) at the\n * origin, with BFS shells at `ring × ringRadius`; disconnected nodes land in\n * one extra outer ring so positions are never `NaN`.\n * - `\"force\"` runs `d3-force` synchronously for a fixed number of `iterations`,\n * seeded on a circle by index (no randomness) so runs are reproducible.\n * - `\"grid\"` lays nodes out row-major using `spacing`.\n */\nexport function layoutGraph<NodeType extends Node = Node, EdgeType extends Edge = Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n if (nodes.length === 0) return [];\n\n switch (options.algorithm) {\n case \"layered-lr\":\n return layoutLayered(nodes, edges, options, \"LR\");\n case \"layered-tb\":\n return layoutLayered(nodes, edges, options, \"TB\");\n case \"concentric\":\n return layoutConcentric(nodes, edges, options);\n case \"force\":\n return layoutForce(nodes, edges, options);\n case \"grid\":\n return layoutGrid(nodes, options);\n default: {\n const exhaustiveCheck: never = options.algorithm;\n throw new Error(`layoutGraph: unknown algorithm \"${String(exhaustiveCheck)}\"`);\n }\n }\n}\n\nfunction layoutLayered<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n direction: FlowLayoutDirection,\n): NodeType[] {\n const { spacing, nodeSize } = options;\n\n // dagre (via layoutFlow) reads sizes off `measured`/`width`/`height` — feed it\n // resolved sizes on a throwaway copy so a caller-supplied `nodeSize` is honored\n // without mutating (or permanently resizing) the nodes we return.\n const sizedNodes = nodeSize\n ? nodes.map((node) => {\n const { width, height } = nodeSize(node.id);\n return { ...node, width, height, measured: { width, height } };\n })\n : nodes;\n\n // Ranks progress along the layout axis and nodes stack across it, so the two\n // directions map the SAME `spacing.x`/`spacing.y` onto dagre's rank/node sep\n // with the axes swapped: LR ranks horizontally (x), TB ranks vertically (y).\n const horizontalRanks = direction === \"LR\" || direction === \"RL\";\n const { nodes: laidOut } = layoutFlow(sizedNodes, edges, {\n direction,\n nodeSpacing: horizontalRanks ? spacing?.y : spacing?.x,\n rankSpacing: horizontalRanks ? spacing?.x : spacing?.y,\n });\n\n // Carry the handle sides layoutFlow stamped on (LR right-out/left-in, TB\n // bottom-out/top-in), not just the position — otherwise the layout moves the\n // nodes but leaves the anchors on the wrong sides.\n return nodes.map((node, i) => ({\n ...node,\n sourcePosition: laidOut[i]!.sourcePosition,\n targetPosition: laidOut[i]!.targetPosition,\n position: laidOut[i]!.position,\n }));\n}\n\nfunction resolveCenterId(\n centerId: string | undefined,\n nodeIds: string[],\n adjacency: Map<string, Set<string>>,\n): string {\n if (centerId && nodeIds.includes(centerId)) return centerId;\n\n let best = nodeIds[0]!;\n let bestDegree = -1;\n for (const id of nodeIds) {\n const degree = adjacency.get(id)?.size ?? 0;\n if (degree > bestDegree) {\n bestDegree = degree;\n best = id;\n }\n }\n return best;\n}\n\nfunction layoutConcentric<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n const ringRadius = options.ringRadius ?? DEFAULT_RING_RADIUS;\n const nodeIds = nodes.map((node) => node.id);\n const idSet = new Set(nodeIds);\n\n const adjacency = new Map<string, Set<string>>();\n for (const id of nodeIds) adjacency.set(id, new Set());\n for (const edge of edges) {\n if (!idSet.has(edge.source) || !idSet.has(edge.target)) continue;\n adjacency.get(edge.source)!.add(edge.target);\n adjacency.get(edge.target)!.add(edge.source);\n }\n\n const centerId = resolveCenterId(options.centerId, nodeIds, adjacency);\n\n // BFS shells from the center — hop distance doubles as the ring index.\n const hopOf = new Map<string, number>();\n hopOf.set(centerId, 0);\n const queue: string[] = [centerId];\n let maxHop = 0;\n while (queue.length > 0) {\n const current = queue.shift()!;\n const currentHop = hopOf.get(current)!;\n for (const neighbor of adjacency.get(current) ?? []) {\n if (hopOf.has(neighbor)) continue;\n const hop = currentHop + 1;\n hopOf.set(neighbor, hop);\n maxHop = Math.max(maxHop, hop);\n queue.push(neighbor);\n }\n }\n\n // Disconnected nodes (unreachable from the center) get one extra ring beyond\n // the farthest reached hop — never left with a NaN/undefined position.\n const leftoverHop = maxHop + 1;\n for (const id of nodeIds) {\n if (!hopOf.has(id)) hopOf.set(id, leftoverHop);\n }\n\n const rings = new Map<number, string[]>();\n for (const id of nodeIds) {\n const hop = hopOf.get(id)!;\n if (hop === 0) continue;\n if (!rings.has(hop)) rings.set(hop, []);\n rings.get(hop)!.push(id);\n }\n\n const positionById = new Map<string, { x: number; y: number }>();\n positionById.set(centerId, { x: 0, y: 0 });\n\n for (const [hop, ids] of rings) {\n const count = ids.length;\n const angleStep = (2 * Math.PI) / count;\n // Stagger successive rings by a growing half-step so radial edges don't\n // trivially line up with the ring behind/ahead of them.\n const ringOffset = (hop - 1) * (angleStep / 2);\n const radius = hop * ringRadius;\n ids.forEach((id, i) => {\n const angle = ringOffset + i * angleStep;\n positionById.set(id, { x: radius * Math.cos(angle), y: radius * Math.sin(angle) });\n });\n }\n\n return nodes.map((node) => ({\n ...node,\n position: positionById.get(node.id) ?? { x: 0, y: 0 },\n }));\n}\n\ninterface ForceSimNode extends SimulationNodeDatum {\n id: string;\n}\n\nfunction layoutForce<NodeType extends Node, EdgeType extends Edge>(\n nodes: NodeType[],\n edges: EdgeType[],\n options: LayoutOptions,\n): NodeType[] {\n const iterations = options.iterations ?? DEFAULT_FORCE_ITERATIONS;\n const nodeIds = new Set(nodes.map((node) => node.id));\n\n // Seed initial positions deterministically on a circle by index — never\n // `Math.random()` — so two runs with the same input converge identically.\n const seedRadius = Math.max(nodes.length * 20, 100);\n const simNodes: ForceSimNode[] = nodes.map((node, i) => {\n const angle = (2 * Math.PI * i) / Math.max(nodes.length, 1);\n return { id: node.id, x: seedRadius * Math.cos(angle), y: seedRadius * Math.sin(angle) };\n });\n\n const simLinks: SimulationLinkDatum<ForceSimNode>[] = edges\n .filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target))\n .map((edge) => ({ source: edge.source, target: edge.target }));\n\n const collisionById = new Map(\n nodes.map((node) => {\n const { width, height } = resolveNodeSize(node, options.nodeSize);\n return [node.id, Math.max(width, height) / 2];\n }),\n );\n\n const simulation = forceSimulation(simNodes)\n .force(\"charge\", forceManyBody().strength(FORCE_CHARGE_STRENGTH))\n .force(\n \"link\",\n forceLink<ForceSimNode, SimulationLinkDatum<ForceSimNode>>(simLinks)\n .id((d) => d.id)\n .distance(FORCE_LINK_DISTANCE),\n )\n .force(\"center\", forceCenter(0, 0))\n .force(\n \"collide\",\n forceCollide<ForceSimNode>((d) => collisionById.get(d.id) ?? DEFAULT_NODE_WIDTH / 2),\n )\n .stop();\n\n for (let i = 0; i < iterations; i++) {\n simulation.tick();\n }\n\n const positionById = new Map(simNodes.map((n) => [n.id, { x: n.x ?? 0, y: n.y ?? 0 }]));\n\n return nodes.map((node) => ({\n ...node,\n position: positionById.get(node.id) ?? { x: 0, y: 0 },\n }));\n}\n\nfunction layoutGrid<NodeType extends Node>(nodes: NodeType[], options: LayoutOptions): NodeType[] {\n const spacing = options.spacing ?? DEFAULT_GRID_SPACING;\n const cols = Math.max(1, Math.ceil(Math.sqrt(nodes.length)));\n // A grid reads top-to-bottom; anchor edges bottom-out / top-in to match.\n const handles = HANDLE_BY_DIRECTION.TB;\n\n return nodes.map((node, i) => {\n const row = Math.floor(i / cols);\n const col = i % cols;\n return {\n ...node,\n sourcePosition: handles.source,\n targetPosition: handles.target,\n position: { x: col * spacing.x, y: row * spacing.y },\n };\n });\n}\n"],"mappings":";;;;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AA+BP,IAAM,qBAAqB;AAC3B,IAAM,sBAAsB;AAC5B,IAAM,sBAAsB;AAC5B,IAAM,2BAA2B;AACjC,IAAM,uBAAsC,EAAE,GAAG,KAAK,GAAG,IAAI;AAE7D,IAAM,wBAAwB;AAC9B,IAAM,sBAAsB;AAE5B,SAAS,gBACP,MACA,UACmC;AACnC,MAAI,SAAU,QAAO,SAAS,KAAK,EAAE;AACrC,SAAO;AAAA,IACL,OAAO,KAAK,UAAU,SAAS,KAAK,SAAS;AAAA,IAC7C,QAAQ,KAAK,UAAU,UAAU,KAAK,UAAU;AAAA,EAClD;AACF;AAsBO,SAAS,YACd,OACA,OACA,SACY;AACZ,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,UAAQ,QAAQ,WAAW;AAAA,IACzB,KAAK;AACH,aAAO,cAAc,OAAO,OAAO,SAAS,IAAI;AAAA,IAClD,KAAK;AACH,aAAO,cAAc,OAAO,OAAO,SAAS,IAAI;AAAA,IAClD,KAAK;AACH,aAAO,iBAAiB,OAAO,OAAO,OAAO;AAAA,IAC/C,KAAK;AACH,aAAO,YAAY,OAAO,OAAO,OAAO;AAAA,IAC1C,KAAK;AACH,aAAO,WAAW,OAAO,OAAO;AAAA,IAClC,SAAS;AACP,YAAM,kBAAyB,QAAQ;AACvC,YAAM,IAAI,MAAM,mCAAmC,OAAO,eAAe,CAAC,GAAG;AAAA,IAC/E;AAAA,EACF;AACF;AAEA,SAAS,cACP,OACA,OACA,SACA,WACY;AACZ,QAAM,EAAE,SAAS,SAAS,IAAI;AAK9B,QAAM,aAAa,WACf,MAAM,IAAI,CAAC,SAAS;AAClB,UAAM,EAAE,OAAO,OAAO,IAAI,SAAS,KAAK,EAAE;AAC1C,WAAO,EAAE,GAAG,MAAM,OAAO,QAAQ,UAAU,EAAE,OAAO,OAAO,EAAE;AAAA,EAC/D,CAAC,IACD;AAKJ,QAAM,kBAAkB,cAAc,QAAQ,cAAc;AAC5D,QAAM,EAAE,OAAO,QAAQ,IAAI,WAAW,YAAY,OAAO;AAAA,IACvD;AAAA,IACA,aAAa,kBAAkB,SAAS,IAAI,SAAS;AAAA,IACrD,aAAa,kBAAkB,SAAS,IAAI,SAAS;AAAA,EACvD,CAAC;AAKD,SAAO,MAAM,IAAI,CAAC,MAAM,OAAO;AAAA,IAC7B,GAAG;AAAA,IACH,gBAAgB,QAAQ,CAAC,EAAG;AAAA,IAC5B,gBAAgB,QAAQ,CAAC,EAAG;AAAA,IAC5B,UAAU,QAAQ,CAAC,EAAG;AAAA,EACxB,EAAE;AACJ;AAEA,SAAS,gBACP,UACA,SACA,WACQ;AACR,MAAI,YAAY,QAAQ,SAAS,QAAQ,EAAG,QAAO;AAEnD,MAAI,OAAO,QAAQ,CAAC;AACpB,MAAI,aAAa;AACjB,aAAW,MAAM,SAAS;AACxB,UAAM,SAAS,UAAU,IAAI,EAAE,GAAG,QAAQ;AAC1C,QAAI,SAAS,YAAY;AACvB,mBAAa;AACb,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,iBACP,OACA,OACA,SACY;AACZ,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE;AAC3C,QAAM,QAAQ,IAAI,IAAI,OAAO;AAE7B,QAAM,YAAY,oBAAI,IAAyB;AAC/C,aAAW,MAAM,QAAS,WAAU,IAAI,IAAI,oBAAI,IAAI,CAAC;AACrD,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,MAAM,IAAI,KAAK,MAAM,KAAK,CAAC,MAAM,IAAI,KAAK,MAAM,EAAG;AACxD,cAAU,IAAI,KAAK,MAAM,EAAG,IAAI,KAAK,MAAM;AAC3C,cAAU,IAAI,KAAK,MAAM,EAAG,IAAI,KAAK,MAAM;AAAA,EAC7C;AAEA,QAAM,WAAW,gBAAgB,QAAQ,UAAU,SAAS,SAAS;AAGrE,QAAM,QAAQ,oBAAI,IAAoB;AACtC,QAAM,IAAI,UAAU,CAAC;AACrB,QAAM,QAAkB,CAAC,QAAQ;AACjC,MAAI,SAAS;AACb,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,UAAU,MAAM,MAAM;AAC5B,UAAM,aAAa,MAAM,IAAI,OAAO;AACpC,eAAW,YAAY,UAAU,IAAI,OAAO,KAAK,CAAC,GAAG;AACnD,UAAI,MAAM,IAAI,QAAQ,EAAG;AACzB,YAAM,MAAM,aAAa;AACzB,YAAM,IAAI,UAAU,GAAG;AACvB,eAAS,KAAK,IAAI,QAAQ,GAAG;AAC7B,YAAM,KAAK,QAAQ;AAAA,IACrB;AAAA,EACF;AAIA,QAAM,cAAc,SAAS;AAC7B,aAAW,MAAM,SAAS;AACxB,QAAI,CAAC,MAAM,IAAI,EAAE,EAAG,OAAM,IAAI,IAAI,WAAW;AAAA,EAC/C;AAEA,QAAM,QAAQ,oBAAI,IAAsB;AACxC,aAAW,MAAM,SAAS;AACxB,UAAM,MAAM,MAAM,IAAI,EAAE;AACxB,QAAI,QAAQ,EAAG;AACf,QAAI,CAAC,MAAM,IAAI,GAAG,EAAG,OAAM,IAAI,KAAK,CAAC,CAAC;AACtC,UAAM,IAAI,GAAG,EAAG,KAAK,EAAE;AAAA,EACzB;AAEA,QAAM,eAAe,oBAAI,IAAsC;AAC/D,eAAa,IAAI,UAAU,EAAE,GAAG,GAAG,GAAG,EAAE,CAAC;AAEzC,aAAW,CAAC,KAAK,GAAG,KAAK,OAAO;AAC9B,UAAM,QAAQ,IAAI;AAClB,UAAM,YAAa,IAAI,KAAK,KAAM;AAGlC,UAAM,cAAc,MAAM,MAAM,YAAY;AAC5C,UAAM,SAAS,MAAM;AACrB,QAAI,QAAQ,CAAC,IAAI,MAAM;AACrB,YAAM,QAAQ,aAAa,IAAI;AAC/B,mBAAa,IAAI,IAAI,EAAE,GAAG,SAAS,KAAK,IAAI,KAAK,GAAG,GAAG,SAAS,KAAK,IAAI,KAAK,EAAE,CAAC;AAAA,IACnF,CAAC;AAAA,EACH;AAEA,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,GAAG;AAAA,IACH,UAAU,aAAa,IAAI,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACtD,EAAE;AACJ;AAMA,SAAS,YACP,OACA,OACA,SACY;AACZ,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,UAAU,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAIpD,QAAM,aAAa,KAAK,IAAI,MAAM,SAAS,IAAI,GAAG;AAClD,QAAM,WAA2B,MAAM,IAAI,CAAC,MAAM,MAAM;AACtD,UAAM,QAAS,IAAI,KAAK,KAAK,IAAK,KAAK,IAAI,MAAM,QAAQ,CAAC;AAC1D,WAAO,EAAE,IAAI,KAAK,IAAI,GAAG,aAAa,KAAK,IAAI,KAAK,GAAG,GAAG,aAAa,KAAK,IAAI,KAAK,EAAE;AAAA,EACzF,CAAC;AAED,QAAM,WAAgD,MACnD,OAAO,CAAC,SAAS,QAAQ,IAAI,KAAK,MAAM,KAAK,QAAQ,IAAI,KAAK,MAAM,CAAC,EACrE,IAAI,CAAC,UAAU,EAAE,QAAQ,KAAK,QAAQ,QAAQ,KAAK,OAAO,EAAE;AAE/D,QAAM,gBAAgB,IAAI;AAAA,IACxB,MAAM,IAAI,CAAC,SAAS;AAClB,YAAM,EAAE,OAAO,OAAO,IAAI,gBAAgB,MAAM,QAAQ,QAAQ;AAChE,aAAO,CAAC,KAAK,IAAI,KAAK,IAAI,OAAO,MAAM,IAAI,CAAC;AAAA,IAC9C,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,gBAAgB,QAAQ,EACxC,MAAM,UAAU,cAAc,EAAE,SAAS,qBAAqB,CAAC,EAC/D;AAAA,IACC;AAAA,IACA,UAA2D,QAAQ,EAChE,GAAG,CAAC,MAAM,EAAE,EAAE,EACd,SAAS,mBAAmB;AAAA,EACjC,EACC,MAAM,UAAU,YAAY,GAAG,CAAC,CAAC,EACjC;AAAA,IACC;AAAA,IACA,aAA2B,CAAC,MAAM,cAAc,IAAI,EAAE,EAAE,KAAK,qBAAqB,CAAC;AAAA,EACrF,EACC,KAAK;AAER,WAAS,IAAI,GAAG,IAAI,YAAY,KAAK;AACnC,eAAW,KAAK;AAAA,EAClB;AAEA,QAAM,eAAe,IAAI,IAAI,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;AAEtF,SAAO,MAAM,IAAI,CAAC,UAAU;AAAA,IAC1B,GAAG;AAAA,IACH,UAAU,aAAa,IAAI,KAAK,EAAE,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE;AAAA,EACtD,EAAE;AACJ;AAEA,SAAS,WAAkC,OAAmB,SAAoC;AAChG,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,KAAK,KAAK,MAAM,MAAM,CAAC,CAAC;AAE3D,QAAM,UAAU,oBAAoB;AAEpC,SAAO,MAAM,IAAI,CAAC,MAAM,MAAM;AAC5B,UAAM,MAAM,KAAK,MAAM,IAAI,IAAI;AAC/B,UAAM,MAAM,IAAI;AAChB,WAAO;AAAA,MACL,GAAG;AAAA,MACH,gBAAgB,QAAQ;AAAA,MACxB,gBAAgB,QAAQ;AAAA,MACxB,UAAU,EAAE,GAAG,MAAM,QAAQ,GAAG,GAAG,MAAM,QAAQ,EAAE;AAAA,IACrD;AAAA,EACF,CAAC;AACH;","names":[]}
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ // src/helper-lines/get-helper-lines.ts
4
+ var ANCHORS = [0, 0.5, 1];
5
+ function getHelperLines(dragged, others, threshold = 5) {
6
+ const result = {};
7
+ let bestVerticalDist = threshold;
8
+ let bestHorizontalDist = threshold;
9
+ const draggedX = ANCHORS.map((a) => ({ a, pos: dragged.x + dragged.width * a }));
10
+ const draggedY = ANCHORS.map((a) => ({ a, pos: dragged.y + dragged.height * a }));
11
+ for (const other of others) {
12
+ const otherX = ANCHORS.map((a) => other.x + other.width * a);
13
+ const otherY = ANCHORS.map((a) => other.y + other.height * a);
14
+ for (const d of draggedX) {
15
+ for (const oPos of otherX) {
16
+ const dx = Math.abs(d.pos - oPos);
17
+ if (dx < bestVerticalDist) {
18
+ bestVerticalDist = dx;
19
+ result.vertical = oPos;
20
+ result.snapX = oPos - dragged.width * d.a;
21
+ }
22
+ }
23
+ }
24
+ for (const d of draggedY) {
25
+ for (const oPos of otherY) {
26
+ const dy = Math.abs(d.pos - oPos);
27
+ if (dy < bestHorizontalDist) {
28
+ bestHorizontalDist = dy;
29
+ result.horizontal = oPos;
30
+ result.snapY = oPos - dragged.height * d.a;
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return result;
36
+ }
37
+
38
+ export {
39
+ getHelperLines
40
+ };
41
+ //# sourceMappingURL=chunk-DNZRAJAQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/helper-lines/get-helper-lines.ts"],"sourcesContent":["/**\n * Pure alignment-guide geometry for the helper-lines feature.\n *\n * Implemented fresh from the documented \"helper lines\" behaviour (the React Flow\n * Pro example is paid — only the concept is reused): while a single node drags,\n * compare its left / horizontal-center / right against every other node's\n * left / center / right (and top / middle / bottom on the vertical axis). When a\n * pair falls within `threshold` flow-coordinate pixels, record the guide's\n * absolute coordinate and the snapped top-left position for the dragged node.\n *\n * Everything here is a pure function of plain rectangles so it is unit-testable\n * without a live React Flow instance.\n */\n\n/** A node's bounding box in absolute flow coordinates (top-left origin). */\nexport interface HelperLineRect {\n /** Left edge (top-left x) in flow coordinates. */\n x: number;\n /** Top edge (top-left y) in flow coordinates. */\n y: number;\n width: number;\n height: number;\n}\n\nexport interface HelperLinesResult {\n /** New top-left x for the dragged node once snapped to the vertical guide. */\n snapX?: number;\n /** New top-left y for the dragged node once snapped to the horizontal guide. */\n snapY?: number;\n /** Absolute flow-x of the active vertical guide line, if any. */\n vertical?: number;\n /** Absolute flow-y of the active horizontal guide line, if any. */\n horizontal?: number;\n}\n\n/**\n * Fractional anchor offsets along an axis: 0 = start edge (left/top),\n * 0.5 = center/middle, 1 = end edge (right/bottom). Comparing all three anchors\n * of the dragged node against all three of every other node yields\n * edge-to-edge, edge-to-center and center-to-center alignment.\n */\nconst ANCHORS = [0, 0.5, 1] as const;\n\n/**\n * Compute the closest vertical + horizontal alignment guides (and the snapped\n * position) for a dragged rectangle against the other rectangles.\n *\n * @param dragged the dragged node's proposed bounding box (flow coordinates)\n * @param others every other node's bounding box\n * @param threshold max distance (flow px) at which alignment engages; a pair at\n * exactly `threshold` does NOT match (strict boundary)\n */\nexport function getHelperLines(\n dragged: HelperLineRect,\n others: Iterable<HelperLineRect>,\n threshold = 5,\n): HelperLinesResult {\n const result: HelperLinesResult = {};\n // Track the closest match on each axis; initialising to `threshold` makes the\n // boundary strict (a distance equal to threshold never wins).\n let bestVerticalDist = threshold;\n let bestHorizontalDist = threshold;\n\n // Each anchor as { a: fractional offset, pos: absolute coordinate }.\n const draggedX = ANCHORS.map((a) => ({ a, pos: dragged.x + dragged.width * a }));\n const draggedY = ANCHORS.map((a) => ({ a, pos: dragged.y + dragged.height * a }));\n\n for (const other of others) {\n const otherX = ANCHORS.map((a) => other.x + other.width * a);\n const otherY = ANCHORS.map((a) => other.y + other.height * a);\n\n // Vertical guide (align on the X axis).\n for (const d of draggedX) {\n for (const oPos of otherX) {\n const dx = Math.abs(d.pos - oPos);\n if (dx < bestVerticalDist) {\n bestVerticalDist = dx;\n result.vertical = oPos;\n result.snapX = oPos - dragged.width * d.a;\n }\n }\n }\n\n // Horizontal guide (align on the Y axis).\n for (const d of draggedY) {\n for (const oPos of otherY) {\n const dy = Math.abs(d.pos - oPos);\n if (dy < bestHorizontalDist) {\n bestHorizontalDist = dy;\n result.horizontal = oPos;\n result.snapY = oPos - dragged.height * d.a;\n }\n }\n }\n }\n\n return result;\n}\n"],"mappings":";;;AAyCA,IAAM,UAAU,CAAC,GAAG,KAAK,CAAC;AAWnB,SAAS,eACd,SACA,QACA,YAAY,GACO;AACnB,QAAM,SAA4B,CAAC;AAGnC,MAAI,mBAAmB;AACvB,MAAI,qBAAqB;AAGzB,QAAM,WAAW,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,QAAQ,QAAQ,EAAE,EAAE;AAC/E,QAAM,WAAW,QAAQ,IAAI,CAAC,OAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,QAAQ,SAAS,EAAE,EAAE;AAEhF,aAAW,SAAS,QAAQ;AAC1B,UAAM,SAAS,QAAQ,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,QAAQ,CAAC;AAC3D,UAAM,SAAS,QAAQ,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,SAAS,CAAC;AAG5D,eAAW,KAAK,UAAU;AACxB,iBAAW,QAAQ,QAAQ;AACzB,cAAM,KAAK,KAAK,IAAI,EAAE,MAAM,IAAI;AAChC,YAAI,KAAK,kBAAkB;AACzB,6BAAmB;AACnB,iBAAO,WAAW;AAClB,iBAAO,QAAQ,OAAO,QAAQ,QAAQ,EAAE;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAGA,eAAW,KAAK,UAAU;AACxB,iBAAW,QAAQ,QAAQ;AACzB,cAAM,KAAK,KAAK,IAAI,EAAE,MAAM,IAAI;AAChC,YAAI,KAAK,oBAAoB;AAC3B,+BAAqB;AACrB,iBAAO,aAAa;AACpB,iBAAO,QAAQ,OAAO,QAAQ,SAAS,EAAE;AAAA,QAC3C;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-ELCLRVD4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,2 @@
1
+ "use client";
2
+ //# sourceMappingURL=chunk-ELOADZSX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,21 @@
1
+ "use client";
2
+
3
+ // src/canvas-shell/clamped-fit-offset.ts
4
+ function clampedFitOffset({
5
+ size,
6
+ pad,
7
+ start,
8
+ extent,
9
+ zoom,
10
+ anchorCentre
11
+ }) {
12
+ const startPinned = pad - start * zoom;
13
+ if (anchorCentre === void 0) return startPinned;
14
+ const endPinned = size - pad - (start + extent) * zoom;
15
+ return Math.min(startPinned, Math.max(endPinned, size / 2 - anchorCentre * zoom));
16
+ }
17
+
18
+ export {
19
+ clampedFitOffset
20
+ };
21
+ //# sourceMappingURL=chunk-EUNV2MR5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/canvas-shell/clamped-fit-offset.ts"],"sourcesContent":["/**\n * The viewport offset for ONE overflowing axis of a clamped fit (pure; exported for its test).\n * Without an anchor the content's start is pinned at the padding. With one, the anchor is\n * centred — but never so far that the pane shows empty space before the content's start or\n * after its end.\n */\nexport function clampedFitOffset({\n size,\n pad,\n start,\n extent,\n zoom,\n anchorCentre,\n}: {\n /** Pane size on this axis, px. */\n size: number;\n /** Padding the fit keeps on this axis, px. */\n pad: number;\n /** Content start and extent on this axis, in flow coordinates. */\n start: number;\n extent: number;\n zoom: number;\n /** Centre of the anchor nodes on this axis, in flow coordinates. */\n anchorCentre?: number;\n}): number {\n const startPinned = pad - start * zoom;\n if (anchorCentre === undefined) return startPinned;\n const endPinned = size - pad - (start + extent) * zoom;\n return Math.min(startPinned, Math.max(endPinned, size / 2 - anchorCentre * zoom));\n}\n"],"mappings":";;;AAMO,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAWW;AACT,QAAM,cAAc,MAAM,QAAQ;AAClC,MAAI,iBAAiB,OAAW,QAAO;AACvC,QAAM,YAAY,OAAO,OAAO,QAAQ,UAAU;AAClD,SAAO,KAAK,IAAI,aAAa,KAAK,IAAI,WAAW,OAAO,IAAI,eAAe,IAAI,CAAC;AAClF;","names":[]}