@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,103 @@
1
+ export type { CurveType } from "@delacour/react-native-charts/core";
2
+ export { Chart, type ChartProps } from "./chart";
3
+ export {
4
+ ChartContext,
5
+ type ChartContextValue,
6
+ ChartProvider,
7
+ type ChartSlots,
8
+ useChart,
9
+ useChartContext,
10
+ useChartSeries,
11
+ useSeriesColor,
12
+ } from "./chart.context";
13
+ export type {
14
+ ChartAreaSpec,
15
+ ChartBarLayout,
16
+ ChartBarLayoutMode,
17
+ ChartBarSlot,
18
+ ChartBarSpec,
19
+ ChartCandleColors,
20
+ ChartCandlestickKeys,
21
+ ChartConfig,
22
+ ChartDatum,
23
+ ChartResolvedSeries,
24
+ ChartSeriesConfig,
25
+ ChartTooltipInput,
26
+ ChartTooltipRow,
27
+ } from "./chart.types";
28
+ export {
29
+ AREA_FILL_OPACITY,
30
+ AREA_STACKED_FILL_OPACITY,
31
+ type AreaFill,
32
+ type AreaFillInput,
33
+ applyChartColors,
34
+ BAR_RADIUS_MULTIPLIER,
35
+ BAR_RADIUS_STEP,
36
+ CANDLE_SENTIMENT_TOKENS,
37
+ CANDLE_Y_PADDING,
38
+ CATEGORY_TICK_CAP,
39
+ CHART_CURVES,
40
+ CHART_MAX_TOKEN_SERIES,
41
+ CHART_SERIES_TOKENS,
42
+ CHART_SIZES,
43
+ type ChartColorPartition,
44
+ type ChartCurve,
45
+ type ChartSeriesToken,
46
+ type ChartSize,
47
+ type ChartTooltipCandle,
48
+ type ChartVariantProps,
49
+ chartAxisFontSize,
50
+ chartSeriesToken,
51
+ chartTickCount,
52
+ chartTooltipOffset,
53
+ chartVariants,
54
+ type DomainDefaults,
55
+ type DomainDefaultsInput,
56
+ EDGE_X_PADDING,
57
+ PIE_DEFAULT_INNER_RADIUS,
58
+ type PieLabelFormat,
59
+ type PieLabelSlice,
60
+ type PieResolvedSeries,
61
+ partitionChartColors,
62
+ pieInnerRadiusSpec,
63
+ pieLabelText,
64
+ pieSlicePercent,
65
+ resolveAreaFill,
66
+ resolveBarLayout,
67
+ resolveBarRadius,
68
+ resolveCategoryTickCount,
69
+ resolveChartKeys,
70
+ resolveChartSeries,
71
+ resolveDomainDefaults,
72
+ resolvePieSeries,
73
+ resolveStackedAreaKeys,
74
+ resolveTooltipRows,
75
+ resolveXValueFormat,
76
+ } from "./chart.variants";
77
+ export type { ChartAreaProps } from "./chart-area";
78
+ export type { ChartBarProps } from "./chart-bar";
79
+ export type { ChartCandlestickProps } from "./chart-candlestick";
80
+ export type { ChartGridProps } from "./chart-grid";
81
+ export type { ChartLegendProps } from "./chart-legend";
82
+ export type { ChartLineProps } from "./chart-line";
83
+ export type { ChartScatterProps } from "./chart-scatter";
84
+ export type { ChartTooltipProps } from "./chart-tooltip";
85
+ export type { ChartTooltipDotProps } from "./chart-tooltip-dot";
86
+ export type { ChartTooltipXProps } from "./chart-tooltip-x";
87
+ export type { ChartTooltipYProps } from "./chart-tooltip-y";
88
+ export type { ChartXAxisProps } from "./chart-x-axis";
89
+ export type { ChartYAxisProps } from "./chart-y-axis";
90
+ export { PieChart, type PieChartProps } from "./pie-chart";
91
+ export {
92
+ PieChartContext,
93
+ type PieChartContextValue,
94
+ PieChartProvider,
95
+ usePieChart,
96
+ usePieChartContext,
97
+ } from "./pie-chart.context";
98
+ export type { PieChartCenterProps } from "./pie-chart-center";
99
+ export type { PieChartLabelProps } from "./pie-chart-label";
100
+ export type { PieChartSliceProps } from "./pie-chart-slice";
101
+ export type { PieChartTooltipProps } from "./pie-chart-tooltip";
102
+ export { useChartFont } from "./use-chart-font";
103
+ export { useChartPalette } from "./use-chart-palette";
@@ -0,0 +1,40 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import { View } from "react-native";
3
+ import { cn } from "../../lib/cn";
4
+ import { Text } from "../text";
5
+ import { usePieChart } from "./pie-chart.context";
6
+
7
+ export type PieChartCenterProps = {
8
+ /** The headline figure — a total, a percentage. On the type scale, in `foreground`. */
9
+ value?: string;
10
+ /** A caption under the value, in `muted-foreground`. */
11
+ label?: string;
12
+ className?: string;
13
+ /** Anything else to place in the hole, after the value and the label. */
14
+ children?: ReactNode;
15
+ };
16
+
17
+ /**
18
+ * Content in the hole of a donut.
19
+ *
20
+ * A React Native view, not a Skia mark, because what goes in a donut's hole
21
+ * is a headline number and a caption, and those want the type scale, a colour
22
+ * token and a className — none of which exist on a canvas. It is centred over
23
+ * the whole frame, which is where the circle's centre is: the engine centres
24
+ * the largest circle the canvas holds, and this chart gives it no padding.
25
+ *
26
+ * It takes no touches, so a tap through it still lands on a slice.
27
+ */
28
+ export function PieChartCenter({ value, label, className, children }: PieChartCenterProps): ReactElement {
29
+ const { slots } = usePieChart();
30
+
31
+ return (
32
+ <View className={cn(slots.pieCenter(), className)} pointerEvents="none">
33
+ {value === undefined ? null : <Text className={slots.pieCenterValue()}>{value}</Text>}
34
+ {label === undefined ? null : <Text className={slots.pieCenterLabel()}>{label}</Text>}
35
+ {children}
36
+ </View>
37
+ );
38
+ }
39
+
40
+ PieChartCenter.displayName = "DelacourUI.PieChart.Center";
@@ -0,0 +1,56 @@
1
+ import { PieLabel } from "@delacour/react-native-charts";
2
+ import type { PieSliceData } from "@delacour/react-native-charts/core";
3
+ import { type ReactElement, useCallback } from "react";
4
+ import { type PieLabelFormat, pieLabelText } from "./chart.variants";
5
+ import { usePieChart } from "./pie-chart.context";
6
+
7
+ export type PieChartLabelProps = {
8
+ /** What each label prints. Defaults to the slice's share, as `42%`. */
9
+ format?: PieLabelFormat;
10
+ /**
11
+ * Overrides the chart's background, which the labels are drawn in.
12
+ *
13
+ * A literal, or a value already resolved — a Skia mark cannot look a token
14
+ * up, for the reason the folder's `AGENTS.md` opens with.
15
+ */
16
+ color?: string;
17
+ /** Across the annulus: 0 the inner edge, 1 the outer, more than 1 outside. */
18
+ radiusOffset?: number;
19
+ /** Slices narrower than this, in degrees, get no label. */
20
+ minSweep?: number;
21
+ opacity?: number;
22
+ };
23
+
24
+ /**
25
+ * A text label on every slice wide enough to hold one.
26
+ *
27
+ * Skia text in the chart's background colour, so it reads on any slice of the
28
+ * ramp; the axis labels are the precedent for text living in the canvas. A
29
+ * slice under twelve degrees gets none, because a label wider than its wedge
30
+ * lands on the neighbours and three overlapping labels are less legible than
31
+ * none.
32
+ */
33
+ export function PieChartLabel({
34
+ format,
35
+ color,
36
+ radiusOffset,
37
+ minSweep = 12,
38
+ opacity,
39
+ }: PieChartLabelProps): ReactElement | null {
40
+ const { surfaceColor } = usePieChart();
41
+ const formatLabel = useCallback((slice: PieSliceData) => pieLabelText(format, slice), [format]);
42
+ const paint = color ?? surfaceColor;
43
+ if (paint === undefined) return null;
44
+
45
+ return (
46
+ <PieLabel
47
+ color={paint}
48
+ formatLabel={formatLabel}
49
+ minSweep={minSweep}
50
+ opacity={opacity}
51
+ radiusOffset={radiusOffset}
52
+ />
53
+ );
54
+ }
55
+
56
+ PieChartLabel.displayName = "DelacourUI.PieChart.Label";
@@ -0,0 +1,45 @@
1
+ import { PieInset, PieSlices } from "@delacour/react-native-charts";
2
+ import { type ReactElement, useMemo } from "react";
3
+ import { usePieChart } from "./pie-chart.context";
4
+
5
+ export type PieChartSliceProps = {
6
+ opacity?: number;
7
+ /**
8
+ * A hairline between neighbouring slices, in the chart's background.
9
+ *
10
+ * On by default: two ramp colours side by side with no gap read as one
11
+ * shape with a stripe, and the gap is what makes each wedge its own.
12
+ */
13
+ stroke?: boolean;
14
+ /** The hairline's width. */
15
+ strokeWidth?: number;
16
+ /** Opacity of every slice but the selected one, while one is selected. `1` turns the dim off. */
17
+ dimUnselected?: number;
18
+ };
19
+
20
+ /**
21
+ * Every slice of the pie, one wedge per row, in the series' colours.
22
+ *
23
+ * The colours come from the context already resolved, so this is the engine's
24
+ * `PieSlices` given a list of strings. While a slice is selected — by a tap or
25
+ * through `selectedIndex` — the others dim, because a selection that changes
26
+ * nothing on screen looks like a tap that missed.
27
+ */
28
+ export function PieChartSlice({
29
+ opacity,
30
+ stroke = true,
31
+ strokeWidth = 2,
32
+ dimUnselected = 0.4,
33
+ }: PieChartSliceProps): ReactElement {
34
+ const { series, surfaceColor } = usePieChart();
35
+ const colors = useMemo(() => series.map((entry) => entry.color), [series]);
36
+
37
+ return (
38
+ <>
39
+ <PieSlices colors={colors} dimUnselected={dimUnselected} opacity={opacity} />
40
+ {stroke && surfaceColor !== undefined ? <PieInset color={surfaceColor} strokeWidth={strokeWidth} /> : null}
41
+ </>
42
+ );
43
+ }
44
+
45
+ PieChartSlice.displayName = "DelacourUI.PieChart.Slice";
@@ -0,0 +1,85 @@
1
+ import { sliceLabelPosition } from "@delacour/react-native-charts/core";
2
+ import { type ReactElement, useState } from "react";
3
+ import type { LayoutChangeEvent } from "react-native";
4
+ import { View } from "react-native";
5
+ import { cn } from "../../lib/cn";
6
+ import { Text } from "../text";
7
+ import { chartTooltipOffset, formatPieValue, pieSlicePercent } from "./chart.variants";
8
+ import { usePieChart } from "./pie-chart.context";
9
+
10
+ export type PieChartTooltipProps = {
11
+ className?: string;
12
+ /** Prints the selected slice's value. Defaults to digits grouped in threes, `1,240`. */
13
+ formatValue?: (value: number, name: string) => string;
14
+ /** Prints the selected slice's share. Defaults to a whole-number percentage. */
15
+ formatPercent?: (percent: number) => string;
16
+ /** Overrides the measured size used to keep the tooltip inside the frame. */
17
+ size?: { width: number; height: number };
18
+ };
19
+
20
+ /** Assumed size before layout, so the first frame is not placed off-screen. */
21
+ const DEFAULT_SIZE = { width: 120, height: 48 };
22
+
23
+ /**
24
+ * A readout for the selected slice: its label, its value and its share.
25
+ *
26
+ * Selection-driven, not scrub-driven. A pie has no x to scrub along, so the
27
+ * readout answers a tap rather than following a finger — which is also why it
28
+ * needs no shared values and no `useAnimatedStyle`: it moves when the
29
+ * selection changes, a few times a session, not once a frame.
30
+ *
31
+ * It sits just outside the slice's outer edge on its bisector, the position a
32
+ * callout line would point at, and `chartTooltipOffset` keeps it inside the
33
+ * frame the same way the cartesian tooltip is kept. Hidden while nothing is
34
+ * selected; a tap outside every slice clears the selection and hides it.
35
+ */
36
+ export function PieChartTooltip({
37
+ className,
38
+ formatValue,
39
+ formatPercent,
40
+ size,
41
+ }: PieChartTooltipProps): ReactElement | null {
42
+ const { series, values, total, slices, slots, frame, selectedIndex } = usePieChart();
43
+ const [measured, setMeasured] = useState(DEFAULT_SIZE);
44
+ const box = size ?? measured;
45
+
46
+ const onLayout = (event: LayoutChangeEvent): void => {
47
+ const { width, height } = event.nativeEvent.layout;
48
+ setMeasured((current) => (current.width === width && current.height === height ? current : { width, height }));
49
+ };
50
+
51
+ if (selectedIndex === null) return null;
52
+ const entry = series[selectedIndex];
53
+ const slice = slices[selectedIndex];
54
+ const value = values[selectedIndex];
55
+ if (entry === undefined || slice === undefined || value === undefined) return null;
56
+
57
+ const anchor = sliceLabelPosition(slice, 1.05);
58
+ const offset = chartTooltipOffset({
59
+ x: anchor.x,
60
+ y: anchor.y,
61
+ width: box.width,
62
+ height: box.height,
63
+ frameWidth: frame.width,
64
+ frameHeight: frame.height,
65
+ });
66
+ const percent = pieSlicePercent(value, total);
67
+
68
+ return (
69
+ <View
70
+ className={cn(slots.tooltip(), className)}
71
+ onLayout={onLayout}
72
+ pointerEvents="none"
73
+ style={{ transform: [{ translateX: offset.x }, { translateY: offset.y }] }}
74
+ >
75
+ <View className={slots.tooltipRow()}>
76
+ <View className={slots.tooltipSwatch()} style={{ backgroundColor: entry.color }} />
77
+ <Text className={slots.tooltipName()}>{entry.label}</Text>
78
+ <Text className={slots.tooltipValue()}>{formatValue?.(value, entry.label) ?? formatPieValue(value)}</Text>
79
+ </View>
80
+ <Text className={slots.tooltipHeading()}>{formatPercent?.(percent) ?? `${Math.round(percent)}%`}</Text>
81
+ </View>
82
+ );
83
+ }
84
+
85
+ PieChartTooltip.displayName = "DelacourUI.PieChart.Tooltip";
@@ -0,0 +1,77 @@
1
+ import type { PieSliceData } from "@delacour/react-native-charts/core";
2
+ import { createContext, type ReactNode, useContext } from "react";
3
+ import type { ChartSlots } from "./chart.context";
4
+ import type { ChartResolvedSeries } from "./chart.types";
5
+ import type { ChartSize } from "./chart.variants";
6
+
7
+ export type PieChartContextValue = {
8
+ /** One per drawn slice, in data order, colours already resolved to values. */
9
+ readonly series: readonly ChartResolvedSeries[];
10
+ /** The slices' values, index-aligned with `series`. */
11
+ readonly values: readonly number[];
12
+ readonly total: number;
13
+ /**
14
+ * The slices' geometry, index-aligned with `series`.
15
+ *
16
+ * Resolved here as well as in the engine, from the same pure functions on
17
+ * the same frame, because the tooltip is a React Native view outside the
18
+ * canvas and the engine's context does not reach it.
19
+ */
20
+ readonly slices: readonly PieSliceData[];
21
+ readonly slots: ChartSlots;
22
+ readonly size: ChartSize;
23
+ /** The surface under the pie — what the hairline between slices is painted in. */
24
+ readonly surfaceColor: string | undefined;
25
+ /** The frame's measured size, for placing an overlay inside it. */
26
+ readonly frame: { readonly width: number; readonly height: number };
27
+ readonly nameKey: string;
28
+ readonly valueKey: string;
29
+ /** The hole, as a fraction of the radius. `0` is a pie. */
30
+ readonly innerRadius: number;
31
+ /** The slice singled out by a tap or by the caller, or `null`. */
32
+ readonly selectedIndex: number | null;
33
+ /** Selects a slice by index, or clears the selection with `null`. */
34
+ readonly select: (index: number | null) => void;
35
+ };
36
+
37
+ /**
38
+ * The resolved pie, published twice from one value — once around the React
39
+ * Native subtree and once inside the canvas — for the reason `ChartContext`
40
+ * documents: Skia's renderer is its own reconciler and context does not cross
41
+ * into it.
42
+ */
43
+ export const PieChartContext = createContext<PieChartContextValue | null>(null);
44
+
45
+ PieChartContext.displayName = "DelacourUI.PieChartContext";
46
+
47
+ export function PieChartProvider({
48
+ value,
49
+ children,
50
+ }: {
51
+ readonly value: PieChartContextValue;
52
+ readonly children: ReactNode;
53
+ }): ReactNode {
54
+ return <PieChartContext.Provider value={value}>{children}</PieChartContext.Provider>;
55
+ }
56
+
57
+ PieChartProvider.displayName = "DelacourUI.PieChartProvider";
58
+
59
+ /**
60
+ * The pie a part belongs to. Throws outside one, for the reason `useChart`
61
+ * does: a mark that silently drew nothing would be a blank canvas with no
62
+ * error to read.
63
+ */
64
+ export function usePieChart(): PieChartContextValue {
65
+ const value = useContext(PieChartContext);
66
+ if (value === null) {
67
+ throw new Error(
68
+ "[DelacourUI] a PieChart part was used outside a <PieChart>. Every part reads the chart from context."
69
+ );
70
+ }
71
+ return value;
72
+ }
73
+
74
+ /** The pie, or `null` outside one. */
75
+ export function usePieChartContext(): PieChartContextValue | null {
76
+ return useContext(PieChartContext);
77
+ }
@@ -0,0 +1,247 @@
1
+ import { PolarChart } from "@delacour/react-native-charts";
2
+ import { type PieSliceData, resolvePolarLayout, resolveSlices } 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 { useControllableState } from "../../hooks/use-controllable-state";
7
+ import { useThemeColor } from "../../hooks/use-theme-color";
8
+ import type { ChartConfig, ChartDatum } from "./chart.types";
9
+ import {
10
+ type ChartSize,
11
+ chartAxisFontSize,
12
+ chartVariants,
13
+ PIE_DEFAULT_INNER_RADIUS,
14
+ pieInnerRadiusSpec,
15
+ resolvePieSeries,
16
+ } from "./chart.variants";
17
+ import { ChartLegend } from "./chart-legend";
18
+ import { type PieChartContextValue, PieChartProvider } from "./pie-chart.context";
19
+ import { PieChartCenter } from "./pie-chart-center";
20
+ import { PieChartLabel } from "./pie-chart-label";
21
+ import { PieChartSlice } from "./pie-chart-slice";
22
+ import { PieChartTooltip } from "./pie-chart-tooltip";
23
+ import { useChartFont } from "./use-chart-font";
24
+ import { useChartPalette } from "./use-chart-palette";
25
+
26
+ export type PieChartProps = {
27
+ /**
28
+ * Labels and colours by row name, for the rows that want one.
29
+ *
30
+ * Optional, because a pie's categories are its rows and the ramp already
31
+ * colours them in order. An entry is keyed by the row's `nameKey` value.
32
+ */
33
+ config?: ChartConfig;
34
+ data: readonly ChartDatum[];
35
+ /** The field that names a slice. */
36
+ nameKey: string;
37
+ /** The field that sizes a slice. Negative, missing and unreadable values are dropped. */
38
+ valueKey: string;
39
+ size?: ChartSize;
40
+ /** The hole, as a fraction of the radius. `0` is a pie; `0.6` is a donut. */
41
+ innerRadius?: number;
42
+ /** Where the first slice begins. Degrees clockwise from 12 o'clock. */
43
+ startAngle?: number;
44
+ /** The selected slice's index in the drawn order, or `null`. Controlled when given. */
45
+ selectedIndex?: number | null;
46
+ /** Called with a tapped slice's index, or `null` for a tap outside every slice. */
47
+ onSelect?: (index: number | null) => void;
48
+ className?: string;
49
+ /** Named on the frame, so a capture flow or a test can find the plot. */
50
+ testID?: string;
51
+ children?: ReactNode;
52
+ };
53
+
54
+ /** The field names the engine rows are built with. */
55
+ const ENGINE_NAME_KEY = "name";
56
+ const ENGINE_VALUE_KEY = "value";
57
+
58
+ type EngineRow = { readonly name: string; readonly value: number };
59
+
60
+ function PieChartRoot({
61
+ config,
62
+ data,
63
+ nameKey,
64
+ valueKey,
65
+ size = "md",
66
+ innerRadius = PIE_DEFAULT_INNER_RADIUS,
67
+ startAngle = 0,
68
+ selectedIndex,
69
+ onSelect,
70
+ className,
71
+ testID,
72
+ children,
73
+ }: PieChartProps): ReactElement {
74
+ const slots = chartVariants({ size });
75
+ const [frame, setFrame] = useState({ width: 0, height: 0 });
76
+
77
+ const resolved = useMemo(
78
+ () => resolvePieSeries(data, nameKey, valueKey, config ?? {}),
79
+ [data, nameKey, valueKey, config]
80
+ );
81
+
82
+ // Every theme lookup happens here, above the canvas — see `Chart`.
83
+ const series = useChartPalette(resolved.series);
84
+ const surfaceColor = useThemeColor("background");
85
+ const font = useChartFont(chartAxisFontSize(size));
86
+
87
+ const [selected, select] = useControllableState<number | null>({
88
+ value: selectedIndex,
89
+ defaultValue: null,
90
+ onChange: onSelect,
91
+ });
92
+
93
+ // The engine draws the rows this resolved to, not the caller's data, so a
94
+ // dropped row is dropped everywhere and the slice indices, the series and
95
+ // the values all agree.
96
+ const rows = useMemo<EngineRow[]>(
97
+ () => series.map((entry, index) => ({ name: entry.label, value: resolved.values[index] ?? 0 })),
98
+ [series, resolved.values]
99
+ );
100
+
101
+ const innerRadiusSpec = pieInnerRadiusSpec(innerRadius);
102
+
103
+ // The same geometry the engine resolves, resolved again here for the parts
104
+ // that live outside the canvas: the tooltip needs a slice's bisector, and
105
+ // the engine's context stops at the canvas boundary.
106
+ const slices = useMemo<PieSliceData[]>(() => {
107
+ const layout = resolvePolarLayout({ canvas: frame, innerRadius: innerRadiusSpec });
108
+ return resolveSlices({
109
+ values: resolved.values,
110
+ labels: series.map((entry) => entry.label),
111
+ layout,
112
+ startAngle,
113
+ });
114
+ }, [frame, innerRadiusSpec, resolved.values, series, startAngle]);
115
+
116
+ const value = useMemo<PieChartContextValue>(
117
+ () => ({
118
+ series,
119
+ values: resolved.values,
120
+ total: resolved.total,
121
+ slices,
122
+ slots,
123
+ size,
124
+ surfaceColor,
125
+ frame,
126
+ nameKey,
127
+ valueKey,
128
+ innerRadius,
129
+ selectedIndex: selected,
130
+ select,
131
+ }),
132
+ [series, resolved, slices, slots, size, surfaceColor, frame, nameKey, valueKey, innerRadius, selected, select]
133
+ );
134
+
135
+ const { canvas, overlay, below, selectable } = useMemo(() => partitionPieChildren(children), [children]);
136
+
137
+ const onLayout = (event: LayoutChangeEvent): void => {
138
+ const { width, height } = event.nativeEvent.layout;
139
+ setFrame((current) => (current.width === width && current.height === height ? current : { width, height }));
140
+ };
141
+
142
+ // A tap selects only when something would show it: a tooltip, a caller's
143
+ // handler, or a controlled selection. Otherwise the chart takes no gesture
144
+ // at all, and a pie in a list is nothing more than a picture.
145
+ const onSlicePress = selectable || onSelect !== undefined || selectedIndex !== undefined ? select : undefined;
146
+
147
+ return (
148
+ <PieChartProvider value={value}>
149
+ <View className={slots.root({ className })}>
150
+ <View className={slots.frame()} onLayout={onLayout} testID={testID}>
151
+ <PolarChart
152
+ data={rows}
153
+ font={font}
154
+ innerRadius={innerRadiusSpec}
155
+ labelKey={ENGINE_NAME_KEY}
156
+ onSlicePress={onSlicePress}
157
+ selectedIndex={selected}
158
+ startAngle={startAngle}
159
+ valueKey={ENGINE_VALUE_KEY}
160
+ >
161
+ <PieChartProvider value={value}>{canvas}</PieChartProvider>
162
+ </PolarChart>
163
+ {overlay}
164
+ </View>
165
+ {below}
166
+ </View>
167
+ </PieChartProvider>
168
+ );
169
+ }
170
+
171
+ /**
172
+ * Splits the children by where they have to be mounted, the way `Chart` does.
173
+ *
174
+ * Slices and labels are Skia marks and go inside the canvas; the centre and
175
+ * the tooltip are React Native views floated over it; a legend, or anything
176
+ * else, sits under it. Matching is by component identity, so a part must be a
177
+ * direct child of `<PieChart>` — an array from `.map()` is fine, a part wrapped
178
+ * in a caller's own component is not.
179
+ *
180
+ * It also reports whether a tooltip was placed, because that is what decides
181
+ * whether a tap should select anything.
182
+ */
183
+ function partitionPieChildren(children: ReactNode): {
184
+ canvas: ReactNode[];
185
+ overlay: ReactNode[];
186
+ below: ReactNode[];
187
+ selectable: boolean;
188
+ } {
189
+ const canvas: ReactNode[] = [];
190
+ const overlay: ReactNode[] = [];
191
+ const below: ReactNode[] = [];
192
+ let selectable = false;
193
+
194
+ for (const child of Children.toArray(children)) {
195
+ if (!isValidElement(child)) continue;
196
+ if (child.type === PieChartSlice || child.type === PieChartLabel) canvas.push(child);
197
+ else if (child.type === PieChartCenter) overlay.push(child);
198
+ else if (child.type === PieChartTooltip) {
199
+ overlay.push(child);
200
+ selectable = true;
201
+ } else below.push(child);
202
+ }
203
+
204
+ return { canvas, overlay, below, selectable };
205
+ }
206
+
207
+ /**
208
+ * A pie or donut, wearing the theme's five-colour series ramp.
209
+ *
210
+ * A second root beside `Chart` rather than a part of it, because a pie shares
211
+ * nothing with a cartesian plot on screen — no axes, no grid, no scrub — and
212
+ * only the palette, the legend and the font underneath. Its categories are its
213
+ * **rows**: the first row is `chart-1`, the second `chart-2`, and a `config`
214
+ * entry keyed by a row's name overrides that one row's label or colour.
215
+ *
216
+ * `PieChart.Slice` and `PieChart.Label` draw into the canvas; `PieChart.Center`
217
+ * and `PieChart.Tooltip` float over it as React Native views; `PieChart.Legend`
218
+ * sits beneath. The tooltip answers a tap rather than a scrub — tap a slice to
219
+ * select it and dim the rest, tap outside to clear — and `selectedIndex` with
220
+ * `onSelect` makes the selection controlled.
221
+ *
222
+ * @example
223
+ * <PieChart data={rows} nameKey="browser" valueKey="visitors">
224
+ * <PieChart.Slice />
225
+ * <PieChart.Legend />
226
+ * </PieChart>
227
+ *
228
+ * @example
229
+ * <PieChart data={rows} innerRadius={0.6} nameKey="browser" valueKey="visitors">
230
+ * <PieChart.Slice />
231
+ * <PieChart.Center label="Visitors" value="1,125" />
232
+ * <PieChart.Tooltip />
233
+ * </PieChart>
234
+ */
235
+ export const PieChart = Object.assign(PieChartRoot, {
236
+ /** Every slice, in the series' colours, with a hairline between them. */
237
+ Slice: PieChartSlice,
238
+ /** A percentage — or a value, or the name — on every slice wide enough to hold one. */
239
+ Label: PieChartLabel,
240
+ /** A headline figure and a caption in the hole of a donut. A React Native view. */
241
+ Center: PieChartCenter,
242
+ /** A readout for the tapped slice. A React Native view, over the canvas. */
243
+ Tooltip: PieChartTooltip,
244
+ /** A swatch and a label per slice, under the chart. */
245
+ Legend: ChartLegend,
246
+ displayName: "DelacourUI.PieChart",
247
+ });
@@ -0,0 +1,25 @@
1
+ import { type ChartFont, useSystemFont } from "@delacour/react-native-charts";
2
+ import { useCSSVariable } from "uniwind";
3
+
4
+ /**
5
+ * The font Skia draws axis labels with.
6
+ *
7
+ * The family comes from `--font-sans`, which `theme.css` declares per platform
8
+ * under `@variant ios` / `@variant android`. It is deliberately **not** aliased
9
+ * through `@theme inline` — aliasing a font would redefine each name as itself
10
+ * and every `font-*` utility would silently draw nothing — so the raw variable
11
+ * is the only thing there to read, which is exactly what `useCSSVariable` takes.
12
+ *
13
+ * An unresolved family is not a failure. `matchFont` then picks the platform's
14
+ * own default, which is what `--font-sans` names on both platforms anyway; the
15
+ * variable only matters once an app overrides it.
16
+ *
17
+ * No font file ships with this package or with `@delacour/react-native-charts`. That is the
18
+ * same choice `theme.css` already made for the rest of the type: OS-bundled
19
+ * families mean nothing has to be loaded through `expo-font` and a consuming
20
+ * app needs no config plugin.
21
+ */
22
+ export function useChartFont(size: number): ChartFont {
23
+ const family = useCSSVariable("--font-sans") as string | undefined;
24
+ return useSystemFont(family, size);
25
+ }