@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,448 @@
1
+ import { CartesianChart, type ChartOrientation, type ScrubConfig, useChartScrub } from "@delacour/react-native-charts";
2
+ import { type CurveType, resolveXValues } from "@delacour/react-native-charts/core";
3
+ import { Children, isValidElement, type ReactElement, type ReactNode, useMemo, useState } from "react";
4
+ import type { LayoutChangeEvent } from "react-native";
5
+ import { View } from "react-native";
6
+ import { useCSSVariable } from "uniwind";
7
+ import { useThemeColor } from "../../hooks/use-theme-color";
8
+ import { type ChartContextValue, ChartProvider } from "./chart.context";
9
+ import type {
10
+ ChartAreaSpec,
11
+ ChartBarLayout,
12
+ ChartBarSpec,
13
+ ChartCandlestickKeys,
14
+ ChartConfig,
15
+ ChartDatum,
16
+ } from "./chart.types";
17
+ import {
18
+ CANDLE_SENTIMENT_TOKENS,
19
+ type ChartSize,
20
+ chartAxisFontSize,
21
+ chartTickCount,
22
+ chartVariants,
23
+ resolveBarLayout,
24
+ resolveBarRadius,
25
+ resolveCategoryTickCount,
26
+ resolveChartKeys,
27
+ resolveChartSeries,
28
+ resolveDomainDefaults,
29
+ resolveStackedAreaKeys,
30
+ resolveXValueFormat,
31
+ } from "./chart.variants";
32
+ import { ChartArea, type ChartAreaProps } from "./chart-area";
33
+ import { ChartBar, type ChartBarProps } from "./chart-bar";
34
+ import { ChartBars } from "./chart-bars";
35
+ import { ChartCandlestick, type ChartCandlestickProps, candlestickKeysOf } from "./chart-candlestick";
36
+ import { ChartGrid } from "./chart-grid";
37
+ import { ChartLegend } from "./chart-legend";
38
+ import { ChartLine, type ChartLineProps } from "./chart-line";
39
+ import { ChartScatter, type ChartScatterProps } from "./chart-scatter";
40
+ import { ChartTooltip } from "./chart-tooltip";
41
+ import { ChartTooltipDot } from "./chart-tooltip-dot";
42
+ import { ChartTooltipX } from "./chart-tooltip-x";
43
+ import { ChartTooltipY } from "./chart-tooltip-y";
44
+ import { ChartXAxis } from "./chart-x-axis";
45
+ import { ChartYAxis } from "./chart-y-axis";
46
+ import { useChartFont } from "./use-chart-font";
47
+ import { useChartPalette } from "./use-chart-palette";
48
+
49
+ export type ChartProps = {
50
+ /** The series, keyed by the data field each reads. Key order is draw order. */
51
+ config: ChartConfig;
52
+ data: readonly ChartDatum[];
53
+ /** The field every series is plotted against. */
54
+ xKey: string;
55
+ /** Show only these series. Defaults to every key of `config`. */
56
+ yKeys?: readonly string[];
57
+ size?: ChartSize;
58
+ /** The interpolator every mark uses unless it overrides it. */
59
+ curve?: CurveType;
60
+ /** Explicit y bounds. Either end may be omitted to keep the data's own. */
61
+ domain?: { y?: readonly [number | undefined, number | undefined] };
62
+ /** Pull zero into the y domain. Defaults on when the chart holds bars. */
63
+ includeZero?: boolean;
64
+ /**
65
+ * Categories along x (the default) or along y.
66
+ *
67
+ * `horizontal` swaps the axis roles, so bars grow rightward from a category
68
+ * axis on the left. `Chart.XAxis` and `Chart.YAxis` keep their names — they
69
+ * label the axis they sit beside, whichever field it now carries.
70
+ */
71
+ orientation?: ChartOrientation;
72
+ /** How the scrub coexists with a scrolling parent. Defaults to holding. */
73
+ scrubConfig?: ScrubConfig;
74
+ className?: string;
75
+ /** Named on the frame, so a capture flow or a test can find the plot. */
76
+ testID?: string;
77
+ children?: ReactNode;
78
+ };
79
+
80
+ /** Parts that draw into the canvas. Everything else is layered around it. */
81
+ const CANVAS_PARTS = new Set<unknown>([
82
+ ChartGrid,
83
+ ChartLine,
84
+ ChartArea,
85
+ ChartBar,
86
+ ChartScatter,
87
+ ChartCandlestick,
88
+ ChartXAxis,
89
+ ChartYAxis,
90
+ // The cursor marks are named under `Chart.Tooltip` but drawn in the canvas,
91
+ // not inside the view the tooltip renders.
92
+ ChartTooltipDot,
93
+ ChartTooltipX,
94
+ ChartTooltipY,
95
+ ]);
96
+
97
+ function ChartRoot({
98
+ config,
99
+ data,
100
+ xKey,
101
+ yKeys,
102
+ size = "md",
103
+ curve = "monotone",
104
+ domain,
105
+ includeZero,
106
+ orientation = "vertical",
107
+ scrubConfig,
108
+ className,
109
+ testID,
110
+ children,
111
+ }: ChartProps): ReactElement {
112
+ const slots = chartVariants({ size });
113
+ const [frame, setFrame] = useState({ width: 0, height: 0 });
114
+
115
+ const declared = useMemo(() => resolveChartSeries(config, yKeys), [config, yKeys]);
116
+ const { canvas, overlay, axes, bars, stackedAreaKeys, candlestick, pointKeys, scatter } = useMemo(
117
+ () => partitionChildren(children),
118
+ [children]
119
+ );
120
+
121
+ // Every theme lookup happens here, above the canvas. Skia's renderer is a
122
+ // second React reconciler with no Uniwind provider in it, so a hook called
123
+ // inside would resolve nothing — the marks receive resolved strings instead.
124
+ // The series palette goes through `useChartPalette`, which holds the eight
125
+ // fixed `useThemeColor` slots the ramp resolves into.
126
+ const series = useChartPalette(declared);
127
+ const gridColor = useThemeColor("border");
128
+ const axisColor = useThemeColor("muted-foreground");
129
+ const surfaceColor = useThemeColor("background");
130
+
131
+ // The three candle sentiments, as three fixed calls — the same reason the
132
+ // palette hook writes its eight out. Only literal overrides are accepted
133
+ // on the part, so nothing else has to be resolved here.
134
+ const candlePositive = useThemeColor(CANDLE_SENTIMENT_TOKENS.positive);
135
+ const candleNegative = useThemeColor(CANDLE_SENTIMENT_TOKENS.negative);
136
+ const candleNeutral = useThemeColor(CANDLE_SENTIMENT_TOKENS.neutral);
137
+ const candleColors = useMemo(
138
+ () => ({
139
+ positive: candlePositive ?? CANDLE_SENTIMENT_TOKENS.positive,
140
+ negative: candleNegative ?? CANDLE_SENTIMENT_TOKENS.negative,
141
+ neutral: candleNeutral ?? CANDLE_SENTIMENT_TOKENS.neutral,
142
+ }),
143
+ [candlePositive, candleNegative, candleNeutral]
144
+ );
145
+
146
+ // `--radius` is the only step of the corner scale that survives to runtime,
147
+ // so a bar's corner is computed from the base — see `resolveBarRadius`.
148
+ const radius = (useCSSVariable("--radius") as number | undefined) ?? 0;
149
+ const barRadius = resolveBarRadius(size, radius);
150
+
151
+ // The engine plots every series plus the candle's other three fields; the
152
+ // scrub follows the same list so a candle key has a value to read.
153
+ const keys = useMemo(
154
+ () =>
155
+ resolveChartKeys(
156
+ series.map((entry) => entry.key),
157
+ candlestick
158
+ ),
159
+ [series, candlestick]
160
+ );
161
+ const stackKeys = bars.mode === "stacked" ? bars.keys : stackedAreaKeys;
162
+ const scrub = useChartScrub(keys);
163
+ const font = useChartFont(chartAxisFontSize(size));
164
+
165
+ const formatXValue = useMemo(() => resolveXValueFormat(data, xKey), [data, xKey]);
166
+ // Only a categorical x has one label per row to protect; a numeric or a
167
+ // time axis is a scale, and its ticks stay a budget.
168
+ const categoryCount = useMemo(() => (resolveXValues(data, xKey).isCategorical ? data.length : 0), [data, xKey]);
169
+
170
+ const value = useMemo<ChartContextValue>(
171
+ () => ({
172
+ series,
173
+ data,
174
+ xKey,
175
+ formatXValue,
176
+ size,
177
+ slots,
178
+ gridColor,
179
+ axisColor,
180
+ surfaceColor,
181
+ scrub,
182
+ frame,
183
+ bars,
184
+ barRadius,
185
+ candlestick,
186
+ candleColors,
187
+ pointKeys,
188
+ orientation,
189
+ }),
190
+ [
191
+ series,
192
+ data,
193
+ xKey,
194
+ formatXValue,
195
+ size,
196
+ slots,
197
+ gridColor,
198
+ axisColor,
199
+ surfaceColor,
200
+ scrub,
201
+ frame,
202
+ bars,
203
+ barRadius,
204
+ candlestick,
205
+ candleColors,
206
+ pointKeys,
207
+ orientation,
208
+ ]
209
+ );
210
+
211
+ const onLayout = (event: LayoutChangeEvent): void => {
212
+ const { width, height } = event.nativeEvent.layout;
213
+ setFrame((current) => (current.width === width && current.height === height ? current : { width, height }));
214
+ };
215
+
216
+ // A bar or a candle owns a band; the tooltip's band and the category tick
217
+ // count key on that. What the domain needs — zero pulled in for bars, the
218
+ // data's own extent for candles, half a step of x for anything centred on
219
+ // its datum — is `resolveDomainDefaults`' call.
220
+ const hasBands = bars.mode !== "none" || candlestick !== null;
221
+ const domainDefaults = resolveDomainDefaults({ bars, candlestick, scatter, includeZero, domain });
222
+
223
+ // The category axis labels every bar; the value axis keeps the size's
224
+ // budget. Which axis is which follows the orientation — the engine puts
225
+ // the categories on y when horizontal.
226
+ const tickCount = chartTickCount(size);
227
+ const categoryTickCount = resolveCategoryTickCount(size, categoryCount, hasBands);
228
+ const xTickCount = orientation === "horizontal" ? tickCount : categoryTickCount;
229
+ const yTickCount = orientation === "horizontal" ? categoryTickCount : tickCount;
230
+
231
+ return (
232
+ <ChartProvider value={value}>
233
+ <View className={slots.root({ className })}>
234
+ <View className={slots.frame()} onLayout={onLayout} testID={testID}>
235
+ <CartesianChart
236
+ curve={curve}
237
+ data={data}
238
+ domain={domain}
239
+ domainPadding={domainDefaults.domainPadding}
240
+ font={font}
241
+ includeZero={domainDefaults.includeZero}
242
+ niceDomain
243
+ orientation={orientation}
244
+ scrub={scrub}
245
+ scrubConfig={scrubConfig}
246
+ stackKeys={stackKeys.length === 0 ? undefined : stackKeys}
247
+ xAxis={{ show: axes.x, tickCount: xTickCount }}
248
+ xKey={xKey}
249
+ yAxis={{ show: axes.y, tickCount: yTickCount }}
250
+ yKeys={keys}
251
+ >
252
+ <ChartProvider value={value}>{canvas}</ChartProvider>
253
+ </CartesianChart>
254
+ {overlay.canvasOverlay}
255
+ </View>
256
+ {overlay.below}
257
+ </View>
258
+ </ChartProvider>
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Splits the children by where they have to be mounted.
264
+ *
265
+ * Marks go inside the Skia canvas; a tooltip is absolutely positioned over it;
266
+ * a legend sits under it. The caller writes all three as siblings because that
267
+ * is how a chart reads, and the root puts each where it belongs.
268
+ *
269
+ * It also reports which axes were placed, and that is not bookkeeping: an axis
270
+ * gutter is reserved from the *measured width of its labels*, so a chart with
271
+ * no `Chart.YAxis` would otherwise inset its plot by the width of labels it
272
+ * never draws. On a chart inside a card that reads as the plot failing to line
273
+ * up with the text above it, with nothing on screen to explain the gap.
274
+ *
275
+ * The bars are the other thing it has to see whole. A `Chart.Bar` cannot
276
+ * know how many siblings share its step, so every bar's props are collected,
277
+ * resolved into one layout, and drawn by a single internal `ChartBars`
278
+ * spliced in where the first bar stood — the later ones are dropped from the
279
+ * canvas list. Stacked areas and the candlestick's field names are read the
280
+ * same way, because the engine's `stackKeys` and `yKeys` are root props.
281
+ *
282
+ * Matching is by component identity, the same technique `Spinner` uses on its
283
+ * own children. The limit that comes with it: a part has to be a **direct**
284
+ * child of `<Chart>`. An array from `.map()` is fine — `Children.toArray`
285
+ * flattens it — but a mark wrapped in a caller's own component is not, because
286
+ * there is nothing to match against until it renders.
287
+ */
288
+ function partitionChildren(children: ReactNode): ChartPartition {
289
+ const collected: PartitionState = {
290
+ canvas: [],
291
+ canvasOverlay: [],
292
+ below: [],
293
+ axes: { x: false, y: false },
294
+ barProps: [],
295
+ areaSpecs: [],
296
+ pointKeys: [],
297
+ candlestick: null,
298
+ barsAt: -1,
299
+ scatter: false,
300
+ };
301
+
302
+ for (const child of Children.toArray(children)) {
303
+ if (isValidElement(child)) collectChild(child, collected);
304
+ }
305
+
306
+ const { canvas, canvasOverlay, below, axes, barProps, areaSpecs, pointKeys, candlestick, barsAt, scatter } =
307
+ collected;
308
+ const bars = resolveBarLayout(barProps.map(barSpecOf));
309
+ if (barsAt !== -1) canvas.splice(barsAt, 0, <ChartBars bars={barProps} key="bars" />);
310
+
311
+ return {
312
+ canvas,
313
+ overlay: { canvasOverlay, below },
314
+ axes,
315
+ bars,
316
+ stackedAreaKeys: resolveStackedAreaKeys(areaSpecs, bars),
317
+ candlestick,
318
+ pointKeys,
319
+ scatter,
320
+ };
321
+ }
322
+
323
+ type ChartPartition = {
324
+ canvas: ReactNode[];
325
+ overlay: { canvasOverlay: ReactNode[]; below: ReactNode[] };
326
+ axes: { x: boolean; y: boolean };
327
+ bars: ChartBarLayout;
328
+ stackedAreaKeys: readonly string[];
329
+ candlestick: ChartCandlestickKeys | null;
330
+ pointKeys: readonly string[];
331
+ /** Whether any `Chart.Scatter` is placed — its outermost dots need the x padding bars get. */
332
+ scatter: boolean;
333
+ };
334
+
335
+ type PartitionState = {
336
+ canvas: ReactNode[];
337
+ canvasOverlay: ReactNode[];
338
+ below: ReactNode[];
339
+ axes: { x: boolean; y: boolean };
340
+ barProps: ChartBarProps[];
341
+ areaSpecs: ChartAreaSpec[];
342
+ pointKeys: string[];
343
+ candlestick: ChartCandlestickKeys | null;
344
+ barsAt: number;
345
+ scatter: boolean;
346
+ };
347
+
348
+ /** Files one child by identity. A bar is recorded and not placed — `ChartBars` stands in for all of them. */
349
+ function collectChild(child: ReactElement, state: PartitionState): void {
350
+ if (child.type === ChartXAxis) state.axes.x = true;
351
+ if (child.type === ChartYAxis) state.axes.y = true;
352
+
353
+ if (child.type === ChartBar) {
354
+ state.barProps.push(child.props as ChartBarProps);
355
+ if (state.barsAt === -1) state.barsAt = state.canvas.length;
356
+ return;
357
+ }
358
+ if (child.type === ChartArea) state.areaSpecs.push(child.props as ChartAreaProps);
359
+ if (child.type === ChartCandlestick && state.candlestick === null) {
360
+ state.candlestick = candlestickKeysOf(child.props as ChartCandlestickProps);
361
+ }
362
+ if (child.type === ChartScatter) state.scatter = true;
363
+ if (child.type === ChartLine || child.type === ChartArea || child.type === ChartScatter) {
364
+ state.pointKeys.push((child.props as ChartLineProps | ChartAreaProps | ChartScatterProps).yKey);
365
+ }
366
+
367
+ if (CANVAS_PARTS.has(child.type)) state.canvas.push(child);
368
+ else if (child.type === ChartTooltip) state.canvasOverlay.push(child);
369
+ else state.below.push(child);
370
+ }
371
+
372
+ /** The part of a bar's props the layout reads. */
373
+ function barSpecOf(props: ChartBarProps): ChartBarSpec {
374
+ return props.stackId === undefined ? { yKey: props.yKey } : { yKey: props.yKey, stackId: props.stackId };
375
+ }
376
+
377
+ /**
378
+ * Skia charts, wearing the theme's five-colour series ramp.
379
+ *
380
+ * The `config` is the chart: it names each series, decides the draw order, and
381
+ * assigns the ramp by position, so `<Chart.Line yKey="desktop" />` names the
382
+ * data and never the paint. Its shape is shadcn's on purpose — a chart config
383
+ * written for a web dashboard moves across as a copy rather than a translation.
384
+ *
385
+ * Parts are placed, not configured. `Chart.Grid`, `Chart.Line`, `Chart.Area`,
386
+ * `Chart.Bar`, `Chart.Scatter`, `Chart.Candlestick`, `Chart.XAxis` and
387
+ * `Chart.YAxis` draw into the canvas; `Chart.Tooltip` floats over it and
388
+ * `Chart.Legend` sits beneath, both as ordinary React Native views so they can
389
+ * carry the type scale and a colour token. Sibling bars group; bars sharing a
390
+ * `stackId` stack, and so do areas.
391
+ *
392
+ * A colour may be a theme token or a literal — `lib/color`'s `isLiteralColor`
393
+ * tells them apart, so `color: "#EC4899"` works beside `color: "chart-4"` with
394
+ * no special-casing at the call site. At most eight *distinct* tokens can be
395
+ * resolved in one render; past that, give the extras literals.
396
+ *
397
+ * Touch is opt-in through the parts: without a `Chart.Tooltip` the chart takes
398
+ * a scrub but nothing shows it. The scrub activates on a hold by default, so a
399
+ * chart inside a scrolling list does not steal the scroll.
400
+ *
401
+ * @example
402
+ * <Chart config={{ revenue: { label: "Revenue" } }} data={rows} xKey="month">
403
+ * <Chart.Grid />
404
+ * <Chart.YAxis />
405
+ * <Chart.XAxis />
406
+ * <Chart.Area yKey="revenue" />
407
+ * <Chart.Line yKey="revenue" />
408
+ * </Chart>
409
+ *
410
+ * @example
411
+ * <Chart config={config} data={rows} size="lg" xKey="month">
412
+ * <Chart.Line yKey="desktop" />
413
+ * <Chart.Line yKey="mobile" />
414
+ * <Chart.Tooltip />
415
+ * <Chart.Legend />
416
+ * </Chart>
417
+ *
418
+ * @example
419
+ * <Chart config={config} data={rows} xKey="month">
420
+ * <Chart.Bar stackId="traffic" yKey="desktop" />
421
+ * <Chart.Bar stackId="traffic" yKey="mobile" />
422
+ * <Chart.Tooltip.X />
423
+ * <Chart.Tooltip />
424
+ * </Chart>
425
+ */
426
+ export const Chart = Object.assign(ChartRoot, {
427
+ /** A hairline rule at each tick, painted with `border`. Draws first, under every mark. */
428
+ Grid: ChartGrid,
429
+ /** A stroked line through one series, in that series' colour. */
430
+ Line: ChartLine,
431
+ /** The region under one series, fading out towards the baseline. `stackId` stacks it. */
432
+ Area: ChartArea,
433
+ /** One bar per datum. Siblings group; a shared `stackId` stacks. */
434
+ Bar: ChartBar,
435
+ /** One dot per datum, in the series' colour. */
436
+ Scatter: ChartScatter,
437
+ /** Open-high-low-close candles in `chart-1`, `chart-2` and `muted-foreground`. */
438
+ Candlestick: ChartCandlestick,
439
+ /** Tick labels below the plot, in `muted-foreground`. */
440
+ XAxis: ChartXAxis,
441
+ /** Tick labels beside the plot, right-aligned into a column. */
442
+ YAxis: ChartYAxis,
443
+ /** A floating readout that follows the scrub. A React Native view, over the canvas. */
444
+ Tooltip: ChartTooltip,
445
+ /** A swatch and a label per series, under the chart. */
446
+ Legend: ChartLegend,
447
+ displayName: "DelacourUI.Chart",
448
+ });
@@ -0,0 +1,108 @@
1
+ import type { ChartRow, Sentiment } from "@delacour/react-native-charts/core";
2
+
3
+ /** One series' name and colour. */
4
+ export type ChartSeriesConfig = {
5
+ /** Shown in the legend and the tooltip. */
6
+ label: string;
7
+ /**
8
+ * A theme token (`chart-3`, `primary`) or a literal (`#EC4899`).
9
+ *
10
+ * Defaults to this series' position on the ramp, so the first series in the
11
+ * config is `chart-1` and nothing has to be named at all.
12
+ */
13
+ color?: string;
14
+ };
15
+
16
+ /**
17
+ * The chart's series, keyed by the data field each reads.
18
+ *
19
+ * Key order is draw order and ramp order both, so reordering the object
20
+ * reorders the chart. This is shadcn's shape deliberately — a config written
21
+ * for a web dashboard moves across as a copy rather than a translation.
22
+ */
23
+ export type ChartConfig = Record<string, ChartSeriesConfig>;
24
+
25
+ /** A series after its colour has been decided and resolved. */
26
+ export type ChartResolvedSeries = {
27
+ readonly key: string;
28
+ readonly label: string;
29
+ /** A colour value by the time a mark sees it, never a token name. */
30
+ readonly color: string;
31
+ };
32
+
33
+ /** A row of the caller's data. */
34
+ export type ChartDatum = ChartRow;
35
+
36
+ /** What `chartTooltipOffset` needs to place a tooltip inside its frame. */
37
+ export type ChartTooltipInput = {
38
+ /** The cursor, in frame coordinates. */
39
+ readonly x: number;
40
+ readonly y: number;
41
+ readonly width: number;
42
+ readonly height: number;
43
+ readonly frameWidth: number;
44
+ readonly frameHeight: number;
45
+ /** Space between the cursor and the tooltip. Defaults to 12. */
46
+ readonly gap?: number;
47
+ };
48
+
49
+ /** What one `Chart.Bar` declares about where it stands, before any bar is laid out. */
50
+ export type ChartBarSpec = {
51
+ readonly yKey: string;
52
+ /** Bars sharing a stack id stack on one another; bars without one group side by side. */
53
+ readonly stackId?: string;
54
+ };
55
+
56
+ export type ChartBarLayoutMode = "none" | "single" | "grouped" | "stacked";
57
+
58
+ /** Where one bar series sits in the layout. */
59
+ export type ChartBarSlot = {
60
+ /** Its position among the side-by-side bars of one step. Zero in a stack. */
61
+ readonly groupIndex: number;
62
+ /** How many bars share one step. One in a stack. */
63
+ readonly groupCount: number;
64
+ /** Its position from the bottom of the stack. Absent unless stacked. */
65
+ readonly stackIndex?: number;
66
+ };
67
+
68
+ /**
69
+ * Every `Chart.Bar` in the chart, resolved into one arrangement.
70
+ *
71
+ * Bars group by being siblings and stack by sharing a `stackId`, so the root
72
+ * has to see them all before any of them can know its width or its base. One
73
+ * stack per chart: the layout is either all stacked or all side by side.
74
+ */
75
+ export type ChartBarLayout = {
76
+ readonly mode: ChartBarLayoutMode;
77
+ /** The bar series, in the order they were placed — bottom first for a stack. */
78
+ readonly keys: readonly string[];
79
+ readonly stackId?: string;
80
+ readonly slotOf: Readonly<Record<string, ChartBarSlot>>;
81
+ };
82
+
83
+ /** The four fields a candlestick reads from each row. */
84
+ export type ChartCandlestickKeys = {
85
+ readonly open: string;
86
+ readonly high: string;
87
+ readonly low: string;
88
+ readonly close: string;
89
+ };
90
+
91
+ /** One resolved colour per candle sentiment. */
92
+ export type ChartCandleColors = Readonly<Record<Sentiment, string>>;
93
+
94
+ /** What one `Chart.Area` declares about stacking. */
95
+ export type ChartAreaSpec = {
96
+ readonly yKey: string;
97
+ readonly stackId?: string;
98
+ };
99
+
100
+ /** One line of the tooltip's readout. */
101
+ export type ChartTooltipRow = {
102
+ readonly key: string;
103
+ readonly label: string;
104
+ /** Resolved, or `undefined` when the theme emits nothing for it. */
105
+ readonly color: string | undefined;
106
+ /** The row's value as written, for the caller's formatter. */
107
+ readonly value: unknown;
108
+ };