panelui-native 0.63.0 → 0.66.1

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 (226) hide show
  1. package/README.md +5 -0
  2. package/lib/module/components/badge/index.js +1 -1
  3. package/lib/module/components/badge/index.js.map +1 -1
  4. package/lib/module/components/bar-chart/index.js +39 -2
  5. package/lib/module/components/bar-chart/index.js.map +1 -1
  6. package/lib/module/components/bottom-sheet/index.js +2 -2
  7. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +19 -9
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/carousel/carousel-lifecycle.js +51 -0
  11. package/lib/module/components/carousel/carousel-lifecycle.js.map +1 -0
  12. package/lib/module/components/carousel/index.js +86 -35
  13. package/lib/module/components/carousel/index.js.map +1 -1
  14. package/lib/module/components/checkbox/index.js +2 -3
  15. package/lib/module/components/checkbox/index.js.map +1 -1
  16. package/lib/module/components/context-menu/context-menu-invocation.js +23 -0
  17. package/lib/module/components/context-menu/context-menu-invocation.js.map +1 -0
  18. package/lib/module/components/context-menu/index.js +78 -9
  19. package/lib/module/components/context-menu/index.js.map +1 -1
  20. package/lib/module/components/dialog/index.js +2 -2
  21. package/lib/module/components/dialog/index.js.map +1 -1
  22. package/lib/module/components/drawer/index.js +2 -2
  23. package/lib/module/components/drawer/index.js.map +1 -1
  24. package/lib/module/components/fab/index.js +4 -3
  25. package/lib/module/components/fab/index.js.map +1 -1
  26. package/lib/module/components/field/index.js +41 -6
  27. package/lib/module/components/field/index.js.map +1 -1
  28. package/lib/module/components/flow/flow-accessibility.js +100 -0
  29. package/lib/module/components/flow/flow-accessibility.js.map +1 -0
  30. package/lib/module/components/flow/flow-identifiers.js +81 -0
  31. package/lib/module/components/flow/flow-identifiers.js.map +1 -0
  32. package/lib/module/components/flow/index.js +112 -57
  33. package/lib/module/components/flow/index.js.map +1 -1
  34. package/lib/module/components/form/use-form.js +93 -66
  35. package/lib/module/components/form/use-form.js.map +1 -1
  36. package/lib/module/components/heatmap-chart/index.js +2 -2
  37. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  38. package/lib/module/components/hex-chart/index.js +2 -2
  39. package/lib/module/components/hex-chart/index.js.map +1 -1
  40. package/lib/module/components/kpi/index.js +3 -1
  41. package/lib/module/components/kpi/index.js.map +1 -1
  42. package/lib/module/components/line-chart/index.js +3 -3
  43. package/lib/module/components/line-chart/index.js.map +1 -1
  44. package/lib/module/components/map/index.js +71 -8
  45. package/lib/module/components/map/index.js.map +1 -1
  46. package/lib/module/components/map/map-accessibility.js +20 -0
  47. package/lib/module/components/map/map-accessibility.js.map +1 -0
  48. package/lib/module/components/map/maplibre.js.map +1 -1
  49. package/lib/module/components/menu/index.js +4 -1
  50. package/lib/module/components/menu/index.js.map +1 -1
  51. package/lib/module/components/message-scroller/index.js +17 -9
  52. package/lib/module/components/message-scroller/index.js.map +1 -1
  53. package/lib/module/components/message-scroller/message-scroller-math.js +20 -0
  54. package/lib/module/components/message-scroller/message-scroller-math.js.map +1 -0
  55. package/lib/module/components/number-input/index.js +1 -23
  56. package/lib/module/components/number-input/index.js.map +1 -1
  57. package/lib/module/components/number-input/number-input-math.js +44 -0
  58. package/lib/module/components/number-input/number-input-math.js.map +1 -0
  59. package/lib/module/components/plot/index.js +2 -2
  60. package/lib/module/components/plot/index.js.map +1 -1
  61. package/lib/module/components/popover/index.js.map +1 -1
  62. package/lib/module/components/radio-group/index.js +1 -2
  63. package/lib/module/components/radio-group/index.js.map +1 -1
  64. package/lib/module/components/selection-mode/index.js +26 -31
  65. package/lib/module/components/selection-mode/index.js.map +1 -1
  66. package/lib/module/components/selection-mode/selection-mode-contracts.js +48 -0
  67. package/lib/module/components/selection-mode/selection-mode-contracts.js.map +1 -0
  68. package/lib/module/components/signature/index.js +62 -20
  69. package/lib/module/components/signature/index.js.map +1 -1
  70. package/lib/module/components/signature/signature-accessibility.js +58 -0
  71. package/lib/module/components/signature/signature-accessibility.js.map +1 -0
  72. package/lib/module/components/slider/index.js +22 -18
  73. package/lib/module/components/slider/index.js.map +1 -1
  74. package/lib/module/components/sortable/index.js +5 -55
  75. package/lib/module/components/sortable/index.js.map +1 -1
  76. package/lib/module/components/sortable/reorder.js +63 -0
  77. package/lib/module/components/sortable/reorder.js.map +1 -0
  78. package/lib/module/components/swipe/index.js +4 -4
  79. package/lib/module/components/swipe/index.js.map +1 -1
  80. package/lib/module/components/switch/index.js +36 -3
  81. package/lib/module/components/switch/index.js.map +1 -1
  82. package/lib/module/components/time-picker/accessibility.js +21 -0
  83. package/lib/module/components/time-picker/accessibility.js.map +1 -0
  84. package/lib/module/components/time-picker/index.js +34 -8
  85. package/lib/module/components/time-picker/index.js.map +1 -1
  86. package/lib/module/components/waterfall-chart/index.js +1325 -0
  87. package/lib/module/components/waterfall-chart/index.js.map +1 -0
  88. package/lib/module/index.js +1 -0
  89. package/lib/module/index.js.map +1 -1
  90. package/lib/module/primitives/modal-isolation-store.js +29 -0
  91. package/lib/module/primitives/modal-isolation-store.js.map +1 -0
  92. package/lib/module/primitives/portal.js +36 -4
  93. package/lib/module/primitives/portal.js.map +1 -1
  94. package/lib/module/primitives/scrim.js +94 -3
  95. package/lib/module/primitives/scrim.js.map +1 -1
  96. package/lib/module/providers/panel-ui-provider.js +27 -6
  97. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  98. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  100. package/lib/typescript/src/components/bottom-sheet/index.d.ts +3 -0
  101. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  103. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts +23 -0
  104. package/lib/typescript/src/components/carousel/carousel-lifecycle.d.ts.map +1 -0
  105. package/lib/typescript/src/components/carousel/index.d.ts +5 -2
  106. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts +20 -0
  109. package/lib/typescript/src/components/context-menu/context-menu-invocation.d.ts.map +1 -0
  110. package/lib/typescript/src/components/context-menu/index.d.ts +18 -8
  111. package/lib/typescript/src/components/context-menu/index.d.ts.map +1 -1
  112. package/lib/typescript/src/components/dialog/index.d.ts +3 -0
  113. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/drawer/index.d.ts +3 -0
  115. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  117. package/lib/typescript/src/components/field/index.d.ts +8 -1
  118. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  119. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +57 -0
  120. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
  121. package/lib/typescript/src/components/flow/flow-identifiers.d.ts +24 -0
  122. package/lib/typescript/src/components/flow/flow-identifiers.d.ts.map +1 -0
  123. package/lib/typescript/src/components/flow/index.d.ts +20 -7
  124. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  125. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  126. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  127. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  129. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/map/index.d.ts +13 -5
  131. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
  133. package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
  134. package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
  135. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
  136. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts +8 -0
  139. package/lib/typescript/src/components/message-scroller/message-scroller-math.d.ts.map +1 -0
  140. package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
  142. package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
  143. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  144. package/lib/typescript/src/components/popover/index.d.ts +3 -0
  145. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/selection-mode/index.d.ts +6 -4
  148. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts +18 -0
  150. package/lib/typescript/src/components/selection-mode/selection-mode-contracts.d.ts.map +1 -0
  151. package/lib/typescript/src/components/signature/index.d.ts +6 -0
  152. package/lib/typescript/src/components/signature/index.d.ts.map +1 -1
  153. package/lib/typescript/src/components/signature/signature-accessibility.d.ts +27 -0
  154. package/lib/typescript/src/components/signature/signature-accessibility.d.ts.map +1 -0
  155. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  156. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  157. package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
  158. package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
  159. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
  160. package/lib/typescript/src/components/switch/index.d.ts +3 -3
  161. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  162. package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
  163. package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
  164. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  165. package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
  166. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
  167. package/lib/typescript/src/index.d.ts +3 -2
  168. package/lib/typescript/src/index.d.ts.map +1 -1
  169. package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
  170. package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
  171. package/lib/typescript/src/primitives/portal.d.ts +6 -0
  172. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  173. package/lib/typescript/src/primitives/scrim.d.ts +5 -3
  174. package/lib/typescript/src/primitives/scrim.d.ts.map +1 -1
  175. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  176. package/package.json +11 -5
  177. package/src/components/badge/index.tsx +4 -1
  178. package/src/components/bar-chart/index.tsx +40 -3
  179. package/src/components/bottom-sheet/index.tsx +6 -3
  180. package/src/components/button/index.tsx +15 -21
  181. package/src/components/carousel/carousel-lifecycle.ts +74 -0
  182. package/src/components/carousel/index.tsx +118 -41
  183. package/src/components/checkbox/index.tsx +1 -2
  184. package/src/components/context-menu/context-menu-invocation.ts +44 -0
  185. package/src/components/context-menu/index.tsx +113 -13
  186. package/src/components/dialog/index.tsx +6 -3
  187. package/src/components/drawer/index.tsx +6 -3
  188. package/src/components/fab/index.tsx +8 -12
  189. package/src/components/field/index.tsx +52 -10
  190. package/src/components/flow/flow-accessibility.ts +123 -0
  191. package/src/components/flow/flow-identifiers.ts +98 -0
  192. package/src/components/flow/index.tsx +191 -69
  193. package/src/components/form/use-form.ts +101 -65
  194. package/src/components/heatmap-chart/index.tsx +2 -3
  195. package/src/components/hex-chart/index.tsx +2 -3
  196. package/src/components/kpi/index.tsx +3 -1
  197. package/src/components/line-chart/index.tsx +3 -2
  198. package/src/components/map/index.tsx +110 -7
  199. package/src/components/map/map-accessibility.ts +41 -0
  200. package/src/components/map/maplibre.ts +2 -0
  201. package/src/components/menu/index.tsx +4 -1
  202. package/src/components/message-scroller/index.tsx +39 -7
  203. package/src/components/message-scroller/message-scroller-math.ts +24 -0
  204. package/src/components/number-input/index.tsx +1 -26
  205. package/src/components/number-input/number-input-math.ts +50 -0
  206. package/src/components/plot/index.tsx +2 -1
  207. package/src/components/popover/index.tsx +3 -0
  208. package/src/components/radio-group/index.tsx +1 -2
  209. package/src/components/selection-mode/index.tsx +37 -30
  210. package/src/components/selection-mode/selection-mode-contracts.ts +53 -0
  211. package/src/components/signature/index.tsx +107 -20
  212. package/src/components/signature/signature-accessibility.ts +67 -0
  213. package/src/components/slider/index.tsx +28 -18
  214. package/src/components/sortable/index.tsx +12 -64
  215. package/src/components/sortable/reorder.ts +88 -0
  216. package/src/components/swipe/index.tsx +7 -4
  217. package/src/components/switch/index.tsx +42 -6
  218. package/src/components/time-picker/accessibility.ts +24 -0
  219. package/src/components/time-picker/index.tsx +48 -2
  220. package/src/components/waterfall-chart/index.tsx +1589 -0
  221. package/src/index.ts +25 -0
  222. package/src/primitives/modal-isolation-store.ts +31 -0
  223. package/src/primitives/portal.tsx +34 -4
  224. package/src/primitives/scrim.tsx +127 -7
  225. package/src/providers/panel-ui-provider.tsx +41 -9
  226. package/theme.css +30 -3
@@ -20,7 +20,10 @@ const badgeVariants = tv({
20
20
  default: { root: 'bg-primary', label: 'text-primary-foreground' },
21
21
  secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
22
22
  outline: { root: 'border-border bg-transparent', label: 'text-foreground' },
23
- destructive: { root: 'bg-destructive', label: 'text-white' },
23
+ destructive: {
24
+ root: 'bg-destructive',
25
+ label: 'text-destructive-solid-foreground',
26
+ },
24
27
  success: { root: 'bg-success-subtle', label: 'text-success-foreground' },
25
28
  warning: { root: 'bg-warning-subtle', label: 'text-warning-foreground' },
26
29
  info: { root: 'bg-info-subtle', label: 'text-info-foreground' },
@@ -1083,6 +1083,15 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1083
1083
  const width = plot.width;
1084
1084
  const height = plot.height;
1085
1085
 
1086
+ /*
1087
+ * The readout's own height, measured rather than assumed. Sideways it has to
1088
+ * be clamped inside the plot vertically, and how tall it is depends on how
1089
+ * many series are listed in it — a constant here would either let a
1090
+ * three-series readout hang off the bottom or reserve room a one-series
1091
+ * readout never uses.
1092
+ */
1093
+ const labelHeight = useSharedValue(0);
1094
+
1086
1095
  /*
1087
1096
  * Declared inside the memo, next to its callers: a worklet may only call
1088
1097
  * another worklet, and the rule is enforced by crashing rather than warning.
@@ -1118,8 +1127,17 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1118
1127
  });
1119
1128
  }, [total, left, top, width, height, horizontal, activeIndex, setActiveIndexJS]);
1120
1129
 
1121
- // The readout centres over its band and is clamped inside the plot, so it
1122
- // never runs off the edge at the first or last one.
1130
+ /*
1131
+ * The readout centres over its band and is clamped inside the plot, so it
1132
+ * never runs off the edge at the first or last one.
1133
+ *
1134
+ * Which axis the band runs along is the whole difference between the two
1135
+ * orientations. Upright, the bands are side by side, so the readout slides
1136
+ * across and sits above them all. Sideways, the bands are stacked down the
1137
+ * plot, so it slides *down* to the row it is describing and stays centred
1138
+ * across — held at the top instead, it would name the row under the finger
1139
+ * while covering the first one, which is the row a reader checks it against.
1140
+ */
1123
1141
  const labelStyle = useAnimatedStyle(() => {
1124
1142
  const index = activeIndex.value;
1125
1143
  if (index < 0 || !total) return { opacity: 0 };
@@ -1130,7 +1148,23 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1130
1148
  plot.left + plot.width - half,
1131
1149
  Math.max(plot.left + half, horizontal ? plot.left + plot.width / 2 : centre)
1132
1150
  );
1133
- return { opacity: 1, transform: [{ translateX: clamped - half }] };
1151
+
1152
+ if (!horizontal) {
1153
+ return { opacity: 1, transform: [{ translateX: clamped - half }] };
1154
+ }
1155
+
1156
+ // Until the first measurement lands the height is zero, which clamps to
1157
+ // the top of the plot — the same place it used to sit, rather than a jump
1158
+ // from somewhere it never was.
1159
+ const tall = labelHeight.value;
1160
+ const y = Math.min(
1161
+ plot.top + Math.max(plot.height - tall, 0),
1162
+ Math.max(plot.top, centre - tall / 2)
1163
+ );
1164
+ return {
1165
+ opacity: 1,
1166
+ transform: [{ translateX: clamped - half }, { translateY: y }],
1167
+ };
1134
1168
  });
1135
1169
 
1136
1170
  const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
@@ -1151,6 +1185,9 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1151
1185
  >
1152
1186
  {active ? (
1153
1187
  <View
1188
+ onLayout={(event) => {
1189
+ labelHeight.value = event.nativeEvent.layout.height;
1190
+ }}
1154
1191
  className={cn(
1155
1192
  'rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg',
1156
1193
  className
@@ -31,7 +31,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
31
31
  import { tv } from 'tailwind-variants';
32
32
  import { getNativeUI } from '../../native';
33
33
  import { XIcon } from '../../icons';
34
- import { Portal } from '../../primitives/portal';
34
+ import { ModalPortal } from '../../primitives/portal';
35
35
  import { Scrim } from '../../primitives/scrim';
36
36
  import { Text, textChildren } from '../../primitives/text';
37
37
  import { useBackHandler } from '../../hooks/use-back-handler';
@@ -236,6 +236,9 @@ export interface BottomSheetContentProps extends ViewProps {
236
236
  * Frost the screen behind the sheet instead of dimming it, so what is behind
237
237
  * stays legible as shape and colour while losing its detail. Needs the
238
238
  * optional `expo-blur`; without it this dims, rather than failing.
239
+ *
240
+ * Someone who has Reduce Transparency switched on gets an opaque
241
+ * backdrop instead, which is the whole point of the setting.
239
242
  */
240
243
  blur?: boolean;
241
244
  /**
@@ -435,7 +438,7 @@ function BottomSheetContent({
435
438
  if (!open) return null;
436
439
 
437
440
  return (
438
- <Portal>
441
+ <ModalPortal>
439
442
  {/* Portal content mounts under PortalHost, outside this provider's
440
443
  subtree — re-provide the context so nested consumers keep working. */}
441
444
  <BottomSheetContext.Provider value={context}>
@@ -507,7 +510,7 @@ function BottomSheetContent({
507
510
  </GestureDetector>
508
511
  </View>
509
512
  </BottomSheetContext.Provider>
510
- </Portal>
513
+ </ModalPortal>
511
514
  );
512
515
  }
513
516
 
@@ -42,8 +42,9 @@ const buttonVariants = tv({
42
42
  },
43
43
  destructive: {
44
44
  root: 'border-destructive bg-destructive shadow-sm',
45
- label: 'text-white',
46
- spinner: 'border-white/32 border-t-white',
45
+ label: 'text-destructive-solid-foreground',
46
+ spinner:
47
+ 'border-destructive-solid-foreground/32 border-t-destructive-solid-foreground',
47
48
  },
48
49
  /** Neutral surface for third-party sign-in, sized for a full-width stack. */
49
50
  social: {
@@ -53,8 +54,8 @@ const buttonVariants = tv({
53
54
  },
54
55
  },
55
56
  size: {
56
- sm: { root: 'h-9 gap-1.5 px-2.5', label: 'text-sm' },
57
- md: { root: 'h-11 px-4', label: 'text-base' },
57
+ sm: { root: 'h-9 min-w-9 gap-1.5 px-2.5', label: 'text-sm' },
58
+ md: { root: 'h-11 min-w-11 px-4', label: 'text-base' },
58
59
  lg: { root: 'h-12 px-6', label: 'text-lg' },
59
60
  icon: { root: 'h-11 w-11 px-0' },
60
61
  },
@@ -134,18 +135,16 @@ const ATTACHED = 'rounded-none border-transparent shadow-none active:bg-accent';
134
135
 
135
136
  const SPINNER_SIZE = { sm: 'sm', md: 'sm', lg: 'md', icon: 'sm' } as const;
136
137
 
138
+ /** Extra room around the styled sizes so even the compact button reaches 48dp. */
139
+ const BUTTON_HIT_SLOP = { sm: 6, md: 2, lg: 0, icon: 2 } as const;
140
+
137
141
  /**
138
142
  * The theme token each variant's content reads against. Icons in the content
139
143
  * slots inherit the resolved value, so they follow the theme automatically.
140
- *
141
- * `destructive` is absent on purpose: its background is a saturated red in
142
- * every theme, so its content is always white rather than a themed token.
143
144
  */
144
- const CONTENT_COLOR_VAR: Record<
145
- Exclude<NonNullable<ButtonVariantProps['variant']>, 'destructive'>,
146
- string
147
- > = {
145
+ const CONTENT_COLOR_VAR: Record<NonNullable<ButtonVariantProps['variant']>, string> = {
148
146
  primary: '--color-primary-foreground',
147
+ destructive: '--color-destructive-solid-foreground',
149
148
  secondary: '--color-secondary-foreground',
150
149
  outline: '--color-foreground',
151
150
  ghost: '--color-foreground',
@@ -328,17 +327,9 @@ export const Button = forwardRef<View, ButtonProps>(
328
327
  // Icons in the content slots inherit this, so they stay legible when the
329
328
  // theme inverts the button's background. Without it every caller has to
330
329
  // hardcode a hex that is wrong in one theme or the other.
331
- const themedColor = useCSSVariable(
332
- CONTENT_COLOR_VAR[
333
- resolvedVariant === 'destructive' ? 'primary' : (resolvedVariant ?? 'primary')
334
- ]
335
- );
330
+ const themedColor = useCSSVariable(CONTENT_COLOR_VAR[resolvedVariant ?? 'primary']);
336
331
  const contentColor =
337
- resolvedVariant === 'destructive'
338
- ? '#ffffff'
339
- : typeof themedColor === 'string'
340
- ? themedColor
341
- : undefined;
332
+ typeof themedColor === 'string' ? themedColor : undefined;
342
333
 
343
334
  if (nativeUI) {
344
335
  const { Host, Button: NativeButton, RNHostView } = nativeUI;
@@ -446,6 +437,9 @@ export const Button = forwardRef<View, ButtonProps>(
446
437
  accessibilityRole="button"
447
438
  accessibilityState={{ disabled: isDisabled, busy: loading }}
448
439
  disabled={isDisabled}
440
+ // Joined segments cannot borrow room from each other without making
441
+ // two actions claim the same pixels. ButtonGroup owns that layout.
442
+ hitSlop={attached ? undefined : BUTTON_HIT_SLOP[resolvedSize ?? 'md']}
449
443
  // Before the spread, so a caller can still ask for the scale back.
450
444
  pressScale={attached ? 1 : undefined}
451
445
  className={root({ className: cn(attached && ATTACHED, className) })}
@@ -0,0 +1,74 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ export function normalizeCarouselIndex(index: number, count: number, loop: boolean): number {
4
+ 'worklet';
5
+ if (count <= 0 || !Number.isFinite(index)) return 0;
6
+ if (loop) return ((index % count) + count) % count;
7
+ return Math.max(0, Math.min(index, count - 1));
8
+ }
9
+
10
+ interface CarouselIndexLifecycleOptions {
11
+ requestedIndex: number;
12
+ count: number;
13
+ countKnown: boolean;
14
+ loop: boolean;
15
+ onCorrection: (index: number) => void;
16
+ onSettledIndex: (index: number) => void;
17
+ }
18
+
19
+ /** Keeps the rendered position inside the current run and reports corrections once. */
20
+ export function useCarouselIndexLifecycle({
21
+ requestedIndex,
22
+ count,
23
+ countKnown,
24
+ loop,
25
+ onCorrection,
26
+ onSettledIndex,
27
+ }: CarouselIndexLifecycleOptions): number {
28
+ const index = countKnown
29
+ ? normalizeCarouselIndex(requestedIndex, count, loop)
30
+ : requestedIndex;
31
+ const reportedCorrection = useRef<string | null>(null);
32
+
33
+ useEffect(() => {
34
+ if (!countKnown) return;
35
+ onSettledIndex(index);
36
+
37
+ if (index === requestedIndex) {
38
+ reportedCorrection.current = null;
39
+ return;
40
+ }
41
+
42
+ const correction = `${requestedIndex}:${count}:${loop}`;
43
+ if (reportedCorrection.current === correction) return;
44
+ reportedCorrection.current = correction;
45
+ onCorrection(index);
46
+ }, [count, countKnown, index, loop, onCorrection, onSettledIndex, requestedIndex]);
47
+
48
+ return index;
49
+ }
50
+
51
+ interface CarouselAutoplayOptions {
52
+ enabled: boolean;
53
+ index: number;
54
+ count: number;
55
+ loop: boolean;
56
+ interval: number;
57
+ onAdvance: (index: number) => void;
58
+ }
59
+
60
+ /** One autoplay request per accepted index; configuration changes re-arm it. */
61
+ export function useCarouselAutoplay({
62
+ enabled,
63
+ index,
64
+ count,
65
+ loop,
66
+ interval,
67
+ onAdvance,
68
+ }: CarouselAutoplayOptions): void {
69
+ useEffect(() => {
70
+ if (!enabled || count <= 1 || (!loop && index >= count - 1)) return;
71
+ const timer = setTimeout(() => onAdvance(index + 1), interval);
72
+ return () => clearTimeout(timer);
73
+ }, [count, enabled, index, interval, loop, onAdvance]);
74
+ }
@@ -50,11 +50,13 @@ import {
50
50
  Children,
51
51
  createContext,
52
52
  forwardRef,
53
+ isValidElement,
53
54
  useCallback,
54
55
  useContext,
55
56
  useEffect,
56
57
  useImperativeHandle,
57
58
  useMemo,
59
+ useRef,
58
60
  useState,
59
61
  type ReactNode,
60
62
  } from 'react';
@@ -74,6 +76,11 @@ import Animated, {
74
76
  import { ChevronLeftIcon, ChevronRightIcon } from '../../icons';
75
77
  import { Text, textChildren } from '../../primitives/text';
76
78
  import { cn } from '../../utils/cn';
79
+ import {
80
+ normalizeCarouselIndex,
81
+ useCarouselAutoplay,
82
+ useCarouselIndexLifecycle,
83
+ } from './carousel-lifecycle';
77
84
 
78
85
  /** Settles the run onto a whole index. Tuned to stop rather than to bounce. */
79
86
  const SPRING = { damping: 22, stiffness: 190, mass: 0.55 } as const;
@@ -161,8 +168,7 @@ export function useCarouselState() {
161
168
  */
162
169
  function wrap(value: number, count: number) {
163
170
  'worklet';
164
- if (count <= 0) return 0;
165
- return ((value % count) + count) % count;
171
+ return normalizeCarouselIndex(value, count, true);
166
172
  }
167
173
 
168
174
  /**
@@ -183,6 +189,17 @@ function distance(index: number, progress: number, count: number, loop: boolean)
183
189
  return raw;
184
190
  }
185
191
 
192
+ /** Drop the empty nodes React omits before assigning slide positions. */
193
+ function renderableChildren(children: ReactNode) {
194
+ // React treats a Fragment as one child here. Keep that keyed boundary rather
195
+ // than recursively flattening it and changing React's reconciliation model.
196
+ return Children.toArray(children);
197
+ }
198
+
199
+ function renderableChildKey(child: ReactNode, index: number) {
200
+ return isValidElement(child) && child.key !== null ? child.key : index;
201
+ }
202
+
186
203
  export interface CarouselProps extends ViewProps {
187
204
  className?: string;
188
205
  /** How the slides are arranged, and how they move. */
@@ -203,11 +220,14 @@ export interface CarouselProps extends ViewProps {
203
220
  * wants; set it for a run that shows more than one at a time.
204
221
  */
205
222
  itemSize?: number;
206
- /** Advance on a timer. Stops for good the first time a finger lands. */
223
+ /** Advance on a timer. Stops at the non-looping end or after the first touch. */
207
224
  autoplay?: boolean;
208
225
  /** Milliseconds each slide is held when `autoplay` is set. */
209
226
  autoplayInterval?: number;
210
- /** Controlled active slide. */
227
+ /**
228
+ * Controlled active slide. Requests move visually only after this value changes;
229
+ * an index invalidated by a child-count change is normalized and reported.
230
+ */
211
231
  index?: number;
212
232
  /** Starting slide when uncontrolled. */
213
233
  defaultIndex?: number;
@@ -245,14 +265,15 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
245
265
  ) {
246
266
  const [size, setSize] = useState({ width: 0, height: 0 });
247
267
  const [internalIndex, setInternalIndex] = useState(defaultIndex);
248
- const [count, setCount] = useState(0);
268
+ const [count, setCountValue] = useState(0);
269
+ const [countKnown, setCountKnown] = useState(false);
249
270
  const [touched, setTouched] = useState(false);
250
271
  const reducedMotion = useReducedMotion();
251
272
 
252
273
  const isControlled = indexProp !== undefined;
253
- const index = isControlled ? indexProp : internalIndex;
274
+ const requestedIndex = isControlled ? indexProp : internalIndex;
254
275
 
255
- const progress = useSharedValue(defaultIndex);
276
+ const progress = useSharedValue(indexProp ?? defaultIndex);
256
277
  const engaged = useSharedValue(0);
257
278
 
258
279
  // A deck is dealt from the top of a pile, so it is dragged sideways whatever
@@ -261,6 +282,11 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
261
282
  const along = axis === 'horizontal' ? size.width : size.height;
262
283
  const itemSize = itemSizeProp ?? along ?? 0;
263
284
 
285
+ const setCount = useCallback((next: number) => {
286
+ setCountValue(next);
287
+ setCountKnown(true);
288
+ }, []);
289
+
264
290
  const setIndex = useCallback(
265
291
  (next: number) => {
266
292
  if (!isControlled) setInternalIndex(next);
@@ -269,28 +295,68 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
269
295
  [isControlled, onIndexChange]
270
296
  );
271
297
 
272
- const scrollTo = useCallback(
273
- (target: number) => {
274
- if (count <= 0) return;
275
- const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
298
+ /*
299
+ * The slide the run is currently travelling to.
300
+ *
301
+ * Moving the run and recording where it went are two steps: `scrollTo` starts
302
+ * the spring, then the new index arrives back through state and the lifecycle
303
+ * effect asks for it again. Without this the second ask restarts the spring a
304
+ * frame into the first — from a standstill, so a flick loses the momentum it
305
+ * was carrying. Remembering the target lets the echo be recognised and
306
+ * ignored, while a genuine request for the same slide still animates, because
307
+ * that one comes through `scrollTo` and sets this first.
308
+ */
309
+ const animatedTarget = useRef<number | null>(null);
276
310
 
311
+ const animateTo = useCallback(
312
+ (target: number) => {
313
+ animatedTarget.current = target;
277
314
  if (reducedMotion) {
278
- progress.value = settled;
315
+ progress.value = target;
279
316
  } else if (loop) {
280
317
  // Spring to the nearest representation of the target rather than to the
281
318
  // target itself, so a wrap from the last slide to the first travels one
282
319
  // step forward instead of winding all the way back through the run.
283
- const shortest = progress.value + distance(settled, progress.value, count, true);
320
+ const shortest = progress.value + distance(target, progress.value, count, true);
284
321
  progress.value = withSpring(shortest, SPRING, (finished) => {
285
322
  if (finished) progress.value = wrap(progress.value, count);
286
323
  });
287
324
  } else {
288
- progress.value = withSpring(settled, SPRING);
325
+ progress.value = withSpring(target, SPRING);
289
326
  }
327
+ },
328
+ [count, loop, progress, reducedMotion]
329
+ );
330
+
331
+ const settleIndex = useCallback(
332
+ (next: number) => {
333
+ if (animatedTarget.current === next) return;
334
+ if (Math.abs(progress.value - next) >= 0.001) animateTo(next);
335
+ },
336
+ [animateTo, progress]
337
+ );
338
+
339
+ const index = useCarouselIndexLifecycle({
340
+ requestedIndex,
341
+ count,
342
+ countKnown,
343
+ loop,
344
+ onCorrection: setIndex,
345
+ onSettledIndex: settleIndex,
346
+ });
290
347
 
348
+ const scrollTo = useCallback(
349
+ (target: number) => {
350
+ if (count <= 0) return;
351
+ const settled = normalizeCarouselIndex(target, count, loop);
352
+
353
+ // A controlled request belongs to its owner. The finger may move the run,
354
+ // but after release it returns to the current prop until the owner accepts
355
+ // the request by changing that prop.
356
+ animateTo(isControlled ? index : settled);
291
357
  setIndex(settled);
292
358
  },
293
- [count, loop, progress, reducedMotion, setIndex]
359
+ [animateTo, count, index, isControlled, loop, setIndex]
294
360
  );
295
361
 
296
362
  const next = useCallback(() => scrollTo(index + 1), [index, scrollTo]);
@@ -298,23 +364,19 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
298
364
 
299
365
  useImperativeHandle(ref, () => ({ next, previous, scrollTo }), [next, previous, scrollTo]);
300
366
 
301
- // A controlled index moving underneath us animates rather than jumps.
302
- useEffect(() => {
303
- if (!isControlled || indexProp === undefined || count <= 0) return;
304
- if (Math.abs(progress.value - indexProp) < 0.001) return;
305
- progress.value = reducedMotion ? indexProp : withSpring(indexProp, SPRING);
306
- }, [isControlled, indexProp, count, progress, reducedMotion]);
307
-
308
367
  /*
309
368
  * Autoplay stops for good the first time a finger lands, rather than pausing.
310
369
  * Someone who has taken hold of the run is reading it, and having it start
311
370
  * moving again a few seconds later is the behaviour everybody hates.
312
371
  */
313
- useEffect(() => {
314
- if (!autoplay || touched || count <= 1) return;
315
- const timer = setInterval(() => scrollTo(index + 1), autoplayInterval);
316
- return () => clearInterval(timer);
317
- }, [autoplay, touched, count, index, autoplayInterval, scrollTo]);
372
+ useCarouselAutoplay({
373
+ enabled: autoplay && !touched,
374
+ index,
375
+ count,
376
+ loop,
377
+ interval: autoplayInterval,
378
+ onAdvance: scrollTo,
379
+ });
318
380
 
319
381
  const settle = useCallback(
320
382
  (target: number) => {
@@ -438,13 +500,13 @@ export interface CarouselContentProps extends ViewProps {
438
500
  */
439
501
  function CarouselContent({ className, children, ...props }: CarouselContentProps) {
440
502
  const { setCount, variant, orientation, align } = useCarousel('Carousel.Content');
441
- const slides = Children.count(children);
503
+ const slides = renderableChildren(children);
442
504
  const horizontal = orientation === 'horizontal';
443
505
  const centred = align === 'center' || variant === 'coverflow' || variant === 'stack';
444
506
 
445
507
  useEffect(() => {
446
- setCount(slides);
447
- }, [setCount, slides]);
508
+ setCount(slides.length);
509
+ }, [setCount, slides.length]);
448
510
 
449
511
  return (
450
512
  <View
@@ -465,8 +527,10 @@ function CarouselContent({ className, children, ...props }: CarouselContentProps
465
527
  props.style,
466
528
  ]}
467
529
  >
468
- {Children.map(children, (child, index) => (
469
- <ItemIndexContext.Provider value={index}>{child}</ItemIndexContext.Provider>
530
+ {slides.map((child, index) => (
531
+ <ItemIndexContext.Provider key={renderableChildKey(child, index)} value={index}>
532
+ {child}
533
+ </ItemIndexContext.Provider>
470
534
  ))}
471
535
  </View>
472
536
  );
@@ -688,16 +752,30 @@ function CarouselDots({
688
752
  key={dot}
689
753
  disabled={!interactive}
690
754
  onPress={() => scrollTo(dot)}
691
- hitSlop={6}
692
755
  accessibilityRole="tab"
693
756
  accessibilityLabel={`Slide ${dot + 1} of ${count}`}
694
757
  accessibilityState={{ selected: active }}
695
- className={cn(
696
- 'rounded-full',
697
- horizontal ? (active ? 'h-1 w-4' : 'h-1 w-1') : active ? 'h-4 w-1' : 'h-1 w-1',
698
- active ? 'bg-foreground' : 'bg-foreground/30'
699
- )}
700
- />
758
+ // 24, not the 48 the buttons get. Dots sit a few points apart, so
759
+ // a 48 box would either overlap its neighbours — making a tap near
760
+ // the join land on the wrong slide — or push a five-slide run out
761
+ // to the width of the screen. 24 clears the minimum with the pitch
762
+ // still wider than the target, so no two dots contend for a touch.
763
+ className={cn('items-center justify-center', interactive && 'h-6 w-6')}
764
+ >
765
+ <View
766
+ className={cn(
767
+ 'rounded-full',
768
+ horizontal
769
+ ? active
770
+ ? 'h-1 w-4'
771
+ : 'h-1 w-1'
772
+ : active
773
+ ? 'h-4 w-1'
774
+ : 'h-1 w-1',
775
+ active ? 'bg-foreground' : 'bg-foreground/30'
776
+ )}
777
+ />
778
+ </Pressable>
701
779
  );
702
780
  })}
703
781
  </View>
@@ -723,12 +801,11 @@ function makeArrow(direction: 'previous' | 'next') {
723
801
  <Pressable
724
802
  disabled={disabled}
725
803
  onPress={direction === 'next' ? next : previous}
726
- hitSlop={8}
727
804
  accessibilityRole="button"
728
805
  accessibilityLabel={direction === 'next' ? 'Next slide' : 'Previous slide'}
729
806
  accessibilityState={{ disabled }}
730
807
  className={cn(
731
- 'items-center justify-center rounded-full p-1',
808
+ 'h-12 w-12 items-center justify-center rounded-full',
732
809
  disabled ? 'opacity-30' : 'active:bg-foreground/10',
733
810
  className
734
811
  )}
@@ -13,7 +13,7 @@ import { Text } from '../../primitives/text';
13
13
 
14
14
  const checkboxVariants = tv({
15
15
  slots: {
16
- row: 'flex-row items-center gap-2.5',
16
+ row: 'min-h-12 min-w-12 flex-row items-center gap-2.5',
17
17
  box: 'h-5 w-5 items-center justify-center rounded-md border border-input bg-background',
18
18
  fill: 'absolute inset-0 items-center justify-center rounded-md bg-primary',
19
19
  content: 'flex-1 gap-0.5',
@@ -130,7 +130,6 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
130
130
  disabled={disabled}
131
131
  // An indeterminate box resolves upward: the press turns the group on.
132
132
  onPress={() => onCheckedChange?.(indeterminate ? true : !checked)}
133
- hitSlop={8}
134
133
  // A description makes the row two lines tall — align the box to the
135
134
  // label rather than centring it against the whole block.
136
135
  className={slots.row({
@@ -0,0 +1,44 @@
1
+ export type ContextMenuInvocation = 'menu' | 'press';
2
+
3
+ export interface ContextMenuKey {
4
+ key: string;
5
+ shiftKey?: boolean;
6
+ altKey?: boolean;
7
+ ctrlKey?: boolean;
8
+ metaKey?: boolean;
9
+ repeat?: boolean;
10
+ }
11
+
12
+ /** The cross-platform subset ContextMenu consumes from a key-down event. */
13
+ export interface ContextMenuKeyDownEvent {
14
+ nativeEvent: ContextMenuKey;
15
+ preventDefault: () => void;
16
+ isDefaultPrevented: () => boolean;
17
+ }
18
+
19
+ /** Resolve a screen-reader/switch action to the same paths touch users have. */
20
+ export function contextMenuAccessibilityInvocation(
21
+ actionName: string,
22
+ hasPrimaryAction: boolean,
23
+ disabled: boolean
24
+ ): ContextMenuInvocation | undefined {
25
+ if (disabled) return undefined;
26
+ if (actionName === 'showMenu') return 'menu';
27
+ if (actionName === 'activate') return hasPrimaryAction ? 'press' : 'menu';
28
+ return undefined;
29
+ }
30
+
31
+ /** Resolve web/hardware keys without stealing modified shortcuts or repeats. */
32
+ export function contextMenuKeyInvocation(
33
+ event: ContextMenuKey,
34
+ hasPrimaryAction: boolean,
35
+ disabled: boolean
36
+ ): ContextMenuInvocation | undefined {
37
+ if (disabled || event.repeat) return undefined;
38
+ if (event.key === 'ContextMenu' || (event.key === 'F10' && event.shiftKey)) return 'menu';
39
+ if (event.altKey || event.ctrlKey || event.metaKey) return undefined;
40
+ if (event.key === 'Enter' || event.key === ' ') {
41
+ return hasPrimaryAction ? 'press' : 'menu';
42
+ }
43
+ return undefined;
44
+ }