panelui-native 0.22.6 → 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 (258) hide show
  1. package/README.md +1 -0
  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 +958 -0
  7. package/lib/module/components/area-chart/index.js.map +1 -0
  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 +4 -4
  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 +994 -0
  15. package/lib/module/components/bar-chart/index.js.map +1 -0
  16. package/lib/module/components/bottom-sheet/index.js +213 -24
  17. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  18. package/lib/module/components/breadcrumb/index.js +275 -0
  19. package/lib/module/components/breadcrumb/index.js.map +1 -0
  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 +557 -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 +6 -6
  27. package/lib/module/components/chip/index.js.map +1 -1
  28. package/lib/module/components/date-picker/index.js +158 -0
  29. package/lib/module/components/date-picker/index.js.map +1 -0
  30. package/lib/module/components/dialog/index.js +7 -2
  31. package/lib/module/components/dialog/index.js.map +1 -1
  32. package/lib/module/components/direction/index.js +15 -20
  33. package/lib/module/components/direction/index.js.map +1 -1
  34. package/lib/module/components/empty-state/index.js +2 -2
  35. package/lib/module/components/empty-state/index.js.map +1 -1
  36. package/lib/module/components/field/index.js +5 -5
  37. package/lib/module/components/field/index.js.map +1 -1
  38. package/lib/module/components/flow/index.js +3 -3
  39. package/lib/module/components/flow/index.js.map +1 -1
  40. package/lib/module/components/frame/index.js +22 -5
  41. package/lib/module/components/frame/index.js.map +1 -1
  42. package/lib/module/components/heatmap-chart/index.js +28 -8
  43. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  44. package/lib/module/components/input-group/index.js +5 -4
  45. package/lib/module/components/input-group/index.js.map +1 -1
  46. package/lib/module/components/item/index.js +9 -9
  47. package/lib/module/components/item/index.js.map +1 -1
  48. package/lib/module/components/label/index.js +4 -4
  49. package/lib/module/components/label/index.js.map +1 -1
  50. package/lib/module/components/line-chart/index.js +165 -184
  51. package/lib/module/components/line-chart/index.js.map +1 -1
  52. package/lib/module/components/loader/index.js +758 -0
  53. package/lib/module/components/loader/index.js.map +1 -0
  54. package/lib/module/components/map/basemap.js +265 -0
  55. package/lib/module/components/map/basemap.js.map +1 -0
  56. package/lib/module/components/map/index.js +850 -0
  57. package/lib/module/components/map/index.js.map +1 -0
  58. package/lib/module/components/map/maplibre.js +87 -0
  59. package/lib/module/components/map/maplibre.js.map +1 -0
  60. package/lib/module/components/marker/index.js +3 -3
  61. package/lib/module/components/marker/index.js.map +1 -1
  62. package/lib/module/components/message/index.js +15 -14
  63. package/lib/module/components/message/index.js.map +1 -1
  64. package/lib/module/components/message-scroller/index.js +5 -4
  65. package/lib/module/components/message-scroller/index.js.map +1 -1
  66. package/lib/module/components/otp-input/index.js +2 -2
  67. package/lib/module/components/popover/index.js +39 -7
  68. package/lib/module/components/popover/index.js.map +1 -1
  69. package/lib/module/components/progress/index.js +8 -2
  70. package/lib/module/components/progress/index.js.map +1 -1
  71. package/lib/module/components/radio-group/index.js +2 -2
  72. package/lib/module/components/radio-group/index.js.map +1 -1
  73. package/lib/module/components/ring-chart/index.js +423 -0
  74. package/lib/module/components/ring-chart/index.js.map +1 -0
  75. package/lib/module/components/section-rail/index.js +6 -6
  76. package/lib/module/components/section-rail/index.js.map +1 -1
  77. package/lib/module/components/select/index.js +10 -4
  78. package/lib/module/components/select/index.js.map +1 -1
  79. package/lib/module/components/separator/index.js +76 -22
  80. package/lib/module/components/separator/index.js.map +1 -1
  81. package/lib/module/components/shimmer/index.js +10 -1
  82. package/lib/module/components/shimmer/index.js.map +1 -1
  83. package/lib/module/components/slider/index.js +19 -5
  84. package/lib/module/components/slider/index.js.map +1 -1
  85. package/lib/module/components/steps/index.js +5 -5
  86. package/lib/module/components/steps/index.js.map +1 -1
  87. package/lib/module/components/switch/index.js +6 -1
  88. package/lib/module/components/switch/index.js.map +1 -1
  89. package/lib/module/components/tabs/index.js +7 -7
  90. package/lib/module/components/tabs/index.js.map +1 -1
  91. package/lib/module/components/timeline/index.js +4 -4
  92. package/lib/module/components/timeline/index.js.map +1 -1
  93. package/lib/module/components/toast/index.js +2 -2
  94. package/lib/module/components/toast/index.js.map +1 -1
  95. package/lib/module/components/toggle-button/index.js +5 -5
  96. package/lib/module/components/toggle-button/index.js.map +1 -1
  97. package/lib/module/components/tooltip/index.js +492 -0
  98. package/lib/module/components/tooltip/index.js.map +1 -0
  99. package/lib/module/components/typography/index.js +7 -2
  100. package/lib/module/components/typography/index.js.map +1 -1
  101. package/lib/module/hooks/index.js +1 -0
  102. package/lib/module/hooks/index.js.map +1 -1
  103. package/lib/module/hooks/use-back-handler.js +37 -0
  104. package/lib/module/hooks/use-back-handler.js.map +1 -0
  105. package/lib/module/hooks/use-direction.js +43 -0
  106. package/lib/module/hooks/use-direction.js.map +1 -0
  107. package/lib/module/icons/index.js +180 -2
  108. package/lib/module/icons/index.js.map +1 -1
  109. package/lib/module/index.js +12 -2
  110. package/lib/module/index.js.map +1 -1
  111. package/lib/module/primitives/keyboard-avoider.js +2 -1
  112. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  113. package/lib/module/primitives/text.js +55 -11
  114. package/lib/module/primitives/text.js.map +1 -1
  115. package/lib/module/utils/chart.js +279 -0
  116. package/lib/module/utils/chart.js.map +1 -0
  117. package/lib/module/utils/date.js +386 -0
  118. package/lib/module/utils/date.js.map +1 -0
  119. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  120. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  121. package/lib/typescript/src/components/area-chart/index.d.ts +237 -0
  122. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
  123. package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
  124. package/lib/typescript/src/components/bar-chart/index.d.ts +270 -0
  125. package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
  126. package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
  127. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  128. package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
  129. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
  130. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  131. package/lib/typescript/src/components/calendar/index.d.ts +108 -0
  132. package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
  133. package/lib/typescript/src/components/carousel/index.d.ts +179 -0
  134. package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
  135. package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
  136. package/lib/typescript/src/components/date-picker/index.d.ts +91 -0
  137. package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
  138. package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
  139. package/lib/typescript/src/components/direction/index.d.ts +11 -10
  140. package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  142. package/lib/typescript/src/components/field/index.d.ts.map +1 -1
  143. package/lib/typescript/src/components/frame/index.d.ts +17 -0
  144. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
  146. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  148. package/lib/typescript/src/components/item/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  150. package/lib/typescript/src/components/line-chart/index.d.ts +42 -2
  151. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  152. package/lib/typescript/src/components/loader/index.d.ts +38 -0
  153. package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
  154. package/lib/typescript/src/components/map/basemap.d.ts +76 -0
  155. package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
  156. package/lib/typescript/src/components/map/index.d.ts +367 -0
  157. package/lib/typescript/src/components/map/index.d.ts.map +1 -0
  158. package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
  159. package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
  160. package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/message/index.d.ts.map +1 -1
  162. package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
  163. package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
  164. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  165. package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
  166. package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
  167. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  168. package/lib/typescript/src/components/separator/index.d.ts +36 -1
  169. package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
  170. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  171. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  172. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  173. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  174. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  175. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  176. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  178. package/lib/typescript/src/components/tooltip/index.d.ts +125 -0
  179. package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
  180. package/lib/typescript/src/components/typography/index.d.ts +6 -0
  181. package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
  182. package/lib/typescript/src/hooks/index.d.ts +1 -0
  183. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  184. package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
  185. package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
  186. package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
  187. package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
  188. package/lib/typescript/src/icons/index.d.ts +8 -0
  189. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  190. package/lib/typescript/src/index.d.ts +14 -4
  191. package/lib/typescript/src/index.d.ts.map +1 -1
  192. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  193. package/lib/typescript/src/primitives/text.d.ts +20 -0
  194. package/lib/typescript/src/primitives/text.d.ts.map +1 -1
  195. package/lib/typescript/src/utils/chart.d.ts +96 -0
  196. package/lib/typescript/src/utils/chart.d.ts.map +1 -0
  197. package/lib/typescript/src/utils/date.d.ts +116 -0
  198. package/lib/typescript/src/utils/date.d.ts.map +1 -0
  199. package/package.json +9 -1
  200. package/src/components/accordion/index.tsx +8 -12
  201. package/src/components/alert/index.tsx +2 -2
  202. package/src/components/area-chart/index.tsx +1147 -0
  203. package/src/components/attachment/index.tsx +2 -2
  204. package/src/components/avatar/index.tsx +4 -4
  205. package/src/components/badge/index.tsx +6 -6
  206. package/src/components/bar-chart/index.tsx +1208 -0
  207. package/src/components/bottom-sheet/index.tsx +282 -21
  208. package/src/components/breadcrumb/index.tsx +363 -0
  209. package/src/components/button/index.tsx +12 -8
  210. package/src/components/calendar/index.tsx +703 -0
  211. package/src/components/carousel/index.tsx +788 -0
  212. package/src/components/chip/index.tsx +6 -8
  213. package/src/components/date-picker/index.tsx +237 -0
  214. package/src/components/dialog/index.tsx +7 -2
  215. package/src/components/direction/index.tsx +22 -22
  216. package/src/components/empty-state/index.tsx +2 -2
  217. package/src/components/field/index.tsx +3 -5
  218. package/src/components/flow/index.tsx +3 -3
  219. package/src/components/frame/index.tsx +22 -7
  220. package/src/components/heatmap-chart/index.tsx +58 -11
  221. package/src/components/input-group/index.tsx +5 -4
  222. package/src/components/item/index.tsx +9 -9
  223. package/src/components/label/index.tsx +2 -2
  224. package/src/components/line-chart/index.tsx +194 -223
  225. package/src/components/loader/index.tsx +812 -0
  226. package/src/components/map/basemap.ts +304 -0
  227. package/src/components/map/index.tsx +1116 -0
  228. package/src/components/map/maplibre.ts +201 -0
  229. package/src/components/marker/index.tsx +3 -3
  230. package/src/components/message/index.tsx +15 -14
  231. package/src/components/message-scroller/index.tsx +5 -4
  232. package/src/components/otp-input/index.tsx +2 -2
  233. package/src/components/popover/index.tsx +43 -7
  234. package/src/components/progress/index.tsx +10 -2
  235. package/src/components/radio-group/index.tsx +2 -2
  236. package/src/components/ring-chart/index.tsx +543 -0
  237. package/src/components/section-rail/index.tsx +6 -6
  238. package/src/components/select/index.tsx +10 -4
  239. package/src/components/separator/index.tsx +77 -19
  240. package/src/components/shimmer/index.tsx +10 -1
  241. package/src/components/slider/index.tsx +20 -5
  242. package/src/components/steps/index.tsx +5 -5
  243. package/src/components/switch/index.tsx +6 -1
  244. package/src/components/tabs/index.tsx +7 -9
  245. package/src/components/timeline/index.tsx +4 -4
  246. package/src/components/toast/index.tsx +2 -2
  247. package/src/components/toggle-button/index.tsx +5 -7
  248. package/src/components/tooltip/index.tsx +610 -0
  249. package/src/components/typography/index.tsx +7 -2
  250. package/src/hooks/index.ts +1 -0
  251. package/src/hooks/use-back-handler.ts +37 -0
  252. package/src/hooks/use-direction.ts +45 -0
  253. package/src/icons/index.tsx +111 -0
  254. package/src/index.ts +129 -0
  255. package/src/primitives/keyboard-avoider.tsx +2 -1
  256. package/src/primitives/text.tsx +53 -8
  257. package/src/utils/chart.ts +355 -0
  258. package/src/utils/date.ts +446 -0
@@ -28,7 +28,7 @@ import { createContext, forwardRef, useContext, type ReactNode } from 'react';
28
28
  import { View, type ViewProps } from 'react-native';
29
29
  import { tv, type VariantProps } from 'tailwind-variants';
30
30
  import { AnimatedPressable, type AnimatedPressableProps } from '../../primitives/animated-pressable';
31
- import { Text, type TextProps } from '../../primitives/text';
31
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
32
32
  import { cn } from '../../utils/cn';
33
33
 
34
34
  type ItemSize = 'default' | 'sm' | 'xs';
@@ -152,7 +152,7 @@ const ItemRoot = forwardRef<View, ItemProps>(
152
152
  className={root({ className })}
153
153
  {...(props as ViewProps)}
154
154
  >
155
- {children}
155
+ {textChildren(children)}
156
156
  </View>
157
157
  ) : (
158
158
  <AnimatedPressable
@@ -164,7 +164,7 @@ const ItemRoot = forwardRef<View, ItemProps>(
164
164
  className={root({ className })}
165
165
  {...props}
166
166
  >
167
- {children}
167
+ {textChildren(children)}
168
168
  </AnimatedPressable>
169
169
  );
170
170
 
@@ -200,7 +200,7 @@ const ItemGroup = forwardRef<View, ItemGroupProps>(
200
200
  )}
201
201
  {...props}
202
202
  >
203
- {children}
203
+ {textChildren(children)}
204
204
  </View>
205
205
  )
206
206
  );
@@ -246,7 +246,7 @@ const ItemMedia = forwardRef<View, ItemMediaProps>(
246
246
  className={mediaVariants({ variant, size: size ?? item.size, className })}
247
247
  {...props}
248
248
  >
249
- {children}
249
+ {textChildren(children)}
250
250
  </View>
251
251
  );
252
252
  }
@@ -277,7 +277,7 @@ const ItemContent = forwardRef<View, ItemContentProps>(
277
277
  )}
278
278
  {...props}
279
279
  >
280
- {children}
280
+ {textChildren(children)}
281
281
  </View>
282
282
  );
283
283
  }
@@ -326,7 +326,7 @@ const ItemActions = forwardRef<View, ItemActionsProps>(
326
326
  className={cn('shrink-0 flex-row items-center gap-1.5', className)}
327
327
  {...props}
328
328
  >
329
- {children}
329
+ {textChildren(children)}
330
330
  </View>
331
331
  )
332
332
  );
@@ -349,7 +349,7 @@ const ItemHeader = forwardRef<View, ItemHeaderProps>(
349
349
  className={cn('w-full flex-row items-center justify-between gap-2', className)}
350
350
  {...props}
351
351
  >
352
- {children}
352
+ {textChildren(children)}
353
353
  </View>
354
354
  )
355
355
  );
@@ -368,7 +368,7 @@ const ItemFooter = forwardRef<View, ItemFooterProps>(
368
368
  className={cn('w-full flex-row items-center gap-2', className)}
369
369
  {...props}
370
370
  >
371
- {children}
371
+ {textChildren(children)}
372
372
  </View>
373
373
  )
374
374
  );
@@ -16,7 +16,7 @@ import {
16
16
  } from 'react';
17
17
  import { View, type Text as RNText, type ViewProps } from 'react-native';
18
18
  import { tv } from 'tailwind-variants';
19
- import { Text, type TextProps } from '../../primitives/text';
19
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
20
20
 
21
21
  const labelVariants = tv({
22
22
  slots: {
@@ -86,7 +86,7 @@ const LabelRoot = forwardRef<View, LabelProps>(
86
86
  {...props}
87
87
  >
88
88
  {/* Bare strings are the common case — wrap them so callers don't have to. */}
89
- {typeof children === 'string' ? <LabelText>{children}</LabelText> : children}
89
+ {textChildren(children, (text) => <LabelText>{text}</LabelText>)}
90
90
  </View>
91
91
  </LabelContext.Provider>
92
92
  );
@@ -81,6 +81,16 @@ import Svg, {
81
81
  } from 'react-native-svg';
82
82
  import { useCSSVariable } from 'uniwind';
83
83
  import { Text } from '../../primitives/text';
84
+ import {
85
+ areaPath,
86
+ columnValues,
87
+ compactNumber,
88
+ linePath,
89
+ useSeriesColor,
90
+ xOf,
91
+ yOf,
92
+ type Plot,
93
+ } from '../../utils/chart';
84
94
  import { cn } from '../../utils/cn';
85
95
 
86
96
  const AnimatedPath = Animated.createAnimatedComponent(Path);
@@ -96,19 +106,12 @@ const PADDING = { top: 12, right: 10, bottom: 22, left: 10 };
96
106
  * stays a flat list of children instead of two nested slots the caller has to
97
107
  * remember the order of.
98
108
  */
99
- type Layer = 'svg' | 'overlay';
109
+ type Layer = 'svg' | 'overlay' | 'header';
100
110
 
101
111
  export type LineChartStatus = 'loading' | 'ready';
102
112
  export type LineChartCurve = 'monotone' | 'linear';
103
113
  export type LineChartDatum = Record<string, string | number | null | undefined>;
104
114
 
105
- interface Plot {
106
- width: number;
107
- height: number;
108
- left: number;
109
- top: number;
110
- }
111
-
112
115
  interface LineChartContextValue {
113
116
  data: LineChartDatum[];
114
117
  xDataKey: string;
@@ -385,39 +388,43 @@ const LineChartRoot = forwardRef<LineChartHandle, LineChartProps>(function LineC
385
388
  ]
386
389
  );
387
390
 
388
- const { svg, overlay } = partition(children);
391
+ const { svg, overlay, header } = partition(children);
389
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
+ */
390
399
  return (
391
400
  <LineChartContext.Provider value={context}>
392
- <View
393
- {...props}
394
- onLayout={onLayout}
395
- style={[{ aspectRatio }, props.style]}
396
- className={cn('w-full', className)}
397
- >
398
- {plot.width > 0 ? (
399
- <>
400
- <Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
401
- <Defs>
402
- {/*
403
- * One clip for everything in the plot. Sharing it is what makes
404
- * the reveal read as the chart arriving, rather than as three
405
- * separate things animating in at once.
406
- */}
407
- <ClipPath id={clipId}>
408
- <AnimatedRect
409
- x={plot.left}
410
- y={0}
411
- height={size.height}
412
- animatedProps={clipProps}
413
- />
414
- </ClipPath>
415
- </Defs>
416
- {svg}
417
- </Svg>
418
- {overlay}
419
- </>
420
- ) : 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>
421
428
  </View>
422
429
  </LineChartContext.Provider>
423
430
  );
@@ -428,18 +435,19 @@ LineChartRoot.displayName = 'LineChart';
428
435
  function partition(children: ReactNode) {
429
436
  const svg: ReactNode[] = [];
430
437
  const overlay: ReactNode[] = [];
438
+ const header: ReactNode[] = [];
431
439
 
432
440
  Children.forEach(children, (child, index) => {
433
441
  if (!isValidElement(child)) return;
434
442
  const layer = (child.type as { layer?: Layer }).layer ?? 'svg';
435
- (layer === 'overlay' ? overlay : svg).push(
443
+ (layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(
436
444
  // Children of a `Children.forEach` need keys of their own once they are
437
445
  // put into a new array.
438
446
  <ChildSlot key={index}>{child}</ChildSlot>
439
447
  );
440
448
  });
441
449
 
442
- return { svg, overlay };
450
+ return { svg, overlay, header };
443
451
  }
444
452
 
445
453
  /** Identity wrapper, purely so the partitioned arrays can carry keys. */
@@ -524,7 +532,7 @@ function LineChartLine({
524
532
  return () => unregisterSeries(dataKey);
525
533
  }, [dataKey, stroke, registerSeries, unregisterSeries]);
526
534
 
527
- const values = useMemo(() => numbersOf(data, dataKey), [data, dataKey]);
535
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
528
536
  const loading = status === 'loading';
529
537
 
530
538
  const animatedProps = useAnimatedProps(() => ({
@@ -609,7 +617,7 @@ function LineChartArea({ dataKey, color, colorIndex = 1, opacity = 0.18 }: LineC
609
617
  const fill = useSeriesColor(color, colorIndex);
610
618
  const gradientId = useRef(`panelui-area-${Math.random().toString(36).slice(2, 9)}`).current;
611
619
 
612
- const values = useMemo(() => numbersOf(data, dataKey), [data, dataKey]);
620
+ const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
613
621
  const loading = status === 'loading';
614
622
 
615
623
  const animatedProps = useAnimatedProps(() => ({
@@ -734,20 +742,44 @@ function LineChartXAxis({ ticks = 4, format, className }: LineChartXAxisProps) {
734
742
  }).filter((label): label is { key: number; text: string } => label !== null);
735
743
  }, [data, ticks, format, xDataKey]);
736
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
+ */
737
754
  return (
738
755
  <View
739
- pointerEvents="none"
740
- style={{
741
- position: 'absolute',
742
- left: plot.left,
743
- right: PADDING.right,
744
- bottom: 0,
745
- height: PADDING.bottom,
746
- }}
747
- className={cn('flex-row items-center justify-between', className)}
756
+ style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
757
+ className={cn(className)}
748
758
  >
749
759
  {labels.map((label) => (
750
- <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
+ >
751
783
  {label.text}
752
784
  </Text>
753
785
  ))}
@@ -893,7 +925,7 @@ function LineChartTooltip({ color, showLabel = true, formatValue, formatX }: Lin
893
925
  {series.map(([key, seriesColor]) => (
894
926
  <TooltipDot
895
927
  key={key}
896
- values={numbersOf(data, key)}
928
+ values={columnValues(data, key)}
897
929
  plot={plot}
898
930
  domainMin={domainMin}
899
931
  domainMax={domainMax}
@@ -952,14 +984,17 @@ LineChartTooltip.layer = 'overlay' as Layer;
952
984
 
953
985
  const LABEL_WIDTH = 112;
954
986
 
955
- /** A compact number for the crosshair label: 1204 → "1.2k", 24801 → "24.8k". */
956
- function compactNumber(value: number): string {
957
- const abs = Math.abs(value);
958
- if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
959
- if (abs >= 1_000) return `${(value / 1_000).toFixed(1)}k`;
960
- return `${Math.round(value)}`;
961
- }
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;
962
996
 
997
+ /** A compact number for the crosshair label: 1204 → "1.2k", 24801 → "24.8k". */
963
998
  const DOT = 9;
964
999
 
965
1000
  /** The dot riding one series under the crosshair. */
@@ -1022,20 +1057,16 @@ export interface LineChartLegendProps extends ViewProps {
1022
1057
  */
1023
1058
  function LineChartLegend({ className, labels, ...props }: LineChartLegendProps) {
1024
1059
  const { series } = useChart('LineChart.Legend');
1060
+ if (!series.length) return null;
1025
1061
 
1026
1062
  return (
1027
1063
  <View
1028
- pointerEvents="none"
1029
1064
  className={cn('absolute left-2.5 top-0 flex-row flex-wrap items-center gap-4', className)}
1030
1065
  {...props}
1066
+ style={[{ pointerEvents: 'none' }, props.style]}
1031
1067
  >
1032
1068
  {series.map(([key, color]) => (
1033
- <View key={key} className="flex-row items-center gap-1.5">
1034
- <View style={{ backgroundColor: color }} className="h-2 w-2 rounded-full" />
1035
- <Text size="xs" muted>
1036
- {labels?.[key] ?? key}
1037
- </Text>
1038
- </View>
1069
+ <SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
1039
1070
  ))}
1040
1071
  </View>
1041
1072
  );
@@ -1043,177 +1074,117 @@ function LineChartLegend({ className, labels, ...props }: LineChartLegendProps)
1043
1074
  LineChartLegend.displayName = 'LineChart.Legend';
1044
1075
  LineChartLegend.layer = 'overlay' as Layer;
1045
1076
 
1046
- /* -------------------------------------------------------------------------- */
1047
- /* Scales and paths */
1048
- /* -------------------------------------------------------------------------- */
1049
-
1050
- /** Only reached if the theme CSS was never imported. */
1051
- const FALLBACK_SERIES = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];
1052
-
1053
- /** Series colour: an explicit one, else the `--color-chart-*` token. */
1054
- function useSeriesColor(explicit: string | undefined, index: 1 | 2 | 3 | 4 | 5): string {
1055
- const token = useCSSVariable(`--color-chart-${index}`);
1056
- return explicit ?? (typeof token === 'string' ? token : FALLBACK_SERIES[index - 1]!);
1057
- }
1058
-
1059
- function numbersOf(data: LineChartDatum[], key: string): (number | null)[] {
1060
- return data.map((row) => {
1061
- const value = row[key];
1062
- return typeof value === 'number' && !Number.isNaN(value) ? value : null;
1063
- });
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
+ );
1064
1087
  }
1065
1088
 
1066
- function xOf(index: number, total: number, plot: Plot): number {
1067
- 'worklet';
1068
- if (total <= 1) return plot.left + plot.width / 2;
1069
- return plot.left + (plot.width * index) / (total - 1);
1070
- }
1089
+ /* -------------------------------------------------------------------------- */
1090
+ /* Header layer */
1091
+ /* -------------------------------------------------------------------------- */
1071
1092
 
1072
- function yOf(value: number, plot: Plot, min: number, max: number): number {
1073
- 'worklet';
1074
- const span = max - min || 1;
1075
- return plot.top + plot.height - ((value - min) / span) * plot.height;
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;
1076
1111
  }
1077
1112
 
1078
1113
  /**
1079
- * Monotone cubic tangents.
1114
+ * The strip above the plot: what the chart is of, what it currently reads, and
1115
+ * what the colours mean.
1080
1116
  *
1081
- * Written out rather than taken from a charting dependency, because it is forty
1082
- * lines and it has to run inside a worklet — the path is rebuilt on the UI
1083
- * thread on every frame the y-domain is tweening.
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.
1084
1121
  *
1085
- * Monotone is the right default for a time series. A plain cubic spline
1086
- * overshoots between points, so a series that never goes below zero draws a dip
1087
- * under the axis between two low values — a shape that is not in the data. This
1088
- * one cannot, because the tangent at each point is clamped against the slopes
1089
- * either side of it, and a local peak or trough is given a flat one.
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.
1090
1126
  */
1091
- function tangents(xs: number[], ys: number[]): number[] {
1092
- 'worklet';
1093
- const n = xs.length;
1094
- const slopes: number[] = [];
1095
- for (let i = 0; i < n - 1; i += 1) {
1096
- slopes.push((ys[i + 1]! - ys[i]!) / (xs[i + 1]! - xs[i]! || 1));
1097
- }
1098
-
1099
- const result: number[] = new Array(n).fill(0);
1100
- result[0] = slopes[0] ?? 0;
1101
- result[n - 1] = slopes[n - 2] ?? 0;
1102
-
1103
- for (let i = 1; i < n - 1; i += 1) {
1104
- const previous = slopes[i - 1]!;
1105
- const next = slopes[i]!;
1106
- result[i] = previous * next <= 0 ? 0 : (previous + next) / 2;
1107
- }
1108
-
1109
- // Fritsch–Carlson: pull any tangent back inside the circle of radius 3, which
1110
- // is the condition for the segment to stay monotone.
1111
- for (let i = 0; i < n - 1; i += 1) {
1112
- const slope = slopes[i]!;
1113
- if (slope === 0) {
1114
- result[i] = 0;
1115
- result[i + 1] = 0;
1116
- continue;
1117
- }
1118
- const a = result[i]! / slope;
1119
- const b = result[i + 1]! / slope;
1120
- const magnitude = Math.sqrt(a * a + b * b);
1121
- if (magnitude > 3) {
1122
- result[i] = (3 / magnitude) * a * slope;
1123
- result[i + 1] = (3 / magnitude) * b * slope;
1124
- }
1125
- }
1126
-
1127
- return result;
1128
- }
1129
-
1130
- /** Points, split at the gaps — a null breaks the series rather than crossing it. */
1131
- function runsOf(
1132
- values: (number | null)[],
1133
- plot: Plot,
1134
- min: number,
1135
- max: number
1136
- ): { x: number; y: number }[][] {
1137
- 'worklet';
1138
- const runs: { x: number; y: number }[][] = [];
1139
- let run: { x: number; y: number }[] = [];
1140
-
1141
- for (let i = 0; i < values.length; i += 1) {
1142
- const value = values[i];
1143
- if (value === null || value === undefined) {
1144
- if (run.length) runs.push(run);
1145
- run = [];
1146
- continue;
1147
- }
1148
- run.push({ x: xOf(i, values.length, plot), y: yOf(value, plot, min, max) });
1149
- }
1150
- if (run.length) runs.push(run);
1151
- return runs;
1152
- }
1153
-
1154
- function segment(points: { x: number; y: number }[], curve: LineChartCurve): string {
1155
- 'worklet';
1156
- if (!points.length) return '';
1157
- if (points.length < 3 || curve === 'linear') {
1158
- return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ');
1159
- }
1160
-
1161
- const xs = points.map((p) => p.x);
1162
- const ys = points.map((p) => p.y);
1163
- const ms = tangents(xs, ys);
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);
1164
1147
 
1165
- let d = `M${xs[0]},${ys[0]}`;
1166
- for (let i = 0; i < points.length - 1; i += 1) {
1167
- const dx = (xs[i + 1]! - xs[i]!) / 3;
1168
- d += ` C${xs[i]! + dx},${ys[i]! + ms[i]! * dx} ${xs[i + 1]! - dx},${ys[i + 1]! - ms[i + 1]! * dx} ${xs[i + 1]},${ys[i + 1]}`;
1169
- }
1170
- return d;
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
+ );
1171
1176
  }
1177
+ LineChartHeader.displayName = 'LineChart.Header';
1178
+ LineChartHeader.layer = 'header' as Layer;
1172
1179
 
1173
- function linePath(
1174
- values: (number | null)[],
1175
- plot: Plot,
1176
- min: number,
1177
- max: number,
1178
- curve: LineChartCurve,
1179
- loading: boolean
1180
- ): string {
1181
- 'worklet';
1182
- if (loading || plot.width <= 0) {
1183
- // Flat down the middle: the shape the skeleton holds, and the shape the
1184
- // real series grows out of once the data arrives.
1185
- const y = plot.top + plot.height / 2;
1186
- return `M${plot.left},${y} L${plot.left + plot.width},${y}`;
1187
- }
1188
- return runsOf(values, plot, min, max)
1189
- .map((run) => segment(run, curve))
1190
- .join(' ');
1191
- }
1180
+ /* -------------------------------------------------------------------------- */
1181
+ /* Scales and paths */
1182
+ /* -------------------------------------------------------------------------- */
1192
1183
 
1193
- function areaPath(
1194
- values: (number | null)[],
1195
- plot: Plot,
1196
- min: number,
1197
- max: number,
1198
- curve: LineChartCurve,
1199
- loading: boolean
1200
- ): string {
1201
- 'worklet';
1202
- if (loading || plot.width <= 0) return '';
1203
-
1204
- const baseline = plot.top + plot.height;
1205
- return runsOf(values, plot, min, max)
1206
- .map((run) => {
1207
- const top = segment(run, curve);
1208
- if (!top) return '';
1209
- const first = run[0]!;
1210
- const last = run[run.length - 1]!;
1211
- return `${top} L${last.x},${baseline} L${first.x},${baseline} Z`;
1212
- })
1213
- .join(' ');
1214
- }
1184
+ /** Only reached if the theme CSS was never imported. */
1215
1185
 
1216
1186
  export const LineChart = Object.assign(LineChartRoot, {
1187
+ Header: LineChartHeader,
1217
1188
  Grid: LineChartGrid,
1218
1189
  Area: LineChartArea,
1219
1190
  Line: LineChartLine,