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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +192 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +435 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +232 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +245 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +148 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +95 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +276 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +100 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +33 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +78 -0
  108. package/src/components/field/field.tsx +144 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +216 -0
  111. package/src/components/field/field.variants.ts +136 -0
  112. package/src/components/field/index.ts +21 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +64 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +256 -0
  146. package/src/components/pressable/pressable.variants.test.ts +99 -0
  147. package/src/components/pressable/pressable.variants.ts +63 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +247 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +581 -0
  159. package/src/components/radio/radio.variants.ts +272 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +48 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +161 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +804 -0
  202. package/src/components/slider/slider.variants.ts +607 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +286 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +672 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,61 @@
1
+ import { Children, type ReactElement, type ReactNode } from "react";
2
+ import { Text } from "../text";
3
+ import { useFieldContext } from "./field.context";
4
+ import type { FieldTextProps } from "./field.types";
5
+ import { fieldVariants, resolveFieldTextColor } from "./field.variants";
6
+
7
+ /**
8
+ * `Text` renders `Animated.Text`, so its `children` may also be a Reanimated
9
+ * `SharedValue<ReactNode>`. An error message is a string, and this part has to
10
+ * count its children to know whether to render at all, so the narrower type is
11
+ * both the honest one and the one that can be counted.
12
+ */
13
+ export type FieldErrorProps = Omit<FieldTextProps, "children"> & { children?: ReactNode };
14
+
15
+ /**
16
+ * What is wrong with the value.
17
+ *
18
+ * Renders nothing when it has no children, so
19
+ * `<Field.Error>{errors.email}</Field.Error>` disappears on its own once the
20
+ * value is fixed and needs no conditional at the call site.
21
+ *
22
+ * Deliberately **not** gated on the field's `isInvalid`. A part that swallowed
23
+ * the children a caller actually wrote, because of a prop on a sibling, would be
24
+ * a part whose absence is unexplainable from the call site — and pairing the two
25
+ * is the caller's own `isInvalid={!!error}`, which is one expression, in view.
26
+ *
27
+ * Always destructive, in a valid field as much as an invalid one: an error message is
28
+ * never the calm case.
29
+ */
30
+ export function FieldError({ className, color, children, ...props }: FieldErrorProps): ReactElement | null {
31
+ const field = useFieldContext();
32
+ const isInvalid = field?.isInvalid ?? false;
33
+ const isDisabled = field?.isDisabled ?? false;
34
+
35
+ if (!hasContent(children)) return null;
36
+
37
+ return (
38
+ <Text.Caption
39
+ accessibilityLiveRegion="polite"
40
+ className={fieldVariants({ isDisabled, isInvalid }).error({ className })}
41
+ color={color ?? resolveFieldTextColor("error", isInvalid)}
42
+ role="alert"
43
+ {...props}
44
+ >
45
+ {children}
46
+ </Text.Caption>
47
+ );
48
+ }
49
+ FieldError.displayName = "DelacourUI.Field.Error";
50
+
51
+ /**
52
+ * Whether there is anything worth drawing a line of destructive text for.
53
+ *
54
+ * `Children.toArray` drops the `null`, `undefined` and booleans a conditional
55
+ * child leaves behind, so `{error && <Text>{error}</Text>}` counts as empty
56
+ * rather than as one child. An empty string is dropped too — it is what a
57
+ * cleared validation message usually is.
58
+ */
59
+ function hasContent(children: ReactNode): boolean {
60
+ return Children.toArray(children).some((child) => child !== "");
61
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { fieldVariants } from "./field.variants";
4
+
5
+ export type FieldGroupProps = ViewProps & { className?: string };
6
+
7
+ /**
8
+ * A stack of fields, spaced so that two fields never read as one.
9
+ *
10
+ * Its gap is the loosest step in the ladder — looser than the gap inside a
11
+ * field — which is the whole mechanism by which a label attaches to the control
12
+ * under it rather than to the one above.
13
+ *
14
+ * **Inserts no dividers**, unlike `ListGroup`. A list of rows without lines is a
15
+ * wall of text, but fields are already separated by whitespace and a rule
16
+ * between every one of them is noise. Place a `Field.Separator` where a form
17
+ * genuinely changes subject.
18
+ */
19
+ export function FieldGroup({ className, ...props }: FieldGroupProps): ReactElement {
20
+ return <View className={fieldVariants().group({ className })} {...props} />;
21
+ }
22
+ FieldGroup.displayName = "DelacourUI.Field.Group";
@@ -0,0 +1,33 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useFieldContext } from "./field.context";
4
+ import type { FieldTextProps } from "./field.types";
5
+ import { fieldVariants, resolveFieldTextColor } from "./field.variants";
6
+
7
+ /**
8
+ * The name of the control this field holds.
9
+ *
10
+ * Renders `Text.Label` and passes it a colour, never a size or a weight — the
11
+ * type scale belongs to the preset, and restating it here would be a second
12
+ * definition of `Text.Label` that could drift from it. It turns destructive with the
13
+ * control when the field is invalid, so the pair reads as one state.
14
+ *
15
+ * There is no `htmlFor`. React Native has no `<label>` element and no
16
+ * label-for-control association, which is also why there is no `Field.Title`:
17
+ * on the web that part exists only because a `<div>` is not a `<label>`, and
18
+ * here both would be the same `Text`.
19
+ */
20
+ export function FieldLabel({ className, color, ...props }: FieldTextProps): ReactElement {
21
+ const field = useFieldContext();
22
+ const isInvalid = field?.isInvalid ?? false;
23
+ const isDisabled = field?.isDisabled ?? false;
24
+
25
+ return (
26
+ <Text.Label
27
+ className={fieldVariants({ isDisabled, isInvalid }).label({ className })}
28
+ color={color ?? resolveFieldTextColor("label", isInvalid)}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+ FieldLabel.displayName = "DelacourUI.Field.Label";
@@ -0,0 +1,34 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import type { FieldTextProps } from "./field.types";
4
+ import { type FieldLegendVariant, fieldVariants } from "./field.variants";
5
+
6
+ export type FieldLegendProps = FieldTextProps & {
7
+ /** `legend` titles a set; `label` matches the fields under it. */
8
+ variant?: FieldLegendVariant;
9
+ };
10
+
11
+ /**
12
+ * The title of a `Field.Set`.
13
+ *
14
+ * Two sizes, chosen by picking a `Text` preset rather than by naming a scale:
15
+ * `legend` renders `Text.Paragraph` with a medium weight — a heading for a
16
+ * section of a form — and `label` renders `Text.Label`, which is the treatment
17
+ * the fields below it already use. Reach for `label` when the set is nested
18
+ * inside another, where a second full-size heading would compete with the first.
19
+ *
20
+ * Reads no context: a legend titles a set, and a set holds no state of its own —
21
+ * invalid and disabled belong to each `Field` inside it. That is also why it
22
+ * takes no `isInvalid`; a whole section turning destructive says less than the one
23
+ * field that is wrong.
24
+ */
25
+ export function FieldLegend({ className, variant = "legend", ...props }: FieldLegendProps): ReactElement {
26
+ const legendClassName = fieldVariants({ variant }).legend({ className });
27
+
28
+ if (variant === "label") {
29
+ return <Text.Label className={legendClassName} {...props} />;
30
+ }
31
+
32
+ return <Text.Paragraph className={legendClassName} {...props} />;
33
+ }
34
+ FieldLegend.displayName = "DelacourUI.Field.Legend";
@@ -0,0 +1,81 @@
1
+ import { Children, type ReactElement, type ReactNode } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { Separator } from "../separator";
4
+ import { Text } from "../text";
5
+ import { TextClassProvider } from "../text/text.context";
6
+ import { fieldVariants } from "./field.variants";
7
+
8
+ export type FieldSeparatorProps = Omit<ViewProps, "children"> & {
9
+ className?: string;
10
+ /** Optional label, drawn between two rules rather than on top of one. */
11
+ children?: ReactNode;
12
+ };
13
+
14
+ /**
15
+ * A rule between two sections of a form, optionally labelled.
16
+ *
17
+ * With children it draws **two rules with the label between them**, not one rule
18
+ * with the label sitting on top. The web version absolutely-positions a single
19
+ * rule and punches a hole in it with an opaque `bg-background` label, which is
20
+ * only invisible while the separator sits on exactly that colour — put it on a
21
+ * card, a sheet or a tinted section and the hole shows as a block of the wrong
22
+ * shade. Two rules and a gap assume nothing about what is behind them.
23
+ *
24
+ * Hidden from assistive technology when it is unlabelled, for the reason
25
+ * `Separator` itself is: a bare line carries nothing a screen reader can use. A
26
+ * labelled one keeps its text, which is the part worth announcing.
27
+ */
28
+ export function FieldSeparator({ className, children, ...props }: FieldSeparatorProps): ReactElement {
29
+ const slots = fieldVariants();
30
+ const lineClassName = slots.separatorLine();
31
+
32
+ if (!hasContent(children)) {
33
+ return <Separator className={className} {...props} />;
34
+ }
35
+
36
+ return (
37
+ <View className={slots.separator({ className })} {...props}>
38
+ <Separator className={lineClassName} />
39
+ <TextClassProvider value={undefined}>{wrapTextChildren(children)}</TextClassProvider>
40
+ <Separator className={lineClassName} />
41
+ </View>
42
+ );
43
+ }
44
+ FieldSeparator.displayName = "DelacourUI.Field.Separator";
45
+
46
+ /** Whether there is a label to make room for between the rules. */
47
+ function hasContent(children: ReactNode): boolean {
48
+ return Children.toArray(children).some((child) => child !== "");
49
+ }
50
+
51
+ /**
52
+ * Wraps a bare string label in a `Text.Caption`.
53
+ *
54
+ * React Native cannot render a string outside a `<Text>`, and
55
+ * `<Field.Separator>Or</Field.Separator>` is the shortest thing anyone will
56
+ * write. Consecutive strings collapse into one, so an interpolated label is a
57
+ * single run rather than several spaced apart by the row's own gap.
58
+ */
59
+ function wrapTextChildren(children: ReactNode): ReactNode {
60
+ const items = Children.toArray(children);
61
+ const output: ReactNode[] = [];
62
+ let run: (string | number)[] = [];
63
+
64
+ const flushRun = () => {
65
+ if (run.length === 0) return;
66
+ output.push(<Text.Caption key={`label-${output.length}`}>{run.join("")}</Text.Caption>);
67
+ run = [];
68
+ };
69
+
70
+ for (const child of items) {
71
+ if (typeof child === "string" || typeof child === "number") {
72
+ run.push(child);
73
+ continue;
74
+ }
75
+ flushRun();
76
+ output.push(child);
77
+ }
78
+ flushRun();
79
+
80
+ return output;
81
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReactElement } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { fieldVariants } from "./field.variants";
4
+
5
+ export type FieldSetProps = ViewProps & { className?: string };
6
+
7
+ /**
8
+ * A named section of a form — a legend, an optional description, and the fields
9
+ * under them.
10
+ *
11
+ * The step above `Field.Group`: a group is spacing and a set is meaning. Reach
12
+ * for a set when the fields share a heading a reader needs, and for a bare group
13
+ * when they are simply a list.
14
+ *
15
+ * Holds no state. Invalid and disabled belong to each `Field`, because a section
16
+ * that turned destructive as a whole would say less than the one field that is
17
+ * actually wrong.
18
+ */
19
+ export function FieldSet({ className, ...props }: FieldSetProps): ReactElement {
20
+ return <View className={fieldVariants().set({ className })} {...props} />;
21
+ }
22
+ FieldSet.displayName = "DelacourUI.Field.Set";
@@ -0,0 +1,78 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { FieldOrientation } from "./field.variants";
3
+
4
+ export type FieldContextValue = {
5
+ /** Whether the field reports an invalid value. */
6
+ isInvalid: boolean;
7
+ /** Whether the field's control is unavailable. */
8
+ isDisabled: boolean;
9
+ /** Which way the field's parts are laid out. */
10
+ orientation: FieldOrientation;
11
+ /**
12
+ * A control inside offers its press here, so the whole row can drive it.
13
+ *
14
+ * The counterpart of `isInvalid` travelling the other way: state cascades
15
+ * down, and the one thing a control can hand back up is "tapping my label
16
+ * should do this". Pass `null` on unmount. A field holds one control, so a
17
+ * second registration replaces the first.
18
+ */
19
+ registerPress: (press: (() => void) | null) => void;
20
+ };
21
+
22
+ const FieldContext = createContext<FieldContextValue | null>(null);
23
+
24
+ /**
25
+ * Supplies the enclosing field's state to its subtree.
26
+ *
27
+ * This is the whole reason `Field` has a context rather than a set of classes.
28
+ * On the web, shadcn cascades a field's invalid state to its control with
29
+ * `group-data-[invalid=true]/field:` — a parent-scoped selector. Uniwind has no
30
+ * equivalent: its compiler reads `data-*` off a single flat selector and its
31
+ * runtime matches them against the props of **the same component**, so a class
32
+ * on a `Field` cannot reach the `Input` inside it. A context can.
33
+ *
34
+ * So `<Field isInvalid>` turns the control inside it destructive with nothing said at
35
+ * the control, and the same channel will serve `Checkbox` and `Radio` when they
36
+ * land. An explicit prop on the control still wins — see `Input`'s ladder.
37
+ *
38
+ * Lives in its own module, importing nothing but React and a type, so a
39
+ * component in another folder can read it without importing `../field` and
40
+ * closing a cycle. See AGENTS.md rule 3.
41
+ *
42
+ * **There is no `useFieldPart` here**, unlike every other compound in this
43
+ * package, because no part of this one needs a `Field` to work. A description
44
+ * under a `Field.Set` describes the whole set, and a label under one is a
45
+ * section heading — both are compositions the web kit documents, and a hook that
46
+ * threw would turn them into a red box. Every part reads
47
+ * {@link useFieldContext} and falls back to "not invalid, not disabled", so
48
+ * state applies where there is a field and nothing happens where there is not.
49
+ */
50
+ export function FieldProvider({ value, children }: { value: FieldContextValue; children: ReactNode }): ReactElement {
51
+ return <FieldContext value={value}>{children}</FieldContext>;
52
+ }
53
+ FieldProvider.displayName = "DelacourUI.Field.Provider";
54
+
55
+ /**
56
+ * The enclosing field's state, or null outside a `<Field>`.
57
+ *
58
+ * This is the export a control reads. It is nullable because a control has to
59
+ * work perfectly well on its own — a `Field` is a layout a caller opts into, not
60
+ * a wrapper anything requires.
61
+ */
62
+ export function useFieldContext(): FieldContextValue | null {
63
+ return use(FieldContext);
64
+ }
65
+
66
+ /**
67
+ * Reads the enclosing field's state.
68
+ *
69
+ * For a custom part that has to match the field it sits in. Throws outside one —
70
+ * use {@link useFieldContext} where the field is optional, as every control does.
71
+ */
72
+ export function useField(): FieldContextValue {
73
+ const context = useFieldContext();
74
+ if (!context) {
75
+ throw new Error("useField must be called inside a <Field>.");
76
+ }
77
+ return context;
78
+ }
@@ -0,0 +1,144 @@
1
+ import { type ReactElement, useCallback, useMemo, useState } from "react";
2
+ import { View, type ViewProps } from "react-native";
3
+ import { Pressable } from "../pressable";
4
+ import { type FieldContextValue, FieldProvider } from "./field.context";
5
+ import { type FieldOrientation, fieldVariants, resolveFieldInteractive } from "./field.variants";
6
+ import { FieldContent } from "./field-content";
7
+ import { FieldDescription } from "./field-description";
8
+ import { FieldError } from "./field-error";
9
+ import { FieldGroup } from "./field-group";
10
+ import { FieldLabel } from "./field-label";
11
+ import { FieldLegend } from "./field-legend";
12
+ import { FieldSeparator } from "./field-separator";
13
+ import { FieldSet } from "./field-set";
14
+
15
+ export type FieldProps = ViewProps & {
16
+ /** `vertical` stacks label over control; `horizontal` puts them side by side. */
17
+ orientation?: FieldOrientation;
18
+ /** Reports an invalid value. The label and the control inside both turn destructive. */
19
+ isInvalid?: boolean;
20
+ /** The control inside is unavailable. Fades the label and disables the control. */
21
+ isDisabled?: boolean;
22
+ className?: string;
23
+ };
24
+
25
+ function FieldRoot({
26
+ orientation = "vertical",
27
+ isInvalid = false,
28
+ isDisabled = false,
29
+ className,
30
+ ...props
31
+ }: FieldProps): ReactElement {
32
+ // State rather than a ref, because the answer changes what the row renders
33
+ // as. A control registers in an effect, so this costs one extra render on
34
+ // mount and none afterwards — the registered callback is stable.
35
+ const [controlPress, setControlPress] = useState<(() => void) | null>(null);
36
+
37
+ // The updater form would call `press` instead of storing it.
38
+ const registerPress = useCallback((press: (() => void) | null) => setControlPress(() => press), []);
39
+
40
+ const context = useMemo<FieldContextValue>(
41
+ () => ({ isDisabled, isInvalid, orientation, registerPress }),
42
+ [isDisabled, isInvalid, orientation, registerPress]
43
+ );
44
+
45
+ const rootClassName = fieldVariants({ isDisabled, isInvalid, orientation }).root({ className });
46
+
47
+ // A field is layout until a control asks for the row — see `resolveFieldInteractive`.
48
+ if (!resolveFieldInteractive(controlPress)) {
49
+ return (
50
+ <FieldProvider value={context}>
51
+ <View accessibilityRole="none" className={rootClassName} {...props} />
52
+ </FieldProvider>
53
+ );
54
+ }
55
+
56
+ return (
57
+ <FieldProvider value={context}>
58
+ <Pressable
59
+ // The control inside is the accessibility element, not this row.
60
+ // Without it a screen reader would announce the label twice and
61
+ // offer a wrapper with no state of its own.
62
+ accessible={false}
63
+ className={rootClassName}
64
+ disabled={isDisabled}
65
+ feedback="none"
66
+ onPress={controlPress}
67
+ {...props}
68
+ />
69
+ </FieldProvider>
70
+ );
71
+ }
72
+
73
+ /**
74
+ * One control, with its label, its description and its error.
75
+ *
76
+ * The layout every form in this library repeats, done once — and the one place a
77
+ * field's state is written down. `<Field isInvalid>` turns the label destructive
78
+ * **and the control inside it too**, with nothing said at the control: `Input`
79
+ * and `Checkbox` both read this field's context, and `Radio` will read the same one.
80
+ *
81
+ * That cascade is a React context rather than a class because it has to be.
82
+ * Uniwind's data selectors match against the props of the component carrying the
83
+ * class, so there is no parent-scoped `group-data-*` for a `Field` to style its
84
+ * control with. See `field.context.tsx`.
85
+ *
86
+ * A control's own prop still wins, so one field inside an invalid group can opt
87
+ * out with `<Input isInvalid={false} />`.
88
+ *
89
+ * **The whole row drives the control**, once one offers a press through the same
90
+ * context — so tapping "Accept the terms", or the description under it, ticks
91
+ * the `Checkbox` beside it. A checkbox in a form is a small square next to a
92
+ * sentence, and the sentence is the part people aim at. The row is a `Pressable`
93
+ * with `feedback="none"` only while a control has registered; a field of static
94
+ * text mounts no gesture detector at all.
95
+ *
96
+ * The text parts render the `Text` presets and pass a colour, never a size —
97
+ * `Field.Label` *is* `Text.Label`. There is no `Field.Title`: on the web it
98
+ * exists because a `<div>` is not a `<label>`, and React Native has neither.
99
+ *
100
+ * @example
101
+ * <Field>
102
+ * <Field.Label>Full name</Field.Label>
103
+ * <Input placeholder="Ada Lovelace" />
104
+ * <Field.Description>This appears on invoices.</Field.Description>
105
+ * </Field>
106
+ *
107
+ * @example
108
+ * <Field isInvalid={!isValid}>
109
+ * <Field.Label>Email</Field.Label>
110
+ * <Input inputMode="email" onChangeText={setEmail} value={email} />
111
+ * <Field.Error>{error}</Field.Error>
112
+ * </Field>
113
+ *
114
+ * @example
115
+ * <Field.Set>
116
+ * <Field.Legend>Profile</Field.Legend>
117
+ * <Field.Description>Shown on invoices and emails.</Field.Description>
118
+ * <Field.Group>
119
+ * <Field>
120
+ * <Field.Label>Username</Field.Label>
121
+ * <Input autoCapitalize="none" />
122
+ * </Field>
123
+ * </Field.Group>
124
+ * </Field.Set>
125
+ */
126
+ export const Field = Object.assign(FieldRoot, {
127
+ /** A named section of a form — a legend, a description, and the fields under them. */
128
+ Set: FieldSet,
129
+ /** The title of a `Field.Set`. `variant="label"` for a nested one. */
130
+ Legend: FieldLegend,
131
+ /** A stack of fields, spaced so two never read as one. Inserts no dividers. */
132
+ Group: FieldGroup,
133
+ /** The control's name. Turns destructive with the control when the field is invalid. */
134
+ Label: FieldLabel,
135
+ /** Supporting copy. Stays muted in every state, so an error is the line that changed. */
136
+ Description: FieldDescription,
137
+ /** What is wrong with the value. Renders nothing when it has no children. */
138
+ Error: FieldError,
139
+ /** A label and its description as one block, for a horizontal field. */
140
+ Content: FieldContent,
141
+ /** A rule between sections, optionally labelled between two rules. */
142
+ Separator: FieldSeparator,
143
+ displayName: "DelacourUI.Field",
144
+ });
@@ -0,0 +1,15 @@
1
+ import type { TextProps } from "../text";
2
+
3
+ /**
4
+ * The shape of a field's text parts.
5
+ *
6
+ * Shared by `Field.Label`, `Field.Description`, `Field.Error` and
7
+ * `Field.Legend`, which differ only in the preset they render and the colour
8
+ * they resolve, so it lives in a leaf rather than in one of the four files
9
+ * arbitrarily.
10
+ *
11
+ * `variant` is omitted because each part already is one — naming a second would
12
+ * let a caller turn a description into a heading and lose the type scale the
13
+ * component exists to keep.
14
+ */
15
+ export type FieldTextProps = Omit<TextProps, "variant">;