panelui-native 0.19.0 → 0.21.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 (40) hide show
  1. package/lib/module/components/bottom-sheet/index.js +35 -10
  2. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  3. package/lib/module/components/flow/flow-paths.js +18 -0
  4. package/lib/module/components/flow/flow-paths.js.map +1 -1
  5. package/lib/module/components/flow/index.js +156 -31
  6. package/lib/module/components/flow/index.js.map +1 -1
  7. package/lib/module/components/rating/index.js +339 -0
  8. package/lib/module/components/rating/index.js.map +1 -0
  9. package/lib/module/components/signature/index.js +7 -3
  10. package/lib/module/components/signature/index.js.map +1 -1
  11. package/lib/module/components/slider/index.js +14 -1
  12. package/lib/module/components/slider/index.js.map +1 -1
  13. package/lib/module/icons/index.js +58 -0
  14. package/lib/module/icons/index.js.map +1 -1
  15. package/lib/module/index.js +5 -0
  16. package/lib/module/index.js.map +1 -1
  17. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  18. package/lib/typescript/src/components/flow/flow-paths.d.ts +8 -0
  19. package/lib/typescript/src/components/flow/flow-paths.d.ts.map +1 -1
  20. package/lib/typescript/src/components/flow/index.d.ts +13 -1
  21. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/rating/index.d.ts +122 -0
  23. package/lib/typescript/src/components/rating/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/signature/index.d.ts +4 -0
  25. package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/slider/index.d.ts +6 -0
  27. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  28. package/lib/typescript/src/icons/index.d.ts +10 -0
  29. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  30. package/lib/typescript/src/index.d.ts +2 -0
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/components/bottom-sheet/index.tsx +52 -21
  34. package/src/components/flow/flow-paths.ts +14 -0
  35. package/src/components/flow/index.tsx +197 -31
  36. package/src/components/rating/index.tsx +416 -0
  37. package/src/components/signature/index.tsx +13 -3
  38. package/src/components/slider/index.tsx +20 -1
  39. package/src/icons/index.tsx +49 -0
  40. package/src/index.ts +10 -0
@@ -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, parent?: string) => void;
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, group ?? undefined);
935
- return () => unregisterNode(id);
936
- }, [group, id, registerNode, unregisterNode]);
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
- .enabled(draggable && !locked)
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
- const x = start.value.x + event.translationX / zoom.value;
998
- const y = start.value.y + event.translationY / zoom.value;
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
- const a = { x: rawA.x + origin.x, y: rawA.y + origin.y };
1371
- const b = { x: rawB.x + origin.x, y: rawB.y + origin.y };
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
- (x: number, y: number) => onNodeDragEnd?.(id, { x, y }),
1545
- [id, onNodeDragEnd]
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 rect = rects.value[id];
1594
- if (rect) runOnJS(dragEnd)(rect.x, rect.y);
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 } = useFlow('Flow.MiniMap');
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
- {nodeIds.map((id) => (
1857
- <MiniMapNode
1858
- key={id}
1859
- id={id}
1860
- rects={rects}
1861
- fit={fit}
1862
- padding={padding}
1863
- color={tint}
1864
- width={width}
1865
- height={height}
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