panelui-native 0.25.0 → 0.28.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 (193) hide show
  1. package/lib/module/components/accordion/index.js +8 -8
  2. package/lib/module/components/accordion/index.js.map +1 -1
  3. package/lib/module/components/alert/index.js +2 -2
  4. package/lib/module/components/alert/index.js.map +1 -1
  5. package/lib/module/components/area-chart/index.js +182 -50
  6. package/lib/module/components/area-chart/index.js.map +1 -1
  7. package/lib/module/components/attachment/index.js +2 -1
  8. package/lib/module/components/attachment/index.js.map +1 -1
  9. package/lib/module/components/avatar/index.js +3 -3
  10. package/lib/module/components/avatar/index.js.map +1 -1
  11. package/lib/module/components/badge/index.js +4 -4
  12. package/lib/module/components/badge/index.js.map +1 -1
  13. package/lib/module/components/bar-chart/index.js +197 -56
  14. package/lib/module/components/bar-chart/index.js.map +1 -1
  15. package/lib/module/components/bottom-sheet/index.js +15 -10
  16. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  17. package/lib/module/components/breadcrumb/index.js +6 -6
  18. package/lib/module/components/breadcrumb/index.js.map +1 -1
  19. package/lib/module/components/button/index.js +11 -6
  20. package/lib/module/components/button/index.js.map +1 -1
  21. package/lib/module/components/calendar/index.js +557 -0
  22. package/lib/module/components/calendar/index.js.map +1 -0
  23. package/lib/module/components/carousel/index.js +680 -0
  24. package/lib/module/components/carousel/index.js.map +1 -0
  25. package/lib/module/components/chip/index.js +4 -4
  26. package/lib/module/components/chip/index.js.map +1 -1
  27. package/lib/module/components/date-picker/index.js +158 -0
  28. package/lib/module/components/date-picker/index.js.map +1 -0
  29. package/lib/module/components/dialog/index.js +7 -2
  30. package/lib/module/components/dialog/index.js.map +1 -1
  31. package/lib/module/components/direction/index.js +2 -1
  32. package/lib/module/components/direction/index.js.map +1 -1
  33. package/lib/module/components/empty-state/index.js +2 -2
  34. package/lib/module/components/empty-state/index.js.map +1 -1
  35. package/lib/module/components/field/index.js +5 -5
  36. package/lib/module/components/field/index.js.map +1 -1
  37. package/lib/module/components/flow/index.js +3 -3
  38. package/lib/module/components/flow/index.js.map +1 -1
  39. package/lib/module/components/frame/index.js +5 -5
  40. package/lib/module/components/frame/index.js.map +1 -1
  41. package/lib/module/components/input-group/index.js +3 -2
  42. package/lib/module/components/input-group/index.js.map +1 -1
  43. package/lib/module/components/item/index.js +9 -9
  44. package/lib/module/components/item/index.js.map +1 -1
  45. package/lib/module/components/label/index.js +4 -4
  46. package/lib/module/components/label/index.js.map +1 -1
  47. package/lib/module/components/line-chart/index.js +167 -44
  48. package/lib/module/components/line-chart/index.js.map +1 -1
  49. package/lib/module/components/map/index.js +7 -7
  50. package/lib/module/components/map/index.js.map +1 -1
  51. package/lib/module/components/marker/index.js +3 -3
  52. package/lib/module/components/marker/index.js.map +1 -1
  53. package/lib/module/components/message/index.js +7 -7
  54. package/lib/module/components/message/index.js.map +1 -1
  55. package/lib/module/components/message-scroller/index.js +5 -4
  56. package/lib/module/components/message-scroller/index.js.map +1 -1
  57. package/lib/module/components/popover/index.js +39 -7
  58. package/lib/module/components/popover/index.js.map +1 -1
  59. package/lib/module/components/radio-group/index.js +2 -2
  60. package/lib/module/components/radio-group/index.js.map +1 -1
  61. package/lib/module/components/section-rail/index.js +4 -4
  62. package/lib/module/components/section-rail/index.js.map +1 -1
  63. package/lib/module/components/select/index.js +10 -4
  64. package/lib/module/components/select/index.js.map +1 -1
  65. package/lib/module/components/steps/index.js +4 -4
  66. package/lib/module/components/steps/index.js.map +1 -1
  67. package/lib/module/components/tabs/index.js +7 -7
  68. package/lib/module/components/tabs/index.js.map +1 -1
  69. package/lib/module/components/timeline/index.js +4 -4
  70. package/lib/module/components/timeline/index.js.map +1 -1
  71. package/lib/module/components/toast/index.js +2 -2
  72. package/lib/module/components/toast/index.js.map +1 -1
  73. package/lib/module/components/toggle-button/index.js +5 -5
  74. package/lib/module/components/toggle-button/index.js.map +1 -1
  75. package/lib/module/components/tooltip/index.js +492 -0
  76. package/lib/module/components/tooltip/index.js.map +1 -0
  77. package/lib/module/components/typography/index.js +2 -2
  78. package/lib/module/components/typography/index.js.map +1 -1
  79. package/lib/module/hooks/index.js +1 -0
  80. package/lib/module/hooks/index.js.map +1 -1
  81. package/lib/module/hooks/use-back-handler.js +37 -0
  82. package/lib/module/hooks/use-back-handler.js.map +1 -0
  83. package/lib/module/icons/index.js +63 -0
  84. package/lib/module/icons/index.js.map +1 -1
  85. package/lib/module/index.js +5 -1
  86. package/lib/module/index.js.map +1 -1
  87. package/lib/module/primitives/keyboard-avoider.js +2 -1
  88. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  89. package/lib/module/primitives/text.js +25 -1
  90. package/lib/module/primitives/text.js.map +1 -1
  91. package/lib/module/utils/date.js +386 -0
  92. package/lib/module/utils/date.js.map +1 -0
  93. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  94. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  95. package/lib/typescript/src/components/area-chart/index.d.ts +41 -1
  96. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  97. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
  98. package/lib/typescript/src/components/bar-chart/index.d.ts +44 -2
  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.map +1 -1
  101. package/lib/typescript/src/components/breadcrumb/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/calendar/index.d.ts +108 -0
  104. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
  105. package/lib/typescript/src/components/carousel/index.d.ts +179 -0
  106. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
  107. package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
  108. package/lib/typescript/src/components/date-picker/index.d.ts +91 -0
  109. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
  110. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  111. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  112. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  113. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  114. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  115. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  116. package/lib/typescript/src/components/item/index.d.ts.map +1 -1
  117. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  118. package/lib/typescript/src/components/line-chart/index.d.ts +41 -2
  119. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/map/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
  122. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  123. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  124. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  125. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  126. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  127. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  129. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  130. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/tooltip/index.d.ts +125 -0
  132. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
  133. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  134. package/lib/typescript/src/hooks/index.d.ts +1 -0
  135. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  136. package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
  137. package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
  138. package/lib/typescript/src/icons/index.d.ts +3 -0
  139. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  140. package/lib/typescript/src/index.d.ts +8 -4
  141. package/lib/typescript/src/index.d.ts.map +1 -1
  142. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  143. package/lib/typescript/src/primitives/text.d.ts +20 -0
  144. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  145. package/lib/typescript/src/utils/date.d.ts +116 -0
  146. package/lib/typescript/src/utils/date.d.ts.map +1 -0
  147. package/package.json +1 -1
  148. package/src/components/accordion/index.tsx +8 -12
  149. package/src/components/alert/index.tsx +2 -2
  150. package/src/components/area-chart/index.tsx +208 -55
  151. package/src/components/attachment/index.tsx +2 -2
  152. package/src/components/avatar/index.tsx +3 -3
  153. package/src/components/badge/index.tsx +6 -6
  154. package/src/components/bar-chart/index.tsx +214 -54
  155. package/src/components/bottom-sheet/index.tsx +17 -16
  156. package/src/components/breadcrumb/index.tsx +6 -8
  157. package/src/components/button/index.tsx +12 -8
  158. package/src/components/calendar/index.tsx +703 -0
  159. package/src/components/carousel/index.tsx +788 -0
  160. package/src/components/chip/index.tsx +4 -6
  161. package/src/components/date-picker/index.tsx +237 -0
  162. package/src/components/dialog/index.tsx +7 -2
  163. package/src/components/direction/index.tsx +2 -1
  164. package/src/components/empty-state/index.tsx +2 -2
  165. package/src/components/field/index.tsx +3 -5
  166. package/src/components/flow/index.tsx +3 -3
  167. package/src/components/frame/index.tsx +5 -7
  168. package/src/components/input-group/index.tsx +3 -2
  169. package/src/components/item/index.tsx +9 -9
  170. package/src/components/label/index.tsx +2 -2
  171. package/src/components/line-chart/index.tsx +189 -50
  172. package/src/components/map/index.tsx +7 -9
  173. package/src/components/marker/index.tsx +3 -3
  174. package/src/components/message/index.tsx +7 -7
  175. package/src/components/message-scroller/index.tsx +5 -4
  176. package/src/components/popover/index.tsx +43 -7
  177. package/src/components/radio-group/index.tsx +2 -2
  178. package/src/components/section-rail/index.tsx +4 -4
  179. package/src/components/select/index.tsx +10 -4
  180. package/src/components/steps/index.tsx +4 -4
  181. package/src/components/tabs/index.tsx +7 -9
  182. package/src/components/timeline/index.tsx +4 -4
  183. package/src/components/toast/index.tsx +2 -2
  184. package/src/components/toggle-button/index.tsx +5 -7
  185. package/src/components/tooltip/index.tsx +610 -0
  186. package/src/components/typography/index.tsx +2 -2
  187. package/src/hooks/index.ts +1 -0
  188. package/src/hooks/use-back-handler.ts +37 -0
  189. package/src/icons/index.tsx +45 -0
  190. package/src/index.ts +45 -0
  191. package/src/primitives/keyboard-avoider.tsx +2 -1
  192. package/src/primitives/text.tsx +28 -1
  193. package/src/utils/date.ts +446 -0
@@ -106,7 +106,7 @@ const PADDING = { top: 12, right: 10, bottom: 22, left: 10 };
106
106
  * stays a flat list of children instead of two nested slots the caller has to
107
107
  * remember the order of.
108
108
  */
109
- type Layer = 'svg' | 'overlay';
109
+ type Layer = 'svg' | 'overlay' | 'header';
110
110
 
111
111
  export type LineChartStatus = 'loading' | 'ready';
112
112
  export type LineChartCurve = 'monotone' | 'linear';
@@ -388,39 +388,43 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
388
388
  ]
389
389
  );
390
390
 
391
- const { svg, overlay } = partition(children);
391
+ const { svg, overlay, header } = partition(children);
392
392
 
393
+ /*
394
+ * Two views, because the header is not part of the plot. `aspectRatio` and
395
+ * the layout measurement belong to the drawing area alone — measured on the
396
+ * outer view they would take in the header too, and the plot would lose as
397
+ * much height as the readout took while still claiming the shape asked for.
398
+ */
393
399
  return (
394
400
  <LineChartContext.Provider value={context}>
395
- <View
396
- {...props}
397
- onLayout={onLayout}
398
- style={[{ aspectRatio }, props.style]}
399
- className={cn('w-full', className)}
400
- >
401
- {plot.width > 0 ? (
402
- <>
403
- <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
404
- <Defs>
405
- {/*
406
- * One clip for everything in the plot. Sharing it is what makes
407
- * the reveal read as the chart arriving, rather than as three
408
- * separate things animating in at once.
409
- */}
410
- <ClipPath id={clipId}>
411
- <AnimatedRect
412
- x={plot.left}
413
- y={0}
414
- height={size.height}
415
- animatedProps={clipProps}
416
- />
417
- </ClipPath>
418
- </Defs>
419
- {svg}
420
- </Svg>
421
- {overlay}
422
- </>
423
- ) : null}
401
+ <View {...props} style={props.style} className={cn('w-full', className)}>
402
+ {header}
403
+ <View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
404
+ {plot.width > 0 ? (
405
+ <>
406
+ <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
407
+ <Defs>
408
+ {/*
409
+ * One clip for everything in the plot. Sharing it is what
410
+ * makes the reveal read as the chart arriving, rather than as
411
+ * three separate things animating in at once.
412
+ */}
413
+ <ClipPath id={clipId}>
414
+ <AnimatedRect
415
+ x={plot.left}
416
+ y={0}
417
+ height={size.height}
418
+ animatedProps={clipProps}
419
+ />
420
+ </ClipPath>
421
+ </Defs>
422
+ {svg}
423
+ </Svg>
424
+ {overlay}
425
+ </>
426
+ ) : null}
427
+ </View>
424
428
  </View>
425
429
  </LineChartContext.Provider>
426
430
  );
@@ -431,18 +435,19 @@ LineChartRoot.displayName = 'LineChart';
431
435
  function partition(children: ReactNode) {
432
436
  const svg: ReactNode[] = [];
433
437
  const overlay: ReactNode[] = [];
438
+ const header: ReactNode[] = [];
434
439
 
435
440
  Children.forEach(children, (child, index) => {
436
441
  if (!isValidElement(child)) return;
437
442
  const layer = (child.type as { layer?: Layer }).layer ?? 'svg';
438
- (layer === 'overlay' ? overlay : svg).push(
443
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(
439
444
  // Children of a `Children.forEach` need keys of their own once they are
440
445
  // put into a new array.
441
446
  <ChildSlot key={index}>{child}</ChildSlot>
442
447
  );
443
448
  });
444
449
 
445
- return { svg, overlay };
450
+ return { svg, overlay, header };
446
451
  }
447
452
 
448
453
  /** Identity wrapper, purely so the partitioned arrays can carry keys. */
@@ -737,20 +742,44 @@ function LineChartXAxis({ ticks = 4, format, className }: LineChartXAxisProps) {
737
742
  }).filter((label): label is { key: number; text: string } => label !== null);
738
743
  }, [data, ticks, format, xDataKey]);
739
744
 
745
+ /*
746
+ * Each label sits on its own point rather than being spread along the axis.
747
+ * The indices `ticks` picks are not evenly spaced — twelve months shown five
748
+ * at a time gives 0,3,6,8,11 — so spreading them evenly put the ones in
749
+ * between over the wrong part of the line.
750
+ *
751
+ * The box is backed off by half its own width rather than translated by
752
+ * `-50%`, which is not reliable across React Native versions.
753
+ */
740
754
  return (
741
755
  <View
742
- pointerEvents="none"
743
- style={{
744
- position: 'absolute',
745
- left: plot.left,
746
- right: PADDING.right,
747
- bottom: 0,
748
- height: PADDING.bottom,
749
- }}
750
- className={cn('flex-row items-center justify-between', className)}
756
+ style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
757
+ className={cn(className)}
751
758
  >
752
759
  {labels.map((label) => (
753
- <Text key={label.key} size="xs" muted>
760
+ <Text
761
+ key={label.key}
762
+ size="xs"
763
+ muted
764
+ numberOfLines={1}
765
+ style={{
766
+ position: 'absolute',
767
+ bottom: 0,
768
+ // Centred on its point, then held inside the chart. The first and
769
+ // last points sit on the plot's own edges, so a box centred on
770
+ // them hangs half its width off the side — the clamp slides those
771
+ // two back in rather than letting the numbers leave the frame.
772
+ left: Math.max(
773
+ 0,
774
+ Math.min(
775
+ xOf(label.key, data.length, plot) - POINT_LABEL_WIDTH / 2,
776
+ plot.left + plot.width + PADDING.right - POINT_LABEL_WIDTH
777
+ )
778
+ ),
779
+ width: POINT_LABEL_WIDTH,
780
+ textAlign: 'center',
781
+ }}
782
+ >
754
783
  {label.text}
755
784
  </Text>
756
785
  ))}
@@ -955,6 +984,16 @@ LineChartTooltip.layer = 'overlay' as Layer;
955
984
 
956
985
  const LABEL_WIDTH = 112;
957
986
 
987
+ /**
988
+ * Box each x label is centred in. Wide enough for a short label to sit over
989
+ * its point without the neighbours colliding; a longer one is ellipsised
990
+ * rather than allowed to push the others out of place.
991
+ */
992
+ const POINT_LABEL_WIDTH = 56;
993
+
994
+ /** Line height of an `xs` label, for centring one on the grid line it names. */
995
+ const AXIS_LABEL_HEIGHT = 16;
996
+
958
997
  /** A compact number for the crosshair label: 1204 → "1.2k", 24801 → "24.8k". */
959
998
  const DOT = 9;
960
999
 
@@ -1018,20 +1057,16 @@ export interface LineChartLegendProps extends ViewProps {
1018
1057
  */
1019
1058
  function LineChartLegend({ className, labels, ...props }: LineChartLegendProps) {
1020
1059
  const { series } = useChart('LineChart.Legend');
1060
+ if (!series.length) return null;
1021
1061
 
1022
1062
  return (
1023
1063
  <View
1024
- pointerEvents="none"
1025
1064
  className={cn('absolute left-2.5 top-0 flex-row flex-wrap items-center gap-4', className)}
1026
1065
  {...props}
1066
+ style={[{ pointerEvents: 'none' }, props.style]}
1027
1067
  >
1028
1068
  {series.map(([key, color]) => (
1029
- <View key={key} className="flex-row items-center gap-1.5">
1030
- <View style={{ backgroundColor: color }} className="h-2 w-2 rounded-full" />
1031
- <Text size="xs" muted>
1032
- {labels?.[key] ?? key}
1033
- </Text>
1034
- </View>
1069
+ <SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
1035
1070
  ))}
1036
1071
  </View>
1037
1072
  );
@@ -1039,6 +1074,109 @@ function LineChartLegend({ className, labels, ...props }: LineChartLegendProps)
1039
1074
  LineChartLegend.displayName = 'LineChart.Legend';
1040
1075
  LineChartLegend.layer = 'overlay' as Layer;
1041
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
+
1089
+ /* -------------------------------------------------------------------------- */
1090
+ /* Header layer */
1091
+ /* -------------------------------------------------------------------------- */
1092
+
1093
+ export interface LineChartHeaderProps extends ViewProps {
1094
+ className?: string;
1095
+ /** Small line above the value — what the chart is of. */
1096
+ title?: string;
1097
+ /** The readout. The largest thing on the card, and the first thing read. */
1098
+ value?: string;
1099
+ /** One muted line under the value — a period, a comparison, a total. */
1100
+ caption?: string;
1101
+ /** Prettier names for the series keys, as the legend takes. */
1102
+ labels?: Record<string, string>;
1103
+ /**
1104
+ * Draw a swatch and a name per series along the trailing edge. Prefer this to
1105
+ * `LineChart.Legend` on a chart that has a header: the legend floats over the
1106
+ * plot, where it competes with the lines for the same corner.
1107
+ */
1108
+ legend?: boolean;
1109
+ /** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
1110
+ children?: ReactNode;
1111
+ }
1112
+
1113
+ /**
1114
+ * The strip above the plot: what the chart is of, what it currently reads, and
1115
+ * what the colours mean.
1116
+ *
1117
+ * It belongs to the chart rather than to the card around it because it is about
1118
+ * the *plot* — the number changes as a finger moves along the line, and the
1119
+ * legend is the series list the chart itself is holding. The card's header is a
1120
+ * caption on the tray the chart sits in; this is the chart introducing itself.
1121
+ *
1122
+ * The value is not derived here. A readout that follows the finger belongs to
1123
+ * whoever owns the data — take it from `onActiveIndexChange` and pass the
1124
+ * formatted string down, so one header can show a total when nothing is pressed
1125
+ * and a point's value when something is.
1126
+ */
1127
+ function LineChartHeader({
1128
+ className,
1129
+ title,
1130
+ value,
1131
+ caption,
1132
+ labels,
1133
+ legend = false,
1134
+ children,
1135
+ ...props
1136
+ }: LineChartHeaderProps) {
1137
+ const { series } = useChart('LineChart.Header');
1138
+ const trailing =
1139
+ children ??
1140
+ (legend && series.length ? (
1141
+ <View className="flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1">
1142
+ {series.map(([key, color]) => (
1143
+ <SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
1144
+ ))}
1145
+ </View>
1146
+ ) : null);
1147
+
1148
+ return (
1149
+ <View
1150
+ {...props}
1151
+ className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
1152
+ >
1153
+ <View className="flex-1 gap-0.5">
1154
+ {title ? (
1155
+ <Text size="xs" muted>
1156
+ {title}
1157
+ </Text>
1158
+ ) : null}
1159
+ {value ? (
1160
+ <Text size="xl" weight="bold">
1161
+ {value}
1162
+ </Text>
1163
+ ) : null}
1164
+ {caption ? (
1165
+ <Text size="xs" muted>
1166
+ {caption}
1167
+ </Text>
1168
+ ) : null}
1169
+ </View>
1170
+ {/* Shrinkable, unlike a view's default in React Native. Held rigid, a
1171
+ three-series key takes the width it wants and the caption underneath
1172
+ the value wraps to two lines to make room for it. */}
1173
+ {trailing ? <View className="shrink pt-1">{trailing}</View> : null}
1174
+ </View>
1175
+ );
1176
+ }
1177
+ LineChartHeader.displayName = 'LineChart.Header';
1178
+ LineChartHeader.layer = 'header' as Layer;
1179
+
1042
1180
  /* -------------------------------------------------------------------------- */
1043
1181
  /* Scales and paths */
1044
1182
  /* -------------------------------------------------------------------------- */
@@ -1046,6 +1184,7 @@ LineChartLegend.layer = 'overlay' as Layer;
1046
1184
  /** Only reached if the theme CSS was never imported. */
1047
1185
 
1048
1186
  export const LineChart = Object.assign(LineChartRoot, {
1187
+ Header: LineChartHeader,
1049
1188
  Grid: LineChartGrid,
1050
1189
  Area: LineChartArea,
1051
1190
  Line: LineChartLine,
@@ -46,7 +46,7 @@ import { Pressable, View, type ViewProps } from 'react-native';
46
46
  import { useCSSVariable } from 'uniwind';
47
47
  import { tv } from 'tailwind-variants';
48
48
  import { CompassIcon, CrosshairIcon, MinusIcon, PlusIcon } from '../../icons';
49
- import { Text } from '../../primitives/text';
49
+ import { Text, textChildren } from '../../primitives/text';
50
50
  import { cn } from '../../utils/cn';
51
51
  import {
52
52
  buildBasemapStyle,
@@ -334,7 +334,7 @@ const MapRoot = forwardRef<MapHandle, MapProps>(function MapRoot(
334
334
  : { center: center ?? [0, 20], zoom, bearing, pitch }
335
335
  }
336
336
  />
337
- {children}
337
+ {textChildren(children)}
338
338
  </MapLibreMap>
339
339
  </MapContext.Provider>
340
340
  </View>
@@ -471,13 +471,11 @@ function MapLabel({ children, className, side = 'bottom' }: MapLabelProps) {
471
471
  style={{ left: -120, right: -120 }}
472
472
  >
473
473
  <View className={cn('rounded-md bg-background/85 px-1.5 py-0.5', className)}>
474
- {typeof children === 'string' ? (
474
+ {textChildren(children, (text) => (
475
475
  <Text size="xs" weight="medium" numberOfLines={1}>
476
- {children}
476
+ {text}
477
477
  </Text>
478
- ) : (
479
- children
480
- )}
478
+ ))}
481
479
  </View>
482
480
  </View>
483
481
  );
@@ -525,7 +523,7 @@ function MapPopup({ children, className, title, lngLat }: MapPopupProps) {
525
523
  {title}
526
524
  </Text>
527
525
  ) : null}
528
- {children}
526
+ {textChildren(children)}
529
527
  </View>
530
528
  </Marker>
531
529
  );
@@ -563,7 +561,7 @@ function ControlButton({
563
561
  className={slots.control()}
564
562
  onPress={onPress}
565
563
  >
566
- {children}
564
+ {textChildren(children)}
567
565
  </Pressable>
568
566
  );
569
567
  }
@@ -25,7 +25,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
25
25
  import { useCSSVariable } from 'uniwind';
26
26
  import { IconColorProvider } from '../../icons';
27
27
  import { AnimatedPressable, type AnimatedPressableProps } from '../../primitives/animated-pressable';
28
- import { Text, type TextProps } from '../../primitives/text';
28
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
29
29
  import { cn } from '../../utils/cn';
30
30
  import { Separator } from '../separator';
31
31
  import { Shimmer } from '../shimmer';
@@ -90,7 +90,7 @@ const MarkerRoot = forwardRef<View, MarkerProps>(
90
90
  variant === 'separator' ? (
91
91
  <>
92
92
  <Separator className="flex-1" />
93
- {children}
93
+ {textChildren(children)}
94
94
  <Separator className="flex-1" />
95
95
  </>
96
96
  ) : (
@@ -149,7 +149,7 @@ const MarkerIcon = forwardRef<View, MarkerIconProps>(
149
149
  {...props}
150
150
  >
151
151
  <IconColorProvider color={typeof muted === 'string' ? muted : undefined}>
152
- {children}
152
+ {textChildren(children)}
153
153
  </IconColorProvider>
154
154
  </View>
155
155
  );
@@ -34,7 +34,7 @@ import { tv, type VariantProps } from 'tailwind-variants';
34
34
  import { useCSSVariable } from 'uniwind';
35
35
  import { IconColorProvider } from '../../icons';
36
36
  import { AnimatedPressable } from '../../primitives/animated-pressable';
37
- import { Text, type TextProps } from '../../primitives/text';
37
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
38
38
  import { cn } from '../../utils/cn';
39
39
 
40
40
  const messageVariants = tv({
@@ -137,11 +137,11 @@ const MessageRoot = forwardRef<View, MessageProps>(
137
137
  className={root({ className })}
138
138
  {...props}
139
139
  >
140
- {children}
140
+ {textChildren(children)}
141
141
  </AnimatedPressable>
142
142
  ) : (
143
143
  <View ref={ref} className={root({ className })} {...props}>
144
- {children}
144
+ {textChildren(children)}
145
145
  </View>
146
146
  );
147
147
 
@@ -207,7 +207,7 @@ const MessageAvatar = forwardRef<View, MessageAvatarProps>(
207
207
 
208
208
  return (
209
209
  <View ref={ref} className={avatar({ className })} {...props}>
210
- {stacked ? <View className="opacity-0">{children}</View> : children}
210
+ {stacked ? <View className="opacity-0">{textChildren(children)}</View> : children}
211
211
  </View>
212
212
  );
213
213
  }
@@ -227,7 +227,7 @@ const MessageContent = forwardRef<View, MessageContentProps>(
227
227
 
228
228
  return (
229
229
  <View ref={ref} className={content({ className })} {...props}>
230
- {children}
230
+ {textChildren(children)}
231
231
  </View>
232
232
  );
233
233
  }
@@ -275,7 +275,7 @@ const MessageBubble = forwardRef<View, MessageBubbleProps>(
275
275
  return (
276
276
  <View ref={ref} className={bubble({ className })} {...props}>
277
277
  <IconColorProvider color={typeof foreground === 'string' ? foreground : undefined}>
278
- {children}
278
+ {textChildren(children)}
279
279
  </IconColorProvider>
280
280
  </View>
281
281
  );
@@ -334,7 +334,7 @@ const MessageActions = forwardRef<View, MessageActionsProps>(
334
334
  )}
335
335
  {...props}
336
336
  >
337
- {children}
337
+ {textChildren(children)}
338
338
  </View>
339
339
  );
340
340
  }
@@ -56,6 +56,7 @@ import { useCSSVariable } from 'uniwind';
56
56
  import { ChevronDownIcon } from '../../icons';
57
57
  import { AnimatedPressable } from '../../primitives/animated-pressable';
58
58
  import { cn } from '../../utils/cn';
59
+ import { textChildren } from '../../primitives/text';
59
60
 
60
61
  /** Within this many pixels of the bottom still counts as being at the bottom. */
61
62
  const END_THRESHOLD = 24;
@@ -233,7 +234,7 @@ function MessageScrollerRoot({
233
234
  return (
234
235
  <MessageScrollerContext.Provider value={context}>
235
236
  <View className={cn('relative overflow-hidden', className)} {...props}>
236
- {children}
237
+ {textChildren(children)}
237
238
  </View>
238
239
  </MessageScrollerContext.Provider>
239
240
  );
@@ -404,7 +405,7 @@ function MessageScrollerViewport({
404
405
  onMomentumScrollEnd={settleAnchor}
405
406
  onContentSizeChange={onContentSizeChange}
406
407
  >
407
- {children}
408
+ {textChildren(children)}
408
409
  </Animated.ScrollView>
409
410
  );
410
411
  }
@@ -445,7 +446,7 @@ function MessageScrollerContent({
445
446
  className={cn('gap-3 p-4 pb-16', className)}
446
447
  {...props}
447
448
  >
448
- {children}
449
+ {textChildren(children)}
449
450
  </View>
450
451
  );
451
452
  }
@@ -487,7 +488,7 @@ function MessageScrollerItem({
487
488
 
488
489
  return (
489
490
  <View className={className} {...props} onLayout={onLayout}>
490
- {children}
491
+ {textChildren(children)}
491
492
  </View>
492
493
  );
493
494
  }
@@ -60,8 +60,9 @@ import Animated, {
60
60
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
61
61
  import { Portal } from '../../primitives/portal';
62
62
  import { Scrim } from '../../primitives/scrim';
63
+ import { useBackHandler } from '../../hooks/use-back-handler';
63
64
  import { BottomSheet } from '../bottom-sheet';
64
- import { Text, type TextProps } from '../../primitives/text';
65
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
65
66
  import { cn } from '../../utils/cn';
66
67
 
67
68
  /** Gap between the trigger and the panel. */
@@ -255,6 +256,13 @@ function PopoverContent({
255
256
  }: PopoverContentProps) {
256
257
  const context = usePopover('Popover.Content');
257
258
  const { open, setOpen, trigger, setPlacement, setArrowOffset, presentation } = context;
259
+
260
+ // The anchored panel owns the Android back button while it is up. The sheet
261
+ // presentation is left alone — BottomSheet installs its own handler.
262
+ useBackHandler(open && dismissible && presentation !== 'bottom-sheet', () =>
263
+ setOpen(false)
264
+ );
265
+
258
266
  const { width: screenWidth, height: screenHeight } = useWindowDimensions();
259
267
  const insets = useSafeAreaInsets();
260
268
  const [size, setSize] = useState<{ width: number; height: number } | null>(null);
@@ -349,14 +357,42 @@ function PopoverContent({
349
357
  ],
350
358
  }));
351
359
 
352
- // The sheet presentation hands off entirely to BottomSheet — it owns its own
353
- // portal, backdrop and dismiss gesture, so there is nothing to anchor here.
354
- // The context is re-provided inside so Popover.Close keeps closing it.
360
+ /*
361
+ * The sheet presentation hands off entirely to BottomSheet — it owns its own
362
+ * portal, backdrop and dismiss gesture, so there is nothing to anchor here.
363
+ * The context is re-provided inside so Popover.Close keeps closing it.
364
+ *
365
+ * `width` still means something, though. A sheet is the full width of the
366
+ * screen and lays its children out in a column, so a panel sized to its
367
+ * content takes the cross-axis *start* and ends up flush against one edge —
368
+ * the right-hand one under RTL, directly below the close button. Centring it
369
+ * is what makes `width="content-fit"` mean the same thing in both
370
+ * presentations.
371
+ *
372
+ * The className goes on an inner view rather than on the sheet: passed to
373
+ * the surface it is merged into the sheet's own padding classes and replaces
374
+ * them, so a panel asking for `p-3` silently strips the sheet's `px-5 pt-2`.
375
+ */
355
376
  if (presentation === 'bottom-sheet') {
356
377
  return (
357
378
  <BottomSheet open={open} onOpenChange={setOpen}>
358
- <BottomSheet.Content dismissible={dismissible} className={className}>
359
- <PopoverContext.Provider value={context}>{children}</PopoverContext.Provider>
379
+ <BottomSheet.Content dismissible={dismissible}>
380
+ <PopoverContext.Provider value={context}>
381
+ <View
382
+ className={cn(width === 'content-fit' && 'self-center', className)}
383
+ style={[
384
+ width === 'full' || width === 'trigger' ? { width: '100%' } : null,
385
+ typeof width === 'number' ? { width } : null,
386
+ minWidth === undefined ? null : { minWidth },
387
+ // Clear of the close button, which floats over the top-end
388
+ // corner of the sheet rather than sitting in the flow.
389
+ { paddingTop: 8 },
390
+ style,
391
+ ]}
392
+ >
393
+ {textChildren(children)}
394
+ </View>
395
+ </PopoverContext.Provider>
360
396
  </BottomSheet.Content>
361
397
  </BottomSheet>
362
398
  );
@@ -410,7 +446,7 @@ function PopoverContent({
410
446
  )}
411
447
  {...props}
412
448
  >
413
- {children}
449
+ {textChildren(children)}
414
450
  </Animated.View>
415
451
  </Animated.View>
416
452
  </View>
@@ -35,7 +35,7 @@ import Animated, {
35
35
  } from 'react-native-reanimated';
36
36
  import { tv, type VariantProps } from 'tailwind-variants';
37
37
  import { cn } from '../../utils/cn';
38
- import { Text } from '../../primitives/text';
38
+ import { Text, textChildren } from '../../primitives/text';
39
39
 
40
40
  type RadioVariant = 'dot' | 'card';
41
41
  type RadioOrientation = 'vertical' | 'horizontal';
@@ -152,7 +152,7 @@ const RadioGroupRoot = forwardRef<View, RadioGroupProps>(
152
152
  )}
153
153
  {...props}
154
154
  >
155
- {children}
155
+ {textChildren(children)}
156
156
  </View>
157
157
  </RadioGroupContext.Provider>
158
158
  );
@@ -50,7 +50,7 @@ import Animated, {
50
50
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
51
51
  import { useCSSVariable } from 'uniwind';
52
52
  import { Portal } from '../../primitives/portal';
53
- import { Text } from '../../primitives/text';
53
+ import { Text, textChildren } from '../../primitives/text';
54
54
  import { cn } from '../../utils/cn';
55
55
  import { selectionTick } from '../../utils/haptics';
56
56
 
@@ -242,7 +242,7 @@ function SectionRailRoot({
242
242
  }}
243
243
  {...props}
244
244
  >
245
- {children}
245
+ {textChildren(children)}
246
246
  </View>
247
247
  </SectionRailContext.Provider>
248
248
  );
@@ -276,7 +276,7 @@ function SectionRailTrigger({ className, children, ...props }: SectionRailTrigge
276
276
  )}
277
277
  {...props}
278
278
  >
279
- {children}
279
+ {textChildren(children)}
280
280
  </Pressable>
281
281
  );
282
282
  }
@@ -407,7 +407,7 @@ function SectionRailPanel({ className, children, ...props }: SectionRailContentP
407
407
  {...props}
408
408
  >
409
409
  <ScrollView bounces={false} showsVerticalScrollIndicator={false}>
410
- {children}
410
+ {textChildren(children)}
411
411
  </ScrollView>
412
412
  </Animated.View>
413
413
  );