@delacour/react-native-ui 0.1.0-alpha.3

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 (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +192 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +435 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +232 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +100 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +33 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +78 -0
  108. package/src/components/field/field.tsx +144 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +216 -0
  111. package/src/components/field/field.variants.ts +136 -0
  112. package/src/components/field/index.ts +21 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +64 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +256 -0
  146. package/src/components/pressable/pressable.variants.test.ts +99 -0
  147. package/src/components/pressable/pressable.variants.ts +63 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +247 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +581 -0
  159. package/src/components/radio/radio.variants.ts +272 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +48 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +161 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +804 -0
  202. package/src/components/slider/slider.variants.ts +607 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +286 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +672 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,210 @@
1
+ # Chart
2
+
3
+ Skia charts wearing the theme's five-colour series ramp — the skin over
4
+ [`@delacour/react-native-charts`](../../../../charts/AGENTS.md), which does the drawing and
5
+ knows nothing about tokens.
6
+
7
+ `import { Chart, PieChart } from "@delacour/react-native-ui/chart";`
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | --- | --- |
13
+ | `index.ts` | → `@delacour/react-native-ui/chart` |
14
+ | `chart.tsx` | The root, the child partition, and the single `Object.assign` |
15
+ | `chart.context.tsx` | The context, mounted twice — see below |
16
+ | `chart.types.ts` | `ChartConfig`, `ChartSeriesConfig`, `ChartResolvedSeries` |
17
+ | `chart.variants.ts` | The slotted `tv()`, the ramp, and every pure resolver |
18
+ | `chart.variants.test.ts` | Including the assertion that the ramp tokens exist |
19
+ | `use-chart-font.ts` | `--font-sans` into an `SkFont` |
20
+ | `use-chart-palette.ts` | The eight fixed `useThemeColor` slots a series list resolves through |
21
+ | `chart-grid.tsx`, `chart-line.tsx`, `chart-area.tsx` | Skia marks; `Chart.Area` takes a `stackId` |
22
+ | `chart-bar.tsx` | `Chart.Bar` — a spec the root collects, and a lone engine bar when reached unrouted |
23
+ | `chart-bars.tsx` | Internal `ChartBars`: every `Chart.Bar` drawn as one bar, group or stack, spliced in by the root |
24
+ | `chart-scatter.tsx`, `chart-candlestick.tsx` | Skia marks — dots per datum, and O/H/L/C candles in the sentiment tokens |
25
+ | `chart-x-axis.tsx`, `chart-y-axis.tsx` | Skia axis labels |
26
+ | `chart-legend.tsx`, `chart-tooltip.tsx` | React Native, around and over the canvas |
27
+ | `chart-tooltip-dot.tsx`, `chart-tooltip-x.tsx`, `chart-tooltip-y.tsx` | Skia cursor marks, named under the tooltip; `.X` is a band over bars |
28
+ | `pie-chart.tsx` | The second root: `PieChart`, its child partition, and its `Object.assign` |
29
+ | `pie-chart.context.tsx` | The pie's context, mounted twice the same way; `useChartSeries` in `chart.context.tsx` reads either |
30
+ | `pie-chart-slice.tsx`, `pie-chart-label.tsx` | Skia marks — the wedges with a hairline between them, and a label per wedge |
31
+ | `pie-chart-center.tsx`, `pie-chart-tooltip.tsx` | React Native, over the canvas — the donut hole's content and the tap readout |
32
+
33
+ ## Design
34
+
35
+ - **Skia's `<Canvas>` is a second React reconciler, so every theme lookup
36
+ happens in the root, above it.** A hook called inside the canvas has no
37
+ Uniwind provider above it and resolves nothing. `ChartRoot` calls
38
+ `useThemeColor` for the series ramp, the grid and the axis labels, and the
39
+ marks receive resolved strings as plain props. This is the single most
40
+ important thing to know before editing anything in this folder.
41
+
42
+ - **The context is mounted twice, from one value.** Once around the React
43
+ Native subtree — legend and tooltip — and once *inside* the canvas around the
44
+ marks. Context resolves by which reconciler renders the provider node, so a
45
+ provider placed among the canvas' children lands in the Skia tree and marks
46
+ below it read it normally. That is why no context bridge and no `its-fine`
47
+ dependency is needed.
48
+
49
+ - **A Skia mark takes values, never a `className`.** There is nothing for
50
+ `cn()` to merge on a canvas node and nothing Uniwind could compile. Only the
51
+ root, the legend and the tooltip accept one. Rule 7's `withUniwind` carve-out
52
+ is already spent on `icon.tsx`, and this component does not need a second: it
53
+ sizes the canvas by sizing the `View` around it.
54
+
55
+ - **The `config` is the chart.** It names each series, sets the draw order, and
56
+ assigns the ramp by position, so a call site writes `yKey="desktop"` and
57
+ never a colour. The shape is shadcn's deliberately — rule 11's principle,
58
+ that a theme written for a web app moves across as a copy rather than a
59
+ translation, extends past token names to the component's own API.
60
+
61
+ - **The ramp stops at five, and cycles.** `--chart-1` … `--chart-5` are what
62
+ shadcn's palette declares. A sixth token would not survive a pasted theme: a
63
+ designer copying a tweakcn palette supplies five, so a six-series chart would
64
+ draw five themed lines and one stranger — and one wrong-coloured line reads
65
+ as a bug in a way a slightly-off surface does not.
66
+
67
+ - **At most eight *distinct* tokens per render, and it throws past that.**
68
+ `useThemeColor` is a hook and cannot run in a loop over a list whose length
69
+ changes, so `useChartPalette` resolves a fixed eight slots and
70
+ `partitionChartColors` dedupes the tokens and pads to reach them. The ramp
71
+ dedupes, so a pie of twenty ramp slices needs five. Literal colours bypass
72
+ the hook entirely, so a chart of twenty literal-coloured series is fine.
73
+ Eight is an opinion — a categorical legend stops being readable well before
74
+ it — not a limit of the technique.
75
+
76
+ - **No new colour token.** The grid is `border`, which is what `Separator`
77
+ already paints a rule between regions with; axis labels are
78
+ `muted-foreground`, the colour every secondary label here uses; the tooltip
79
+ is `popover` on `border`. Rule 11 would require a `--chart-grid` to be
80
+ derived from a shadcn token anyway, and every borrowed palette would leave it
81
+ on this package's default.
82
+
83
+ - **One new size token trio**, `--spacing-chart-sm/md/lg`. A canvas has no
84
+ intrinsic height, and without a token every chart on a dashboard would be a
85
+ different one. A height rather than `aspect-video` because two charts at
86
+ different widths must still line up — and registering the names with
87
+ tailwind-merge is what lets a caller's `className="aspect-video h-auto"`
88
+ cleanly win when they want that instead.
89
+
90
+ - **The tooltip is a React Native view even though it floats over the canvas.**
91
+ It wants `popover`, `border`, the radius scale and the type scale, none of
92
+ which exist in Skia, and it is the part a caller most wants to restyle —
93
+ which a className cannot do to a Skia rounded rect. Axis labels are the
94
+ opposite call and stay in Skia: they move with the plot under a scrub, and an
95
+ overlaid React Native label would lag them by a frame.
96
+
97
+ - **The tooltip's position rides the UI thread; its text does not.**
98
+ `useAnimatedStyle` follows the finger with no bridge hop, while the contents
99
+ update through `scheduleOnRN` only when the nearest datum index changes —
100
+ which is a few times per drag, not once per frame. Text on a shared value
101
+ would mean an animated `TextInput` with a `value` prop pretending to be a
102
+ label.
103
+
104
+ - **The cursor marks are named under `Chart.Tooltip` but placed beside it.**
105
+ `Chart.Tooltip.Dot`, `.X` and `.Y` are one feature with the readout, so they
106
+ share its namespace — but the readout is a React Native view over the canvas
107
+ and the marks are drawn inside it. Nesting them
108
+ (`<Chart.Tooltip><Chart.Tooltip.Dot /></Chart.Tooltip>`) would put a canvas
109
+ node in a view's tree and draw nothing, with no error. They are siblings, and
110
+ `partitionChildren` routes them to opposite sides of the boundary.
111
+
112
+ - **The marks snap to the datum, because the readout names one.** A rule
113
+ tracking the raw touch while the label beside it snaps says two different
114
+ things about one gesture. `snappedX` exists on the scrub for exactly this —
115
+ the crosshair, the dot and the number all read the same row. `glide` opts
116
+ into the continuous curve position, and the doc says what it costs.
117
+
118
+ - **A dot rings itself in the chart's background.** A dot in the series colour
119
+ sitting on a line of that same colour is a slight thickening and nothing
120
+ more; the ring is what makes it a knob. That is why `surfaceColor` is
121
+ resolved in the root and carried on the context.
122
+
123
+ - **Bars group by being siblings, and stack by sharing a `stackId`.** That
124
+ is shadcn's surface, and it is the reason `Chart.Bar` does not draw itself:
125
+ a bar's width depends on how many share its step, and its base on what is
126
+ stacked under it, so the root collects every `Chart.Bar`, resolves them with
127
+ `resolveBarLayout`, and mounts one internal `ChartBars` where the first bar
128
+ stood. A `Chart.Bar` wrapped in a caller's own component never reaches that
129
+ pass — it renders a single engine bar in its own colour, so it degrades to
130
+ "not grouped" rather than to nothing, but it cannot group or stack.
131
+
132
+ - **One stack per chart.** Two `stackId`s, a stack beside a loose bar, or an
133
+ area stack beside a bar stack all throw by name. The engine's `stackKeys` is
134
+ one ordered list because the y domain has to cover one set of running
135
+ totals; a second stack would need a group of stacks, which nothing here
136
+ draws yet. Throwing beats drawing something that looks stacked and is not.
137
+
138
+ - **Bar corners follow `--radius`.** `--radius` × a per-size multiplier, the
139
+ way `Checkbox`'s fill does, so a theme that squares its buttons squares its
140
+ bars. Only `--radius` survives to runtime — the scale is `@theme inline` —
141
+ so `BAR_RADIUS_MULTIPLIER` restates the steps and a test pins it against
142
+ `tokens.css`. A stack rounds only its outermost segment, so a column reads
143
+ as one bar rather than a pile of pills. `rounded={false}` is square.
144
+
145
+ - **Candle sentiment is the top of the ramp.** A rising candle is `chart-1`
146
+ and a falling one `chart-2`, so a candlestick is painted from the same five
147
+ colours as every other chart and a pasted palette recolours it with them.
148
+ `success` and `destructive` were tried first and made the one chart on a
149
+ dashboard that ignored the ramp; a flat candle is `muted-foreground`. The root
150
+ resolves the three with three fixed hook calls, which is why the part takes
151
+ only literal overrides — a token given to a mark inside the canvas has
152
+ nothing to resolve it. The config names the close field alone, so the
153
+ legend says one price; the tooltip prints O/H/L/C, swatched by sentiment.
154
+
155
+ - **The band is `Chart.Tooltip.X`, not a new part.** Over bars or candles the
156
+ rule becomes a wash one `xStep` wide, snapped to the datum, at a twelfth of
157
+ the axis colour. It is the same question — where along x are you — with an
158
+ answer sized to the mark: a hairline through the middle of a bar says less
159
+ than the whole column lit. `band` flips it either way. A cursor dot skips a
160
+ series that is only a bar, because a bar has no curve to sit on and the band
161
+ already picks out its column.
162
+
163
+ - **Parts must be direct children of `<Chart>`.** The root partitions them by
164
+ component identity to decide what goes inside the canvas, what floats over
165
+ it and what sits under it — the technique `Spinner` uses on its own children.
166
+ An array from `.map()` is fine, since `Children.toArray` flattens it; a mark
167
+ wrapped in a caller's own component is not, because there is nothing to match
168
+ until it renders.
169
+
170
+ - **Pie is a second root, not a part.** A pie shares nothing with a cartesian
171
+ plot on screen — no axes, no grid, no scrub, no x — and only the palette,
172
+ the legend and the font underneath, so `<Chart><Chart.Pie /></Chart>` would
173
+ be a root whose every other part throws under it. `PieChart` reuses
174
+ `chartVariants` for its frame heights, `useChartPalette` for its colours and
175
+ `ChartLegend` verbatim, through `useChartSeries`, which reads whichever
176
+ context is above it. Its categories are its **rows**: the ramp walks the
177
+ data, and the `config` is an optional override keyed by a row's name.
178
+
179
+ - **A pie's colours dedupe before the hook.** Twenty slices walk the
180
+ five-token ramp, and `partitionChartColors` collects *distinct* tokens, so
181
+ that costs five of the eight slots rather than throwing at the ninth slice.
182
+ Rows are dropped for a negative or unreadable value **before** the engine
183
+ sees them, so the series, the values, the engine's slices and the legend all
184
+ agree on an index — a selection reports a position in the drawn order.
185
+
186
+ - **`PieChart.Center` is a React Native view because it wants the type
187
+ scale.** What sits in a donut's hole is a headline number and a caption,
188
+ which want `text-2xl`, `foreground`, `muted-foreground` and a className —
189
+ none of which a Skia `<Text>` has. It centres over the frame, which is the
190
+ circle's centre because the root gives the engine no padding. It is named
191
+ `Center` rather than `Inset` because the engine's `PieInset` is the hairline
192
+ between slices, and the two would otherwise share a name for two things.
193
+
194
+ - **The pie tooltip is selection-driven, not scrub-driven.** There is no x to
195
+ scrub along, so a tap selects a slice, the others dim, and the readout sits
196
+ just outside that slice's outer edge on its bisector — the position a
197
+ callout would point at. It needs no shared values: the selection changes a
198
+ few times a session, not once a frame. The slice geometry it reads is
199
+ resolved a second time in the root, with the engine's own pure functions on
200
+ the same frame, because the engine's context stops at the canvas boundary
201
+ and the tooltip lives outside it. A tap selects only when something would
202
+ show it — a `PieChart.Tooltip`, an `onSelect`, or a controlled
203
+ `selectedIndex` — so a pie with none of those takes no gesture at all.
204
+
205
+ - **`@delacour/react-native-charts` is an optional peer, not a dependency.** A dependency
206
+ may be nested, and two copies of the engine mean two chart contexts — so a
207
+ correctly-nested `<Chart.Line>` would throw "must be used inside a `<Chart>`"
208
+ from inside a `<Chart>`. It also peer-depends on Skia, so a dependency would
209
+ install a native module for someone who only ever imported
210
+ `@delacour/react-native-ui/button`.
@@ -0,0 +1,93 @@
1
+ import { ChartArea as EngineArea, useChartContext as useEngineChart } from "@delacour/react-native-charts";
2
+ import type { CurveType } from "@delacour/react-native-charts/core";
3
+ import type { ReactElement } from "react";
4
+ import { useSeriesColor } from "./chart.context";
5
+ import { resolveAreaFill } from "./chart.variants";
6
+
7
+ export type ChartAreaProps = {
8
+ yKey: string;
9
+ /**
10
+ * Stack this area on every sibling area that names the same id.
11
+ *
12
+ * A stacked area is a band from the top of the series below it to its
13
+ * own running total, so the topmost edge reads as the sum. One stack per
14
+ * chart, shared with the bars: a second id, or an area stack beside a bar
15
+ * stack, throws by name.
16
+ */
17
+ stackId?: string;
18
+ color?: string;
19
+ curve?: CurveType;
20
+ connectMissingData?: boolean;
21
+ /**
22
+ * Fade the fill out towards the baseline instead of painting it flat.
23
+ *
24
+ * On by default for a lone area: a flat fill at any readable opacity
25
+ * competes with the line it sits under. Off by default for a stacked band,
26
+ * which has to read as a distinct region — see `resolveAreaFill`.
27
+ */
28
+ gradient?: boolean;
29
+ /** Opacity of the fill — at its top when it fades, throughout when flat. */
30
+ opacity?: number;
31
+ };
32
+
33
+ /**
34
+ * The region under a series, in that series' colour.
35
+ *
36
+ * The fade is built from the resolved colour rather than from an opacity prop
37
+ * on the whole mark, so the top of the fill keeps the series' hue at reduced
38
+ * strength and the bottom reaches genuine transparency. Fading the node instead
39
+ * would leave a visible flat edge along the baseline.
40
+ *
41
+ * A stacked area reads its segments from the engine's context — the root
42
+ * stacked them in data space so the y domain covers the totals — and draws
43
+ * the band between each segment's base and its top. It is placed inside the
44
+ * canvas, which is why the engine's own context is in reach here. A band is
45
+ * painted flat: three fades over the full plot height blur into one wash,
46
+ * and nothing then says where one series ends and the next begins.
47
+ */
48
+ export function ChartArea({
49
+ yKey,
50
+ stackId,
51
+ color,
52
+ curve,
53
+ connectMissingData,
54
+ gradient: gradientProp,
55
+ opacity: opacityProp,
56
+ }: ChartAreaProps): ReactElement | null {
57
+ const resolved = useSeriesColor(yKey);
58
+ const { stacked } = useEngineChart();
59
+ const paint = color ?? resolved;
60
+ if (paint === undefined) return null;
61
+ const segments = stackId === undefined ? undefined : stacked[yKey];
62
+ const { gradient, opacity } = resolveAreaFill({
63
+ stacked: stackId !== undefined,
64
+ gradient: gradientProp,
65
+ opacity: opacityProp,
66
+ });
67
+
68
+ if (!gradient) {
69
+ return (
70
+ <EngineArea
71
+ color={paint}
72
+ connectMissingData={connectMissingData}
73
+ curve={curve}
74
+ opacity={opacity}
75
+ segments={segments}
76
+ yKey={yKey}
77
+ />
78
+ );
79
+ }
80
+
81
+ return (
82
+ <EngineArea
83
+ connectMissingData={connectMissingData}
84
+ curve={curve}
85
+ gradient={[paint, "transparent"]}
86
+ opacity={opacity}
87
+ segments={segments}
88
+ yKey={yKey}
89
+ />
90
+ );
91
+ }
92
+
93
+ ChartArea.displayName = "DelacourUI.Chart.Area";
@@ -0,0 +1,103 @@
1
+ import { type ChartBarLabels, ChartBar as EngineBar } from "@delacour/react-native-charts";
2
+ import type { ChartPoint, CornerRadii } from "@delacour/react-native-charts/core";
3
+ import type { ReactElement } from "react";
4
+ import { useChart, useSeriesColor } from "./chart.context";
5
+ import type { ChartDatum } from "./chart.types";
6
+
7
+ export type ChartBarProps = {
8
+ /** Which series to draw. Names a key of the chart's `config`. */
9
+ yKey: string;
10
+ /**
11
+ * Stack this bar on every sibling that names the same id.
12
+ *
13
+ * Sibling bars without one sit side by side within each step. One stack
14
+ * per chart: a second id, or a stack beside a loose bar, throws by name.
15
+ */
16
+ stackId?: string;
17
+ /** Overrides the series' colour from the config. A literal — the canvas can resolve no token. */
18
+ color?: string;
19
+ /**
20
+ * Round the value end. On by default, following `--radius` at the chart's
21
+ * size; `false` is square and a number is an exact radius in points.
22
+ */
23
+ rounded?: boolean | number;
24
+ /**
25
+ * Print each value against its bar.
26
+ *
27
+ * `true` prints the value as written; a function receives the value and
28
+ * its row and returns the text, `""` to skip that bar. Drawn in the axis
29
+ * colour with the axis font. A grouped or stacked chart draws no labels —
30
+ * there is no room between the bars for them to read.
31
+ */
32
+ labels?: boolean | ((value: number, row: ChartDatum) => string);
33
+ opacity?: number;
34
+ };
35
+
36
+ /**
37
+ * One bar per datum for one series, standing on zero.
38
+ *
39
+ * Placed as a direct child of `<Chart>`, it never renders itself: the root
40
+ * collects every `Chart.Bar`, lays them out together — grouped by being
41
+ * siblings, stacked by `stackId` — and draws them through one internal
42
+ * `Chart.Bars` in the first bar's place. That is what lets two bars know
43
+ * they share a step before either has rendered.
44
+ *
45
+ * Reached any other way — wrapped in a caller's own component, say — it
46
+ * draws a single engine bar in its series' colour. It degrades to "not
47
+ * grouped" rather than to nothing, but it cannot know its siblings, so a
48
+ * wrapped bar never groups or stacks. See AGENTS.md.
49
+ */
50
+ export function ChartBar({ yKey, color, rounded, labels, opacity }: ChartBarProps): ReactElement | null {
51
+ const { barRadius, axisColor, data } = useChart();
52
+ const resolved = useSeriesColor(yKey);
53
+ const paint = color ?? resolved;
54
+ if (paint === undefined) return null;
55
+
56
+ return (
57
+ <EngineBar
58
+ color={paint}
59
+ labels={resolveBarLabels(labels, axisColor, data)}
60
+ opacity={opacity}
61
+ roundedCorners={barCorners(barRadiusFor(rounded, barRadius))}
62
+ yKey={yKey}
63
+ />
64
+ );
65
+ }
66
+
67
+ ChartBar.displayName = "DelacourUI.Chart.Bar";
68
+
69
+ /** The value end's two corners at one radius. The base stays square. */
70
+ export function barCorners(radius: number): CornerRadii {
71
+ return { topLeft: radius, topRight: radius };
72
+ }
73
+
74
+ /**
75
+ * A bar's own `rounded` against the chart's resolved radius.
76
+ *
77
+ * The context already holds `--radius` × the size multiplier, so this only
78
+ * has to honour `false` and a number. `resolveBarRadius` does the same in
79
+ * the root, where `--radius` is readable; this runs where a part is not.
80
+ */
81
+ export function barRadiusFor(rounded: boolean | number | undefined, barRadius: number): number {
82
+ if (rounded === false) return 0;
83
+ if (typeof rounded === "number") return Number.isFinite(rounded) ? Math.max(0, rounded) : 0;
84
+ return barRadius;
85
+ }
86
+
87
+ /** The engine's label options for a `labels` prop, or nothing. */
88
+ export function resolveBarLabels(
89
+ labels: ChartBarProps["labels"],
90
+ color: string | undefined,
91
+ data: readonly ChartDatum[]
92
+ ): ChartBarLabels | undefined {
93
+ if (labels === undefined || labels === false || color === undefined) return undefined;
94
+ if (labels === true) return { color };
95
+ const format = labels;
96
+ return {
97
+ color,
98
+ formatLabel: (point: ChartPoint, index: number): string => {
99
+ const row = data[index];
100
+ return point.yValue === null || row === undefined ? "" : format(point.yValue, row);
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ type ChartBarStackSegmentInfo,
3
+ type ChartBarStackSegmentOptions,
4
+ ChartBar as EngineBar,
5
+ ChartBarGroup as EngineBarGroup,
6
+ ChartBarStack as EngineBarStack,
7
+ } from "@delacour/react-native-charts";
8
+ import { type ReactElement, useCallback, useMemo } from "react";
9
+ import { useChart } from "./chart.context";
10
+ import { barCorners, barRadiusFor, type ChartBarProps, resolveBarLabels } from "./chart-bar";
11
+
12
+ export type ChartBarsProps = {
13
+ /** Every `Chart.Bar`'s props, in placement order. */
14
+ readonly bars: readonly ChartBarProps[];
15
+ };
16
+
17
+ /**
18
+ * Every `Chart.Bar` of the chart, drawn as one arrangement.
19
+ *
20
+ * Internal. The root collects the `Chart.Bar` children, resolves them with
21
+ * `resolveBarLayout`, and mounts one of these where the first bar stood —
22
+ * a single engine bar, a group or a stack, depending on what it found. A
23
+ * bar cannot draw itself because it cannot see its siblings, and the width
24
+ * of every bar depends on how many share a step.
25
+ *
26
+ * Colours come from each bar's series; `rounded` and `opacity` are read
27
+ * from the first bar that sets them, because a group is one path per series
28
+ * but one radius for all — corners that differed across a group would read
29
+ * as a mistake. A stack rounds only its outermost segment, so the column
30
+ * reads as one bar; every inner segment is square where it meets the next.
31
+ */
32
+ export function ChartBars({ bars }: ChartBarsProps): ReactElement | null {
33
+ const { bars: layout, series, barRadius, axisColor, data } = useChart();
34
+
35
+ const radius = barRadiusFor(bars.find((bar) => bar.rounded !== undefined)?.rounded, barRadius);
36
+ const corners = useMemo(() => barCorners(radius), [radius]);
37
+ const opacity = bars.find((bar) => bar.opacity !== undefined)?.opacity;
38
+
39
+ const colors = useMemo(
40
+ () =>
41
+ layout.keys.map((key) => {
42
+ const bar = bars.find((entry) => entry.yKey === key);
43
+ return bar?.color ?? series.find((entry) => entry.key === key)?.color ?? "transparent";
44
+ }),
45
+ [layout.keys, bars, series]
46
+ );
47
+
48
+ const barOptions = useCallback(
49
+ (info: ChartBarStackSegmentInfo): ChartBarStackSegmentOptions => (info.isTop ? { roundedCorners: corners } : {}),
50
+ [corners]
51
+ );
52
+
53
+ const first = bars[0];
54
+ if (first === undefined || layout.mode === "none") return null;
55
+
56
+ if (layout.mode === "single") {
57
+ return (
58
+ <EngineBar
59
+ color={colors[0]}
60
+ labels={resolveBarLabels(first.labels, axisColor, data)}
61
+ opacity={opacity}
62
+ roundedCorners={corners}
63
+ yKey={first.yKey}
64
+ />
65
+ );
66
+ }
67
+
68
+ if (layout.mode === "grouped") {
69
+ return <EngineBarGroup colors={colors} opacity={opacity} roundedCorners={corners} yKeys={layout.keys} />;
70
+ }
71
+
72
+ return <EngineBarStack barOptions={barOptions} colors={colors} opacity={opacity} yKeys={layout.keys} />;
73
+ }
74
+
75
+ ChartBars.displayName = "DelacourUI.Chart.Bars";
@@ -0,0 +1,60 @@
1
+ import { ChartCandlestick as EngineCandlestick } from "@delacour/react-native-charts";
2
+ import type { ReactElement } from "react";
3
+ import { useChart } from "./chart.context";
4
+ import type { ChartCandleColors, ChartCandlestickKeys } from "./chart.types";
5
+
6
+ export type ChartCandlestickProps = {
7
+ /** The row field holding each price. Default to the conventional names. */
8
+ open?: string;
9
+ high?: string;
10
+ low?: string;
11
+ close?: string;
12
+ /**
13
+ * Overrides the sentiment colours. Literals only.
14
+ *
15
+ * The defaults are `chart-1`, `chart-2` and `muted-foreground`,
16
+ * resolved in the root; a token here would reach the canvas unresolved.
17
+ */
18
+ colors?: Partial<ChartCandleColors>;
19
+ /** The fraction of each step a body fills. Defaults to 0.6. */
20
+ bodyWidth?: number;
21
+ /** The wick's stroke width in points. Defaults to 1. */
22
+ wickWidth?: number;
23
+ opacity?: number;
24
+ };
25
+
26
+ /**
27
+ * Open-high-low-close candles, coloured by whether each closed up or down.
28
+ *
29
+ * The chart's `config` names the close field alone, so the legend and the
30
+ * tooltip name one price rather than four; the root reads the other three
31
+ * fields off this part and plots them for the engine. A rising candle is
32
+ * `chart-1` and a falling one `chart-2` — the top of the same ramp every
33
+ * other chart wears — and a flat candle is `muted-foreground`.
34
+ */
35
+ export function ChartCandlestick(props: ChartCandlestickProps): ReactElement {
36
+ const { candleColors } = useChart();
37
+ const { colors, bodyWidth, wickWidth, opacity } = props;
38
+
39
+ return (
40
+ <EngineCandlestick
41
+ candleColors={{ ...candleColors, ...colors }}
42
+ candleRatio={bodyWidth}
43
+ keys={candlestickKeysOf(props)}
44
+ opacity={opacity}
45
+ wickStrokeWidth={wickWidth}
46
+ />
47
+ );
48
+ }
49
+
50
+ ChartCandlestick.displayName = "DelacourUI.Chart.Candlestick";
51
+
52
+ /** The four field names, with the conventional defaults filled in. */
53
+ export function candlestickKeysOf(props: ChartCandlestickProps): ChartCandlestickKeys {
54
+ return {
55
+ open: props.open ?? "open",
56
+ high: props.high ?? "high",
57
+ low: props.low ?? "low",
58
+ close: props.close ?? "close",
59
+ };
60
+ }
@@ -0,0 +1,33 @@
1
+ import { ChartGrid as EngineGrid } from "@delacour/react-native-charts";
2
+ import type { GridAxis } from "@delacour/react-native-charts/core";
3
+ import type { ReactElement } from "react";
4
+ import { useChart } from "./chart.context";
5
+
6
+ export type ChartGridProps = {
7
+ /** Which ticks get a rule. Defaults to the y ticks alone. */
8
+ axis?: GridAxis;
9
+ /** Overrides the `border` token this paints with. */
10
+ color?: string;
11
+ lineWidth?: number;
12
+ opacity?: number;
13
+ /** Dash on/off lengths, in points. */
14
+ dash?: readonly [number, number];
15
+ };
16
+
17
+ /**
18
+ * A hairline rule at each tick.
19
+ *
20
+ * Painted with `border`, not a chart token of its own. A gridline is a rule
21
+ * between regions, which is what `Separator` already uses `border` for — and
22
+ * inventing a `--chart-grid` would be a token no pasted shadcn theme supplies,
23
+ * so every borrowed palette would leave the grid on this package's default.
24
+ */
25
+ export function ChartGrid({ axis, color, lineWidth, opacity, dash }: ChartGridProps): ReactElement | null {
26
+ const { gridColor } = useChart();
27
+ const paint = color ?? gridColor;
28
+ if (paint === undefined) return null;
29
+
30
+ return <EngineGrid axis={axis} color={paint} dash={dash} lineWidth={lineWidth} opacity={opacity} />;
31
+ }
32
+
33
+ ChartGrid.displayName = "DelacourUI.Chart.Grid";
@@ -0,0 +1,41 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { cn } from "../../lib/cn";
4
+ import { Text } from "../text";
5
+ import { useChartSeries } from "./chart.context";
6
+
7
+ export type ChartLegendProps = {
8
+ className?: string;
9
+ /** Show only these series, in this order. Defaults to all of them. */
10
+ keys?: readonly string[];
11
+ };
12
+
13
+ /**
14
+ * A swatch and a label per series, under the chart.
15
+ *
16
+ * Shared by `Chart` and `PieChart`: it reads whichever context is above it,
17
+ * since a legend row is a colour and a name under either root.
18
+ *
19
+ * A React Native view, not a Skia node, and the distinction is whether it
20
+ * moves *with* the canvas. A legend sits beside the plot and is static
21
+ * relative to it, so it can have the type scale, a colour token and a
22
+ * className — none of which exist on a canvas. Axis labels are the opposite
23
+ * case: they move under a pan and belong in Skia.
24
+ */
25
+ export function ChartLegend({ className, keys }: ChartLegendProps): ReactElement {
26
+ const { series, slots } = useChartSeries();
27
+ const shown = keys === undefined ? series : keys.flatMap((key) => series.filter((entry) => entry.key === key));
28
+
29
+ return (
30
+ <View className={cn(slots.legend(), className)}>
31
+ {shown.map((entry) => (
32
+ <View className={slots.legendItem()} key={entry.key}>
33
+ <View className={slots.legendSwatch()} style={{ backgroundColor: entry.color }} />
34
+ <Text className={slots.legendLabel()}>{entry.label}</Text>
35
+ </View>
36
+ ))}
37
+ </View>
38
+ );
39
+ }
40
+
41
+ ChartLegend.displayName = "DelacourUI.Chart.Legend";
@@ -0,0 +1,50 @@
1
+ import { ChartLine as EngineLine } from "@delacour/react-native-charts";
2
+ import type { CurveType } from "@delacour/react-native-charts/core";
3
+ import type { ReactElement } from "react";
4
+ import { useSeriesColor } from "./chart.context";
5
+
6
+ export type ChartLineProps = {
7
+ /** Which series to draw. Names a key of the chart's `config`. */
8
+ yKey: string;
9
+ /** Overrides the series' colour from the config. A token or a literal. */
10
+ color?: string;
11
+ strokeWidth?: number;
12
+ curve?: CurveType;
13
+ /** Draw straight through a gap instead of breaking the line. */
14
+ connectMissingData?: boolean;
15
+ opacity?: number;
16
+ };
17
+
18
+ /**
19
+ * A stroked line through one series, in that series' colour.
20
+ *
21
+ * A Skia mark, so it takes values and not classNames — there is nothing for
22
+ * `cn()` to merge on a canvas node and nothing Uniwind could compile. The
23
+ * colour comes from the chart's `config` by key, so a call site names the data
24
+ * and never the paint.
25
+ */
26
+ export function ChartLine({
27
+ yKey,
28
+ color,
29
+ strokeWidth = 2,
30
+ curve,
31
+ connectMissingData,
32
+ opacity,
33
+ }: ChartLineProps): ReactElement | null {
34
+ const resolved = useSeriesColor(yKey);
35
+ const paint = color ?? resolved;
36
+ if (paint === undefined) return null;
37
+
38
+ return (
39
+ <EngineLine
40
+ color={paint}
41
+ connectMissingData={connectMissingData}
42
+ curve={curve}
43
+ opacity={opacity}
44
+ strokeWidth={strokeWidth}
45
+ yKey={yKey}
46
+ />
47
+ );
48
+ }
49
+
50
+ ChartLine.displayName = "DelacourUI.Chart.Line";