@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,329 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { cn } from "../../lib/cn";
3
+ import { tv } from "../../lib/tv";
4
+ import type { ButtonGroupOrientation, ButtonGroupSlotPosition } from "../button/button.variants";
5
+
6
+ export const INPUT_VARIANTS = ["primary", "secondary"] as const;
7
+
8
+ export const INPUT_SIZES = ["sm", "md", "lg"] as const;
9
+
10
+ export type InputVariant = (typeof INPUT_VARIANTS)[number];
11
+ export type InputSize = (typeof INPUT_SIZES)[number];
12
+
13
+ /** Theme token an icon in a decorator inherits, and the one it takes when invalid. */
14
+ export const INPUT_DECORATOR_ICON_TOKEN = "muted-foreground";
15
+ export const INPUT_INVALID_DECORATOR_ICON_TOKEN = "destructive";
16
+
17
+ /**
18
+ * Default colours for the three `TextInput` props that take a colour rather
19
+ * than a style — the placeholder, and the caret and selection highlight.
20
+ *
21
+ * These are `accent-*` utilities, not `text-*` ones, and that is not a style
22
+ * choice. Uniwind bridges a className to one of these props by compiling it and
23
+ * reading `styles.accentColor`, so a class that sets any other property
24
+ * resolves to nothing: uniwind warns once in development and the prop is left
25
+ * undefined, which reads as "the platform default" rather than as an error.
26
+ */
27
+ export const INPUT_PLACEHOLDER_ACCENT_CLASS = "accent-muted-foreground";
28
+ export const INPUT_SELECTION_ACCENT_CLASS = "accent-primary";
29
+ export const INPUT_INVALID_SELECTION_ACCENT_CLASS = "accent-destructive";
30
+
31
+ /**
32
+ * Styling for every part of a text field.
33
+ *
34
+ * One slotted `tv()` rather than a call per part, so `size` and `variant` are
35
+ * declared once — and, more importantly here, so the box is declared once. The
36
+ * `root` slot is the whole bordered box, and it lands on the `TextInput` when
37
+ * the field stands alone or on `Input.Group`'s row when it does not. That is
38
+ * what makes a grouped field identical to a lone one by construction rather
39
+ * than by two class strings happening to agree; `input.variants.test.ts` pins
40
+ * it as a property over every combination of the axes.
41
+ *
42
+ * `root` therefore holds no `text-*` utility. When the field is grouped that
43
+ * slot is worn by a `View`, and a React Native `View` does not cascade colour
44
+ * to a `Text` descendant — so the type scale and the colour live on `field`,
45
+ * which is always the `TextInput` itself. See AGENTS.md rule 1.
46
+ *
47
+ * A size names tokens rather than raw utilities: `h-input-md` for the box,
48
+ * `text-input-md` for the value, `size-icon-md` for a decorator's glyph. The
49
+ * values live in `tokens.css`, so a field and the icon inside it resolve
50
+ * against one scale rather than two numbers happening to agree. `cn` and `tv`
51
+ * both have to know these token names — see `lib/cn.ts` and `styles/tokens.ts`.
52
+ *
53
+ * Prefix and suffix share the one `decorator` slot. They are the same box in
54
+ * different places, and a second identical slot is a second definition that can
55
+ * drift.
56
+ *
57
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
58
+ * parse React Native's Flow-typed source. See AGENTS.md.
59
+ */
60
+ export const inputVariants = tv({
61
+ slots: {
62
+ root: "overflow-hidden border",
63
+ /** Layout for the group's row. Never applied to a lone field, which is not a row. */
64
+ row: "flex-row items-center",
65
+ /*
66
+ * The one text surface in this package that is not a `Text`: a raw React
67
+ * Native `TextInput`, which inherits nothing. `font-sans` is restated for
68
+ * the same reason `TEXT_BASE_CLASS` carries it — without it a themed
69
+ * typeface would stop at the field's edge.
70
+ */
71
+ field: "font-sans text-foreground",
72
+ decorator: "items-center justify-center",
73
+ /** Edge length an `Icon` in a decorator inherits. */
74
+ decoratorIcon: "",
75
+ /** Treatment a bare `Text` affix in a decorator inherits. */
76
+ decoratorText: "text-muted-foreground",
77
+ },
78
+ variants: {
79
+ variant: {
80
+ primary: { root: "border-input bg-card" },
81
+ secondary: { root: "border-transparent bg-secondary" },
82
+ },
83
+ size: {
84
+ sm: {
85
+ root: "px-2.5",
86
+ row: "gap-1.5",
87
+ field: "text-input-sm leading-tight",
88
+ decoratorIcon: "size-icon-sm",
89
+ decoratorText: "text-input-sm",
90
+ },
91
+ md: {
92
+ root: "px-3",
93
+ row: "gap-2",
94
+ field: "text-input-md leading-tight",
95
+ decoratorIcon: "size-icon-md",
96
+ decoratorText: "text-input-md",
97
+ },
98
+ lg: {
99
+ root: "px-3.5",
100
+ row: "gap-2.5",
101
+ field: "text-input-lg leading-tight",
102
+ decoratorIcon: "size-icon-lg",
103
+ decoratorText: "text-input-lg",
104
+ },
105
+ },
106
+ // The empty `false` branches are load-bearing typing, not placeholders.
107
+ // `tv` derives the prop type from the declared keys, so a map with only
108
+ // `true` types the prop as `true` rather than `boolean` and rejects
109
+ // `inputVariants({ isFocused })`. See button.variants.ts for the same note.
110
+ isFocused: { true: { root: "border-ring" }, false: {} },
111
+ isInvalid: { true: { root: "border-destructive", decoratorText: "text-destructive" }, false: {} },
112
+ isDisabled: { true: { root: "opacity-50" }, false: {} },
113
+ isMultiline: { true: {}, false: {} },
114
+ // A grouped field shares its row, so it stretches — and it clears React
115
+ // Native's own padding, which on Android would otherwise inset the value
116
+ // from a gutter the row has already applied.
117
+ isGrouped: { true: { field: "flex-1 p-0" }, false: {} },
118
+ /** Which way an enclosing `Button.Group` runs, when there is one. */
119
+ orientation: { horizontal: {}, vertical: {} },
120
+ // A field standing on its own is `none` and keeps the corner off the
121
+ // generic ramp. Every other value means it is joined, and a joined
122
+ // control takes the group's corner rather than its own — see the note on
123
+ // the corner compounds below.
124
+ groupPosition: { none: {}, first: {}, middle: { root: "rounded-none" }, last: {}, only: {} },
125
+ isSeamed: { true: {}, false: {} },
126
+ },
127
+ compoundVariants: [
128
+ // The height and the vertical rhythm are declared together rather than
129
+ // fighting each other in the merge, the way a button's square footprint
130
+ // and its horizontal padding are.
131
+ //
132
+ // `py-0` on a single-line field is load-bearing on Android: React
133
+ // Native's `TextInput` inherits the platform's own vertical padding,
134
+ // which would push the value off centre inside a fixed height.
135
+ { isMultiline: false, size: "sm", class: { root: "h-input-sm py-0" } },
136
+ { isMultiline: false, size: "md", class: { root: "h-input-md py-0" } },
137
+ { isMultiline: false, size: "lg", class: { root: "h-input-lg py-0" } },
138
+ // A multiline field grows with its text, so the height becomes a floor.
139
+ // The row aligns to the top with it: centred decorators would drift down
140
+ // the side of a paragraph instead of sitting on its first line.
141
+ { isMultiline: true, size: "sm", class: { root: "min-h-input-sm py-2", row: "items-start" } },
142
+ { isMultiline: true, size: "md", class: { root: "min-h-input-md py-2.5", row: "items-start" } },
143
+ { isMultiline: true, size: "lg", class: { root: "min-h-input-lg py-3", row: "items-start" } },
144
+ // Focus is transient and invalid is reported, so invalid outranks it. A
145
+ // field that went grey the moment it was tapped would drop the only
146
+ // signal it has that its value is wrong, exactly while it is being fixed.
147
+ { isFocused: true, isInvalid: true, class: { root: "border-destructive" } },
148
+ // The corner a lone field draws: the generic ramp, deliberately not the
149
+ // button's, because a field and the button beside it are meant to be
150
+ // retunable apart. `sm` steps down a notch; the other two share `lg`.
151
+ { groupPosition: "none", size: "sm", class: { root: "rounded-md" } },
152
+ { groupPosition: "none", size: "md", class: { root: "rounded-lg" } },
153
+ { groupPosition: "none", size: "lg", class: { root: "rounded-lg" } },
154
+ // Joined, it takes the *group's* corner instead. A group owns the shape
155
+ // of its run, so a field capping one end of it has to draw the same arc
156
+ // the button capping the other end does — two ramps meeting in one row
157
+ // is the mismatch this replaces. `button.variants.test.ts` asserts these
158
+ // strings match the button's cell for cell.
159
+ { groupPosition: "only", size: "sm", class: { root: "rounded-button-sm" } },
160
+ { groupPosition: "only", size: "md", class: { root: "rounded-button-md" } },
161
+ { groupPosition: "only", size: "lg", class: { root: "rounded-button-lg" } },
162
+ {
163
+ groupPosition: "first",
164
+ orientation: "horizontal",
165
+ size: "sm",
166
+ class: { root: "rounded-s-button-sm rounded-e-none" },
167
+ },
168
+ {
169
+ groupPosition: "first",
170
+ orientation: "horizontal",
171
+ size: "md",
172
+ class: { root: "rounded-s-button-md rounded-e-none" },
173
+ },
174
+ {
175
+ groupPosition: "first",
176
+ orientation: "horizontal",
177
+ size: "lg",
178
+ class: { root: "rounded-s-button-lg rounded-e-none" },
179
+ },
180
+ {
181
+ groupPosition: "last",
182
+ orientation: "horizontal",
183
+ size: "sm",
184
+ class: { root: "rounded-e-button-sm rounded-s-none" },
185
+ },
186
+ {
187
+ groupPosition: "last",
188
+ orientation: "horizontal",
189
+ size: "md",
190
+ class: { root: "rounded-e-button-md rounded-s-none" },
191
+ },
192
+ {
193
+ groupPosition: "last",
194
+ orientation: "horizontal",
195
+ size: "lg",
196
+ class: { root: "rounded-e-button-lg rounded-s-none" },
197
+ },
198
+ {
199
+ groupPosition: "first",
200
+ orientation: "vertical",
201
+ size: "sm",
202
+ class: { root: "rounded-t-button-sm rounded-b-none" },
203
+ },
204
+ {
205
+ groupPosition: "first",
206
+ orientation: "vertical",
207
+ size: "md",
208
+ class: { root: "rounded-t-button-md rounded-b-none" },
209
+ },
210
+ {
211
+ groupPosition: "first",
212
+ orientation: "vertical",
213
+ size: "lg",
214
+ class: { root: "rounded-t-button-lg rounded-b-none" },
215
+ },
216
+ {
217
+ groupPosition: "last",
218
+ orientation: "vertical",
219
+ size: "sm",
220
+ class: { root: "rounded-b-button-sm rounded-t-none" },
221
+ },
222
+ {
223
+ groupPosition: "last",
224
+ orientation: "vertical",
225
+ size: "md",
226
+ class: { root: "rounded-b-button-md rounded-t-none" },
227
+ },
228
+ {
229
+ groupPosition: "last",
230
+ orientation: "vertical",
231
+ size: "lg",
232
+ class: { root: "rounded-b-button-lg rounded-t-none" },
233
+ },
234
+ { isSeamed: true, orientation: "horizontal", class: { root: "-ms-px" } },
235
+ { isSeamed: true, orientation: "vertical", class: { root: "-mt-px" } },
236
+ ],
237
+ defaultVariants: {
238
+ variant: "primary",
239
+ size: "md",
240
+ isFocused: false,
241
+ isInvalid: false,
242
+ isDisabled: false,
243
+ isMultiline: false,
244
+ isGrouped: false,
245
+ orientation: "horizontal",
246
+ groupPosition: "none",
247
+ isSeamed: false,
248
+ },
249
+ });
250
+
251
+ /** The axes that decide how a field's box is drawn, wherever that box lands. */
252
+ export type InputBoxState = {
253
+ variant?: InputVariant;
254
+ size?: InputSize;
255
+ isDisabled?: boolean;
256
+ isFocused?: boolean;
257
+ isInvalid?: boolean;
258
+ isMultiline?: boolean;
259
+ /** Where the field sits in an enclosing `Button.Group`, if it is in one. */
260
+ groupPosition?: ButtonGroupSlotPosition;
261
+ /** Which way that group runs. Read only when `groupPosition` is not `none`. */
262
+ orientation?: ButtonGroupOrientation;
263
+ /** Overlap the member before it, so the shared edge is drawn once. */
264
+ isSeamed?: boolean;
265
+ };
266
+
267
+ /**
268
+ * The class the `TextInput` itself wears.
269
+ *
270
+ * Standalone, the field *is* the box, so it takes the chrome and the text
271
+ * treatment together. Inside an `Input.Group` the box belongs to the row and
272
+ * the field takes the text treatment alone — otherwise the two would draw two
273
+ * borders, one inside the other.
274
+ *
275
+ * This is the single decision that keeps a grouped field indistinguishable from
276
+ * a lone one: both read the same `root` slot, so there is no second class
277
+ * string that could drift from the first.
278
+ *
279
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
280
+ */
281
+ export function resolveInputFieldClass({
282
+ className,
283
+ isGrouped = false,
284
+ ...state
285
+ }: InputBoxState & { className?: string; isGrouped?: boolean }): string {
286
+ const slots = inputVariants({ ...state, isGrouped });
287
+ return isGrouped ? slots.field({ className }) : cn(slots.root(), slots.field({ className }));
288
+ }
289
+
290
+ /**
291
+ * The class `Input.Group`'s row wears: the same box a lone field would draw,
292
+ * laid out as a row so the decorators and the field can share it.
293
+ *
294
+ * Pure, so the whole matrix is reachable from `bun test`. See AGENTS.md.
295
+ */
296
+ export function resolveInputGroupClass({ className, ...state }: InputBoxState & { className?: string }): string {
297
+ const slots = inputVariants({ ...state, isGrouped: true });
298
+ return cn(slots.root(), slots.row({ className }));
299
+ }
300
+
301
+ /**
302
+ * The `accent-*` class uniwind resolves the placeholder colour from.
303
+ *
304
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
305
+ */
306
+ export function resolvePlaceholderAccentClass(className?: string): string {
307
+ return cn(INPUT_PLACEHOLDER_ACCENT_CLASS, className);
308
+ }
309
+
310
+ /**
311
+ * The `accent-*` class uniwind resolves the caret and selection colour from.
312
+ *
313
+ * An invalid field tints them destructive along with its border, so the state is
314
+ * still legible while the value is being corrected — the moment the border is
315
+ * the only thing carrying it. A caller's class still wins.
316
+ *
317
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
318
+ */
319
+ export function resolveSelectionAccentClass({
320
+ className,
321
+ isInvalid = false,
322
+ }: {
323
+ className?: string;
324
+ isInvalid?: boolean;
325
+ }): string {
326
+ return cn(isInvalid ? INPUT_INVALID_SELECTION_ACCENT_CLASS : INPUT_SELECTION_ACCENT_CLASS, className);
327
+ }
328
+
329
+ export type InputVariantProps = VariantProps<typeof inputVariants>;
@@ -0,0 +1,67 @@
1
+ # ListGroup
2
+
3
+ A surface grouping related rows. Compound root plus `ListGroup.Item` and its
4
+ five slots: `ItemPrefix`, `ItemContent`, `ItemTitle`, `ItemDescription`,
5
+ `ItemSuffix`.
6
+
7
+ `import { ListGroup } from "@delacour/react-native-ui/list-group";`
8
+
9
+ ## Files
10
+
11
+ | File | What it holds |
12
+ | --- | --- |
13
+ | `index.ts` | → `@delacour/react-native-ui/list-group` |
14
+ | `list-group.tsx` | Root + the `Object.assign` compound surface |
15
+ | `list-group-item.tsx` | `ListGroup.Item`, and the bare-text wrap it owns |
16
+ | `list-group-item-prefix.tsx` | `ListGroup.ItemPrefix` |
17
+ | `list-group-item-content.tsx` | `ListGroup.ItemContent` |
18
+ | `list-group-item-title.tsx` | `ListGroup.ItemTitle` |
19
+ | `list-group-item-description.tsx` | `ListGroup.ItemDescription` |
20
+ | `list-group-item-suffix.tsx` | `ListGroup.ItemSuffix` |
21
+ | `list-group.context.tsx` | `ListGroupProvider`, `useListGroup()`, `useListGroupContext()`, `useListGroupPart()` |
22
+ | `list-group.types.ts` | Prop types shared by two or more parts |
23
+ | `list-group.variants.ts` | Pure `tv()` slots, no RN imports |
24
+ | `list-group.variants.test.ts` | |
25
+
26
+ ## Design
27
+
28
+ - **Variants**: `default`, `secondary`, `tertiary`, `transparent`.
29
+ **Sizes**: `sm`, `md`, `lg`. Size is not decoration — it drives the row
30
+ metrics, the title and description type scale, both icon sizes *and* the
31
+ divider inset, which is why those five numbers live in one axis rather than
32
+ five magic values.
33
+ - **The corner is `rounded-md`, the 0.8 step of the shared ramp.** shadcn puts a
34
+ grouped surface a step or two below a card, so a group reads as a control
35
+ surface rather than as one. Size does not move it, and neither does a button's
36
+ own corner — `--radius-button-*` is a capsule at half its height and outside
37
+ this ramp. See the corner-scale note in
38
+ [the package AGENTS.md](../../../AGENTS.md).
39
+ - **Dividers are inserted, not written out.** The root walks its children and
40
+ puts a [`Separator`](../separator/AGENTS.md) between adjacent ones, inset to
41
+ line up with the rows' padding. A `Separator` placed by hand suppresses the
42
+ automatic one on either side of it, so a caller can make one gap full-bleed
43
+ without turning the feature off; `isDivided={false}` turns it off entirely.
44
+ `Children.toArray` drops the nulls a conditional child leaves behind, so a row
45
+ rendered only some of the time does not strand a divider.
46
+ - **The root clips.** `overflow-hidden` is load-bearing: a pressed row fades to
47
+ the edge of its own box, and the first and last rows would square off the
48
+ group's corners without it.
49
+ - **A row is a `Pressable`.** `ListGroupItemProps` extends `PressableProps`, so
50
+ `feedback`, `haptic`, `pressedScale` and the rest are inherited rather than
51
+ restated — the row owns no vocabulary of its own. Only the default differs:
52
+ `feedback` defaults to `fade`, because a full-bleed row that scales reads as
53
+ the whole card flexing rather than as one row responding. A prop is only
54
+ redeclared where it genuinely changes, as [`Button`](../button/AGENTS.md) does
55
+ with its narrower union; redeclaring one unchanged just to hang a doc comment
56
+ on it puts a second definition in the tree that can drift.
57
+ - **Icons are composed, never passed as props.** `ItemPrefix` wraps its subtree
58
+ in an `IconDefaultsProvider` carrying the `prefixIcon` slot — a step on the
59
+ shared icon scale, like every other glyph in the library — and `foreground`,
60
+ so a bare `<Icon icon={IconUser} />` needs nothing said at the call site.
61
+ `ItemSuffix` draws a chevron when it has no children of its own; `iconProps`
62
+ tunes that glyph and is ignored once it does.
63
+ - **String children** are wrapped in an `ItemContent` around an `ItemTitle`
64
+ automatically, consecutive strings collapsing into one — the same rule, and
65
+ the same reason, as [`Button`](../button/AGENTS.md).
66
+ - **Title colour goes on the title.** The `item` slot carries no `text-*`; a row
67
+ is a `View` and cannot cascade colour to a `Text`. The tests assert this.
@@ -0,0 +1,20 @@
1
+ export { ListGroup, type ListGroupProps } from "./list-group";
2
+ export {
3
+ type ListGroupContextValue,
4
+ ListGroupProvider,
5
+ useListGroup,
6
+ useListGroupContext,
7
+ } from "./list-group.context";
8
+ export type { ListGroupSlotProps, ListGroupTextProps } from "./list-group.types";
9
+ export {
10
+ LIST_GROUP_FOREGROUND_TOKEN,
11
+ LIST_GROUP_SIZES,
12
+ LIST_GROUP_SUFFIX_ICON_TOKEN,
13
+ LIST_GROUP_VARIANTS,
14
+ type ListGroupSize,
15
+ type ListGroupVariant,
16
+ type ListGroupVariantProps,
17
+ listGroupVariants,
18
+ } from "./list-group.variants";
19
+ export type { ListGroupItemProps } from "./list-group-item";
20
+ export type { ListGroupIconProps, ListGroupItemSuffixProps } from "./list-group-item-suffix";
@@ -0,0 +1,10 @@
1
+ import type { ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import type { ListGroupSlotProps } from "./list-group.types";
4
+ import { listGroupVariants } from "./list-group.variants";
5
+
6
+ /** The text column of a row, taking whatever width the prefix and suffix leave. */
7
+ export function ListGroupItemContent({ className, ...props }: ListGroupSlotProps): ReactElement {
8
+ return <View className={listGroupVariants().content({ className })} {...props} />;
9
+ }
10
+ ListGroupItemContent.displayName = "DelacourUI.ListGroup.ItemContent";
@@ -0,0 +1,12 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useListGroupPart } from "./list-group.context";
4
+ import type { ListGroupTextProps } from "./list-group.types";
5
+ import { listGroupVariants } from "./list-group.variants";
6
+
7
+ /** The row's secondary line, a step down in scale and on the muted token. */
8
+ export function ListGroupItemDescription({ className, ...props }: ListGroupTextProps): ReactElement {
9
+ const { size } = useListGroupPart("ListGroup.ItemDescription");
10
+ return <Text className={listGroupVariants({ size }).description({ className })} {...props} />;
11
+ }
12
+ ListGroupItemDescription.displayName = "DelacourUI.ListGroup.ItemDescription";
@@ -0,0 +1,30 @@
1
+ import { type ReactElement, useMemo } from "react";
2
+ import { View } from "react-native";
3
+ import { IconDefaultsProvider } from "../icon";
4
+ import { useListGroupPart } from "./list-group.context";
5
+ import type { ListGroupSlotProps } from "./list-group.types";
6
+ import { LIST_GROUP_FOREGROUND_TOKEN, listGroupVariants } from "./list-group.variants";
7
+
8
+ /**
9
+ * The leading slot of a row.
10
+ *
11
+ * Its subtree inherits the list group's icon size and the foreground token, so a
12
+ * bare `<Icon icon={IconWifi} />` comes out right with nothing said at the call
13
+ * site — the same cascade a `Button` gives its icons.
14
+ */
15
+ export function ListGroupItemPrefix({ className, children, ...props }: ListGroupSlotProps): ReactElement {
16
+ const { size } = useListGroupPart("ListGroup.ItemPrefix");
17
+ const slots = listGroupVariants({ size });
18
+ const iconClassName = slots.prefixIcon();
19
+ const iconDefaults = useMemo(
20
+ () => ({ className: iconClassName, color: LIST_GROUP_FOREGROUND_TOKEN }),
21
+ [iconClassName]
22
+ );
23
+
24
+ return (
25
+ <View className={slots.prefix({ className })} {...props}>
26
+ <IconDefaultsProvider value={iconDefaults}>{children}</IconDefaultsProvider>
27
+ </View>
28
+ );
29
+ }
30
+ ListGroupItemPrefix.displayName = "DelacourUI.ListGroup.ItemPrefix";
@@ -0,0 +1,55 @@
1
+ import { Children, type ReactElement } from "react";
2
+ import { View } from "react-native";
3
+ import { IconChevronRight } from "../../icons/central";
4
+ import { cn } from "../../lib/cn";
5
+ import { Icon, type IconSize } from "../icon";
6
+ import { useListGroupPart } from "./list-group.context";
7
+ import type { ListGroupSlotProps } from "./list-group.types";
8
+ import { LIST_GROUP_SUFFIX_ICON_TOKEN, listGroupVariants } from "./list-group.variants";
9
+
10
+ export type ListGroupIconProps = {
11
+ /** A named size, or an edge length in points. Beats `className`. */
12
+ size?: IconSize | number;
13
+ /** A theme colour token or a literal. Defaults to `muted-foreground`. */
14
+ color?: string;
15
+ /** A `size-*` utility, beating the group's own suffix size. */
16
+ className?: string;
17
+ };
18
+
19
+ export type ListGroupItemSuffixProps = ListGroupSlotProps & {
20
+ /** Tunes the default chevron. Ignored once the suffix has children of its own. */
21
+ iconProps?: ListGroupIconProps;
22
+ };
23
+
24
+ /**
25
+ * The trailing slot of a row, showing a chevron unless given content.
26
+ *
27
+ * The chevron is a hint that the row leads somewhere, so it sits on the muted
28
+ * token and a step below the leading icon rather than competing with either.
29
+ */
30
+ export function ListGroupItemSuffix({
31
+ className,
32
+ iconProps,
33
+ children,
34
+ ...props
35
+ }: ListGroupItemSuffixProps): ReactElement {
36
+ const { size } = useListGroupPart("ListGroup.ItemSuffix");
37
+ const slots = listGroupVariants({ size });
38
+ const hasContent = Children.toArray(children).length > 0;
39
+
40
+ return (
41
+ <View className={slots.suffix({ className })} {...props}>
42
+ {hasContent ? (
43
+ children
44
+ ) : (
45
+ <Icon
46
+ className={cn(slots.suffixIcon(), iconProps?.className)}
47
+ color={iconProps?.color ?? LIST_GROUP_SUFFIX_ICON_TOKEN}
48
+ icon={IconChevronRight}
49
+ size={iconProps?.size}
50
+ />
51
+ )}
52
+ </View>
53
+ );
54
+ }
55
+ ListGroupItemSuffix.displayName = "DelacourUI.ListGroup.ItemSuffix";
@@ -0,0 +1,17 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text } from "../text";
3
+ import { useListGroupPart } from "./list-group.context";
4
+ import type { ListGroupTextProps } from "./list-group.types";
5
+ import { listGroupVariants } from "./list-group.variants";
6
+
7
+ /**
8
+ * The row's primary line.
9
+ *
10
+ * Carries its own colour and type scale, read from the list group's context: a
11
+ * React Native `View` does not cascade colour to a `Text` descendant.
12
+ */
13
+ export function ListGroupItemTitle({ className, ...props }: ListGroupTextProps): ReactElement {
14
+ const { size } = useListGroupPart("ListGroup.ItemTitle");
15
+ return <Text className={listGroupVariants({ size }).title({ className })} {...props} />;
16
+ }
17
+ ListGroupItemTitle.displayName = "DelacourUI.ListGroup.ItemTitle";
@@ -0,0 +1,87 @@
1
+ import { Children, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { Pressable, type PressableProps } from "../pressable";
3
+ import { useListGroupPart } from "./list-group.context";
4
+ import { listGroupVariants } from "./list-group.variants";
5
+ import { ListGroupItemContent } from "./list-group-item-content";
6
+ import { ListGroupItemTitle } from "./list-group-item-title";
7
+
8
+ export type ListGroupItemProps = Omit<PressableProps, "children" | "disabled"> & {
9
+ isDisabled?: boolean;
10
+ children?: ReactNode;
11
+ };
12
+
13
+ /**
14
+ * One row of a list group.
15
+ *
16
+ * Plain string or number children are wrapped in a title inside a content
17
+ * column; pass the compound parts when a row needs a prefix, a description or a
18
+ * suffix. React Native cannot render a string outside a `<Text>`, so the wrap is
19
+ * not a convenience — without it `<ListGroup.Item>Wi-Fi</ListGroup.Item>` would
20
+ * crash.
21
+ *
22
+ * A row is a `Pressable`, so `feedback`, `haptic` and the rest come from there
23
+ * rather than being restated here. Only the default differs: `fade`, because a
24
+ * full-bleed row that scales reads as the whole card flexing rather than as one
25
+ * row responding.
26
+ */
27
+ export function ListGroupItem({
28
+ feedback = "fade",
29
+ isDisabled = false,
30
+ className,
31
+ children,
32
+ ...props
33
+ }: ListGroupItemProps): ReactElement {
34
+ const { size } = useListGroupPart("ListGroup.Item");
35
+ const content = useMemo(() => wrapTextChildren(children), [children]);
36
+
37
+ return (
38
+ <Pressable
39
+ className={listGroupVariants({ isDisabled, size }).item({ className })}
40
+ disabled={isDisabled}
41
+ feedback={feedback}
42
+ {...props}
43
+ >
44
+ {content}
45
+ </Pressable>
46
+ );
47
+ }
48
+ ListGroupItem.displayName = "DelacourUI.ListGroup.Item";
49
+
50
+ /**
51
+ * Wraps bare text children in a title inside a content column.
52
+ *
53
+ * Consecutive strings and numbers are collected into a single title rather than
54
+ * one each — `Row {index}` is one piece of text, and wrapping the parts
55
+ * separately would space them apart by the row's gap.
56
+ *
57
+ * Lives here rather than with the root: it is the row that wraps its own text,
58
+ * and importing it from `./list-group` would close a cycle back through the
59
+ * root. See AGENTS.md rule 3.
60
+ */
61
+ function wrapTextChildren(children: ReactNode): ReactNode {
62
+ const items = Children.toArray(children);
63
+ const output: ReactNode[] = [];
64
+ let run: (string | number)[] = [];
65
+
66
+ const flushRun = () => {
67
+ if (run.length === 0) return;
68
+ output.push(
69
+ <ListGroupItemContent key={`content-${output.length}`}>
70
+ <ListGroupItemTitle>{run.join("")}</ListGroupItemTitle>
71
+ </ListGroupItemContent>
72
+ );
73
+ run = [];
74
+ };
75
+
76
+ for (const child of items) {
77
+ if (typeof child === "string" || typeof child === "number") {
78
+ run.push(child);
79
+ continue;
80
+ }
81
+ flushRun();
82
+ output.push(child);
83
+ }
84
+ flushRun();
85
+
86
+ return output;
87
+ }