panelui-native 0.20.1 → 0.22.3
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/lib/module/components/bottom-sheet/index.js +35 -10
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/flow/flow-paths.js +18 -0
- package/lib/module/components/flow/flow-paths.js.map +1 -1
- package/lib/module/components/flow/index.js +156 -31
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/input/index.js +15 -5
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/number-input/index.js +350 -0
- package/lib/module/components/number-input/index.js.map +1 -0
- package/lib/module/components/signature/index.js +7 -3
- package/lib/module/components/signature/index.js.map +1 -1
- package/lib/module/components/switch/index.js +7 -2
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/icons/index.js +39 -1
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-paths.d.ts +8 -0
- package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/index.d.ts +13 -1
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/index.d.ts +169 -0
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -0
- package/lib/typescript/src/components/signature/index.d.ts +4 -0
- package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +7 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +10 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/bottom-sheet/index.tsx +52 -21
- package/src/components/flow/flow-paths.ts +14 -0
- package/src/components/flow/index.tsx +197 -31
- package/src/components/input/index.tsx +15 -5
- package/src/components/number-input/index.tsx +373 -0
- package/src/components/signature/index.tsx +13 -3
- package/src/components/switch/index.tsx +16 -2
- package/src/icons/index.tsx +32 -2
- package/src/index.ts +8 -0
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isValidElement,
|
|
5
5
|
useCallback,
|
|
6
6
|
useContext,
|
|
7
|
+
useEffect,
|
|
7
8
|
useMemo,
|
|
8
9
|
useState,
|
|
9
10
|
type ReactElement,
|
|
@@ -234,24 +235,49 @@ function BottomSheetContent({
|
|
|
234
235
|
|
|
235
236
|
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
236
237
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
238
|
+
/*
|
|
239
|
+
* The offset survives a close — the early return below sits after every
|
|
240
|
+
* hook, so this component keeps its state while the sheet is hidden. A
|
|
241
|
+
* swipe-dismiss leaves it parked a screen height down, and without this the
|
|
242
|
+
* next open would draw the sheet below the fold behind a full backdrop:
|
|
243
|
+
* a dimmed screen with nothing on it and no reachable close button.
|
|
244
|
+
*/
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (open) translateY.value = 0;
|
|
247
|
+
}, [open, translateY]);
|
|
248
|
+
|
|
249
|
+
const pan = useMemo(
|
|
250
|
+
() =>
|
|
251
|
+
Gesture.Pan()
|
|
252
|
+
/*
|
|
253
|
+
* A drag has to travel before it takes the touch. This detector wraps
|
|
254
|
+
* the whole sheet, close button included, and an unqualified Pan
|
|
255
|
+
* activates on a few pixels of drift — cancelling the press the button
|
|
256
|
+
* is in the middle of, so a tap with any finger travel does nothing.
|
|
257
|
+
*/
|
|
258
|
+
.activeOffsetY([-12, 12])
|
|
259
|
+
.onChange((event) => {
|
|
260
|
+
// Rubber-band when dragging upward, follow the finger downward.
|
|
261
|
+
const next = translateY.value + event.changeY;
|
|
262
|
+
translateY.value = next > 0 ? next : next / 3;
|
|
263
|
+
})
|
|
264
|
+
.onEnd((event) => {
|
|
265
|
+
if (
|
|
266
|
+
translateY.value > DISMISS_DISTANCE ||
|
|
267
|
+
event.velocityY > DISMISS_VELOCITY
|
|
268
|
+
) {
|
|
269
|
+
translateY.value = withTiming(screenHeight, { duration: 200 }, () => {
|
|
270
|
+
runOnJS(close)();
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
translateY.value = withSpring(0, SPRING);
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
// Rebuilt only when one of these changes. Built inline it would be a new
|
|
277
|
+
// gesture on every render — and the sheet re-renders while it is being
|
|
278
|
+
// used, each time re-attaching the handler and dropping the live touch.
|
|
279
|
+
[close, screenHeight, translateY]
|
|
280
|
+
);
|
|
255
281
|
|
|
256
282
|
const sheetStyle = useAnimatedStyle(() => ({
|
|
257
283
|
transform: [{ translateY: translateY.value }],
|
|
@@ -325,16 +351,21 @@ function BottomSheetContent({
|
|
|
325
351
|
<Animated.View
|
|
326
352
|
entering={SlideInDown.springify().damping(22).stiffness(240).mass(0.8)}
|
|
327
353
|
exiting={SlideOutDown.duration(200)}
|
|
354
|
+
accessibilityViewIsModal
|
|
355
|
+
className={sheetVariants({ detached, className })}
|
|
356
|
+
{...props}
|
|
357
|
+
// After the spread, and with the caller's own style folded in
|
|
358
|
+
// rather than replacing the array: spread last, a caller passing
|
|
359
|
+
// `style` for a height would silently drop the drag transform and
|
|
360
|
+
// the safe-area padding with it.
|
|
328
361
|
style={[
|
|
329
362
|
sheetStyle,
|
|
330
363
|
// A detached sheet's own bottom margin already clears the home
|
|
331
364
|
// indicator, so it takes plain padding rather than stacking the
|
|
332
365
|
// inset on top of the gap.
|
|
333
366
|
{ paddingBottom: detached ? 16 : Math.max(insets.bottom, 16) },
|
|
367
|
+
props.style,
|
|
334
368
|
]}
|
|
335
|
-
accessibilityViewIsModal
|
|
336
|
-
className={sheetVariants({ detached, className })}
|
|
337
|
-
{...props}
|
|
338
369
|
>
|
|
339
370
|
<View className="mb-3 self-center">
|
|
340
371
|
<View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
|
|
@@ -56,6 +56,20 @@ export function anchorOf(rect: FlowRect, side: FlowSide, offset: number): FlowPo
|
|
|
56
56
|
return { x: rect.x + rect.width, y: rect.y + rect.height * offset };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Pushes an anchor out along its own face's normal.
|
|
61
|
+
*
|
|
62
|
+
* For an end attached to something that draws its own border: landing exactly
|
|
63
|
+
* on the border puts the line under the stroke, where it reads as stopping
|
|
64
|
+
* short rather than as arriving.
|
|
65
|
+
*/
|
|
66
|
+
export function standOff(point: FlowPoint, side: FlowSide, by: number): FlowPoint {
|
|
67
|
+
'worklet';
|
|
68
|
+
if (by === 0) return point;
|
|
69
|
+
const normal = sideNormal(side);
|
|
70
|
+
return { x: point.x + normal.x * by, y: point.y + normal.y * by };
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/**
|
|
60
74
|
* The faces two nodes should use when nobody has said. Whichever axis they are
|
|
61
75
|
* further apart on wins, so nodes side by side connect left-to-right and nodes
|
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
arrowHeadPath,
|
|
93
93
|
autoSides,
|
|
94
94
|
edgePath,
|
|
95
|
+
standOff,
|
|
95
96
|
type FlowPoint,
|
|
96
97
|
type FlowRect,
|
|
97
98
|
type FlowSide,
|
|
@@ -111,6 +112,16 @@ const TRANSFORM_ORIGIN = { transformOrigin: 'top left' } as const;
|
|
|
111
112
|
/** How many screens of grid to draw around the container in each direction. */
|
|
112
113
|
const GRID_SPAN = 4;
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* The step the layer grows in once the graph outruns it.
|
|
117
|
+
*
|
|
118
|
+
* Quantised because the layer's size is React state on the render path: sized
|
|
119
|
+
* to the graph exactly, every frame of every drag would resize it and the SVG
|
|
120
|
+
* drawn at its size. Rounded up to a step, it changes a handful of times over a
|
|
121
|
+
* session instead.
|
|
122
|
+
*/
|
|
123
|
+
const LAYER_STEP = 600;
|
|
124
|
+
|
|
114
125
|
/**
|
|
115
126
|
* An SVG is backed by a single texture, and a texture has a maximum size the
|
|
116
127
|
* platform will allocate — past it, nothing is drawn at all rather than
|
|
@@ -124,6 +135,13 @@ function clampCanvas(value: number): number {
|
|
|
124
135
|
/** How close a finger has to get to a handle for a connection to land. */
|
|
125
136
|
const CONNECT_RADIUS = 44;
|
|
126
137
|
|
|
138
|
+
/**
|
|
139
|
+
* How far short of a container an edge stops. A group draws its own border, so
|
|
140
|
+
* an edge landing exactly on it would run under the stroke; a node has no
|
|
141
|
+
* border of its own and takes none of this.
|
|
142
|
+
*/
|
|
143
|
+
const GROUP_EDGE_STANDOFF = 5;
|
|
144
|
+
|
|
127
145
|
export type FlowEdgeVariant = 'bezier' | 'smoothstep' | 'step' | 'straight';
|
|
128
146
|
|
|
129
147
|
export interface FlowViewport {
|
|
@@ -198,10 +216,18 @@ interface FlowContextValue {
|
|
|
198
216
|
locked: boolean;
|
|
199
217
|
setLocked: (locked: boolean) => void;
|
|
200
218
|
nodeIds: string[];
|
|
219
|
+
/**
|
|
220
|
+
* Which of `nodeIds` are containers rather than nodes. A group writes its box
|
|
221
|
+
* into the same registry — that is what lets an edge name one and the minimap
|
|
222
|
+
* find it — so the only thing separating the two is this list.
|
|
223
|
+
*/
|
|
224
|
+
groupIds: string[];
|
|
201
225
|
handles: HandleEntry[];
|
|
202
226
|
edges: { key: string; props: FlowEdgeProps }[];
|
|
203
|
-
registerNode: (id: string
|
|
227
|
+
registerNode: (id: string) => void;
|
|
204
228
|
unregisterNode: (id: string) => void;
|
|
229
|
+
registerGroup: (id: string) => void;
|
|
230
|
+
unregisterGroup: (id: string) => void;
|
|
205
231
|
registerHandle: (entry: HandleEntry) => void;
|
|
206
232
|
unregisterHandle: (key: string) => void;
|
|
207
233
|
registerEdge: (key: string, props: FlowEdgeProps) => void;
|
|
@@ -307,6 +333,7 @@ function FlowRoot({
|
|
|
307
333
|
|
|
308
334
|
const [locked, setLocked] = useState(false);
|
|
309
335
|
const [nodeIds, setNodeIds] = useState<string[]>([]);
|
|
336
|
+
const [groupIds, setGroupIds] = useState<string[]>([]);
|
|
310
337
|
const [handles, setHandles] = useState<HandleEntry[]>([]);
|
|
311
338
|
const [edges, setEdges] = useState<{ key: string; props: FlowEdgeProps }[]>([]);
|
|
312
339
|
const [box, setBox] = useState({ width: 0, height: 0 });
|
|
@@ -371,12 +398,36 @@ function FlowRoot({
|
|
|
371
398
|
* of the canvas would quietly become unmovable. Sizing the layer to the same
|
|
372
399
|
* span as the grid keeps every node inside its parent, where it can be hit.
|
|
373
400
|
*/
|
|
401
|
+
/**
|
|
402
|
+
* How far the graph reaches from graph (0, 0), in its worst direction. The
|
|
403
|
+
* layer is symmetric about the origin, so one number covers all four sides.
|
|
404
|
+
*
|
|
405
|
+
* Without this the layer is a fixed multiple of the container, and a graph
|
|
406
|
+
* that grows past it has its edges clipped by the SVG drawn at that size —
|
|
407
|
+
* leaving nodes on screen with the lines between them missing, which reads as
|
|
408
|
+
* the graph having lost its connections rather than as a clipped canvas.
|
|
409
|
+
*/
|
|
410
|
+
const reach = useMemo(() => {
|
|
411
|
+
let out = 0;
|
|
412
|
+
for (const id in boxes) {
|
|
413
|
+
const rect = boxes[id]!;
|
|
414
|
+
out = Math.max(
|
|
415
|
+
out,
|
|
416
|
+
Math.abs(rect.x),
|
|
417
|
+
Math.abs(rect.y),
|
|
418
|
+
Math.abs(rect.x + rect.width),
|
|
419
|
+
Math.abs(rect.y + rect.height)
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return Math.ceil(out / LAYER_STEP) * LAYER_STEP;
|
|
423
|
+
}, [boxes]);
|
|
424
|
+
|
|
374
425
|
const layer = useMemo(
|
|
375
426
|
() => ({
|
|
376
|
-
width: clampCanvas(Math.max(box.width, 320) * GRID_SPAN),
|
|
377
|
-
height: clampCanvas(Math.max(box.height, 480) * GRID_SPAN),
|
|
427
|
+
width: clampCanvas(Math.max(Math.max(box.width, 320) * GRID_SPAN, reach * 2)),
|
|
428
|
+
height: clampCanvas(Math.max(Math.max(box.height, 480) * GRID_SPAN, reach * 2)),
|
|
378
429
|
}),
|
|
379
|
-
[box.height, box.width]
|
|
430
|
+
[box.height, box.width, reach]
|
|
380
431
|
);
|
|
381
432
|
const origin = useMemo(
|
|
382
433
|
() => ({ x: layer.width / 2, y: layer.height / 2 }),
|
|
@@ -398,6 +449,14 @@ function FlowRoot({
|
|
|
398
449
|
setNodeIds((current) => current.filter((entry) => entry !== id));
|
|
399
450
|
}, []);
|
|
400
451
|
|
|
452
|
+
const registerGroup = useCallback((id: string) => {
|
|
453
|
+
setGroupIds((current) => (current.includes(id) ? current : [...current, id]));
|
|
454
|
+
}, []);
|
|
455
|
+
|
|
456
|
+
const unregisterGroup = useCallback((id: string) => {
|
|
457
|
+
setGroupIds((current) => current.filter((entry) => entry !== id));
|
|
458
|
+
}, []);
|
|
459
|
+
|
|
401
460
|
const registerHandle = useCallback((entry: HandleEntry) => {
|
|
402
461
|
setHandles((current) => [...current.filter((h) => h.key !== entry.key), entry]);
|
|
403
462
|
}, []);
|
|
@@ -589,10 +648,13 @@ function FlowRoot({
|
|
|
589
648
|
locked,
|
|
590
649
|
setLocked,
|
|
591
650
|
nodeIds,
|
|
651
|
+
groupIds,
|
|
592
652
|
handles,
|
|
593
653
|
edges,
|
|
594
654
|
registerNode,
|
|
595
655
|
unregisterNode,
|
|
656
|
+
registerGroup,
|
|
657
|
+
unregisterGroup,
|
|
596
658
|
registerHandle,
|
|
597
659
|
unregisterHandle,
|
|
598
660
|
registerEdge,
|
|
@@ -621,14 +683,17 @@ function FlowRoot({
|
|
|
621
683
|
maxZoom,
|
|
622
684
|
minZoom,
|
|
623
685
|
nodeIds,
|
|
686
|
+
groupIds,
|
|
624
687
|
onConnect,
|
|
625
688
|
onNodeDragEnd,
|
|
626
689
|
rects,
|
|
690
|
+
registerGroup,
|
|
627
691
|
registerHandle,
|
|
628
692
|
registerNode,
|
|
629
693
|
size,
|
|
630
694
|
translateX,
|
|
631
695
|
translateY,
|
|
696
|
+
unregisterGroup,
|
|
632
697
|
unregisterHandle,
|
|
633
698
|
unregisterNode,
|
|
634
699
|
zoom,
|
|
@@ -891,6 +956,18 @@ export interface FlowNodeProps extends Omit<ViewProps, 'children'> {
|
|
|
891
956
|
className?: string;
|
|
892
957
|
/** Let a finger move it. */
|
|
893
958
|
draggable?: boolean;
|
|
959
|
+
/**
|
|
960
|
+
* Keep the node inside the `Flow.Group` it is drawn in. A drag stops at the
|
|
961
|
+
* container's edge instead of leaving it. Ignored outside a group — there is
|
|
962
|
+
* nothing to be kept inside of.
|
|
963
|
+
*/
|
|
964
|
+
confine?: boolean;
|
|
965
|
+
/**
|
|
966
|
+
* Hold the node still: it takes no drag of its own and moves only when its
|
|
967
|
+
* container does. For a diagram where the boxes are what you rearrange and
|
|
968
|
+
* their contents are a fixed part of them.
|
|
969
|
+
*/
|
|
970
|
+
pinned?: boolean;
|
|
894
971
|
/** Draw the selected ring. */
|
|
895
972
|
selected?: boolean;
|
|
896
973
|
/** Tapping the node — separate from dragging it. */
|
|
@@ -910,6 +987,8 @@ function FlowNode({
|
|
|
910
987
|
position,
|
|
911
988
|
className,
|
|
912
989
|
draggable = true,
|
|
990
|
+
confine = false,
|
|
991
|
+
pinned = false,
|
|
913
992
|
selected = false,
|
|
914
993
|
onPress,
|
|
915
994
|
accessibilityLabel,
|
|
@@ -927,13 +1006,25 @@ function FlowNode({
|
|
|
927
1006
|
dropNodeRect,
|
|
928
1007
|
registerNode,
|
|
929
1008
|
unregisterNode,
|
|
1009
|
+
registerGroup,
|
|
1010
|
+
unregisterGroup,
|
|
930
1011
|
onNodeDragEnd,
|
|
931
1012
|
} = flow;
|
|
932
1013
|
|
|
1014
|
+
/*
|
|
1015
|
+
* Registered twice, on purpose. As a node, because its box belongs in the
|
|
1016
|
+
* same registry every other box lives in — that is what lets an edge name it
|
|
1017
|
+
* and fitView account for it. As a group, because the minimap and the edge
|
|
1018
|
+
* layer both need to know it is a container and not a card.
|
|
1019
|
+
*/
|
|
933
1020
|
useEffect(() => {
|
|
934
|
-
registerNode(id
|
|
935
|
-
|
|
936
|
-
|
|
1021
|
+
registerNode(id);
|
|
1022
|
+
registerGroup(id);
|
|
1023
|
+
return () => {
|
|
1024
|
+
unregisterNode(id);
|
|
1025
|
+
unregisterGroup(id);
|
|
1026
|
+
};
|
|
1027
|
+
}, [id, registerGroup, registerNode, unregisterGroup, unregisterNode]);
|
|
937
1028
|
|
|
938
1029
|
// Writing on every render would fight the drag, snapping a node back to the
|
|
939
1030
|
// position it was first given. Only an actual change to the prop moves it.
|
|
@@ -982,7 +1073,9 @@ function FlowNode({
|
|
|
982
1073
|
const drag = useMemo(
|
|
983
1074
|
() =>
|
|
984
1075
|
Gesture.Pan()
|
|
985
|
-
|
|
1076
|
+
// A pinned node takes no drag at all: it is part of its container, and
|
|
1077
|
+
// the container is what moves.
|
|
1078
|
+
.enabled(draggable && !pinned && !locked)
|
|
986
1079
|
.onBegin(() => {
|
|
987
1080
|
'worklet';
|
|
988
1081
|
const rect = rects.value[id];
|
|
@@ -994,8 +1087,24 @@ function FlowNode({
|
|
|
994
1087
|
if (!rect) return;
|
|
995
1088
|
// Divided by the zoom, so the node keeps up with the finger rather
|
|
996
1089
|
// than with the graph coordinate the finger happens to be over.
|
|
997
|
-
|
|
998
|
-
|
|
1090
|
+
let x = start.value.x + event.translationX / zoom.value;
|
|
1091
|
+
let y = start.value.y + event.translationY / zoom.value;
|
|
1092
|
+
|
|
1093
|
+
/*
|
|
1094
|
+
* Clamped before anything is written, so the box React is told about
|
|
1095
|
+
* and the box on screen are the same one. Clamping after the write
|
|
1096
|
+
* would leave the edges drawn to a position the node never reached.
|
|
1097
|
+
*/
|
|
1098
|
+
if (confine && group) {
|
|
1099
|
+
const box = rects.value[group];
|
|
1100
|
+
if (box) {
|
|
1101
|
+
const maxX = box.x + box.width - rect.width;
|
|
1102
|
+
const maxY = box.y + box.height - rect.height;
|
|
1103
|
+
x = x < box.x ? box.x : x > maxX ? maxX : x;
|
|
1104
|
+
y = y < box.y ? box.y : y > maxY ? maxY : y;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
999
1108
|
rects.value = { ...rects.value, [id]: { ...rect, x, y } };
|
|
1000
1109
|
runOnJS(dragTo)(x, y);
|
|
1001
1110
|
})
|
|
@@ -1004,7 +1113,7 @@ function FlowNode({
|
|
|
1004
1113
|
const rect = rects.value[id];
|
|
1005
1114
|
if (rect) runOnJS(dragEnd)(rect.x, rect.y);
|
|
1006
1115
|
}),
|
|
1007
|
-
[dragEnd, dragTo, draggable, id, locked, rects, start, zoom]
|
|
1116
|
+
[confine, dragEnd, dragTo, draggable, group, id, locked, pinned, rects, start, zoom]
|
|
1008
1117
|
);
|
|
1009
1118
|
|
|
1010
1119
|
// Raced with the drag rather than layered on top of it. A plain touch handler
|
|
@@ -1340,11 +1449,13 @@ function FlowEdgePath({
|
|
|
1340
1449
|
curvature = 0.25,
|
|
1341
1450
|
boxes,
|
|
1342
1451
|
handles,
|
|
1452
|
+
groups,
|
|
1343
1453
|
tint,
|
|
1344
1454
|
origin,
|
|
1345
1455
|
}: FlowEdgeProps & {
|
|
1346
1456
|
boxes: Record<string, FlowRect>;
|
|
1347
1457
|
handles: HandleEntry[];
|
|
1458
|
+
groups: Set<string>;
|
|
1348
1459
|
tint: string;
|
|
1349
1460
|
origin: { x: number; y: number };
|
|
1350
1461
|
}) {
|
|
@@ -1367,8 +1478,18 @@ function FlowEdgePath({
|
|
|
1367
1478
|
// to fit whenever the viewport and the laid-out size disagree, and an edge
|
|
1368
1479
|
// drawn at a slightly different scale from the nodes misses them by a little
|
|
1369
1480
|
// everywhere — which looks like bad routing rather than a bad transform.
|
|
1370
|
-
|
|
1371
|
-
|
|
1481
|
+
/*
|
|
1482
|
+
* A container's edge is a drawn border rather than the invisible edge of a
|
|
1483
|
+
* card, so an edge that lands exactly on it disappears under the stroke.
|
|
1484
|
+
* Standing a few points off it reads as a line arriving at the box.
|
|
1485
|
+
*/
|
|
1486
|
+
const offA = groups.has(source.node) ? GROUP_EDGE_STANDOFF : 0;
|
|
1487
|
+
const offB = groups.has(target.node) ? GROUP_EDGE_STANDOFF : 0;
|
|
1488
|
+
const outA = standOff(rawA, sideA, offA);
|
|
1489
|
+
const outB = standOff(rawB, sideB, offB);
|
|
1490
|
+
|
|
1491
|
+
const a = { x: outA.x + origin.x, y: outA.y + origin.y };
|
|
1492
|
+
const b = { x: outB.x + origin.x, y: outB.y + origin.y };
|
|
1372
1493
|
|
|
1373
1494
|
const d = edgePath(variant, a, sideA, b, sideB, curvature, radius, gap);
|
|
1374
1495
|
const dash = dashed || animated ? 6 : 0;
|
|
@@ -1425,10 +1546,13 @@ function resolveEnd(
|
|
|
1425
1546
|
* it, for free, and no edge does any work per frame.
|
|
1426
1547
|
*/
|
|
1427
1548
|
function FlowEdgeLayer() {
|
|
1428
|
-
const { boxes, edges, handles, connection, layer, origin } = useFlow('Flow');
|
|
1549
|
+
const { boxes, edges, handles, groupIds, connection, layer, origin } = useFlow('Flow');
|
|
1429
1550
|
const token = useCSSVariable('--color-muted-foreground');
|
|
1430
1551
|
const tint = typeof token === 'string' ? token : '#878787';
|
|
1431
1552
|
|
|
1553
|
+
// A set rather than the array: every edge asks about both of its ends.
|
|
1554
|
+
const groups = useMemo(() => new Set(groupIds), [groupIds]);
|
|
1555
|
+
|
|
1432
1556
|
const lineProps = useAnimatedProps(() => {
|
|
1433
1557
|
const current = connection.value;
|
|
1434
1558
|
if (current.active === 0) return { d: '' };
|
|
@@ -1452,6 +1576,7 @@ function FlowEdgeLayer() {
|
|
|
1452
1576
|
{...edge.props}
|
|
1453
1577
|
boxes={boxes}
|
|
1454
1578
|
handles={handles}
|
|
1579
|
+
groups={groups}
|
|
1455
1580
|
tint={tint}
|
|
1456
1581
|
origin={origin}
|
|
1457
1582
|
/>
|
|
@@ -1540,9 +1665,16 @@ function FlowGroup({
|
|
|
1540
1665
|
|
|
1541
1666
|
const start = useSharedValue<Record<string, FlowNodePosition>>({});
|
|
1542
1667
|
|
|
1668
|
+
/**
|
|
1669
|
+
* Reports the group *and* everything that travelled with it. Reporting only
|
|
1670
|
+
* the container would leave a graph whose positions are driven from outside
|
|
1671
|
+
* holding stale coordinates for every node in it.
|
|
1672
|
+
*/
|
|
1543
1673
|
const dragEnd = useCallback(
|
|
1544
|
-
(
|
|
1545
|
-
|
|
1674
|
+
(moved: Record<string, FlowNodePosition>) => {
|
|
1675
|
+
for (const [key, next] of Object.entries(moved)) onNodeDragEnd?.(key, next);
|
|
1676
|
+
},
|
|
1677
|
+
[onNodeDragEnd]
|
|
1546
1678
|
);
|
|
1547
1679
|
|
|
1548
1680
|
/** Tells React where the group and everything in it got to. */
|
|
@@ -1590,8 +1722,14 @@ function FlowGroup({
|
|
|
1590
1722
|
})
|
|
1591
1723
|
.onEnd(() => {
|
|
1592
1724
|
'worklet';
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1725
|
+
const settled: Record<string, FlowNodePosition> = {};
|
|
1726
|
+
const ids = Object.keys(start.value);
|
|
1727
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
1728
|
+
const key = ids[i]!;
|
|
1729
|
+
const rect = rects.value[key];
|
|
1730
|
+
if (rect) settled[key] = { x: rect.x, y: rect.y };
|
|
1731
|
+
}
|
|
1732
|
+
runOnJS(dragEnd)(settled);
|
|
1595
1733
|
}),
|
|
1596
1734
|
[childIds, dragEnd, dragTo, draggable, id, locked, rects, start, zoom]
|
|
1597
1735
|
);
|
|
@@ -1781,11 +1919,16 @@ function FlowMiniMap({
|
|
|
1781
1919
|
height = 84,
|
|
1782
1920
|
nodeColor,
|
|
1783
1921
|
}: FlowMiniMapProps) {
|
|
1784
|
-
const { rects, nodeIds, translateX, translateY, zoom, size } =
|
|
1922
|
+
const { rects, nodeIds, groupIds, translateX, translateY, zoom, size } =
|
|
1923
|
+
useFlow('Flow.MiniMap');
|
|
1785
1924
|
const token = useCSSVariable('--color-muted-foreground');
|
|
1786
1925
|
const tint = nodeColor ?? (typeof token === 'string' ? token : '#737373');
|
|
1787
1926
|
const ringToken = useCSSVariable('--color-ring');
|
|
1788
1927
|
const ring = typeof ringToken === 'string' ? ringToken : '#737373';
|
|
1928
|
+
const borderToken = useCSSVariable('--color-border');
|
|
1929
|
+
const outline = typeof borderToken === 'string' ? borderToken : '#404040';
|
|
1930
|
+
|
|
1931
|
+
const groups = useMemo(() => new Set(groupIds), [groupIds]);
|
|
1789
1932
|
|
|
1790
1933
|
const padding = 6;
|
|
1791
1934
|
|
|
@@ -1853,18 +1996,25 @@ function FlowMiniMap({
|
|
|
1853
1996
|
accessibilityLabel="Graph overview"
|
|
1854
1997
|
>
|
|
1855
1998
|
<Svg width={width} height={height}>
|
|
1856
|
-
{
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1999
|
+
{/* Containers first and drawn as outlines, so the map reads the way
|
|
2000
|
+
the canvas does — boxes with things in them, rather than a wash of
|
|
2001
|
+
identical rectangles where the largest happens to be a group. */}
|
|
2002
|
+
{nodeIds.map((id) => {
|
|
2003
|
+
const group = groups.has(id);
|
|
2004
|
+
return (
|
|
2005
|
+
<MiniMapNode
|
|
2006
|
+
key={id}
|
|
2007
|
+
id={id}
|
|
2008
|
+
group={group}
|
|
2009
|
+
rects={rects}
|
|
2010
|
+
fit={fit}
|
|
2011
|
+
padding={padding}
|
|
2012
|
+
color={group ? outline : tint}
|
|
2013
|
+
width={width}
|
|
2014
|
+
height={height}
|
|
2015
|
+
/>
|
|
2016
|
+
);
|
|
2017
|
+
})}
|
|
1868
2018
|
<AnimatedRect
|
|
1869
2019
|
animatedProps={viewportProps}
|
|
1870
2020
|
fill="none"
|
|
@@ -1881,6 +2031,7 @@ FlowMiniMap.slot = 'overlay' as FlowSlot;
|
|
|
1881
2031
|
|
|
1882
2032
|
function MiniMapNode({
|
|
1883
2033
|
id,
|
|
2034
|
+
group = false,
|
|
1884
2035
|
rects,
|
|
1885
2036
|
fit,
|
|
1886
2037
|
padding,
|
|
@@ -1889,6 +2040,8 @@ function MiniMapNode({
|
|
|
1889
2040
|
height,
|
|
1890
2041
|
}: {
|
|
1891
2042
|
id: string;
|
|
2043
|
+
/** Draw it as a container: outlined, not filled. */
|
|
2044
|
+
group?: boolean;
|
|
1892
2045
|
rects: SharedValue<Record<string, FlowRect>>;
|
|
1893
2046
|
fit: SharedValue<{ left: number; top: number; scale: number }>;
|
|
1894
2047
|
padding: number;
|
|
@@ -1908,6 +2061,19 @@ function MiniMapNode({
|
|
|
1908
2061
|
};
|
|
1909
2062
|
});
|
|
1910
2063
|
|
|
2064
|
+
if (group) {
|
|
2065
|
+
return (
|
|
2066
|
+
<AnimatedRect
|
|
2067
|
+
animatedProps={props}
|
|
2068
|
+
fill="none"
|
|
2069
|
+
stroke={color}
|
|
2070
|
+
strokeWidth={1}
|
|
2071
|
+
opacity={0.9}
|
|
2072
|
+
rx={2}
|
|
2073
|
+
/>
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1911
2077
|
return <AnimatedRect animatedProps={props} fill={color} opacity={0.55} rx={1.5} />;
|
|
1912
2078
|
}
|
|
1913
2079
|
|
|
@@ -52,14 +52,24 @@ const inputVariants = tv({
|
|
|
52
52
|
filled: { field: 'bg-muted' },
|
|
53
53
|
},
|
|
54
54
|
size: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
/*
|
|
56
|
+
* The font size is given as a length rather than through a `text-*`
|
|
57
|
+
* step, because those set a size and a line height together — 16px text
|
|
58
|
+
* in a 24px line box. The extra leading lands above the glyphs, so in a
|
|
59
|
+
* field of fixed height the text and the placeholder end up sitting
|
|
60
|
+
* below the middle of the box, a few pixels off whatever is beside them.
|
|
61
|
+
* A length sets the size alone and leaves the line box the font's own.
|
|
62
|
+
*/
|
|
63
|
+
sm: { field: 'h-10 px-3 text-[14px]' },
|
|
64
|
+
md: { field: 'h-12 px-3.5 text-[16px]' },
|
|
65
|
+
lg: { field: 'h-14 px-4 text-[16px]' },
|
|
58
66
|
},
|
|
59
67
|
multiline: {
|
|
60
68
|
// A multiline field grows, so a fixed height would crop it. Text starts
|
|
61
|
-
// at the top rather than floating in the middle of an empty box
|
|
62
|
-
|
|
69
|
+
// at the top rather than floating in the middle of an empty box — and
|
|
70
|
+
// once there are several lines the leading is wanted back, since here it
|
|
71
|
+
// separates the lines instead of pushing one off centre.
|
|
72
|
+
true: { field: 'h-auto min-h-24 py-3 leading-normal' },
|
|
63
73
|
},
|
|
64
74
|
disabled: {
|
|
65
75
|
true: { field: 'opacity-[0.64]' },
|