@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,245 @@
1
+ import {
2
+ type BottomSheetBackdropProps,
3
+ BottomSheetModal,
4
+ type BottomSheetModalProps,
5
+ type BottomSheetBackgroundProps as GorhomBottomSheetBackgroundProps,
6
+ type BottomSheetFooterProps as GorhomBottomSheetFooterProps,
7
+ type BottomSheetHandleProps as GorhomBottomSheetHandleProps,
8
+ } from "@gorhom/bottom-sheet";
9
+ import {
10
+ Children,
11
+ cloneElement,
12
+ isValidElement,
13
+ type ReactElement,
14
+ type ReactNode,
15
+ useCallback,
16
+ useEffect,
17
+ useMemo,
18
+ useRef,
19
+ } from "react";
20
+ import { useSharedValue } from "react-native-reanimated";
21
+ import { useKeyboardAnimationGuard } from "../../hooks/use-keyboard-state-sync";
22
+ import {
23
+ BottomSheetContainerProvider,
24
+ BottomSheetProvider,
25
+ useBottomSheetPart,
26
+ useBottomSheetPortalContext,
27
+ } from "./bottom-sheet.context";
28
+ import {
29
+ BOTTOM_SHEET_KEYBOARD_DEFAULTS,
30
+ type BottomSheetFooterFlag,
31
+ type BottomSheetFooterPlacement,
32
+ resolveFooterPlacement,
33
+ } from "./bottom-sheet.variants";
34
+ import { BottomSheetBackground } from "./bottom-sheet-background";
35
+ import { BottomSheetFooter, type BottomSheetFooterProps } from "./bottom-sheet-footer";
36
+ import { BottomSheetHandle } from "./bottom-sheet-handle";
37
+
38
+ export type BottomSheetContainerProps = Omit<
39
+ BottomSheetModalProps,
40
+ "backdropComponent" | "backgroundComponent" | "children" | "footerComponent" | "handleComponent" | "onDismiss" | "ref"
41
+ > & {
42
+ children?: ReactNode;
43
+ /** Classes for the sheet's surface — the box behind the handle and the content. */
44
+ backgroundClassName?: string;
45
+ /** Classes for the row the grabber sits in. */
46
+ handleClassName?: string;
47
+ /** Classes for the grabber itself. */
48
+ handleIndicatorClassName?: string;
49
+ };
50
+
51
+ /**
52
+ * The sheet itself: the surface that slides up, and everything inside it.
53
+ *
54
+ * Every gorhom prop passes through, so snap points, dynamic sizing, the pan
55
+ * gestures and the animation configs are all reachable. Four are not, because
56
+ * this component supplies them: the backdrop and the footer are cloned from the
57
+ * parts a caller wrote, and the background and the handle are rendered as views
58
+ * of this package's own so they can take a `className` rather than a style.
59
+ *
60
+ * **It takes no `className`.** gorhom's modal wears an animated style that the
61
+ * sheet's own position writes to every frame, so there is nothing here for a
62
+ * class to reach that `backgroundClassName` does not already reach better. The
63
+ * same trade `Screen.Footer` makes — that file takes a `style` and never a
64
+ * `className`, and so wraps nothing in `withUniwind`.
65
+ *
66
+ * **Presenting is driven by state, never by the ref.** `isOpen` on the root is
67
+ * the one source, so a swipe-down, a backdrop press, `BottomSheet.Close` and a
68
+ * programmatic close all arrive at the same `onOpenChange`. gorhom's own
69
+ * `onDismiss` is therefore not forwarded: two callbacks for one event is two
70
+ * things that can disagree. `useBottomSheet().sheetRef` is still there for the
71
+ * imperative calls with no declarative form — `snapToIndex`, `expand`.
72
+ *
73
+ * **`accessible` defaults to `false`, and that is not a small thing.** gorhom
74
+ * marks the sheet's content container `accessible` with the label "Bottom
75
+ * Sheet" and the `adjustable` role. On iOS an `accessible` container collapses
76
+ * its whole subtree into ONE element, so every field, button and line of copy in
77
+ * the sheet becomes unreachable to VoiceOver — a form in a sheet cannot be
78
+ * filled in at all. This is `Field`'s rule one component along: the row is
79
+ * `accessible={false}` so the control stays the element a screen reader sees.
80
+ * Pass `accessible` to opt back into gorhom's behaviour for a sheet whose body
81
+ * really is one adjustable thing.
82
+ *
83
+ * Three defaults differ from gorhom's. `enablePanDownToClose` is on, because a
84
+ * modal sheet has no collapsed resting state to pan down to. And the keyboard
85
+ * trio comes from `BOTTOM_SHEET_KEYBOARD_DEFAULTS` — see that const for why
86
+ * Android must not be left on `adjustPan`. Both are ordinary props, so either
87
+ * can be turned back off.
88
+ *
89
+ * @example
90
+ * <BottomSheet.Container>
91
+ * <BottomSheet.Content>{…}</BottomSheet.Content>
92
+ * </BottomSheet.Container>
93
+ *
94
+ * @example
95
+ * // A scrollable sheet needs a fixed height to scroll within.
96
+ * <BottomSheet.Container enableDynamicSizing={false} snapPoints={["50%", "90%"]}>
97
+ * <BottomSheet.ScrollView>{rows}</BottomSheet.ScrollView>
98
+ * <BottomSheet.Footer sticky>{actions}</BottomSheet.Footer>
99
+ * </BottomSheet.Container>
100
+ */
101
+ export function BottomSheetContainer({
102
+ accessible = false,
103
+ backgroundClassName,
104
+ children,
105
+ enablePanDownToClose = true,
106
+ handleClassName,
107
+ handleIndicatorClassName,
108
+ ...props
109
+ }: BottomSheetContainerProps): ReactElement {
110
+ const sheet = useBottomSheetPart("BottomSheet.Container");
111
+ const { isOpen, setOpen, sheetRef } = sheet;
112
+ const portal = useBottomSheetPortalContext();
113
+ // A sheet that opens while `KeyboardProvider`'s shared values are pinned open
114
+ // by a keyboard that vanished without a `will` event would size itself for a
115
+ // keyboard that is not there. `Screen.Footer` runs the same repair on mount.
116
+ useKeyboardAnimationGuard();
117
+
118
+ // Whether this sheet has ever been presented. A JS ref rather than state:
119
+ // nothing renders differently for it, and it has to be readable inside the
120
+ // effect that sets it.
121
+ const hasPresentedRef = useRef(false);
122
+
123
+ const { content, footer, placement } = useMemo(() => hoistFooter(children), [children]);
124
+ const hasStickyFooter = placement === "sticky";
125
+ const overlay = portal?.overlay ?? null;
126
+
127
+ // Written by the pinned footer's own onLayout, read by the body's spacer. It
128
+ // lives here because the two are siblings in gorhom's tree and neither can see
129
+ // the other — the footer is rendered outside the children this file wraps.
130
+ const footerHeight = useSharedValue(0);
131
+
132
+ const containerValue = useMemo(() => ({ footerHeight, hasStickyFooter }), [footerHeight, hasStickyFooter]);
133
+
134
+ const backdropComponent = useMemo(
135
+ () =>
136
+ overlay === null ? undefined : (backdropProps: BottomSheetBackdropProps) => cloneElement(overlay, backdropProps),
137
+ [overlay]
138
+ );
139
+
140
+ const footerComponent = useMemo(
141
+ () =>
142
+ footer === null
143
+ ? undefined
144
+ : (footerProps: GorhomBottomSheetFooterProps) => cloneElement(footer, { ...footerProps, footerHeight }),
145
+ [footer, footerHeight]
146
+ );
147
+
148
+ const backgroundComponent = useCallback(
149
+ (backgroundProps: GorhomBottomSheetBackgroundProps) => (
150
+ <BottomSheetBackground {...backgroundProps} className={backgroundClassName} />
151
+ ),
152
+ [backgroundClassName]
153
+ );
154
+
155
+ const handleComponent = useCallback(
156
+ (handleProps: GorhomBottomSheetHandleProps) => (
157
+ <BottomSheetHandle {...handleProps} className={handleClassName} indicatorClassName={handleIndicatorClassName} />
158
+ ),
159
+ [handleClassName, handleIndicatorClassName]
160
+ );
161
+
162
+ const handleDismiss = useCallback(() => {
163
+ hasPresentedRef.current = false;
164
+ setOpen(false);
165
+ }, [setOpen]);
166
+
167
+ useEffect(() => {
168
+ if (isOpen) {
169
+ hasPresentedRef.current = true;
170
+ sheetRef.current?.present();
171
+ return;
172
+ }
173
+
174
+ // Dismissing a modal that was never presented is NOT a no-op, and this is
175
+ // the sharpest edge in the component. gorhom's modal starts at status
176
+ // `INITIAL`; `dismiss()` reads that as a live sheet being closed and moves
177
+ // it to `DISMISSING`, where it stays. The next `present()` then mounts the
178
+ // portal and renders the sheet — and it never animates in, never lays out
179
+ // its content, and never fires `onChange`. Nothing is logged, nothing
180
+ // throws, and the screen simply does not change.
181
+ if (!hasPresentedRef.current) return;
182
+ hasPresentedRef.current = false;
183
+ sheetRef.current?.dismiss();
184
+ }, [isOpen, sheetRef]);
185
+
186
+ return (
187
+ <BottomSheetModal
188
+ accessible={accessible}
189
+ backdropComponent={backdropComponent}
190
+ backgroundComponent={backgroundComponent}
191
+ enablePanDownToClose={enablePanDownToClose}
192
+ footerComponent={footerComponent}
193
+ handleComponent={handleComponent}
194
+ onDismiss={handleDismiss}
195
+ ref={sheetRef}
196
+ {...BOTTOM_SHEET_KEYBOARD_DEFAULTS}
197
+ {...props}
198
+ >
199
+ <BottomSheetProvider value={sheet}>
200
+ <BottomSheetContainerProvider value={containerValue}>{content}</BottomSheetContainerProvider>
201
+ </BottomSheetProvider>
202
+ </BottomSheetModal>
203
+ );
204
+ }
205
+ BottomSheetContainer.displayName = "DelacourUI.BottomSheet.Container";
206
+
207
+ /**
208
+ * Splits a container's children into the body and a footer to be pinned.
209
+ *
210
+ * gorhom takes a footer as a `footerComponent` render prop rather than as a
211
+ * child, so a sticky one has to leave the tree it was written in; an inline one
212
+ * stays put and is simply part of the body. {@link resolveFooterPlacement} is the
213
+ * decision and is pure, so `bun test` reaches the whole matrix — this walk only
214
+ * turns children into the flags it reads.
215
+ *
216
+ * Lives here rather than with the parts: it is the container that lifts its own
217
+ * children, and importing it from `bottom-sheet-footer.tsx` would close a cycle.
218
+ * See AGENTS.md.
219
+ */
220
+ function hoistFooter(children: ReactNode): {
221
+ content: ReactNode;
222
+ footer: ReactElement<BottomSheetFooterProps> | null;
223
+ placement: BottomSheetFooterPlacement;
224
+ } {
225
+ const items = Children.toArray(children);
226
+ const flags: BottomSheetFooterFlag[] = [];
227
+ let footer: ReactElement<BottomSheetFooterProps> | null = null;
228
+
229
+ for (const child of items) {
230
+ if (!isValidElement(child) || child.type !== BottomSheetFooter) {
231
+ flags.push({ isFooter: false, isSticky: false });
232
+ continue;
233
+ }
234
+
235
+ const element = child as ReactElement<BottomSheetFooterProps>;
236
+ const isSticky = element.props.sticky === true;
237
+ flags.push({ isFooter: true, isSticky });
238
+ if (isSticky) footer ??= element;
239
+ }
240
+
241
+ const placement = resolveFooterPlacement(flags);
242
+ if (placement !== "sticky" || footer === null) return { content: items, footer: null, placement };
243
+
244
+ return { content: items.filter((child) => child !== footer), footer, placement };
245
+ }
@@ -0,0 +1,66 @@
1
+ import { BottomSheetView } from "@gorhom/bottom-sheet";
2
+ import type { ReactElement } from "react";
3
+ import { View, type ViewProps } from "react-native";
4
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
6
+ import { useBottomSheetContainerContext } from "./bottom-sheet.context";
7
+ import { BOTTOM_SHEET_FOOTER_GAP, bottomSheetVariants, resolveSheetBottomInset } from "./bottom-sheet.variants";
8
+
9
+ export type BottomSheetContentProps = ViewProps & {
10
+ className?: string;
11
+ };
12
+
13
+ /**
14
+ * The sheet's body — what a caller writes the title, the copy and the controls
15
+ * into.
16
+ *
17
+ * Two boxes, and neither is a `withUniwind` wrapper. gorhom's `BottomSheetView`
18
+ * is the outer one: it measures itself into the sheet's layout state, which is
19
+ * what makes `enableDynamicSizing` size the sheet to whatever is inside. The
20
+ * inner `View` is an ordinary React Native one and carries every class, so this
21
+ * file wraps nothing — the same move `Screen.Footer` makes with
22
+ * `KeyboardStickyView`.
23
+ *
24
+ * **A pinned footer is reserved by a spacer, not by gorhom's
25
+ * `enableFooterMarginAdjustment`.** That prop turns the footer's measured height
26
+ * into content-container padding through React state — a commit on every frame
27
+ * of the keyboard animation, because the footer changes height as it gives up
28
+ * its safe-area band. A spacer whose height is an animated style off the same
29
+ * shared value costs nothing on the JS thread, and `Screen` already reserves its
30
+ * own chrome exactly this way. The spacer sits OUTSIDE the classed box so the
31
+ * content's own `gap` does not land in front of it and quietly double the
32
+ * {@link BOTTOM_SHEET_FOOTER_GAP} the footer is held off by.
33
+ *
34
+ * **The safe-area band goes on the OUTER box, and only when nothing else is
35
+ * paying for it** — a pinned footer's own box carries it, so
36
+ * {@link resolveSheetBottomInset} withholds it here. The band comes from
37
+ * `useSafeAreaInsets()` and never from `pb-safe`, which compiles to
38
+ * `env(safe-area-inset-bottom)` and resolves to zero on React Native.
39
+ *
40
+ * @example
41
+ * <BottomSheet.Content>
42
+ * <BottomSheet.Title>Delete this file?</BottomSheet.Title>
43
+ * <BottomSheet.Description>This cannot be undone.</BottomSheet.Description>
44
+ * </BottomSheet.Content>
45
+ */
46
+ export function BottomSheetContent({ className, children, style, ...props }: BottomSheetContentProps): ReactElement {
47
+ const container = useBottomSheetContainerContext();
48
+ const { bottom } = useSafeAreaInsets();
49
+ const hasStickyFooter = container?.hasStickyFooter ?? false;
50
+ const footerHeight = container?.footerHeight;
51
+ const paddingBottom = resolveSheetBottomInset({ bottom, hasStickyFooter });
52
+
53
+ const reserve = useAnimatedStyle(() => ({
54
+ height: footerHeight === undefined ? 0 : footerHeight.value + BOTTOM_SHEET_FOOTER_GAP,
55
+ }));
56
+
57
+ return (
58
+ <BottomSheetView style={{ paddingBottom }}>
59
+ <View className={bottomSheetVariants().content({ className })} style={style} {...props}>
60
+ {children}
61
+ </View>
62
+ {hasStickyFooter ? <Animated.View style={reserve} /> : null}
63
+ </BottomSheetView>
64
+ );
65
+ }
66
+ BottomSheetContent.displayName = "DelacourUI.BottomSheet.Content";
@@ -0,0 +1,29 @@
1
+ import type { ReactElement } from "react";
2
+ import { cn } from "../../lib/cn";
3
+ import { Text, type TextPresetProps } from "../text";
4
+
5
+ export type BottomSheetDescriptionProps = TextPresetProps;
6
+
7
+ /**
8
+ * Supporting copy under the sheet's title.
9
+ *
10
+ * *Is* a `Text.Paragraph`, defaulting to the muted token — the title is what the
11
+ * sheet is about and this is what it means, so the two need to read as a
12
+ * hierarchy rather than as two equal lines. `color` is an ordinary prop, so a
13
+ * description that needs the page colour asks for it.
14
+ *
15
+ * It has no slot in `bottomSheetVariants`, and that is deliberate: it carries no
16
+ * layout of its own, and `tv` emits `undefined` for an empty class string, so a
17
+ * slot here would be one nothing could assert against.
18
+ *
19
+ * @example
20
+ * <BottomSheet.Description>Update to the latest version.</BottomSheet.Description>
21
+ */
22
+ export function BottomSheetDescription({
23
+ className,
24
+ color = "muted",
25
+ ...props
26
+ }: BottomSheetDescriptionProps): ReactElement {
27
+ return <Text.Paragraph className={cn(className)} color={color} {...props} />;
28
+ }
29
+ BottomSheetDescription.displayName = "DelacourUI.BottomSheet.Description";
@@ -0,0 +1,148 @@
1
+ import { BottomSheetFooter as GorhomBottomSheetFooter } from "@gorhom/bottom-sheet";
2
+ import { type ComponentProps, type ReactElement, type ReactNode, useCallback, useEffect } from "react";
3
+ import { type LayoutChangeEvent, View, type ViewProps } from "react-native";
4
+ import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
5
+ import Animated, { type SharedValue, useAnimatedStyle } from "react-native-reanimated";
6
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
7
+ import { BOTTOM_SHEET_FOOTER_PADDING, bottomSheetVariants } from "./bottom-sheet.variants";
8
+
9
+ type GorhomFooterProps = ComponentProps<typeof GorhomBottomSheetFooter>;
10
+
11
+ export type BottomSheetFooterProps = Omit<ViewProps, "children"> & {
12
+ /**
13
+ * Pin the footer to the bottom of the sheet, above the keyboard, instead of
14
+ * leaving it in the content's flow.
15
+ *
16
+ * Off by default, matching `Screen.Footer` — a footer holding a submit button
17
+ * under a short sheet wants to sit where it was written, and a sheet only
18
+ * needs the pinned treatment once its body can scroll under one.
19
+ *
20
+ * @default false
21
+ */
22
+ sticky?: boolean;
23
+ className?: string;
24
+ children?: ReactNode;
25
+ /**
26
+ * The sheet's own footer position. Injected by `BottomSheet.Container` when it
27
+ * hoists a sticky footer; never written at a call site.
28
+ */
29
+ animatedFooterPosition?: GorhomFooterProps["animatedFooterPosition"];
30
+ /**
31
+ * Where this footer publishes its measured height. Injected alongside the
32
+ * position, and read by whatever the sheet's body is so it can reserve exactly
33
+ * what the footer covers. Never written at a call site.
34
+ */
35
+ footerHeight?: SharedValue<number>;
36
+ };
37
+
38
+ /**
39
+ * A row of controls at the bottom of the sheet — pinned, or in the flow.
40
+ *
41
+ * **A sticky footer is not rendered where it is written.** gorhom takes it as a
42
+ * `footerComponent` render prop rather than as a child, so
43
+ * `BottomSheet.Container` lifts this element out of its children and clones it
44
+ * into that slot with the sheet's animated footer position attached. An inline
45
+ * footer needs none of that and stays exactly where it was put.
46
+ *
47
+ * The two branches deliberately look different. An inline footer is in the flow
48
+ * and inherits the sheet's surface; a pinned one draws OVER the content, so it
49
+ * brings a background and a top hairline of its own — without them the content
50
+ * scrolls straight through it and its buttons are legible only where they happen
51
+ * to overlap blank space. That is `Screen.Footer`'s rule about its backing, one
52
+ * component along.
53
+ *
54
+ * **A pinned footer is TALLER by the safe-area band; it is not moved up by it.**
55
+ * gorhom's own `bottomInset` translates the whole footer up instead, which
56
+ * leaves a band the height of the home indicator between the footer and the
57
+ * bottom of the sheet — and the content scrolls through it in plain view. So
58
+ * `bottomInset` stays 0 and the band becomes the footer's own `paddingBottom`,
59
+ * which puts its surface against the bottom of the screen where it belongs.
60
+ *
61
+ * The keyboard then needs that band back, and **only that band**. Its own
62
+ * padding stays, so the controls never end up flush against the keyboard, and
63
+ * the footer genuinely gets shorter rather than sliding down — which is what
64
+ * lets the body's reserve shrink with it instead of over-reserving by the height
65
+ * of a home indicator that is no longer there.
66
+ *
67
+ * It publishes its measured height into the sheet, and `BottomSheet.Content` and
68
+ * `BottomSheet.ScrollView` reserve exactly that plus a gap. The height is a
69
+ * shared value read by an animated spacer, never content-container padding —
70
+ * `Screen` reserves its own chrome the same way and for the same reason.
71
+ *
72
+ * **Do not put a `KeyboardStickyView` in here.** gorhom's `animatedFooterPosition`
73
+ * already carries the footer clear of the keyboard; this translate only gives up
74
+ * the safe-area band on top of that. A third mechanism moving the same view
75
+ * would fight both for prop ownership every frame.
76
+ *
77
+ * Written with `sticky` but rendered outside a container, it falls back to the
78
+ * inline branch rather than throwing: there is no footer position to ride, and a
79
+ * row of buttons in the flow is the useful failure.
80
+ *
81
+ * @example
82
+ * <BottomSheet.Footer>
83
+ * <Button onPress={save}>Save</Button>
84
+ * </BottomSheet.Footer>
85
+ *
86
+ * @example
87
+ * <BottomSheet.Footer sticky>
88
+ * <Button onPress={submit}>Submit</Button>
89
+ * </BottomSheet.Footer>
90
+ */
91
+ export function BottomSheetFooter({
92
+ animatedFooterPosition,
93
+ className,
94
+ children,
95
+ footerHeight,
96
+ onLayout,
97
+ sticky = false,
98
+ style,
99
+ ...props
100
+ }: BottomSheetFooterProps): ReactElement {
101
+ const { bottom } = useSafeAreaInsets();
102
+ const { progress } = useReanimatedKeyboardAnimation();
103
+ const slots = bottomSheetVariants();
104
+
105
+ // The footer keeps its own padding and gives up only the safe-area band, which
106
+ // collapses as the keyboard arrives. `progress` runs 0 → 1 over the keyboard's
107
+ // own animation, so the two travel together.
108
+ const padding = useAnimatedStyle(() => ({
109
+ paddingBottom: BOTTOM_SHEET_FOOTER_PADDING + bottom * (1 - progress.value),
110
+ paddingTop: BOTTOM_SHEET_FOOTER_PADDING,
111
+ }));
112
+
113
+ const handleLayout = useCallback(
114
+ (event: LayoutChangeEvent) => {
115
+ if (footerHeight) footerHeight.value = event.nativeEvent.layout.height;
116
+ onLayout?.(event);
117
+ },
118
+ [footerHeight, onLayout]
119
+ );
120
+
121
+ useEffect(() => {
122
+ return () => {
123
+ if (footerHeight) footerHeight.value = 0;
124
+ };
125
+ }, [footerHeight]);
126
+
127
+ if (!sticky || animatedFooterPosition === undefined) {
128
+ return (
129
+ <View className={slots.footer({ className })} style={style} {...props}>
130
+ {children}
131
+ </View>
132
+ );
133
+ }
134
+
135
+ return (
136
+ <GorhomBottomSheetFooter animatedFooterPosition={animatedFooterPosition} bottomInset={0}>
137
+ <Animated.View
138
+ className={slots.stickyFooter({ className })}
139
+ onLayout={handleLayout}
140
+ style={[padding, style]}
141
+ {...props}
142
+ >
143
+ {children}
144
+ </Animated.View>
145
+ </GorhomBottomSheetFooter>
146
+ );
147
+ }
148
+ BottomSheetFooter.displayName = "DelacourUI.BottomSheet.Footer";
@@ -0,0 +1,47 @@
1
+ import type { BottomSheetHandleProps as GorhomBottomSheetHandleProps } from "@gorhom/bottom-sheet";
2
+ import type { ReactElement } from "react";
3
+ import { View, type ViewProps } from "react-native";
4
+ import { bottomSheetVariants } from "./bottom-sheet.variants";
5
+
6
+ export type BottomSheetHandleProps = Partial<GorhomBottomSheetHandleProps> &
7
+ Omit<ViewProps, "children"> & {
8
+ className?: string;
9
+ /** Style for the pill itself. What gorhom's `handleIndicatorStyle` becomes. */
10
+ indicatorStyle?: ViewProps["style"];
11
+ indicatorClassName?: string;
12
+ };
13
+
14
+ /**
15
+ * The grabber at the top of the sheet.
16
+ *
17
+ * Supplied to gorhom as `handleComponent` for the same reason the background is:
18
+ * this package paints with classNames, and gorhom's `handleStyle` /
19
+ * `handleIndicatorStyle` cannot carry one. Both still arrive — gorhom passes
20
+ * them down as `style` and `indicatorStyle` — so a caller who has a style object
21
+ * rather than a class is not locked out.
22
+ *
23
+ * The pill is drawn but never announced: gorhom's own handle container wraps this
24
+ * in the `Animated.View` that owns the pan and carries the accessibility surface,
25
+ * so a second announced element here would put a nameless control in front of
26
+ * every sheet.
27
+ *
28
+ * The two animated variables are destructured off rather than spread — see
29
+ * `BottomSheetBackground`.
30
+ */
31
+ export function BottomSheetHandle({
32
+ animatedIndex: _animatedIndex,
33
+ animatedPosition: _animatedPosition,
34
+ className,
35
+ indicatorClassName,
36
+ indicatorStyle,
37
+ ...props
38
+ }: BottomSheetHandleProps): ReactElement {
39
+ const slots = bottomSheetVariants();
40
+
41
+ return (
42
+ <View className={slots.handle({ className })} {...props}>
43
+ <View className={slots.handleIndicator({ className: indicatorClassName })} style={indicatorStyle} />
44
+ </View>
45
+ );
46
+ }
47
+ BottomSheetHandle.displayName = "DelacourUI.BottomSheet.Handle";