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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +113 -0
  2. package/package.json +125 -0
  3. package/src/components/accordion/AGENTS.md +193 -0
  4. package/src/components/accordion/accordion-content.tsx +125 -0
  5. package/src/components/accordion/accordion-description.tsx +17 -0
  6. package/src/components/accordion/accordion-indicator.tsx +104 -0
  7. package/src/components/accordion/accordion-item.tsx +78 -0
  8. package/src/components/accordion/accordion-title.tsx +18 -0
  9. package/src/components/accordion/accordion-trigger.tsx +148 -0
  10. package/src/components/accordion/accordion.context.tsx +153 -0
  11. package/src/components/accordion/accordion.tsx +253 -0
  12. package/src/components/accordion/accordion.types.ts +11 -0
  13. package/src/components/accordion/accordion.variants.test.ts +434 -0
  14. package/src/components/accordion/accordion.variants.ts +358 -0
  15. package/src/components/accordion/index.ts +45 -0
  16. package/src/components/badge/AGENTS.md +83 -0
  17. package/src/components/badge/badge-close-button.tsx +48 -0
  18. package/src/components/badge/badge-end-content.tsx +15 -0
  19. package/src/components/badge/badge-label.tsx +24 -0
  20. package/src/components/badge/badge-start-content.tsx +16 -0
  21. package/src/components/badge/badge.context.tsx +64 -0
  22. package/src/components/badge/badge.tsx +192 -0
  23. package/src/components/badge/badge.types.ts +10 -0
  24. package/src/components/badge/badge.variants.test.ts +269 -0
  25. package/src/components/badge/badge.variants.ts +238 -0
  26. package/src/components/badge/index.ts +17 -0
  27. package/src/components/bottom-sheet/AGENTS.md +242 -0
  28. package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
  29. package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
  30. package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
  31. package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
  32. package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
  33. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
  34. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
  35. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
  36. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
  37. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
  38. package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
  39. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
  40. package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
  41. package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
  42. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
  43. package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
  44. package/src/components/bottom-sheet/index.ts +40 -0
  45. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
  46. package/src/components/button/AGENTS.md +217 -0
  47. package/src/components/button/button-end-content.tsx +15 -0
  48. package/src/components/button/button-group-separator.tsx +37 -0
  49. package/src/components/button/button-group-text.tsx +101 -0
  50. package/src/components/button/button-group.tsx +158 -0
  51. package/src/components/button/button-label.tsx +24 -0
  52. package/src/components/button/button-start-content.tsx +15 -0
  53. package/src/components/button/button.context.tsx +187 -0
  54. package/src/components/button/button.tsx +273 -0
  55. package/src/components/button/button.types.ts +10 -0
  56. package/src/components/button/button.variants.test.ts +811 -0
  57. package/src/components/button/button.variants.ts +480 -0
  58. package/src/components/button/index.ts +50 -0
  59. package/src/components/chart/AGENTS.md +210 -0
  60. package/src/components/chart/chart-area.tsx +93 -0
  61. package/src/components/chart/chart-bar.tsx +103 -0
  62. package/src/components/chart/chart-bars.tsx +75 -0
  63. package/src/components/chart/chart-candlestick.tsx +60 -0
  64. package/src/components/chart/chart-grid.tsx +33 -0
  65. package/src/components/chart/chart-legend.tsx +41 -0
  66. package/src/components/chart/chart-line.tsx +50 -0
  67. package/src/components/chart/chart-scatter.tsx +35 -0
  68. package/src/components/chart/chart-tooltip-dot.tsx +70 -0
  69. package/src/components/chart/chart-tooltip-x.tsx +121 -0
  70. package/src/components/chart/chart-tooltip-y.tsx +55 -0
  71. package/src/components/chart/chart-tooltip.tsx +133 -0
  72. package/src/components/chart/chart-x-axis.tsx +27 -0
  73. package/src/components/chart/chart-y-axis.tsx +21 -0
  74. package/src/components/chart/chart.context.tsx +127 -0
  75. package/src/components/chart/chart.tsx +448 -0
  76. package/src/components/chart/chart.types.ts +108 -0
  77. package/src/components/chart/chart.variants.test.ts +893 -0
  78. package/src/components/chart/chart.variants.ts +726 -0
  79. package/src/components/chart/index.ts +103 -0
  80. package/src/components/chart/pie-chart-center.tsx +40 -0
  81. package/src/components/chart/pie-chart-label.tsx +56 -0
  82. package/src/components/chart/pie-chart-slice.tsx +45 -0
  83. package/src/components/chart/pie-chart-tooltip.tsx +85 -0
  84. package/src/components/chart/pie-chart.context.tsx +77 -0
  85. package/src/components/chart/pie-chart.tsx +247 -0
  86. package/src/components/chart/use-chart-font.ts +25 -0
  87. package/src/components/chart/use-chart-palette.ts +38 -0
  88. package/src/components/checkbox/AGENTS.md +185 -0
  89. package/src/components/checkbox/checkbox-box.tsx +152 -0
  90. package/src/components/checkbox/checkbox-group.tsx +107 -0
  91. package/src/components/checkbox/checkbox-label.tsx +34 -0
  92. package/src/components/checkbox/checkbox.context.tsx +136 -0
  93. package/src/components/checkbox/checkbox.tsx +258 -0
  94. package/src/components/checkbox/checkbox.types.ts +14 -0
  95. package/src/components/checkbox/checkbox.variants.test.ts +634 -0
  96. package/src/components/checkbox/checkbox.variants.ts +484 -0
  97. package/src/components/checkbox/index.ts +42 -0
  98. package/src/components/field/AGENTS.md +112 -0
  99. package/src/components/field/field-content.tsx +24 -0
  100. package/src/components/field/field-description.tsx +29 -0
  101. package/src/components/field/field-error.tsx +61 -0
  102. package/src/components/field/field-group.tsx +22 -0
  103. package/src/components/field/field-label.tsx +47 -0
  104. package/src/components/field/field-legend.tsx +34 -0
  105. package/src/components/field/field-separator.tsx +81 -0
  106. package/src/components/field/field-set.tsx +22 -0
  107. package/src/components/field/field.context.tsx +95 -0
  108. package/src/components/field/field.tsx +157 -0
  109. package/src/components/field/field.types.ts +15 -0
  110. package/src/components/field/field.variants.test.ts +240 -0
  111. package/src/components/field/field.variants.ts +156 -0
  112. package/src/components/field/index.ts +22 -0
  113. package/src/components/icon/AGENTS.md +104 -0
  114. package/src/components/icon/icon.context.tsx +29 -0
  115. package/src/components/icon/icon.tsx +92 -0
  116. package/src/components/icon/icon.variants.test.ts +111 -0
  117. package/src/components/icon/icon.variants.ts +79 -0
  118. package/src/components/icon/index.ts +11 -0
  119. package/src/components/input/AGENTS.md +126 -0
  120. package/src/components/input/index.ts +27 -0
  121. package/src/components/input/input-group-decorator.tsx +84 -0
  122. package/src/components/input/input-group-prefix.tsx +16 -0
  123. package/src/components/input/input-group-suffix.tsx +15 -0
  124. package/src/components/input/input-group.tsx +140 -0
  125. package/src/components/input/input.context.tsx +87 -0
  126. package/src/components/input/input.tsx +184 -0
  127. package/src/components/input/input.types.ts +10 -0
  128. package/src/components/input/input.variants.test.ts +369 -0
  129. package/src/components/input/input.variants.ts +329 -0
  130. package/src/components/list-group/AGENTS.md +67 -0
  131. package/src/components/list-group/index.ts +20 -0
  132. package/src/components/list-group/list-group-item-content.tsx +10 -0
  133. package/src/components/list-group/list-group-item-description.tsx +12 -0
  134. package/src/components/list-group/list-group-item-prefix.tsx +30 -0
  135. package/src/components/list-group/list-group-item-suffix.tsx +55 -0
  136. package/src/components/list-group/list-group-item-title.tsx +17 -0
  137. package/src/components/list-group/list-group-item.tsx +87 -0
  138. package/src/components/list-group/list-group.context.tsx +65 -0
  139. package/src/components/list-group/list-group.tsx +116 -0
  140. package/src/components/list-group/list-group.types.ts +14 -0
  141. package/src/components/list-group/list-group.variants.test.ts +223 -0
  142. package/src/components/list-group/list-group.variants.ts +107 -0
  143. package/src/components/pressable/AGENTS.md +74 -0
  144. package/src/components/pressable/index.ts +9 -0
  145. package/src/components/pressable/pressable.tsx +261 -0
  146. package/src/components/pressable/pressable.variants.test.ts +128 -0
  147. package/src/components/pressable/pressable.variants.ts +80 -0
  148. package/src/components/provider/AGENTS.md +90 -0
  149. package/src/components/provider/index.ts +1 -0
  150. package/src/components/provider/provider.tsx +105 -0
  151. package/src/components/radio/AGENTS.md +245 -0
  152. package/src/components/radio/index.ts +35 -0
  153. package/src/components/radio/radio-group.tsx +126 -0
  154. package/src/components/radio/radio-indicator.tsx +91 -0
  155. package/src/components/radio/radio-label.tsx +39 -0
  156. package/src/components/radio/radio.context.tsx +132 -0
  157. package/src/components/radio/radio.tsx +215 -0
  158. package/src/components/radio/radio.variants.test.ts +580 -0
  159. package/src/components/radio/radio.variants.ts +271 -0
  160. package/src/components/screen/AGENTS.md +287 -0
  161. package/src/components/screen/index.ts +85 -0
  162. package/src/components/screen/screen-chat-list.tsx +485 -0
  163. package/src/components/screen/screen-content.tsx +69 -0
  164. package/src/components/screen/screen-debug.ts +43 -0
  165. package/src/components/screen/screen-error.tsx +61 -0
  166. package/src/components/screen/screen-flat-list.tsx +88 -0
  167. package/src/components/screen/screen-footer-background.tsx +60 -0
  168. package/src/components/screen/screen-footer.tsx +170 -0
  169. package/src/components/screen/screen-header.tsx +36 -0
  170. package/src/components/screen/screen-legend-list.tsx +105 -0
  171. package/src/components/screen/screen-list-component.tsx +23 -0
  172. package/src/components/screen/screen-loading.tsx +50 -0
  173. package/src/components/screen/screen-navbar-back-button.tsx +79 -0
  174. package/src/components/screen/screen-navbar-background.tsx +45 -0
  175. package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
  176. package/src/components/screen/screen-navbar-title.tsx +31 -0
  177. package/src/components/screen/screen-navbar.tsx +154 -0
  178. package/src/components/screen/screen-root.tsx +38 -0
  179. package/src/components/screen/screen-scroll-area.tsx +116 -0
  180. package/src/components/screen/screen-scroll-shadow.tsx +186 -0
  181. package/src/components/screen/screen-section-list.tsx +81 -0
  182. package/src/components/screen/screen-view.tsx +59 -0
  183. package/src/components/screen/screen.context.tsx +167 -0
  184. package/src/components/screen/screen.tsx +114 -0
  185. package/src/components/screen/screen.types.ts +66 -0
  186. package/src/components/screen/screen.variants.test.ts +602 -0
  187. package/src/components/screen/screen.variants.ts +475 -0
  188. package/src/components/screen/use-screen-scroll-insets.ts +218 -0
  189. package/src/components/separator/AGENTS.md +30 -0
  190. package/src/components/separator/index.ts +8 -0
  191. package/src/components/separator/separator.tsx +85 -0
  192. package/src/components/slider/AGENTS.md +274 -0
  193. package/src/components/slider/index.ts +50 -0
  194. package/src/components/slider/slider-fill.tsx +68 -0
  195. package/src/components/slider/slider-output.tsx +55 -0
  196. package/src/components/slider/slider-thumb.tsx +193 -0
  197. package/src/components/slider/slider-track.tsx +233 -0
  198. package/src/components/slider/slider.context.tsx +118 -0
  199. package/src/components/slider/slider.tsx +321 -0
  200. package/src/components/slider/slider.types.ts +26 -0
  201. package/src/components/slider/slider.variants.test.ts +856 -0
  202. package/src/components/slider/slider.variants.ts +661 -0
  203. package/src/components/spinner/AGENTS.md +73 -0
  204. package/src/components/spinner/index.ts +17 -0
  205. package/src/components/spinner/spinner-arc.tsx +90 -0
  206. package/src/components/spinner/spinner-content.tsx +63 -0
  207. package/src/components/spinner/spinner.context.tsx +52 -0
  208. package/src/components/spinner/spinner.tsx +128 -0
  209. package/src/components/spinner/spinner.variants.test.ts +273 -0
  210. package/src/components/spinner/spinner.variants.ts +187 -0
  211. package/src/components/switch/AGENTS.md +213 -0
  212. package/src/components/switch/index.ts +42 -0
  213. package/src/components/switch/switch-content.tsx +111 -0
  214. package/src/components/switch/switch-end-content.tsx +18 -0
  215. package/src/components/switch/switch-start-content.tsx +20 -0
  216. package/src/components/switch/switch-thumb.tsx +102 -0
  217. package/src/components/switch/switch.context.tsx +79 -0
  218. package/src/components/switch/switch.tsx +423 -0
  219. package/src/components/switch/switch.types.ts +14 -0
  220. package/src/components/switch/switch.variants.test.ts +570 -0
  221. package/src/components/switch/switch.variants.ts +511 -0
  222. package/src/components/tabs/AGENTS.md +287 -0
  223. package/src/components/tabs/index.ts +69 -0
  224. package/src/components/tabs/tabs-content.tsx +65 -0
  225. package/src/components/tabs/tabs-indicator.tsx +97 -0
  226. package/src/components/tabs/tabs-label.tsx +62 -0
  227. package/src/components/tabs/tabs-list.tsx +139 -0
  228. package/src/components/tabs/tabs-pager.tsx +59 -0
  229. package/src/components/tabs/tabs-scroll-view.tsx +171 -0
  230. package/src/components/tabs/tabs-separator.tsx +73 -0
  231. package/src/components/tabs/tabs-trigger.tsx +210 -0
  232. package/src/components/tabs/tabs.context.tsx +294 -0
  233. package/src/components/tabs/tabs.tsx +435 -0
  234. package/src/components/tabs/tabs.types.ts +13 -0
  235. package/src/components/tabs/tabs.variants.test.ts +1020 -0
  236. package/src/components/tabs/tabs.variants.ts +670 -0
  237. package/src/components/text/AGENTS.md +95 -0
  238. package/src/components/text/index.ts +25 -0
  239. package/src/components/text/text.context.tsx +60 -0
  240. package/src/components/text/text.tsx +251 -0
  241. package/src/components/text/text.variants.test.ts +422 -0
  242. package/src/components/text/text.variants.ts +282 -0
  243. package/src/display-name.test.ts +145 -0
  244. package/src/docs.test.ts +98 -0
  245. package/src/expo/navigation-theme.tsx +58 -0
  246. package/src/hooks/use-controllable-state.ts +45 -0
  247. package/src/hooks/use-keyboard-state-sync.tsx +147 -0
  248. package/src/hooks/use-navigation-theme.ts +78 -0
  249. package/src/hooks/use-theme-color.ts +44 -0
  250. package/src/icons/central.ts +1 -0
  251. package/src/lib/cn.test.ts +136 -0
  252. package/src/lib/cn.ts +29 -0
  253. package/src/lib/color.test.ts +80 -0
  254. package/src/lib/color.ts +79 -0
  255. package/src/lib/compose-refs.test.ts +64 -0
  256. package/src/lib/compose-refs.ts +37 -0
  257. package/src/lib/keyboard-animation.test.ts +33 -0
  258. package/src/lib/keyboard-animation.ts +28 -0
  259. package/src/lib/merge-props.test.ts +82 -0
  260. package/src/lib/merge-props.ts +47 -0
  261. package/src/lib/navigation-theme.test.ts +49 -0
  262. package/src/lib/navigation-theme.ts +53 -0
  263. package/src/lib/slot.tsx +45 -0
  264. package/src/lib/tv.ts +20 -0
  265. package/src/styles/base.css +2 -0
  266. package/src/styles/geometry.test.ts +117 -0
  267. package/src/styles/index.css +3 -0
  268. package/src/styles/theme-tokens.test.ts +252 -0
  269. package/src/styles/theme.css +447 -0
  270. package/src/styles/tokens.css +132 -0
  271. package/src/styles/tokens.test.ts +181 -0
  272. package/src/styles/tokens.ts +86 -0
  273. package/src/uniwind-env.d.ts +1 -0
@@ -0,0 +1,180 @@
1
+ import {
2
+ BottomSheetBackdrop,
3
+ type BottomSheetBackdropProps,
4
+ useBottomSheet as useGorhomBottomSheet,
5
+ } from "@gorhom/bottom-sheet";
6
+ import type { ReactElement, ReactNode } from "react";
7
+ import { StyleSheet } from "react-native";
8
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
9
+ import { withUniwind } from "uniwind";
10
+ import { Pressable } from "../pressable";
11
+ import {
12
+ BOTTOM_SHEET_BACKDROP_INDICES,
13
+ BOTTOM_SHEET_OVERLAY_OPACITY,
14
+ bottomSheetVariants,
15
+ } from "./bottom-sheet.variants";
16
+
17
+ /**
18
+ * gorhom's backdrop is third-party and takes a `style`, never a `className`, and
19
+ * it is a leaf — there is no inner view to hang classes on the way
20
+ * `BottomSheet.Content` and `BottomSheet.Footer` do. So it is wrapped, once, at
21
+ * module scope. In render `withUniwind` would mint a new component type every
22
+ * frame and remount the scrim mid-fade. See AGENTS.md rule 7.
23
+ */
24
+ const StyledBackdrop = withUniwind(BottomSheetBackdrop);
25
+
26
+ export type BottomSheetOverlayProps = Partial<BottomSheetBackdropProps> & {
27
+ className?: string;
28
+ /**
29
+ * Opacity at full appearance.
30
+ *
31
+ * Defaults to 1 rather than gorhom's 0.5, because `--overlay` carries
32
+ * its own alpha — the two would otherwise multiply and land the scrim at a
33
+ * fifth of what the theme asked for.
34
+ */
35
+ opacity?: number;
36
+ /** Snap index the scrim is fully in by. @default 0, the first snap point */
37
+ appearsOnIndex?: number;
38
+ /** Snap index the scrim is gone by. @default -1, the closed position */
39
+ disappearsOnIndex?: number;
40
+ /** Let touches reach the app behind the scrim. @default false */
41
+ enableTouchThrough?: boolean;
42
+ /** Close the sheet when the scrim is pressed. @default true */
43
+ isCloseOnPress?: boolean;
44
+ /** Announced on the pressable scrim. Ignored when it does not close. */
45
+ accessibilityLabel?: string;
46
+ onPress?: () => void;
47
+ children?: ReactNode;
48
+ };
49
+
50
+ /**
51
+ * The scrim over the app behind the sheet.
52
+ *
53
+ * **It is not rendered where it is written.** gorhom takes the backdrop as a
54
+ * `backdropComponent` render prop rather than as a child, so
55
+ * `BottomSheet.Portal` lifts this element out of its own children, publishes it
56
+ * on context, and `BottomSheet.Container` clones it into that slot with the
57
+ * sheet's animated position attached. Writing it as a sibling of the container is
58
+ * what makes the anatomy read like the thing it draws.
59
+ *
60
+ * Omit it and the sheet has no scrim at all — gorhom's own default for
61
+ * `backdropComponent` is nothing.
62
+ *
63
+ * **gorhom's own press handling is deliberately turned off, and the press area
64
+ * is rebuilt here.** Its backdrop puts a full-screen `Gesture.Tap()` behind the
65
+ * sheet, and Gesture Handler resolves a touch between competing GESTURES: a
66
+ * `Button` in the sheet has one of its own and wins, but a `TextInput` has none,
67
+ * so the backdrop takes the tap and the sheet closes instead of the field
68
+ * focusing. That is why gorhom ships its own `BottomSheetTextInput` — and taking
69
+ * that route would mean this package's `Input`, with its variants, its
70
+ * `Input.Group` and its `Field` cascade, could not be used in a sheet at all.
71
+ *
72
+ * So the scrim keeps `pressBehavior="none"` — no gesture anywhere — and a
73
+ * `Pressable` is laid over the region ABOVE the sheet, sized from the sheet's own
74
+ * `animatedPosition`. It never overlaps the sheet, so there is nothing for it to
75
+ * take, and pressing the part of the scrim anyone can actually see still closes.
76
+ * The colour and the fade stay gorhom's, driven off the sheet's position on the
77
+ * UI thread; the slot therefore carries no `opacity-*`, or a class and an
78
+ * animated style would be two writers of one property.
79
+ *
80
+ * @example
81
+ * <BottomSheet.Overlay />
82
+ *
83
+ * @example
84
+ * // A scrim that does not close the sheet — a decision the caller has to make.
85
+ * <BottomSheet.Overlay isCloseOnPress={false} />
86
+ */
87
+ export function BottomSheetOverlay({
88
+ accessibilityLabel = "Close",
89
+ animatedIndex,
90
+ animatedPosition,
91
+ appearsOnIndex = BOTTOM_SHEET_BACKDROP_INDICES.appearsOnIndex,
92
+ className,
93
+ disappearsOnIndex = BOTTOM_SHEET_BACKDROP_INDICES.disappearsOnIndex,
94
+ isCloseOnPress = true,
95
+ opacity = BOTTOM_SHEET_OVERLAY_OPACITY,
96
+ onPress,
97
+ ...props
98
+ }: BottomSheetOverlayProps): ReactElement {
99
+ if (animatedIndex === undefined || animatedPosition === undefined) {
100
+ throw new Error(
101
+ "BottomSheet.Overlay must be written inside a <BottomSheet.Portal>, beside its <BottomSheet.Container>."
102
+ );
103
+ }
104
+
105
+ return (
106
+ <>
107
+ <StyledBackdrop
108
+ animatedIndex={animatedIndex}
109
+ animatedPosition={animatedPosition}
110
+ appearsOnIndex={appearsOnIndex}
111
+ className={bottomSheetVariants().overlay({ className })}
112
+ disappearsOnIndex={disappearsOnIndex}
113
+ opacity={opacity}
114
+ pressBehavior="none"
115
+ {...props}
116
+ />
117
+ {isCloseOnPress ? (
118
+ <BottomSheetOverlayPressArea
119
+ accessibilityLabel={accessibilityLabel}
120
+ animatedPosition={animatedPosition}
121
+ onPress={onPress}
122
+ />
123
+ ) : null}
124
+ </>
125
+ );
126
+ }
127
+ BottomSheetOverlay.displayName = "DelacourUI.BottomSheet.Overlay";
128
+
129
+ /**
130
+ * The pressable band between the top of the screen and the top of the sheet.
131
+ *
132
+ * Its height is the sheet's own `animatedPosition`, so it grows and shrinks with
133
+ * the sheet on the UI thread and stops exactly where the sheet begins — which is
134
+ * the whole point. The `Pressable` is a child rather than the animated view
135
+ * itself because `Pressable` takes no `style`: its own `Animated.View` already
136
+ * owns one, and a second would fight it for prop ownership every frame.
137
+ *
138
+ * It reads gorhom's `useBottomSheet()` rather than this package's. The backdrop
139
+ * is rendered by gorhom's own container, OUTSIDE the children
140
+ * `BottomSheet.Container` re-publishes its context around, so the sheet's own
141
+ * handle is the one thing in scope here — and closing through it lands on
142
+ * `onDismiss`, which is where `onOpenChange` already is.
143
+ */
144
+ function BottomSheetOverlayPressArea({
145
+ accessibilityLabel,
146
+ animatedPosition,
147
+ onPress,
148
+ }: {
149
+ accessibilityLabel: string;
150
+ animatedPosition: NonNullable<BottomSheetBackdropProps["animatedPosition"]>;
151
+ onPress?: () => void;
152
+ }): ReactElement {
153
+ const { close } = useGorhomBottomSheet();
154
+
155
+ const style = useAnimatedStyle(() => ({ height: Math.max(0, animatedPosition.value) }));
156
+
157
+ const handlePress = () => {
158
+ onPress?.();
159
+ close();
160
+ };
161
+
162
+ return (
163
+ <Animated.View style={[styles.pressArea, style]}>
164
+ <Pressable
165
+ accessibilityLabel={accessibilityLabel}
166
+ accessibilityRole="button"
167
+ className="flex-1"
168
+ feedback="none"
169
+ onPress={handlePress}
170
+ />
171
+ </Animated.View>
172
+ );
173
+ }
174
+ BottomSheetOverlayPressArea.displayName = "DelacourUI.BottomSheet.Overlay.PressArea";
175
+
176
+ // A style rather than a class: the height beside it is an animated value, and the
177
+ // two have to arrive on the same node.
178
+ const styles = StyleSheet.create({
179
+ pressArea: { left: 0, position: "absolute", right: 0, top: 0 },
180
+ });
@@ -0,0 +1,72 @@
1
+ import { Children, isValidElement, type ReactElement, type ReactNode, useMemo } from "react";
2
+ import { BottomSheetPortalProvider } from "./bottom-sheet.context";
3
+ import { BottomSheetOverlay } from "./bottom-sheet-overlay";
4
+
5
+ export type BottomSheetPortalProps = {
6
+ children?: ReactNode;
7
+ };
8
+
9
+ /**
10
+ * Everything that draws above the app once the sheet is open.
11
+ *
12
+ * It renders no view of its own, and it does not portal anything either —
13
+ * `BottomSheetModal` already hosts itself in the `BottomSheetModalProvider` that
14
+ * `DelacourProvider` mounts innermost. What this part does is one thing:
15
+ * **it lifts the `BottomSheet.Overlay` out of its own children** and publishes it
16
+ * on context, so `BottomSheet.Container` can clone it into gorhom's
17
+ * `backdropComponent` slot.
18
+ *
19
+ * That indirection buys the anatomy. The scrim and the sheet are siblings on
20
+ * screen, so they read best as siblings in the tree — but gorhom takes the scrim
21
+ * as a render prop, and a component cannot hand a prop to a sibling. This is the
22
+ * seam where the two shapes are reconciled, and it is why the part exists at all
23
+ * rather than the container simply taking a `backdrop` prop.
24
+ *
25
+ * @example
26
+ * <BottomSheet.Portal>
27
+ * <BottomSheet.Overlay />
28
+ * <BottomSheet.Container>{…}</BottomSheet.Container>
29
+ * </BottomSheet.Portal>
30
+ */
31
+ export function BottomSheetPortal({ children }: BottomSheetPortalProps): ReactElement {
32
+ const { content, overlay } = useMemo(() => hoistOverlay(children), [children]);
33
+ const value = useMemo(() => ({ overlay }), [overlay]);
34
+
35
+ return <BottomSheetPortalProvider value={value}>{content}</BottomSheetPortalProvider>;
36
+ }
37
+ BottomSheetPortal.displayName = "DelacourUI.BottomSheet.Portal";
38
+
39
+ /**
40
+ * Splits a portal's children into the scrim and everything else.
41
+ *
42
+ * Lives here rather than with the parts: it is the portal that lifts its own
43
+ * children, and importing it from `bottom-sheet-overlay.tsx` would close a cycle.
44
+ * The same rule that keeps `withIndicator` in `radio.tsx`. See AGENTS.md.
45
+ *
46
+ * A second overlay is dropped rather than rendered — there is one
47
+ * `backdropComponent` slot to clone into, and silently drawing two scrims at
48
+ * different opacities is worse than the warning.
49
+ */
50
+ function hoistOverlay(children: ReactNode): { content: ReactNode; overlay: ReactElement | null } {
51
+ const items = Children.toArray(children);
52
+ const content: ReactNode[] = [];
53
+ let overlay: ReactElement | null = null;
54
+ let seen = 0;
55
+
56
+ for (const child of items) {
57
+ if (isValidElement(child) && child.type === BottomSheetOverlay) {
58
+ seen += 1;
59
+ overlay ??= child;
60
+ continue;
61
+ }
62
+ content.push(child);
63
+ }
64
+
65
+ if (process.env.NODE_ENV !== "production" && seen > 1) {
66
+ console.warn(
67
+ `BottomSheet.Portal: ${seen} <BottomSheet.Overlay> elements were written; only the first is drawn. A sheet has one backdrop.`
68
+ );
69
+ }
70
+
71
+ return { content, overlay };
72
+ }
@@ -0,0 +1,105 @@
1
+ import { BottomSheetScrollView as GorhomBottomSheetScrollView } from "@gorhom/bottom-sheet";
2
+ import type { ComponentProps, ReactElement, ReactNode } from "react";
3
+ import { View } from "react-native";
4
+ import { useAnimatedStyle } from "react-native-reanimated";
5
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
6
+ import { withUniwind } from "uniwind";
7
+ import { cn } from "../../lib/cn";
8
+ import { useBottomSheetContainerContext } from "./bottom-sheet.context";
9
+ import { bottomSheetVariants, resolveSheetScrollEndPadding } from "./bottom-sheet.variants";
10
+
11
+ type GorhomScrollViewProps = ComponentProps<typeof GorhomBottomSheetScrollView>;
12
+
13
+ /**
14
+ * The one class prop this file uses, restated so the props survive the wrapper.
15
+ *
16
+ * `withUniwind`'s return type maps over a component's props, and over a
17
+ * scrollable's animated ones it collapses to a single entry — `data`,
18
+ * `contentContainerStyle` and the rest would all check against nothing. Writing
19
+ * the signature out keeps them, the way `Screen.LegendList` already has to.
20
+ *
21
+ * `contentContainerClassName` is deliberately absent even though the wrapper adds
22
+ * one at runtime: passing it would put a second writer on `contentContainerStyle`
23
+ * and cost the safe-area band — see the component's doc comment.
24
+ */
25
+ type StyledScrollViewComponent = (props: GorhomScrollViewProps & { className?: string }) => ReactElement | null;
26
+
27
+ // Third-party, so `className` needs the wrapper — and it has to be built at
28
+ // module scope or every render mints a new component type and remounts the list.
29
+ const StyledScrollView = withUniwind(GorhomBottomSheetScrollView) as unknown as StyledScrollViewComponent;
30
+
31
+ export type BottomSheetScrollViewProps = Omit<
32
+ GorhomScrollViewProps,
33
+ "children" | "contentContainerStyle" | "enableFooterMarginAdjustment"
34
+ > & {
35
+ className?: string;
36
+ /** Classes for the padded box the children sit in. */
37
+ contentContainerClassName?: string;
38
+ children?: ReactNode;
39
+ };
40
+
41
+ /**
42
+ * A scrolling body for a sheet taller than its snap point.
43
+ *
44
+ * Use this rather than a plain `ScrollView`: gorhom's scrollable and the sheet's
45
+ * pan negotiate with each other, so dragging a list that is already at its top
46
+ * moves the sheet instead of fighting it. A React Native `ScrollView` in here has
47
+ * no such arrangement, and the sheet stops responding to a drag over the list.
48
+ *
49
+ * **It needs a height to scroll within.** With `enableDynamicSizing` left on, the
50
+ * sheet grows to whatever the content measures and there is nothing to scroll —
51
+ * pass `enableDynamicSizing={false}` and explicit `snapPoints` on the
52
+ * `BottomSheet.Container` around it.
53
+ *
54
+ * The classes go on an inner `View`, not on gorhom's own content container, and
55
+ * that is not tidiness: uniwind compiles a `contentContainerClassName` into an
56
+ * *array* alongside any `contentContainerStyle`, and the two then fight over the
57
+ * one style this component has to own — the safe-area band. One writer for it.
58
+ *
59
+ * **A pinned footer shortens the scroll view; it is not reserved inside it.**
60
+ * The footer draws over the sheet, so a scroll view that ran the sheet's full
61
+ * height would carry its indicator and its overscroll bounce on underneath the
62
+ * footer, the bottom of the track hidden behind the buttons. So the frame gives
63
+ * up the footer's measured height as a `marginBottom` and ends at the footer's
64
+ * hairline, and the content keeps only the gap
65
+ * ({@link resolveSheetScrollEndPadding}). The margin is an animated style off
66
+ * the footer's shared value, never gorhom's `enableFooterMarginAdjustment`,
67
+ * which routes that height through React state and would commit a render on
68
+ * every frame of the keyboard animation — see `BottomSheet.Content`.
69
+ *
70
+ * @example
71
+ * <BottomSheet.Container enableDynamicSizing={false} snapPoints={["60%", "90%"]}>
72
+ * <BottomSheet.ScrollView>{rows}</BottomSheet.ScrollView>
73
+ * </BottomSheet.Container>
74
+ */
75
+ export function BottomSheetScrollView({
76
+ className,
77
+ contentContainerClassName,
78
+ children,
79
+ style,
80
+ ...props
81
+ }: BottomSheetScrollViewProps): ReactElement {
82
+ const container = useBottomSheetContainerContext();
83
+ const { bottom } = useSafeAreaInsets();
84
+ const hasStickyFooter = container?.hasStickyFooter ?? false;
85
+ const footerHeight = container?.footerHeight;
86
+
87
+ // The footer's height, band included, as the frame's own margin — so the
88
+ // indicator's track ends where the footer begins and follows it as the
89
+ // keyboard takes the band back.
90
+ const aboveFooter = useAnimatedStyle(() => ({
91
+ marginBottom: hasStickyFooter && footerHeight !== undefined ? footerHeight.value : 0,
92
+ }));
93
+
94
+ return (
95
+ <StyledScrollView
96
+ className={cn(className)}
97
+ contentContainerStyle={{ paddingBottom: resolveSheetScrollEndPadding({ bottom, hasStickyFooter }) }}
98
+ {...props}
99
+ style={[aboveFooter, style]}
100
+ >
101
+ <View className={bottomSheetVariants().scrollContent({ className: contentContainerClassName })}>{children}</View>
102
+ </StyledScrollView>
103
+ );
104
+ }
105
+ BottomSheetScrollView.displayName = "DelacourUI.BottomSheet.ScrollView";
@@ -0,0 +1,24 @@
1
+ import type { ReactElement } from "react";
2
+ import { Text, type TextPresetProps } from "../text";
3
+ import { bottomSheetVariants } from "./bottom-sheet.variants";
4
+
5
+ export type BottomSheetTitleProps = TextPresetProps;
6
+
7
+ /**
8
+ * The sheet's heading.
9
+ *
10
+ * *Is* a `Text.Header` — it renders the preset and adds layout, never a size or a
11
+ * weight of its own, the same trade `Field.Label` makes with `Text.Label`. A
12
+ * `text-lg font-semibold` here would be a second definition of that preset which
13
+ * could drift from it, and the tests assert the slot carries neither.
14
+ *
15
+ * The preset already announces itself as a heading, so a screen reader reaches
16
+ * the sheet's purpose first with nothing said at the call site.
17
+ *
18
+ * @example
19
+ * <BottomSheet.Title>Keep yourself safe</BottomSheet.Title>
20
+ */
21
+ export function BottomSheetTitle({ className, ...props }: BottomSheetTitleProps): ReactElement {
22
+ return <Text.Header className={bottomSheetVariants().title({ className })} {...props} />;
23
+ }
24
+ BottomSheetTitle.displayName = "DelacourUI.BottomSheet.Title";
@@ -0,0 +1,73 @@
1
+ import { type ReactElement, useCallback } from "react";
2
+ import { Slot } from "../../lib/slot";
3
+ import { Pressable, type PressableProps } from "../pressable";
4
+ import { useBottomSheetPart } from "./bottom-sheet.context";
5
+
6
+ export type BottomSheetTriggerProps = PressableProps;
7
+
8
+ /**
9
+ * The control that opens the sheet.
10
+ *
11
+ * On its own it is a `Pressable`, so `feedback`, `haptic` and the rest are
12
+ * inherited rather than restated. It opens through the same state a controlled
13
+ * `isOpen` drives, so a sheet can be opened from here and from elsewhere on the
14
+ * same screen without the two disagreeing, and a caller's own `onPress` still
15
+ * runs after the open.
16
+ *
17
+ * **`asChild` donates the press rather than wrapping the child**, which is where
18
+ * it differs from `Pressable`'s own. `Pressable asChild` keeps its gesture and
19
+ * renders the child inside it — correct for a child that is not itself a
20
+ * control, and wrong here, because the thing anyone wraps in a trigger is a
21
+ * `Button`. Two tap gestures in an ancestor/descendant pair are not
22
+ * simultaneous, so Gesture Handler gives the press to the DESCENDANT: the
23
+ * button's own detector wins, fires the `onPress` it does not have, and the
24
+ * sheet never opens. Nothing announces this — the press simply does nothing.
25
+ *
26
+ * So the trigger hands its `onPress` down as a prop instead, through `Slot`,
27
+ * which chains it ahead of any the child already had. The child keeps its own
28
+ * gesture, its own feedback and its own haptic, and there is still no extra view
29
+ * in the tree.
30
+ *
31
+ * The corollary: **the child has to be something that handles `onPress`.** A
32
+ * `View` or a `Text` takes the prop and ignores it. Wrap a `Button`, a
33
+ * `ListGroup.Item`, or anything else built on `Pressable`.
34
+ *
35
+ * @example
36
+ * <BottomSheet.Trigger asChild>
37
+ * <Button variant="secondary">Open</Button>
38
+ * </BottomSheet.Trigger>
39
+ *
40
+ * @example
41
+ * // No asChild: the trigger is its own pressable.
42
+ * <BottomSheet.Trigger className="p-4">
43
+ * <Text>Open</Text>
44
+ * </BottomSheet.Trigger>
45
+ */
46
+ export function BottomSheetTrigger({
47
+ asChild = false,
48
+ children,
49
+ onPress,
50
+ ...props
51
+ }: BottomSheetTriggerProps): ReactElement {
52
+ const { open } = useBottomSheetPart("BottomSheet.Trigger");
53
+
54
+ const handlePress = useCallback(() => {
55
+ open();
56
+ onPress?.();
57
+ }, [onPress, open]);
58
+
59
+ if (asChild) {
60
+ return (
61
+ <Slot onPress={handlePress} {...props}>
62
+ {children}
63
+ </Slot>
64
+ );
65
+ }
66
+
67
+ return (
68
+ <Pressable onPress={handlePress} {...props}>
69
+ {children}
70
+ </Pressable>
71
+ );
72
+ }
73
+ BottomSheetTrigger.displayName = "DelacourUI.BottomSheet.Trigger";
@@ -0,0 +1,135 @@
1
+ import type { BottomSheetModal } from "@gorhom/bottom-sheet";
2
+ import { type ComponentRef, createContext, type ReactElement, type ReactNode, type RefObject, use } from "react";
3
+ import type { SharedValue } from "react-native-reanimated";
4
+
5
+ /**
6
+ * The imperative handle a `BottomSheetModal` exposes.
7
+ *
8
+ * Derived from the component rather than imported by name: gorhom re-exports the
9
+ * modal's *value* from its package root but not the `BottomSheetModalMethods`
10
+ * type behind its ref, and reaching into `lib/typescript/` for one would pin this
11
+ * package to a path that is not part of that package's public surface.
12
+ */
13
+ export type BottomSheetHandle = ComponentRef<typeof BottomSheetModal>;
14
+
15
+ export type BottomSheetContextValue = {
16
+ isOpen: boolean;
17
+ /** The controllable-state setter. Reports upward whether controlled or not. */
18
+ setOpen: (isOpen: boolean) => void;
19
+ open: () => void;
20
+ close: () => void;
21
+ /**
22
+ * The modal's handle, for the imperative calls that have no declarative form —
23
+ * `snapToIndex`, `expand`, `collapse`. Presenting and dismissing are driven by
24
+ * `isOpen` instead, so a caller never has to keep two sources in step.
25
+ */
26
+ sheetRef: RefObject<BottomSheetHandle | null>;
27
+ };
28
+
29
+ const BottomSheetContext = createContext<BottomSheetContextValue | null>(null);
30
+
31
+ export function BottomSheetProvider({
32
+ value,
33
+ children,
34
+ }: {
35
+ value: BottomSheetContextValue;
36
+ children: ReactNode;
37
+ }): ReactElement {
38
+ return <BottomSheetContext value={value}>{children}</BottomSheetContext>;
39
+ }
40
+ BottomSheetProvider.displayName = "DelacourUI.BottomSheet.Provider";
41
+
42
+ /** The sheet's state, or `null` outside one. For a component that works either way. */
43
+ export function useBottomSheetContext(): BottomSheetContextValue | null {
44
+ return use(BottomSheetContext);
45
+ }
46
+
47
+ /**
48
+ * The enclosing sheet's open state and controls.
49
+ *
50
+ * Reach for this to drive a sheet from a control of your own — a row that closes
51
+ * it, a button that snaps it somewhere — without threading props down.
52
+ */
53
+ export function useBottomSheet(): BottomSheetContextValue {
54
+ const context = use(BottomSheetContext);
55
+ if (context === null) throw new Error("useBottomSheet must be called inside a <BottomSheet>.");
56
+ return context;
57
+ }
58
+
59
+ /** The same read, with a message naming the part that is misplaced. Internal. */
60
+ export function useBottomSheetPart(component: string): BottomSheetContextValue {
61
+ const context = use(BottomSheetContext);
62
+ if (context === null) throw new Error(`${component} must be rendered inside a <BottomSheet>.`);
63
+ return context;
64
+ }
65
+
66
+ export type BottomSheetPortalContextValue = {
67
+ /**
68
+ * The `BottomSheet.Overlay` element the portal lifted out of its own children,
69
+ * or `null` when none was written.
70
+ *
71
+ * gorhom takes the scrim as a `backdropComponent` render prop rather than as a
72
+ * child, so the element cannot be rendered where it was declared. The portal
73
+ * publishes it here and `BottomSheet.Container` clones it into that slot.
74
+ */
75
+ overlay: ReactElement | null;
76
+ };
77
+
78
+ const BottomSheetPortalContext = createContext<BottomSheetPortalContextValue | null>(null);
79
+
80
+ export function BottomSheetPortalProvider({
81
+ value,
82
+ children,
83
+ }: {
84
+ value: BottomSheetPortalContextValue;
85
+ children: ReactNode;
86
+ }): ReactElement {
87
+ return <BottomSheetPortalContext value={value}>{children}</BottomSheetPortalContext>;
88
+ }
89
+ BottomSheetPortalProvider.displayName = "DelacourUI.BottomSheet.Portal.Provider";
90
+
91
+ /** What the enclosing portal lifted out, or `null` for a container written without one. */
92
+ export function useBottomSheetPortalContext(): BottomSheetPortalContextValue | null {
93
+ return use(BottomSheetPortalContext);
94
+ }
95
+
96
+ export type BottomSheetContainerContextValue = {
97
+ /**
98
+ * Whether a pinned footer is drawing at the bottom of this sheet.
99
+ *
100
+ * `BottomSheet.Content` reads it to decide whether the safe-area band is its
101
+ * to apply. Exactly one part takes it: both applying leaves a gap the height
102
+ * of the home indicator between the content and the footer.
103
+ */
104
+ hasStickyFooter: boolean;
105
+ /**
106
+ * What a pinned footer covers, measured, in points.
107
+ *
108
+ * A shared value rather than state: it changes on every frame of the keyboard
109
+ * animation as the footer gives up its safe-area band, and the body reserves it
110
+ * with an animated spacer. Routing that through React would commit a dozen
111
+ * renders per keystroke's worth of keyboard travel. `Screen` publishes its own
112
+ * chrome the same way.
113
+ *
114
+ * Zero when there is no pinned footer.
115
+ */
116
+ footerHeight: SharedValue<number>;
117
+ };
118
+
119
+ const BottomSheetContainerContext = createContext<BottomSheetContainerContextValue | null>(null);
120
+
121
+ export function BottomSheetContainerProvider({
122
+ value,
123
+ children,
124
+ }: {
125
+ value: BottomSheetContainerContextValue;
126
+ children: ReactNode;
127
+ }): ReactElement {
128
+ return <BottomSheetContainerContext value={value}>{children}</BottomSheetContainerContext>;
129
+ }
130
+ BottomSheetContainerProvider.displayName = "DelacourUI.BottomSheet.Container.Provider";
131
+
132
+ /** The enclosing container's layout facts, or `null` outside one. */
133
+ export function useBottomSheetContainerContext(): BottomSheetContainerContextValue | null {
134
+ return use(BottomSheetContainerContext);
135
+ }