@elabs-ai/components-flow 4.1.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 (268) 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 +241 -2
  217. package/dist/index.js +167 -2306
  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 +14 -7
  238. package/src/__contract__/inspector-panel.contract.test.tsx +49 -0
  239. package/src/__contract__/legend.contract.test.tsx +49 -0
  240. package/src/canvas-shell/canvas-shell.stories.tsx +1 -0
  241. package/src/canvas-shell/canvas-shell.tsx +51 -4
  242. package/src/canvas-shell/clamped-fit-offset.test.ts +25 -0
  243. package/src/canvas-shell/clamped-fit-offset.ts +30 -0
  244. package/src/custom-nodes/custom-nodes.stories.tsx +718 -0
  245. package/src/flow-edge-tokens/edge-path-sampler.test.ts +108 -0
  246. package/src/flow-edge-tokens/edge-path-sampler.ts +362 -0
  247. package/src/flow-edge-tokens/flow-edge-tokens.stories.tsx +312 -0
  248. package/src/flow-edge-tokens/flow-edge-tokens.test.tsx +115 -0
  249. package/src/flow-edge-tokens/flow-edge-tokens.tsx +94 -0
  250. package/src/flow-edge-tokens/index.ts +12 -0
  251. package/src/flow-layout/backbone.test.ts +88 -0
  252. package/src/flow-layout/backbone.ts +58 -0
  253. package/src/flow-layout/layout-flow-elk.stories.tsx +154 -0
  254. package/src/flow-layout/layout-flow-elk.test.ts +152 -0
  255. package/src/flow-layout/layout-flow-elk.ts +399 -0
  256. package/src/flow-mini-map/flow-mini-map.stories.tsx +20 -0
  257. package/src/flow-node/flow-node.stories.tsx +40 -1
  258. package/src/flow-self-loop-edge/flow-self-loop-edge.test.tsx +38 -0
  259. package/src/flow-self-loop-edge/flow-self-loop-edge.tsx +7 -0
  260. package/src/flow-weighted-edge/edge-label-pill.test.tsx +20 -0
  261. package/src/flow-weighted-edge/flow-weighted-edge.stories.tsx +1 -0
  262. package/src/flow-weighted-edge/flow-weighted-edge.test.tsx +40 -0
  263. package/src/flow-weighted-edge/flow-weighted-edge.tsx +7 -0
  264. package/src/index.ts +13 -0
  265. package/src/inspector-panel/inspector-panel.tsx +9 -6
  266. package/src/legend/legend.test.tsx +11 -0
  267. package/src/legend/legend.tsx +15 -7
  268. package/src/templates-flow-workspace.stories.tsx +7 -2
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ InspectorPanel
4
+ } from "../chunk-NAQWNX2A.js";
5
+ export {
6
+ InspectorPanel
7
+ };
8
+ //# sourceMappingURL=inspector-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ import "../chunk-UPV2MT4O.js";
3
+ import {
4
+ Legend
5
+ } from "../chunk-NVJVWGUS.js";
6
+ import "../chunk-3BTU2VWZ.js";
7
+ export {
8
+ Legend
9
+ };
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import {
3
+ Legend
4
+ } from "../chunk-NVJVWGUS.js";
5
+ import "../chunk-3BTU2VWZ.js";
6
+ export {
7
+ Legend
8
+ };
9
+ //# sourceMappingURL=legend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import {
3
+ FLOW_GROUP_NODE_TYPE,
4
+ collapseGroup,
5
+ expandGroup,
6
+ groupNodes,
7
+ isFlowGroupProxyEdge,
8
+ toggleGroupCollapsed,
9
+ ungroup
10
+ } from "../chunk-TGWPM2CG.js";
11
+ export {
12
+ FLOW_GROUP_NODE_TYPE,
13
+ collapseGroup,
14
+ expandGroup,
15
+ groupNodes,
16
+ isFlowGroupProxyEdge,
17
+ toggleGroupCollapsed,
18
+ ungroup
19
+ };
20
+ //# sourceMappingURL=group-operations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import "../chunk-ELOADZSX.js";
3
+ import {
4
+ useFlowGroups
5
+ } from "../chunk-LQOZCOK3.js";
6
+ import {
7
+ FLOW_GROUP_NODE_TYPE,
8
+ collapseGroup,
9
+ expandGroup,
10
+ groupNodes,
11
+ isFlowGroupProxyEdge,
12
+ toggleGroupCollapsed,
13
+ ungroup
14
+ } from "../chunk-TGWPM2CG.js";
15
+ export {
16
+ FLOW_GROUP_NODE_TYPE,
17
+ collapseGroup,
18
+ expandGroup,
19
+ groupNodes,
20
+ isFlowGroupProxyEdge,
21
+ toggleGroupCollapsed,
22
+ ungroup,
23
+ useFlowGroups
24
+ };
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import {
3
+ useFlowGroups
4
+ } from "../chunk-LQOZCOK3.js";
5
+ import "../chunk-TGWPM2CG.js";
6
+ export {
7
+ useFlowGroups
8
+ };
9
+ //# sourceMappingURL=use-flow-groups.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import "../chunk-3LK6OOXE.js";
3
+ import {
4
+ ZoomControls
5
+ } from "../chunk-ATO4LXJF.js";
6
+ export {
7
+ ZoomControls
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ import {
3
+ ZoomControls
4
+ } from "../chunk-ATO4LXJF.js";
5
+ export {
6
+ ZoomControls
7
+ };
8
+ //# sourceMappingURL=zoom-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-flow",
3
- "version": "4.1.0",
3
+ "version": "5.0.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,10 +30,16 @@
30
30
  },
31
31
  "peerDependencies": {
32
32
  "@xyflow/react": "^12.11.1",
33
+ "elkjs": "^0.12.0",
33
34
  "react": "^18.2.0 || ^19.0.0",
34
35
  "react-dom": "^18.2.0 || ^19.0.0",
35
- "@elabs-ai/components-tokens": "4.1.0",
36
- "@elabs-ai/components-ui": "4.1.0"
36
+ "@elabs-ai/components-tokens": "5.0.0",
37
+ "@elabs-ai/components-ui": "5.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "elkjs": {
41
+ "optional": true
42
+ }
37
43
  },
38
44
  "devDependencies": {
39
45
  "@testing-library/jest-dom": "^6.6.3",
@@ -44,6 +50,7 @@
44
50
  "@types/react-dom": "^19.0.2",
45
51
  "@vitejs/plugin-react": "^4.3.4",
46
52
  "@xyflow/react": "^12.11.1",
53
+ "elkjs": "^0.12.0",
47
54
  "eslint": "^9.17.0",
48
55
  "jsdom": "^25.0.1",
49
56
  "react": "^19.0.0",
@@ -51,11 +58,11 @@
51
58
  "tsup": "^8.3.5",
52
59
  "typescript": "^5.7.3",
53
60
  "vitest": "^3.0.2",
54
- "@elabs-ai/components-tokens": "4.1.0",
55
- "@elabs-ai/components-icons": "4.1.0",
56
61
  "@elabs-ai/components-eslint-config": "0.1.0",
57
- "@elabs-ai/components-ui": "4.1.0",
58
- "@elabs-ai/components-typescript-config": "0.1.0"
62
+ "@elabs-ai/components-icons": "5.0.0",
63
+ "@elabs-ai/components-tokens": "5.0.0",
64
+ "@elabs-ai/components-typescript-config": "0.1.0",
65
+ "@elabs-ai/components-ui": "5.0.0"
59
66
  },
60
67
  "scripts": {
61
68
  "build": "tsup",
@@ -0,0 +1,49 @@
1
+ // GENERATED by scripts/gen-contract-tests.mjs — do not edit; re-run the generator instead.
2
+ /**
3
+ * Contract probe for InspectorPanel (`packages/flow/src/inspector-panel/inspector-panel.tsx`), derived from its
4
+ * `Default` story (packages/flow/src/inspector-panel/inspector-panel.stories.tsx). See scripts/gen-contract-tests.mjs.
5
+ */
6
+ import { describe, it, expect, afterEach } from "vitest";
7
+ import { render, cleanup } from "@testing-library/react";
8
+ import { createRef } from "react";
9
+ import * as stories from "../inspector-panel/inspector-panel.stories";
10
+ import knownFailuresJson from "../../../../scripts/check/contract-known-failures.json";
11
+
12
+ afterEach(cleanup);
13
+
14
+ // Every component here has its own prop/ref/element shape; a generated probe
15
+ // stays generic on purpose (loosely typed, not untyped — see
16
+ // scripts/gen-contract-tests.mjs) rather than re-deriving each one.
17
+ const KNOWN_FAILURES: Record<string, string> = knownFailuresJson;
18
+ const meta = stories.default as { component?: unknown; args?: Record<string, unknown> };
19
+ const Default = (stories as { Default?: { args?: Record<string, unknown> } }).Default;
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- probe target; see comment above
21
+ const Component = meta.component as any;
22
+ const args = { ...(meta.args ?? {}), ...(Default?.args ?? {}) };
23
+ const isForwardRefComponent = Component?.["$$typeof"] === Symbol.for("react.forward_ref");
24
+
25
+ /** Wrap a known, tracked failure in `it.fails` so fixing it forces the key's removal. */
26
+ function contractIt(assertion: string, name: string, fn: () => void) {
27
+ const key = `flow-inspectorpanel--default|jsdom|jsdom|${assertion}`;
28
+ const reason = KNOWN_FAILURES[key];
29
+ if (reason) return it.fails(`${name} (known failure: ${reason})`, fn);
30
+ return it(name, fn);
31
+ }
32
+
33
+ describe("InspectorPanel contract", () => {
34
+ it.skipIf(!isForwardRefComponent)("forwards a ref to a DOM element", () => {
35
+ const ref = createRef<Element>();
36
+ render(<Component {...args} ref={ref} />);
37
+ expect(ref.current).toBeInstanceOf(Element);
38
+ });
39
+
40
+ contractIt("className", "merges a caller className onto the root", () => {
41
+ const { container } = render(<Component {...args} className="contract-probe" />);
42
+ expect(container.querySelector(".contract-probe")).not.toBeNull();
43
+ });
44
+
45
+ contractIt("data-slot", 'exposes data-slot="inspector-panel" on its root', () => {
46
+ const { container } = render(<Component {...args} />);
47
+ expect(container.querySelector('[data-slot="inspector-panel"]')).not.toBeNull();
48
+ });
49
+ });
@@ -0,0 +1,49 @@
1
+ // GENERATED by scripts/gen-contract-tests.mjs — do not edit; re-run the generator instead.
2
+ /**
3
+ * Contract probe for Legend (`packages/flow/src/legend/legend.tsx`), derived from its
4
+ * `Default` story (packages/flow/src/legend/legend.stories.tsx). See scripts/gen-contract-tests.mjs.
5
+ */
6
+ import { describe, it, expect, afterEach } from "vitest";
7
+ import { render, cleanup } from "@testing-library/react";
8
+ import { createRef } from "react";
9
+ import * as stories from "../legend/legend.stories";
10
+ import knownFailuresJson from "../../../../scripts/check/contract-known-failures.json";
11
+
12
+ afterEach(cleanup);
13
+
14
+ // Every component here has its own prop/ref/element shape; a generated probe
15
+ // stays generic on purpose (loosely typed, not untyped — see
16
+ // scripts/gen-contract-tests.mjs) rather than re-deriving each one.
17
+ const KNOWN_FAILURES: Record<string, string> = knownFailuresJson;
18
+ const meta = stories.default as { component?: unknown; args?: Record<string, unknown> };
19
+ const Default = (stories as { Default?: { args?: Record<string, unknown> } }).Default;
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- probe target; see comment above
21
+ const Component = meta.component as any;
22
+ const args = { ...(meta.args ?? {}), ...(Default?.args ?? {}) };
23
+ const isForwardRefComponent = Component?.["$$typeof"] === Symbol.for("react.forward_ref");
24
+
25
+ /** Wrap a known, tracked failure in `it.fails` so fixing it forces the key's removal. */
26
+ function contractIt(assertion: string, name: string, fn: () => void) {
27
+ const key = `flow-legend--default|jsdom|jsdom|${assertion}`;
28
+ const reason = KNOWN_FAILURES[key];
29
+ if (reason) return it.fails(`${name} (known failure: ${reason})`, fn);
30
+ return it(name, fn);
31
+ }
32
+
33
+ describe("Legend contract", () => {
34
+ it.skipIf(!isForwardRefComponent)("forwards a ref to a DOM element", () => {
35
+ const ref = createRef<Element>();
36
+ render(<Component {...args} ref={ref} />);
37
+ expect(ref.current).toBeInstanceOf(Element);
38
+ });
39
+
40
+ contractIt("className", "merges a caller className onto the root", () => {
41
+ const { container } = render(<Component {...args} className="contract-probe" />);
42
+ expect(container.querySelector(".contract-probe")).not.toBeNull();
43
+ });
44
+
45
+ contractIt("data-slot", 'exposes data-slot="legend" on its root', () => {
46
+ const { container } = render(<Component {...args} />);
47
+ expect(container.querySelector('[data-slot="legend"]')).not.toBeNull();
48
+ });
49
+ });
@@ -51,6 +51,7 @@ const edges: Edge[] = [
51
51
  const meta = {
52
52
  title: "Flow/CanvasShell",
53
53
  component: CanvasShell,
54
+ tags: ["autodocs"],
54
55
  parameters: { layout: "fullscreen" },
55
56
  } satisfies Meta<typeof CanvasShell>;
56
57
  export default meta;
@@ -15,6 +15,7 @@ import {
15
15
  import { cn } from "@elabs-ai/components-ui/lib/cn";
16
16
  import { HelperLines } from "../helper-lines/helper-lines";
17
17
  import { useHelperLines } from "../helper-lines/use-helper-lines";
18
+ import { clampedFitOffset } from "./clamped-fit-offset";
18
19
  import { useMeasuredNodes } from "./use-measured-nodes";
19
20
 
20
21
  export interface CanvasShellProps<
@@ -52,6 +53,15 @@ export interface CanvasShellProps<
52
53
  fitViewKey?: string | number;
53
54
  /** Options for the {@link fitViewKey} re-fit. Ignored without one. */
54
55
  fitViewKeyOptions?: FitViewOptions;
56
+ /**
57
+ * Which nodes a CLAMPED {@link fitViewKey} re-fit keeps in view — typically where the
58
+ * diagram begins. A fit that cannot shrink far enough (a `minZoom` floor) is pinned to the
59
+ * content's top-left corner by default, which is where a top-down layout starts; a
60
+ * left-to-right layout starts at the left edge but at mid-height, so pinning to the top
61
+ * opens on an empty corner. With anchors, each overflowing axis centres the anchor nodes
62
+ * instead, without ever scrolling past the content's own edge. Unset: today's corner pin.
63
+ */
64
+ fitViewAnchorNodeIds?: readonly string[];
55
65
  /** Overlays rendered inside the flow (ZoomControls, Legend, Panels). */
56
66
  children?: ReactNode;
57
67
  className?: string;
@@ -120,6 +130,7 @@ function CanvasShellBase<NodeType extends Node, EdgeType extends Edge>({
120
130
  onNodesChange,
121
131
  fitViewKey,
122
132
  fitViewKeyOptions,
133
+ fitViewAnchorNodeIds,
123
134
  ...props
124
135
  }: CanvasShellInnerProps<NodeType, EdgeType>) {
125
136
  const measured = useMeasuredNodes<NodeType>(nodes, onNodesChange);
@@ -135,7 +146,11 @@ function CanvasShellBase<NodeType extends Node, EdgeType extends Edge>({
135
146
  >
136
147
  {background ? <Background gap={20} size={1} color="var(--canvas-grid)" /> : null}
137
148
  {fitViewKey === undefined ? null : (
138
- <FitViewOnKey fitViewKey={fitViewKey} options={fitViewKeyOptions} />
149
+ <FitViewOnKey
150
+ fitViewKey={fitViewKey}
151
+ options={fitViewKeyOptions}
152
+ anchorNodeIds={fitViewAnchorNodeIds}
153
+ />
139
154
  )}
140
155
  {children}
141
156
  </ReactFlow>
@@ -152,9 +167,11 @@ function CanvasShellBase<NodeType extends Node, EdgeType extends Edge>({
152
167
  function FitViewOnKey({
153
168
  fitViewKey,
154
169
  options,
170
+ anchorNodeIds,
155
171
  }: {
156
172
  fitViewKey: string | number;
157
173
  options?: FitViewOptions;
174
+ anchorNodeIds?: readonly string[];
158
175
  }) {
159
176
  const { fitView, getNodes, getNodesBounds, setViewport } = useReactFlow();
160
177
  const store = useStoreApi();
@@ -162,7 +179,14 @@ function FitViewOnKey({
162
179
  useEffect(() => {
163
180
  if (!nodesInitialized) return;
164
181
  void fitView(options).then(() =>
165
- anchorToStartWhenClamped(store, setViewport, getNodesBounds, getNodes(), options),
182
+ anchorToStartWhenClamped(
183
+ store,
184
+ setViewport,
185
+ getNodesBounds,
186
+ getNodes(),
187
+ options,
188
+ anchorNodeIds,
189
+ ),
166
190
  );
167
191
  // `options` is deliberately absent: an inline object literal would re-fit on every
168
192
  // render, which is a viewport jump under the reader's cursor. The key is the trigger.
@@ -196,6 +220,7 @@ function anchorToStartWhenClamped(
196
220
  getNodesBounds: ReturnType<typeof useReactFlow>["getNodesBounds"],
197
221
  nodes: Node[],
198
222
  options: FitViewOptions | undefined,
223
+ anchorNodeIds?: readonly string[],
199
224
  ): void {
200
225
  if (nodes.length === 0) return;
201
226
  const { width, height, transform, panZoom } = store.getState();
@@ -213,9 +238,31 @@ function anchorToStartWhenClamped(
213
238
  const overflowsY = bounds.height * zoom > height - padY * 2 + 1;
214
239
  if (!overflowsX && !overflowsY) return;
215
240
 
241
+ // With anchors: centre them on an overflowing axis, but never scroll past the content's
242
+ // own start or end. On the axis the diagram runs along, the anchors sit at the content's
243
+ // start, so this lands exactly where the corner pin does; on the cross axis it keeps the
244
+ // first rank in view instead of opening on whatever happens to be top-most.
245
+ const anchors = anchorNodeIds?.length
246
+ ? nodes.filter((node) => anchorNodeIds.includes(node.id))
247
+ : [];
248
+ const anchor = anchors.length > 0 ? getNodesBounds(anchors) : null;
249
+ const along = (size: number, pad: number, start: number, extent: number, centre: number) =>
250
+ clampedFitOffset({
251
+ size,
252
+ pad,
253
+ start,
254
+ extent,
255
+ zoom,
256
+ anchorCentre: anchor === null ? undefined : centre,
257
+ });
258
+
216
259
  const next = {
217
- x: overflowsX ? padX - bounds.x * zoom : x,
218
- y: overflowsY ? padY - bounds.y * zoom : y,
260
+ x: overflowsX
261
+ ? along(width, padX, bounds.x, bounds.width, (anchor?.x ?? 0) + (anchor?.width ?? 0) / 2)
262
+ : x,
263
+ y: overflowsY
264
+ ? along(height, padY, bounds.y, bounds.height, (anchor?.y ?? 0) + (anchor?.height ?? 0) / 2)
265
+ : y,
219
266
  zoom,
220
267
  };
221
268
  // `setViewport` is a no-op before the pan/zoom instance exists (the very first commit).
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { clampedFitOffset } from "./clamped-fit-offset";
3
+
4
+ describe("clampedFitOffset", () => {
5
+ const axis = { size: 1000, pad: 100, start: 0, extent: 4000, zoom: 0.75 };
6
+
7
+ it("pins the content's start at the padding when there is no anchor", () => {
8
+ expect(clampedFitOffset(axis)).toBe(100);
9
+ expect(clampedFitOffset({ ...axis, start: 200 })).toBe(100 - 200 * 0.75);
10
+ });
11
+
12
+ it("an anchor at the content's start lands exactly where the corner pin does", () => {
13
+ expect(clampedFitOffset({ ...axis, anchorCentre: 90 })).toBe(100);
14
+ });
15
+
16
+ it("centres an anchor that sits in the middle of the overflowing axis", () => {
17
+ // Anchor centre at 2000 → 1500 px at zoom 0.75 → offset 500 - 1500.
18
+ expect(clampedFitOffset({ ...axis, anchorCentre: 2000 })).toBe(-1000);
19
+ });
20
+
21
+ it("never scrolls past the content's end", () => {
22
+ // End pinned: 1000 - 100 - 4000 * 0.75 = -2100; an anchor at the very end asks for less.
23
+ expect(clampedFitOffset({ ...axis, anchorCentre: 3990 })).toBe(-2100);
24
+ });
25
+ });
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The viewport offset for ONE overflowing axis of a clamped fit (pure; exported for its test).
3
+ * Without an anchor the content's start is pinned at the padding. With one, the anchor is
4
+ * centred — but never so far that the pane shows empty space before the content's start or
5
+ * after its end.
6
+ */
7
+ export function clampedFitOffset({
8
+ size,
9
+ pad,
10
+ start,
11
+ extent,
12
+ zoom,
13
+ anchorCentre,
14
+ }: {
15
+ /** Pane size on this axis, px. */
16
+ size: number;
17
+ /** Padding the fit keeps on this axis, px. */
18
+ pad: number;
19
+ /** Content start and extent on this axis, in flow coordinates. */
20
+ start: number;
21
+ extent: number;
22
+ zoom: number;
23
+ /** Centre of the anchor nodes on this axis, in flow coordinates. */
24
+ anchorCentre?: number;
25
+ }): number {
26
+ const startPinned = pad - start * zoom;
27
+ if (anchorCentre === undefined) return startPinned;
28
+ const endPinned = size - pad - (start + extent) * zoom;
29
+ return Math.min(startPinned, Math.max(endPinned, size / 2 - anchorCentre * zoom));
30
+ }