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

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 +193 -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 +434 -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 +242 -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 +246 -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 +150 -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 +105 -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 +293 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -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 +112 -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 +47 -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 +95 -0
  108. package/src/components/field/field.tsx +157 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +240 -0
  111. package/src/components/field/field.variants.ts +156 -0
  112. package/src/components/field/index.ts +22 -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 +74 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +261 -0
  146. package/src/components/pressable/pressable.variants.test.ts +128 -0
  147. package/src/components/pressable/pressable.variants.ts +80 -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 +245 -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 +580 -0
  159. package/src/components/radio/radio.variants.ts +271 -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 +50 -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 +193 -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 +856 -0
  202. package/src/components/slider/slider.variants.ts +661 -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 +287 -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 +670 -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,38 @@
1
+ import { useMemo } from "react";
2
+ import { useThemeColor } from "../../hooks/use-theme-color";
3
+ import type { ChartResolvedSeries } from "./chart.types";
4
+ import { applyChartColors, CHART_MAX_TOKEN_SERIES, partitionChartColors } from "./chart.variants";
5
+
6
+ /**
7
+ * Resolves a series list's theme tokens into colour strings.
8
+ *
9
+ * Every theme lookup happens here, above the canvas. Skia's renderer is a
10
+ * second React reconciler with no Uniwind provider in it, so a hook called
11
+ * inside would resolve nothing — the marks receive resolved strings instead.
12
+ * A hook rather than a helper in a root so `Chart` and `PieChart` share one
13
+ * palette and one cap.
14
+ *
15
+ * Eight calls, written out. `partitionChartColors` pads its token list to
16
+ * exactly this many so the hook count cannot vary between renders, and a loop
17
+ * over the series would break the rules of hooks the moment a series was
18
+ * added. The repetition is the mechanism, not an oversight. Tokens are
19
+ * deduped first, so the eight are eight *distinct* tokens — the five-token
20
+ * ramp costs five, however many series walk it.
21
+ */
22
+ export function useChartPalette(declared: readonly ChartResolvedSeries[]): ChartResolvedSeries[] {
23
+ const partition = useMemo(() => partitionChartColors(declared, CHART_MAX_TOKEN_SERIES), [declared]);
24
+
25
+ const color0 = useThemeColor(partition.tokens[0] as string);
26
+ const color1 = useThemeColor(partition.tokens[1] as string);
27
+ const color2 = useThemeColor(partition.tokens[2] as string);
28
+ const color3 = useThemeColor(partition.tokens[3] as string);
29
+ const color4 = useThemeColor(partition.tokens[4] as string);
30
+ const color5 = useThemeColor(partition.tokens[5] as string);
31
+ const color6 = useThemeColor(partition.tokens[6] as string);
32
+ const color7 = useThemeColor(partition.tokens[7] as string);
33
+
34
+ return useMemo(
35
+ () => applyChartColors(declared, partition, [color0, color1, color2, color3, color4, color5, color6, color7]),
36
+ [declared, partition, color0, color1, color2, color3, color4, color5, color6, color7]
37
+ );
38
+ }
@@ -0,0 +1,185 @@
1
+ # Checkbox
2
+
3
+ A box that is ticked or not — alone, or as one of a group sharing a value list.
4
+ Root plus `Checkbox.Label` and `Checkbox.Group`.
5
+
6
+ `import { Checkbox } from "@delacour/react-native-ui/checkbox";`
7
+
8
+ ## Files
9
+
10
+ | File | What it holds |
11
+ | --- | --- |
12
+ | `index.ts` | → `@delacour/react-native-ui/checkbox` |
13
+ | `checkbox.tsx` | Root + the `Object.assign` compound surface |
14
+ | `checkbox-label.tsx` | `Checkbox.Label`, the `Text.Label` inside the tap target |
15
+ | `checkbox-group.tsx` | `Checkbox.Group`, which owns the checked list |
16
+ | `checkbox-box.tsx` | The square, its animated border, fill and tick — internal |
17
+ | `checkbox.context.tsx` | `CheckboxContext` and `CheckboxGroupContext`, with their hooks |
18
+ | `checkbox.types.ts` | Prop types shared by two or more parts |
19
+ | `checkbox.variants.ts` | Pure `tv()` slots + six resolvers, no RN imports |
20
+ | `checkbox.variants.test.ts` | |
21
+
22
+ ## Design
23
+
24
+ - **Colours**: `default`, `primary`, `success`, `warning`, `destructive`, `info` —
25
+ [Badge](../badge/AGENTS.md)'s set, reusing tokens the theme already has. **Sizes**: `sm`, `md`, `lg`.
26
+ **Alignment**: `start`, `end`. There is no `variant` axis: a checkbox has one
27
+ shape, and a second way to paint it would be a second thing to keep in step
28
+ with the [radio](../radio/AGENTS.md) that will sit beside it.
29
+ - **The root draws the box itself**, which is what makes `<Checkbox />` a
30
+ complete control with no children. Anything composed inside lands *beside* the
31
+ box and shares its tap target — which is the entire reason `Checkbox.Label`
32
+ exists next to [`Field.Label`](../field/AGENTS.md). `Field.Label` names a control from a row away;
33
+ this one is inside the pressable, so tapping the words toggles the box. Use the
34
+ field's in a horizontal `Field` and this one everywhere else.
35
+ - **`Checkbox.Label` *is* [`Text.Label`](../text/AGENTS.md).** It renders the preset and passes a
36
+ step and a colour, never a class for either — `resolveCheckboxLabelSize` maps
37
+ the checkbox's own step names onto `TEXT_SIZES`', and
38
+ `resolveCheckboxLabelColor` returns `undefined` to mean "leave the preset's own
39
+ alone". The `label` slot carries layout and nothing else, and a test asserts it
40
+ holds no `text-*` or `font-*` at any size. Same rule as [`Field`](../field/AGENTS.md), same reason
41
+ [`Input`](../input/AGENTS.md) ships no label part at all.
42
+ - **`color` paints the indicator, not the box.** The indicator is an
43
+ absolute-fill layer that is invisible until the box is ticked, so an unticked
44
+ box is `border-input bg-card` at every colour — the same chrome a field wears,
45
+ because it is the same kind of thing. Only the border has to know both states,
46
+ which is why the six `color × isFilled` cells are the whole of
47
+ `compoundVariants` instead of a thirty-six cell matrix. The border always
48
+ matches its own fill, and a test pins the pair rather than trusting two maps.
49
+ - **`isFilled`, not `isChecked`, is the tv axis.** Checked and indeterminate both
50
+ paint the surface and only the glyph tells them apart, so the axis is named for
51
+ what it does and `resolveCheckboxFilled` is the translation. Indeterminate also
52
+ reports `checked="mixed"`, so a "select all" row says what it means rather than
53
+ claiming a half-truth.
54
+ - **Invalid outranks the colour**, on the border and the fill, ticked or not.
55
+ A checkbox that stayed green while its value was rejected would drop the only
56
+ signal it has, exactly while the value is being corrected — the precedence
57
+ [`Input`](../input/AGENTS.md) sets between invalid and focus.
58
+ - **The axis ladder is `own ?? group ?? field ?? default`, and it is deliberately
59
+ not [`Input`](../input/AGENTS.md)'s.** `Input.Group` puts itself *first* because it owns the one box
60
+ a grouped field renders into: two answers to one question is not a state worth
61
+ expressing. `Checkbox.Group` owns no box. It is a state controller that also
62
+ carries shared defaults, which makes it the same kind of thing as [`Field`](../field/AGENTS.md) — a
63
+ wrapper a control overrides — so "make the group `lg`" and "make this one
64
+ `destructive`" are different questions and both get an answer. `??` throughout and
65
+ never `||`, so `isDisabled={false}` opts a child out of a disabled group.
66
+ - **`Checkbox.Group`'s state is one array of the children's `value`s.**
67
+ `toggleCheckedValue` is the whole transition and it is pure, so `bun test`
68
+ reaches it — including that it always returns a *new* array, since React bails
69
+ out of a re-render on an unchanged reference and a mutation would flip the
70
+ state while leaving the screen alone. A grouped checkbox with no `value`
71
+ throws by name: group membership is invisible in the child's props at compile
72
+ time, so it cannot be a type error.
73
+ - **The group is a plain `View` with no role.** A group of checkboxes is a
74
+ container, and announcing it as a control would put an actionless element in
75
+ front of every child. Lay them out any other way with a `className` —
76
+ `flex-row flex-wrap` for a row.
77
+ - **Inside a `Field` the box hands its toggle back up**, so the row is the
78
+ target and a form checkbox can be a bare `<Checkbox />` with the field naming
79
+ it, rather than a `Checkbox.Label` repeating the name. It registers a
80
+ ref-backed trampoline rather than the toggle itself: the toggle is a new
81
+ function whenever `checked` changes, and re-registering on every tick would
82
+ re-render the field for nothing. See [Field](../field/AGENTS.md).
83
+ - **The whole [`Pressable`](../pressable/AGENTS.md) surface passes through**, because the root *is* one.
84
+ Two defaults differ from a bare `Pressable` and only two: `feedback="fade"`,
85
+ since a spring on a 20pt square reads as a jitter rather than a press, and
86
+ `haptic="selection"`, since a checkbox is a state toggle and the tick landing
87
+ is the confirmation — [`Button`](../button/AGENTS.md) and [`Badge`](../badge/AGENTS.md) leave it off because their press is
88
+ an action, not a state change. Both are ordinary props, so `haptic={false}`
89
+ silences it. `onPress` is the one prop `Omit`ed rather than forwarded: the
90
+ press *is* the toggle, and `onCheckedChange` is where a side effect goes.
91
+ - **The fill, the tick and the border are three gestures off one shared value.**
92
+ The fill fades and scales **from the centre**: a box is filled, not slid into,
93
+ and there is no edge a checkbox is filled *from* — a `translateX` here reads as
94
+ a panel arriving rather than a surface appearing, so there is none. The tick is
95
+ not faded up with it; it sits behind a container whose width opens from the
96
+ box's left edge, so the stroke is drawn on when ticking and taken back when
97
+ unticking. `tickDelay` holds it until the surface it is drawn on is most of the
98
+ way there — starting both at once reads as one blurred event. The border comes
99
+ last, held by `borderDelay` until the surface is near the edge, so it reads as
100
+ the fill *arriving* at the border rather than as an outline changing on its
101
+ own. All three interpolate off a single `withTiming`, so they cannot drift, and
102
+ the values live in `CHECKBOX_INDICATOR_ANIMATION` where a test pins that every
103
+ track travels and that the filled end is a finished box rather than something
104
+ stopped mid-way.
105
+ - **The fill overlaps the border rather than meeting it, and its corner is the
106
+ box's own.** `-inset-px` on the `indicator` slot puts it a point past the
107
+ padding box on every side, so it runs *under* the border ring instead of
108
+ stopping against it, and `resolveCheckboxFillRadius` gives it the same corner
109
+ the box wears. It does not animate: `scale` shrinks the rendered corner along
110
+ with the square, which is what keeps a half-grown fill looking like a smaller
111
+ version of the finished one, and animating the radius would only make it
112
+ correct at one end.
113
+
114
+ **The overlap is the whole point, and it replaced the opposite rule.** The fill
115
+ used to be `inset-0` with the box's radius *minus* its border width — the rule
116
+ for two rounded rectangles to sit concentric. Concentric is exactly what went
117
+ wrong: the fill's outer curve and the border's inner curve became the same
118
+ curve, rasterised twice on two layers and antialiased independently. At a
119
+ corner pixel where each gives coverage `a` the composite covers `2a - a²`, so
120
+ `(1 - a)²` of the box's own `bg-card` bleeds through — a dull arc at each of
121
+ the four corners, on iOS and Android alike. The straight edges are pixel
122
+ aligned and showed nothing, which is what made it read as a corner artifact
123
+ rather than as a sizing bug. Overlapping the two removes the shared edge
124
+ instead of concealing it, and holds whichever way a platform orders its paint:
125
+ iOS draws a `CALayer` border above its sublayers, Android draws it below.
126
+
127
+ It went unnoticed until the playground opened in the house preset, whose
128
+ `radius: "small"` is 7.2 against the library default's 10. The seam is a fixed
129
+ sub-pixel width, so shrinking an `md` box's corner from 4pt to 2.88pt made the
130
+ same defect a third louder. Do not "tidy" the inset back to `inset-0`, or
131
+ restore the subtraction; a test fails by name if either happens.
132
+ - **It is computed from `--radius`, not written down.** Minting a token for it
133
+ would only give the box and the fill a second place to disagree. It cannot be
134
+ read back either: the corner scale is `@theme inline`, so Tailwind substitutes
135
+ each step into its utilities and no `--radius-xs` variable reaches the runtime
136
+ — though what it substitutes still carries a live `var(--radius)`, which is
137
+ what keeps a class-based `rounded-xs` and this function in step when a preset
138
+ retunes the base at runtime.
139
+ `resolveCheckboxFillRadius` takes the live `--radius` and applies
140
+ `CHECKBOX_RADIUS_MULTIPLIER`, which is why a consumer pasting a theme with a
141
+ different `--radius` moves the fill with the border instead of leaving it
142
+ behind. `checkbox.variants.test.ts` reads `tokens.css` and asserts the
143
+ multipliers match it, that the result *is* the box's own step at any
144
+ `--radius`, that `CHECKBOX_RADIUS_STEP` names the `rounded-*` the `box` slot
145
+ actually wears, and that the box's border really is the bare 1pt `border` the
146
+ `-inset-px` assumes. Retuning the scale, or reaching for `border-2`, fails the
147
+ build rather than quietly reopening the gap.
148
+ - **The border is the one part of the box no `tv()` describes.** A colour that
149
+ fades cannot be a class, so it interpolates between two token *values* —
150
+ `resolveCheckboxBorderTokens` names them, and `CHECKBOX_SURFACE_TOKEN` is the
151
+ same colour the `indicator` slot paints as a `bg-*`, pinned against it by a
152
+ test. The base keeps `border-input` as the resting appearance the animated
153
+ style starts from, and nothing else in the slot set mentions a border colour;
154
+ two sources for one border is how a class and a style end up disagreeing for a
155
+ frame on every toggle. An **invalid** box returns `destructive` at *both* ends, so
156
+ there is nothing to fade — the border is the signal the value is wrong, and it
157
+ has to be there before the box is ticked as much as after.
158
+ - **The clip is measured, not tabulated.** It needs the box's width in points and
159
+ `size-checkbox-md` cannot be read from JavaScript, so it comes from the fill
160
+ layer's own `onLayout` — that layer is already exactly the width the clip has
161
+ to span. A table of numbers here would be `tokens.css` restated in TypeScript,
162
+ which is the drift `tokens.test.ts` exists to catch everywhere else.
163
+ - **Reduce-motion takes Reanimated's default `System` policy here**, unlike
164
+ [`Spinner`](../spinner/AGENTS.md). Under it `withTiming` completes instantly, which for a checkbox is
165
+ right: the state change is the point and the travel is decoration. A spinner
166
+ had to opt out because its animation *is* the status signal.
167
+ - **`hitSlop` is new to this package, and only a bare box gets any.** A bare `md`
168
+ checkbox is a 20pt square against a 44pt minimum, with no padded capsule to
169
+ absorb the difference the way [`Badge.CloseButton`](../badge/AGENTS.md) has. Once there is a label
170
+ the row is the target and is already wide, so `resolveCheckboxHitSlop` returns
171
+ nothing — slop on top of that would overlap the row below and make a tap
172
+ between two checkboxes ambiguous, which is worse than a merely adequate target.
173
+ - **There is no `Checkbox.Description` and no `Checkbox.Indicator`.**
174
+ `Field.Description` already is the first, and a label defined twice is a type
175
+ scale that can drift. The second has nothing to configure that
176
+ `isIndeterminate` does not already decide — the glyph swap is the only choice
177
+ it would offer.
178
+ - **The box mints no scale of its own — it reads `--spacing-icon-*`, two steps
179
+ above its own glyph.** A checkbox *is* a glyph in a box, and both measurements
180
+ already sit on that scale: 18/14, 20/16, 24/18. A private
181
+ `--spacing-checkbox-*` would be three numbers that have to be retuned in step
182
+ with three others forever, and nothing would notice when they stopped
183
+ agreeing. The two-step offset is what leaves the tick breathing room, and a
184
+ test pins the *offset* rather than the points, so the icon scale can be
185
+ retuned without the test becoming a transcript of it.
@@ -0,0 +1,152 @@
1
+ import { type ReactElement, useCallback, useEffect } from "react";
2
+ import type { LayoutChangeEvent } from "react-native";
3
+ import Animated, {
4
+ cancelAnimation,
5
+ Extrapolation,
6
+ interpolate,
7
+ interpolateColor,
8
+ useAnimatedStyle,
9
+ useSharedValue,
10
+ withTiming,
11
+ } from "react-native-reanimated";
12
+ import { useCSSVariable } from "uniwind";
13
+ import { useThemeColor } from "../../hooks/use-theme-color";
14
+ import { IconCheckmark1Small, IconMinusSmall } from "../../icons/central";
15
+ import { Icon } from "../icon";
16
+ import { useCheckboxPart } from "./checkbox.context";
17
+ import {
18
+ CHECKBOX_BORDER_WIDTH,
19
+ CHECKBOX_GLYPH_TOKEN,
20
+ CHECKBOX_INDICATOR_ANIMATION,
21
+ CHECKBOX_INVALID_GLYPH_TOKEN,
22
+ checkboxVariants,
23
+ resolveCheckboxBorderTokens,
24
+ resolveCheckboxFilled,
25
+ resolveCheckboxFillRadius,
26
+ } from "./checkbox.variants";
27
+
28
+ /**
29
+ * The square itself: its border, the fill behind it and the tick drawn on top.
30
+ *
31
+ * Internal — the checkbox renders one and there is nothing for a caller to
32
+ * compose here that `isIndeterminate` does not already decide. It takes a file
33
+ * of its own because it owns every animated value; keeping them in the root
34
+ * would make the root a component that changes for two unrelated reasons.
35
+ *
36
+ * Three things move, off one shared progress, so they cannot drift out of step:
37
+ *
38
+ * - the **fill** fades and scales from the centre. A box is filled, not slid
39
+ * into, so it arrives from no edge. Its corner radius is fixed rather than
40
+ * animated — {@link resolveCheckboxFillRadius} gives it the box's own corner at
41
+ * every scale, and the transform shrinks the rendered one with it. It spans the
42
+ * whole border box rather than the padding box, so it runs *under* the border
43
+ * ring instead of stopping against it; two coincident antialiased curves leave
44
+ * a seam at each corner, and there is no longer a shared edge to leave one.
45
+ * - the **tick** is clipped by a container whose width opens from the left, so
46
+ * the stroke is drawn on when ticking and taken back when unticking rather
47
+ * than faded up in place.
48
+ * - the **border** interpolates from the field chrome to the fill's own colour,
49
+ * held back by `borderDelay` until the surface is near the edge — so it reads
50
+ * as the fill arriving at the border rather than as an outline changing on its
51
+ * own. A colour being interpolated cannot be a class, which is why this one is
52
+ * the only part of the box a `tv()` does not describe.
53
+ *
54
+ * The clip needs the box's width in points, and a `size-checkbox-*` class cannot
55
+ * be read from JavaScript. It comes from the box's own `onLayout` rather than a
56
+ * table of numbers restating `tokens.css`, less its two borders — `onLayout`
57
+ * reports the border box, and the clip is positioned in the padding box inside
58
+ * it. The fill cannot be the thing measured: it deliberately overhangs.
59
+ *
60
+ * Reduce-motion takes Reanimated's default `System` policy here, deliberately
61
+ * unlike `Spinner`. Under it `withTiming` completes instantly, which for a
62
+ * checkbox is exactly right: the state change is the point and the travel is
63
+ * decoration. A spinner had to opt out because its animation *is* the signal.
64
+ */
65
+ export function CheckboxBox(): ReactElement {
66
+ const { color, size, isChecked, isIndeterminate, isInvalid } = useCheckboxPart("Checkbox.Box");
67
+ const isFilled = resolveCheckboxFilled({ isChecked, isIndeterminate });
68
+
69
+ const progress = useSharedValue(isFilled ? 1 : 0);
70
+ const boxWidth = useSharedValue(0);
71
+
72
+ useEffect(() => {
73
+ progress.value = withTiming(isFilled ? 1 : 0, { duration: CHECKBOX_INDICATOR_ANIMATION.durationMs });
74
+
75
+ // Without this a box unmounted mid-toggle leaves its timing running.
76
+ return () => cancelAnimation(progress);
77
+ }, [isFilled, progress]);
78
+
79
+ // Subtracting the border twice is what keeps the glyph on the box's centre
80
+ // line. `tick` and `tickInner` are positioned in the padding box, so handing
81
+ // them the border-box width moves the centre they resolve against.
82
+ const handleLayout = useCallback(
83
+ (event: LayoutChangeEvent) => {
84
+ boxWidth.value = Math.max(0, event.nativeEvent.layout.width - 2 * CHECKBOX_BORDER_WIDTH);
85
+ },
86
+ [boxWidth]
87
+ );
88
+
89
+ // `--radius` is the only step of the corner scale that survives to runtime —
90
+ // the rest are `@theme inline` and get substituted into their utilities — so
91
+ // the fill's own corner is computed from the base rather than read back.
92
+ const radius = (useCSSVariable("--radius") as number | undefined) ?? 0;
93
+ const fillRadius = resolveCheckboxFillRadius(size, radius);
94
+
95
+ const border = resolveCheckboxBorderTokens({ color, isInvalid });
96
+ const restBorderColor = useThemeColor(border.rest) ?? "transparent";
97
+ const activeBorderColor = useThemeColor(border.active) ?? "transparent";
98
+
99
+ const boxStyle = useAnimatedStyle(() => ({
100
+ borderColor: interpolateColor(
101
+ interpolate(progress.value, [CHECKBOX_INDICATOR_ANIMATION.borderDelay, 1], [0, 1], Extrapolation.CLAMP),
102
+ [0, 1],
103
+ [restBorderColor, activeBorderColor]
104
+ ),
105
+ }));
106
+
107
+ const fillStyle = useAnimatedStyle(() => ({
108
+ opacity: interpolate(progress.value, [0, 1], CHECKBOX_INDICATOR_ANIMATION.opacity),
109
+ transform: [{ scale: interpolate(progress.value, [0, 1], CHECKBOX_INDICATOR_ANIMATION.scale) }],
110
+ }));
111
+
112
+ // Clamped, so the tick sits at zero width through the delay rather than being
113
+ // extrapolated to a negative one.
114
+ const tickStyle = useAnimatedStyle(() => ({
115
+ width:
116
+ boxWidth.value *
117
+ interpolate(progress.value, [CHECKBOX_INDICATOR_ANIMATION.tickDelay, 1], [0, 1], Extrapolation.CLAMP),
118
+ }));
119
+
120
+ // Full width regardless of the clip in front of it, so the glyph stays on the
121
+ // box's centre line while the clip opens instead of sliding across with it.
122
+ const tickInnerStyle = useAnimatedStyle(() => ({ width: boxWidth.value }));
123
+
124
+ const slots = checkboxVariants({ color, isFilled, isInvalid, size });
125
+ // A colour that has to reach an SVG paint prop cannot be a class. See Theming.
126
+ const glyphColor = useThemeColor(isInvalid ? CHECKBOX_INVALID_GLYPH_TOKEN : CHECKBOX_GLYPH_TOKEN[color]);
127
+
128
+ return (
129
+ <Animated.View className={slots.box()} onLayout={handleLayout} style={boxStyle}>
130
+ <Animated.View
131
+ className={slots.indicator()}
132
+ // A fixed radius rather than an animated one: the fill wears the box's
133
+ // own corner, and that value does not change while the box is growing.
134
+ // `scale` shrinks the rendered corner along with the square, which is
135
+ // what keeps a half-grown fill looking like a smaller version of the
136
+ // finished one. It is read from `--radius` rather than written down
137
+ // because a consumer's theme is allowed to retune that.
138
+ style={[{ borderRadius: fillRadius }, fillStyle]}
139
+ />
140
+ <Animated.View className={slots.tick()} style={tickStyle}>
141
+ <Animated.View className={slots.tickInner()} style={tickInnerStyle}>
142
+ <Icon
143
+ className={slots.glyph()}
144
+ color={glyphColor}
145
+ icon={isIndeterminate ? IconMinusSmall : IconCheckmark1Small}
146
+ />
147
+ </Animated.View>
148
+ </Animated.View>
149
+ </Animated.View>
150
+ );
151
+ }
152
+ CheckboxBox.displayName = "DelacourUI.Checkbox.Box";
@@ -0,0 +1,107 @@
1
+ import { type ReactElement, type ReactNode, useCallback, useMemo } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { useControllableState } from "../../hooks/use-controllable-state";
4
+ import { type CheckboxGroupContextValue, CheckboxGroupProvider } from "./checkbox.context";
5
+ import {
6
+ type CheckboxAlignment,
7
+ type CheckboxColor,
8
+ type CheckboxSize,
9
+ checkboxVariants,
10
+ toggleCheckedValue,
11
+ } from "./checkbox.variants";
12
+
13
+ export type CheckboxGroupProps = Omit<ViewProps, "children"> & {
14
+ /** The `value` of every checked child. Controlled. */
15
+ checked?: string[];
16
+ /** The values checked to begin with, while uncontrolled. */
17
+ defaultChecked?: string[];
18
+ /** Fired with the whole list every time a child is toggled. */
19
+ onChecked?: (checked: string[]) => void;
20
+ /** Defaults every child takes unless it names its own. */
21
+ color?: CheckboxColor;
22
+ size?: CheckboxSize;
23
+ alignment?: CheckboxAlignment;
24
+ isInvalid?: boolean;
25
+ isDisabled?: boolean;
26
+ className?: string;
27
+ children?: ReactNode;
28
+ };
29
+
30
+ function CheckboxGroupRoot({
31
+ checked,
32
+ defaultChecked = EMPTY,
33
+ onChecked,
34
+ color,
35
+ size,
36
+ alignment,
37
+ isInvalid,
38
+ isDisabled,
39
+ className,
40
+ children,
41
+ ...props
42
+ }: CheckboxGroupProps): ReactElement {
43
+ const [values, setValues] = useControllableState<string[]>({
44
+ defaultValue: defaultChecked,
45
+ onChange: onChecked,
46
+ value: checked,
47
+ });
48
+
49
+ const toggle = useCallback((value: string) => setValues(toggleCheckedValue(values, value)), [setValues, values]);
50
+
51
+ const context = useMemo<CheckboxGroupContextValue>(
52
+ () => ({ alignment, checked: values, color, isDisabled, isInvalid, size, toggle }),
53
+ [alignment, color, isDisabled, isInvalid, size, toggle, values]
54
+ );
55
+
56
+ return (
57
+ <CheckboxGroupProvider value={context}>
58
+ <View className={checkboxVariants().group({ className })} {...props}>
59
+ {children}
60
+ </View>
61
+ </CheckboxGroupProvider>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * A stable empty list, so an uncontrolled group does not seed its state from a
67
+ * fresh array on every render.
68
+ */
69
+ const EMPTY: string[] = [];
70
+
71
+ /**
72
+ * The checked list for the checkboxes inside it, and the axes they share.
73
+ *
74
+ * State is one array of the children's `value`s: `checked={["email"]}` ticks the
75
+ * box called `email`, and `onChecked` fires with the whole new list each time
76
+ * one is toggled. Controlled or uncontrolled from the same hook — pass `checked`
77
+ * to own it, or `defaultChecked` and let the group hold it.
78
+ *
79
+ * **The axes here are defaults, not overrides.** A child's own `color` or `size`
80
+ * wins, and so does an explicit `isDisabled={false}` under a disabled group.
81
+ * That is the opposite of `Input.Group`, and the difference is what each owns:
82
+ * `Input.Group` *is* the box its field renders into, so the axes that draw one
83
+ * can only have a single answer. A `Checkbox.Group` owns no box — it is a
84
+ * wrapper supplying context, the same kind of thing as `Field`, and a control
85
+ * overrides one of those.
86
+ *
87
+ * A plain `View`, with no role. A group of checkboxes is a container, and
88
+ * announcing it as a control would put an element with no action in front of
89
+ * every child. Lay them out any other way with a `className` —
90
+ * `flex-row flex-wrap` for a row.
91
+ *
92
+ * @example
93
+ * <Checkbox.Group checked={channels} onChecked={setChannels}>
94
+ * <Checkbox value="email">Email</Checkbox>
95
+ * <Checkbox value="sms">SMS</Checkbox>
96
+ * <Checkbox value="push">Push</Checkbox>
97
+ * </Checkbox.Group>
98
+ *
99
+ * @example
100
+ * <Checkbox.Group color="success" defaultChecked={["daily"]} size="lg">
101
+ * <Checkbox value="daily">Daily digest</Checkbox>
102
+ * <Checkbox color="destructive" value="alerts">Incident alerts</Checkbox>
103
+ * </Checkbox.Group>
104
+ */
105
+ export const CheckboxGroup = Object.assign(CheckboxGroupRoot, {
106
+ displayName: "DelacourUI.Checkbox.Group",
107
+ });
@@ -0,0 +1,34 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useCheckboxPart } from "./checkbox.context";
4
+ import type { CheckboxLabelProps } from "./checkbox.types";
5
+ import { checkboxVariants, resolveCheckboxLabelColor, resolveCheckboxLabelSize } from "./checkbox.variants";
6
+
7
+ /**
8
+ * The checkbox's text, inside the checkbox's own tap target.
9
+ *
10
+ * That is the whole reason this part exists next to `Field.Label`, which does
11
+ * the same job a row away: this one sits inside the pressable, so tapping the
12
+ * words toggles the box. Use `Field.Label` for a checkbox that sits beside its
13
+ * name in a horizontal `Field`, and this one for a checkbox that carries its
14
+ * own.
15
+ *
16
+ * Renders `Text.Label` and passes it a step and a colour, never a class for
17
+ * either — the type scale belongs to the preset, and restating it here would be
18
+ * a second definition of `Text.Label` that could drift from it. The `className`
19
+ * carries layout alone. It turns destructive with the box it names, so the pair reads
20
+ * as one state.
21
+ */
22
+ export function CheckboxLabel({ className, color, size, ...props }: CheckboxLabelProps): ReactElement {
23
+ const { alignment, isInvalid, size: checkboxSize } = useCheckboxPart("Checkbox.Label");
24
+
25
+ return (
26
+ <Text.Label
27
+ className={checkboxVariants({ alignment }).label({ className })}
28
+ color={color ?? resolveCheckboxLabelColor(isInvalid)}
29
+ size={size ?? resolveCheckboxLabelSize(checkboxSize)}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+ CheckboxLabel.displayName = "DelacourUI.Checkbox.Label";
@@ -0,0 +1,136 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { CheckboxAlignment, CheckboxColor, CheckboxSize } from "./checkbox.variants";
3
+
4
+ export type CheckboxContextValue = {
5
+ /** What a ticked box means. */
6
+ color: CheckboxColor;
7
+ /** Size of the box, its glyph and the label beside it. */
8
+ size: CheckboxSize;
9
+ /** Which side of the label the box sits on. */
10
+ alignment: CheckboxAlignment;
11
+ /** Whether the box is ticked. */
12
+ isChecked: boolean;
13
+ /** Whether the box reports a mixed state rather than a settled one. */
14
+ isIndeterminate: boolean;
15
+ /** Whether the box reports an invalid value. */
16
+ isInvalid: boolean;
17
+ /** Whether the box is unavailable. */
18
+ isDisabled: boolean;
19
+ };
20
+
21
+ export type CheckboxGroupContextValue = {
22
+ /** The `value` of every checked child. */
23
+ checked: readonly string[];
24
+ /** Flips one child's value in that list. */
25
+ toggle: (value: string) => void;
26
+ /** Defaults a child takes when it names none of its own. */
27
+ color?: CheckboxColor;
28
+ size?: CheckboxSize;
29
+ alignment?: CheckboxAlignment;
30
+ isInvalid?: boolean;
31
+ isDisabled?: boolean;
32
+ };
33
+
34
+ const CheckboxContext = createContext<CheckboxContextValue | null>(null);
35
+
36
+ const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null);
37
+
38
+ /**
39
+ * Supplies the enclosing checkbox's axes and state to its subtree.
40
+ *
41
+ * Lives in its own module, importing nothing but React and a type, so a part can
42
+ * read it without importing `./checkbox`. That import would close a cycle, and
43
+ * Metro serves a partially initialised module for a cycle — leaving the context
44
+ * `undefined` at import time and red-boxing the app on a cold start.
45
+ */
46
+ export function CheckboxProvider({
47
+ value,
48
+ children,
49
+ }: {
50
+ value: CheckboxContextValue;
51
+ children: ReactNode;
52
+ }): ReactElement {
53
+ return <CheckboxContext value={value}>{children}</CheckboxContext>;
54
+ }
55
+ CheckboxProvider.displayName = "DelacourUI.Checkbox.Provider";
56
+
57
+ /** The enclosing checkbox's context, or null outside a `<Checkbox>`. */
58
+ export function useCheckboxContext(): CheckboxContextValue | null {
59
+ return use(CheckboxContext);
60
+ }
61
+
62
+ /**
63
+ * Reads the enclosing checkbox's axes and state.
64
+ *
65
+ * Lets a custom child style itself to match without the checkbox having to pass
66
+ * props down through every slot. Throws outside a `<Checkbox>` — use
67
+ * {@link useCheckboxContext} where the enclosing checkbox is optional.
68
+ */
69
+ export function useCheckbox(): CheckboxContextValue {
70
+ const context = useCheckboxContext();
71
+ if (!context) {
72
+ throw new Error("useCheckbox must be called inside a <Checkbox>.");
73
+ }
74
+ return context;
75
+ }
76
+
77
+ /**
78
+ * The enclosing checkbox's context, for a compound part that cannot work without
79
+ * one.
80
+ *
81
+ * Internal: deliberately not re-exported from `index.ts`. A caller outside the
82
+ * library wants {@link useCheckbox}, whose error message names the hook rather
83
+ * than a part.
84
+ */
85
+ export function useCheckboxPart(component: string): CheckboxContextValue {
86
+ const context = useCheckboxContext();
87
+ if (!context) {
88
+ throw new Error(`${component} must be rendered inside a <Checkbox>.`);
89
+ }
90
+ return context;
91
+ }
92
+
93
+ /**
94
+ * Supplies the enclosing group's checked list and its shared defaults.
95
+ *
96
+ * A second context rather than a field on the first: a checkbox reads its group
97
+ * to work out what it *is*, and publishes its own context describing what it
98
+ * *became*. One value carrying both would have to exist before it could be
99
+ * computed.
100
+ */
101
+ export function CheckboxGroupProvider({
102
+ value,
103
+ children,
104
+ }: {
105
+ value: CheckboxGroupContextValue;
106
+ children: ReactNode;
107
+ }): ReactElement {
108
+ return <CheckboxGroupContext value={value}>{children}</CheckboxGroupContext>;
109
+ }
110
+ CheckboxGroupProvider.displayName = "DelacourUI.Checkbox.Group.Provider";
111
+
112
+ /**
113
+ * The enclosing group's context, or null for a checkbox standing on its own.
114
+ *
115
+ * This is the export the root reads. It is nullable because a checkbox has to
116
+ * work perfectly well alone — a group is a layout and a state owner a caller
117
+ * opts into, not a wrapper anything requires.
118
+ */
119
+ export function useCheckboxGroupContext(): CheckboxGroupContextValue | null {
120
+ return use(CheckboxGroupContext);
121
+ }
122
+
123
+ /**
124
+ * Reads the enclosing group's checked list and defaults.
125
+ *
126
+ * For a custom control that has to sit in a group the way a `Checkbox` does.
127
+ * Throws outside one — use {@link useCheckboxGroupContext} where the group is
128
+ * optional, as the checkbox itself does.
129
+ */
130
+ export function useCheckboxGroup(): CheckboxGroupContextValue {
131
+ const context = useCheckboxGroupContext();
132
+ if (!context) {
133
+ throw new Error("useCheckboxGroup must be called inside a <Checkbox.Group>.");
134
+ }
135
+ return context;
136
+ }