panelui-native 0.16.0 → 0.19.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 (66) hide show
  1. package/lib/module/components/bottom-sheet/index.js +38 -10
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/checkbox/index.js +24 -11
  4. package/lib/module/components/checkbox/index.js.map +1 -1
  5. package/lib/module/components/field/index.js +337 -0
  6. package/lib/module/components/field/index.js.map +1 -0
  7. package/lib/module/components/flow/flow-paths.js +378 -0
  8. package/lib/module/components/flow/flow-paths.js.map +1 -0
  9. package/lib/module/components/flow/index.js +1598 -0
  10. package/lib/module/components/flow/index.js.map +1 -0
  11. package/lib/module/components/form/index.js +77 -0
  12. package/lib/module/components/form/index.js.map +1 -0
  13. package/lib/module/components/form/use-field.js +50 -0
  14. package/lib/module/components/form/use-field.js.map +1 -0
  15. package/lib/module/components/form/use-form.js +217 -0
  16. package/lib/module/components/form/use-form.js.map +1 -0
  17. package/lib/module/components/frame/index.js +124 -22
  18. package/lib/module/components/frame/index.js.map +1 -1
  19. package/lib/module/components/otp-input/index.js +303 -0
  20. package/lib/module/components/otp-input/index.js.map +1 -0
  21. package/lib/module/components/signature/index.js +500 -0
  22. package/lib/module/components/signature/index.js.map +1 -0
  23. package/lib/module/icons/index.js +218 -0
  24. package/lib/module/icons/index.js.map +1 -1
  25. package/lib/module/index.js +6 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/typescript/src/components/bottom-sheet/index.d.ts +15 -1
  28. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/checkbox/index.d.ts +8 -0
  30. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/field/index.d.ts +94 -0
  32. package/lib/typescript/src/components/field/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/flow/flow-paths.d.ts +89 -0
  34. package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -0
  35. package/lib/typescript/src/components/flow/index.d.ts +310 -0
  36. package/lib/typescript/src/components/flow/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/form/index.d.ts +64 -0
  38. package/lib/typescript/src/components/form/index.d.ts.map +1 -0
  39. package/lib/typescript/src/components/form/use-field.d.ts +16 -0
  40. package/lib/typescript/src/components/form/use-field.d.ts.map +1 -0
  41. package/lib/typescript/src/components/form/use-form.d.ts +37 -0
  42. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -0
  43. package/lib/typescript/src/components/frame/index.d.ts +39 -2
  44. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/otp-input/index.d.ts +141 -0
  46. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -0
  47. package/lib/typescript/src/components/signature/index.d.ts +254 -0
  48. package/lib/typescript/src/components/signature/index.d.ts.map +1 -0
  49. package/lib/typescript/src/icons/index.d.ts +17 -0
  50. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +7 -2
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/package.json +9 -1
  54. package/src/components/bottom-sheet/index.tsx +50 -12
  55. package/src/components/checkbox/index.tsx +40 -12
  56. package/src/components/field/index.tsx +329 -0
  57. package/src/components/flow/flow-paths.ts +348 -0
  58. package/src/components/flow/index.tsx +1924 -0
  59. package/src/components/form/index.tsx +85 -0
  60. package/src/components/form/use-field.ts +65 -0
  61. package/src/components/form/use-form.ts +252 -0
  62. package/src/components/frame/index.tsx +162 -14
  63. package/src/components/otp-input/index.tsx +396 -0
  64. package/src/components/signature/index.tsx +681 -0
  65. package/src/icons/index.tsx +159 -0
  66. package/src/index.ts +72 -0
@@ -0,0 +1,1598 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Flow — a canvas of nodes joined by edges, that you pan, pinch and rearrange
5
+ * with a finger.
6
+ *
7
+ * ```tsx
8
+ * <Flow>
9
+ * <Flow.Background variant="dots" />
10
+ * <Flow.Node id="db" position={{ x: 24, y: 40 }}>
11
+ * <Frame>…</Frame>
12
+ * </Flow.Node>
13
+ * <Flow.Node id="web" position={{ x: 220, y: 200 }}>
14
+ * <Frame>…</Frame>
15
+ * </Flow.Node>
16
+ * <Flow.Edge from="web" to="db" dashed animated />
17
+ * <Flow.Controls />
18
+ * </Flow>
19
+ * ```
20
+ *
21
+ * ## Where the positions live
22
+ *
23
+ * Every node's box is kept twice: on the UI thread, where a drag writes it
24
+ * every frame and the node's own transform reads it, and in React state, where
25
+ * the edges are rendered from it. A dragged node therefore never lags its own
26
+ * finger, and the edges attached to it redraw as it moves.
27
+ *
28
+ * The tempting design is one copy — everything on the UI thread, edges
29
+ * animating their own path strings, no renders at all. It does not draw. An
30
+ * animated SVG path in React Native reliably animates its `d` and nothing
31
+ * else, and only while nothing else about it is animated; anything more is
32
+ * dropped with no error, leaving a path that never receives its geometry. So
33
+ * the edges are ordinary elements and cost a render per drag frame. That is a
34
+ * real cost and it is the right trade.
35
+ *
36
+ * JavaScript is told a drag has finished through `onNodeDragEnd`. Positions
37
+ * are otherwise yours to leave alone: pass `position` once and the canvas
38
+ * takes it from there, or keep it in state and pass it back to drive nodes
39
+ * from outside.
40
+ *
41
+ * ## Two gestures, one canvas
42
+ *
43
+ * The pane pans and pinches; a node drags; a handle draws a new connection.
44
+ * They are nested gesture detectors rather than one gesture doing three jobs,
45
+ * so the innermost thing under the finger wins, which is what a finger expects.
46
+ */
47
+ import { createContext, Fragment, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
48
+ import { StyleSheet, View } from 'react-native';
49
+ import { useCSSVariable } from 'uniwind';
50
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
51
+ import Animated, { runOnJS, useAnimatedProps, useAnimatedStyle, useDerivedValue, useSharedValue, withTiming, Easing } from 'react-native-reanimated';
52
+ import Svg, { Circle, Defs, Path, Pattern, Rect } from 'react-native-svg';
53
+ import { tv } from 'tailwind-variants';
54
+ import { LockIcon, MaximizeIcon, MinusIcon, PlusIcon, UnlockIcon } from "../../icons/index.js";
55
+ import { AnimatedPressable } from "../../primitives/animated-pressable.js";
56
+ import { Text } from "../../primitives/text.js";
57
+ import { cn } from "../../utils/cn.js";
58
+ import { anchorOf, arrivalDirection, arrowHeadPath, autoSides, edgePath } from "./flow-paths.js";
59
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
60
+ const AnimatedPath = Animated.createAnimatedComponent(Path);
61
+ const AnimatedRect = Animated.createAnimatedComponent(Rect);
62
+ const SPRING = {
63
+ duration: 320,
64
+ easing: Easing.out(Easing.cubic)
65
+ };
66
+
67
+ /**
68
+ * Without this the transform pivots on the layer's centre, and every zoom
69
+ * throws the graph across the screen.
70
+ */
71
+ const TRANSFORM_ORIGIN = {
72
+ transformOrigin: 'top left'
73
+ };
74
+
75
+ /** How many screens of grid to draw around the container in each direction. */
76
+ const GRID_SPAN = 4;
77
+
78
+ /**
79
+ * An SVG is backed by a single texture, and a texture has a maximum size the
80
+ * platform will allocate — past it, nothing is drawn at all rather than
81
+ * something clipped. This keeps the grid comfortably under it at 3× device
82
+ * scale, which is why the grid is the only thing sized in graph space.
83
+ */
84
+ function clampCanvas(value) {
85
+ return Math.max(Math.min(value, 4000), 1);
86
+ }
87
+
88
+ /** How close a finger has to get to a handle for a connection to land. */
89
+ const CONNECT_RADIUS = 44;
90
+
91
+ /** What `onConnect` is handed when a connection lands. */
92
+
93
+ const FlowContext = /*#__PURE__*/createContext(null);
94
+ function useFlow(component) {
95
+ const context = useContext(FlowContext);
96
+ if (!context) {
97
+ throw new Error(`${component} must be used within a <Flow>`);
98
+ }
99
+ return context;
100
+ }
101
+
102
+ /**
103
+ * Which layer a part belongs to. Declared on the component as a static so the
104
+ * root can sort its children without depending on function identity.
105
+ */
106
+
107
+ /** Set by Flow.Group, so a node inside one registers as its child. */
108
+ const FlowGroupContext = /*#__PURE__*/createContext(null);
109
+
110
+ /** Set by Flow.Node, so a handle knows which node it belongs to. */
111
+ const FlowNodeContext = /*#__PURE__*/createContext(null);
112
+ const flowVariants = tv({
113
+ slots: {
114
+ root: 'flex-1 overflow-hidden bg-background',
115
+ controls: 'absolute bottom-4 right-4 overflow-hidden rounded-xl border border-border bg-card',
116
+ control: 'h-10 w-10 items-center justify-center active:bg-muted',
117
+ minimap: 'absolute overflow-hidden rounded-xl border border-border bg-card/90',
118
+ handle: 'absolute h-3.5 w-3.5 rounded-full border-2 border-background bg-muted-foreground'
119
+ }
120
+ });
121
+
122
+ /* -------------------------------------------------------------------------- */
123
+ /* Root */
124
+ /* -------------------------------------------------------------------------- */
125
+
126
+ function FlowRoot({
127
+ className,
128
+ defaultViewport,
129
+ minZoom = 0.3,
130
+ maxZoom = 2.5,
131
+ panOnDrag = true,
132
+ zoomOnPinch = true,
133
+ fitViewOnMount = false,
134
+ fitViewPadding = 48,
135
+ onViewportChange,
136
+ onNodeDragEnd,
137
+ onConnect,
138
+ isValidConnection,
139
+ children,
140
+ ...props
141
+ }) {
142
+ const rects = useSharedValue({});
143
+ const translateX = useSharedValue(defaultViewport?.x ?? 0);
144
+ const translateY = useSharedValue(defaultViewport?.y ?? 0);
145
+ const zoom = useSharedValue(defaultViewport?.zoom ?? 1);
146
+ const connection = useSharedValue({
147
+ active: 0,
148
+ x1: 0,
149
+ y1: 0,
150
+ x2: 0,
151
+ y2: 0
152
+ });
153
+ const size = useSharedValue({
154
+ width: 0,
155
+ height: 0
156
+ });
157
+ const [locked, setLocked] = useState(false);
158
+ const [nodeIds, setNodeIds] = useState([]);
159
+ const [handles, setHandles] = useState([]);
160
+ const [edges, setEdges] = useState([]);
161
+ const [box, setBox] = useState({
162
+ width: 0,
163
+ height: 0
164
+ });
165
+ const [boxes, setBoxes] = useState({});
166
+ /** The authoritative copy. Both `boxes` and `rects` are written from it. */
167
+ const boxesRef = useRef({});
168
+
169
+ /**
170
+ * Node geometry is kept twice on purpose: on the UI thread, where a drag
171
+ * writes it every frame and the node's own transform reads it, and in React
172
+ * state, where the edges are rendered from it.
173
+ *
174
+ * The single-copy version — everything on the UI thread, edges animating
175
+ * their own path strings — is the tempting one, and it does not draw. An
176
+ * animated SVG path in React Native only reliably animates its `d`, and only
177
+ * when nothing else about it is animated; anything more and the update is
178
+ * dropped with no error, leaving a path that never receives its geometry.
179
+ * So the edges are plain elements, the way they are in every implementation
180
+ * of this that works, and they cost a render per drag frame. That is a real
181
+ * cost and it is the right trade: a graph that redraws is worth more than
182
+ * one that theoretically would not have to.
183
+ */
184
+ const setNodeRect = useCallback((id, patch) => {
185
+ const existing = boxesRef.current[id] ?? {
186
+ x: 0,
187
+ y: 0,
188
+ width: 0,
189
+ height: 0
190
+ };
191
+ const next = {
192
+ ...existing,
193
+ ...patch
194
+ };
195
+ if (existing.x === next.x && existing.y === next.y && existing.width === next.width && existing.height === next.height) {
196
+ return;
197
+ }
198
+ // Merged against the ref, never against the state updater's argument.
199
+ // A state updater runs during render, and touching a shared value there
200
+ // is both a Reanimated violation and a correctness one: the write can be
201
+ // replayed or dropped, so the two copies drift and the edges end up
202
+ // drawn against positions the nodes are not at.
203
+ boxesRef.current = {
204
+ ...boxesRef.current,
205
+ [id]: next
206
+ };
207
+ rects.value = boxesRef.current;
208
+ setBoxes(boxesRef.current);
209
+ }, [rects]);
210
+ const dropNodeRect = useCallback(id => {
211
+ if (!(id in boxesRef.current)) return;
212
+ const {
213
+ [id]: _removed,
214
+ ...rest
215
+ } = boxesRef.current;
216
+ boxesRef.current = rest;
217
+ rects.value = rest;
218
+ setBoxes(rest);
219
+ }, [rects]);
220
+
221
+ /**
222
+ * The node layer is given real extent rather than being left the size of the
223
+ * container. A view translated past its parent's bounds still draws, but it
224
+ * stops receiving touches — which is why a node dragged off toward the edge
225
+ * of the canvas would quietly become unmovable. Sizing the layer to the same
226
+ * span as the grid keeps every node inside its parent, where it can be hit.
227
+ */
228
+ const layer = useMemo(() => ({
229
+ width: clampCanvas(Math.max(box.width, 320) * GRID_SPAN),
230
+ height: clampCanvas(Math.max(box.height, 480) * GRID_SPAN)
231
+ }), [box.height, box.width]);
232
+ const origin = useMemo(() => ({
233
+ x: layer.width / 2,
234
+ y: layer.height / 2
235
+ }), [layer.height, layer.width]);
236
+ const viewportChangeRef = useRef(onViewportChange);
237
+ viewportChangeRef.current = onViewportChange;
238
+ const reportViewport = useCallback((x, y, z) => {
239
+ viewportChangeRef.current?.({
240
+ x,
241
+ y,
242
+ zoom: z
243
+ });
244
+ }, []);
245
+ const registerNode = useCallback(id => {
246
+ setNodeIds(current => current.includes(id) ? current : [...current, id]);
247
+ }, []);
248
+ const unregisterNode = useCallback(id => {
249
+ setNodeIds(current => current.filter(entry => entry !== id));
250
+ }, []);
251
+ const registerHandle = useCallback(entry => {
252
+ setHandles(current => [...current.filter(h => h.key !== entry.key), entry]);
253
+ }, []);
254
+ const unregisterHandle = useCallback(key => {
255
+ setHandles(current => current.filter(h => h.key !== key));
256
+ }, []);
257
+ const registerEdge = useCallback((key, props) => {
258
+ setEdges(current => {
259
+ const rest = current.filter(edge => edge.key !== key);
260
+ return [...rest, {
261
+ key,
262
+ props
263
+ }];
264
+ });
265
+ }, []);
266
+ const unregisterEdge = useCallback(key => {
267
+ setEdges(current => current.filter(edge => edge.key !== key));
268
+ }, []);
269
+ const fitView = useCallback(() => {
270
+ const {
271
+ width,
272
+ height
273
+ } = size.value;
274
+ const all = Object.values(rects.value);
275
+ if (!width || !height || all.length === 0) return;
276
+ let left = Infinity;
277
+ let top = Infinity;
278
+ let right = -Infinity;
279
+ let bottom = -Infinity;
280
+ for (const rect of all) {
281
+ if (rect.width === 0 || rect.height === 0) continue;
282
+ left = Math.min(left, rect.x);
283
+ top = Math.min(top, rect.y);
284
+ right = Math.max(right, rect.x + rect.width);
285
+ bottom = Math.max(bottom, rect.y + rect.height);
286
+ }
287
+ if (!Number.isFinite(left)) return;
288
+ const graphWidth = Math.max(right - left, 1);
289
+ const graphHeight = Math.max(bottom - top, 1);
290
+ const next = Math.min(Math.max(Math.min((width - fitViewPadding * 2) / graphWidth, (height - fitViewPadding * 2) / graphHeight), minZoom), maxZoom);
291
+ zoom.value = withTiming(next, SPRING);
292
+ translateX.value = withTiming(width / 2 - (left + graphWidth / 2) * next, SPRING);
293
+ translateY.value = withTiming(height / 2 - (top + graphHeight / 2) * next, SPRING);
294
+ reportViewport(translateX.value, translateY.value, next);
295
+ }, [fitViewPadding, maxZoom, minZoom, rects, reportViewport, size, translateX, translateY, zoom]);
296
+
297
+ /** Zoom about the middle of the screen, which is where a button press means. */
298
+ const zoomBy = useCallback(factor => {
299
+ const {
300
+ width,
301
+ height
302
+ } = size.value;
303
+ const current = zoom.value;
304
+ const next = Math.min(Math.max(current * factor, minZoom), maxZoom);
305
+ if (next === current) return;
306
+ const focalX = width / 2;
307
+ const focalY = height / 2;
308
+ const ratio = next / current;
309
+ const x = focalX - (focalX - translateX.value) * ratio;
310
+ const y = focalY - (focalY - translateY.value) * ratio;
311
+ zoom.value = withTiming(next, SPRING);
312
+ translateX.value = withTiming(x, SPRING);
313
+ translateY.value = withTiming(y, SPRING);
314
+ reportViewport(x, y, next);
315
+ }, [maxZoom, minZoom, reportViewport, size, translateX, translateY, zoom]);
316
+
317
+ // Nodes measure themselves on their first layout pass, so a fit asked for at
318
+ // mount has nothing to fit to yet. One frame later they do.
319
+ const fitOnMount = useRef(fitViewOnMount);
320
+ useEffect(() => {
321
+ if (!fitOnMount.current || nodeIds.length === 0) return;
322
+ const timer = setTimeout(fitView, 32);
323
+ return () => clearTimeout(timer);
324
+ }, [fitView, nodeIds.length]);
325
+ const panStart = useSharedValue({
326
+ x: 0,
327
+ y: 0
328
+ });
329
+ const pinchStart = useSharedValue({
330
+ zoom: 1,
331
+ x: 0,
332
+ y: 0
333
+ });
334
+ const paneGesture = useMemo(() => {
335
+ const pan = Gesture.Pan().enabled(panOnDrag && !locked).averageTouches(true).onBegin(() => {
336
+ 'worklet';
337
+
338
+ panStart.value = {
339
+ x: translateX.value,
340
+ y: translateY.value
341
+ };
342
+ }).onUpdate(event => {
343
+ 'worklet';
344
+
345
+ translateX.value = panStart.value.x + event.translationX;
346
+ translateY.value = panStart.value.y + event.translationY;
347
+ }).onEnd(() => {
348
+ 'worklet';
349
+
350
+ runOnJS(reportViewport)(translateX.value, translateY.value, zoom.value);
351
+ });
352
+ const pinch = Gesture.Pinch().enabled(zoomOnPinch && !locked).onBegin(event => {
353
+ 'worklet';
354
+
355
+ pinchStart.value = {
356
+ zoom: zoom.value,
357
+ x: translateX.value,
358
+ y: translateY.value
359
+ };
360
+ // Keep the point under the fingers still: the translation that holds a
361
+ // focal point in place is the one that scales the distance from it.
362
+ connection.value = {
363
+ ...connection.value
364
+ };
365
+ panStart.value = {
366
+ x: event.focalX,
367
+ y: event.focalY
368
+ };
369
+ }).onUpdate(event => {
370
+ 'worklet';
371
+
372
+ const next = Math.min(Math.max(pinchStart.value.zoom * event.scale, minZoom), maxZoom);
373
+ const ratio = next / pinchStart.value.zoom;
374
+ zoom.value = next;
375
+ translateX.value = event.focalX - (event.focalX - pinchStart.value.x) * ratio;
376
+ translateY.value = event.focalY - (event.focalY - pinchStart.value.y) * ratio;
377
+ }).onEnd(() => {
378
+ 'worklet';
379
+
380
+ runOnJS(reportViewport)(translateX.value, translateY.value, zoom.value);
381
+ });
382
+ return Gesture.Simultaneous(pan, pinch);
383
+ }, [connection, locked, maxZoom, minZoom, panOnDrag, panStart, pinchStart, reportViewport, translateX, translateY, zoom, zoomOnPinch]);
384
+ const contentStyle = useAnimatedStyle(() => ({
385
+ transform: [{
386
+ translateX: translateX.value
387
+ }, {
388
+ translateY: translateY.value
389
+ }, {
390
+ scale: zoom.value
391
+ }]
392
+ }));
393
+ const context = useMemo(() => ({
394
+ rects,
395
+ boxes,
396
+ setNodeRect,
397
+ dropNodeRect,
398
+ translateX,
399
+ translateY,
400
+ zoom,
401
+ connection,
402
+ size,
403
+ box,
404
+ layer,
405
+ origin,
406
+ minZoom,
407
+ maxZoom,
408
+ locked,
409
+ setLocked,
410
+ nodeIds,
411
+ handles,
412
+ edges,
413
+ registerNode,
414
+ unregisterNode,
415
+ registerHandle,
416
+ unregisterHandle,
417
+ registerEdge,
418
+ unregisterEdge,
419
+ onConnect,
420
+ isValidConnection,
421
+ onNodeDragEnd,
422
+ fitView,
423
+ zoomBy
424
+ }), [boxes, setNodeRect, dropNodeRect, box, layer, origin, connection, edges, fitView, handles, registerEdge, unregisterEdge, isValidConnection, locked, maxZoom, minZoom, nodeIds, onConnect, onNodeDragEnd, rects, registerHandle, registerNode, size, translateX, translateY, unregisterHandle, unregisterNode, zoom, zoomBy]);
425
+ const onLayout = useCallback(event => {
426
+ const {
427
+ width,
428
+ height
429
+ } = event.nativeEvent.layout;
430
+ size.value = {
431
+ width,
432
+ height
433
+ };
434
+ setBox(current => current.width === width && current.height === height ? current : {
435
+ width,
436
+ height
437
+ });
438
+ }, [size]);
439
+
440
+ // Children are sorted into the layers that actually draw them. Only nodes go
441
+ // inside the transform; the grid and the edges map graph coordinates to
442
+ // screen coordinates themselves, and the controls must not pan away with the
443
+ // graph they control.
444
+ const {
445
+ background,
446
+ overlay,
447
+ canvas
448
+ } = useMemo(() => splitChildren(children), [children]);
449
+ return /*#__PURE__*/_jsx(View, {
450
+ className: flowVariants().root({
451
+ className
452
+ }),
453
+ onLayout: onLayout,
454
+ collapsable: false,
455
+ accessibilityRole: "none",
456
+ ...props,
457
+ children: /*#__PURE__*/_jsxs(FlowContext.Provider, {
458
+ value: context,
459
+ children: [/*#__PURE__*/_jsx(GestureDetector, {
460
+ gesture: paneGesture,
461
+ children: /*#__PURE__*/_jsx(View, {
462
+ collapsable: false,
463
+ style: {
464
+ flex: 1
465
+ },
466
+ children: /*#__PURE__*/_jsxs(Animated.View, {
467
+ collapsable: false,
468
+ style: [{
469
+ position: 'absolute',
470
+ left: -origin.x,
471
+ top: -origin.y,
472
+ width: layer.width,
473
+ height: layer.height,
474
+ // Pivot on graph (0, 0), not on this layer's own corner.
475
+ // The layer starts half its width up and to the left of the
476
+ // container, so scaling about its corner would throw the
477
+ // graph off screen by half the layer on every zoom.
478
+ transformOrigin: [origin.x, origin.y, 0]
479
+ }, contentStyle],
480
+ children: [background, /*#__PURE__*/_jsx(FlowEdgeLayer, {}), canvas]
481
+ })
482
+ })
483
+ }), overlay]
484
+ })
485
+ });
486
+ }
487
+
488
+ /**
489
+ * Sorts what was declared inside `<Flow>` into the three layers that actually
490
+ * draw it.
491
+ *
492
+ * An edge reads best written next to the nodes it joins, but it is an SVG
493
+ * element and has to be inside the canvas's single `<Svg>`; controls and the
494
+ * minimap read best written last, but have to sit outside the transform or
495
+ * they pan away with the graph they control. Rather than make the caller
496
+ * arrange the file to match the paint order, the parts are recognised here.
497
+ *
498
+ * Nested fragments and arrays are flattened on the way, so a `.map()` over
499
+ * edges works exactly as it looks like it should.
500
+ */
501
+ function splitChildren(children) {
502
+ const background = [];
503
+ const overlay = [];
504
+ const canvas = [];
505
+ const visit = node => {
506
+ if (node === null || node === undefined || typeof node === 'boolean') return;
507
+ if (Array.isArray(node)) {
508
+ node.forEach(visit);
509
+ return;
510
+ }
511
+ if (typeof node === 'object' && 'type' in node) {
512
+ const type = node.type;
513
+ // Matched on a static marker rather than on the function itself. Identity
514
+ // is the obvious test and the fragile one: it fails the moment two copies
515
+ // of the module exist, and it fails silently — an unrecognised edge would
516
+ // land among the nodes, where it is not an SVG element and draws nothing.
517
+ const slot = typeof type === 'function' ? type.slot : undefined;
518
+ if (slot === 'background') {
519
+ background.push(node);
520
+ return;
521
+ }
522
+ if (slot === 'overlay') {
523
+ overlay.push(node);
524
+ return;
525
+ }
526
+ // A fragment is a wrapper the caller used for their own reasons, not a
527
+ // layer — look through it rather than dropping its contents on the floor.
528
+ if (type === Fragment) {
529
+ visit(node.props?.children);
530
+ return;
531
+ }
532
+ }
533
+ canvas.push(node);
534
+ };
535
+ visit(children);
536
+ return {
537
+ background,
538
+ overlay,
539
+ canvas
540
+ };
541
+ }
542
+
543
+ /* -------------------------------------------------------------------------- */
544
+ /* Background */
545
+ /* -------------------------------------------------------------------------- */
546
+
547
+ /**
548
+ * The grid behind everything. It lives inside the transformed layer, so it
549
+ * pans and scales with the graph — which is the whole point: a grid that
550
+ * stayed put would say the canvas was not moving.
551
+ */
552
+ function FlowBackground({
553
+ variant = 'dots',
554
+ gap = 24,
555
+ size = 1.6,
556
+ color
557
+ }) {
558
+ const {
559
+ box,
560
+ layer,
561
+ origin,
562
+ translateX,
563
+ translateY,
564
+ zoom
565
+ } = useFlow('Flow.Background');
566
+ const token = useCSSVariable('--color-muted-foreground');
567
+ const tint = color ?? (typeof token === 'string' ? token : '#737373');
568
+ // A pattern is referenced by id, and two canvases on one screen would collide.
569
+ const id = useRef(`panelui-flow-${Math.random().toString(36).slice(2, 7)}`).current;
570
+
571
+ /*
572
+ * The grid is a tile that follows the viewport in whole-cell steps.
573
+ *
574
+ * It is drawn inside the transformed layer, so panning and zooming it costs
575
+ * nothing and the dots stay nailed to the canvas — a grid whose pattern is
576
+ * re-tiled each frame visibly crawls instead of travelling. But a tile fixed
577
+ * in graph space runs out the moment you pan past its edge, and it cannot
578
+ * simply be made enormous: an SVG is one texture, and a texture has a size
579
+ * the platform will not exceed, past which it draws nothing at all.
580
+ *
581
+ * So the tile moves with you, rounded to a whole number of cells. Rounding is
582
+ * what makes the move invisible: shifted by an exact multiple of the grid
583
+ * spacing, the tile is the same picture it was before.
584
+ */
585
+ const follow = useAnimatedStyle(() => {
586
+ // The graph coordinate at the middle of the container. Following the
587
+ // middle rather than the left edge is what makes the tile cover the view
588
+ // at every zoom: centred, it reaches half its width in each direction, and
589
+ // zooming out grows the visible area in both.
590
+ const centreX = (-translateX.value + box.width / 2) / zoom.value;
591
+ const centreY = (-translateY.value + box.height / 2) / zoom.value;
592
+ return {
593
+ transform: [{
594
+ translateX: Math.round(centreX / gap) * gap
595
+ }, {
596
+ translateY: Math.round(centreY / gap) * gap
597
+ }]
598
+ };
599
+ });
600
+ if (variant === 'none') return null;
601
+
602
+ /*
603
+ * The grid fills its layer exactly, and the layer is the same box the nodes
604
+ * live in. It used to be an oversized SVG inside a container-sized parent,
605
+ * which the canvas's own `overflow-hidden` then clipped back down — so the
606
+ * dots ran out as soon as you zoomed out far enough to see past the
607
+ * container, which looked like them disappearing.
608
+ */
609
+ const width = layer.width;
610
+ const height = layer.height;
611
+ return /*#__PURE__*/_jsx(Animated.View, {
612
+ pointerEvents: "none",
613
+ style: [StyleSheet.absoluteFill, follow],
614
+ children: /*#__PURE__*/_jsxs(Svg, {
615
+ pointerEvents: "none",
616
+ width: width,
617
+ height: height,
618
+ children: [/*#__PURE__*/_jsx(Defs, {
619
+ children: /*#__PURE__*/_jsxs(Pattern, {
620
+ id: id,
621
+ x: 0,
622
+ y: 0,
623
+ width: gap,
624
+ height: gap,
625
+ patternUnits: "userSpaceOnUse",
626
+ children: [variant === 'dots' ? /*#__PURE__*/_jsx(Circle, {
627
+ cx: gap / 2,
628
+ cy: gap / 2,
629
+ r: size,
630
+ fill: tint,
631
+ opacity: 0.5
632
+ }) : null, variant === 'lines' ? /*#__PURE__*/_jsx(Path, {
633
+ d: `M0,0 L${gap},0 M0,0 L0,${gap}`,
634
+ stroke: tint,
635
+ strokeWidth: Math.max(size / 2, 0.4),
636
+ opacity: 0.3
637
+ }) : null, variant === 'cross' ? /*#__PURE__*/_jsx(Path, {
638
+ d: `M${gap / 2 - size * 2},${gap / 2} L${gap / 2 + size * 2},${gap / 2} M${gap / 2},${gap / 2 - size * 2} L${gap / 2},${gap / 2 + size * 2}`,
639
+ stroke: tint,
640
+ strokeWidth: Math.max(size / 2, 0.4),
641
+ opacity: 0.42
642
+ }) : null]
643
+ })
644
+ }), /*#__PURE__*/_jsx(Rect, {
645
+ x: 0,
646
+ y: 0,
647
+ width: width,
648
+ height: height,
649
+ fill: `url(#${id})`
650
+ })]
651
+ })
652
+ });
653
+ }
654
+ FlowBackground.displayName = 'Flow.Background';
655
+ FlowBackground.slot = 'background';
656
+
657
+ /* -------------------------------------------------------------------------- */
658
+ /* Node */
659
+ /* -------------------------------------------------------------------------- */
660
+
661
+ /**
662
+ * One box on the canvas. Its own content is whatever you put inside — a Frame
663
+ * is the usual answer, since a node is a titled card of rows more often than
664
+ * it is anything else.
665
+ */
666
+ function FlowNode({
667
+ id,
668
+ position,
669
+ className,
670
+ draggable = true,
671
+ selected = false,
672
+ onPress,
673
+ accessibilityLabel,
674
+ children,
675
+ ...props
676
+ }) {
677
+ const flow = useFlow('Flow.Node');
678
+ const group = useContext(FlowGroupContext);
679
+ const {
680
+ rects,
681
+ zoom,
682
+ locked,
683
+ origin,
684
+ setNodeRect,
685
+ dropNodeRect,
686
+ registerNode,
687
+ unregisterNode,
688
+ onNodeDragEnd
689
+ } = flow;
690
+ useEffect(() => {
691
+ registerNode(id, group ?? undefined);
692
+ return () => unregisterNode(id);
693
+ }, [group, id, registerNode, unregisterNode]);
694
+
695
+ // Writing on every render would fight the drag, snapping a node back to the
696
+ // position it was first given. Only an actual change to the prop moves it.
697
+ const lastPosition = useRef(null);
698
+ useEffect(() => {
699
+ const previous = lastPosition.current;
700
+ if (previous && previous.x === position.x && previous.y === position.y) return;
701
+ lastPosition.current = position;
702
+ setNodeRect(id, {
703
+ x: position.x,
704
+ y: position.y
705
+ });
706
+ }, [id, position, setNodeRect]);
707
+ useEffect(() => {
708
+ return () => dropNodeRect(id);
709
+ }, [dropNodeRect, id]);
710
+ const onLayout = useCallback(event => {
711
+ const {
712
+ width,
713
+ height
714
+ } = event.nativeEvent.layout;
715
+ setNodeRect(id, {
716
+ width,
717
+ height
718
+ });
719
+ }, [id, setNodeRect]);
720
+ const dragEnd = useCallback((x, y) => onNodeDragEnd?.(id, {
721
+ x,
722
+ y
723
+ }), [id, onNodeDragEnd]);
724
+
725
+ /**
726
+ * The node itself moves on the UI thread; this tells React where it got to,
727
+ * so the edges attached to it redraw. Called every frame of a drag — which
728
+ * is what it costs to have edges that follow, and what every working
729
+ * implementation of this pays.
730
+ */
731
+ const dragTo = useCallback((x, y) => setNodeRect(id, {
732
+ x,
733
+ y
734
+ }), [id, setNodeRect]);
735
+ const start = useSharedValue({
736
+ x: 0,
737
+ y: 0
738
+ });
739
+ const pressRef = useRef(onPress);
740
+ pressRef.current = onPress;
741
+ const press = useCallback(() => pressRef.current?.(), []);
742
+ const drag = useMemo(() => Gesture.Pan().enabled(draggable && !locked).onBegin(() => {
743
+ 'worklet';
744
+
745
+ const rect = rects.value[id];
746
+ start.value = {
747
+ x: rect?.x ?? 0,
748
+ y: rect?.y ?? 0
749
+ };
750
+ }).onUpdate(event => {
751
+ 'worklet';
752
+
753
+ const rect = rects.value[id];
754
+ if (!rect) return;
755
+ // Divided by the zoom, so the node keeps up with the finger rather
756
+ // than with the graph coordinate the finger happens to be over.
757
+ const x = start.value.x + event.translationX / zoom.value;
758
+ const y = start.value.y + event.translationY / zoom.value;
759
+ rects.value = {
760
+ ...rects.value,
761
+ [id]: {
762
+ ...rect,
763
+ x,
764
+ y
765
+ }
766
+ };
767
+ runOnJS(dragTo)(x, y);
768
+ }).onEnd(() => {
769
+ 'worklet';
770
+
771
+ const rect = rects.value[id];
772
+ if (rect) runOnJS(dragEnd)(rect.x, rect.y);
773
+ }), [dragEnd, dragTo, draggable, id, locked, rects, start, zoom]);
774
+
775
+ // Raced with the drag rather than layered on top of it. A plain touch handler
776
+ // would fire at the end of a drag too, so every rearrangement would also read
777
+ // as a tap on whatever was moved.
778
+ const gesture = useMemo(() => onPress ? Gesture.Race(drag, Gesture.Tap().onEnd(() => {
779
+ 'worklet';
780
+
781
+ runOnJS(press)();
782
+ })) : drag, [drag, onPress, press]);
783
+ const style = useAnimatedStyle(() => {
784
+ const rect = rects.value[id];
785
+ return {
786
+ transform: [{
787
+ translateX: (rect?.x ?? 0) + origin.x
788
+ }, {
789
+ translateY: (rect?.y ?? 0) + origin.y
790
+ }]
791
+ };
792
+ });
793
+ return /*#__PURE__*/_jsx(FlowNodeContext.Provider, {
794
+ value: id,
795
+ children: /*#__PURE__*/_jsx(GestureDetector, {
796
+ gesture: gesture,
797
+ children: /*#__PURE__*/_jsx(Animated.View, {
798
+ collapsable: false,
799
+ onLayout: onLayout,
800
+ style: [{
801
+ position: 'absolute',
802
+ top: 0,
803
+ left: 0
804
+ }, style],
805
+ className: cn(selected && 'rounded-3xl ring-2 ring-ring', className),
806
+ accessible: true,
807
+ accessibilityRole: onPress ? 'button' : 'none',
808
+ accessibilityLabel: accessibilityLabel ?? id,
809
+ accessibilityState: {
810
+ selected
811
+ },
812
+ ...props,
813
+ children: children
814
+ })
815
+ })
816
+ });
817
+ }
818
+ FlowNode.displayName = 'Flow.Node';
819
+
820
+ /* -------------------------------------------------------------------------- */
821
+ /* Handle */
822
+ /* -------------------------------------------------------------------------- */
823
+
824
+ /**
825
+ * A port on a node — both the point an edge attaches to and the grip a new
826
+ * connection is dragged from.
827
+ *
828
+ * Its position is worked out from the node's box and the face it names, so it
829
+ * never has to measure itself. That matters: a handle that measured would be
830
+ * one frame behind the node it sits on, and the edge would trail its own port.
831
+ */
832
+ function FlowHandle({
833
+ id = 'default',
834
+ position = 'right',
835
+ type = 'both',
836
+ offset = 0.5,
837
+ className,
838
+ hidden = false
839
+ }) {
840
+ const flow = useFlow('Flow.Handle');
841
+ const node = useContext(FlowNodeContext);
842
+ if (!node) {
843
+ throw new Error('Flow.Handle must be used within a <Flow.Node>');
844
+ }
845
+ const {
846
+ rects,
847
+ zoom,
848
+ connection,
849
+ handles,
850
+ locked,
851
+ registerHandle,
852
+ unregisterHandle
853
+ } = flow;
854
+ const key = `${node}.${id}`;
855
+ useEffect(() => {
856
+ registerHandle({
857
+ key,
858
+ node,
859
+ id,
860
+ side: position,
861
+ offset,
862
+ type
863
+ });
864
+ return () => unregisterHandle(key);
865
+ }, [id, key, node, offset, position, registerHandle, type, unregisterHandle]);
866
+ const connectRef = useRef(flow.onConnect);
867
+ connectRef.current = flow.onConnect;
868
+ const validRef = useRef(flow.isValidConnection);
869
+ validRef.current = flow.isValidConnection;
870
+ const land = useCallback((target, targetHandle) => {
871
+ const payload = {
872
+ source: node,
873
+ sourceHandle: id,
874
+ target,
875
+ targetHandle
876
+ };
877
+ if (validRef.current && !validRef.current(payload)) return;
878
+ connectRef.current?.(payload);
879
+ }, [id, node]);
880
+
881
+ // A snapshot of what a drag can land on, rebuilt only when handles come and
882
+ // go — the drop test runs on the UI thread and cannot read React state.
883
+ const targets = useMemo(() => handles.filter(entry => entry.node !== node && entry.type !== 'source').map(entry => ({
884
+ node: entry.node,
885
+ id: entry.id,
886
+ side: entry.side,
887
+ offset: entry.offset
888
+ })), [handles, node]);
889
+ const connect = useMemo(() => Gesture.Pan().enabled(!locked && type !== 'target').onBegin(() => {
890
+ 'worklet';
891
+
892
+ const rect = rects.value[node];
893
+ if (!rect) return;
894
+ const anchor = anchorOf(rect, position, offset);
895
+ connection.value = {
896
+ active: 1,
897
+ x1: anchor.x,
898
+ y1: anchor.y,
899
+ x2: anchor.x,
900
+ y2: anchor.y
901
+ };
902
+ }).onUpdate(event => {
903
+ 'worklet';
904
+
905
+ const current = connection.value;
906
+ if (current.active === 0) return;
907
+ connection.value = {
908
+ ...current,
909
+ x2: current.x1 + event.translationX / zoom.value,
910
+ y2: current.y1 + event.translationY / zoom.value
911
+ };
912
+ }).onEnd(() => {
913
+ 'worklet';
914
+
915
+ const current = connection.value;
916
+ connection.value = {
917
+ active: 0,
918
+ x1: 0,
919
+ y1: 0,
920
+ x2: 0,
921
+ y2: 0
922
+ };
923
+ if (current.active === 0) return;
924
+ const drop = {
925
+ x: current.x2,
926
+ y: current.y2
927
+ };
928
+ const reach = CONNECT_RADIUS / zoom.value;
929
+
930
+ // Nearest handle wins. A finger is blunt, so the first thing inside
931
+ // the radius is rarely the one that was meant.
932
+ let bestNode = null;
933
+ let bestHandle;
934
+ let bestDistance = reach;
935
+ for (let i = 0; i < targets.length; i += 1) {
936
+ const entry = targets[i];
937
+ const rect = rects.value[entry.node];
938
+ if (!rect) continue;
939
+ const anchor = anchorOf(rect, entry.side, entry.offset);
940
+ const dx = anchor.x - drop.x;
941
+ const dy = anchor.y - drop.y;
942
+ const distance = Math.sqrt(dx * dx + dy * dy);
943
+ if (distance < bestDistance) {
944
+ bestDistance = distance;
945
+ bestNode = entry.node;
946
+ bestHandle = entry.id;
947
+ }
948
+ }
949
+
950
+ // Nothing close enough to a handle: dropping anywhere on a node is
951
+ // the friendlier reading of the gesture on a small screen.
952
+ if (bestNode === null) {
953
+ const entries = Object.entries(rects.value);
954
+ for (let i = 0; i < entries.length; i += 1) {
955
+ const [candidate, rect] = entries[i];
956
+ if (candidate === node) continue;
957
+ if (drop.x >= rect.x && drop.x <= rect.x + rect.width && drop.y >= rect.y && drop.y <= rect.y + rect.height) {
958
+ bestNode = candidate;
959
+ bestHandle = undefined;
960
+ break;
961
+ }
962
+ }
963
+ }
964
+ if (bestNode !== null) runOnJS(land)(bestNode, bestHandle);
965
+ }), [connection, land, locked, node, offset, position, rects, targets, type, zoom]);
966
+ if (hidden) return null;
967
+
968
+ // Positioned as a percentage of the node it sits in, so it follows the node's
969
+ // own size without either of them measuring the other.
970
+ const along = `${offset * 100}%`;
971
+ const placement = position === 'top' ? {
972
+ top: -7,
973
+ left: along,
974
+ marginLeft: -7
975
+ } : position === 'bottom' ? {
976
+ bottom: -7,
977
+ left: along,
978
+ marginLeft: -7
979
+ } : position === 'left' ? {
980
+ left: -7,
981
+ top: along,
982
+ marginTop: -7
983
+ } : {
984
+ right: -7,
985
+ top: along,
986
+ marginTop: -7
987
+ };
988
+ return /*#__PURE__*/_jsx(GestureDetector, {
989
+ gesture: connect,
990
+ children: /*#__PURE__*/_jsx(View, {
991
+ collapsable: false,
992
+ hitSlop: 12,
993
+ accessibilityRole: "button",
994
+ accessibilityLabel: `${position} connection point`,
995
+ style: placement,
996
+ className: flowVariants().handle({
997
+ className
998
+ })
999
+ })
1000
+ });
1001
+ }
1002
+ FlowHandle.displayName = 'Flow.Handle';
1003
+
1004
+ /* -------------------------------------------------------------------------- */
1005
+ /* Edges */
1006
+ /* -------------------------------------------------------------------------- */
1007
+
1008
+ /**
1009
+ * A line between two nodes. It names them rather than coordinates, and works
1010
+ * out its own geometry from wherever they currently are — including which face
1011
+ * to use, which is why a graph the user rearranges does not need re-specifying.
1012
+ *
1013
+ * It draws nothing where it is written. Every edge in a canvas has to end up
1014
+ * inside one `<Svg>`, under every node — an SVG element rendered among the
1015
+ * nodes is not in an SVG at all and silently draws nothing — so an edge
1016
+ * registers itself and the canvas paints it in the right layer. Which means an
1017
+ * edge can be written wherever it reads best: beside the nodes it joins, inside
1018
+ * a group, inside a `.map`, behind a condition.
1019
+ */
1020
+ function FlowEdge(props) {
1021
+ const {
1022
+ registerEdge,
1023
+ unregisterEdge
1024
+ } = useFlow('Flow.Edge');
1025
+ const key = `${props.from}->${props.to}`;
1026
+
1027
+ // Re-registered whenever any prop changes, so `animated` toggling or a
1028
+ // variant switch reaches the layer that draws it.
1029
+ const json = JSON.stringify(props);
1030
+ useEffect(() => {
1031
+ registerEdge(key, props);
1032
+ return () => unregisterEdge(key);
1033
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1034
+ }, [key, json, registerEdge, unregisterEdge]);
1035
+ return null;
1036
+ }
1037
+
1038
+ /** The part that actually draws an edge, inside the canvas's one `<Svg>`. */
1039
+ function FlowEdgePath({
1040
+ from,
1041
+ to,
1042
+ variant = 'bezier',
1043
+ animated = false,
1044
+ dashed = false,
1045
+ color,
1046
+ width = 2,
1047
+ arrow = false,
1048
+ fromSide,
1049
+ toSide,
1050
+ radius = 8,
1051
+ gap = 20,
1052
+ curvature = 0.25,
1053
+ boxes,
1054
+ handles,
1055
+ tint,
1056
+ origin
1057
+ }) {
1058
+ const source = resolveEnd(from, fromSide, handles);
1059
+ const target = resolveEnd(to, toSide, handles);
1060
+ const fromRect = boxes[source.node];
1061
+ const toRect = boxes[target.node];
1062
+ if (!fromRect || !toRect || fromRect.width === 0 || toRect.width === 0) return null;
1063
+ const auto = autoSides(fromRect, toRect);
1064
+ const sideA = source.side ?? auto.from;
1065
+ const sideB = target.side ?? auto.to;
1066
+ const rawA = anchorOf(fromRect, sideA, source.offset);
1067
+ const rawB = anchorOf(toRect, sideB, target.offset);
1068
+
1069
+ // Shifted into the layer's own coordinates, which is exactly what a node
1070
+ // does with its translate. Both use the same arithmetic on purpose: an SVG
1071
+ // `viewBox` would have done the shift too, but it also rescales its contents
1072
+ // to fit whenever the viewport and the laid-out size disagree, and an edge
1073
+ // drawn at a slightly different scale from the nodes misses them by a little
1074
+ // everywhere — which looks like bad routing rather than a bad transform.
1075
+ const a = {
1076
+ x: rawA.x + origin.x,
1077
+ y: rawA.y + origin.y
1078
+ };
1079
+ const b = {
1080
+ x: rawB.x + origin.x,
1081
+ y: rawB.y + origin.y
1082
+ };
1083
+ const d = edgePath(variant, a, sideA, b, sideB, curvature, radius, gap);
1084
+ const dash = dashed || animated ? 6 : 0;
1085
+ const stroke = color ?? tint;
1086
+ const head = arrow ? (() => {
1087
+ const dir = arrivalDirection(variant, a, b, sideB);
1088
+ return arrowHeadPath(b, dir.x, dir.y, Math.max(width * 3.5, 7));
1089
+ })() : null;
1090
+ return /*#__PURE__*/_jsxs(_Fragment, {
1091
+ children: [/*#__PURE__*/_jsx(Path, {
1092
+ d: d,
1093
+ fill: "none",
1094
+ stroke: stroke,
1095
+ strokeWidth: width,
1096
+ strokeLinecap: "round",
1097
+ strokeLinejoin: "round",
1098
+ strokeDasharray: dash ? `${dash} ${dash}` : undefined
1099
+ }), head ? /*#__PURE__*/_jsx(Path, {
1100
+ d: head,
1101
+ fill: stroke
1102
+ }) : null]
1103
+ });
1104
+ }
1105
+
1106
+ /**
1107
+ * Splits `"node"` or `"node.handle"` into the node and, when a handle was
1108
+ * named, the face and offset that handle registered.
1109
+ */
1110
+ function resolveEnd(reference, explicit, handles) {
1111
+ const dot = reference.indexOf('.');
1112
+ if (dot === -1) {
1113
+ return {
1114
+ node: reference,
1115
+ side: explicit,
1116
+ offset: 0.5
1117
+ };
1118
+ }
1119
+ const node = reference.slice(0, dot);
1120
+ const entry = handles.find(handle => handle.key === reference);
1121
+ return {
1122
+ node,
1123
+ side: explicit ?? entry?.side,
1124
+ offset: entry?.offset ?? 0.5
1125
+ };
1126
+ }
1127
+
1128
+ /**
1129
+ * The one SVG every edge draws into, sitting inside the transformed layer in
1130
+ * graph coordinates — so panning and zooming the canvas moves the edges with
1131
+ * it, for free, and no edge does any work per frame.
1132
+ */
1133
+ function FlowEdgeLayer() {
1134
+ const {
1135
+ boxes,
1136
+ edges,
1137
+ handles,
1138
+ connection,
1139
+ layer,
1140
+ origin
1141
+ } = useFlow('Flow');
1142
+ const token = useCSSVariable('--color-muted-foreground');
1143
+ const tint = typeof token === 'string' ? token : '#878787';
1144
+ const lineProps = useAnimatedProps(() => {
1145
+ const current = connection.value;
1146
+ if (current.active === 0) return {
1147
+ d: ''
1148
+ };
1149
+ return {
1150
+ d: `M${current.x1 + origin.x},${current.y1 + origin.y} ` + `L${current.x2 + origin.x},${current.y2 + origin.y}`
1151
+ };
1152
+ });
1153
+ return /*#__PURE__*/_jsxs(Svg, {
1154
+ pointerEvents: "none",
1155
+ style: {
1156
+ position: 'absolute',
1157
+ left: 0,
1158
+ top: 0
1159
+ },
1160
+ width: layer.width,
1161
+ height: layer.height,
1162
+ children: [edges.map(edge => /*#__PURE__*/_jsx(FlowEdgePath, {
1163
+ ...edge.props,
1164
+ boxes: boxes,
1165
+ handles: handles,
1166
+ tint: tint,
1167
+ origin: origin
1168
+ }, edge.key)), /*#__PURE__*/_jsx(AnimatedPath, {
1169
+ animatedProps: lineProps,
1170
+ fill: "none",
1171
+ stroke: tint,
1172
+ strokeWidth: 2,
1173
+ strokeDasharray: "4 4",
1174
+ strokeLinecap: "round"
1175
+ })]
1176
+ });
1177
+ }
1178
+
1179
+ /* -------------------------------------------------------------------------- */
1180
+ /* Group */
1181
+ /* -------------------------------------------------------------------------- */
1182
+
1183
+ /**
1184
+ * A container other nodes sit in and travel with.
1185
+ *
1186
+ * Dragging it offsets every child in the same worklet that moves the group, so
1187
+ * a group of twenty nodes costs one frame's work rather than twenty.
1188
+ */
1189
+ function FlowGroup({
1190
+ id,
1191
+ position,
1192
+ size,
1193
+ label,
1194
+ className,
1195
+ draggable = true,
1196
+ children,
1197
+ ...props
1198
+ }) {
1199
+ const flow = useFlow('Flow.Group');
1200
+ const {
1201
+ rects,
1202
+ zoom,
1203
+ locked,
1204
+ origin,
1205
+ setNodeRect,
1206
+ dropNodeRect,
1207
+ registerNode,
1208
+ unregisterNode,
1209
+ onNodeDragEnd
1210
+ } = flow;
1211
+ useEffect(() => {
1212
+ registerNode(id);
1213
+ return () => unregisterNode(id);
1214
+ }, [id, registerNode, unregisterNode]);
1215
+ useEffect(() => {
1216
+ setNodeRect(id, {
1217
+ x: position.x,
1218
+ y: position.y,
1219
+ width: size.width,
1220
+ height: size.height
1221
+ });
1222
+ return () => dropNodeRect(id);
1223
+ }, [dropNodeRect, id, position.x, position.y, setNodeRect, size.height, size.width]);
1224
+
1225
+ // Which nodes travel with the group, captured when they register rather than
1226
+ // read per frame.
1227
+ const childIds = useMemo(() => collectNodeIds(children), [children]);
1228
+ const start = useSharedValue({});
1229
+ const dragEnd = useCallback((x, y) => onNodeDragEnd?.(id, {
1230
+ x,
1231
+ y
1232
+ }), [id, onNodeDragEnd]);
1233
+
1234
+ /** Tells React where the group and everything in it got to. */
1235
+ const dragTo = useCallback(moved => {
1236
+ for (const [key, next] of Object.entries(moved)) setNodeRect(key, next);
1237
+ }, [setNodeRect]);
1238
+ const drag = useMemo(() => Gesture.Pan().enabled(draggable && !locked).onBegin(() => {
1239
+ 'worklet';
1240
+
1241
+ const snapshot = {};
1242
+ const group = rects.value[id];
1243
+ if (group) snapshot[id] = {
1244
+ x: group.x,
1245
+ y: group.y
1246
+ };
1247
+ for (let i = 0; i < childIds.length; i += 1) {
1248
+ const child = rects.value[childIds[i]];
1249
+ if (child) snapshot[childIds[i]] = {
1250
+ x: child.x,
1251
+ y: child.y
1252
+ };
1253
+ }
1254
+ start.value = snapshot;
1255
+ }).onUpdate(event => {
1256
+ 'worklet';
1257
+
1258
+ const dx = event.translationX / zoom.value;
1259
+ const dy = event.translationY / zoom.value;
1260
+ const next = {
1261
+ ...rects.value
1262
+ };
1263
+ const moved = {};
1264
+ const ids = Object.keys(start.value);
1265
+ for (let i = 0; i < ids.length; i += 1) {
1266
+ const key = ids[i];
1267
+ const rect = next[key];
1268
+ const from = start.value[key];
1269
+ if (!rect || !from) continue;
1270
+ const x = from.x + dx;
1271
+ const y = from.y + dy;
1272
+ next[key] = {
1273
+ ...rect,
1274
+ x,
1275
+ y
1276
+ };
1277
+ moved[key] = {
1278
+ x,
1279
+ y
1280
+ };
1281
+ }
1282
+ rects.value = next;
1283
+ runOnJS(dragTo)(moved);
1284
+ }).onEnd(() => {
1285
+ 'worklet';
1286
+
1287
+ const rect = rects.value[id];
1288
+ if (rect) runOnJS(dragEnd)(rect.x, rect.y);
1289
+ }), [childIds, dragEnd, dragTo, draggable, id, locked, rects, start, zoom]);
1290
+ const style = useAnimatedStyle(() => {
1291
+ const rect = rects.value[id];
1292
+ return {
1293
+ transform: [{
1294
+ translateX: (rect?.x ?? 0) + origin.x
1295
+ }, {
1296
+ translateY: (rect?.y ?? 0) + origin.y
1297
+ }]
1298
+ };
1299
+ });
1300
+ return /*#__PURE__*/_jsxs(FlowGroupContext.Provider, {
1301
+ value: id,
1302
+ children: [/*#__PURE__*/_jsx(GestureDetector, {
1303
+ gesture: drag,
1304
+ children: /*#__PURE__*/_jsx(Animated.View, {
1305
+ collapsable: false,
1306
+ style: [{
1307
+ position: 'absolute',
1308
+ top: 0,
1309
+ left: 0,
1310
+ width: size.width,
1311
+ height: size.height
1312
+ }, style],
1313
+ className: cn('rounded-2xl border border-dashed border-border bg-muted/25', className),
1314
+ accessibilityRole: "none",
1315
+ accessibilityLabel: label ?? id,
1316
+ ...props,
1317
+ children: label ? /*#__PURE__*/_jsx(Text, {
1318
+ size: "xs",
1319
+ muted: true,
1320
+ className: "px-3 pt-2 uppercase tracking-wider",
1321
+ children: label
1322
+ }) : null
1323
+ })
1324
+ }), children]
1325
+ });
1326
+ }
1327
+ FlowGroup.displayName = 'Flow.Group';
1328
+ function collectNodeIds(children) {
1329
+ const ids = [];
1330
+ const list = Array.isArray(children) ? children.flat(Infinity) : [children];
1331
+ for (const child of list) {
1332
+ if (child && typeof child === 'object' && 'props' in child && typeof child.props?.id === 'string') {
1333
+ ids.push(child.props.id);
1334
+ }
1335
+ }
1336
+ return ids;
1337
+ }
1338
+
1339
+ /* -------------------------------------------------------------------------- */
1340
+ /* Controls */
1341
+ /* -------------------------------------------------------------------------- */
1342
+
1343
+ /**
1344
+ * The button stack in the corner. It sits outside the transformed layer, so it
1345
+ * stays put while the canvas moves under it.
1346
+ *
1347
+ * Worth having even where pinch works: on a phone, pinching to a specific
1348
+ * scale is imprecise, and framing the whole graph by hand is worse.
1349
+ */
1350
+ function FlowControls({
1351
+ className,
1352
+ zoom: showZoom = true,
1353
+ fit = true,
1354
+ lock = true,
1355
+ step = 1.3
1356
+ }) {
1357
+ const {
1358
+ zoomBy,
1359
+ fitView,
1360
+ locked,
1361
+ setLocked
1362
+ } = useFlow('Flow.Controls');
1363
+ const slots = flowVariants();
1364
+ return /*#__PURE__*/_jsxs(View, {
1365
+ className: slots.controls({
1366
+ className
1367
+ }),
1368
+ children: [showZoom ? /*#__PURE__*/_jsxs(_Fragment, {
1369
+ children: [/*#__PURE__*/_jsx(AnimatedPressable, {
1370
+ accessibilityRole: "button",
1371
+ accessibilityLabel: "Zoom in",
1372
+ className: slots.control(),
1373
+ onPress: () => zoomBy(step),
1374
+ children: /*#__PURE__*/_jsx(PlusIcon, {
1375
+ size: 16
1376
+ })
1377
+ }), /*#__PURE__*/_jsx(View, {
1378
+ className: "h-px bg-border"
1379
+ }), /*#__PURE__*/_jsx(AnimatedPressable, {
1380
+ accessibilityRole: "button",
1381
+ accessibilityLabel: "Zoom out",
1382
+ className: slots.control(),
1383
+ onPress: () => zoomBy(1 / step),
1384
+ children: /*#__PURE__*/_jsx(MinusIcon, {
1385
+ size: 16
1386
+ })
1387
+ })]
1388
+ }) : null, fit ? /*#__PURE__*/_jsxs(_Fragment, {
1389
+ children: [/*#__PURE__*/_jsx(View, {
1390
+ className: "h-px bg-border"
1391
+ }), /*#__PURE__*/_jsx(AnimatedPressable, {
1392
+ accessibilityRole: "button",
1393
+ accessibilityLabel: "Fit the whole graph on screen",
1394
+ className: slots.control(),
1395
+ onPress: fitView,
1396
+ children: /*#__PURE__*/_jsx(MaximizeIcon, {
1397
+ size: 16
1398
+ })
1399
+ })]
1400
+ }) : null, lock ? /*#__PURE__*/_jsxs(_Fragment, {
1401
+ children: [/*#__PURE__*/_jsx(View, {
1402
+ className: "h-px bg-border"
1403
+ }), /*#__PURE__*/_jsx(AnimatedPressable, {
1404
+ accessibilityRole: "button",
1405
+ accessibilityLabel: locked ? 'Unlock the canvas' : 'Lock the canvas',
1406
+ accessibilityState: {
1407
+ selected: locked
1408
+ },
1409
+ className: slots.control(),
1410
+ onPress: () => setLocked(!locked),
1411
+ children: locked ? /*#__PURE__*/_jsx(LockIcon, {
1412
+ size: 16
1413
+ }) : /*#__PURE__*/_jsx(UnlockIcon, {
1414
+ size: 16
1415
+ })
1416
+ })]
1417
+ }) : null]
1418
+ });
1419
+ }
1420
+ FlowControls.displayName = 'Flow.Controls';
1421
+ FlowControls.slot = 'overlay';
1422
+
1423
+ /* -------------------------------------------------------------------------- */
1424
+ /* MiniMap */
1425
+ /* -------------------------------------------------------------------------- */
1426
+
1427
+ const MINIMAP_CORNERS = {
1428
+ 'top-left': 'left-4 top-4',
1429
+ 'top-right': 'right-4 top-4',
1430
+ 'bottom-left': 'bottom-4 left-4',
1431
+ 'bottom-right': 'bottom-20 right-4'
1432
+ };
1433
+
1434
+ /**
1435
+ * An overview of the whole graph with the visible region marked on it — for a
1436
+ * canvas bigger than the screen, where panning alone loses you.
1437
+ *
1438
+ * The map's own scale is derived once per frame and shared by every node in it,
1439
+ * so N nodes cost one bounds calculation rather than N.
1440
+ */
1441
+ function FlowMiniMap({
1442
+ className,
1443
+ position = 'bottom-right',
1444
+ width = 120,
1445
+ height = 84,
1446
+ nodeColor
1447
+ }) {
1448
+ const {
1449
+ rects,
1450
+ nodeIds,
1451
+ translateX,
1452
+ translateY,
1453
+ zoom,
1454
+ size
1455
+ } = useFlow('Flow.MiniMap');
1456
+ const token = useCSSVariable('--color-muted-foreground');
1457
+ const tint = nodeColor ?? (typeof token === 'string' ? token : '#737373');
1458
+ const ringToken = useCSSVariable('--color-ring');
1459
+ const ring = typeof ringToken === 'string' ? ringToken : '#737373';
1460
+ const padding = 6;
1461
+
1462
+ /** The graph's bounds and the scale that fits them in the box. Once a frame. */
1463
+ const fit = useDerivedValue(() => {
1464
+ let left = Infinity;
1465
+ let top = Infinity;
1466
+ let right = -Infinity;
1467
+ let bottom = -Infinity;
1468
+ const all = Object.values(rects.value);
1469
+ for (let i = 0; i < all.length; i += 1) {
1470
+ const rect = all[i];
1471
+ if (rect.width === 0) continue;
1472
+ left = Math.min(left, rect.x);
1473
+ top = Math.min(top, rect.y);
1474
+ right = Math.max(right, rect.x + rect.width);
1475
+ bottom = Math.max(bottom, rect.y + rect.height);
1476
+ }
1477
+ if (!Number.isFinite(left)) {
1478
+ return {
1479
+ left: 0,
1480
+ top: 0,
1481
+ scale: 1
1482
+ };
1483
+ }
1484
+
1485
+ // A floor under the graph's size, and a ceiling over the scale. Before
1486
+ // every node has measured, the bounds can be a single small box — and the
1487
+ // scale that fits one box to the map blows every rectangle up until the
1488
+ // map is a solid block of colour.
1489
+ const graphWidth = Math.max(right - left, 200);
1490
+ const graphHeight = Math.max(bottom - top, 200);
1491
+ const scale = Math.min((width - padding * 2) / graphWidth, (height - padding * 2) / graphHeight, 1);
1492
+ return {
1493
+ left,
1494
+ top,
1495
+ scale
1496
+ };
1497
+ });
1498
+ const viewportProps = useAnimatedProps(() => {
1499
+ const {
1500
+ left,
1501
+ top,
1502
+ scale
1503
+ } = fit.value;
1504
+ const screen = size.value;
1505
+ if (screen.width === 0) return {
1506
+ x: 0,
1507
+ y: 0,
1508
+ width: 0,
1509
+ height: 0
1510
+ };
1511
+
1512
+ // The graph rectangle currently on screen, mapped into the map's space.
1513
+ const graphX = -translateX.value / zoom.value;
1514
+ const graphY = -translateY.value / zoom.value;
1515
+ return {
1516
+ x: padding + (graphX - left) * scale,
1517
+ y: padding + (graphY - top) * scale,
1518
+ width: Math.min(screen.width / zoom.value * scale, width),
1519
+ height: Math.min(screen.height / zoom.value * scale, height)
1520
+ };
1521
+ });
1522
+ return /*#__PURE__*/_jsx(View, {
1523
+ pointerEvents: "none",
1524
+ style: {
1525
+ width,
1526
+ height
1527
+ },
1528
+ className: cn(flowVariants().minimap(), MINIMAP_CORNERS[position], className),
1529
+ accessibilityLabel: "Graph overview",
1530
+ children: /*#__PURE__*/_jsxs(Svg, {
1531
+ width: width,
1532
+ height: height,
1533
+ children: [nodeIds.map(id => /*#__PURE__*/_jsx(MiniMapNode, {
1534
+ id: id,
1535
+ rects: rects,
1536
+ fit: fit,
1537
+ padding: padding,
1538
+ color: tint,
1539
+ width: width,
1540
+ height: height
1541
+ }, id)), /*#__PURE__*/_jsx(AnimatedRect, {
1542
+ animatedProps: viewportProps,
1543
+ fill: "none",
1544
+ stroke: ring,
1545
+ strokeWidth: 1,
1546
+ rx: 2
1547
+ })]
1548
+ })
1549
+ });
1550
+ }
1551
+ FlowMiniMap.displayName = 'Flow.MiniMap';
1552
+ FlowMiniMap.slot = 'overlay';
1553
+ function MiniMapNode({
1554
+ id,
1555
+ rects,
1556
+ fit,
1557
+ padding,
1558
+ color,
1559
+ width,
1560
+ height
1561
+ }) {
1562
+ const props = useAnimatedProps(() => {
1563
+ const rect = rects.value[id];
1564
+ if (!rect || rect.width === 0) return {
1565
+ x: 0,
1566
+ y: 0,
1567
+ width: 0,
1568
+ height: 0
1569
+ };
1570
+ const {
1571
+ left,
1572
+ top,
1573
+ scale
1574
+ } = fit.value;
1575
+ return {
1576
+ x: padding + (rect.x - left) * scale,
1577
+ y: padding + (rect.y - top) * scale,
1578
+ width: Math.min(Math.max(rect.width * scale, 2), width),
1579
+ height: Math.min(Math.max(rect.height * scale, 2), height)
1580
+ };
1581
+ });
1582
+ return /*#__PURE__*/_jsx(AnimatedRect, {
1583
+ animatedProps: props,
1584
+ fill: color,
1585
+ opacity: 0.55,
1586
+ rx: 1.5
1587
+ });
1588
+ }
1589
+ export const Flow = Object.assign(FlowRoot, {
1590
+ Background: FlowBackground,
1591
+ Node: FlowNode,
1592
+ Handle: FlowHandle,
1593
+ Edge: FlowEdge,
1594
+ Group: FlowGroup,
1595
+ Controls: FlowControls,
1596
+ MiniMap: FlowMiniMap
1597
+ });
1598
+ //# sourceMappingURL=index.js.map