@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
package/src/index.ts ADDED
@@ -0,0 +1,134 @@
1
+ // Public API for `@barefootjs/xyflow`.
2
+ //
3
+ // JSX-native renderer components (`<Flow>` / `<Background>` /
4
+ // `<Controls>` / `<MiniMap>` / `<Handle>` / `<NodeWrapper>` /
5
+ // `<SimpleEdge>`) are distributed via the shadcn registry at
6
+ // `ui/components/ui/xyflow/` — install with `bf add xyflow`.
7
+ // This package ships the utility helpers, types, signal hooks, store,
8
+ // and the imperative pointer-paced subsystems those components attach
9
+ // via `ref` callbacks.
10
+
11
+ // Store / context / signal hooks
12
+ export { createFlowStore } from './store'
13
+ export { FlowContext } from './context'
14
+ export {
15
+ useFlow,
16
+ useViewport,
17
+ useNodes,
18
+ useEdges,
19
+ useNodesInitialized,
20
+ useStore,
21
+ screenToFlowPosition,
22
+ } from './hooks'
23
+
24
+ // Geometry helpers consumed by the JSX `<SimpleEdge>` component
25
+ export { computeEdgePosition, getEdgePath } from './edge-path'
26
+ export type { EdgePathTuple } from './edge-path'
27
+
28
+ // Pointer-paced subsystems attached via `<Flow>` / `<Handle>` `ref`
29
+ // callbacks. JSX gives these no leverage — pan/zoom is owned by
30
+ // `XYPanZoom` (D3-zoom-derived), the selection rectangle owns global
31
+ // pointer capture, connection drag uses `elementFromPoint`, and the
32
+ // node resizer needs raw dimension math.
33
+ export { attachFlowSubsystems, clampDragPositionToParent } from './flow-subsystems'
34
+ export { attachConnectionHandler, attachReconnectionHandler } from './connection'
35
+ export { dispatchNodeType } from './node-type-dispatch'
36
+ export type { NodeInitFn } from './node-type-dispatch'
37
+ export { initNodeResizer, ResizeControlVariant } from './node-resizer'
38
+ export type {
39
+ NodeResizerOptions,
40
+ ControlPosition,
41
+ ControlLinePosition,
42
+ OnResize,
43
+ OnResizeStart,
44
+ OnResizeEnd,
45
+ ShouldResize,
46
+ ResizeControlDirection,
47
+ } from './node-resizer'
48
+ export { setupKeyboardHandlers, setupNodeSelection, setupSelectionRectangle } from './selection'
49
+ export type { SelectionRectOptions } from './selection'
50
+
51
+ // Stable CSS class names for the registry-side JSX components.
52
+ // Imported (rather than declared as inline literals) so site/ui's
53
+ // cssLayerPrefixer leaves the `bf-flow*` names un-prefixed, matching
54
+ // the chart pattern (`CHART_CLASS_*` from `@barefootjs/chart`).
55
+ export {
56
+ BF_FLOW,
57
+ BF_FLOW_VIEWPORT,
58
+ BF_FLOW_EDGES,
59
+ BF_FLOW_NODES,
60
+ BF_FLOW_NODE,
61
+ BF_FLOW_NODE_GROUP,
62
+ BF_FLOW_NODE_CHILD,
63
+ BF_FLOW_NODE_SELECTED,
64
+ BF_FLOW_NODE_CUSTOM,
65
+ BF_FLOW_EDGE,
66
+ BF_FLOW_EDGE_SELECTED,
67
+ BF_FLOW_EDGE_ANIMATED,
68
+ BF_FLOW_HANDLE,
69
+ BF_FLOW_HANDLE_TARGET,
70
+ BF_FLOW_HANDLE_SOURCE,
71
+ BF_FLOW_CONTROLS,
72
+ BF_FLOW_CONTROLS_BUTTON,
73
+ BF_FLOW_MINIMAP,
74
+ BF_FLOW_MINIMAP_MASK,
75
+ XYFLOW_VIEWPORT,
76
+ } from './classes'
77
+
78
+
79
+ // Types
80
+ export type {
81
+ FlowProps,
82
+ FlowStore,
83
+ InternalFlowStore,
84
+ FlowStoreOptions,
85
+ FitViewOptions,
86
+ NodeBase,
87
+ EdgeBase,
88
+ InternalNodeBase,
89
+ Viewport,
90
+ NodeLookup,
91
+ ParentLookup,
92
+ EdgeLookup,
93
+ ConnectionLookup,
94
+ CoordinateExtent,
95
+ SnapGrid,
96
+ NodeOrigin,
97
+ Transform,
98
+ PanZoomInstance,
99
+ XYPosition,
100
+ OnConnect,
101
+ OnConnectStart,
102
+ OnConnectEnd,
103
+ IsValidConnection,
104
+ NodeDragItem,
105
+ ConnectionMode,
106
+ NodeComponentProps,
107
+ EdgeComponentProps,
108
+ SelectionMode,
109
+ OnReconnect,
110
+ Connection,
111
+ } from './types'
112
+ // HandleType is consumed by the JSX `<Handle>` component (registry-side)
113
+ // for its `type` prop typing. Re-exported here from `@xyflow/system` so
114
+ // consumers don't need a separate import.
115
+ export type { HandleType } from '@xyflow/system'
116
+
117
+ // Compat layer (React Flow API shims for desk migration)
118
+ export { useNodesState, useEdgesState, useReactFlow, addEdge, reconnectEdge } from './compat'
119
+
120
+ // Re-export useful utilities from @xyflow/system
121
+ export {
122
+ getBezierPath,
123
+ getSmoothStepPath,
124
+ getStraightPath,
125
+ getConnectedEdges,
126
+ getOutgoers,
127
+ getIncomers,
128
+ getNodesBounds,
129
+ getNodesInside,
130
+ getEdgeToolbarTransform,
131
+ Position,
132
+ ConnectionMode as ConnectionModeEnum,
133
+ MarkerType,
134
+ } from '@xyflow/system'
@@ -0,0 +1,276 @@
1
+ import { onCleanup, untrack } from '@barefootjs/client'
2
+ import {
3
+ XYResizer,
4
+ XY_RESIZER_HANDLE_POSITIONS,
5
+ XY_RESIZER_LINE_POSITIONS,
6
+ ResizeControlVariant,
7
+ updateNodeInternals,
8
+ } from '@xyflow/system'
9
+ import type {
10
+ NodeBase,
11
+ InternalNodeUpdate,
12
+ ControlPosition,
13
+ ControlLinePosition,
14
+ OnResize,
15
+ OnResizeStart,
16
+ OnResizeEnd,
17
+ ShouldResize,
18
+ ResizeControlDirection,
19
+ } from '@xyflow/system'
20
+ import type { XYResizerChange, XYResizerChildChange, XYResizerInstance } from '@xyflow/system'
21
+ import type { FlowStore } from './types'
22
+
23
+ /**
24
+ * Options for initNodeResizer.
25
+ */
26
+ export type NodeResizerOptions = {
27
+ /** Minimum width the node can be resized to (default: 10) */
28
+ minWidth?: number
29
+ /** Minimum height the node can be resized to (default: 10) */
30
+ minHeight?: number
31
+ /** Maximum width the node can be resized to (default: Infinity) */
32
+ maxWidth?: number
33
+ /** Maximum height the node can be resized to (default: Infinity) */
34
+ maxHeight?: number
35
+ /** Whether to keep the aspect ratio during resize (default: false) */
36
+ keepAspectRatio?: boolean
37
+ /** Which variant of resize controls to use: 'handle' (corners) or 'line' (edges) */
38
+ variant?: ResizeControlVariant | 'handle' | 'line'
39
+ /** Callback fired when resize starts */
40
+ onResizeStart?: OnResizeStart
41
+ /** Callback fired during resize with new dimensions */
42
+ onResize?: OnResize
43
+ /** Callback fired when resize ends */
44
+ onResizeEnd?: OnResizeEnd
45
+ /** Callback to determine if resize should proceed */
46
+ shouldResize?: ShouldResize
47
+ /** Whether the node is resizable (default: true) */
48
+ isVisible?: boolean
49
+ /** CSS color for the resize handle lines/corners */
50
+ color?: string
51
+ }
52
+
53
+ /**
54
+ * Initialize resize handles on a node element.
55
+ *
56
+ * Creates resize handle elements (corners and/or lines) and attaches
57
+ * XYResizer from @xyflow/system for the resize logic.
58
+ *
59
+ * Usage:
60
+ * // Inside a custom node type function:
61
+ * initNodeResizer(this.parentElement, {
62
+ * nodeId: props.id,
63
+ * store,
64
+ * minWidth: 100,
65
+ * minHeight: 50,
66
+ * onResize: (event, params) => console.log('Resized:', params),
67
+ * })
68
+ */
69
+ export function initNodeResizer<NodeType extends NodeBase>(
70
+ nodeEl: HTMLElement,
71
+ nodeId: string,
72
+ store: FlowStore<NodeType>,
73
+ options: NodeResizerOptions = {},
74
+ ): () => void {
75
+ const {
76
+ minWidth = 10,
77
+ minHeight = 10,
78
+ maxWidth = Number.MAX_SAFE_INTEGER,
79
+ maxHeight = Number.MAX_SAFE_INTEGER,
80
+ keepAspectRatio = false,
81
+ variant = ResizeControlVariant.Handle,
82
+ onResizeStart,
83
+ onResize,
84
+ onResizeEnd,
85
+ shouldResize,
86
+ isVisible = true,
87
+ color,
88
+ } = options
89
+
90
+ if (!isVisible) return () => {}
91
+
92
+ const resolvedVariant =
93
+ typeof variant === 'string'
94
+ ? variant === 'line'
95
+ ? ResizeControlVariant.Line
96
+ : ResizeControlVariant.Handle
97
+ : variant
98
+
99
+ // Determine which positions to use based on variant
100
+ const positions: ControlPosition[] =
101
+ resolvedVariant === ResizeControlVariant.Line
102
+ ? (XY_RESIZER_LINE_POSITIONS as ControlPosition[])
103
+ : XY_RESIZER_HANDLE_POSITIONS
104
+
105
+ // Mark node as resizable for CSS
106
+ nodeEl.classList.add('bf-flow__node--resizable')
107
+
108
+ // Container for resize handles
109
+ const container = document.createElement('div')
110
+ container.className = 'bf-flow__node-resizer'
111
+ nodeEl.appendChild(container)
112
+
113
+ const resizerInstances: XYResizerInstance[] = []
114
+
115
+ for (const position of positions) {
116
+ const handleEl = document.createElement('div')
117
+ handleEl.className = `bf-flow__resize-handle bf-flow__resize-handle--${position}`
118
+
119
+ if (resolvedVariant === ResizeControlVariant.Line) {
120
+ handleEl.classList.add('bf-flow__resize-handle--line')
121
+ } else {
122
+ handleEl.classList.add('bf-flow__resize-handle--corner')
123
+ }
124
+
125
+ handleEl.dataset.position = position
126
+
127
+ if (color) {
128
+ handleEl.style.setProperty('--bf-resize-color', color)
129
+ }
130
+
131
+ container.appendChild(handleEl)
132
+
133
+ // Create XYResizer instance for this handle
134
+ const resizerInstance = XYResizer({
135
+ domNode: handleEl as HTMLDivElement,
136
+ nodeId,
137
+ getStoreItems: () => {
138
+ const nodeLookup = untrack(store.nodeLookup)
139
+ const transform = store.getTransform()
140
+ return {
141
+ nodeLookup,
142
+ transform,
143
+ snapGrid: store.snapToGrid ? store.snapGrid : undefined,
144
+ snapToGrid: store.snapToGrid,
145
+ nodeOrigin: store.nodeOrigin,
146
+ paneDomNode: untrack(store.domNode) as HTMLDivElement | null,
147
+ }
148
+ },
149
+ onChange: (changes: XYResizerChange, childChanges: XYResizerChildChange[]) => {
150
+ // Apply dimension and position changes to the node
151
+ const lookup = untrack(store.nodeLookup)
152
+ const node = lookup.get(nodeId)
153
+ if (!node) return
154
+
155
+ // Update measured dimensions
156
+ if (changes.width != null) {
157
+ node.measured.width = changes.width
158
+ nodeEl.style.width = `${changes.width}px`
159
+ }
160
+ if (changes.height != null) {
161
+ node.measured.height = changes.height
162
+ nodeEl.style.height = `${changes.height}px`
163
+ }
164
+
165
+ // Update position if changed (e.g., resizing from top-left)
166
+ if (changes.x != null || changes.y != null) {
167
+ const newX = changes.x ?? node.internals.positionAbsolute.x
168
+ const newY = changes.y ?? node.internals.positionAbsolute.y
169
+
170
+ node.internals.positionAbsolute = { x: newX, y: newY }
171
+ node.internals.userNode.position = { x: newX, y: newY }
172
+ nodeEl.style.transform = `translate(${newX}px, ${newY}px)`
173
+ }
174
+
175
+ // Apply child position changes
176
+ for (const childChange of childChanges) {
177
+ const childNode = lookup.get(childChange.id)
178
+ if (childNode) {
179
+ childNode.internals.positionAbsolute = childChange.position
180
+ childNode.internals.userNode.position = childChange.position
181
+ }
182
+ }
183
+
184
+ // Update node internals for edge recalculation
185
+ const updates = new Map<string, InternalNodeUpdate>()
186
+ updates.set(nodeId, {
187
+ id: nodeId,
188
+ nodeElement: nodeEl as HTMLDivElement,
189
+ force: true,
190
+ })
191
+
192
+ const parentLookup = untrack(store.parentLookup)
193
+ updateNodeInternals(
194
+ updates,
195
+ lookup,
196
+ parentLookup,
197
+ untrack(store.domNode),
198
+ store.nodeOrigin,
199
+ store.nodeExtent,
200
+ )
201
+
202
+ // Notify position-dependent subscribers (edges etc.)
203
+ store.triggerPositionUpdate()
204
+ },
205
+ onEnd: (change) => {
206
+ // Commit final dimensions to the nodes array
207
+ store.setNodes((prev) =>
208
+ prev.map((n) =>
209
+ n.id === nodeId
210
+ ? {
211
+ ...n,
212
+ position: { x: change.x, y: change.y },
213
+ measured: { width: change.width, height: change.height },
214
+ style: {
215
+ ...(n as any).style,
216
+ width: change.width,
217
+ height: change.height,
218
+ },
219
+ }
220
+ : n,
221
+ ),
222
+ )
223
+ },
224
+ })
225
+
226
+ // Determine resize direction for line handles
227
+ const isLineHandle = resolvedVariant === ResizeControlVariant.Line
228
+ let resizeDirection: ResizeControlDirection | undefined
229
+ if (isLineHandle) {
230
+ if (position === 'left' || position === 'right') {
231
+ resizeDirection = 'horizontal'
232
+ } else if (position === 'top' || position === 'bottom') {
233
+ resizeDirection = 'vertical'
234
+ }
235
+ }
236
+
237
+ // Configure the resizer instance
238
+ resizerInstance.update({
239
+ controlPosition: position,
240
+ boundaries: { minWidth, minHeight, maxWidth, maxHeight },
241
+ keepAspectRatio,
242
+ resizeDirection,
243
+ onResizeStart,
244
+ onResize,
245
+ onResizeEnd,
246
+ shouldResize,
247
+ })
248
+
249
+ resizerInstances.push(resizerInstance)
250
+ }
251
+
252
+ // Cleanup function
253
+ const cleanup = () => {
254
+ for (const instance of resizerInstances) {
255
+ instance.destroy()
256
+ }
257
+ container.remove()
258
+ nodeEl.classList.remove('bf-flow__node--resizable')
259
+ }
260
+
261
+ onCleanup(cleanup)
262
+
263
+ return cleanup
264
+ }
265
+
266
+ // Re-export types that consumers might need
267
+ export { ResizeControlVariant }
268
+ export type {
269
+ ControlPosition,
270
+ ControlLinePosition,
271
+ OnResize,
272
+ OnResizeStart,
273
+ OnResizeEnd,
274
+ ShouldResize,
275
+ ResizeControlDirection,
276
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Node-type dispatch — the helper that funnels `<Flow nodeTypes={...}>`
3
+ * entries into the host `<div>` produced by `FlowNodeTypeBridge`.
4
+ *
5
+ * `nodeTypes` accepts two entry shapes through the same map so projects
6
+ * can migrate custom nodes from imperative to JSX one file at a time
7
+ * without touching every `<Flow>` call site:
8
+ *
9
+ * 1. Imperative: `function MyNode(this: HTMLElement, props): void`
10
+ * — mutates `this` (the host element) and returns nothing.
11
+ * 2. JSX-component shim: post-`'use client'` `.tsx` compile output
12
+ * — ignores `this` and returns a real DOM `Node`. The bridge
13
+ * appends that returned node into the host so subsequent
14
+ * wipe+rebuild cycles see it.
15
+ *
16
+ * See piconic-ai/barefootjs#1236 for the migration motivation.
17
+ */
18
+
19
+ import type { NodeBase } from './types'
20
+ import type { NodeComponentProps } from './types'
21
+
22
+ /**
23
+ * Cross-shape `nodeTypes` entry. The return type covers both supported
24
+ * shapes — imperative entries return `void`, JSX-component shim
25
+ * entries return a `Node`.
26
+ */
27
+ export type NodeInitFn<NodeType extends NodeBase = NodeBase> = (
28
+ this: HTMLElement,
29
+ props: NodeComponentProps<NodeType>,
30
+ ) => void | Node | undefined
31
+
32
+ /**
33
+ * Dispatch a `nodeTypes` entry into the host element. Invokes `initFn`
34
+ * with `el` bound as `this` (imperative path) and appends its return
35
+ * value when it is a `Node` (JSX-component shim path). The
36
+ * `instanceof Node` guard is harmless on the imperative path: `void`
37
+ * is not an instance of `Node`, so the branch is skipped.
38
+ */
39
+ export function dispatchNodeType<NT extends NodeBase>(
40
+ el: HTMLElement,
41
+ initFn: NodeInitFn<NT>,
42
+ props: NodeComponentProps<NT>,
43
+ ): void {
44
+ const result = initFn.call(el, props)
45
+ if (result instanceof Node) el.appendChild(result)
46
+ }