@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,484 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { TextColor, TextSize } from "../text/text.variants";
4
+
5
+ /** What a ticked box means. The same six `Badge` paints, and for the same reason. */
6
+ export const CHECKBOX_COLORS = ["default", "primary", "success", "warning", "destructive", "info"] as const;
7
+
8
+ export const CHECKBOX_SIZES = ["sm", "md", "lg"] as const;
9
+
10
+ /** Which side of its label the box sits on. */
11
+ export const CHECKBOX_ALIGNMENTS = ["start", "end"] as const;
12
+
13
+ export type CheckboxColor = (typeof CHECKBOX_COLORS)[number];
14
+ export type CheckboxSize = (typeof CHECKBOX_SIZES)[number];
15
+ export type CheckboxAlignment = (typeof CHECKBOX_ALIGNMENTS)[number];
16
+
17
+ /**
18
+ * Theme token whose value gives the tick its colour.
19
+ *
20
+ * The checkbox's counterpart to `BADGE_FOREGROUND_TOKEN`, and it exists for the
21
+ * same reason: a colour that has to reach an SVG paint prop cannot be a class.
22
+ * Every entry names the `-foreground` of the fill its own `indicator` slot
23
+ * paints, so the glyph and the surface under it are never a shade apart — a test
24
+ * asserts the pair rather than trusting two maps to stay in step.
25
+ */
26
+ export const CHECKBOX_GLYPH_TOKEN: Record<CheckboxColor, string> = {
27
+ default: "secondary-foreground",
28
+ primary: "primary-foreground",
29
+ success: "success-foreground",
30
+ warning: "warning-foreground",
31
+ destructive: "destructive-foreground",
32
+ info: "info-foreground",
33
+ };
34
+
35
+ /** The tick's colour once the box is reporting an invalid value. */
36
+ export const CHECKBOX_INVALID_GLYPH_TOKEN = "destructive-foreground";
37
+
38
+ /**
39
+ * Theme token each colour paints its filled surface with.
40
+ *
41
+ * The same value the `indicator` slot names as a `bg-*`, as a token this time,
42
+ * because the border interpolates *to* it and a colour being animated has to be
43
+ * a value rather than a class. A test pins the two against each other.
44
+ *
45
+ * `default` is `secondary` rather than a colour of its own: this theme's
46
+ * `primary` is a near-black, and its neutral fill already exists.
47
+ */
48
+ export const CHECKBOX_SURFACE_TOKEN: Record<CheckboxColor, string> = {
49
+ default: "secondary",
50
+ primary: "primary",
51
+ success: "success",
52
+ warning: "warning",
53
+ destructive: "destructive",
54
+ info: "info",
55
+ };
56
+
57
+ /**
58
+ * The `rounded-*` step the box wears at each size.
59
+ *
60
+ * Named here as well as written into the `box` slot so the fill can be given
61
+ * the same corner — see {@link resolveCheckboxFillRadius}. A test asserts the
62
+ * two still agree.
63
+ */
64
+ export const CHECKBOX_RADIUS_STEP: Record<CheckboxSize, "xs" | "sm"> = { sm: "xs", md: "xs", lg: "sm" };
65
+
66
+ /**
67
+ * Width of the box's border in points — Tailwind's bare `border` utility.
68
+ *
69
+ * Restated here because two other things have to agree with it: the `indicator`
70
+ * slot's `-inset-px`, which is how the fill reaches past the padding box to
71
+ * cover the border ring, and `CheckboxBox`'s `onLayout`, which subtracts it
72
+ * twice to turn the measured border box back into the padding box the tick clip
73
+ * is positioned in.
74
+ */
75
+ export const CHECKBOX_BORDER_WIDTH = 1;
76
+
77
+ /**
78
+ * What each of those steps multiplies `--radius` by, restating `tokens.css`.
79
+ *
80
+ * It has to be restated: the corner scale is declared `@theme inline`, so
81
+ * Tailwind substitutes each step into the utilities that use it and emits no
82
+ * `--radius-xs` variable at all. `--radius` is the only one that survives to
83
+ * runtime, so anything computing a corner in JavaScript has to do the
84
+ * multiplication itself. `checkbox.variants.test.ts` pins both numbers against
85
+ * `tokens.css`, so retuning the scale fails the build rather than quietly
86
+ * leaving the fill behind.
87
+ */
88
+ export const CHECKBOX_RADIUS_MULTIPLIER: Record<"sm" | "xs", number> = { xs: 0.4, sm: 0.6 };
89
+
90
+ /**
91
+ * Corner radius of the animated fill, in points.
92
+ *
93
+ * The box's **own** radius, not a tighter one. The fill spans the whole border
94
+ * box — see the `indicator` slot's `-inset-px` — so it wears the same corner the
95
+ * box does and runs underneath the border ring rather than stopping against it.
96
+ *
97
+ * It was this minus the border width until it was not, which is the rule for two
98
+ * rounded rectangles to stay concentric — and concentric was the bug. Two curves
99
+ * that coincide are rasterised twice, on two layers, and antialiased
100
+ * independently: at a corner pixel where each gives coverage `a` the composite
101
+ * covers `2a - a²`, so `(1 - a)²` of the box's own `bg-card` bleeds through as a
102
+ * dull arc. The straight edges are pixel-aligned and show nothing, which is why
103
+ * the artifact was corner-only. Overlapping the two removes the shared edge
104
+ * instead of concealing it, and holds on both platforms even though iOS paints a
105
+ * `CALayer` border above its sublayers and Android paints it below.
106
+ *
107
+ * A function of the live `--radius` rather than a table of points, because
108
+ * `--radius` is a consumer's to set — a pasted theme retunes every corner in
109
+ * the package, and a fill left at a number tuned for the default would come
110
+ * away from the border the moment anyone did. Clamped at zero so a
111
+ * square-cornered theme draws a square fill rather than a negative radius.
112
+ *
113
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
114
+ */
115
+ export function resolveCheckboxFillRadius(size: CheckboxSize, radius: number): number {
116
+ const step = CHECKBOX_RADIUS_MULTIPLIER[CHECKBOX_RADIUS_STEP[size]];
117
+
118
+ return Math.max(0, radius * step);
119
+ }
120
+
121
+ /** The border of a box that is not filled — the same chrome a field wears. */
122
+ export const CHECKBOX_REST_BORDER_TOKEN = "input";
123
+
124
+ /** The border, filled or not, once the box is reporting an invalid value. */
125
+ export const CHECKBOX_INVALID_BORDER_TOKEN = "destructive";
126
+
127
+ /**
128
+ * Styling for every part of a checkbox.
129
+ *
130
+ * One slotted `tv()` rather than a call per part, so `size` is declared once
131
+ * across a box, the glyph inside it, the row's gap and the step the label asks
132
+ * its preset for. The sibling file is what makes that possible: the slot set is
133
+ * read by files that cannot import each other's roots without closing a cycle.
134
+ *
135
+ * **`color` paints the `indicator`, not the box.** The indicator is an
136
+ * absolute-fill layer that is invisible until the box is ticked, so a colour
137
+ * needs no unchecked branch — an unticked box is `border-input bg-card` at every
138
+ * colour, the same chrome a field wears. That leaves `compoundVariants` holding
139
+ * one cell rather than a thirty-six cell matrix.
140
+ *
141
+ * **The border is not a class at all.** It fades from the field chrome to the
142
+ * fill's own colour as the surface scales out to meet it, which is a value being
143
+ * interpolated rather than a class being swapped. {@link CHECKBOX_SURFACE_TOKEN}
144
+ * and {@link resolveCheckboxBorderTokens} name the two ends; the base keeps
145
+ * `border-input` as the resting appearance the animated style starts from.
146
+ *
147
+ * `overflow-hidden` on the box is load-bearing, not tidiness. The fill overhangs
148
+ * the border box by a point on every side, and on Android a border is painted
149
+ * below its children rather than above them — so the clip is what terminates the
150
+ * fill at the box's corner rather than a point outside it.
151
+ *
152
+ * `border` sits in the base rather than on the filled branch. A border declared
153
+ * only where it shows would move the glyph inside by a point the moment the box
154
+ * was ticked.
155
+ *
156
+ * **The box reads the shared icon scale, two steps above its own glyph.** It
157
+ * mints no scale of its own: a checkbox is a glyph in a box, both measurements
158
+ * are already on `--spacing-icon-*`, and a private `--spacing-checkbox-*` would
159
+ * be three numbers that have to be retuned in step with three others forever.
160
+ * Two steps is the gap that leaves the tick breathing room — 18/14, 20/16,
161
+ * 24/18 — and a test pins the offset rather than the numbers, so the scale can
162
+ * be retuned without the test becoming a transcript of it.
163
+ *
164
+ * **The `label` slot holds layout and nothing else** — no size, no weight, no
165
+ * colour. `Checkbox.Label` renders the `Text.Label` preset and passes a step and
166
+ * a colour through {@link resolveCheckboxLabelSize} and
167
+ * {@link resolveCheckboxLabelColor}, so the type scale lives in exactly one
168
+ * place. A `text-sm font-medium` here would be a second definition of
169
+ * `Text.Label` that could drift from it — `Field`'s rule, and the reason `Input`
170
+ * ships no label part at all. The `View` slots hold no `text-*` either: a React
171
+ * Native `View` does not cascade colour to a `Text` descendant. See AGENTS.md
172
+ * rule 1.
173
+ *
174
+ * **The row aligns to the top, and the box carries a margin that puts it back.**
175
+ * A wrapped label centred against its own paragraph drifts the box down the side
176
+ * of it instead of leaving it on the first line — the same thing `Input` refuses
177
+ * for a multiline field's decorators. `items-start` fixes that and would break
178
+ * the ordinary single-line case, so each size adds a top margin of half the
179
+ * difference between the label's line box and the box: 20/24/28 against
180
+ * 18/20/24, which is 1pt, 2pt and 2pt. One line then renders exactly as
181
+ * `items-center` did, and every line after it is a bonus rather than a
182
+ * regression.
183
+ *
184
+ * `opacity-50` for a disabled checkbox goes on the **row**, so the label fades
185
+ * with the box it names rather than standing at full strength beside a ghost.
186
+ *
187
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
188
+ * parse React Native's Flow-typed source. See AGENTS.md.
189
+ */
190
+ export const checkboxVariants = tv({
191
+ slots: {
192
+ root: "flex-row items-start",
193
+ box: "items-center justify-center overflow-hidden border border-input bg-card",
194
+ /**
195
+ * The animated fill. Runs under the box's border, and under the tick.
196
+ *
197
+ * `-inset-px` rather than `inset-0`, because an absolute child is positioned
198
+ * against the padding box: a negative inset of one border width is what
199
+ * reaches the border box. The overlap is what keeps the fill's edge and the
200
+ * border's from coinciding — see {@link resolveCheckboxFillRadius}.
201
+ */
202
+ indicator: "absolute -inset-px",
203
+ /**
204
+ * Clips the tick. Pinned to the box's left edge with an animated width, so
205
+ * the glyph is revealed across rather than grown into.
206
+ */
207
+ tick: "absolute inset-y-0 left-0 overflow-hidden",
208
+ /** Holds the glyph at the box's centre while the clip above it moves. */
209
+ tickInner: "h-full items-center justify-center",
210
+ /** Layout only. The treatment is the `Text.Label` preset's. */
211
+ label: "shrink",
212
+ /** Edge length the tick inherits. */
213
+ glyph: "",
214
+ /** `Checkbox.Group`'s column. */
215
+ group: "gap-4",
216
+ },
217
+ variants: {
218
+ // Paints the fill and nothing else — see the note above on why the box
219
+ // needs no unchecked branch per colour.
220
+ color: {
221
+ default: { indicator: "bg-secondary" },
222
+ primary: { indicator: "bg-primary" },
223
+ success: { indicator: "bg-success" },
224
+ warning: { indicator: "bg-warning" },
225
+ destructive: { indicator: "bg-destructive" },
226
+ info: { indicator: "bg-info" },
227
+ },
228
+ size: {
229
+ // The box's top margin is half the difference between the label's line
230
+ // box and the box itself — 20/24/28 against 18/20/24 — so a one-line
231
+ // label reads as centred and a wrapped one keeps the box on its first
232
+ // line. See the note above the slots.
233
+ // The box and the glyph inside it read the *same* scale, two steps
234
+ // apart — see the note above the slots.
235
+ sm: { root: "gap-2", box: "mt-px size-icon-md rounded-xs", glyph: "size-icon-xs" },
236
+ md: { root: "gap-2.5", box: "mt-0.5 size-icon-lg rounded-xs", glyph: "size-icon-sm" },
237
+ lg: { root: "gap-3", box: "mt-0.5 size-icon-xl rounded-sm", glyph: "size-icon-md" },
238
+ },
239
+ alignment: {
240
+ start: {},
241
+ // `flex-row-reverse` rather than a branch in the JSX: the box is
242
+ // rendered first either way, so the decision stays a class and the whole
243
+ // matrix stays reachable from `bun test`.
244
+ end: { root: "flex-row-reverse", label: "grow" },
245
+ },
246
+ // The empty `false` branches are load-bearing typing, not placeholders.
247
+ // `tv` derives the prop type from the declared keys, so a map with only
248
+ // `true` types the prop as `true` rather than `boolean` and rejects
249
+ // `checkboxVariants({ isFilled })`. See the note in button.variants.ts.
250
+ //
251
+ // Named `isFilled` rather than `isChecked` because checked and
252
+ // indeterminate both paint the surface and only the glyph tells them
253
+ // apart. `resolveCheckboxFilled` is that translation.
254
+ isFilled: { true: {}, false: {} },
255
+ isInvalid: { true: {}, false: {} },
256
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
257
+ },
258
+ compoundVariants: [
259
+ // Invalid outranks the colour on the fill, whether the box is ticked or
260
+ // not. A checkbox that stayed green while its value was rejected would
261
+ // drop the only signal it has, exactly while the value is being corrected
262
+ // — the precedence `Input` sets between invalid and focus.
263
+ //
264
+ // The *border* is not here. It interpolates between two token values as
265
+ // the fill approaches the edge, so it is a style rather than a class —
266
+ // see `resolveCheckboxBorderTokens`.
267
+ { isInvalid: true, class: { indicator: "bg-destructive" } },
268
+ ],
269
+ defaultVariants: {
270
+ color: "default",
271
+ size: "md",
272
+ alignment: "start",
273
+ isFilled: false,
274
+ isInvalid: false,
275
+ isDisabled: false,
276
+ },
277
+ });
278
+
279
+ /**
280
+ * [unfilled, filled] for the fill's two tracks, plus the tick's timing.
281
+ *
282
+ * The fill fades and scales **from the centre of the box**. It does not travel:
283
+ * a translation would make the surface arrive from one edge, which reads as a
284
+ * panel sliding in rather than as a box being filled, and there is no direction
285
+ * a checkbox is filled *from*.
286
+ *
287
+ * The tick is not part of that. It is clipped by a container whose width opens
288
+ * from the box's left edge, so the glyph is drawn on rather than faded up —
289
+ * which is what makes ticking look like ticking, and unticking look like the
290
+ * stroke being taken back. `tickDelay` holds it until the surface it is drawn on
291
+ * is most of the way there; starting both at once reads as one blurred event.
292
+ *
293
+ * One shared progress value drives all of it through `interpolate`, so the
294
+ * tracks cannot drift out of step and there is one animation rather than three.
295
+ *
296
+ * Pure data, so a test can pin that every track travels and that the filled end
297
+ * is a finished box rather than something stopped mid-animation.
298
+ */
299
+ export const CHECKBOX_INDICATOR_ANIMATION = {
300
+ opacity: [0, 1],
301
+ scale: [0.8, 1],
302
+ /** Fraction of the fill's travel that passes before the tick starts to draw. */
303
+ tickDelay: 0.25,
304
+ /**
305
+ * Fraction that passes before the border starts taking the fill's colour.
306
+ *
307
+ * Later than the tick, because the border should read as the surface
308
+ * *reaching* it rather than as a second thing changing alongside it. The fill
309
+ * scales 0.8 → 1, so it is only near the edge in the last of its travel.
310
+ */
311
+ borderDelay: 0.55,
312
+ durationMs: 140,
313
+ } as const;
314
+
315
+ /**
316
+ * Points of slop that bring a bare box up to the 44pt minimum touch target.
317
+ *
318
+ * New to this package, and deliberate: `Badge.CloseButton` needs none because it
319
+ * sits inside a padded capsule, while a bare `md` checkbox is a 20pt square with
320
+ * nothing around it.
321
+ */
322
+ export const CHECKBOX_HIT_SLOP: Record<CheckboxSize, number> = { sm: 13, md: 12, lg: 10 };
323
+
324
+ /**
325
+ * Whether the box paints its surface.
326
+ *
327
+ * Indeterminate fills exactly as checked does — only the glyph differs, a dash
328
+ * instead of a tick — so the two collapse to one visual state here and the tv()
329
+ * axis is named for that rather than for `checked`.
330
+ *
331
+ * Pure, so the matrix is reachable from `bun test`. See AGENTS.md.
332
+ */
333
+ export function resolveCheckboxFilled({
334
+ isChecked,
335
+ isIndeterminate,
336
+ }: {
337
+ isChecked: boolean;
338
+ isIndeterminate: boolean;
339
+ }): boolean {
340
+ return isChecked || isIndeterminate;
341
+ }
342
+
343
+ /** The axes a checkbox can take from itself, its group, or the field around it. */
344
+ export type CheckboxAxes = {
345
+ color?: CheckboxColor;
346
+ size?: CheckboxSize;
347
+ alignment?: CheckboxAlignment;
348
+ isInvalid?: boolean;
349
+ isDisabled?: boolean;
350
+ };
351
+
352
+ /** What an enclosing `Field` contributes — state only; it has no opinion on colour. */
353
+ export type CheckboxFieldAxes = { isInvalid?: boolean; isDisabled?: boolean };
354
+
355
+ /**
356
+ * The axes a checkbox actually draws with, given the wrappers around it.
357
+ *
358
+ * The ladder is `own ?? group ?? field ?? default`, on every axis, and the
359
+ * middle rung is where this differs from `Input`.
360
+ *
361
+ * `Input`'s ladder puts its group **first**, because `Input.Group` owns the one
362
+ * box a grouped field renders into — a field's own `variant` there would be a
363
+ * second answer to a question already settled. `Checkbox.Group` owns no box. It
364
+ * is a state controller that also carries shared defaults, which makes it the
365
+ * same kind of thing as `Field`: a wrapper a control can override. So "make the
366
+ * group `lg`" and "make this one destructive" are different questions, and both get
367
+ * an answer.
368
+ *
369
+ * `??` throughout and never `||`, so an explicit `false` opts a control out of
370
+ * an invalid field rather than reading as an absence.
371
+ *
372
+ * Pure, so the whole ladder is reachable from `bun test`. See AGENTS.md.
373
+ */
374
+ export function resolveCheckboxAxes({
375
+ own,
376
+ group,
377
+ field,
378
+ }: {
379
+ own: CheckboxAxes;
380
+ group?: CheckboxAxes;
381
+ field?: CheckboxFieldAxes;
382
+ }): Required<CheckboxAxes> {
383
+ return {
384
+ color: own.color ?? group?.color ?? "default",
385
+ size: own.size ?? group?.size ?? "md",
386
+ alignment: own.alignment ?? group?.alignment ?? "start",
387
+ isInvalid: own.isInvalid ?? group?.isInvalid ?? field?.isInvalid ?? false,
388
+ isDisabled: own.isDisabled ?? group?.isDisabled ?? field?.isDisabled ?? false,
389
+ };
390
+ }
391
+
392
+ /**
393
+ * The value list a group holds after one entry is toggled.
394
+ *
395
+ * Appends when absent and filters when present, so the order values were checked
396
+ * in survives and a duplicate that somehow got in is cleared rather than halved.
397
+ * Always a new array: React bails out of a re-render on an unchanged reference,
398
+ * so a mutation here would flip the state and leave the screen alone.
399
+ *
400
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
401
+ */
402
+ export function toggleCheckedValue(current: readonly string[], value: string): string[] {
403
+ return current.includes(value) ? current.filter((entry) => entry !== value) : [...current, value];
404
+ }
405
+
406
+ /**
407
+ * The `Text` step `Checkbox.Label` hands its preset.
408
+ *
409
+ * A size, never a class. The checkbox's own step names map onto `TEXT_SIZES`'
410
+ * own, so the two scales stay level without either restating the other.
411
+ *
412
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
413
+ */
414
+ export function resolveCheckboxLabelSize(size: CheckboxSize): TextSize {
415
+ switch (size) {
416
+ case "sm":
417
+ return "sm";
418
+ case "lg":
419
+ return "lg";
420
+ default:
421
+ return "md";
422
+ }
423
+ }
424
+
425
+ /**
426
+ * The `Text` colour `Checkbox.Label` hands its preset.
427
+ *
428
+ * Returning `undefined` is meaningful rather than lazy: `Text`'s colour axis
429
+ * emits nothing when it is not named, so the label falls through to
430
+ * `Text.Label`'s own `text-foreground`. The same contract `resolveFieldTextColor`
431
+ * uses, and it is what lets the label say "leave it alone".
432
+ *
433
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
434
+ */
435
+ export function resolveCheckboxLabelColor(isInvalid: boolean): TextColor | undefined {
436
+ return isInvalid ? "destructive" : undefined;
437
+ }
438
+
439
+ /**
440
+ * The slop a checkbox's tap target takes beyond the box itself.
441
+ *
442
+ * Only a bare box gets any. Once there is a label the whole row is the target
443
+ * and is already wide, and slop on top of that would overlap the next row's —
444
+ * making a tap between two checkboxes ambiguous, which is worse than the target
445
+ * being merely adequate.
446
+ *
447
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
448
+ */
449
+ export function resolveCheckboxHitSlop({
450
+ size,
451
+ hasLabel,
452
+ }: {
453
+ size: CheckboxSize;
454
+ hasLabel: boolean;
455
+ }): number | undefined {
456
+ return hasLabel ? undefined : CHECKBOX_HIT_SLOP[size];
457
+ }
458
+
459
+ /**
460
+ * The two theme tokens a box's border travels between.
461
+ *
462
+ * `rest` is what an unfilled box wears and `active` is what a filled one settles
463
+ * on — the fill's own colour, so the border reads as the surface having reached
464
+ * the edge rather than as an outline that changed on its own.
465
+ *
466
+ * An invalid box returns destructive for **both**, so there is nothing to fade: the
467
+ * border is the signal that the value is wrong, and it has to be there before
468
+ * the box is ticked as much as after. That is the same precedence `Input` sets
469
+ * between invalid and focus, expressed as a pair of endpoints instead of a
470
+ * compound variant.
471
+ *
472
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
473
+ */
474
+ export function resolveCheckboxBorderTokens({ color, isInvalid }: { color: CheckboxColor; isInvalid: boolean }): {
475
+ rest: string;
476
+ active: string;
477
+ } {
478
+ if (isInvalid) {
479
+ return { active: CHECKBOX_INVALID_BORDER_TOKEN, rest: CHECKBOX_INVALID_BORDER_TOKEN };
480
+ }
481
+ return { active: CHECKBOX_SURFACE_TOKEN[color], rest: CHECKBOX_REST_BORDER_TOKEN };
482
+ }
483
+
484
+ export type CheckboxVariantProps = VariantProps<typeof checkboxVariants>;
@@ -0,0 +1,42 @@
1
+ export { Checkbox, type CheckboxProps } from "./checkbox";
2
+ export {
3
+ type CheckboxContextValue,
4
+ type CheckboxGroupContextValue,
5
+ CheckboxGroupProvider,
6
+ CheckboxProvider,
7
+ useCheckbox,
8
+ useCheckboxContext,
9
+ useCheckboxGroup,
10
+ useCheckboxGroupContext,
11
+ } from "./checkbox.context";
12
+ export type { CheckboxLabelProps } from "./checkbox.types";
13
+ export {
14
+ CHECKBOX_ALIGNMENTS,
15
+ CHECKBOX_BORDER_WIDTH,
16
+ CHECKBOX_COLORS,
17
+ CHECKBOX_GLYPH_TOKEN,
18
+ CHECKBOX_HIT_SLOP,
19
+ CHECKBOX_INDICATOR_ANIMATION,
20
+ CHECKBOX_INVALID_BORDER_TOKEN,
21
+ CHECKBOX_INVALID_GLYPH_TOKEN,
22
+ CHECKBOX_RADIUS_STEP,
23
+ CHECKBOX_REST_BORDER_TOKEN,
24
+ CHECKBOX_SIZES,
25
+ CHECKBOX_SURFACE_TOKEN,
26
+ type CheckboxAlignment,
27
+ type CheckboxAxes,
28
+ type CheckboxColor,
29
+ type CheckboxFieldAxes,
30
+ type CheckboxSize,
31
+ type CheckboxVariantProps,
32
+ checkboxVariants,
33
+ resolveCheckboxAxes,
34
+ resolveCheckboxBorderTokens,
35
+ resolveCheckboxFilled,
36
+ resolveCheckboxFillRadius,
37
+ resolveCheckboxHitSlop,
38
+ resolveCheckboxLabelColor,
39
+ resolveCheckboxLabelSize,
40
+ toggleCheckedValue,
41
+ } from "./checkbox.variants";
42
+ export type { CheckboxGroupProps } from "./checkbox-group";
@@ -0,0 +1,112 @@
1
+ # Field
2
+
3
+ A form field's layout, and the one place its state is written down. Root plus
4
+ `Field.Set`, `Field.Legend`, `Field.Group`, `Field.Content`, `Field.Label`,
5
+ `Field.Description`, `Field.Error` and `Field.Separator`.
6
+
7
+ `import { Field } from "@delacour/react-native-ui/field";`
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | --- | --- |
13
+ | `index.ts` | → `@delacour/react-native-ui/field` |
14
+ | `field.tsx` | Root + the `Object.assign` compound surface |
15
+ | `field-set.tsx` | `Field.Set` |
16
+ | `field-legend.tsx` | `Field.Legend` |
17
+ | `field-group.tsx` | `Field.Group` |
18
+ | `field-content.tsx` | `Field.Content` |
19
+ | `field-label.tsx` | `Field.Label` |
20
+ | `field-description.tsx` | `Field.Description` |
21
+ | `field-error.tsx` | `Field.Error` |
22
+ | `field-separator.tsx` | `Field.Separator` |
23
+ | `field.context.tsx` | `FieldProvider`, `useField()`, `useFieldContext()` |
24
+ | `field.types.ts` | Prop types shared by two or more parts |
25
+ | `field.variants.ts` | Pure `tv()` slots + resolvers, no RN imports |
26
+ | `field.variants.test.ts` | |
27
+
28
+ ## Design
29
+
30
+ - **Orientations**: `vertical` (default), `horizontal`.
31
+ - **The cascade is a context, and it had to be.** `<Field isInvalid>` reddens the
32
+ control inside it, not just its own label. On the web shadcn does that with
33
+ `group-data-[invalid=true]/field:` — a parent-scoped selector. Uniwind has no
34
+ equivalent: its compiler reads `data-*` off a **single flat selector**
35
+ (`bundler/css-processor/processor.ts`) and its runtime matches them against
36
+ `props[attribute]` on **the component carrying the class**
37
+ (`core/native/store.ts`), so no class on a `Field` can reach the
38
+ [`Input`](../input/AGENTS.md) inside it. There is no `group-*`, no `peer-*`,
39
+ no `:has()`. Do not go looking for one again.
40
+ - **The whole row drives the control, once one offers a press.** A control
41
+ registers a callback through the same context the state cascades down, and the
42
+ row becomes a `Pressable` with `feedback="none"` that calls it — so tapping
43
+ "Accept the terms", or the description under it, ticks the
44
+ [`Checkbox`](../checkbox/AGENTS.md) beside
45
+ it. A checkbox in a form is a small square next to a sentence, and the sentence
46
+ is what people aim at. This is [`Input.Group`](../input/AGENTS.md)'s trick one level out: that group
47
+ is a pressable whose press focuses the field through a ref on its context.
48
+ `resolveFieldInteractive` is the decision and it is pure, so `bun test` reaches
49
+ it. A field of static text registers nothing and stays a `View` — mounting a
50
+ detector regardless would put one under every label and description in a form,
51
+ the thing [`Badge`](../badge/AGENTS.md) refuses for a list of fifty tags. The row is
52
+ `accessible={false}`, so the control stays the element a screen reader sees,
53
+ and the inner detector claims a tap on the box itself rather than firing both.
54
+ A field holds one control, so a second registration replaces the first.
55
+ - **The label names the control, through the same context.** `Field.Label`
56
+ registers its text with `registerLabel`, the mirror of `registerPress`: a
57
+ control hands its press up, a label hands its name down. A control with no
58
+ text of its own — a [Slider](../slider/AGENTS.md) thumb is a capsule — reads
59
+ `field.label` as its `accessibilityLabel`, so "Volume" is announced rather than
60
+ a bare "40". React Native has no `<label for>`; this is that association, done
61
+ by hand. `resolveFieldLabelText` decides what qualifies and it is strict on
62
+ purpose: a plain string or number, trimmed, and nothing else. A label built
63
+ from elements has no text this side of a render, and guessing at one — an
64
+ icon's key, say — would name the control after the wrong thing. State rather
65
+ than a ref for the same reason as the press: the control renders differently
66
+ for it, and the label registers in an effect, so it costs one render on mount.
67
+ - **A data-attribute class would also leave `bun test`.** Even for a part styling
68
+ itself, `data-invalid:text-destructive` moves the decision from `field.variants.ts`
69
+ into uniwind's runtime matcher, where no unit test can see it. The parts style
70
+ themselves from `tv()` booleans; the context is only for crossing a component
71
+ boundary.
72
+ - **The text parts render the `Text` presets and pass a colour, never a scale.**
73
+ `Field.Label` *is* [`Text.Label`](../text/AGENTS.md); `Field.Description` and `Field.Error` are
74
+ `Text.Caption`. `resolveFieldTextColor` picks the colour and returns
75
+ `undefined` to mean "leave the preset's own alone" — which is exactly what
76
+ `Text`'s unnamed axes do. A `text-sm font-medium` in a slot here would be a
77
+ second definition of `Text.Label`, the thing that kept [`Input`](../input/AGENTS.md) from shipping a
78
+ label part at all. A test asserts the text slots carry no size, weight or
79
+ colour.
80
+ - **The gap ladder is the component.** `content` 0.5 → `root` 1.5 → `set` 4 →
81
+ `group` 5. A label attaches to the control beneath it rather than the one above
82
+ purely because the gap inside a field is tighter than the gap between two, and
83
+ nothing else is doing that work. The test pins the **ordering**, not the
84
+ numbers, so the spacing can be retuned without the test becoming a transcript
85
+ of it.
86
+ - **Only the label fades when disabled.** The control dims itself, and a dimmed
87
+ description stacked on a dimmed control reads as two problems rather than one
88
+ state. The description stays muted when invalid too, so an appearing
89
+ `Field.Error` is the one line that changed.
90
+ - **`Field.Error` renders nothing when it has no children**, so
91
+ `<Field.Error>{error}</Field.Error>` removes itself once the value is fixed.
92
+ It is deliberately **not** gated on `isInvalid`: a part that swallowed children
93
+ a caller actually wrote, because of a prop on a sibling, would be a part whose
94
+ absence is unexplainable from the call site. shadcn's `errors` array prop is
95
+ not ported — it exists to accept react-hook-form and Standard Schema shapes,
96
+ and this package takes no form dependency.
97
+ - **`Field.Separator` draws two rules, not one with a label on top.** The web
98
+ version absolutely-positions a single rule and punches a hole in it with an
99
+ opaque `bg-background` label, which is invisible only while the separator sits
100
+ on exactly that colour — on a card or a sheet the hole shows as a block of the
101
+ wrong shade. Two rules and a gap assume nothing about what is behind them. The
102
+ playground's `/field/grouping` has the card case on screen.
103
+ - **`Field.Group` inserts no dividers**, unlike [`ListGroup`](../list-group/AGENTS.md). A list of rows
104
+ without lines is a wall of text; fields are already held apart by whitespace,
105
+ and a rule between every one is noise.
106
+ - **There is no `Field.Title`.** On the web it exists because a `<div>` is not a
107
+ `<label>` — label-styled text with nothing to point `htmlFor` at. React Native
108
+ has neither element nor association, so it and `Field.Label` would render the
109
+ same `Text`.
110
+ - **A set holds no state.** `isInvalid` and `isDisabled` live on each `Field`,
111
+ because a whole section going `destructive` says less than the one field that is
112
+ actually wrong.
@@ -0,0 +1,24 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { useFieldContext } from "./field.context";
4
+ import { fieldVariants } from "./field.variants";
5
+
6
+ export type FieldContentProps = ViewProps & { className?: string };
7
+
8
+ /**
9
+ * A label and its description, bundled into one block beside a control.
10
+ *
11
+ * Only needed in a horizontal field, and only when there is a description: it
12
+ * makes the text one flex child so the row lays out as `[text] [control]`
13
+ * instead of `[label] [description] [control]`. `flex-1` is what pushes the
14
+ * control to the far edge; `min-w-0` is what lets the text wrap instead of
15
+ * forcing the row wider than the screen.
16
+ */
17
+ export function FieldContent({ className, ...props }: FieldContentProps): ReactElement {
18
+ const field = useFieldContext();
19
+ const isInvalid = field?.isInvalid ?? false;
20
+ const isDisabled = field?.isDisabled ?? false;
21
+
22
+ return <View className={fieldVariants({ isDisabled, isInvalid }).content({ className })} {...props} />;
23
+ }
24
+ FieldContent.displayName = "DelacourUI.Field.Content";