@oxyhq/bloom 0.15.0 → 0.16.1

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 (208) hide show
  1. package/lib/commonjs/alert-dialog/AlertDialog.js +35 -27
  2. package/lib/commonjs/alert-dialog/AlertDialog.js.map +1 -1
  3. package/lib/commonjs/alert-dialog/index.js +1 -1
  4. package/lib/commonjs/alert-dialog/index.js.map +1 -1
  5. package/lib/commonjs/alert-dialog/index.web.js +1 -1
  6. package/lib/commonjs/alert-dialog/index.web.js.map +1 -1
  7. package/lib/commonjs/command/Command.js +27 -16
  8. package/lib/commonjs/command/Command.js.map +1 -1
  9. package/lib/commonjs/command/index.js +1 -1
  10. package/lib/commonjs/command/index.js.map +1 -1
  11. package/lib/commonjs/command/index.web.js +1 -1
  12. package/lib/commonjs/command/index.web.js.map +1 -1
  13. package/lib/commonjs/dialog/Dialog.js +39 -152
  14. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  15. package/lib/commonjs/dialog/Dialog.web.js +61 -196
  16. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  17. package/lib/commonjs/dialog/DialogBottomSheet.js +205 -0
  18. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -0
  19. package/lib/commonjs/dialog/DialogContent.js +155 -0
  20. package/lib/commonjs/dialog/DialogContent.js.map +1 -0
  21. package/lib/commonjs/dialog/index.js +0 -13
  22. package/lib/commonjs/dialog/index.js.map +1 -1
  23. package/lib/commonjs/dialog/index.web.js +0 -19
  24. package/lib/commonjs/dialog/index.web.js.map +1 -1
  25. package/lib/commonjs/index.js +0 -29
  26. package/lib/commonjs/index.js.map +1 -1
  27. package/lib/commonjs/index.web.js +6 -35
  28. package/lib/commonjs/index.web.js.map +1 -1
  29. package/lib/module/alert-dialog/AlertDialog.js +35 -27
  30. package/lib/module/alert-dialog/AlertDialog.js.map +1 -1
  31. package/lib/module/alert-dialog/index.js +2 -2
  32. package/lib/module/alert-dialog/index.js.map +1 -1
  33. package/lib/module/alert-dialog/index.web.js +2 -2
  34. package/lib/module/alert-dialog/index.web.js.map +1 -1
  35. package/lib/module/command/Command.js +27 -16
  36. package/lib/module/command/Command.js.map +1 -1
  37. package/lib/module/command/index.js +2 -2
  38. package/lib/module/command/index.js.map +1 -1
  39. package/lib/module/command/index.web.js +2 -2
  40. package/lib/module/command/index.web.js.map +1 -1
  41. package/lib/module/dialog/Dialog.js +42 -155
  42. package/lib/module/dialog/Dialog.js.map +1 -1
  43. package/lib/module/dialog/Dialog.web.js +63 -198
  44. package/lib/module/dialog/Dialog.web.js.map +1 -1
  45. package/lib/module/dialog/DialogBottomSheet.js +200 -0
  46. package/lib/module/dialog/DialogBottomSheet.js.map +1 -0
  47. package/lib/module/dialog/DialogContent.js +148 -0
  48. package/lib/module/dialog/DialogContent.js.map +1 -0
  49. package/lib/module/dialog/index.js +0 -1
  50. package/lib/module/dialog/index.js.map +1 -1
  51. package/lib/module/dialog/index.web.js +0 -1
  52. package/lib/module/dialog/index.web.js.map +1 -1
  53. package/lib/module/index.js +1 -3
  54. package/lib/module/index.js.map +1 -1
  55. package/lib/module/index.web.js +1 -3
  56. package/lib/module/index.web.js.map +1 -1
  57. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts +13 -12
  58. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/alert-dialog/index.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/alert-dialog/index.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/command/Command.d.ts +8 -8
  62. package/lib/typescript/commonjs/command/Command.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/command/index.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/command/index.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/dialog/Dialog.d.ts +2 -3
  66. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +11 -8
  68. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +29 -0
  70. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/dialog/DialogContent.d.ts +32 -0
  72. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -0
  73. package/lib/typescript/commonjs/dialog/index.d.ts +0 -2
  74. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/dialog/index.web.d.ts +0 -2
  76. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/index.d.ts +2 -4
  78. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/index.web.d.ts +2 -4
  80. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  81. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts +13 -12
  82. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts.map +1 -1
  83. package/lib/typescript/module/alert-dialog/index.d.ts.map +1 -1
  84. package/lib/typescript/module/alert-dialog/index.web.d.ts.map +1 -1
  85. package/lib/typescript/module/command/Command.d.ts +8 -8
  86. package/lib/typescript/module/command/Command.d.ts.map +1 -1
  87. package/lib/typescript/module/command/index.d.ts.map +1 -1
  88. package/lib/typescript/module/command/index.web.d.ts.map +1 -1
  89. package/lib/typescript/module/dialog/Dialog.d.ts +2 -3
  90. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  91. package/lib/typescript/module/dialog/Dialog.web.d.ts +11 -8
  92. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  93. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +29 -0
  94. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -0
  95. package/lib/typescript/module/dialog/DialogContent.d.ts +32 -0
  96. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -0
  97. package/lib/typescript/module/dialog/index.d.ts +0 -2
  98. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  99. package/lib/typescript/module/dialog/index.web.d.ts +0 -2
  100. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  101. package/lib/typescript/module/index.d.ts +2 -4
  102. package/lib/typescript/module/index.d.ts.map +1 -1
  103. package/lib/typescript/module/index.web.d.ts +2 -4
  104. package/lib/typescript/module/index.web.d.ts.map +1 -1
  105. package/package.json +1 -18
  106. package/src/__tests__/AlertDialog.test.tsx +122 -0
  107. package/src/__tests__/BottomSheetGesture.test.tsx +135 -0
  108. package/src/__tests__/Command.test.tsx +107 -0
  109. package/src/__tests__/DialogBottomSheet.test.tsx +130 -0
  110. package/src/alert-dialog/AlertDialog.tsx +46 -36
  111. package/src/alert-dialog/index.ts +2 -2
  112. package/src/alert-dialog/index.web.ts +2 -2
  113. package/src/command/Command.tsx +30 -19
  114. package/src/command/index.ts +2 -2
  115. package/src/command/index.web.ts +2 -2
  116. package/src/dialog/Dialog.tsx +35 -168
  117. package/src/dialog/Dialog.web.tsx +52 -209
  118. package/src/dialog/DialogBottomSheet.tsx +235 -0
  119. package/src/dialog/DialogContent.tsx +157 -0
  120. package/src/dialog/index.ts +0 -2
  121. package/src/dialog/index.web.ts +0 -6
  122. package/src/index.ts +0 -11
  123. package/src/index.web.ts +0 -11
  124. package/lib/commonjs/dialog/CenteredDialog.js +0 -252
  125. package/lib/commonjs/dialog/CenteredDialog.js.map +0 -1
  126. package/lib/commonjs/dialog/CenteredDialog.web.js +0 -330
  127. package/lib/commonjs/dialog/CenteredDialog.web.js.map +0 -1
  128. package/lib/commonjs/dialog/centered-dialog-tokens.js +0 -62
  129. package/lib/commonjs/dialog/centered-dialog-tokens.js.map +0 -1
  130. package/lib/commonjs/dialog/centered-dialog-types.js +0 -6
  131. package/lib/commonjs/dialog/centered-dialog-types.js.map +0 -1
  132. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +0 -97
  133. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +0 -1
  134. package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js +0 -84
  135. package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js.map +0 -1
  136. package/lib/commonjs/responsive-sheet/index.js +0 -19
  137. package/lib/commonjs/responsive-sheet/index.js.map +0 -1
  138. package/lib/commonjs/responsive-sheet/index.web.js +0 -19
  139. package/lib/commonjs/responsive-sheet/index.web.js.map +0 -1
  140. package/lib/commonjs/responsive-sheet/types.js +0 -6
  141. package/lib/commonjs/responsive-sheet/types.js.map +0 -1
  142. package/lib/module/dialog/CenteredDialog.js +0 -246
  143. package/lib/module/dialog/CenteredDialog.js.map +0 -1
  144. package/lib/module/dialog/CenteredDialog.web.js +0 -324
  145. package/lib/module/dialog/CenteredDialog.web.js.map +0 -1
  146. package/lib/module/dialog/centered-dialog-tokens.js +0 -58
  147. package/lib/module/dialog/centered-dialog-tokens.js.map +0 -1
  148. package/lib/module/dialog/centered-dialog-types.js +0 -4
  149. package/lib/module/dialog/centered-dialog-types.js.map +0 -1
  150. package/lib/module/responsive-sheet/ResponsiveSheet.js +0 -90
  151. package/lib/module/responsive-sheet/ResponsiveSheet.js.map +0 -1
  152. package/lib/module/responsive-sheet/ResponsiveSheet.web.js +0 -77
  153. package/lib/module/responsive-sheet/ResponsiveSheet.web.js.map +0 -1
  154. package/lib/module/responsive-sheet/index.js +0 -4
  155. package/lib/module/responsive-sheet/index.js.map +0 -1
  156. package/lib/module/responsive-sheet/index.web.js +0 -12
  157. package/lib/module/responsive-sheet/index.web.js.map +0 -1
  158. package/lib/module/responsive-sheet/types.js +0 -4
  159. package/lib/module/responsive-sheet/types.js.map +0 -1
  160. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts +0 -22
  161. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts.map +0 -1
  162. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts +0 -31
  163. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +0 -1
  164. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts +0 -47
  165. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts.map +0 -1
  166. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts +0 -74
  167. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts.map +0 -1
  168. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +0 -22
  169. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  170. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts +0 -16
  171. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts.map +0 -1
  172. package/lib/typescript/commonjs/responsive-sheet/index.d.ts +0 -3
  173. package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +0 -1
  174. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +0 -3
  175. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +0 -1
  176. package/lib/typescript/commonjs/responsive-sheet/types.d.ts +0 -58
  177. package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +0 -1
  178. package/lib/typescript/module/dialog/CenteredDialog.d.ts +0 -22
  179. package/lib/typescript/module/dialog/CenteredDialog.d.ts.map +0 -1
  180. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts +0 -31
  181. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +0 -1
  182. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts +0 -47
  183. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts.map +0 -1
  184. package/lib/typescript/module/dialog/centered-dialog-types.d.ts +0 -74
  185. package/lib/typescript/module/dialog/centered-dialog-types.d.ts.map +0 -1
  186. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +0 -22
  187. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  188. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts +0 -16
  189. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts.map +0 -1
  190. package/lib/typescript/module/responsive-sheet/index.d.ts +0 -3
  191. package/lib/typescript/module/responsive-sheet/index.d.ts.map +0 -1
  192. package/lib/typescript/module/responsive-sheet/index.web.d.ts +0 -3
  193. package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +0 -1
  194. package/lib/typescript/module/responsive-sheet/types.d.ts +0 -58
  195. package/lib/typescript/module/responsive-sheet/types.d.ts.map +0 -1
  196. package/src/__tests__/CenteredDialog.test.tsx +0 -108
  197. package/src/__tests__/ResponsiveSheet.test.tsx +0 -88
  198. package/src/dialog/CenteredDialog.stories.tsx +0 -125
  199. package/src/dialog/CenteredDialog.tsx +0 -305
  200. package/src/dialog/CenteredDialog.web.tsx +0 -374
  201. package/src/dialog/centered-dialog-tokens.ts +0 -68
  202. package/src/dialog/centered-dialog-types.ts +0 -74
  203. package/src/responsive-sheet/ResponsiveSheet.stories.tsx +0 -54
  204. package/src/responsive-sheet/ResponsiveSheet.tsx +0 -88
  205. package/src/responsive-sheet/ResponsiveSheet.web.tsx +0 -76
  206. package/src/responsive-sheet/index.ts +0 -6
  207. package/src/responsive-sheet/index.web.ts +0 -14
  208. package/src/responsive-sheet/types.ts +0 -60
@@ -10,11 +10,8 @@ import React, {
10
10
  } from 'react';
11
11
  import {
12
12
  Pressable,
13
- Text,
14
- TouchableOpacity,
15
13
  useWindowDimensions,
16
14
  View,
17
- type GestureResponderEvent,
18
15
  type StyleProp,
19
16
  type ViewStyle,
20
17
  } from 'react-native';
@@ -22,29 +19,24 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
22
19
 
23
20
  import { Portal } from '../portal/index.web';
24
21
  import { createOverlayZIndex } from '../styles/z-index';
25
- import type { ThemeColors } from '../theme/types';
26
22
  import { useTheme } from '../theme/use-theme';
27
- import { Context, useDialogContext, useDialogControl } from './context';
23
+ import { Context, useDialogControl } from './context';
24
+ import { DialogBody } from './DialogContent';
25
+ import { DialogBottomSheet } from './DialogBottomSheet';
28
26
  import {
29
27
  ANIMATION_DURATION,
30
28
  CENTER_FADE_OUT_DURATION,
31
29
  DEFAULT_CENTER_MAX_WIDTH,
32
- DEFAULT_MAX_HEIGHT_RATIO,
33
30
  DEFAULT_SIDE_WIDTH,
34
31
  DIALOG_SHEET_BACKDROP_TESTID,
35
32
  EASE_OUT,
36
- HANDLE_HEIGHT,
37
- HANDLE_RADIUS,
38
- HANDLE_WIDTH,
39
33
  PANEL_RADIUS,
40
34
  SHEET_BACKDROP_OPACITY,
41
35
  SIDE_SHEET_MIN_GUTTER,
42
36
  useResolvedPlacement,
43
- type DialogPlacement,
44
37
  } from './placement';
45
38
  import type {
46
39
  DialogAction,
47
- DialogActionColor,
48
40
  DialogControlProps,
49
41
  DialogInset,
50
42
  DialogProps,
@@ -61,9 +53,10 @@ const ClosingContext = createContext(false);
61
53
  * Web variant of `<Dialog>`.
62
54
  *
63
55
  * A centered modal card (default) rendered into the bloom Portal at the end of
64
- * `document.body`, or when `placement` is `left`/`right`/`bottom`an
65
- * anchored side-sheet or bottom-sheet. Same prop API as native, so call sites
66
- * are platform agnostic.
56
+ * `document.body`, an anchored side-sheet for `left`/`right`, or for
57
+ * `bottom` bloom's cross-platform `BottomSheet` (the SAME component native
58
+ * uses), so the bottom placement drags-to-dismiss on web too. Same prop API as
59
+ * native, so call sites are platform agnostic.
67
60
  *
68
61
  * Open/close is driven by EITHER the imperative `control` (legacy) OR the
69
62
  * controlled `open` prop. When `open` is provided it wins.
@@ -75,10 +68,32 @@ const ClosingContext = createContext(false);
75
68
  *
76
69
  * Web motion never uses reanimated `exiting` (which throws `removeChild` on
77
70
  * concurrent React unmounts): the centered card uses CSS keyframes and the
78
- * side/bottom modes use self-contained CSS transitions (the `responsive-sheet`
79
- * technique) on a mounted-through-exit node.
71
+ * side modes use self-contained CSS transitions on a mounted-through-exit
72
+ * node. The bottom placement animates via `BottomSheet`'s shared-value/gesture
73
+ * animation (no `exiting` layout animation).
80
74
  */
81
- export function Dialog({
75
+ export function Dialog({ placement, ...rest }: DialogProps) {
76
+ const resolvedPlacement = useResolvedPlacement(placement);
77
+
78
+ // `bottom` routes to the shared cross-platform `BottomSheet` surface — the
79
+ // SAME component native uses — so drag-to-dismiss works on web too and there
80
+ // is no duplicated CSS slide-up sheet. `center` (default) and the side-sheets
81
+ // keep their DOM-portal implementation below. Each branch is its own
82
+ // component so the dispatcher only ever calls `useResolvedPlacement`, keeping
83
+ // the rules-of-hooks contract intact across a responsive placement change.
84
+ if (resolvedPlacement === 'bottom') {
85
+ return <DialogBottomSheet {...rest} />;
86
+ }
87
+ return <CenterOrSideDialog {...rest} placement={resolvedPlacement} />;
88
+ }
89
+
90
+ /**
91
+ * Web `center` (default) and `left`/`right` placements. The centered card is a
92
+ * DOM-portal modal; the side placements are CSS-transition sheets. The `bottom`
93
+ * placement is handled separately by `DialogBottomSheet` (bloom's cross-platform
94
+ * `BottomSheet`).
95
+ */
96
+ function CenterOrSideDialog({
82
97
  control,
83
98
  open: controlledOpen,
84
99
  onClose,
@@ -89,9 +104,7 @@ export function Dialog({
89
104
  placement,
90
105
  width = DEFAULT_SIDE_WIDTH,
91
106
  maxWidth = DEFAULT_CENTER_MAX_WIDTH,
92
- maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
93
107
  inset,
94
- showHandle = true,
95
108
  dismissOnBackdrop = true,
96
109
  style,
97
110
  panelStyle,
@@ -100,11 +113,11 @@ export function Dialog({
100
113
  containerClassName,
101
114
  label,
102
115
  children,
103
- }: DialogProps) {
116
+ }: Omit<DialogProps, 'placement'> & { placement: 'center' | 'left' | 'right' }) {
104
117
  // Controlled mode is opt-in: when `open` is a boolean the host owns the
105
118
  // visible state; otherwise the legacy imperative `control` path drives it.
106
119
  const isControlled = controlledOpen !== undefined;
107
- const resolvedPlacement = useResolvedPlacement(placement);
120
+ const resolvedPlacement = placement;
108
121
 
109
122
  const [isOpen, setIsOpen] = useState(false);
110
123
  const [isClosing, setIsClosing] = useState(false);
@@ -184,9 +197,12 @@ export function Dialog({
184
197
 
185
198
  // Escape-to-close while open. The listener is intentionally scoped to the
186
199
  // open lifetime so stacked dialogs don't fight for the keydown — the
187
- // top-most one wins via document-level event order.
200
+ // top-most one wins via document-level event order. Escape honors
201
+ // `dismissOnBackdrop`: a blocking dialog (e.g. an unanswered confirm) is not
202
+ // dismissible by Escape, matching the backdrop's behavior. The default
203
+ // (`dismissOnBackdrop` true) is unchanged — Escape still closes.
188
204
  useEffect(() => {
189
- if (!isOpen || typeof document === 'undefined') return;
205
+ if (!isOpen || !dismissOnBackdrop || typeof document === 'undefined') return;
190
206
  const handler = (e: KeyboardEvent) => {
191
207
  if (e.key === 'Escape') {
192
208
  e.stopPropagation();
@@ -195,7 +211,7 @@ export function Dialog({
195
211
  };
196
212
  document.addEventListener('keydown', handler);
197
213
  return () => document.removeEventListener('keydown', handler);
198
- }, [close, isOpen]);
214
+ }, [close, dismissOnBackdrop, isOpen]);
199
215
 
200
216
  useImperativeHandle(
201
217
  control?.ref,
@@ -269,9 +285,7 @@ export function Dialog({
269
285
  placement={resolvedPlacement}
270
286
  shown={!isClosing}
271
287
  width={width}
272
- maxHeightRatio={maxHeightRatio}
273
288
  inset={inset}
274
- showHandle={showHandle}
275
289
  dismissOnBackdrop={dismissOnBackdrop}
276
290
  onDismiss={close}
277
291
  panelStyle={panelStyle}
@@ -288,64 +302,6 @@ export function Dialog({
288
302
  );
289
303
  }
290
304
 
291
- /**
292
- * Renders the dialog's body: optional declarative title + description, any
293
- * `children`, then the action row. Shared by the centered panel and the
294
- * side/bottom sheet so all placements present identical content. The
295
- * `titleId`/`descriptionId` are wired by the caller onto the `role="dialog"`
296
- * element for `aria-labelledby` / `aria-describedby`.
297
- */
298
- function DialogBody({
299
- titleId,
300
- descriptionId,
301
- title,
302
- description,
303
- actions,
304
- children,
305
- }: {
306
- titleId: string;
307
- descriptionId: string;
308
- title?: string;
309
- description?: string;
310
- actions?: DialogAction[];
311
- children?: React.ReactNode;
312
- }) {
313
- const theme = useTheme();
314
- return (
315
- <>
316
- {title ? (
317
- <Text
318
- nativeID={titleId}
319
- style={{
320
- fontSize: 22,
321
- fontWeight: '600',
322
- color: theme.colors.text,
323
- paddingBottom: description ? 4 : 16,
324
- lineHeight: 30,
325
- }}
326
- >
327
- {title}
328
- </Text>
329
- ) : null}
330
- {description ? (
331
- <Text
332
- nativeID={descriptionId}
333
- style={{
334
- fontSize: 16,
335
- color: theme.colors.textSecondary,
336
- paddingBottom: 16,
337
- lineHeight: 22,
338
- }}
339
- >
340
- {description}
341
- </Text>
342
- ) : null}
343
- {children}
344
- {actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
345
- </>
346
- );
347
- }
348
-
349
305
  function DialogPanel({
350
306
  testID,
351
307
  label,
@@ -419,10 +375,13 @@ function DialogPanel({
419
375
  export { DIALOG_SHEET_BACKDROP_TESTID };
420
376
 
421
377
  /**
422
- * Side-sheet / bottom-sheet surface for the `left`/`right`/`bottom`
423
- * placements. Pure CSS transitions (the `responsive-sheet` technique): the
424
- * node stays mounted through the exit while a `shown` flag drives the
378
+ * Side-sheet surface for the `left`/`right` placements. Pure CSS transitions:
379
+ * the node stays mounted through the exit while a `shown` flag drives the
425
380
  * transform/opacity, so both directions animate without reanimated `exiting`.
381
+ *
382
+ * The `bottom` placement is NOT handled here — it routes through
383
+ * `DialogBottomSheet` (bloom's cross-platform `BottomSheet`) so it shares one
384
+ * implementation with native and supports drag-to-dismiss on web.
426
385
  */
427
386
  function SheetSurface({
428
387
  testID,
@@ -433,9 +392,7 @@ function SheetSurface({
433
392
  placement,
434
393
  shown,
435
394
  width,
436
- maxHeightRatio,
437
395
  inset,
438
- showHandle,
439
396
  dismissOnBackdrop,
440
397
  onDismiss,
441
398
  panelStyle,
@@ -450,12 +407,10 @@ function SheetSurface({
450
407
  title?: string;
451
408
  description?: string;
452
409
  actions?: DialogAction[];
453
- placement: Exclude<DialogPlacement, 'center'>;
410
+ placement: 'left' | 'right';
454
411
  shown: boolean;
455
412
  width: number;
456
- maxHeightRatio: number;
457
413
  inset?: DialogInset;
458
- showHandle: boolean;
459
414
  dismissOnBackdrop: boolean;
460
415
  onDismiss: () => void;
461
416
  panelStyle?: StyleProp<ViewStyle>;
@@ -468,7 +423,7 @@ function SheetSurface({
468
423
  const theme = useTheme();
469
424
  const titleId = useId();
470
425
  const descriptionId = useId();
471
- const { width: viewportWidth, height: viewportHeight } = useWindowDimensions();
426
+ const { width: viewportWidth } = useWindowDimensions();
472
427
 
473
428
  // Defer the entry transition by a frame so the start state paints before the
474
429
  // browser animates to `shown`. `entered` is false on the first committed
@@ -486,7 +441,6 @@ function SheetSurface({
486
441
  }, []);
487
442
 
488
443
  const visible = shown && entered;
489
- const isBottom = placement === 'bottom';
490
444
 
491
445
  const panelTransition = useMemo<ViewStyle>(
492
446
  () =>
@@ -509,19 +463,6 @@ function SheetSurface({
509
463
  );
510
464
 
511
465
  const panelGeometry = useMemo<ViewStyle>(() => {
512
- if (isBottom) {
513
- return {
514
- left: 0,
515
- right: 0,
516
- bottom: 0,
517
- maxHeight: Math.round(viewportHeight * maxHeightRatio),
518
- borderTopLeftRadius: PANEL_RADIUS,
519
- borderTopRightRadius: PANEL_RADIUS,
520
- transform: [{ translateY: visible ? 0 : '100%' }],
521
- opacity: visible ? 1 : 0,
522
- } as ViewStyle;
523
- }
524
-
525
466
  const insetTop = inset?.top ?? 0;
526
467
  const insetBottom = inset?.bottom ?? 0;
527
468
  const insetLeft = inset?.left ?? 0;
@@ -541,7 +482,7 @@ function SheetSurface({
541
482
  transform: [{ translateX: visible ? 0 : hiddenSign }],
542
483
  opacity: visible ? 1 : 0,
543
484
  } as ViewStyle;
544
- }, [isBottom, visible, placement, width, inset, viewportWidth, viewportHeight, maxHeightRatio]);
485
+ }, [visible, placement, width, inset, viewportWidth]);
545
486
 
546
487
  const handleBackdropPress = useCallback(() => {
547
488
  if (dismissOnBackdrop) onDismiss();
@@ -587,11 +528,6 @@ function SheetSurface({
587
528
  ]}
588
529
  pointerEvents="auto"
589
530
  >
590
- {isBottom && showHandle ? (
591
- <View style={sheetStyles.handleRow} pointerEvents="none">
592
- <View style={[sheetStyles.handle, { backgroundColor: theme.colors.border }]} />
593
- </View>
594
- ) : null}
595
531
  <View style={sheetStyles.body}>
596
532
  <DialogBody
597
533
  titleId={titleId}
@@ -629,88 +565,6 @@ function cancelFrame(token: FrameToken): void {
629
565
  clearTimeout(token.timer);
630
566
  }
631
567
 
632
- function ActionRow({ actions }: { actions: DialogAction[] }) {
633
- return (
634
- <View style={{ width: '100%', gap: 8, justifyContent: 'flex-end' }}>
635
- {actions.map((action, idx) => (
636
- <ActionButton
637
- key={`${action.label}-${idx}`}
638
- action={action}
639
- />
640
- ))}
641
- </View>
642
- );
643
- }
644
-
645
- function ActionButton({ action }: { action: DialogAction }) {
646
- const { close } = useDialogContext();
647
- const theme = useTheme();
648
- const color: DialogActionColor = action.color ?? 'default';
649
- const shouldCloseOnPress = action.shouldCloseOnPress ?? true;
650
-
651
- const { background, foreground } = getActionPalette(color, theme.colors);
652
-
653
- const handlePress = useCallback(
654
- (e: GestureResponderEvent) => {
655
- const onPress = action.onPress;
656
- if (color === 'cancel') {
657
- close(onPress ? () => onPress(e) : undefined);
658
- return;
659
- }
660
- if (shouldCloseOnPress) {
661
- close(onPress ? () => onPress(e) : undefined);
662
- } else {
663
- onPress?.(e);
664
- }
665
- },
666
- [action.onPress, close, color, shouldCloseOnPress],
667
- );
668
-
669
- return (
670
- <TouchableOpacity
671
- style={{
672
- borderRadius: 9999,
673
- alignItems: 'center',
674
- justifyContent: 'center',
675
- backgroundColor: background,
676
- opacity: action.disabled ? 0.5 : 1,
677
- paddingVertical: 12,
678
- paddingHorizontal: 24,
679
- }}
680
- onPress={handlePress}
681
- disabled={action.disabled}
682
- activeOpacity={0.7}
683
- testID={action.testID}
684
- >
685
- <Text style={{ fontSize: 16, fontWeight: '500', color: foreground }}>
686
- {action.label}
687
- </Text>
688
- </TouchableOpacity>
689
- );
690
- }
691
-
692
- function getActionPalette(
693
- color: DialogActionColor,
694
- colors: ThemeColors,
695
- ): { background: string; foreground: string } {
696
- switch (color) {
697
- case 'destructive':
698
- return {
699
- background: colors.negative,
700
- foreground: colors.negativeForeground,
701
- };
702
- case 'cancel':
703
- return { background: colors.contrast50, foreground: colors.text };
704
- case 'default':
705
- return { background: colors.primary, foreground: colors.primaryForeground };
706
- /* c8 ignore next 3 -- TS exhaustiveness guard */
707
- default: {
708
- const _exhaustive: never = color;
709
- return { background: colors.primary, foreground: colors.primaryForeground };
710
- }
711
- }
712
- }
713
-
714
568
  function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
715
569
  const style: ViewStyle[] = [
716
570
  {
@@ -787,18 +641,6 @@ const sheetStyles = {
787
641
  shadowOffset: { width: 0, height: 8 },
788
642
  zIndex: dialogZIndex.surface,
789
643
  } as ViewStyle,
790
- handleRow: {
791
- width: '100%',
792
- alignItems: 'center',
793
- paddingTop: 8,
794
- paddingBottom: 4,
795
- } as ViewStyle,
796
- handle: {
797
- width: HANDLE_WIDTH,
798
- height: HANDLE_HEIGHT,
799
- borderRadius: HANDLE_RADIUS,
800
- opacity: 0.5,
801
- } as ViewStyle,
802
644
  body: {
803
645
  padding: 20,
804
646
  } as ViewStyle,
@@ -815,8 +657,9 @@ const sheetStyles = {
815
657
  * @keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }
816
658
  * ```
817
659
  *
818
- * The `left`/`right`/`bottom` placements do NOT depend on this — they animate
819
- * via self-contained inline CSS transitions and need no keyframe injection.
660
+ * The `left`/`right` placements do NOT depend on this — they animate via
661
+ * self-contained inline CSS transitions and need no keyframe injection. The
662
+ * `bottom` placement uses bloom's `BottomSheet` (reanimated) and needs none.
820
663
  */
821
664
  export const BLOOM_DIALOG_CSS = `
822
665
  @keyframes bloomDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }
@@ -0,0 +1,235 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ } from 'react';
9
+ import { View, type StyleProp, type ViewStyle } from 'react-native';
10
+
11
+ import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
12
+ import { useTheme } from '../theme/use-theme';
13
+ import { Context } from './context';
14
+ import { DialogBody } from './DialogContent';
15
+ import {
16
+ DEFAULT_MAX_HEIGHT_RATIO,
17
+ PANEL_RADIUS,
18
+ SHEET_BACKDROP_OPACITY,
19
+ } from './placement';
20
+ import type { DialogControlProps, DialogProps } from './types';
21
+
22
+ /**
23
+ * Padding of the bottom-sheet content container (px). Matches the side-sheet
24
+ * and centered-panel body padding so every placement reads identically.
25
+ */
26
+ const SHEET_CONTENT_PADDING = 20;
27
+
28
+ /**
29
+ * Shared `BottomSheet`-backed surface for the `bottom` placement, used by BOTH
30
+ * `Dialog.tsx` (native) and `Dialog.web.tsx` (web). `BottomSheet` is itself a
31
+ * single cross-platform implementation (react-native-gesture-handler +
32
+ * reanimated, drag-to-dismiss on web and native alike), so routing the bottom
33
+ * placement through here gives ONE code path with drag working everywhere — no
34
+ * duplicated CSS slide-up sheet.
35
+ *
36
+ * This component owns the entire open↔present/dismiss bridge and the Dialog →
37
+ * BottomSheet prop mapping in one place:
38
+ *
39
+ * - `open` (controlled) / `control` (imperative) → `present()` / `dismiss()`
40
+ * - `onClose` → fired once the sheet settles
41
+ * - `maxHeightRatio` → sheet `maxHeight`
42
+ * - `showHandle` / `dismissOnBackdrop` / `label` → forwarded to the surface
43
+ * - `panelStyle` → paints the sheet surface
44
+ * - `containerStyle` → wraps the content subtree
45
+ * so a host's CSS-var theme scope (`vars()`) still applies to descendants
46
+ * - `title` / `description` / `actions` / `children` → declarative body
47
+ */
48
+ export function DialogBottomSheet({
49
+ control,
50
+ open: controlledOpen,
51
+ onClose,
52
+ testID,
53
+ title,
54
+ description,
55
+ actions,
56
+ maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
57
+ showHandle = true,
58
+ dismissOnBackdrop = true,
59
+ style,
60
+ panelStyle,
61
+ panelClassName,
62
+ containerStyle,
63
+ containerClassName,
64
+ label,
65
+ children,
66
+ }: DialogBottomSheetProps) {
67
+ const isControlled = controlledOpen !== undefined;
68
+ const theme = useTheme();
69
+ const ref = useRef<BottomSheetRef>(null);
70
+ const closeCallbacks = useRef<(() => void)[]>([]);
71
+ const titleId = useId();
72
+ const descriptionId = useId();
73
+
74
+ // Read the latest controlled flag / `onClose` inside stable callbacks without
75
+ // re-binding them (the context + imperative handle depend on `close` staying
76
+ // referentially stable).
77
+ const isControlledRef = useRef(isControlled);
78
+ isControlledRef.current = isControlled;
79
+ const onCloseRef = useRef(onClose);
80
+ onCloseRef.current = onClose;
81
+
82
+ // Drain queued close callbacks atomically — capturing the list and resetting
83
+ // it before invocation ensures a callback that synchronously re-opens the
84
+ // dialog (and queues fresh callbacks) does not see the old ones replayed
85
+ // against the new session.
86
+ const callQueuedCallbacks = useCallback(() => {
87
+ const queued = closeCallbacks.current;
88
+ closeCallbacks.current = [];
89
+ for (const cb of queued) {
90
+ try {
91
+ cb();
92
+ } catch (e) {
93
+ if (typeof console !== 'undefined' && console.error) {
94
+ console.error('Dialog close callback error:', e);
95
+ }
96
+ }
97
+ }
98
+ }, []);
99
+
100
+ const open = useCallback(() => {
101
+ ref.current?.present();
102
+ }, []);
103
+
104
+ // A dismissal request (backdrop, pan-to-close, action button). In controlled
105
+ // mode it asks the host to close via `onClose` (the host flips `open`); in
106
+ // imperative mode it dismisses the surface directly. The optional callback
107
+ // runs once the dialog has finished closing.
108
+ const close = useCallback<DialogControlProps['close']>((cb) => {
109
+ if (typeof cb === 'function') {
110
+ closeCallbacks.current.push(cb);
111
+ }
112
+ if (isControlledRef.current) {
113
+ onCloseRef.current?.();
114
+ return;
115
+ }
116
+ ref.current?.dismiss();
117
+ }, []);
118
+
119
+ // Fired when the sheet has finished closing. Drains queued callbacks, then
120
+ // fires `onClose` ONLY in imperative mode — controlled mode already requested
121
+ // the close through `onClose` (the host then flipped `open`), so firing it
122
+ // again here would double-call it.
123
+ const handleDismiss = useCallback(() => {
124
+ callQueuedCallbacks();
125
+ if (!isControlledRef.current) onCloseRef.current?.();
126
+ }, [callQueuedCallbacks]);
127
+
128
+ // In controlled mode, mirror the `open` prop onto the underlying sheet so both
129
+ // modes share one lifecycle.
130
+ useEffect(() => {
131
+ if (!isControlled) return;
132
+ if (controlledOpen) {
133
+ ref.current?.present();
134
+ } else {
135
+ ref.current?.dismiss();
136
+ }
137
+ }, [isControlled, controlledOpen]);
138
+
139
+ useImperativeHandle(control?.ref, () => ({ open, close }), [open, close]);
140
+
141
+ const context = useMemo(() => ({ close, isWithinDialog: true }), [close]);
142
+
143
+ // Backdrop press → dismiss, unless the host disabled it. Returning `false`
144
+ // from `onDismissAttempt` blocks the BottomSheet's own backdrop/pan dismissal,
145
+ // which is how `dismissOnBackdrop: false` maps onto the single sheet API.
146
+ const onDismissAttempt = useCallback(
147
+ () => dismissOnBackdrop,
148
+ [dismissOnBackdrop],
149
+ );
150
+
151
+ // `panelStyle` paints the sheet surface (e.g. a brand background). It composes
152
+ // AFTER the BottomSheet's internal background so a host palette wins; the
153
+ // shape (radius, max height) is supplied here and remains overridable.
154
+ const sheetStyle = useMemo<StyleProp<ViewStyle>>(() => {
155
+ const maxHeightPercent: `${number}%` = `${Math.round(maxHeightRatio * 100)}%`;
156
+ return [
157
+ {
158
+ backgroundColor: theme.colors.background,
159
+ borderTopLeftRadius: PANEL_RADIUS + 4,
160
+ borderTopRightRadius: PANEL_RADIUS + 4,
161
+ maxHeight: maxHeightPercent,
162
+ },
163
+ panelStyle,
164
+ ];
165
+ }, [theme.colors.background, maxHeightRatio, panelStyle]);
166
+
167
+ return (
168
+ <BottomSheet
169
+ ref={ref}
170
+ onDismiss={handleDismiss}
171
+ onDismissAttempt={onDismissAttempt}
172
+ enablePanDownToClose
173
+ showHandle={showHandle}
174
+ // Stronger dim than a lone sheet so an underlying sheet's handle/content
175
+ // doesn't bleed through when a Dialog is stacked over one.
176
+ backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
177
+ style={sheetStyle}
178
+ >
179
+ <Context.Provider value={context}>
180
+ <View
181
+ testID={testID}
182
+ accessibilityLabel={label}
183
+ aria-labelledby={title ? titleId : undefined}
184
+ aria-describedby={description ? descriptionId : undefined}
185
+ {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
186
+ {...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
187
+ style={[
188
+ { padding: SHEET_CONTENT_PADDING },
189
+ // `containerStyle` carries the host's CSS-var theme scope; it wraps
190
+ // the content subtree so descendants read the scoped palette.
191
+ containerStyle,
192
+ style,
193
+ panelStyle,
194
+ ]}
195
+ >
196
+ <DialogBody
197
+ titleId={titleId}
198
+ descriptionId={descriptionId}
199
+ title={title}
200
+ description={description}
201
+ actions={actions}
202
+ >
203
+ {children}
204
+ </DialogBody>
205
+ </View>
206
+ </Context.Provider>
207
+ </BottomSheet>
208
+ );
209
+ }
210
+
211
+ /**
212
+ * Props the shared bottom-sheet surface consumes — the `bottom`-relevant subset
213
+ * of `DialogProps`. `width`, `maxWidth`, and `inset` are placement-specific to
214
+ * the side/centered surfaces and intentionally excluded.
215
+ */
216
+ export type DialogBottomSheetProps = Pick<
217
+ DialogProps,
218
+ | 'control'
219
+ | 'open'
220
+ | 'onClose'
221
+ | 'testID'
222
+ | 'title'
223
+ | 'description'
224
+ | 'actions'
225
+ | 'maxHeightRatio'
226
+ | 'showHandle'
227
+ | 'dismissOnBackdrop'
228
+ | 'style'
229
+ | 'panelStyle'
230
+ | 'panelClassName'
231
+ | 'containerStyle'
232
+ | 'containerClassName'
233
+ | 'label'
234
+ | 'children'
235
+ >;