panelui-native 0.25.0 → 0.30.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 (242) hide show
  1. package/README.md +18 -1
  2. package/lib/module/components/accordion/index.js +8 -8
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/alert/index.js +2 -2
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/area-chart/index.js +182 -50
  7. package/lib/module/components/area-chart/index.js.map +1 -1
  8. package/lib/module/components/attachment/index.js +2 -1
  9. package/lib/module/components/attachment/index.js.map +1 -1
  10. package/lib/module/components/avatar/index.js +3 -3
  11. package/lib/module/components/avatar/index.js.map +1 -1
  12. package/lib/module/components/badge/index.js +4 -4
  13. package/lib/module/components/badge/index.js.map +1 -1
  14. package/lib/module/components/bar-chart/index.js +197 -56
  15. package/lib/module/components/bar-chart/index.js.map +1 -1
  16. package/lib/module/components/bottom-sheet/index.js +15 -10
  17. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  18. package/lib/module/components/breadcrumb/index.js +6 -6
  19. package/lib/module/components/breadcrumb/index.js.map +1 -1
  20. package/lib/module/components/button/index.js +11 -6
  21. package/lib/module/components/button/index.js.map +1 -1
  22. package/lib/module/components/calendar/index.js +792 -0
  23. package/lib/module/components/calendar/index.js.map +1 -0
  24. package/lib/module/components/carousel/index.js +680 -0
  25. package/lib/module/components/carousel/index.js.map +1 -0
  26. package/lib/module/components/chip/index.js +4 -4
  27. package/lib/module/components/chip/index.js.map +1 -1
  28. package/lib/module/components/code-block/highlight.js +363 -0
  29. package/lib/module/components/code-block/highlight.js.map +1 -0
  30. package/lib/module/components/code-block/index.js +313 -0
  31. package/lib/module/components/code-block/index.js.map +1 -0
  32. package/lib/module/components/date-picker/index.js +165 -0
  33. package/lib/module/components/date-picker/index.js.map +1 -0
  34. package/lib/module/components/dialog/index.js +7 -2
  35. package/lib/module/components/dialog/index.js.map +1 -1
  36. package/lib/module/components/direction/index.js +2 -1
  37. package/lib/module/components/direction/index.js.map +1 -1
  38. package/lib/module/components/empty-state/index.js +2 -2
  39. package/lib/module/components/empty-state/index.js.map +1 -1
  40. package/lib/module/components/field/index.js +5 -5
  41. package/lib/module/components/field/index.js.map +1 -1
  42. package/lib/module/components/flow/index.js +3 -3
  43. package/lib/module/components/flow/index.js.map +1 -1
  44. package/lib/module/components/frame/index.js +5 -5
  45. package/lib/module/components/frame/index.js.map +1 -1
  46. package/lib/module/components/input-group/index.js +3 -2
  47. package/lib/module/components/input-group/index.js.map +1 -1
  48. package/lib/module/components/item/index.js +9 -9
  49. package/lib/module/components/item/index.js.map +1 -1
  50. package/lib/module/components/label/index.js +4 -4
  51. package/lib/module/components/label/index.js.map +1 -1
  52. package/lib/module/components/line-chart/index.js +167 -44
  53. package/lib/module/components/line-chart/index.js.map +1 -1
  54. package/lib/module/components/map/index.js +7 -7
  55. package/lib/module/components/map/index.js.map +1 -1
  56. package/lib/module/components/marker/index.js +3 -3
  57. package/lib/module/components/marker/index.js.map +1 -1
  58. package/lib/module/components/menu/index.js +714 -0
  59. package/lib/module/components/menu/index.js.map +1 -0
  60. package/lib/module/components/message/index.js +7 -7
  61. package/lib/module/components/message/index.js.map +1 -1
  62. package/lib/module/components/message-scroller/index.js +5 -4
  63. package/lib/module/components/message-scroller/index.js.map +1 -1
  64. package/lib/module/components/plan/index.js +296 -0
  65. package/lib/module/components/plan/index.js.map +1 -0
  66. package/lib/module/components/popover/index.js +73 -10
  67. package/lib/module/components/popover/index.js.map +1 -1
  68. package/lib/module/components/radio-group/index.js +15 -10
  69. package/lib/module/components/radio-group/index.js.map +1 -1
  70. package/lib/module/components/reasoning/index.js +244 -0
  71. package/lib/module/components/reasoning/index.js.map +1 -0
  72. package/lib/module/components/section-rail/index.js +4 -4
  73. package/lib/module/components/section-rail/index.js.map +1 -1
  74. package/lib/module/components/select/index.js +10 -4
  75. package/lib/module/components/select/index.js.map +1 -1
  76. package/lib/module/components/sources/index.js +221 -0
  77. package/lib/module/components/sources/index.js.map +1 -0
  78. package/lib/module/components/steps/index.js +4 -4
  79. package/lib/module/components/steps/index.js.map +1 -1
  80. package/lib/module/components/tabs/index.js +7 -7
  81. package/lib/module/components/tabs/index.js.map +1 -1
  82. package/lib/module/components/task/index.js +300 -0
  83. package/lib/module/components/task/index.js.map +1 -0
  84. package/lib/module/components/textarea/index.js +295 -0
  85. package/lib/module/components/textarea/index.js.map +1 -0
  86. package/lib/module/components/timeline/index.js +4 -4
  87. package/lib/module/components/timeline/index.js.map +1 -1
  88. package/lib/module/components/toast/index.js +2 -2
  89. package/lib/module/components/toast/index.js.map +1 -1
  90. package/lib/module/components/toggle-button/index.js +5 -5
  91. package/lib/module/components/toggle-button/index.js.map +1 -1
  92. package/lib/module/components/tooltip/index.js +648 -0
  93. package/lib/module/components/tooltip/index.js.map +1 -0
  94. package/lib/module/components/typography/index.js +2 -2
  95. package/lib/module/components/typography/index.js.map +1 -1
  96. package/lib/module/hooks/index.js +1 -0
  97. package/lib/module/hooks/index.js.map +1 -1
  98. package/lib/module/hooks/use-back-handler.js +37 -0
  99. package/lib/module/hooks/use-back-handler.js.map +1 -0
  100. package/lib/module/icons/index.js +202 -0
  101. package/lib/module/icons/index.js.map +1 -1
  102. package/lib/module/index.js +13 -1
  103. package/lib/module/index.js.map +1 -1
  104. package/lib/module/primitives/collapse.js +112 -0
  105. package/lib/module/primitives/collapse.js.map +1 -0
  106. package/lib/module/primitives/keyboard-avoider.js +2 -1
  107. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  108. package/lib/module/primitives/text.js +25 -1
  109. package/lib/module/primitives/text.js.map +1 -1
  110. package/lib/module/utils/date.js +386 -0
  111. package/lib/module/utils/date.js.map +1 -0
  112. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/area-chart/index.d.ts +41 -1
  115. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
  117. package/lib/typescript/src/components/bar-chart/index.d.ts +44 -2
  118. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
  119. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  122. package/lib/typescript/src/components/calendar/index.d.ts +173 -0
  123. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
  124. package/lib/typescript/src/components/carousel/index.d.ts +179 -0
  125. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
  126. package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
  127. package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
  128. package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
  129. package/lib/typescript/src/components/code-block/index.d.ts +113 -0
  130. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
  131. package/lib/typescript/src/components/date-picker/index.d.ts +97 -0
  132. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
  133. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  134. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  135. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  137. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  138. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/item/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/line-chart/index.d.ts +41 -2
  142. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  144. package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/menu/index.d.ts +261 -0
  146. package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
  147. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  148. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/plan/index.d.ts +127 -0
  150. package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
  151. package/lib/typescript/src/components/popover/index.d.ts +35 -1
  152. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  153. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  154. package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
  155. package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
  156. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  157. package/lib/typescript/src/components/sources/index.d.ts +85 -0
  158. package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
  159. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  160. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/task/index.d.ts +103 -0
  162. package/lib/typescript/src/components/task/index.d.ts.map +1 -0
  163. package/lib/typescript/src/components/textarea/index.d.ts +175 -0
  164. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
  165. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  166. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  168. package/lib/typescript/src/components/tooltip/index.d.ts +180 -0
  169. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
  170. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  171. package/lib/typescript/src/hooks/index.d.ts +1 -0
  172. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  173. package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
  174. package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
  175. package/lib/typescript/src/icons/index.d.ts +17 -0
  176. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  177. package/lib/typescript/src/index.d.ts +16 -4
  178. package/lib/typescript/src/index.d.ts.map +1 -1
  179. package/lib/typescript/src/primitives/collapse.d.ts +42 -0
  180. package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
  181. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  182. package/lib/typescript/src/primitives/text.d.ts +20 -0
  183. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  184. package/lib/typescript/src/utils/date.d.ts +116 -0
  185. package/lib/typescript/src/utils/date.d.ts.map +1 -0
  186. package/package.json +1 -1
  187. package/src/components/accordion/index.tsx +8 -12
  188. package/src/components/alert/index.tsx +2 -2
  189. package/src/components/area-chart/index.tsx +208 -55
  190. package/src/components/attachment/index.tsx +2 -2
  191. package/src/components/avatar/index.tsx +3 -3
  192. package/src/components/badge/index.tsx +6 -6
  193. package/src/components/bar-chart/index.tsx +214 -54
  194. package/src/components/bottom-sheet/index.tsx +17 -16
  195. package/src/components/breadcrumb/index.tsx +6 -8
  196. package/src/components/button/index.tsx +12 -8
  197. package/src/components/calendar/index.tsx +970 -0
  198. package/src/components/carousel/index.tsx +788 -0
  199. package/src/components/chip/index.tsx +4 -6
  200. package/src/components/code-block/highlight.ts +278 -0
  201. package/src/components/code-block/index.tsx +341 -0
  202. package/src/components/date-picker/index.tsx +255 -0
  203. package/src/components/dialog/index.tsx +7 -2
  204. package/src/components/direction/index.tsx +2 -1
  205. package/src/components/empty-state/index.tsx +2 -2
  206. package/src/components/field/index.tsx +3 -5
  207. package/src/components/flow/index.tsx +3 -3
  208. package/src/components/frame/index.tsx +5 -7
  209. package/src/components/input-group/index.tsx +3 -2
  210. package/src/components/item/index.tsx +9 -9
  211. package/src/components/label/index.tsx +2 -2
  212. package/src/components/line-chart/index.tsx +189 -50
  213. package/src/components/map/index.tsx +7 -9
  214. package/src/components/marker/index.tsx +3 -3
  215. package/src/components/menu/index.tsx +812 -0
  216. package/src/components/message/index.tsx +7 -7
  217. package/src/components/message-scroller/index.tsx +5 -4
  218. package/src/components/plan/index.tsx +325 -0
  219. package/src/components/popover/index.tsx +111 -8
  220. package/src/components/radio-group/index.tsx +13 -4
  221. package/src/components/reasoning/index.tsx +297 -0
  222. package/src/components/section-rail/index.tsx +4 -4
  223. package/src/components/select/index.tsx +10 -4
  224. package/src/components/sources/index.tsx +278 -0
  225. package/src/components/steps/index.tsx +4 -4
  226. package/src/components/tabs/index.tsx +7 -9
  227. package/src/components/task/index.tsx +329 -0
  228. package/src/components/textarea/index.tsx +381 -0
  229. package/src/components/timeline/index.tsx +4 -4
  230. package/src/components/toast/index.tsx +2 -2
  231. package/src/components/toggle-button/index.tsx +5 -7
  232. package/src/components/tooltip/index.tsx +793 -0
  233. package/src/components/typography/index.tsx +2 -2
  234. package/src/hooks/index.ts +1 -0
  235. package/src/hooks/use-back-handler.ts +37 -0
  236. package/src/icons/index.tsx +147 -0
  237. package/src/index.ts +116 -0
  238. package/src/primitives/collapse.tsx +128 -0
  239. package/src/primitives/keyboard-avoider.tsx +2 -1
  240. package/src/primitives/text.tsx +28 -1
  241. package/src/utils/date.ts +446 -0
  242. package/theme.css +112 -0
@@ -64,6 +64,7 @@ import Svg, { G, Line as SvgLine, Path } from 'react-native-svg';
64
64
  import { useCSSVariable } from 'uniwind';
65
65
  import { Text } from '../../primitives/text';
66
66
  import {
67
+ bandOf,
67
68
  barPath,
68
69
  columnValues,
69
70
  compactNumber,
@@ -90,9 +91,15 @@ const PADDING_SIDEWAYS = { top: 6, right: 10, bottom: 6, left: 68 };
90
91
  const LABEL_WIDTH = 132;
91
92
 
92
93
  /** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
93
- const Y_AXIS_WIDTH = 40;
94
+ const Y_AXIS_WIDTH = 44;
94
95
 
95
- type Layer = 'svg' | 'overlay';
96
+ /** Gap between the value labels and the plot they sit beside. */
97
+ const Y_AXIS_GUTTER = 6;
98
+
99
+ /** Line height of an `xs` label, for centring one on the grid line it names. */
100
+ const AXIS_LABEL_HEIGHT = 16;
101
+
102
+ type Layer = 'svg' | 'overlay' | 'header';
96
103
 
97
104
  export type BarChartStatus = 'loading' | 'ready';
98
105
  export type BarChartOrientation = 'vertical' | 'horizontal';
@@ -213,13 +220,14 @@ export interface BarChartHandle {
213
220
  function partition(children: ReactNode) {
214
221
  const svg: ReactNode[] = [];
215
222
  const overlay: ReactNode[] = [];
223
+ const header: ReactNode[] = [];
216
224
  Children.forEach(children, (child, index) => {
217
225
  if (!isValidElement(child)) return;
218
226
  const layer = (child.type as { layer?: Layer }).layer ?? 'svg';
219
227
  const slot = <ChildSlot key={index}>{child}</ChildSlot>;
220
- (layer === 'overlay' ? overlay : svg).push(slot);
228
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(slot);
221
229
  });
222
- return { svg, overlay };
230
+ return { svg, overlay, header };
223
231
  }
224
232
 
225
233
  function ChildSlot({ children }: { children: ReactNode }) {
@@ -450,24 +458,28 @@ const BarChartRoot = forwardRef<BarChartHandle, BarChartProps>(function BarChart
450
458
  ]
451
459
  );
452
460
 
453
- const { svg, overlay } = partition(children);
461
+ const { svg, overlay, header } = partition(children);
454
462
 
463
+ /*
464
+ * Two views, because the header is not part of the plot. `aspectRatio` and
465
+ * the layout measurement belong to the drawing area alone — measured on the
466
+ * outer view they would take in the header too, and the plot would lose as
467
+ * much height as the readout took while still claiming the shape asked for.
468
+ */
455
469
  return (
456
470
  <BarChartContext.Provider value={context}>
457
- <View
458
- {...props}
459
- onLayout={onLayout}
460
- style={[{ aspectRatio }, props.style]}
461
- className={cn('w-full', className)}
462
- >
463
- {plot.width > 0 ? (
464
- <>
465
- <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
466
- {svg}
467
- </Svg>
468
- {overlay}
469
- </>
470
- ) : null}
471
+ <View {...props} style={props.style} className={cn('w-full', className)}>
472
+ {header}
473
+ <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
474
+ {plot.width > 0 ? (
475
+ <>
476
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
477
+ {svg}
478
+ </Svg>
479
+ {overlay}
480
+ </>
481
+ ) : null}
482
+ </View>
471
483
  </View>
472
484
  </BarChartContext.Provider>
473
485
  );
@@ -730,8 +742,18 @@ BarChartBar.layer = 'svg' as Layer;
730
742
  /* Overlay layer */
731
743
  /* -------------------------------------------------------------------------- */
732
744
 
745
+ /**
746
+ * Narrowest a category label may be drawn before the axis starts dropping
747
+ * some. Roughly three characters at `xs`, which is what a month abbreviation
748
+ * or a short name needs.
749
+ */
750
+ const MIN_BAND_LABEL = 34;
751
+
733
752
  export interface BarChartXAxisProps {
734
- /** How many labels to show. The rest are dropped, evenly. */
753
+ /**
754
+ * How many labels to show. Every category by default, thinned only when the
755
+ * bands get too narrow to read — pass a number to force it lower.
756
+ */
735
757
  ticks?: number;
736
758
  /** Turn a row into its label. Defaults to the value at `xDataKey`. */
737
759
  format?: (datum: BarChartDatum, index: number) => string;
@@ -743,40 +765,77 @@ export interface BarChartXAxisProps {
743
765
  * than SVG text, so they follow the theme's font and the platform's text
744
766
  * scaling — SVG text does neither.
745
767
  */
746
- function BarChartXAxis({ ticks = 6, format, className }: BarChartXAxisProps) {
768
+ function BarChartXAxis({ ticks, format, className }: BarChartXAxisProps) {
747
769
  const { data, xDataKey, plot, orientation } = useChart('BarChart.XAxis');
748
770
 
749
771
  const labels = useMemo(() => {
750
772
  if (!data.length) return [];
751
- const count = Math.min(ticks, data.length);
752
- const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
753
- return Array.from({ length: count }, (_unused, index) => {
754
- const dataIndex = Math.round(index * step);
755
- const datum = data[dataIndex];
756
- if (!datum) return null;
757
- return {
758
- key: dataIndex,
759
- text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? ''),
760
- };
761
- }).filter((label): label is { key: number; text: string } => label !== null);
762
- }, [data, ticks, format, xDataKey]);
773
+
774
+ /*
775
+ * Every category, unless they will not fit. A fixed tick count dropped
776
+ * names that had room to be drawn — eight months at six ticks lost March
777
+ * and June for no reason anyone looking at the chart could see. The axis
778
+ * asks the plot how much room there is instead, and only thins when the
779
+ * answer is not enough.
780
+ */
781
+ const room = Math.max(1, Math.floor(plot.width / MIN_BAND_LABEL));
782
+ const count = Math.max(1, Math.min(ticks ?? room, data.length));
783
+
784
+ // Every nth band, rather than a fractional step rounded to the nearest
785
+ // index — rounding lands on the same band twice and skips its neighbour.
786
+ const stride = Math.ceil(data.length / count);
787
+ const picked: { key: number; text: string }[] = [];
788
+ for (let index = 0; index < data.length; index += stride) {
789
+ const datum = data[index];
790
+ if (!datum) continue;
791
+ picked.push({
792
+ key: index,
793
+ text: format ? format(datum, index) : String(datum[xDataKey] ?? ''),
794
+ });
795
+ }
796
+ return picked;
797
+ }, [data, ticks, format, xDataKey, plot.width]);
763
798
 
764
799
  if (orientation === 'horizontal') return null;
765
800
 
801
+ /*
802
+ * Each label is placed on its own band's centre, not spaced evenly along the
803
+ * axis. Two things went wrong when they were spread with `justify-between`:
804
+ * a bar owns a *band* rather than sitting on a point, so the first and last
805
+ * labels were half a band out at the edges; and the indices `ticks` picks
806
+ * are not evenly spaced (eight months shown six at a time gives 0,1,3,4,6,7),
807
+ * so the ones between were spread evenly over gaps that are not.
808
+ *
809
+ * The box is backed off by half its own width rather than translated by
810
+ * `-50%`, which is not reliable across React Native versions.
811
+ */
812
+ /*
813
+ * One box per band, exactly the band's width. Tiling them rather than giving
814
+ * each a fixed width means they can never overlap each other and the first
815
+ * and last can never hang off the ends of the plot — the row of labels
816
+ * occupies precisely the space the bars do.
817
+ */
818
+ const bandWidth = data.length > 0 ? plot.width / data.length : 0;
819
+
766
820
  return (
767
821
  <View
768
- pointerEvents="none"
769
- style={{
770
- position: 'absolute',
771
- left: plot.left,
772
- right: PADDING.right,
773
- bottom: 0,
774
- height: PADDING.bottom,
775
- }}
776
- className={cn('flex-row items-center justify-between', className)}
822
+ style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
823
+ className={cn(className)}
777
824
  >
778
825
  {labels.map((label) => (
779
- <Text key={label.key} size="xs" muted numberOfLines={1}>
826
+ <Text
827
+ key={label.key}
828
+ size="xs"
829
+ muted
830
+ numberOfLines={1}
831
+ style={{
832
+ position: 'absolute',
833
+ bottom: 0,
834
+ left: bandOf(label.key, data.length, plot) - bandWidth / 2,
835
+ width: bandWidth,
836
+ textAlign: 'center',
837
+ }}
838
+ >
780
839
  {label.text}
781
840
  </Text>
782
841
  ))}
@@ -834,8 +893,12 @@ function BarChartYAxis({ ticks = 4, format, className }: BarChartYAxisProps) {
834
893
  style={{
835
894
  position: 'absolute',
836
895
  left: 0,
837
- top: plot.top,
838
- height: plot.height,
896
+ // Upright, each label is centred on the grid line it names: the strip
897
+ // is lifted half a label and grown by a whole one, so `justify-between`
898
+ // lands the text's middle on the line rather than its top edge on the
899
+ // first and its bottom edge on the last.
900
+ top: horizontal ? plot.top : plot.top - AXIS_LABEL_HEIGHT / 2,
901
+ height: horizontal ? plot.height : plot.height + AXIS_LABEL_HEIGHT,
839
902
  width: horizontal ? Math.max(plot.left - 8, 0) : undefined,
840
903
  }}
841
904
  className={cn(horizontal ? 'items-end' : 'justify-between', className)}
@@ -1011,6 +1074,18 @@ export interface BarChartLegendProps extends ViewProps {
1011
1074
  labels?: Record<string, string>;
1012
1075
  }
1013
1076
 
1077
+ /** One series' colour and name. Shared by the legend and the header. */
1078
+ function SeriesSwatch({ color, label }: { color: string; label: string }) {
1079
+ return (
1080
+ <View className="flex-row items-center gap-1.5">
1081
+ <View style={{ backgroundColor: color }} className="h-2 w-2 rounded-full" />
1082
+ <Text size="xs" muted>
1083
+ {label}
1084
+ </Text>
1085
+ </View>
1086
+ );
1087
+ }
1088
+
1014
1089
  /** A swatch and a name per series, in the order the series were declared. */
1015
1090
  function BarChartLegend({ className, labels, ...props }: BarChartLegendProps) {
1016
1091
  const { series } = useChart('BarChart.Legend');
@@ -1019,18 +1094,11 @@ function BarChartLegend({ className, labels, ...props }: BarChartLegendProps) {
1019
1094
  return (
1020
1095
  <View
1021
1096
  {...props}
1022
- pointerEvents="none"
1097
+ style={[{ pointerEvents: 'none' }, props.style]}
1023
1098
  className={cn('absolute right-2 top-1 flex-row gap-3', className)}
1024
1099
  >
1025
1100
  {series.map(([key, color]) => (
1026
- <View key={key} className="flex-row items-center gap-1.5">
1027
- <View
1028
- style={{ width: 8, height: 8, borderRadius: 2, backgroundColor: color }}
1029
- />
1030
- <Text size="xs" muted>
1031
- {labels?.[key] ?? key}
1032
- </Text>
1033
- </View>
1101
+ <SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
1034
1102
  ))}
1035
1103
  </View>
1036
1104
  );
@@ -1038,7 +1106,99 @@ function BarChartLegend({ className, labels, ...props }: BarChartLegendProps) {
1038
1106
  BarChartLegend.displayName = 'BarChart.Legend';
1039
1107
  BarChartLegend.layer = 'overlay' as Layer;
1040
1108
 
1109
+ /* -------------------------------------------------------------------------- */
1110
+ /* Header layer */
1111
+ /* -------------------------------------------------------------------------- */
1112
+
1113
+ export interface BarChartHeaderProps extends ViewProps {
1114
+ className?: string;
1115
+ /** Small line above the value — what the chart is of. */
1116
+ title?: string;
1117
+ /** The readout. The largest thing on the card, and the first thing read. */
1118
+ value?: string;
1119
+ /** One muted line under the value — a period, a comparison, a total. */
1120
+ caption?: string;
1121
+ /** Prettier names for the series keys, as the legend takes. */
1122
+ labels?: Record<string, string>;
1123
+ /**
1124
+ * Draw a swatch and a name per series along the trailing edge. Prefer this to
1125
+ * `BarChart.Legend` on a chart that has a header: the legend floats over the
1126
+ * plot, where it competes with the bars for the same corner.
1127
+ */
1128
+ legend?: boolean;
1129
+ /** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
1130
+ children?: ReactNode;
1131
+ }
1132
+
1133
+ /**
1134
+ * The strip above the plot: what the chart is of, what it currently reads, and
1135
+ * what the colours mean.
1136
+ *
1137
+ * It belongs to the chart rather than to the card around it because it is about
1138
+ * the *plot* — the number changes as a finger moves along the bars, and the
1139
+ * legend is the series list the chart itself is holding. The card's header is a
1140
+ * caption on the tray the chart sits in; this is the chart introducing itself.
1141
+ *
1142
+ * The value is not derived here. A readout that follows the finger belongs to
1143
+ * whoever owns the data — take it from `onActiveIndexChange` and pass the
1144
+ * formatted string down, so one header can show a total when nothing is pressed
1145
+ * and a band's value when something is.
1146
+ */
1147
+ function BarChartHeader({
1148
+ className,
1149
+ title,
1150
+ value,
1151
+ caption,
1152
+ labels,
1153
+ legend = false,
1154
+ children,
1155
+ ...props
1156
+ }: BarChartHeaderProps) {
1157
+ const { series } = useChart('BarChart.Header');
1158
+ const trailing =
1159
+ children ??
1160
+ (legend && series.length ? (
1161
+ <View className="flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1">
1162
+ {series.map(([key, color]) => (
1163
+ <SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
1164
+ ))}
1165
+ </View>
1166
+ ) : null);
1167
+
1168
+ return (
1169
+ <View
1170
+ {...props}
1171
+ className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
1172
+ >
1173
+ <View className="flex-1 gap-0.5">
1174
+ {title ? (
1175
+ <Text size="xs" muted>
1176
+ {title}
1177
+ </Text>
1178
+ ) : null}
1179
+ {value ? (
1180
+ <Text size="xl" weight="bold">
1181
+ {value}
1182
+ </Text>
1183
+ ) : null}
1184
+ {caption ? (
1185
+ <Text size="xs" muted>
1186
+ {caption}
1187
+ </Text>
1188
+ ) : null}
1189
+ </View>
1190
+ {/* Shrinkable, unlike a view's default in React Native. Held rigid, a
1191
+ three-series key takes the width it wants and the caption underneath
1192
+ the value wraps to two lines to make room for it. */}
1193
+ {trailing ? <View className="shrink pt-1">{trailing}</View> : null}
1194
+ </View>
1195
+ );
1196
+ }
1197
+ BarChartHeader.displayName = 'BarChart.Header';
1198
+ BarChartHeader.layer = 'header' as Layer;
1199
+
1041
1200
  export const BarChart = Object.assign(BarChartRoot, {
1201
+ Header: BarChartHeader,
1042
1202
  Grid: BarChartGrid,
1043
1203
  Bar: BarChartBar,
1044
1204
  XAxis: BarChartXAxis,
@@ -33,7 +33,8 @@ import { getNativeUI } from '../../native';
33
33
  import { XIcon } from '../../icons';
34
34
  import { Portal } from '../../primitives/portal';
35
35
  import { Scrim } from '../../primitives/scrim';
36
- import { Text } from '../../primitives/text';
36
+ import { Text, textChildren } from '../../primitives/text';
37
+ import { useBackHandler } from '../../hooks/use-back-handler';
37
38
  import { cn } from '../../utils/cn';
38
39
 
39
40
  const SPRING = { damping: 22, stiffness: 280, mass: 0.7 } as const;
@@ -290,6 +291,10 @@ function BottomSheetContent({
290
291
 
291
292
  const close = useCallback(() => setOpen(false), [setOpen]);
292
293
 
294
+ // The sheet closes on the Android back button while it is up, the same as a
295
+ // backdrop tap — but only when it is dismissible.
296
+ useBackHandler(open && dismissible, close);
297
+
293
298
  /*
294
299
  * The offset survives a close — the early return below sits after every
295
300
  * hook, so this component keeps its state while the sheet is hidden. A
@@ -418,7 +423,7 @@ function BottomSheetContent({
418
423
  paddingBottom: Math.max(insets.bottom, 16),
419
424
  }}
420
425
  >
421
- {children}
426
+ {textChildren(children)}
422
427
  </View>
423
428
  </BottomSheetContext.Provider>
424
429
  </RNHostView>
@@ -470,7 +475,7 @@ function BottomSheetContent({
470
475
  <View className="mb-3 self-center">
471
476
  <View className="h-1 w-10 rounded-full bg-muted-foreground/30" />
472
477
  </View>
473
- {children}
478
+ {textChildren(children)}
474
479
  {/*
475
480
  * Last, and lifted above the content.
476
481
  *
@@ -534,21 +539,17 @@ function BottomSheetHeader({
534
539
  }: BottomSheetHeaderProps) {
535
540
  return (
536
541
  <View className={cn('gap-1 pb-3 pe-12', className)} {...props}>
537
- {typeof title === 'string' ? (
542
+ {textChildren(title, (text) => (
538
543
  <Text size="lg" weight="semibold">
539
- {title}
544
+ {text}
540
545
  </Text>
541
- ) : (
542
- title
543
- )}
544
- {typeof description === 'string' ? (
546
+ ))}
547
+ {textChildren(description, (text) => (
545
548
  <Text size="sm" muted>
546
- {description}
549
+ {text}
547
550
  </Text>
548
- ) : (
549
- description
550
- )}
551
- {children}
551
+ ))}
552
+ {textChildren(children)}
552
553
  </View>
553
554
  );
554
555
  }
@@ -607,7 +608,7 @@ function BottomSheetBody({
607
608
  className={cn('flex-1', className)}
608
609
  {...props}
609
610
  >
610
- {children}
611
+ {textChildren(children)}
611
612
  </Animated.ScrollView>
612
613
  );
613
614
 
@@ -641,7 +642,7 @@ function BottomSheetFooter({
641
642
  className={cn('gap-2 border-t border-border pt-3', className)}
642
643
  {...props}
643
644
  >
644
- {children}
645
+ {textChildren(children)}
645
646
  </View>
646
647
  );
647
648
  }
@@ -47,7 +47,7 @@ import {
47
47
  AnimatedPressable,
48
48
  type AnimatedPressableProps,
49
49
  } from '../../primitives/animated-pressable';
50
- import { Text, type TextProps } from '../../primitives/text';
50
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
51
51
  import { cn } from '../../utils/cn';
52
52
 
53
53
  type BreadcrumbSize = 'sm' | 'default';
@@ -102,7 +102,7 @@ const BreadcrumbRoot = forwardRef<View, BreadcrumbProps>(
102
102
  className={cn('w-full', className)}
103
103
  {...props}
104
104
  >
105
- {children}
105
+ {textChildren(children)}
106
106
  </View>
107
107
  </BreadcrumbContext.Provider>
108
108
  )
@@ -202,7 +202,7 @@ const BreadcrumbItem = forwardRef<View, BreadcrumbItemProps>(
202
202
  className={cn('flex-row items-center', className)}
203
203
  {...props}
204
204
  >
205
- {children}
205
+ {textChildren(children)}
206
206
  </View>
207
207
  )
208
208
  );
@@ -235,13 +235,11 @@ const BreadcrumbLink = forwardRef<View, BreadcrumbLinkProps>(
235
235
  className={cn('flex-row items-center', className)}
236
236
  {...props}
237
237
  >
238
- {typeof children === 'string' ? (
238
+ {textChildren(children, (text) => (
239
239
  <Text className={crumbText({ size, className: cn('text-muted-foreground', textClassName) })}>
240
- {children}
240
+ {text}
241
241
  </Text>
242
- ) : (
243
- children
244
- )}
242
+ ))}
245
243
  </AnimatedPressable>
246
244
  );
247
245
  }
@@ -6,7 +6,7 @@ import {
6
6
  AnimatedPressable,
7
7
  type AnimatedPressableProps,
8
8
  } from '../../primitives/animated-pressable';
9
- import { Text } from '../../primitives/text';
9
+ import { Text, textChildren } from '../../primitives/text';
10
10
  import { IconColorProvider } from '../../icons';
11
11
  import { getNativeUI } from '../../native';
12
12
  import { Spinner } from '../spinner';
@@ -210,10 +210,16 @@ export const Button = forwardRef<View, ButtonProps>(
210
210
  >
211
211
  {/* Non-string children are React Native views, and the native
212
212
  button cannot measure those directly — they have to be hosted
213
- or they render outside the button's bounds. */}
213
+ or they render outside the button's bounds. An icon beside a
214
+ label is the common case, and the label half of it is still
215
+ bare text once it is in there. */}
214
216
  {isStringLabel ? undefined : (
215
217
  <RNHostView matchContents>
216
- <>{children}</>
218
+ <>
219
+ {textChildren(children, (text) => (
220
+ <Text className={label({ className: labelClassName })}>{text}</Text>
221
+ ))}
222
+ </>
217
223
  </RNHostView>
218
224
  )}
219
225
  </NativeButton>
@@ -236,11 +242,9 @@ export const Button = forwardRef<View, ButtonProps>(
236
242
  ) : (
237
243
  startContent
238
244
  )}
239
- {typeof children === 'string' ? (
240
- <Text className={label({ className: labelClassName })}>{children}</Text>
241
- ) : (
242
- children
243
- )}
245
+ {textChildren(children, (text) => (
246
+ <Text className={label({ className: labelClassName })}>{text}</Text>
247
+ ))}
244
248
  {endContent}
245
249
  </AnimatedPressable>
246
250
  </IconColorProvider>