@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.
- package/README.md +117 -0
- package/dist/classes.d.ts +31 -0
- package/dist/classes.d.ts.map +1 -0
- package/dist/compat.d.ts +67 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/connection.d.ts +20 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/context.d.ts +7 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/edge-path.d.ts +14 -0
- package/dist/edge-path.d.ts.map +1 -0
- package/dist/flow-subsystems.d.ts +28 -0
- package/dist/flow-subsystems.d.ts.map +1 -0
- package/dist/hooks.d.ts +34 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6572 -0
- package/dist/node-resizer.d.ts +52 -0
- package/dist/node-resizer.d.ts.map +1 -0
- package/dist/node-type-dispatch.d.ts +34 -0
- package/dist/node-type-dispatch.d.ts.map +1 -0
- package/dist/selection.d.ts +32 -0
- package/dist/selection.d.ts.map +1 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/xyflow.browser.min.js +95 -0
- package/dist/xyflow.browser.min.js.map +129 -0
- package/package.json +58 -0
- package/src/__tests__/clamp-drag-position.test.ts +111 -0
- package/src/__tests__/compat.test.ts +157 -0
- package/src/__tests__/host-element-store.test.ts +33 -0
- package/src/__tests__/jsx-smoke.test.ts +33 -0
- package/src/__tests__/jsx-smoke.tsx +23 -0
- package/src/__tests__/node-type-dispatch.test.ts +104 -0
- package/src/__tests__/store.test.ts +399 -0
- package/src/__tests__/tsconfig.json +23 -0
- package/src/classes.ts +41 -0
- package/src/compat.ts +237 -0
- package/src/connection.ts +459 -0
- package/src/constants.ts +8 -0
- package/src/context.ts +8 -0
- package/src/edge-path.ts +89 -0
- package/src/flow-subsystems.ts +506 -0
- package/src/hooks.ts +72 -0
- package/src/index.ts +134 -0
- package/src/node-resizer.ts +276 -0
- package/src/node-type-dispatch.ts +46 -0
- package/src/selection.ts +407 -0
- package/src/store.ts +526 -0
- package/src/types.ts +329 -0
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test'
|
|
2
|
+
import { createEffect, createRoot } from '@barefootjs/client'
|
|
3
|
+
import { createFlowStore } from '../store'
|
|
4
|
+
|
|
5
|
+
describe('createFlowStore', () => {
|
|
6
|
+
test('returns initial empty state', () => {
|
|
7
|
+
createRoot(() => {
|
|
8
|
+
const store = createFlowStore()
|
|
9
|
+
expect(store.nodes()).toEqual([])
|
|
10
|
+
expect(store.edges()).toEqual([])
|
|
11
|
+
expect(store.viewport()).toEqual({ x: 0, y: 0, zoom: 1 })
|
|
12
|
+
expect(store.width()).toBe(0)
|
|
13
|
+
expect(store.height()).toBe(0)
|
|
14
|
+
expect(store.dragging()).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('accepts initial nodes and edges', () => {
|
|
19
|
+
createRoot(() => {
|
|
20
|
+
const nodes = [
|
|
21
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
|
|
22
|
+
{ id: '2', position: { x: 100, y: 50 }, data: { label: 'B' } },
|
|
23
|
+
]
|
|
24
|
+
const edges = [{ id: 'e1-2', source: '1', target: '2' }]
|
|
25
|
+
|
|
26
|
+
const store = createFlowStore({ nodes, edges })
|
|
27
|
+
expect(store.nodes()).toHaveLength(2)
|
|
28
|
+
expect(store.edges()).toHaveLength(1)
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('accepts custom viewport', () => {
|
|
33
|
+
createRoot(() => {
|
|
34
|
+
const store = createFlowStore({
|
|
35
|
+
defaultViewport: { x: 50, y: 100, zoom: 1.5 },
|
|
36
|
+
})
|
|
37
|
+
expect(store.viewport()).toEqual({ x: 50, y: 100, zoom: 1.5 })
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('setNodes triggers reactive updates', () => {
|
|
42
|
+
createRoot(() => {
|
|
43
|
+
const store = createFlowStore()
|
|
44
|
+
const updates: number[] = []
|
|
45
|
+
|
|
46
|
+
createEffect(() => {
|
|
47
|
+
updates.push(store.nodes().length)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(updates).toEqual([0])
|
|
51
|
+
|
|
52
|
+
store.setNodes([
|
|
53
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
expect(updates).toEqual([0, 1])
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('setEdges triggers reactive updates', () => {
|
|
61
|
+
createRoot(() => {
|
|
62
|
+
const store = createFlowStore()
|
|
63
|
+
const updates: number[] = []
|
|
64
|
+
|
|
65
|
+
createEffect(() => {
|
|
66
|
+
updates.push(store.edges().length)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
expect(updates).toEqual([0])
|
|
70
|
+
|
|
71
|
+
store.setEdges([{ id: 'e1', source: '1', target: '2' }])
|
|
72
|
+
|
|
73
|
+
expect(updates).toEqual([0, 1])
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('per-node selected getter tracks setNodes updates', () => {
|
|
78
|
+
// Exercises the pattern NodeComponentProps.selected uses internally:
|
|
79
|
+
// a getter that looks the node up in store.nodes() so custom components
|
|
80
|
+
// can observe selection changes after mount via createEffect.
|
|
81
|
+
createRoot(() => {
|
|
82
|
+
const store = createFlowStore({
|
|
83
|
+
nodes: [
|
|
84
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
85
|
+
{ id: '2', position: { x: 10, y: 0 }, data: {} },
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const isSelected = (id: string) => () => {
|
|
90
|
+
const n = store.nodes().find((n) => n.id === id) as
|
|
91
|
+
| { selected?: boolean }
|
|
92
|
+
| undefined
|
|
93
|
+
return n?.selected ?? false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const log: boolean[] = []
|
|
97
|
+
const selectedOne = isSelected('1')
|
|
98
|
+
createEffect(() => {
|
|
99
|
+
log.push(selectedOne())
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(log).toEqual([false])
|
|
103
|
+
|
|
104
|
+
store.setNodes((nds) =>
|
|
105
|
+
nds.map((n) => (n.id === '1' ? { ...n, selected: true } : n)),
|
|
106
|
+
)
|
|
107
|
+
expect(log).toEqual([false, true])
|
|
108
|
+
|
|
109
|
+
// Selecting a different node should not re-emit for node 1 since
|
|
110
|
+
// the underlying value didn't change — but store.nodes() did, so
|
|
111
|
+
// the effect re-runs. We just assert the latest value is still true.
|
|
112
|
+
store.setNodes((nds) =>
|
|
113
|
+
nds.map((n) => (n.id === '2' ? { ...n, selected: true } : n)),
|
|
114
|
+
)
|
|
115
|
+
expect(log[log.length - 1]).toBe(true)
|
|
116
|
+
|
|
117
|
+
store.setNodes((nds) =>
|
|
118
|
+
nds.map((n) => (n.id === '1' ? { ...n, selected: false } : n)),
|
|
119
|
+
)
|
|
120
|
+
expect(log[log.length - 1]).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test('setViewport triggers reactive updates', () => {
|
|
125
|
+
createRoot(() => {
|
|
126
|
+
const store = createFlowStore()
|
|
127
|
+
const viewports: Array<{ x: number; y: number; zoom: number }> = []
|
|
128
|
+
|
|
129
|
+
createEffect(() => {
|
|
130
|
+
viewports.push(store.viewport())
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(viewports).toEqual([{ x: 0, y: 0, zoom: 1 }])
|
|
134
|
+
|
|
135
|
+
store.setViewport({ x: 10, y: 20, zoom: 2 })
|
|
136
|
+
|
|
137
|
+
expect(viewports).toEqual([
|
|
138
|
+
{ x: 0, y: 0, zoom: 1 },
|
|
139
|
+
{ x: 10, y: 20, zoom: 2 },
|
|
140
|
+
])
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('nodesInitialized processes nodes through adoptUserNodes', () => {
|
|
145
|
+
createRoot(() => {
|
|
146
|
+
const nodes = [
|
|
147
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { label: 'A' } },
|
|
148
|
+
{ id: '2', position: { x: 100, y: 50 }, data: { label: 'B' } },
|
|
149
|
+
]
|
|
150
|
+
const store = createFlowStore({ nodes })
|
|
151
|
+
|
|
152
|
+
// nodesInitialized triggers node processing
|
|
153
|
+
const initialized = store.nodesInitialized()
|
|
154
|
+
|
|
155
|
+
// Nodes without measured dimensions are not "initialized"
|
|
156
|
+
expect(initialized).toBe(false)
|
|
157
|
+
|
|
158
|
+
// But nodeLookup should be populated
|
|
159
|
+
const lookup = store.nodeLookup()
|
|
160
|
+
expect(lookup.size).toBe(2)
|
|
161
|
+
expect(lookup.has('1')).toBe(true)
|
|
162
|
+
expect(lookup.has('2')).toBe(true)
|
|
163
|
+
|
|
164
|
+
// Internal node should have positionAbsolute
|
|
165
|
+
const node1 = lookup.get('1')!
|
|
166
|
+
expect(node1.internals.positionAbsolute).toEqual({ x: 0, y: 0 })
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('nodeLookup emits a fresh Map on setNodes-driven update (#1270)', () => {
|
|
171
|
+
// Regression for #1270: `adoptUserNodes` mutates the underlying
|
|
172
|
+
// Map in place, and the old code re-emitted the same reference.
|
|
173
|
+
// barefoot's Object.is dedupe swallowed the notification so per-
|
|
174
|
+
// node consumers stayed stuck on the initial value.
|
|
175
|
+
createRoot(() => {
|
|
176
|
+
const store = createFlowStore({
|
|
177
|
+
nodes: [
|
|
178
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { count: 0 } },
|
|
179
|
+
{ id: '2', position: { x: 10, y: 0 }, data: {} },
|
|
180
|
+
],
|
|
181
|
+
})
|
|
182
|
+
// Touch `nodesInitialized` so the memo evaluates and the lookup
|
|
183
|
+
// is populated before the assertions below.
|
|
184
|
+
store.nodesInitialized()
|
|
185
|
+
|
|
186
|
+
let fires = 0
|
|
187
|
+
let lastSeen: unknown
|
|
188
|
+
createEffect(() => {
|
|
189
|
+
const lookup = store.nodeLookup()
|
|
190
|
+
const entry = lookup.get('1')
|
|
191
|
+
lastSeen = (entry?.internals.userNode.data as { count?: number } | undefined)?.count
|
|
192
|
+
fires++
|
|
193
|
+
})
|
|
194
|
+
expect(fires).toBe(1)
|
|
195
|
+
expect(lastSeen).toBe(0)
|
|
196
|
+
|
|
197
|
+
store.setNodes((nds) =>
|
|
198
|
+
nds.map((n) => (n.id === '1' ? { ...n, data: { count: 1 } } : n)) as typeof nds,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
expect(fires).toBe(2)
|
|
202
|
+
expect(lastSeen).toBe(1)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('nodeSignal(id) fires only for the changed id (#1270 acceptance)', () => {
|
|
207
|
+
// Acceptance criterion from #1270: a single-node setNodes update
|
|
208
|
+
// must NOT wake up every per-node consumer. `nodeSignal(id)`
|
|
209
|
+
// delivers the fine-grained subscription that satisfies this.
|
|
210
|
+
createRoot(() => {
|
|
211
|
+
const store = createFlowStore({
|
|
212
|
+
nodes: [
|
|
213
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { count: 0 } },
|
|
214
|
+
{ id: '2', position: { x: 10, y: 0 }, data: { count: 0 } },
|
|
215
|
+
],
|
|
216
|
+
})
|
|
217
|
+
store.nodesInitialized()
|
|
218
|
+
|
|
219
|
+
let firesForOne = 0
|
|
220
|
+
let firesForTwo = 0
|
|
221
|
+
createEffect(() => {
|
|
222
|
+
store.nodeSignal('1')
|
|
223
|
+
firesForOne++
|
|
224
|
+
})
|
|
225
|
+
createEffect(() => {
|
|
226
|
+
store.nodeSignal('2')
|
|
227
|
+
firesForTwo++
|
|
228
|
+
})
|
|
229
|
+
expect([firesForOne, firesForTwo]).toEqual([1, 1])
|
|
230
|
+
|
|
231
|
+
// Mutate only node '1' — node '2's subscriber must NOT wake up.
|
|
232
|
+
store.setNodes((nds) =>
|
|
233
|
+
nds.map((n) => (n.id === '1' ? { ...n, data: { count: 1 } } : n)) as typeof nds,
|
|
234
|
+
)
|
|
235
|
+
expect([firesForOne, firesForTwo]).toEqual([2, 1])
|
|
236
|
+
|
|
237
|
+
// Mutate only node '2' via `selected` — node '1's subscriber stays asleep.
|
|
238
|
+
store.setNodes((nds) =>
|
|
239
|
+
nds.map((n) => (n.id === '2' ? { ...n, selected: true } : n)) as typeof nds,
|
|
240
|
+
)
|
|
241
|
+
expect([firesForOne, firesForTwo]).toEqual([2, 2])
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('nodeSignal(id) survives remove and re-add of the same id (#1270)', () => {
|
|
246
|
+
// Slots are retained across structural changes so a consumer that
|
|
247
|
+
// subscribed before the node was removed (or before it existed)
|
|
248
|
+
// resumes receiving updates when the node reappears.
|
|
249
|
+
createRoot(() => {
|
|
250
|
+
const store = createFlowStore({
|
|
251
|
+
nodes: [
|
|
252
|
+
{ id: '1', position: { x: 0, y: 0 }, data: { count: 7 } },
|
|
253
|
+
],
|
|
254
|
+
})
|
|
255
|
+
store.nodesInitialized()
|
|
256
|
+
|
|
257
|
+
const seen: Array<number | undefined> = []
|
|
258
|
+
createEffect(() => {
|
|
259
|
+
const entry = store.nodeSignal('1')
|
|
260
|
+
seen.push((entry?.internals.userNode.data as { count?: number } | undefined)?.count)
|
|
261
|
+
})
|
|
262
|
+
expect(seen).toEqual([7])
|
|
263
|
+
|
|
264
|
+
store.setNodes([])
|
|
265
|
+
expect(seen).toEqual([7, undefined])
|
|
266
|
+
|
|
267
|
+
store.setNodes([{ id: '1', position: { x: 0, y: 0 }, data: { count: 99 } }])
|
|
268
|
+
expect(seen).toEqual([7, undefined, 99])
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('edgeLookup is derived from edges', () => {
|
|
273
|
+
createRoot(() => {
|
|
274
|
+
const edges = [
|
|
275
|
+
{ id: 'e1', source: '1', target: '2' },
|
|
276
|
+
{ id: 'e2', source: '2', target: '3' },
|
|
277
|
+
]
|
|
278
|
+
const store = createFlowStore({ edges })
|
|
279
|
+
|
|
280
|
+
const lookup = store.edgeLookup()
|
|
281
|
+
expect(lookup.size).toBe(2)
|
|
282
|
+
expect(lookup.has('e1')).toBe(true)
|
|
283
|
+
expect(lookup.has('e2')).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('getTransform returns [x, y, zoom] tuple', () => {
|
|
288
|
+
createRoot(() => {
|
|
289
|
+
const store = createFlowStore({
|
|
290
|
+
defaultViewport: { x: 10, y: 20, zoom: 1.5 },
|
|
291
|
+
})
|
|
292
|
+
expect(store.getTransform()).toEqual([10, 20, 1.5])
|
|
293
|
+
})
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('configuration defaults', () => {
|
|
297
|
+
createRoot(() => {
|
|
298
|
+
const store = createFlowStore()
|
|
299
|
+
expect(store.minZoom).toBe(0.5)
|
|
300
|
+
expect(store.maxZoom).toBe(2)
|
|
301
|
+
expect(store.nodeOrigin).toEqual([0, 0])
|
|
302
|
+
expect(store.snapToGrid).toBe(false)
|
|
303
|
+
expect(store.snapGrid).toEqual([15, 15])
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test('configuration overrides', () => {
|
|
308
|
+
createRoot(() => {
|
|
309
|
+
const store = createFlowStore({
|
|
310
|
+
minZoom: 0.1,
|
|
311
|
+
maxZoom: 5,
|
|
312
|
+
snapToGrid: true,
|
|
313
|
+
snapGrid: [10, 10],
|
|
314
|
+
nodeOrigin: [0.5, 0.5],
|
|
315
|
+
})
|
|
316
|
+
expect(store.minZoom).toBe(0.1)
|
|
317
|
+
expect(store.maxZoom).toBe(5)
|
|
318
|
+
expect(store.snapToGrid).toBe(true)
|
|
319
|
+
expect(store.snapGrid).toEqual([10, 10])
|
|
320
|
+
expect(store.nodeOrigin).toEqual([0.5, 0.5])
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('addEdge adds to edges', () => {
|
|
325
|
+
createRoot(() => {
|
|
326
|
+
const store = createFlowStore()
|
|
327
|
+
expect(store.edges()).toHaveLength(0)
|
|
328
|
+
|
|
329
|
+
store.addEdge({ id: 'e1', source: '1', target: '2' })
|
|
330
|
+
expect(store.edges()).toHaveLength(1)
|
|
331
|
+
expect(store.edges()[0].id).toBe('e1')
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
test('deleteElements removes nodes and connected edges', () => {
|
|
336
|
+
createRoot(() => {
|
|
337
|
+
const store = createFlowStore({
|
|
338
|
+
nodes: [
|
|
339
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {} },
|
|
340
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {} },
|
|
341
|
+
{ id: '3', position: { x: 200, y: 0 }, data: {} },
|
|
342
|
+
],
|
|
343
|
+
edges: [
|
|
344
|
+
{ id: 'e1-2', source: '1', target: '2' },
|
|
345
|
+
{ id: 'e2-3', source: '2', target: '3' },
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
store.deleteElements({ nodes: [{ id: '2', position: { x: 100, y: 0 }, data: {} }] })
|
|
350
|
+
|
|
351
|
+
expect(store.nodes()).toHaveLength(2)
|
|
352
|
+
expect(store.nodes().map((n) => n.id)).toEqual(['1', '3'])
|
|
353
|
+
// Both edges connected to node 2 should be removed
|
|
354
|
+
expect(store.edges()).toHaveLength(0)
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
test('deleteElements removes only specified edges', () => {
|
|
359
|
+
createRoot(() => {
|
|
360
|
+
const store = createFlowStore({
|
|
361
|
+
edges: [
|
|
362
|
+
{ id: 'e1', source: '1', target: '2' },
|
|
363
|
+
{ id: 'e2', source: '2', target: '3' },
|
|
364
|
+
],
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
store.deleteElements({ edges: [{ id: 'e1', source: '1', target: '2' }] })
|
|
368
|
+
|
|
369
|
+
expect(store.edges()).toHaveLength(1)
|
|
370
|
+
expect(store.edges()[0].id).toBe('e2')
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('unselectNodesAndEdges deselects all', () => {
|
|
375
|
+
createRoot(() => {
|
|
376
|
+
const store = createFlowStore({
|
|
377
|
+
nodes: [
|
|
378
|
+
{ id: '1', position: { x: 0, y: 0 }, data: {}, selected: true },
|
|
379
|
+
{ id: '2', position: { x: 100, y: 0 }, data: {}, selected: true },
|
|
380
|
+
],
|
|
381
|
+
edges: [
|
|
382
|
+
{ id: 'e1', source: '1', target: '2', selected: true },
|
|
383
|
+
],
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
store.unselectNodesAndEdges()
|
|
387
|
+
|
|
388
|
+
expect(store.nodes().every((n) => !n.selected)).toBe(true)
|
|
389
|
+
expect(store.edges().every((e) => !e.selected)).toBe(true)
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('multiSelectionActive defaults to false', () => {
|
|
394
|
+
createRoot(() => {
|
|
395
|
+
const store = createFlowStore()
|
|
396
|
+
expect(store.multiSelectionActive()).toBe(false)
|
|
397
|
+
})
|
|
398
|
+
})
|
|
399
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"jsxImportSource": "@barefootjs/jsx",
|
|
13
|
+
"types": ["bun"],
|
|
14
|
+
"paths": {
|
|
15
|
+
"@barefootjs/jsx": ["../../../jsx/src"],
|
|
16
|
+
"@barefootjs/jsx/jsx-runtime": ["../../../jsx/src/jsx-runtime/index.d.ts"],
|
|
17
|
+
"@barefootjs/jsx/jsx-dev-runtime": ["../../../jsx/src/jsx-dev-runtime/index.d.ts"],
|
|
18
|
+
"@barefootjs/client": ["../../../client/src"],
|
|
19
|
+
"@barefootjs/test": ["../../../test/src"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["./**/*.ts", "./**/*.tsx"]
|
|
23
|
+
}
|
package/src/classes.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable CSS class names for xyflow primitives.
|
|
3
|
+
*
|
|
4
|
+
* Exported as constants so the registry-side `<Flow>` / `<Background>` /
|
|
5
|
+
* etc. can reference them via `className={BF_FLOW}` instead of
|
|
6
|
+
* `className="bf-flow"`. site/ui's `cssLayerPrefixer` only rewrites
|
|
7
|
+
* locally-declared static `className` literals; imported identifiers are
|
|
8
|
+
* left alone, which keeps `bf-flow*` un-prefixed for the e2e selectors
|
|
9
|
+
* (the same trick chart uses with `CHART_CLASS_GRID` etc.).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const BF_FLOW = 'bf-flow'
|
|
13
|
+
export const BF_FLOW_VIEWPORT = 'bf-flow__viewport'
|
|
14
|
+
export const BF_FLOW_EDGES = 'bf-flow__edges'
|
|
15
|
+
export const BF_FLOW_NODES = 'bf-flow__nodes'
|
|
16
|
+
|
|
17
|
+
export const BF_FLOW_NODE = 'bf-flow__node'
|
|
18
|
+
export const BF_FLOW_NODE_GROUP = 'bf-flow__node--group'
|
|
19
|
+
export const BF_FLOW_NODE_CHILD = 'bf-flow__node--child'
|
|
20
|
+
export const BF_FLOW_NODE_SELECTED = 'bf-flow__node--selected'
|
|
21
|
+
// Strips the default node card styling (padding / border / background)
|
|
22
|
+
// so a `renderNode` / `nodeTypes` body provides its own visual.
|
|
23
|
+
export const BF_FLOW_NODE_CUSTOM = 'bf-flow__node--custom'
|
|
24
|
+
|
|
25
|
+
export const BF_FLOW_EDGE = 'bf-flow__edge'
|
|
26
|
+
export const BF_FLOW_EDGE_SELECTED = 'bf-flow__edge--selected'
|
|
27
|
+
export const BF_FLOW_EDGE_ANIMATED = 'bf-flow__edge--animated'
|
|
28
|
+
|
|
29
|
+
export const BF_FLOW_HANDLE = 'bf-flow__handle'
|
|
30
|
+
export const BF_FLOW_HANDLE_TARGET = 'bf-flow__handle--target'
|
|
31
|
+
export const BF_FLOW_HANDLE_SOURCE = 'bf-flow__handle--source'
|
|
32
|
+
|
|
33
|
+
export const BF_FLOW_CONTROLS = 'bf-flow__controls'
|
|
34
|
+
export const BF_FLOW_CONTROLS_BUTTON = 'bf-flow__controls-button'
|
|
35
|
+
|
|
36
|
+
export const BF_FLOW_MINIMAP = 'bf-flow__minimap'
|
|
37
|
+
export const BF_FLOW_MINIMAP_MASK = 'bf-flow__minimap-mask'
|
|
38
|
+
|
|
39
|
+
// `xyflow__viewport` is a @xyflow/system compatibility class kept on the
|
|
40
|
+
// viewport wrapper.
|
|
41
|
+
export const XYFLOW_VIEWPORT = 'xyflow__viewport'
|
package/src/compat.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Flow compatibility shims for desk migration.
|
|
3
|
+
*
|
|
4
|
+
* These functions mirror the React Flow hooks API so that
|
|
5
|
+
* desk components can be ported with minimal changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { untrack } from '@barefootjs/client'
|
|
9
|
+
import { addEdge as addEdgeUtil, reconnectEdge as reconnectEdgeUtil, pointToRendererPoint } from '@xyflow/system'
|
|
10
|
+
import { useFlow } from './hooks'
|
|
11
|
+
import type { NodeBase, EdgeBase, Viewport, XYPosition } from './types'
|
|
12
|
+
import type { Connection, NodeChange, EdgeChange } from '@xyflow/system'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* useNodesState — mirrors React Flow's useNodesState.
|
|
16
|
+
* Returns [nodes getter, setNodes, onNodesChange handler].
|
|
17
|
+
*/
|
|
18
|
+
export function useNodesState<NodeType extends NodeBase = NodeBase>(
|
|
19
|
+
initialNodes: NodeType[],
|
|
20
|
+
): [() => NodeType[], (updater: NodeType[] | ((prev: NodeType[]) => NodeType[])) => void, (changes: NodeChange[]) => void] {
|
|
21
|
+
const store = useFlow<NodeType>()
|
|
22
|
+
store.setNodes(initialNodes)
|
|
23
|
+
|
|
24
|
+
function onNodesChange(changes: NodeChange[]) {
|
|
25
|
+
store.setNodes((prev) => applyNodeChanges(prev, changes))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [store.nodes, store.setNodes, onNodesChange]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* useEdgesState — mirrors React Flow's useEdgesState.
|
|
33
|
+
* Returns [edges getter, setEdges, onEdgesChange handler].
|
|
34
|
+
*/
|
|
35
|
+
export function useEdgesState<EdgeType extends EdgeBase = EdgeBase>(
|
|
36
|
+
initialEdges: EdgeType[],
|
|
37
|
+
): [() => EdgeType[], (updater: EdgeType[] | ((prev: EdgeType[]) => EdgeType[])) => void, (changes: EdgeChange[]) => void] {
|
|
38
|
+
const store = useFlow<NodeBase, EdgeType>()
|
|
39
|
+
store.setEdges(initialEdges)
|
|
40
|
+
|
|
41
|
+
function onEdgesChange(changes: EdgeChange[]) {
|
|
42
|
+
store.setEdges((prev) => applyEdgeChanges(prev, changes))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [store.edges, store.setEdges, onEdgesChange]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* useReactFlow — mirrors React Flow's useReactFlow hook.
|
|
50
|
+
* Returns an object with common flow manipulation methods.
|
|
51
|
+
*/
|
|
52
|
+
export function useReactFlow<
|
|
53
|
+
NodeType extends NodeBase = NodeBase,
|
|
54
|
+
EdgeType extends EdgeBase = EdgeBase,
|
|
55
|
+
>() {
|
|
56
|
+
const store = useFlow<NodeType, EdgeType>()
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
// Getters
|
|
60
|
+
getNodes: (): NodeType[] => untrack(store.nodes),
|
|
61
|
+
getEdges: (): EdgeType[] => untrack(store.edges),
|
|
62
|
+
getNode: (id: string): NodeType | undefined =>
|
|
63
|
+
untrack(store.nodes).find((n) => n.id === id),
|
|
64
|
+
getZoom: (): number => untrack(store.viewport).zoom,
|
|
65
|
+
getViewport: (): Viewport => untrack(store.viewport),
|
|
66
|
+
|
|
67
|
+
// Setters
|
|
68
|
+
setNodes: store.setNodes,
|
|
69
|
+
setEdges: store.setEdges,
|
|
70
|
+
setViewport: (vp: Viewport) => {
|
|
71
|
+
const pz = untrack(store.panZoom)
|
|
72
|
+
if (pz) pz.setViewport(vp)
|
|
73
|
+
},
|
|
74
|
+
setCenter: (x: number, y: number, options?: { zoom?: number; duration?: number }) => {
|
|
75
|
+
const pz = untrack(store.panZoom)
|
|
76
|
+
if (!pz) return
|
|
77
|
+
const w = untrack(store.width)
|
|
78
|
+
const h = untrack(store.height)
|
|
79
|
+
const zoom = options?.zoom ?? untrack(store.viewport).zoom
|
|
80
|
+
pz.setViewport(
|
|
81
|
+
{ x: w / 2 - x * zoom, y: h / 2 - y * zoom, zoom },
|
|
82
|
+
{ duration: options?.duration },
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Actions
|
|
87
|
+
fitView: store.fitView,
|
|
88
|
+
zoomIn: (options?: { duration?: number }) => {
|
|
89
|
+
const pz = untrack(store.panZoom)
|
|
90
|
+
pz?.scaleBy(1.2, options)
|
|
91
|
+
},
|
|
92
|
+
zoomOut: (options?: { duration?: number }) => {
|
|
93
|
+
const pz = untrack(store.panZoom)
|
|
94
|
+
pz?.scaleBy(1 / 1.2, options)
|
|
95
|
+
},
|
|
96
|
+
zoomTo: (zoom: number, options?: { duration?: number }) => {
|
|
97
|
+
const pz = untrack(store.panZoom)
|
|
98
|
+
pz?.scaleTo(zoom, options)
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Node mutations
|
|
102
|
+
updateNode: (id: string, update: Partial<NodeType> | ((node: NodeType) => Partial<NodeType>)) => {
|
|
103
|
+
store.setNodes((prev) =>
|
|
104
|
+
prev.map((n) => {
|
|
105
|
+
if (n.id !== id) return n
|
|
106
|
+
const patch = typeof update === 'function' ? update(n) : update
|
|
107
|
+
return { ...n, ...patch }
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
updateNodeData: (id: string, data: Partial<NodeType['data']> | ((prev: NodeType['data']) => Partial<NodeType['data']>)) => {
|
|
112
|
+
store.setNodes((prev) =>
|
|
113
|
+
prev.map((n) => {
|
|
114
|
+
if (n.id !== id) return n
|
|
115
|
+
const newData = typeof data === 'function' ? data(n.data) : data
|
|
116
|
+
return { ...n, data: { ...n.data, ...newData } }
|
|
117
|
+
}),
|
|
118
|
+
)
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// Edge mutations
|
|
122
|
+
addEdges: (newEdges: EdgeType[]) => {
|
|
123
|
+
store.setEdges((prev) => [...prev, ...newEdges])
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Deletion
|
|
127
|
+
deleteElements: store.deleteElements,
|
|
128
|
+
|
|
129
|
+
// Coordinate conversion
|
|
130
|
+
screenToFlowPosition: (position: XYPosition): XYPosition => {
|
|
131
|
+
const domNode = untrack(store.domNode)
|
|
132
|
+
if (!domNode) return position
|
|
133
|
+
const rect = domNode.getBoundingClientRect()
|
|
134
|
+
const transform = store.getTransform()
|
|
135
|
+
return pointToRendererPoint(
|
|
136
|
+
{ x: position.x - rect.left, y: position.y - rect.top },
|
|
137
|
+
transform,
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* useViewport — reactive viewport getter.
|
|
145
|
+
*/
|
|
146
|
+
export { useViewport } from './hooks'
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* addEdge utility — wraps @xyflow/system's addEdge.
|
|
150
|
+
*/
|
|
151
|
+
export function addEdge<EdgeType extends EdgeBase = EdgeBase>(
|
|
152
|
+
connection: Connection,
|
|
153
|
+
edges: EdgeType[],
|
|
154
|
+
): EdgeType[] {
|
|
155
|
+
return addEdgeUtil(connection, edges) as EdgeType[]
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* reconnectEdge utility — wraps @xyflow/system's reconnectEdge.
|
|
160
|
+
*/
|
|
161
|
+
export function reconnectEdge<EdgeType extends EdgeBase = EdgeBase>(
|
|
162
|
+
oldEdge: EdgeType,
|
|
163
|
+
newConnection: Connection,
|
|
164
|
+
edges: EdgeType[],
|
|
165
|
+
): EdgeType[] {
|
|
166
|
+
return reconnectEdgeUtil(oldEdge, newConnection, edges) as EdgeType[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Internal helpers ---
|
|
170
|
+
|
|
171
|
+
function applyNodeChanges<NodeType extends NodeBase>(
|
|
172
|
+
nodes: NodeType[],
|
|
173
|
+
changes: NodeChange[],
|
|
174
|
+
): NodeType[] {
|
|
175
|
+
let result = [...nodes]
|
|
176
|
+
|
|
177
|
+
for (const change of changes) {
|
|
178
|
+
switch (change.type) {
|
|
179
|
+
case 'position':
|
|
180
|
+
result = result.map((n) =>
|
|
181
|
+
n.id === change.id
|
|
182
|
+
? {
|
|
183
|
+
...n,
|
|
184
|
+
...(change.position ? { position: change.position } : {}),
|
|
185
|
+
dragging: change.dragging ?? n.dragging,
|
|
186
|
+
}
|
|
187
|
+
: n,
|
|
188
|
+
)
|
|
189
|
+
break
|
|
190
|
+
case 'dimensions':
|
|
191
|
+
result = result.map((n) =>
|
|
192
|
+
n.id === change.id && change.dimensions
|
|
193
|
+
? { ...n, width: change.dimensions.width, height: change.dimensions.height }
|
|
194
|
+
: n,
|
|
195
|
+
)
|
|
196
|
+
break
|
|
197
|
+
case 'select':
|
|
198
|
+
result = result.map((n) =>
|
|
199
|
+
n.id === change.id ? { ...n, selected: change.selected } : n,
|
|
200
|
+
)
|
|
201
|
+
break
|
|
202
|
+
case 'remove':
|
|
203
|
+
result = result.filter((n) => n.id !== change.id)
|
|
204
|
+
break
|
|
205
|
+
case 'add':
|
|
206
|
+
result.push(change.item as NodeType)
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return result
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function applyEdgeChanges<EdgeType extends EdgeBase>(
|
|
215
|
+
edges: EdgeType[],
|
|
216
|
+
changes: EdgeChange[],
|
|
217
|
+
): EdgeType[] {
|
|
218
|
+
let result = [...edges]
|
|
219
|
+
|
|
220
|
+
for (const change of changes) {
|
|
221
|
+
switch (change.type) {
|
|
222
|
+
case 'select':
|
|
223
|
+
result = result.map((e) =>
|
|
224
|
+
e.id === change.id ? { ...e, selected: change.selected } : e,
|
|
225
|
+
)
|
|
226
|
+
break
|
|
227
|
+
case 'remove':
|
|
228
|
+
result = result.filter((e) => e.id !== change.id)
|
|
229
|
+
break
|
|
230
|
+
case 'add':
|
|
231
|
+
result.push(change.item as EdgeType)
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return result
|
|
237
|
+
}
|