panelui-native 0.62.0 → 0.65.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 (198) hide show
  1. package/README.md +5 -0
  2. package/lib/module/components/area-chart/index.js +14 -4
  3. package/lib/module/components/area-chart/index.js.map +1 -1
  4. package/lib/module/components/badge/index.js +1 -1
  5. package/lib/module/components/badge/index.js.map +1 -1
  6. package/lib/module/components/bar-chart/index.js +53 -6
  7. package/lib/module/components/bar-chart/index.js.map +1 -1
  8. package/lib/module/components/bottom-sheet/index.js +2 -2
  9. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  10. package/lib/module/components/button/index.js +19 -9
  11. package/lib/module/components/button/index.js.map +1 -1
  12. package/lib/module/components/candlestick-chart/index.js +14 -4
  13. package/lib/module/components/candlestick-chart/index.js.map +1 -1
  14. package/lib/module/components/card/index.js +26 -1
  15. package/lib/module/components/card/index.js.map +1 -1
  16. package/lib/module/components/carousel/index.js +28 -14
  17. package/lib/module/components/carousel/index.js.map +1 -1
  18. package/lib/module/components/checkbox/index.js +2 -3
  19. package/lib/module/components/checkbox/index.js.map +1 -1
  20. package/lib/module/components/dialog/index.js +51 -5
  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 +84 -0
  29. package/lib/module/components/flow/flow-accessibility.js.map +1 -0
  30. package/lib/module/components/flow/index.js +100 -39
  31. package/lib/module/components/flow/index.js.map +1 -1
  32. package/lib/module/components/form/use-form.js +93 -66
  33. package/lib/module/components/form/use-form.js.map +1 -1
  34. package/lib/module/components/heatmap-chart/index.js +2 -2
  35. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  36. package/lib/module/components/hex-chart/index.js +4 -4
  37. package/lib/module/components/hex-chart/index.js.map +1 -1
  38. package/lib/module/components/kpi/index.js +3 -1
  39. package/lib/module/components/kpi/index.js.map +1 -1
  40. package/lib/module/components/line-chart/index.js +17 -7
  41. package/lib/module/components/line-chart/index.js.map +1 -1
  42. package/lib/module/components/map/index.js +71 -8
  43. package/lib/module/components/map/index.js.map +1 -1
  44. package/lib/module/components/map/map-accessibility.js +20 -0
  45. package/lib/module/components/map/map-accessibility.js.map +1 -0
  46. package/lib/module/components/map/maplibre.js.map +1 -1
  47. package/lib/module/components/menu/index.js +4 -1
  48. package/lib/module/components/menu/index.js.map +1 -1
  49. package/lib/module/components/number-input/index.js +1 -23
  50. package/lib/module/components/number-input/index.js.map +1 -1
  51. package/lib/module/components/number-input/number-input-math.js +44 -0
  52. package/lib/module/components/number-input/number-input-math.js.map +1 -0
  53. package/lib/module/components/pie-chart/index.js +2 -2
  54. package/lib/module/components/pie-chart/index.js.map +1 -1
  55. package/lib/module/components/plot/index.js +1329 -0
  56. package/lib/module/components/plot/index.js.map +1 -0
  57. package/lib/module/components/polar-area-chart/index.js +2 -2
  58. package/lib/module/components/polar-area-chart/index.js.map +1 -1
  59. package/lib/module/components/radar-chart/index.js +14 -4
  60. package/lib/module/components/radar-chart/index.js.map +1 -1
  61. package/lib/module/components/radio-group/index.js +1 -2
  62. package/lib/module/components/radio-group/index.js.map +1 -1
  63. package/lib/module/components/ring-chart/index.js +2 -2
  64. package/lib/module/components/ring-chart/index.js.map +1 -1
  65. package/lib/module/components/section-rail/index.js +120 -15
  66. package/lib/module/components/section-rail/index.js.map +1 -1
  67. package/lib/module/components/selection-mode/index.js +129 -18
  68. package/lib/module/components/selection-mode/index.js.map +1 -1
  69. package/lib/module/components/sortable/index.js +20 -60
  70. package/lib/module/components/sortable/index.js.map +1 -1
  71. package/lib/module/components/sortable/reorder.js +63 -0
  72. package/lib/module/components/sortable/reorder.js.map +1 -0
  73. package/lib/module/components/swipe/index.js +4 -4
  74. package/lib/module/components/swipe/index.js.map +1 -1
  75. package/lib/module/components/switch/index.js +36 -3
  76. package/lib/module/components/switch/index.js.map +1 -1
  77. package/lib/module/components/time-picker/accessibility.js +21 -0
  78. package/lib/module/components/time-picker/accessibility.js.map +1 -0
  79. package/lib/module/components/time-picker/index.js +34 -8
  80. package/lib/module/components/time-picker/index.js.map +1 -1
  81. package/lib/module/components/waterfall-chart/index.js +1325 -0
  82. package/lib/module/components/waterfall-chart/index.js.map +1 -0
  83. package/lib/module/index.js +8 -0
  84. package/lib/module/index.js.map +1 -1
  85. package/lib/module/primitives/modal-isolation-store.js +29 -0
  86. package/lib/module/primitives/modal-isolation-store.js.map +1 -0
  87. package/lib/module/primitives/portal.js +36 -4
  88. package/lib/module/primitives/portal.js.map +1 -1
  89. package/lib/module/providers/panel-ui-provider.js +27 -6
  90. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  91. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  92. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  93. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
  96. package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/card/index.d.ts +48 -1
  98. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  99. package/lib/typescript/src/components/carousel/index.d.ts +1 -1
  100. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
  101. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  102. package/lib/typescript/src/components/dialog/index.d.ts +47 -2
  103. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  104. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  105. package/lib/typescript/src/components/field/index.d.ts +8 -1
  106. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  107. package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
  108. package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
  109. package/lib/typescript/src/components/flow/index.d.ts +14 -2
  110. package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
  112. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
  115. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/map/index.d.ts +13 -5
  117. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  118. package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
  119. package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
  120. package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
  121. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
  122. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
  124. package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
  125. package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
  126. package/lib/typescript/src/components/plot/index.d.ts +471 -0
  127. package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
  128. package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
  129. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
  131. package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
  132. package/lib/typescript/src/components/selection-mode/index.d.ts +82 -6
  133. package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
  136. package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
  137. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/switch/index.d.ts +3 -3
  139. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
  141. package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
  142. package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
  144. package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
  145. package/lib/typescript/src/index.d.ts +4 -1
  146. package/lib/typescript/src/index.d.ts.map +1 -1
  147. package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
  148. package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
  149. package/lib/typescript/src/primitives/portal.d.ts +6 -0
  150. package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
  151. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  152. package/package.json +5 -2
  153. package/src/components/area-chart/index.tsx +14 -4
  154. package/src/components/badge/index.tsx +4 -1
  155. package/src/components/bar-chart/index.tsx +54 -7
  156. package/src/components/bottom-sheet/index.tsx +3 -3
  157. package/src/components/button/index.tsx +15 -21
  158. package/src/components/candlestick-chart/index.tsx +20 -7
  159. package/src/components/card/index.tsx +32 -7
  160. package/src/components/carousel/index.tsx +42 -20
  161. package/src/components/checkbox/index.tsx +1 -2
  162. package/src/components/dialog/index.tsx +58 -5
  163. package/src/components/drawer/index.tsx +3 -3
  164. package/src/components/fab/index.tsx +8 -12
  165. package/src/components/field/index.tsx +52 -10
  166. package/src/components/flow/flow-accessibility.ts +109 -0
  167. package/src/components/flow/index.tsx +162 -50
  168. package/src/components/form/use-form.ts +101 -65
  169. package/src/components/heatmap-chart/index.tsx +2 -3
  170. package/src/components/hex-chart/index.tsx +4 -5
  171. package/src/components/kpi/index.tsx +3 -1
  172. package/src/components/line-chart/index.tsx +17 -6
  173. package/src/components/map/index.tsx +110 -7
  174. package/src/components/map/map-accessibility.ts +41 -0
  175. package/src/components/map/maplibre.ts +2 -0
  176. package/src/components/menu/index.tsx +4 -1
  177. package/src/components/number-input/index.tsx +1 -26
  178. package/src/components/number-input/number-input-math.ts +50 -0
  179. package/src/components/pie-chart/index.tsx +2 -2
  180. package/src/components/plot/index.tsx +1565 -0
  181. package/src/components/polar-area-chart/index.tsx +2 -2
  182. package/src/components/radar-chart/index.tsx +14 -4
  183. package/src/components/radio-group/index.tsx +1 -2
  184. package/src/components/ring-chart/index.tsx +2 -2
  185. package/src/components/section-rail/index.tsx +133 -14
  186. package/src/components/selection-mode/index.tsx +163 -26
  187. package/src/components/sortable/index.tsx +27 -69
  188. package/src/components/sortable/reorder.ts +88 -0
  189. package/src/components/swipe/index.tsx +7 -4
  190. package/src/components/switch/index.tsx +42 -6
  191. package/src/components/time-picker/accessibility.ts +24 -0
  192. package/src/components/time-picker/index.tsx +48 -2
  193. package/src/components/waterfall-chart/index.tsx +1589 -0
  194. package/src/index.ts +69 -0
  195. package/src/primitives/modal-isolation-store.ts +31 -0
  196. package/src/primitives/portal.tsx +34 -4
  197. package/src/providers/panel-ui-provider.tsx +41 -9
  198. package/theme.css +54 -3
@@ -245,7 +245,7 @@ const BarChartRoot = forwardRef<BarChartHandle, BarChartProps>(function BarChart
245
245
  xDataKey = 'name',
246
246
  status = 'ready',
247
247
  aspectRatio = 2,
248
- animationDuration = 1100,
248
+ animationDuration = 700,
249
249
  domainDuration = 500,
250
250
  yDomain,
251
251
  orientation = 'vertical',
@@ -374,17 +374,27 @@ const BarChartRoot = forwardRef<BarChartHandle, BarChartProps>(function BarChart
374
374
  reveal.value = 0;
375
375
  reveal.value = withTiming(1, {
376
376
  duration: animationDuration,
377
- easing: Easing.bezier(0.85, 0, 0.15, 1),
377
+ easing: Easing.out(Easing.cubic),
378
378
  });
379
379
  },
380
380
  [reducedMotion, animationDuration, reveal]
381
381
  );
382
382
 
383
383
  useEffect(() => {
384
- if (revealed.current || loading || plot.width <= 0 || !data.length) return;
384
+ /*
385
+ * Going back to `loading` arms the reveal again. Without this a chart that
386
+ * is refetched comes back fully drawn on the frame the data lands, which
387
+ * reads as the loading state having been for nothing.
388
+ */
389
+ if (loading) {
390
+ revealed.current = false;
391
+ reveal.value = 0;
392
+ return;
393
+ }
394
+ if (revealed.current || plot.width <= 0 || !data.length) return;
385
395
  revealed.current = true;
386
396
  playReveal();
387
- }, [loading, plot.width, data.length, playReveal]);
397
+ }, [loading, plot.width, data.length, playReveal, reveal]);
388
398
 
389
399
  useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
390
400
 
@@ -1073,6 +1083,15 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1073
1083
  const width = plot.width;
1074
1084
  const height = plot.height;
1075
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
+
1076
1095
  /*
1077
1096
  * Declared inside the memo, next to its callers: a worklet may only call
1078
1097
  * another worklet, and the rule is enforced by crashing rather than warning.
@@ -1108,8 +1127,17 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1108
1127
  });
1109
1128
  }, [total, left, top, width, height, horizontal, activeIndex, setActiveIndexJS]);
1110
1129
 
1111
- // The readout centres over its band and is clamped inside the plot, so it
1112
- // 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
+ */
1113
1141
  const labelStyle = useAnimatedStyle(() => {
1114
1142
  const index = activeIndex.value;
1115
1143
  if (index < 0 || !total) return { opacity: 0 };
@@ -1120,7 +1148,23 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1120
1148
  plot.left + plot.width - half,
1121
1149
  Math.max(plot.left + half, horizontal ? plot.left + plot.width / 2 : centre)
1122
1150
  );
1123
- 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
+ };
1124
1168
  });
1125
1169
 
1126
1170
  const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
@@ -1141,6 +1185,9 @@ function BarChartTooltip({ formatValue, formatX, className }: BarChartTooltipPro
1141
1185
  >
1142
1186
  {active ? (
1143
1187
  <View
1188
+ onLayout={(event) => {
1189
+ labelHeight.value = event.nativeEvent.layout.height;
1190
+ }}
1144
1191
  className={cn(
1145
1192
  'rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg',
1146
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';
@@ -435,7 +435,7 @@ function BottomSheetContent({
435
435
  if (!open) return null;
436
436
 
437
437
  return (
438
- <Portal>
438
+ <ModalPortal>
439
439
  {/* Portal content mounts under PortalHost, outside this provider's
440
440
  subtree — re-provide the context so nested consumers keep working. */}
441
441
  <BottomSheetContext.Provider value={context}>
@@ -507,7 +507,7 @@ function BottomSheetContent({
507
507
  </GestureDetector>
508
508
  </View>
509
509
  </BottomSheetContext.Provider>
510
- </Portal>
510
+ </ModalPortal>
511
511
  );
512
512
  }
513
513
 
@@ -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) })}
@@ -249,9 +249,12 @@ export interface CandlestickChartProps extends ViewProps {
249
249
  /** Key holding the closing price. */
250
250
  closeDataKey?: string;
251
251
  /**
252
- * `loading` draws a row of flat placeholder candles and grows them into the
253
- * real ones when it turns `ready`. One component throughout, rather than a
254
- * spinner swapped for a chart — swapping loses the transition.
252
+ * `loading` leaves the plot empty — the frame, the grid and the header stay,
253
+ * and no candles are drawn. Turning `ready` grows them in left to right.
254
+ *
255
+ * Nothing stands in for the candles while they are missing. A placeholder
256
+ * candle is four made-up prices, and a reader has no way to tell an invented
257
+ * one from a real one until it changes under them.
255
258
  */
256
259
  status?: CandlestickChartStatus;
257
260
  /** Width ÷ height. `1.6` suits a chart this dense better than `2`. */
@@ -323,7 +326,7 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
323
326
  closeDataKey = 'close',
324
327
  status = 'ready',
325
328
  aspectRatio = 1.6,
326
- animationDuration = 1100,
329
+ animationDuration = 700,
327
330
  domainDuration = 500,
328
331
  yDomain,
329
332
  candleGap = 0.3,
@@ -427,17 +430,27 @@ const CandlestickChartRoot = forwardRef<CandlestickChartHandle, CandlestickChart
427
430
  reveal.value = 0;
428
431
  reveal.value = withTiming(1, {
429
432
  duration: animationDuration,
430
- easing: Easing.bezier(0.85, 0, 0.15, 1),
433
+ easing: Easing.out(Easing.cubic),
431
434
  });
432
435
  },
433
436
  [reducedMotion, animationDuration, reveal]
434
437
  );
435
438
 
436
439
  useEffect(() => {
437
- if (revealed.current || loading || plot.width <= 0 || !data.length) return;
440
+ /*
441
+ * Going back to `loading` arms the reveal again. Without this a chart
442
+ * that is refetched comes back fully drawn on the frame the data lands,
443
+ * which reads as the loading state having been for nothing.
444
+ */
445
+ if (loading) {
446
+ revealed.current = false;
447
+ reveal.value = 0;
448
+ return;
449
+ }
450
+ if (revealed.current || plot.width <= 0 || !data.length) return;
438
451
  revealed.current = true;
439
452
  playReveal();
440
- }, [loading, plot.width, data.length, playReveal]);
453
+ }, [loading, plot.width, data.length, playReveal, reveal]);
441
454
 
442
455
  useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
443
456
 
@@ -23,13 +23,40 @@
23
23
  */
24
24
  import { forwardRef } from 'react';
25
25
  import { View, type ViewProps } from 'react-native';
26
+ import { tv, type VariantProps } from 'tailwind-variants';
26
27
  import { cn } from '../../utils/cn';
27
28
  import { Text, type TextProps } from '../../primitives/text';
28
29
 
30
+ const footerVariants = tv({
31
+ base: 'flex-row items-center gap-2',
32
+ variants: {
33
+ /**
34
+ * Whether the footer draws a surface of its own.
35
+ *
36
+ * `plain` is part of the card: same background, padding continuing from the
37
+ * content above it. `panel` is a band set into it — a rule across the top,
38
+ * a step darker, and the card's own bottom corners — so the buttons read as
39
+ * the card's actions rather than as the last thing in its body. Use it when
40
+ * the footer is what somebody does with the card, not more of what it says.
41
+ */
42
+ variant: {
43
+ plain: 'p-6 pt-0',
44
+ panel: 'rounded-b-2xl border-t border-border bg-inset p-6',
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ variant: 'plain',
49
+ },
50
+ });
51
+
29
52
  export interface CardProps extends ViewProps {
30
53
  className?: string;
31
54
  }
32
55
 
56
+ export interface CardFooterProps
57
+ extends CardProps,
58
+ VariantProps<typeof footerVariants> {}
59
+
33
60
  const CardRoot = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
34
61
  <View
35
62
  ref={ref}
@@ -69,13 +96,11 @@ const CardContent = forwardRef<View, CardProps>(({ className, ...props }, ref) =
69
96
  ));
70
97
  CardContent.displayName = 'Card.Content';
71
98
 
72
- const CardFooter = forwardRef<View, CardProps>(({ className, ...props }, ref) => (
73
- <View
74
- ref={ref}
75
- className={cn('flex-row items-center gap-2 p-6 pt-0', className)}
76
- {...props}
77
- />
78
- ));
99
+ const CardFooter = forwardRef<View, CardFooterProps>(
100
+ ({ className, variant, ...props }, ref) => (
101
+ <View ref={ref} className={cn(footerVariants({ variant }), className)} {...props} />
102
+ )
103
+ );
79
104
  CardFooter.displayName = 'Card.Footer';
80
105
 
81
106
  export const Card = Object.assign(CardRoot, {
@@ -207,7 +207,7 @@ export interface CarouselProps extends ViewProps {
207
207
  autoplay?: boolean;
208
208
  /** Milliseconds each slide is held when `autoplay` is set. */
209
209
  autoplayInterval?: number;
210
- /** Controlled active slide. */
210
+ /** Controlled active slide. Requests move visually only after this value changes. */
211
211
  index?: number;
212
212
  /** Starting slide when uncontrolled. */
213
213
  defaultIndex?: number;
@@ -269,28 +269,37 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
269
269
  [isControlled, onIndexChange]
270
270
  );
271
271
 
272
- const scrollTo = useCallback(
272
+ const animateTo = useCallback(
273
273
  (target: number) => {
274
- if (count <= 0) return;
275
- const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
276
-
277
274
  if (reducedMotion) {
278
- progress.value = settled;
275
+ progress.value = target;
279
276
  } else if (loop) {
280
277
  // Spring to the nearest representation of the target rather than to the
281
278
  // target itself, so a wrap from the last slide to the first travels one
282
279
  // step forward instead of winding all the way back through the run.
283
- const shortest = progress.value + distance(settled, progress.value, count, true);
280
+ const shortest = progress.value + distance(target, progress.value, count, true);
284
281
  progress.value = withSpring(shortest, SPRING, (finished) => {
285
282
  if (finished) progress.value = wrap(progress.value, count);
286
283
  });
287
284
  } else {
288
- progress.value = withSpring(settled, SPRING);
285
+ progress.value = withSpring(target, SPRING);
289
286
  }
287
+ },
288
+ [count, loop, progress, reducedMotion]
289
+ );
290
+
291
+ const scrollTo = useCallback(
292
+ (target: number) => {
293
+ if (count <= 0) return;
294
+ const settled = loop ? wrap(target, count) : Math.max(0, Math.min(target, count - 1));
290
295
 
296
+ // A controlled request belongs to its owner. The finger may move the run,
297
+ // but after release it returns to the current prop until the owner accepts
298
+ // the request by changing that prop.
299
+ animateTo(isControlled ? index : settled);
291
300
  setIndex(settled);
292
301
  },
293
- [count, loop, progress, reducedMotion, setIndex]
302
+ [animateTo, count, index, isControlled, loop, setIndex]
294
303
  );
295
304
 
296
305
  const next = useCallback(() => scrollTo(index + 1), [index, scrollTo]);
@@ -302,8 +311,8 @@ const CarouselRoot = forwardRef<CarouselHandle, CarouselProps>(function Carousel
302
311
  useEffect(() => {
303
312
  if (!isControlled || indexProp === undefined || count <= 0) return;
304
313
  if (Math.abs(progress.value - indexProp) < 0.001) return;
305
- progress.value = reducedMotion ? indexProp : withSpring(indexProp, SPRING);
306
- }, [isControlled, indexProp, count, progress, reducedMotion]);
314
+ animateTo(indexProp);
315
+ }, [animateTo, isControlled, indexProp, count, progress]);
307
316
 
308
317
  /*
309
318
  * Autoplay stops for good the first time a finger lands, rather than pausing.
@@ -688,16 +697,30 @@ function CarouselDots({
688
697
  key={dot}
689
698
  disabled={!interactive}
690
699
  onPress={() => scrollTo(dot)}
691
- hitSlop={6}
692
700
  accessibilityRole="tab"
693
701
  accessibilityLabel={`Slide ${dot + 1} of ${count}`}
694
702
  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
- />
703
+ // 24, not the 48 the buttons get. Dots sit a few points apart, so
704
+ // a 48 box would either overlap its neighbours — making a tap near
705
+ // the join land on the wrong slide — or push a five-slide run out
706
+ // to the width of the screen. 24 clears the minimum with the pitch
707
+ // still wider than the target, so no two dots contend for a touch.
708
+ className={cn('items-center justify-center', interactive && 'h-6 w-6')}
709
+ >
710
+ <View
711
+ className={cn(
712
+ 'rounded-full',
713
+ horizontal
714
+ ? active
715
+ ? 'h-1 w-4'
716
+ : 'h-1 w-1'
717
+ : active
718
+ ? 'h-4 w-1'
719
+ : 'h-1 w-1',
720
+ active ? 'bg-foreground' : 'bg-foreground/30'
721
+ )}
722
+ />
723
+ </Pressable>
701
724
  );
702
725
  })}
703
726
  </View>
@@ -723,12 +746,11 @@ function makeArrow(direction: 'previous' | 'next') {
723
746
  <Pressable
724
747
  disabled={disabled}
725
748
  onPress={direction === 'next' ? next : previous}
726
- hitSlop={8}
727
749
  accessibilityRole="button"
728
750
  accessibilityLabel={direction === 'next' ? 'Next slide' : 'Previous slide'}
729
751
  accessibilityState={{ disabled }}
730
752
  className={cn(
731
- 'items-center justify-center rounded-full p-1',
753
+ 'h-12 w-12 items-center justify-center rounded-full',
732
754
  disabled ? 'opacity-30' : 'active:bg-foreground/10',
733
755
  className
734
756
  )}
@@ -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({
@@ -12,7 +12,8 @@ import {
12
12
  } from 'react';
13
13
  import { Pressable, View, type ViewProps } from 'react-native';
14
14
  import Animated, { FadeIn, FadeOut, ZoomIn } from 'react-native-reanimated';
15
- import { Portal } from '../../primitives/portal';
15
+ import { tv, type VariantProps } from 'tailwind-variants';
16
+ import { ModalPortal } from '../../primitives/portal';
16
17
  import { Scrim } from '../../primitives/scrim';
17
18
  import { Text, type TextProps, textChildren } from '../../primitives/text';
18
19
  import { useBackHandler } from '../../hooks/use-back-handler';
@@ -110,7 +111,7 @@ function DialogContent({
110
111
  if (!open) return null;
111
112
 
112
113
  return (
113
- <Portal>
114
+ <ModalPortal>
114
115
  {/* Portal content mounts under PortalHost, outside this provider's
115
116
  subtree — re-provide the context so Dialog.Close etc. keep working. */}
116
117
  <DialogContext.Provider value={context}>
@@ -129,6 +130,8 @@ function DialogContent({
129
130
  entering={ZoomIn.springify().damping(18).stiffness(250).mass(0.6)}
130
131
  exiting={FadeOut.duration(120)}
131
132
  accessibilityViewIsModal
133
+ // `p-5` is `DIALOG_PADDING` above — a panel footer bleeds back out
134
+ // through exactly this much, so the two move together.
132
135
  className={cn(
133
136
  'w-full max-w-sm gap-1.5 rounded-2xl border border-border bg-popover p-5 shadow-lg',
134
137
  className
@@ -139,7 +142,7 @@ function DialogContent({
139
142
  </Animated.View>
140
143
  </Animated.View>
141
144
  </DialogContext.Provider>
142
- </Portal>
145
+ </ModalPortal>
143
146
  );
144
147
  }
145
148
 
@@ -163,10 +166,60 @@ const DialogDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
163
166
  );
164
167
  DialogDescription.displayName = 'Dialog.Description';
165
168
 
166
- function DialogFooter({ className, ...props }: ViewProps & { className?: string }) {
169
+ /**
170
+ * The dialog's padding, in points. The panel footer bleeds back out through it,
171
+ * so the two have to agree — a mismatch is a band inset from one edge.
172
+ */
173
+ const DIALOG_PADDING = 20;
174
+
175
+ const dialogFooterVariants = tv({
176
+ base: 'flex-row items-center justify-end gap-2',
177
+ variants: {
178
+ /**
179
+ * Whether the footer draws a surface of its own.
180
+ *
181
+ * `plain` is part of the dialog: same background, sitting under the content
182
+ * with a gap above it. `panel` is a band set into it — a rule across the
183
+ * top, a step darker, and the dialog's own bottom corners — which separates
184
+ * what the dialog says from what you can do about it. Worth it on a dialog
185
+ * with a form in it, where the buttons are otherwise one more row of it.
186
+ */
187
+ variant: {
188
+ plain: 'mt-4',
189
+ panel: 'mt-5 rounded-b-2xl border-t border-border bg-inset',
190
+ },
191
+ },
192
+ defaultVariants: {
193
+ variant: 'plain',
194
+ },
195
+ });
196
+
197
+ export interface DialogFooterProps extends ViewProps, VariantProps<typeof dialogFooterVariants> {
198
+ className?: string;
199
+ }
200
+
201
+ function DialogFooter({ className, variant, style, ...props }: DialogFooterProps) {
167
202
  return (
168
203
  <View
169
- className={cn('mt-4 flex-row items-center justify-end gap-2', className)}
204
+ /*
205
+ * The bleed is a style rather than a class because it is arithmetic on the
206
+ * dialog's own padding, and a class would be a second copy of that number
207
+ * waiting to disagree with the first.
208
+ */
209
+ style={
210
+ variant === 'panel'
211
+ ? [
212
+ {
213
+ marginHorizontal: -DIALOG_PADDING,
214
+ marginBottom: -DIALOG_PADDING,
215
+ paddingHorizontal: DIALOG_PADDING,
216
+ paddingVertical: 16,
217
+ },
218
+ style,
219
+ ]
220
+ : style
221
+ }
222
+ className={cn(dialogFooterVariants({ variant }), className)}
170
223
  {...props}
171
224
  />
172
225
  );
@@ -84,7 +84,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
84
84
  import { tv } from 'tailwind-variants';
85
85
  import { useCSSVariable } from 'uniwind';
86
86
  import { XIcon } from '../../icons';
87
- import { Portal } from '../../primitives/portal';
87
+ import { ModalPortal } from '../../primitives/portal';
88
88
  import { Scrim } from '../../primitives/scrim';
89
89
  import { Text, textChildren } from '../../primitives/text';
90
90
  import { useBackHandler } from '../../hooks/use-back-handler';
@@ -458,7 +458,7 @@ function DrawerContent({
458
458
  };
459
459
 
460
460
  return (
461
- <Portal>
461
+ <ModalPortal>
462
462
  {/* Portal content mounts under PortalHost, outside this provider's
463
463
  subtree — re-provide the context so Drawer.Close keeps working. */}
464
464
  <DrawerContext.Provider value={{ open, setOpen }}>
@@ -561,7 +561,7 @@ function DrawerContent({
561
561
  </View>
562
562
  </DirectionContext.Provider>
563
563
  </DrawerContext.Provider>
564
- </Portal>
564
+ </ModalPortal>
565
565
  );
566
566
  }
567
567
 
@@ -129,7 +129,10 @@ const fabVariants = tv({
129
129
  primary: { root: 'bg-primary', label: 'text-primary-foreground' },
130
130
  secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
131
131
  surface: { root: 'border border-border bg-popover', label: 'text-foreground' },
132
- destructive: { root: 'bg-destructive', label: 'text-white' },
132
+ destructive: {
133
+ root: 'bg-destructive',
134
+ label: 'text-destructive-solid-foreground',
135
+ },
133
136
  },
134
137
  disabled: {
135
138
  true: { root: 'opacity-[0.64] shadow-none' },
@@ -158,8 +161,9 @@ export type FabSize = NonNullable<FabVariantProps['size']>;
158
161
  export type FabVariant = NonNullable<FabVariantProps['variant']>;
159
162
 
160
163
  /** The theme token each variant's icon reads against. */
161
- const CONTENT_COLOR_VAR: Record<Exclude<FabVariant, 'destructive'>, string> = {
164
+ const CONTENT_COLOR_VAR: Record<FabVariant, string> = {
162
165
  primary: '--color-primary-foreground',
166
+ destructive: '--color-destructive-solid-foreground',
163
167
  secondary: '--color-secondary-foreground',
164
168
  surface: '--color-foreground',
165
169
  };
@@ -234,17 +238,9 @@ const FabRoot = forwardRef<View, FabProps>(
234
238
  disabled,
235
239
  });
236
240
 
237
- const themed = useCSSVariable(
238
- CONTENT_COLOR_VAR[
239
- variant === 'destructive' ? 'primary' : (variant ?? 'primary')
240
- ]
241
- );
241
+ const themed = useCSSVariable(CONTENT_COLOR_VAR[variant ?? 'primary']);
242
242
  const contentColor =
243
- variant === 'destructive'
244
- ? '#ffffff'
245
- : typeof themed === 'string'
246
- ? themed
247
- : undefined;
243
+ typeof themed === 'string' ? themed : undefined;
248
244
 
249
245
  const handlePress = useCallback(() => {
250
246
  if (haptics) selectionTick();