@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,272 @@
1
+ import type { VariantProps } from "tailwind-variants";
2
+ import { tv } from "../../lib/tv";
3
+ import type { TextSize } from "../text/text.variants";
4
+
5
+ /** How the ring is painted. `secondary` fills it; `primary` leaves it hollow. */
6
+ export const RADIO_VARIANTS = ["primary", "secondary"] as const;
7
+
8
+ export const RADIO_SIZES = ["sm", "md", "lg"] as const;
9
+
10
+ /** Which way a `Radio.Group` lays its radios out. */
11
+ export const RADIO_ORIENTATIONS = ["vertical", "horizontal"] as const;
12
+
13
+ export type RadioVariant = (typeof RADIO_VARIANTS)[number];
14
+ export type RadioSize = (typeof RADIO_SIZES)[number];
15
+ export type RadioOrientation = (typeof RADIO_ORIENTATIONS)[number];
16
+
17
+ /**
18
+ * The axes a radio falls back to when neither it nor its group names one.
19
+ *
20
+ * Named once because two places read them — `defaultVariants` below and
21
+ * {@link resolveRadioState}, which runs before `tv` is ever called. A test pins
22
+ * the pair, since a drift between them is a radio that renders at one size and
23
+ * reports another.
24
+ */
25
+ export const RADIO_DEFAULT_SIZE: RadioSize = "md";
26
+ export const RADIO_DEFAULT_VARIANT: RadioVariant = "primary";
27
+
28
+ /**
29
+ * The spring the dot scales in and out on.
30
+ *
31
+ * Deliberately near `Pressable`'s `PRESS_SPRING` but a touch looser, so the dot
32
+ * settles just after the row it sits in has rebounded. Read on the JS thread
33
+ * inside an effect rather than captured by a worklet, so it stays a plain object.
34
+ */
35
+ export const RADIO_DOT_SPRING = { damping: 16, mass: 0.4, stiffness: 300 } as const;
36
+
37
+ /**
38
+ * The `Text` size step each radio size hands its label.
39
+ *
40
+ * `Radio.Label` renders `Text.Label` and names this step, rather than restating
41
+ * a type scale in the `label` slot. `Text`'s own size axis is built to beat its
42
+ * preset, so this keeps the weight and the colour in exactly one place — the
43
+ * preset — while still letting the label track the radio's size. A test asserts
44
+ * every value here is a size `Text` actually has.
45
+ */
46
+ export const RADIO_LABEL_TEXT_SIZE: Record<RadioSize, TextSize> = { sm: "sm", md: "md", lg: "lg" };
47
+
48
+ /**
49
+ * Styling for every part of a radio, and for the group that holds one.
50
+ *
51
+ * One slotted `tv()` rather than a call per part, because `radio-indicator.tsx`
52
+ * and `radio-label.tsx` cannot import the root without closing a cycle (AGENTS.md
53
+ * rule 3) yet all three read the same `size`, `variant` and `isSelected`.
54
+ *
55
+ * **The root takes no `self-start` and no `w-full`, and both absences are
56
+ * load-bearing.** A radio row *is* its own tap target, so unlike a `Badge` it
57
+ * wants the stretch it gets inside the group's `flex-col` — `self-start` would
58
+ * shrink the target to the width of the word "Yes". But unlike a `ListGroup.Item`
59
+ * it cannot take `w-full` either, because a `horizontal` group would then give
60
+ * every radio the full width of the group and blow the row apart. Vertical
61
+ * stretch already supplies the width; horizontal wants content width.
62
+ *
63
+ * **The label is `shrink`, never `flex-1`.** `flex-1` sets `flex-basis: 0%`, and
64
+ * in a content-sized `horizontal` row Yoga resolves that to zero and collapses
65
+ * the text to nothing. `shrink` lets a long label wrap without claiming a basis.
66
+ *
67
+ * **The row's height is a floor, never fixed.** `Text` respects OS font scaling,
68
+ * so `h-*` would clip a label at a large accessibility step; `min-h-*` exists for
69
+ * the hit target, which scaling may exceed but must never undercut.
70
+ *
71
+ * The ring indexes the shared `--spacing-icon-*` scale rather than minting one of
72
+ * its own — a radio's ring is a small round mark in a row beside a label, the
73
+ * same kind of thing as a row's chevron, and it should stay level with an `Icon`
74
+ * at the same step by construction. The dot takes plain spacing steps and its
75
+ * fit inside the ring is pinned by a test rather than by a token, the way
76
+ * `Field` pins its gap ladder.
77
+ *
78
+ * `border-2` sits in the base rather than on a variant: a border declared only
79
+ * where it is coloured would make the ring four points smaller the moment a
80
+ * caller switched variant.
81
+ *
82
+ * No slot worn by a `View` carries `text-*`, and the `label` slot carries no type
83
+ * scale, weight or colour at all — see {@link RADIO_LABEL_TEXT_SIZE}.
84
+ *
85
+ * Free of React Native imports so it stays unit-testable — `bun test` cannot
86
+ * parse React Native's Flow-typed source. See AGENTS.md.
87
+ */
88
+ export const radioVariants = tv({
89
+ slots: {
90
+ /** The group's box. Paints no state of its own — see the note below. */
91
+ group: "w-full",
92
+ /** The pressable row. Deliberately neither `self-start` nor `w-full`. */
93
+ root: "flex-row items-center",
94
+ /** The ring. Its colour is a class, so the whole matrix stays in `bun test`. */
95
+ indicator: "items-center justify-center rounded-full border-2",
96
+ /** The inner dot. Its scale and opacity are an animated style, never a class. */
97
+ dot: "rounded-full bg-primary",
98
+ /** Handed to a `Text.Label`. Layout only. */
99
+ label: "shrink",
100
+ },
101
+ variants: {
102
+ orientation: {
103
+ vertical: { group: "flex-col" },
104
+ horizontal: { group: "flex-row flex-wrap items-center" },
105
+ },
106
+ size: {
107
+ sm: { group: "gap-2", root: "min-h-9 gap-2", indicator: "size-icon-md", dot: "size-1.5" },
108
+ md: { group: "gap-3", root: "min-h-11 gap-2.5", indicator: "size-icon-lg", dot: "size-2" },
109
+ lg: { group: "gap-3.5", root: "min-h-12 gap-3", indicator: "size-icon-xl", dot: "size-2.5" },
110
+ },
111
+ // The empty branches are load-bearing typing, not placeholders. `tv`
112
+ // derives the prop type from the declared keys, so a map with only `true`
113
+ // types the prop as `true` rather than `boolean`. See button.variants.ts.
114
+ variant: { primary: {}, secondary: {} },
115
+ isSelected: { true: {}, false: {} },
116
+ isInvalid: { true: {}, false: {} },
117
+ // A trailing ring turns the row into a settings row: the content takes the
118
+ // left, the ring the far right. Declared here rather than left to a
119
+ // caller's `justify-between` because the root already knows where the
120
+ // indicator landed — see {@link resolveIndicatorPlacement}.
121
+ isIndicatorTrailing: { true: { root: "justify-between" }, false: {} },
122
+ // The fade lands on the children, never on the `root`, and that is not a
123
+ // style choice. `root` is worn by `Pressable`'s own `Animated.View`, whose
124
+ // `useAnimatedStyle` writes `opacity` on every frame — at rest, 1. An
125
+ // `opacity-50` class on that node is overwritten before it is ever drawn,
126
+ // silently, so the row stays at full contrast while behaving as disabled.
127
+ // The ring and the label are ordinary descendants, so their opacity
128
+ // multiplies with the root's instead of fighting it.
129
+ //
130
+ // The group must not fade either, or a disabled group would compound
131
+ // `opacity-50` with each of its rows and land at a quarter opacity.
132
+ isDisabled: { true: { indicator: "opacity-50", label: "opacity-50" }, false: {} },
133
+ },
134
+ compoundVariants: [
135
+ // Neither `variant` nor `isSelected` paints the ring alone, so all four
136
+ // cells live here — the reason a badge's twenty-four do.
137
+ { variant: "primary", isSelected: false, class: { indicator: "border-input bg-transparent" } },
138
+ { variant: "primary", isSelected: true, class: { indicator: "border-primary bg-transparent" } },
139
+ { variant: "secondary", isSelected: false, class: { indicator: "border-border bg-secondary" } },
140
+ { variant: "secondary", isSelected: true, class: { indicator: "border-primary bg-secondary" } },
141
+ // Invalid outranks selected, and it is a compound rather than a plain
142
+ // variant purely for emission order: `tv` emits the variants first and the
143
+ // compounds after, so a plain `isInvalid` branch would lose to the four
144
+ // cells above. `Input` leans on the same mechanism for focused-and-invalid.
145
+ { isInvalid: true, class: { indicator: "border-destructive", dot: "bg-destructive" } },
146
+ ],
147
+ defaultVariants: {
148
+ orientation: "vertical",
149
+ size: RADIO_DEFAULT_SIZE,
150
+ variant: RADIO_DEFAULT_VARIANT,
151
+ isSelected: false,
152
+ isInvalid: false,
153
+ isDisabled: false,
154
+ isIndicatorTrailing: false,
155
+ },
156
+ });
157
+
158
+ /**
159
+ * Where the caller put the indicator among a radio's children.
160
+ *
161
+ * `none` means they wrote no indicator at all, so the root composes one in at the
162
+ * front. `end` means they placed one last with something before it — a settings
163
+ * row, `[label and description] [ring]` — and the row spreads to push the ring to
164
+ * the far edge. A lone indicator with nothing beside it is `start`: there is
165
+ * nothing to spread it away from.
166
+ *
167
+ * This is what makes the trailing ring work without a `flex-1` spacer wedged
168
+ * between the two, which is the shape a caller would otherwise have to reach for.
169
+ *
170
+ * Takes an array of "is this child an indicator" rather than the children
171
+ * themselves, so it stays free of React and reachable from `bun test` — the trade
172
+ * `resolveSpinnerSwapIndex` already makes.
173
+ */
174
+ export function resolveIndicatorPlacement(isIndicator: readonly boolean[]): "start" | "end" | "none" {
175
+ if (!isIndicator.some(Boolean)) return "none";
176
+ if (isIndicator.length > 1 && isIndicator[isIndicator.length - 1]) return "end";
177
+ return "start";
178
+ }
179
+
180
+ /** What an enclosing `Radio.Group` publishes, or null for a radio standing alone. */
181
+ export type RadioGroupState = {
182
+ /** The selected value. `null` is "nothing selected", never `undefined`. */
183
+ selected: string | null;
184
+ size: RadioSize;
185
+ variant: RadioVariant;
186
+ /** Raw rather than resolved, so `??` can see past a group that named nothing. */
187
+ isDisabled?: boolean;
188
+ isInvalid?: boolean;
189
+ };
190
+
191
+ /** What a radio was given at its own call site. */
192
+ export type RadioOwnState = {
193
+ value?: string;
194
+ isSelected?: boolean;
195
+ isDisabled?: boolean;
196
+ isInvalid?: boolean;
197
+ size?: RadioSize;
198
+ variant?: RadioVariant;
199
+ };
200
+
201
+ /** What an enclosing `Field` publishes, or null outside one. */
202
+ export type RadioFieldState = { isDisabled?: boolean; isInvalid?: boolean };
203
+
204
+ /** Every axis settled, ready to hand to {@link radioVariants} and to context. */
205
+ export type RadioState = {
206
+ size: RadioSize;
207
+ variant: RadioVariant;
208
+ isSelected: boolean;
209
+ isDisabled: boolean;
210
+ isInvalid: boolean;
211
+ /** Whether a `Radio.Group` is driving this radio. */
212
+ isGrouped: boolean;
213
+ };
214
+
215
+ /**
216
+ * Settles a radio's axes from the three places they can come from.
217
+ *
218
+ * **Nearest wins**, the ladder `Input` already runs: a `Radio.Group` first, the
219
+ * radio's own props next, an enclosing `Field` last. A field with no props of its
220
+ * own turns destructive with the `Field` around it, while `<Radio isInvalid={false} />`
221
+ * still opts out of one.
222
+ *
223
+ * The two axes a group owns outright — `size` and `variant` — are published
224
+ * resolved, so a grouped radio's own copies are ignored: a group whose options
225
+ * were different sizes is not a design. The two *state* axes are published raw,
226
+ * which is the one place this differs from `Input.Group`, and deliberately: a
227
+ * group holds many radios, so disabling one option out of five has to be
228
+ * possible. A group that names the axis still wins outright.
229
+ *
230
+ * Selection is the group's to decide when there is one. A radio carrying no
231
+ * `value` inside a group can never be selected — the dev warning for that lives
232
+ * in `radio.tsx`, where the component name is available to name in the message.
233
+ *
234
+ * Pure, so the whole matrix is reachable from `bun test`. `Input` runs this same
235
+ * ladder inline in its own render, where no unit test can see it. See AGENTS.md.
236
+ */
237
+ export function resolveRadioState({
238
+ group,
239
+ own,
240
+ field,
241
+ }: {
242
+ group?: RadioGroupState | null;
243
+ own?: RadioOwnState;
244
+ field?: RadioFieldState | null;
245
+ }): RadioState {
246
+ const isGrouped = group != null;
247
+
248
+ return {
249
+ size: group?.size ?? own?.size ?? RADIO_DEFAULT_SIZE,
250
+ variant: group?.variant ?? own?.variant ?? RADIO_DEFAULT_VARIANT,
251
+ isDisabled: group?.isDisabled ?? own?.isDisabled ?? field?.isDisabled ?? false,
252
+ isInvalid: group?.isInvalid ?? own?.isInvalid ?? field?.isInvalid ?? false,
253
+ isSelected: isGrouped ? own?.value !== undefined && group.selected === own.value : (own?.isSelected ?? false),
254
+ isGrouped,
255
+ };
256
+ }
257
+
258
+ /**
259
+ * Whether a press should write a new selection at all.
260
+ *
261
+ * Re-picking the option already selected is not a change, and a radio group has
262
+ * no deselect gesture — so without this a tap on the current selection would
263
+ * re-notify the caller with a value it already holds. HTML's own radio does not
264
+ * fire `change` there either.
265
+ *
266
+ * Pure, so it is reachable from `bun test`. See AGENTS.md.
267
+ */
268
+ export function shouldEmitSelection(current: string | null, next: string): boolean {
269
+ return current !== next;
270
+ }
271
+
272
+ export type RadioVariantProps = VariantProps<typeof radioVariants>;
@@ -0,0 +1,287 @@
1
+ # Screen
2
+
3
+ A screen's frame: pinned chrome, a content region, and whatever scrolls between
4
+ them. Compound root plus `Screen.Navbar` (itself compound), `Screen.Content`,
5
+ `Screen.View`, `Screen.Header`, `Screen.Footer`, four scrollables, and the two
6
+ whole-screen states.
7
+
8
+ `import { Screen } from "@registry/ui/screen";`
9
+
10
+ ## Files
11
+
12
+ | File | What it holds |
13
+ | --- | --- |
14
+ | `index.ts` | → `@registry/ui/screen` |
15
+ | `screen.tsx` | The `Object.assign` compound surface |
16
+ | `screen-root.tsx` | The root box and its provider — see below for why it is split out |
17
+ | `screen-navbar.tsx` | `Screen.Navbar`, plus its own nested surface |
18
+ | `screen-navbar-title.tsx` | `Screen.Navbar.Title` |
19
+ | `screen-navbar-subtitle.tsx` | `Screen.Navbar.Subtitle` |
20
+ | `screen-navbar-back-button.tsx` | `Screen.Navbar.BackButton`, and `SCREEN_BACK_BUTTON_GLYPHS` |
21
+ | `screen-navbar-background.tsx` | The navbar's opaque backing and its bottom hairline — internal |
22
+ | `screen-content.tsx` | `Screen.Content` |
23
+ | `screen-view.tsx` | `Screen.View` |
24
+ | `screen-header.tsx` | `Screen.Header` |
25
+ | `screen-footer.tsx` | `Screen.Footer` |
26
+ | `screen-footer-background.tsx` | The footer's backing and its top hairline — internal |
27
+ | `screen-scroll-shadow.tsx` | `Screen.ScrollShadow`, and `SCROLL_SHADOW_EDGES` |
28
+ | `screen-scroll-area.tsx` | `Screen.ScrollArea` |
29
+ | `screen-flat-list.tsx` | `Screen.FlatList` |
30
+ | `screen-section-list.tsx` | `Screen.SectionList` |
31
+ | `screen-legend-list.tsx` | `Screen.LegendList` |
32
+ | `screen-chat-list.tsx` | `Screen.ChatList` |
33
+ | `screen-loading.tsx` | `Screen.Loading` |
34
+ | `screen-error.tsx` | `Screen.Error` |
35
+ | `screen-list-component.tsx` | A list header/footer prop, as a node — internal |
36
+ | `screen-debug.ts` | `SCREEN_DEBUG_COLORS` |
37
+ | `screen.context.tsx` | `ScreenProvider`, `useScreen()`, `useScreenContext()`, `useScreenPart()`, `useScreenDebug()` |
38
+ | `screen.types.ts` | Prop types shared by two or more parts |
39
+ | `screen.variants.ts` | Pure `tv()` slots + the footer maths, no RN imports |
40
+ | `screen.variants.test.ts` | |
41
+ | `use-screen-scroll-insets.ts` | The reserve every scrollable shares |
42
+
43
+ ## Design
44
+
45
+ - **The navbar and footer measure themselves into a Reanimated context**, and
46
+ every scrollable reserves exactly that. This is the whole point of the
47
+ component: no screen carries a hand-tuned padding number that is right on one
48
+ device and wrong on the next. The reserves are spacer *views* whose height is
49
+ an animated style, not content-container padding — padding cannot animate on
50
+ the UI thread, and the heights are only known after layout.
51
+ - **`placement`, not `variant`.** `overlay` (the default) floats the chrome over
52
+ the content, which insets itself to match; `static` puts it in the flow. In
53
+ this package `variant` always means a visual variant, so the axis that decides
54
+ where a part *sits* gets its own name.
55
+ - **Every scrollable forwards its ref through `React.forwardRef`.** `ScrollArea`
56
+ hands back `ScreenScrollViewRef`, `FlatList` a `FlatList<ItemT>`, `SectionList`
57
+ a `SectionList<ItemT, SectionT>`, `LegendList` and the chat list's `legend`
58
+ variant a `LegendListRef`, and the chat list's `flat` variant a `FlatList`. The
59
+ `*Props` types carry no `ref` member — `RefAttributes<T>` on the component type
60
+ carries it now — so do not add one back.
61
+
62
+ These — and the chat list's two private variants — are the **only** `forwardRef`
63
+ components in the package; everything else (`Input`, `Pressable`,
64
+ `Tabs.ScrollView`, `lib/slot`) is on React 19's ref-as-prop. The split is deliberate rather than half-finished. What it bought
65
+ here is that the ref is *named*: `FlatList`, `SectionList`, `LegendList` and
66
+ `ChatList`'s flat variant used to declare `ref?:` and never destructure it, so
67
+ it rode `{...props}` into the JSX — and in `LegendList` that spread is cast to
68
+ `AnimatedLegendListProps<ItemT>`, a type with no `ref` member, so nothing
69
+ checked that the ref arrived at all.
70
+ - **`forwardRef` erases type parameters, so every generic scrollable is cast to a
71
+ hand-written component type.** `forwardRef`'s result is a
72
+ `ForwardRefExoticComponent` over one concrete instantiation, so `data` and
73
+ `renderItem` would check against `unknown` and stop constraining each other.
74
+ `ScreenFlatListComponent` and its siblings restate the signature — the same
75
+ move `StyledAnimatedLegendListComponent` already makes for `withUniwind`. Each
76
+ carries a `displayName?: string` member, which is what keeps the trailing
77
+ assignment legal after the cast.
78
+ - **`ChatList`'s two ref types are correlated with `variant` by hand.**
79
+ `forwardRef<T, P>` has a single `T`, so the union arms in
80
+ `ScreenChatListComponent` and the two casts in the dispatcher are the only
81
+ thing holding `variant: "flat"` to a `FlatList<ItemT>` and `legend` to a
82
+ `LegendListRef`. Nothing checks them. Edit the arms and the branches together
83
+ or not at all.
84
+ - **`ChatList`'s legend variant composes its ref through `composeRefs`, memoised
85
+ on the caller's ref.** It needs an inner handle of its own for the
86
+ `scrollToEnd` fallback, so the caller's ref is fanned rather than passed
87
+ through. `composeRefs` mints a new callback per call and React detaches and
88
+ reattaches a ref whose identity changed, so calling it inline would null the
89
+ inner handle and re-resolve the native one on every render — with the
90
+ `scrollToEnd` effect firing against a ref mid-swap.
91
+ - **`ScrollArea` branches on `keyboardAware` rather than picking a component
92
+ into a variable.** A `cond ? A : B` component types its `ref` as the two
93
+ refs' intersection, which is neither engine's, so `forwardRef` could not name
94
+ a single instance type at all. The one cast in that branch is variance and not
95
+ a guess: `RefObject.current` is mutable and so invariant, while
96
+ `KeyboardAwareScrollViewRef` is genuinely `ScreenScrollViewRef` plus
97
+ `assureFocusedInputVisible`.
98
+ - **`ScreenScrollViewRef` is React Native's `ScrollView`, not
99
+ `ComponentRef<typeof Animated.ScrollView>`.** Reanimated's animated component
100
+ types carry no `RefAttributes`, so that expression collapses to `never` — and
101
+ `Ref<never>` still compiles at every call site, silently accepts any ref, and
102
+ hands the caller a `.current` with nothing on it. Both engines forward to the
103
+ same RN instance, so naming it directly is both truthful and usable from a
104
+ JS-thread `useRef` and a UI-thread `useAnimatedRef` alike.
105
+
106
+ ## Reserves and occupancy
107
+
108
+ - **The footer's padding is a style, not a class**, and that is load-bearing.
109
+ `footerOccupancy()` has to add the same band to a height measured at runtime,
110
+ and a class is unreadable from JS. `SCREEN_FOOTER_PADDING` and
111
+ `SCREEN_FLOATING_BOTTOM_GAP` therefore stay numbers in `screen.variants.ts`
112
+ rather than joining the tokens in `tokens.css` — one constant drives both the
113
+ render and the reserve, so they cannot drift. Do not "tidy" them into
114
+ utilities.
115
+ - **Two occupancy numbers, differing by exactly the safe-area inset.**
116
+ `footerOccupancy` is what the footer covers in list-content space and is the
117
+ same in both keyboard states; `footerAboveKeyboard` is what it covers above an
118
+ open keyboard and excludes the band the sticky shift parks behind it.
119
+ Conflating them is a real bug — the derivation is in the block comment above
120
+ them, and the tests pin both.
121
+
122
+ ## Lists
123
+
124
+ - **`Screen.ChatList` is a discriminated union on `variant`.** `legend`
125
+ (default) takes chronological oldest-first data and LegendList's own
126
+ `renderItem`; `flat` is an inverted `FlatList` with newest-first data and RN's.
127
+ The two `renderItem` contracts genuinely differ, so each variant exposes its
128
+ engine's own rather than adapting one into the other — an adapter would have to
129
+ fabricate the `separators` object RN's signature promises, and nothing would
130
+ honour it.
131
+ - **Seed a chat list with `composerBaseHeight`.** The footer publishes its real
132
+ height a commit or two after the list's first layout, and the list has already
133
+ scrolled to the end by then. Without the seed the newest message hides under
134
+ the composer — intermittently, which is the worst kind of wrong.
135
+
136
+ ## Debug
137
+
138
+ - **`<Screen debug>` paints every layer.** Opt-in per screen and deliberately
139
+ *not* gated behind `__DEV__`, so a reserve can still be inspected on a release
140
+ build. The green occupancy band's edge must land on the footer's red one; a red
141
+ sliver past it means the reserve is short. `SCREEN_DEBUG_COLORS` is exported so
142
+ a composer can paint matching bands. These are raw `rgba()` literals rather
143
+ than theme tokens on purpose — a debug layer in a semantic colour would be
144
+ invisible against the surface it sits on.
145
+
146
+ ## Chrome
147
+
148
+ - **Safe-area insets come from `useSafeAreaInsets()`, never from uniwind's
149
+ `*-safe` utilities.** `pt-safe` and friends compile to
150
+ `env(safe-area-inset-top)`, which resolves to **zero** on React Native — the
151
+ class applies, nothing moves, and a navbar draws over the status bar with no
152
+ error anywhere. Verified on a simulator, not assumed. The hook is also the
153
+ source the footer's occupancy maths already reads, so a container's inset and
154
+ the reserve computed against it cannot disagree. `resolveScreenEdgePadding`
155
+ turns an `insets` prop into a style object.
156
+ - **A `static` footer is opaque; an `overlay` footer is not.** The backing lives
157
+ INSIDE the sticky view so it travels with the keyboard translation — put it on
158
+ the positioned outer view instead and a static footer lifted over the content
159
+ lets that content show straight through it, its buttons legible only where
160
+ they happen to overlap blank space. An overlay footer stays transparent on
161
+ purpose: content is meant to scroll under it, and whatever the caller puts
162
+ inside brings its own surface.
163
+ - **Both hairlines are drawn at rest**, and `fadeBorderOnScroll` — the same prop
164
+ name on `Screen.Navbar` and `Screen.Footer` — opts into a scroll-linked ramp
165
+ instead. The two read **opposite ends of the scroll**: the navbar's answers
166
+ "is there content above?" and brightens as you leave the top, while the
167
+ footer's answers "is there content below?" and fades out as the content runs
168
+ out. A footer line driven by `scrollY` would be brightest exactly where there
169
+ is nothing left to scroll to. One `SCREEN_BORDER_FADE_DISTANCE` drives both,
170
+ because two numbers that should always agree are two numbers that can drift.
171
+ - **A worklet cannot rely on a module-scope helper.** `resolveNavbarBorderOpacity`
172
+ and `resolveFooterBorderOpacity` write their `Math.min(1, Math.max(0, …))`
173
+ clamp out separately rather than sharing one. Factoring it into a `clampUnit`
174
+ helper — itself marked `"worklet"` — crashed the UI thread with `undefined is
175
+ not a function`: it was not captured into the worklet's closure. No unit test
176
+ sees it, because on the JS thread the helper resolves perfectly. Keep a
177
+ worklet's body self-contained.
178
+ That default is the way round it is because a screen whose content starts
179
+ flush against the chrome wants the line from the first frame — with the fade
180
+ always on, screens re-added a border by hand, which is the signal the default
181
+ was wrong. Both ramps are written out rather than calling Reanimated's
182
+ `interpolate`, so they stay reachable from `bun test`, and both clamp at each
183
+ end because a rubber-banded overscroll reports a negative offset.
184
+
185
+ ## ScrollShadow
186
+
187
+ A scrollable with a hard edge looks finished. A row cut exactly in half by the
188
+ bottom of the viewport reads as a layout bug; the same row fading out reads as
189
+ more to come. `Screen.ScrollShadow` draws that fade, and it matters most under
190
+ chrome that floats over the content, where the cut lands mid-component and there
191
+ is no bar edge to explain it.
192
+
193
+ **It is a sibling of the body, never a wrapper around it.** The screen context
194
+ already publishes `scrollY`, `contentHeight` and `layoutHeight` from whichever
195
+ scrollable is mounted, so this reads the numbers instead of intercepting them.
196
+ Wrapping would mean cloning the child to attach an `onScroll`, and every
197
+ scrollable here already has one — `use-screen-scroll-insets` drives the reserve
198
+ animations from it — so a second writer would take a handler already spoken for.
199
+ The dividend is that one component works over `ScrollArea`, `FlatList`,
200
+ `SectionList`, `LegendList` and `ChatList` alike, because they all publish to the
201
+ same context.
202
+
203
+ **It places itself against the chrome.** The top fade starts at the navbar's
204
+ measured height and the bottom above the footer's, so it is correct under either
205
+ placement with the caller measuring nothing. A fade at zero would sit behind the
206
+ navbar and be visible nowhere, which is a component that silently does nothing.
207
+
208
+ **`coverTop` extends the band, it does not move it.** For chrome this package
209
+ cannot see — a bar floating between the navbar and the body — the content should
210
+ already be gone where it passes behind, so those points are painted solid and the
211
+ gradient begins below them. Offsetting the fade past the chrome instead leaves
212
+ content crisp behind whatever parts of it are transparent, then cuts abruptly
213
+ where the fade finally starts: a floating pill is mostly transparent row, so rows
214
+ slid behind it at full contrast and were chopped underneath. `coverBottom` is the
215
+ same for chrome at the end.
216
+
217
+ **`anchor` decides what it measures from, and chrome drawing this behind itself
218
+ needs `parent`.** Paint order is source order: a fade mounted as a sibling of the
219
+ body lands on top of anything mounted after it, so chrome that floats over the
220
+ content — a tab bar inside a navigator, where nothing outside can slot between
221
+ the pages and the bar — has to render the fade as its own first child and anchor
222
+ it to that container rather than to the screen.
223
+
224
+ **Each end fades only when there is something past it.** The top is out at rest
225
+ and arrives over the first `size` points; the bottom is there from the start and
226
+ leaves over the last. Content shorter than its viewport never scrolls, so
227
+ `maxScroll` is zero and both stay out — a fade over a short page promises content
228
+ that does not exist.
229
+
230
+ **The gradient is `expo-linear-gradient`, and it is the one hard peer this
231
+ component adds.** `screen.tsx` imports it through the compound, so every app that
232
+ touches `Screen` resolves it whether or not it draws a fade — that is the price
233
+ of a native gradient view over drawing one in SVG, and it is paid by consumers
234
+ rather than by this package.
235
+
236
+ **The far stop is the near colour at zero alpha, never `transparent`.** The
237
+ keyword is transparent *black*, so interpolating toward it drags every stop
238
+ between through grey — a dark bloom over a light ground, a milky one over a
239
+ dark. `transparentOf` in `lib/color.ts` takes the alpha off instead, and returns
240
+ `undefined` for a notation it cannot take apart; the component then declines to
241
+ draw rather than reaching for the keyword after all.
242
+
243
+ **Opacity ramps over a quarter of `size`, not all of it.** A fade that takes its
244
+ own height to arrive reads as lagging the finger; a quarter puts it there while
245
+ the first row is still leaving.
246
+
247
+ ## API
248
+
249
+ - **There is no `Navbar.Action`.** [`Button`](../button/AGENTS.md) already owns that vocabulary —
250
+ loading swap, icon inheritance, haptics, accessibility — so a navbar action is
251
+ written as `<Button size="icon-sm" variant="secondary">`. A second
252
+ definition of props a component does not change is one that can drift.
253
+ - **`Screen.Navbar.BackButton` takes an `onPress`.** This library has no
254
+ navigation dependency and must not gain one for a chevron. The app wires
255
+ `router.back()`. `Screen.Footer`'s `isFocused` prop is the same trade: an app
256
+ with a router passes `useIsFocused()`, and without one the footer behaves as
257
+ focused.
258
+
259
+ ## Structure
260
+
261
+ - **The root lives in `screen-root.tsx`, not `screen.tsx`.** `Screen.Loading`
262
+ and `Screen.Error` are whole screens — they render the root — so putting it in
263
+ the file that runs the `Object.assign` naming them would close a cycle. This is
264
+ rule 3 applied inside a folder.
265
+ - **A nested `<Screen>` passes the outer context through** rather than shadowing
266
+ it, so `Screen.Loading` returned from inside a screen does not start a second,
267
+ unread set of measurements.
268
+
269
+ ## Dependencies
270
+
271
+ - **One optional peer, and one hard one.** `@legendapp/list` is
272
+ `peerDependenciesMeta.optional`, so an app that never imports `Screen` never
273
+ resolves it. `expo-linear-gradient` is not optional: `Screen.ScrollShadow`
274
+ imports it at module scope and the compound imports that, so every consumer of
275
+ `Screen` needs it installed.
276
+ `react-native-keyboard-controller` used to be the second — see
277
+ [DelacourProvider](../provider/AGENTS.md) for why it stopped.
278
+ - **The app must mount `DelacourProvider` at its root.** It supplies the gesture
279
+ root, `SafeAreaProvider`, `KeyboardProvider` and — the one that is not optional
280
+ polish — `<KeyboardStateSync />` inside the keyboard provider.
281
+ `KeyboardProvider` owns exactly one pair of shared animation values for the
282
+ whole app, and on iOS they are written only from the `will` events. Any
283
+ teardown that skips one — an interactive dismiss interrupted by navigation, a
284
+ stack pop, an app suspend — pins them open app-wide, and every screen then
285
+ renders "keyboard open" over a keyboard that is not there. `Screen.Footer` runs
286
+ the same repair on mount as a backstop.
287
+
@@ -0,0 +1,85 @@
1
+ export { Screen } from "./screen";
2
+ export {
3
+ type ScreenContextValue,
4
+ type ScreenFooterMeasurements,
5
+ type ScreenNavbarMeasurements,
6
+ ScreenProvider,
7
+ type ScreenProviderProps,
8
+ useScreen,
9
+ useScreenContext,
10
+ useScreenDebug,
11
+ } from "./screen.context";
12
+ export type {
13
+ ScreenInsetProps,
14
+ ScreenPlacementProps,
15
+ ScreenScrollableProps,
16
+ ScreenScrollViewRef,
17
+ } from "./screen.types";
18
+ export {
19
+ CHAT_COMPOSER_GAP,
20
+ footerAboveKeyboard,
21
+ footerOccupancy,
22
+ resolveFooterBorderOpacity,
23
+ resolveNavbarBorderOpacity,
24
+ resolveScreenEdgePadding,
25
+ resolveScreenViewPadding,
26
+ resolveScrollBottomInset,
27
+ resolveScrollTopInset,
28
+ SCREEN_BORDER_FADE_DISTANCE,
29
+ SCREEN_EDGES,
30
+ SCREEN_FLOATING_BOTTOM_GAP,
31
+ SCREEN_FOOTER_PADDING,
32
+ SCREEN_PLACEMENTS,
33
+ SCREEN_SCROLL_INSET_MODES,
34
+ type ScreenEdge,
35
+ type ScreenEdgeInsets,
36
+ type ScreenEdgePadding,
37
+ type ScreenPlacement,
38
+ type ScreenVariantProps,
39
+ screenVariants,
40
+ } from "./screen.variants";
41
+ export type {
42
+ LegendListRef,
43
+ ScreenChatListFlatProps,
44
+ ScreenChatListLegendProps,
45
+ ScreenChatListProps,
46
+ } from "./screen-chat-list";
47
+ export { SCREEN_CHAT_INPUT_NATIVE_ID, type ScreenContentProps } from "./screen-content";
48
+ export { SCREEN_DEBUG_COLORS, type ScreenDebugLayer } from "./screen-debug";
49
+ export type { ScreenErrorProps } from "./screen-error";
50
+ export type { ScreenFlatListProps } from "./screen-flat-list";
51
+ export type { ScreenFooterProps } from "./screen-footer";
52
+ export type { ScreenFooterBackgroundProps } from "./screen-footer-background";
53
+ export type { ScreenHeaderProps } from "./screen-header";
54
+ export type { ScreenLegendListProps } from "./screen-legend-list";
55
+ export type { ScreenLoadingProps } from "./screen-loading";
56
+ export type { ScreenNavbarProps } from "./screen-navbar";
57
+ export {
58
+ SCREEN_BACK_BUTTON_GLYPHS,
59
+ type ScreenBackButtonGlyph,
60
+ type ScreenNavbarBackButtonProps,
61
+ } from "./screen-navbar-back-button";
62
+ export type { ScreenNavbarBackgroundProps } from "./screen-navbar-background";
63
+ export type { ScreenNavbarSubtitleProps } from "./screen-navbar-subtitle";
64
+ export type { ScreenNavbarTitleProps } from "./screen-navbar-title";
65
+ export type { ScreenRootProps } from "./screen-root";
66
+ export type { ScreenScrollAreaProps } from "./screen-scroll-area";
67
+ export {
68
+ SCROLL_SHADOW_EDGES,
69
+ ScreenScrollShadow,
70
+ type ScreenScrollShadowEdge,
71
+ type ScreenScrollShadowProps,
72
+ } from "./screen-scroll-shadow";
73
+ export type { ScreenSectionListProps } from "./screen-section-list";
74
+ export type { ScreenViewProps } from "./screen-view";
75
+ export {
76
+ type ChatComposerSpacer,
77
+ type ScreenScrollInsetMode,
78
+ type ScreenScrollInsets,
79
+ useChatComposerBaseSpacerHeight,
80
+ useChatComposerGrowthPadding,
81
+ useChatComposerInset,
82
+ useScreenFooterKeyboardClearance,
83
+ useScreenFooterOverlayHeight,
84
+ useScreenScrollInsets,
85
+ } from "./use-screen-scroll-insets";