@barefootjs/xyflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +117 -0
  2. package/dist/classes.d.ts +31 -0
  3. package/dist/classes.d.ts.map +1 -0
  4. package/dist/compat.d.ts +67 -0
  5. package/dist/compat.d.ts.map +1 -0
  6. package/dist/connection.d.ts +20 -0
  7. package/dist/connection.d.ts.map +1 -0
  8. package/dist/constants.d.ts +4 -0
  9. package/dist/constants.d.ts.map +1 -0
  10. package/dist/context.d.ts +7 -0
  11. package/dist/context.d.ts.map +1 -0
  12. package/dist/edge-path.d.ts +14 -0
  13. package/dist/edge-path.d.ts.map +1 -0
  14. package/dist/flow-subsystems.d.ts +28 -0
  15. package/dist/flow-subsystems.d.ts.map +1 -0
  16. package/dist/hooks.d.ts +34 -0
  17. package/dist/hooks.d.ts.map +1 -0
  18. package/dist/index.d.ts +19 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +6572 -0
  21. package/dist/node-resizer.d.ts +52 -0
  22. package/dist/node-resizer.d.ts.map +1 -0
  23. package/dist/node-type-dispatch.d.ts +34 -0
  24. package/dist/node-type-dispatch.d.ts.map +1 -0
  25. package/dist/selection.d.ts +32 -0
  26. package/dist/selection.d.ts.map +1 -0
  27. package/dist/store.d.ts +8 -0
  28. package/dist/store.d.ts.map +1 -0
  29. package/dist/types.d.ts +214 -0
  30. package/dist/types.d.ts.map +1 -0
  31. package/dist/utils.d.ts +6 -0
  32. package/dist/utils.d.ts.map +1 -0
  33. package/dist/xyflow.browser.min.js +95 -0
  34. package/dist/xyflow.browser.min.js.map +129 -0
  35. package/package.json +58 -0
  36. package/src/__tests__/clamp-drag-position.test.ts +111 -0
  37. package/src/__tests__/compat.test.ts +157 -0
  38. package/src/__tests__/host-element-store.test.ts +33 -0
  39. package/src/__tests__/jsx-smoke.test.ts +33 -0
  40. package/src/__tests__/jsx-smoke.tsx +23 -0
  41. package/src/__tests__/node-type-dispatch.test.ts +104 -0
  42. package/src/__tests__/store.test.ts +399 -0
  43. package/src/__tests__/tsconfig.json +23 -0
  44. package/src/classes.ts +41 -0
  45. package/src/compat.ts +237 -0
  46. package/src/connection.ts +459 -0
  47. package/src/constants.ts +8 -0
  48. package/src/context.ts +8 -0
  49. package/src/edge-path.ts +89 -0
  50. package/src/flow-subsystems.ts +506 -0
  51. package/src/hooks.ts +72 -0
  52. package/src/index.ts +134 -0
  53. package/src/node-resizer.ts +276 -0
  54. package/src/node-type-dispatch.ts +46 -0
  55. package/src/selection.ts +407 -0
  56. package/src/store.ts +526 -0
  57. package/src/types.ts +329 -0
  58. package/src/utils.ts +13 -0
@@ -0,0 +1,459 @@
1
+ import { untrack } from '@barefootjs/client'
2
+ import { getSmoothStepPath, Position, reconnectEdge as reconnectEdgeUtil } from '@xyflow/system'
3
+ import type { FlowStore, NodeBase, EdgeBase, Connection } from './types'
4
+ import { SVG_NS } from './constants'
5
+
6
+ /**
7
+ * Build a connection object for the given source handle / target handle pair.
8
+ * Used by both validation and edge creation.
9
+ */
10
+ function buildConnection(
11
+ sourceNodeId: string,
12
+ targetNodeId: string,
13
+ handleType: 'source' | 'target',
14
+ sourceHandleId?: string | null,
15
+ targetHandleId?: string | null,
16
+ ): { source: string; target: string; sourceHandle: string | null; targetHandle: string | null } {
17
+ let source = sourceNodeId
18
+ let target = targetNodeId
19
+ let sourceHandle = sourceHandleId ?? null
20
+ let targetHandle = targetHandleId ?? null
21
+ if (handleType === 'target') {
22
+ source = targetNodeId
23
+ target = sourceNodeId
24
+ // Swap handle IDs when direction is reversed
25
+ const tmp = sourceHandle
26
+ sourceHandle = targetHandle
27
+ targetHandle = tmp
28
+ }
29
+ return { source, target, sourceHandle, targetHandle }
30
+ }
31
+
32
+ /**
33
+ * Check whether a proposed connection is valid according to the store's
34
+ * isValidConnection callback. Returns true when no callback is configured.
35
+ */
36
+ function checkConnectionValidity<
37
+ NodeType extends NodeBase = NodeBase,
38
+ EdgeType extends EdgeBase = EdgeBase,
39
+ >(
40
+ store: FlowStore<NodeType, EdgeType>,
41
+ connection: { source: string; target: string; sourceHandle: string | null; targetHandle: string | null },
42
+ ): boolean {
43
+ if (!store.isValidConnection) return true
44
+ return store.isValidConnection(connection)
45
+ }
46
+
47
+ /**
48
+ * Attach a connection drag handler to a handle element.
49
+ * Called when creating each handle in node-wrapper.
50
+ */
51
+ export function attachConnectionHandler<
52
+ NodeType extends NodeBase = NodeBase,
53
+ EdgeType extends EdgeBase = EdgeBase,
54
+ >(
55
+ handleEl: HTMLElement,
56
+ nodeId: string,
57
+ handleType: 'source' | 'target',
58
+ container: HTMLElement,
59
+ _edgesSvg: SVGSVGElement,
60
+ store: FlowStore<NodeType, EdgeType>,
61
+ ): void {
62
+ handleEl.addEventListener('mousedown', (e) => {
63
+ if (e.button !== 0) return
64
+ if (!untrack(store.nodesDraggable)) return
65
+
66
+ // Stop propagation to prevent node drag
67
+ e.stopPropagation()
68
+ e.preventDefault()
69
+
70
+ // Compute source position in flow coordinates at mousedown time
71
+ const handleRect = handleEl.getBoundingClientRect()
72
+ const containerRect0 = container.getBoundingClientRect()
73
+ const [, , scale0] = store.getTransform()
74
+ const vp0 = untrack(store.viewport)
75
+
76
+ const sourceX = (handleRect.left + handleRect.width / 2 - containerRect0.left - vp0.x) / scale0
77
+ const sourceY = (handleRect.top + handleRect.height / 2 - containerRect0.top - vp0.y) / scale0
78
+
79
+ // Create a temporary overlay SVG for the connection line, above nodes.
80
+ // This makes the line visible on top of nodes during drag.
81
+ // We hide it briefly before elementFromPoint calls so handles are detected.
82
+ const overlaySvg = document.createElementNS(SVG_NS, 'svg')
83
+ overlaySvg.style.position = 'absolute'
84
+ overlaySvg.style.top = '0'
85
+ overlaySvg.style.left = '0'
86
+ overlaySvg.style.width = '100%'
87
+ overlaySvg.style.height = '100%'
88
+ overlaySvg.style.overflow = 'visible'
89
+ overlaySvg.style.pointerEvents = 'none'
90
+ overlaySvg.style.zIndex = '10'
91
+ container.appendChild(overlaySvg)
92
+
93
+ // The line is drawn in viewport-transformed coordinates
94
+ const lineGroup = document.createElementNS(SVG_NS, 'g')
95
+ overlaySvg.appendChild(lineGroup)
96
+
97
+ const connectionLine = document.createElementNS(SVG_NS, 'path')
98
+ connectionLine.setAttribute('fill', 'none')
99
+ // Apply connectionLineStyle from store if available
100
+ const lineStyle = store.connectionLineStyle
101
+ connectionLine.setAttribute('stroke', lineStyle?.stroke ?? '#b1b1b7')
102
+ connectionLine.setAttribute('stroke-width', lineStyle?.strokeWidth ?? '1')
103
+ lineGroup.appendChild(connectionLine)
104
+
105
+ // Track the currently highlighted handle for validation/snap feedback
106
+ let lastHighlightedHandle: HTMLElement | null = null
107
+ // Track the snapped handle so onMouseUp can use it without re-querying
108
+ let snappedHandle: HTMLElement | null = null
109
+
110
+ const SNAP_THRESHOLD = 30
111
+
112
+ /**
113
+ * Find the nearest valid target handle within SNAP_THRESHOLD pixels of
114
+ * the cursor. Returns null when none is close enough.
115
+ */
116
+ function findNearestHandle(cursorX: number, cursorY: number): HTMLElement | null {
117
+ const candidates = container.querySelectorAll<HTMLElement>('.bf-flow__handle')
118
+ let nearest: HTMLElement | null = null
119
+ let nearestDist = SNAP_THRESHOLD
120
+
121
+ for (const candidate of candidates) {
122
+ if (candidate === handleEl) continue
123
+ if (!candidate.dataset.nodeId || candidate.dataset.nodeId === nodeId) continue
124
+ // Skip same-type handles — source can only connect to target and vice versa.
125
+ // When source+target handles overlap at the same position, without this
126
+ // check the source handle (first in DOM) would always win and fail validation.
127
+ const candidateType = candidate.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
128
+ if (candidateType === handleType) continue
129
+
130
+ const rect = candidate.getBoundingClientRect()
131
+ const cx = rect.left + rect.width / 2
132
+ const cy = rect.top + rect.height / 2
133
+ const dist = Math.hypot(cursorX - cx, cursorY - cy)
134
+ if (dist < nearestDist) {
135
+ nearestDist = dist
136
+ nearest = candidate
137
+ }
138
+ }
139
+ return nearest
140
+ }
141
+
142
+ const onMouseMove = (e: MouseEvent) => {
143
+ // Read fresh viewport and container rect each move — the user
144
+ // may pan/zoom while drawing a connection.
145
+ const containerRect = container.getBoundingClientRect()
146
+ const [, , scale] = store.getTransform()
147
+ const vp = untrack(store.viewport)
148
+
149
+ // Determine if we should snap to a nearby handle
150
+ const nearHandle = findNearestHandle(e.clientX, e.clientY)
151
+
152
+ let targetX: number
153
+ let targetY: number
154
+
155
+ if (nearHandle) {
156
+ const rect = nearHandle.getBoundingClientRect()
157
+ targetX = (rect.left + rect.width / 2 - containerRect.left - vp.x) / scale
158
+ targetY = (rect.top + rect.height / 2 - containerRect.top - vp.y) / scale
159
+ } else {
160
+ targetX = (e.clientX - containerRect.left - vp.x) / scale
161
+ targetY = (e.clientY - containerRect.top - vp.y) / scale
162
+ }
163
+
164
+ const [path] = getSmoothStepPath({
165
+ sourceX,
166
+ sourceY,
167
+ sourcePosition: handleType === 'source' ? Position.Bottom : Position.Top,
168
+ targetX,
169
+ targetY,
170
+ targetPosition: handleType === 'source' ? Position.Top : Position.Bottom,
171
+ })
172
+
173
+ connectionLine.setAttribute('d', path)
174
+
175
+ // Sync overlay SVG transform with viewport
176
+ const vpCurrent = untrack(store.viewport)
177
+ lineGroup.setAttribute('transform', `translate(${vpCurrent.x}, ${vpCurrent.y}) scale(${vpCurrent.zoom})`)
178
+
179
+ // Clear classes from the previously highlighted handle
180
+ if (lastHighlightedHandle && lastHighlightedHandle !== nearHandle) {
181
+ lastHighlightedHandle.classList.remove('valid', 'invalid')
182
+ }
183
+
184
+ snappedHandle = null
185
+
186
+ if (nearHandle) {
187
+ const nearHandleType = nearHandle.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
188
+ const isCompatibleType = handleType !== nearHandleType
189
+ const srcHandleId = handleEl.getAttribute("data-handleid") ?? null
190
+ const tgtHandleId = nearHandle.getAttribute("data-handleid") ?? null
191
+ const conn = buildConnection(nodeId, nearHandle.dataset.nodeId!, handleType, srcHandleId, tgtHandleId)
192
+ const isValid = isCompatibleType && checkConnectionValidity(store, conn)
193
+
194
+ nearHandle.classList.remove('valid', 'invalid')
195
+ nearHandle.classList.add(isValid ? 'valid' : 'invalid')
196
+ lastHighlightedHandle = nearHandle
197
+
198
+ if (isValid) snappedHandle = nearHandle
199
+ } else {
200
+ // Fall back to elementFromPoint for hover-only feedback (cursor directly on handle)
201
+ overlaySvg.style.display = 'none'
202
+ const hoverEl = document.elementFromPoint(e.clientX, e.clientY)
203
+ overlaySvg.style.display = ''
204
+ const hoveredHandle = hoverEl?.closest?.('.bf-flow__handle') as HTMLElement | null
205
+
206
+ if (
207
+ hoveredHandle &&
208
+ hoveredHandle !== handleEl &&
209
+ hoveredHandle.dataset.nodeId &&
210
+ hoveredHandle.dataset.nodeId !== nodeId
211
+ ) {
212
+ const hoveredHandleType = hoveredHandle.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
213
+ const isCompatibleType = handleType !== hoveredHandleType
214
+ const srcHandleId = handleEl.getAttribute("data-handleid") ?? null
215
+ const tgtHandleId = hoveredHandle.getAttribute("data-handleid") ?? null
216
+ const conn = buildConnection(nodeId, hoveredHandle.dataset.nodeId, handleType, srcHandleId, tgtHandleId)
217
+ const isValid = isCompatibleType && checkConnectionValidity(store, conn)
218
+
219
+ hoveredHandle.classList.remove('valid', 'invalid')
220
+ if (!isValid) hoveredHandle.classList.add('invalid')
221
+ lastHighlightedHandle = hoveredHandle
222
+ } else {
223
+ lastHighlightedHandle = null
224
+ }
225
+ }
226
+ }
227
+
228
+ const onMouseUp = (e: MouseEvent) => {
229
+ document.removeEventListener('mousemove', onMouseMove)
230
+ document.removeEventListener('mouseup', onMouseUp)
231
+
232
+ // Clean up validation/snap classes from any highlighted handle
233
+ if (lastHighlightedHandle) {
234
+ lastHighlightedHandle.classList.remove('valid', 'invalid')
235
+ }
236
+
237
+ // Prefer the snapped handle; fall back to direct hit-test
238
+ let targetHandle = snappedHandle
239
+
240
+ if (!targetHandle) {
241
+ overlaySvg.style.display = 'none'
242
+ const targetEl = document.elementFromPoint(e.clientX, e.clientY)
243
+ overlaySvg.style.display = ''
244
+ targetHandle = targetEl?.closest?.('.bf-flow__handle') as HTMLElement | null
245
+ }
246
+
247
+ if (
248
+ targetHandle &&
249
+ targetHandle.dataset.nodeId &&
250
+ targetHandle.dataset.nodeId !== nodeId
251
+ ) {
252
+ const targetNodeId = targetHandle.dataset.nodeId
253
+ const targetHandleType = targetHandle.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
254
+ const isCompatibleType = handleType !== targetHandleType
255
+ const srcHandleId = handleEl.getAttribute("data-handleid") ?? null
256
+ const tgtHandleId = targetHandle.getAttribute("data-handleid") ?? null
257
+ const conn = buildConnection(nodeId, targetNodeId, handleType, srcHandleId, tgtHandleId)
258
+
259
+ // Validate: handle type must be compatible + custom validation
260
+ const isValid = isCompatibleType && checkConnectionValidity(store, conn)
261
+
262
+ if (isValid) {
263
+ if (store.onConnect) {
264
+ // When onConnect is provided, the consumer is responsible for
265
+ // creating the edge (matching React Flow behaviour).
266
+ store.onConnect(conn)
267
+ } else {
268
+ // Default: auto-create a plain edge when no onConnect handler
269
+ const edgeId = `e-${conn.source}-${conn.target}-${Date.now()}`
270
+ const newEdge = {
271
+ id: edgeId,
272
+ source: conn.source,
273
+ target: conn.target,
274
+ sourceHandle: conn.sourceHandle ?? undefined,
275
+ targetHandle: conn.targetHandle ?? undefined,
276
+ } as EdgeType
277
+ store.addEdge(newEdge)
278
+ }
279
+ }
280
+ }
281
+
282
+ // Remove connection line overlay
283
+ overlaySvg.remove()
284
+ }
285
+
286
+ document.addEventListener('mousemove', onMouseMove)
287
+ document.addEventListener('mouseup', onMouseUp)
288
+ })
289
+ }
290
+
291
+ /**
292
+ * Attach a reconnection drag handler to an edge endpoint handle.
293
+ * Dragging this handle detaches the edge from its source/target and allows
294
+ * reconnecting to a different handle.
295
+ *
296
+ * @param handleEl - The SVG circle element acting as the reconnection grip
297
+ * @param edge - The edge being reconnected
298
+ * @param endpointType - Which endpoint of the edge is being dragged ('source' | 'target')
299
+ * @param container - The flow container element
300
+ * @param edgesSvg - The SVG element containing edge paths
301
+ * @param store - The flow store
302
+ */
303
+ export function attachReconnectionHandler<
304
+ NodeType extends NodeBase = NodeBase,
305
+ EdgeType extends EdgeBase = EdgeBase,
306
+ >(
307
+ handleEl: SVGCircleElement,
308
+ edge: EdgeType,
309
+ endpointType: 'source' | 'target',
310
+ container: HTMLElement,
311
+ edgesSvg: SVGSVGElement,
312
+ store: FlowStore<NodeType, EdgeType>,
313
+ ): void {
314
+ handleEl.addEventListener('mousedown', (e) => {
315
+ if (e.button !== 0) return
316
+ e.stopPropagation()
317
+ e.preventDefault()
318
+
319
+ // The fixed anchor is the opposite endpoint of the edge
320
+ const anchorNodeId = endpointType === 'source' ? edge.target : edge.source
321
+
322
+ // Determine anchor position from the node
323
+ const nodeLookup = untrack(store.nodeLookup)
324
+ const anchorNode = nodeLookup.get(anchorNodeId)
325
+ if (!anchorNode) return
326
+
327
+ const anchorW = anchorNode.measured.width ?? 150
328
+ const anchorH = anchorNode.measured.height ?? 40
329
+ const anchorPos = anchorNode.internals.positionAbsolute
330
+
331
+ // For the anchor, use the handle position appropriate for the fixed end:
332
+ // If we're dragging the "source" end, the fixed anchor is the "target" end
333
+ // (which has a handle at the top). If dragging "target", anchor is "source" (bottom).
334
+ const anchorX = anchorPos.x + anchorW / 2
335
+ const anchorY = endpointType === 'source'
336
+ ? anchorPos.y // target handle is at top
337
+ : anchorPos.y + anchorH // source handle is at bottom
338
+
339
+ // Hide the original edge path while reconnecting
340
+ const edgePathEl = edgesSvg.querySelector(`.bf-flow__edge[data-id="${edge.id}"]`) as SVGPathElement | null
341
+ const hitPathEl = edgesSvg.querySelector(`path[data-hit-id="${edge.id}"]`) as SVGPathElement | null
342
+ if (edgePathEl) edgePathEl.style.opacity = '0.2'
343
+ if (hitPathEl) hitPathEl.style.display = 'none'
344
+
345
+ // Create temporary connection line from anchor to cursor
346
+ const connectionLine = document.createElementNS(SVG_NS, 'path')
347
+ connectionLine.setAttribute('fill', 'none')
348
+ const reconnectLineStyle = store.connectionLineStyle
349
+ connectionLine.setAttribute('stroke', reconnectLineStyle?.stroke ?? '#b1b1b7')
350
+ connectionLine.setAttribute('stroke-width', reconnectLineStyle?.strokeWidth ?? '1')
351
+ connectionLine.setAttribute('pointer-events', 'none')
352
+ edgesSvg.appendChild(connectionLine)
353
+
354
+ let lastHoveredHandle: HTMLElement | null = null
355
+
356
+ const onMouseMove = (ev: MouseEvent) => {
357
+ const containerRect = container.getBoundingClientRect()
358
+ const [, , scale] = store.getTransform()
359
+ const vp = untrack(store.viewport)
360
+
361
+ const cursorX = (ev.clientX - containerRect.left - vp.x) / scale
362
+ const cursorY = (ev.clientY - containerRect.top - vp.y) / scale
363
+
364
+ // Draw smoothstep path from anchor to cursor
365
+ // sourcePosition/targetPosition depends on which endpoint is the anchor
366
+ const sourcePosition = endpointType === 'source' ? Position.Top : Position.Bottom
367
+ const targetPosition = endpointType === 'source' ? Position.Bottom : Position.Top
368
+
369
+ const [path] = getSmoothStepPath({
370
+ sourceX: anchorX,
371
+ sourceY: anchorY,
372
+ sourcePosition,
373
+ targetX: cursorX,
374
+ targetY: cursorY,
375
+ targetPosition,
376
+ })
377
+
378
+ connectionLine.setAttribute('d', path)
379
+
380
+ // Validate on hover over handles
381
+ const hoverEl = document.elementFromPoint(ev.clientX, ev.clientY)
382
+ const hoveredHandle = hoverEl?.closest?.('.bf-flow__handle') as HTMLElement | null
383
+
384
+ if (lastHoveredHandle && lastHoveredHandle !== hoveredHandle) {
385
+ lastHoveredHandle.classList.remove('invalid')
386
+ }
387
+
388
+ if (
389
+ hoveredHandle &&
390
+ hoveredHandle.dataset.nodeId &&
391
+ hoveredHandle.dataset.nodeId !== anchorNodeId
392
+ ) {
393
+ // Build connection based on the hovered handle's type
394
+ const hoveredNodeId = hoveredHandle.dataset.nodeId
395
+ const hoveredHandleType = hoveredHandle.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
396
+ const conn: Connection = hoveredHandleType === 'target'
397
+ ? { source: anchorNodeId, target: hoveredNodeId, sourceHandle: null, targetHandle: null }
398
+ : { source: hoveredNodeId, target: anchorNodeId, sourceHandle: null, targetHandle: null }
399
+
400
+ const isValid = checkConnectionValidity(store, conn)
401
+ hoveredHandle.classList.remove('invalid')
402
+ if (!isValid) hoveredHandle.classList.add('invalid')
403
+ lastHoveredHandle = hoveredHandle
404
+ } else {
405
+ lastHoveredHandle = null
406
+ }
407
+ }
408
+
409
+ const onMouseUp = (ev: MouseEvent) => {
410
+ document.removeEventListener('mousemove', onMouseMove)
411
+ document.removeEventListener('mouseup', onMouseUp)
412
+
413
+ if (lastHoveredHandle) {
414
+ lastHoveredHandle.classList.remove('invalid')
415
+ }
416
+
417
+ // Restore the original edge appearance
418
+ if (edgePathEl) edgePathEl.style.opacity = ''
419
+ if (hitPathEl) hitPathEl.style.display = ''
420
+
421
+ // Check if released on a valid handle
422
+ const targetEl = document.elementFromPoint(ev.clientX, ev.clientY)
423
+ const droppedHandle = targetEl?.closest?.('.bf-flow__handle') as HTMLElement | null
424
+
425
+ if (
426
+ droppedHandle &&
427
+ droppedHandle.dataset.nodeId &&
428
+ droppedHandle.dataset.nodeId !== anchorNodeId
429
+ ) {
430
+ const droppedNodeId = droppedHandle.dataset.nodeId
431
+ // Determine connection direction from the dropped handle's type
432
+ const droppedHandleType = droppedHandle.classList.contains('bf-flow__handle--target') ? 'target' : 'source'
433
+ const newConnection: Connection = droppedHandleType === 'target'
434
+ ? { source: anchorNodeId, target: droppedNodeId, sourceHandle: null, targetHandle: null }
435
+ : { source: droppedNodeId, target: anchorNodeId, sourceHandle: null, targetHandle: null }
436
+
437
+ const isValid = checkConnectionValidity(store, newConnection)
438
+
439
+ if (isValid) {
440
+ // Fire onReconnect callback
441
+ if (store.onReconnect) {
442
+ store.onReconnect(edge, newConnection)
443
+ }
444
+
445
+ // Update edges using reconnectEdge utility
446
+ const currentEdges = untrack(store.edges)
447
+ const updatedEdges = reconnectEdgeUtil(edge, newConnection, currentEdges)
448
+ store.setEdges(updatedEdges as EdgeType[])
449
+ }
450
+ }
451
+ // If not dropped on a valid handle, the edge reverts (appearance already restored)
452
+
453
+ connectionLine.remove()
454
+ }
455
+
456
+ document.addEventListener('mousemove', onMouseMove)
457
+ document.addEventListener('mouseup', onMouseUp)
458
+ })
459
+ }
@@ -0,0 +1,8 @@
1
+ import type { CoordinateExtent } from '@xyflow/system'
2
+
3
+ export const SVG_NS = 'http://www.w3.org/2000/svg'
4
+
5
+ export const INFINITE_EXTENT: CoordinateExtent = [
6
+ [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
7
+ [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
8
+ ]
package/src/context.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { createContext } from '@barefootjs/client'
2
+ import type { FlowStore } from './types'
3
+
4
+ /**
5
+ * Context for sharing the flow store across child components.
6
+ * Provided by initFlow, consumed by child init functions (e.g., handles, custom nodes).
7
+ */
8
+ export const FlowContext = createContext<FlowStore>()
@@ -0,0 +1,89 @@
1
+ // Pure path-geometry helpers shared between the imperative edge renderer
2
+ // (edge-renderer.ts) and the JSX-native simple-edge component (simple-edge.tsx).
3
+ // Extracted in #1081 step 2 so the JSX path computes geometry the same way
4
+ // the imperative path does, avoiding subtle divergence during the migration.
5
+
6
+ import {
7
+ getBezierPath,
8
+ getSmoothStepPath,
9
+ getStraightPath,
10
+ getEdgePosition,
11
+ ConnectionMode,
12
+ Position,
13
+ } from '@xyflow/system'
14
+ import type { EdgeBase, EdgePosition, InternalNodeBase } from '@xyflow/system'
15
+
16
+ export type EdgePathTuple = [string, number, number, number, number]
17
+
18
+ /**
19
+ * Compute endpoint positions for an edge, falling back to a node-center
20
+ * straight line when no handle bounds are available yet (e.g. before the
21
+ * first measure has populated `nodeLookup`).
22
+ */
23
+ export function computeEdgePosition(
24
+ edge: EdgeBase,
25
+ sourceNode: InternalNodeBase,
26
+ targetNode: InternalNodeBase,
27
+ ): EdgePosition | null {
28
+ const hasHandleIds = !!(edge.sourceHandle || edge.targetHandle)
29
+ const edgePos = getEdgePosition({
30
+ id: edge.id,
31
+ sourceNode,
32
+ sourceHandle: edge.sourceHandle ?? null,
33
+ targetNode,
34
+ targetHandle: edge.targetHandle ?? null,
35
+ connectionMode: hasHandleIds ? ConnectionMode.Strict : ConnectionMode.Loose,
36
+ })
37
+
38
+ if (edgePos) return edgePos
39
+
40
+ const sw = sourceNode.measured.width ?? 150
41
+ const sh = sourceNode.measured.height ?? 40
42
+ const tw = targetNode.measured.width ?? 150
43
+ const sourcePos = sourceNode.internals.positionAbsolute
44
+ const targetPos = targetNode.internals.positionAbsolute
45
+
46
+ return {
47
+ sourceX: sourcePos.x + sw / 2,
48
+ sourceY: sourcePos.y + sh,
49
+ targetX: targetPos.x + tw / 2,
50
+ targetY: targetPos.y,
51
+ sourcePosition: Position.Bottom,
52
+ targetPosition: Position.Top,
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Calculate edge path based on edge type. Returns
58
+ * `[d, labelX, labelY, offsetX, offsetY]` or `null`.
59
+ */
60
+ export function getEdgePath(
61
+ edge: EdgeBase,
62
+ pos: EdgePosition,
63
+ ): EdgePathTuple | null {
64
+ const params = {
65
+ sourceX: pos.sourceX,
66
+ sourceY: pos.sourceY,
67
+ sourcePosition: pos.sourcePosition,
68
+ targetX: pos.targetX,
69
+ targetY: pos.targetY,
70
+ targetPosition: pos.targetPosition,
71
+ }
72
+
73
+ const edgeType = edge.type ?? 'default'
74
+
75
+ switch (edgeType) {
76
+ case 'straight':
77
+ return getStraightPath(params) as EdgePathTuple
78
+ case 'smoothstep':
79
+ case 'step':
80
+ return getSmoothStepPath({
81
+ ...params,
82
+ borderRadius: edgeType === 'step' ? 0 : undefined,
83
+ }) as EdgePathTuple
84
+ case 'default':
85
+ case 'bezier':
86
+ default:
87
+ return getBezierPath(params)
88
+ }
89
+ }