@oxyhq/bloom 0.45.0 → 0.48.0

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 (211) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +34 -12
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/combobox/Combobox.js +3 -3
  4. package/lib/commonjs/combobox/Combobox.js.map +1 -1
  5. package/lib/commonjs/command/Command.js +3 -3
  6. package/lib/commonjs/command/Command.js.map +1 -1
  7. package/lib/commonjs/dialog/Dialog.js +86 -8
  8. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  9. package/lib/commonjs/dialog/Dialog.web.js +176 -18
  10. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  11. package/lib/commonjs/dialog/DialogBottomSheet.js +84 -29
  12. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  13. package/lib/commonjs/dialog/DialogHeader.js +365 -0
  14. package/lib/commonjs/dialog/DialogHeader.js.map +1 -0
  15. package/lib/commonjs/dialog/index.js +7 -0
  16. package/lib/commonjs/dialog/index.js.map +1 -1
  17. package/lib/commonjs/dialog/index.web.js +7 -0
  18. package/lib/commonjs/dialog/index.web.js.map +1 -1
  19. package/lib/commonjs/index.js +40 -4
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/index.web.js +84 -48
  22. package/lib/commonjs/index.web.js.map +1 -1
  23. package/lib/commonjs/react-native-className.d.js +6 -0
  24. package/lib/commonjs/react-native-className.d.js.map +1 -0
  25. package/lib/commonjs/{search-input → search}/index.js +5 -3
  26. package/lib/commonjs/search/index.js.map +1 -0
  27. package/lib/commonjs/settings-list/SettingsList.js +5 -1
  28. package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
  29. package/lib/commonjs/surfaces/SurfaceHost.js +202 -0
  30. package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -0
  31. package/lib/commonjs/surfaces/api.js +83 -0
  32. package/lib/commonjs/surfaces/api.js.map +1 -0
  33. package/lib/commonjs/surfaces/index.js +82 -0
  34. package/lib/commonjs/surfaces/index.js.map +1 -0
  35. package/lib/commonjs/surfaces/index.web.js +77 -0
  36. package/lib/commonjs/surfaces/index.web.js.map +1 -0
  37. package/lib/commonjs/surfaces/prompts.js +154 -0
  38. package/lib/commonjs/surfaces/prompts.js.map +1 -0
  39. package/lib/commonjs/surfaces/surfaceStore.js +172 -0
  40. package/lib/commonjs/surfaces/surfaceStore.js.map +1 -0
  41. package/lib/commonjs/surfaces/types.js +6 -0
  42. package/lib/commonjs/surfaces/types.js.map +1 -0
  43. package/lib/commonjs/surfaces/useSurface.js +36 -0
  44. package/lib/commonjs/surfaces/useSurface.js.map +1 -0
  45. package/lib/commonjs/text-field/index.js +22 -6
  46. package/lib/commonjs/text-field/index.js.map +1 -1
  47. package/lib/module/bottom-sheet/BottomSheetBase.js +34 -12
  48. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  49. package/lib/module/combobox/Combobox.js +3 -3
  50. package/lib/module/combobox/Combobox.js.map +1 -1
  51. package/lib/module/command/Command.js +3 -3
  52. package/lib/module/command/Command.js.map +1 -1
  53. package/lib/module/dialog/Dialog.js +88 -10
  54. package/lib/module/dialog/Dialog.js.map +1 -1
  55. package/lib/module/dialog/Dialog.web.js +179 -21
  56. package/lib/module/dialog/Dialog.web.js.map +1 -1
  57. package/lib/module/dialog/DialogBottomSheet.js +85 -30
  58. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  59. package/lib/module/dialog/DialogHeader.js +356 -0
  60. package/lib/module/dialog/DialogHeader.js.map +1 -0
  61. package/lib/module/dialog/index.js +1 -0
  62. package/lib/module/dialog/index.js.map +1 -1
  63. package/lib/module/dialog/index.web.js +1 -0
  64. package/lib/module/dialog/index.web.js.map +1 -1
  65. package/lib/module/index.js +7 -2
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/index.web.js +7 -2
  68. package/lib/module/index.web.js.map +1 -1
  69. package/lib/module/react-native-className.d.js +20 -0
  70. package/lib/module/react-native-className.d.js.map +1 -0
  71. package/lib/module/{search-input → search}/index.js +6 -2
  72. package/lib/module/search/index.js.map +1 -0
  73. package/lib/module/settings-list/SettingsList.js +5 -1
  74. package/lib/module/settings-list/SettingsList.js.map +1 -1
  75. package/lib/module/surfaces/SurfaceHost.js +197 -0
  76. package/lib/module/surfaces/SurfaceHost.js.map +1 -0
  77. package/lib/module/surfaces/api.js +48 -0
  78. package/lib/module/surfaces/api.js.map +1 -0
  79. package/lib/module/surfaces/index.js +24 -0
  80. package/lib/module/surfaces/index.js.map +1 -0
  81. package/lib/module/surfaces/index.web.js +19 -0
  82. package/lib/module/surfaces/index.web.js.map +1 -0
  83. package/lib/module/surfaces/prompts.js +148 -0
  84. package/lib/module/surfaces/prompts.js.map +1 -0
  85. package/lib/module/surfaces/surfaceStore.js +159 -0
  86. package/lib/module/surfaces/surfaceStore.js.map +1 -0
  87. package/lib/module/surfaces/types.js +4 -0
  88. package/lib/module/surfaces/types.js.map +1 -0
  89. package/lib/module/surfaces/useSurface.js +31 -0
  90. package/lib/module/surfaces/useSurface.js.map +1 -0
  91. package/lib/module/text-field/index.js +22 -6
  92. package/lib/module/text-field/index.js.map +1 -1
  93. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +23 -0
  94. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/combobox/Combobox.d.ts +1 -1
  96. package/lib/typescript/commonjs/command/Command.d.ts +1 -1
  97. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
  100. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  101. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +86 -0
  102. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/dialog/index.d.ts +2 -1
  104. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -1
  106. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  107. package/lib/typescript/commonjs/dialog/types.d.ts +85 -0
  108. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  109. package/lib/typescript/commonjs/icons/common.d.ts +1 -0
  110. package/lib/typescript/commonjs/icons/common.d.ts.map +1 -1
  111. package/lib/typescript/commonjs/index.d.ts +5 -3
  112. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  113. package/lib/typescript/commonjs/index.web.d.ts +5 -3
  114. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  115. package/lib/typescript/commonjs/input-group/types.d.ts +1 -1
  116. package/lib/typescript/commonjs/{search-input → search}/index.d.ts +1 -1
  117. package/lib/typescript/commonjs/search/index.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts +25 -0
  119. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/surfaces/api.d.ts +41 -0
  121. package/lib/typescript/commonjs/surfaces/api.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/surfaces/index.d.ts +25 -0
  123. package/lib/typescript/commonjs/surfaces/index.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/surfaces/index.web.d.ts +20 -0
  125. package/lib/typescript/commonjs/surfaces/index.web.d.ts.map +1 -0
  126. package/lib/typescript/commonjs/surfaces/prompts.d.ts +13 -0
  127. package/lib/typescript/commonjs/surfaces/prompts.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts +51 -0
  129. package/lib/typescript/commonjs/surfaces/surfaceStore.d.ts.map +1 -0
  130. package/lib/typescript/commonjs/surfaces/types.d.ts +161 -0
  131. package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/surfaces/useSurface.d.ts +21 -0
  133. package/lib/typescript/commonjs/surfaces/useSurface.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/text-field/index.d.ts +2 -1
  135. package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
  136. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +23 -0
  137. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  138. package/lib/typescript/module/combobox/Combobox.d.ts +1 -1
  139. package/lib/typescript/module/command/Command.d.ts +1 -1
  140. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  141. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  142. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
  143. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  144. package/lib/typescript/module/dialog/DialogHeader.d.ts +86 -0
  145. package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -0
  146. package/lib/typescript/module/dialog/index.d.ts +2 -1
  147. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  148. package/lib/typescript/module/dialog/index.web.d.ts +2 -1
  149. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  150. package/lib/typescript/module/dialog/types.d.ts +85 -0
  151. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  152. package/lib/typescript/module/icons/common.d.ts +1 -0
  153. package/lib/typescript/module/icons/common.d.ts.map +1 -1
  154. package/lib/typescript/module/index.d.ts +5 -3
  155. package/lib/typescript/module/index.d.ts.map +1 -1
  156. package/lib/typescript/module/index.web.d.ts +5 -3
  157. package/lib/typescript/module/index.web.d.ts.map +1 -1
  158. package/lib/typescript/module/input-group/types.d.ts +1 -1
  159. package/lib/typescript/module/{search-input → search}/index.d.ts +1 -1
  160. package/lib/typescript/module/search/index.d.ts.map +1 -0
  161. package/lib/typescript/module/surfaces/SurfaceHost.d.ts +25 -0
  162. package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -0
  163. package/lib/typescript/module/surfaces/api.d.ts +41 -0
  164. package/lib/typescript/module/surfaces/api.d.ts.map +1 -0
  165. package/lib/typescript/module/surfaces/index.d.ts +25 -0
  166. package/lib/typescript/module/surfaces/index.d.ts.map +1 -0
  167. package/lib/typescript/module/surfaces/index.web.d.ts +20 -0
  168. package/lib/typescript/module/surfaces/index.web.d.ts.map +1 -0
  169. package/lib/typescript/module/surfaces/prompts.d.ts +13 -0
  170. package/lib/typescript/module/surfaces/prompts.d.ts.map +1 -0
  171. package/lib/typescript/module/surfaces/surfaceStore.d.ts +51 -0
  172. package/lib/typescript/module/surfaces/surfaceStore.d.ts.map +1 -0
  173. package/lib/typescript/module/surfaces/types.d.ts +161 -0
  174. package/lib/typescript/module/surfaces/types.d.ts.map +1 -0
  175. package/lib/typescript/module/surfaces/useSurface.d.ts +21 -0
  176. package/lib/typescript/module/surfaces/useSurface.d.ts.map +1 -0
  177. package/lib/typescript/module/text-field/index.d.ts +2 -1
  178. package/lib/typescript/module/text-field/index.d.ts.map +1 -1
  179. package/package.json +24 -8
  180. package/src/__tests__/BottomSheet.test.tsx +47 -0
  181. package/src/__tests__/DialogBottomSheet.test.tsx +30 -5
  182. package/src/__tests__/surfaceStore.test.ts +219 -0
  183. package/src/bottom-sheet/BottomSheetBase.tsx +59 -10
  184. package/src/combobox/Combobox.tsx +3 -3
  185. package/src/command/Command.tsx +3 -3
  186. package/src/dialog/Dialog.tsx +88 -7
  187. package/src/dialog/Dialog.web.tsx +179 -29
  188. package/src/dialog/DialogBottomSheet.tsx +90 -28
  189. package/src/dialog/DialogHeader.tsx +420 -0
  190. package/src/dialog/index.ts +2 -0
  191. package/src/dialog/index.web.ts +2 -0
  192. package/src/dialog/types.ts +86 -0
  193. package/src/index.ts +18 -1
  194. package/src/index.web.ts +18 -1
  195. package/src/input-group/types.ts +1 -1
  196. package/src/react-native-className.d.ts +38 -0
  197. package/src/{search-input → search}/index.tsx +8 -5
  198. package/src/settings-list/SettingsList.tsx +5 -1
  199. package/src/surfaces/SurfaceHost.tsx +222 -0
  200. package/src/surfaces/api.ts +55 -0
  201. package/src/surfaces/index.ts +38 -0
  202. package/src/surfaces/index.web.ts +33 -0
  203. package/src/surfaces/prompts.tsx +178 -0
  204. package/src/surfaces/surfaceStore.ts +168 -0
  205. package/src/surfaces/types.ts +172 -0
  206. package/src/surfaces/useSurface.ts +33 -0
  207. package/src/text-field/index.tsx +24 -5
  208. package/lib/commonjs/search-input/index.js.map +0 -1
  209. package/lib/module/search-input/index.js.map +0 -1
  210. package/lib/typescript/commonjs/search-input/index.d.ts.map +0 -1
  211. package/lib/typescript/module/search-input/index.d.ts.map +0 -1
@@ -25,11 +25,19 @@ import Animated, {
25
25
  } from 'react-native-reanimated';
26
26
 
27
27
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
28
- import { Z_INDEX } from '../styles/z-index';
28
+ import { Z_INDEX, Z_INDEX_LAYER_STEP } from '../styles/z-index';
29
29
  import { useTheme } from '../theme/use-theme';
30
30
  import { Context, useDialogControl } from './context';
31
31
  import { DialogBody } from './DialogContent';
32
32
  import { DialogBottomSheet } from './DialogBottomSheet';
33
+ import {
34
+ DIALOG_NAV_BAR_HEIGHT,
35
+ type DialogHeaderController,
36
+ DialogHeaderProvider,
37
+ DialogLargeTitle,
38
+ DialogNavHeader,
39
+ useDialogHeaderController,
40
+ } from './DialogHeader';
33
41
  import {
34
42
  ANIMATION_DURATION,
35
43
  DEFAULT_DIALOG_CONTENT_PADDING,
@@ -103,7 +111,10 @@ function CenteredOrSideDialog({
103
111
  title,
104
112
  description,
105
113
  actions,
114
+ header,
115
+ scrollable,
106
116
  placement,
117
+ layer,
107
118
  width = DEFAULT_SIDE_WIDTH,
108
119
  inset,
109
120
  dismissOnBackdrop = true,
@@ -118,6 +129,8 @@ function CenteredOrSideDialog({
118
129
  }: Omit<DialogProps, 'placement'> & { placement: 'center' | 'left' | 'right' }) {
119
130
  const isControlled = controlledOpen !== undefined;
120
131
  const isSide = placement === 'left' || placement === 'right';
132
+ const headerController = useDialogHeaderController();
133
+ const scrollableResolved = scrollable ?? true;
121
134
 
122
135
  // Imperative open state for the side placement (the BottomSheet path owns its
123
136
  // own visibility via its ref instead).
@@ -234,6 +247,24 @@ function CenteredOrSideDialog({
234
247
  </DialogBody>
235
248
  );
236
249
 
250
+ // Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
251
+ // collapsing title over its own scroll content. The large title + screen body
252
+ // scroll under the bar, feeding the shared collapse controller. `contentPadding`
253
+ // is dropped (the large title + screens own their padding).
254
+ const headerBody = header ? (
255
+ scrollableResolved ? (
256
+ <>
257
+ <DialogLargeTitle controller={headerController} header={header} />
258
+ <DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
259
+ </>
260
+ ) : (
261
+ <>
262
+ <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
263
+ <DialogHeaderProvider controller={headerController}>{bodyNode}</DialogHeaderProvider>
264
+ </>
265
+ )
266
+ ) : null;
267
+
237
268
  if (isSide) {
238
269
  return (
239
270
  <Context.Provider value={context}>
@@ -241,6 +272,7 @@ function CenteredOrSideDialog({
241
272
  open={sideOpen}
242
273
  onDismiss={handleDismiss}
243
274
  side={placement}
275
+ layer={layer}
244
276
  width={width}
245
277
  inset={inset}
246
278
  dismissOnBackdrop={dismissOnBackdrop}
@@ -251,6 +283,9 @@ function CenteredOrSideDialog({
251
283
  description={description}
252
284
  titleId={titleId}
253
285
  descriptionId={descriptionId}
286
+ header={header}
287
+ headerController={headerController}
288
+ onHeaderDismiss={close}
254
289
  panelStyle={panelStyle}
255
290
  panelClassName={panelClassName}
256
291
  containerStyle={containerStyle}
@@ -269,7 +304,21 @@ function CenteredOrSideDialog({
269
304
  onDismiss={handleDismiss}
270
305
  enablePanDownToClose
271
306
  detached
272
- showHandle
307
+ // Nav-header mode hides the drag handle (the nav bar sits at the very top)
308
+ // and coordinates the body pan with the scroll so content scrolls.
309
+ showHandle={!header}
310
+ manualActivation={!!header}
311
+ scrollY={header ? headerController.scrollY : undefined}
312
+ headerOverlay={
313
+ header ? (
314
+ <DialogNavHeader
315
+ controller={headerController}
316
+ header={header}
317
+ onDismiss={close}
318
+ collapse={scrollableResolved}
319
+ />
320
+ ) : undefined
321
+ }
273
322
  // Stronger dim when a Dialog is stacked over another sheet so the
274
323
  // underlying sheet's handle/content doesn't bleed through.
275
324
  backdropOpacity={0.7}
@@ -286,13 +335,14 @@ function CenteredOrSideDialog({
286
335
  // Detached BottomSheet already adds `marginBottom: insets.bottom + 16`
287
336
  // to the sheet container — the floating card sits ABOVE the
288
337
  // system gesture bar, so we don't add `insets.bottom` here.
289
- { padding: contentPadding },
338
+ // Nav-header mode: the large title + screens own their padding.
339
+ header ? null : { padding: contentPadding },
290
340
  { backgroundColor: theme.colors.background },
291
341
  style,
292
342
  panelStyle,
293
343
  ]}
294
344
  >
295
- {bodyNode}
345
+ {header ? headerBody : bodyNode}
296
346
  </View>
297
347
  </Context.Provider>
298
348
  </BottomSheet>
@@ -311,6 +361,7 @@ function SideSheet({
311
361
  open,
312
362
  onDismiss,
313
363
  side,
364
+ layer,
314
365
  width,
315
366
  inset,
316
367
  dismissOnBackdrop,
@@ -321,6 +372,9 @@ function SideSheet({
321
372
  description,
322
373
  titleId,
323
374
  descriptionId,
375
+ header,
376
+ headerController,
377
+ onHeaderDismiss,
324
378
  panelStyle,
325
379
  panelClassName,
326
380
  containerStyle,
@@ -331,6 +385,7 @@ function SideSheet({
331
385
  open: boolean;
332
386
  onDismiss: () => void;
333
387
  side: 'left' | 'right';
388
+ layer?: number;
334
389
  width: number;
335
390
  inset?: DialogInset;
336
391
  dismissOnBackdrop: boolean;
@@ -341,6 +396,9 @@ function SideSheet({
341
396
  description?: string;
342
397
  titleId: string;
343
398
  descriptionId: string;
399
+ header?: DialogProps['header'];
400
+ headerController: DialogHeaderController;
401
+ onHeaderDismiss: () => void;
344
402
  panelStyle?: StyleProp<ViewStyle>;
345
403
  panelClassName?: string;
346
404
  containerStyle?: StyleProp<ViewStyle>;
@@ -433,7 +491,13 @@ function SideSheet({
433
491
 
434
492
  return (
435
493
  <View
436
- style={[sideStyles.root, containerStyle]}
494
+ style={[
495
+ sideStyles.root,
496
+ // Per-layer offset so a side dialog stacked on top of another (surface
497
+ // stack) paints above it. Layer 0 → offset 0 → unchanged.
498
+ { zIndex: Z_INDEX.fullscreen + Z_INDEX_LAYER_STEP * (layer ?? 0) },
499
+ containerStyle,
500
+ ]}
437
501
  {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
438
502
  pointerEvents="box-none"
439
503
  >
@@ -466,16 +530,33 @@ function SideSheet({
466
530
  ]}
467
531
  pointerEvents="auto"
468
532
  >
469
- <View style={{ padding: contentPadding }}>{children}</View>
533
+ {header ? (
534
+ // Nav-header mode on a side drawer: a static titled bar (the drawer body
535
+ // has no Dialog scroll offset to drive a collapse) over content inset
536
+ // below the bar.
537
+ <View style={{ flex: 1, minHeight: 0 }}>
538
+ <DialogNavHeader
539
+ controller={headerController}
540
+ header={header}
541
+ onDismiss={onHeaderDismiss}
542
+ collapse={false}
543
+ />
544
+ <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
545
+ <DialogHeaderProvider controller={headerController}>{children}</DialogHeaderProvider>
546
+ </View>
547
+ ) : (
548
+ <View style={{ padding: contentPadding }}>{children}</View>
549
+ )}
470
550
  </Animated.View>
471
551
  </View>
472
552
  );
473
553
  }
474
554
 
475
555
  const sideStyles = StyleSheet.create({
556
+ // `zIndex` is applied inline (`Z_INDEX.fullscreen + layer offset`) so a side
557
+ // dialog stacked on top of another paints above it in the surface stack.
476
558
  root: {
477
559
  ...StyleSheet.absoluteFill,
478
- zIndex: Z_INDEX.fullscreen,
479
560
  },
480
561
  backdrop: {
481
562
  ...StyleSheet.absoluteFill,
@@ -10,6 +10,7 @@ import React, {
10
10
  } from 'react';
11
11
  import {
12
12
  Pressable,
13
+ ScrollView,
13
14
  useWindowDimensions,
14
15
  View,
15
16
  type StyleProp,
@@ -20,9 +21,16 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
20
21
  import { Portal } from '../portal/index.web';
21
22
  import { createOverlayZIndex } from '../styles/z-index';
22
23
  import { useTheme } from '../theme/use-theme';
23
- import { Context, useDialogControl } from './context';
24
+ import { Context, useDialogContext, useDialogControl } from './context';
24
25
  import { DialogBody } from './DialogContent';
25
26
  import { DialogBottomSheet } from './DialogBottomSheet';
27
+ import {
28
+ DIALOG_NAV_BAR_HEIGHT,
29
+ DialogHeaderProvider,
30
+ DialogLargeTitle,
31
+ DialogNavHeader,
32
+ useDialogHeaderController,
33
+ } from './DialogHeader';
26
34
  import {
27
35
  ANIMATION_DURATION,
28
36
  CENTER_FADE_OUT_DURATION,
@@ -44,7 +52,6 @@ import type {
44
52
  } from './types';
45
53
 
46
54
  const FADE_OUT_DURATION = CENTER_FADE_OUT_DURATION;
47
- const dialogZIndex = createOverlayZIndex();
48
55
 
49
56
  const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagation();
50
57
 
@@ -126,17 +133,22 @@ export function Dialog({ placement, ...rest }: DialogProps) {
126
133
  function CenterOrSideDialog({
127
134
  control,
128
135
  open: controlledOpen,
136
+ startOpen,
129
137
  onClose,
130
138
  testID,
131
139
  title,
132
140
  description,
133
141
  actions,
142
+ header,
134
143
  placement,
144
+ layer,
135
145
  width = DEFAULT_SIDE_WIDTH,
136
146
  maxWidth = DEFAULT_CENTER_MAX_WIDTH,
137
147
  inset,
138
148
  dismissOnBackdrop = true,
139
149
  contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
150
+ maxHeightRatio,
151
+ scrollable,
140
152
  style,
141
153
  panelStyle,
142
154
  panelClassName,
@@ -150,12 +162,23 @@ function CenterOrSideDialog({
150
162
  // stylesheet is present by the time the animated surface mounts).
151
163
  useDialogCss();
152
164
 
165
+ // Per-layer overlay z-indices, so a dialog presented on top of another (via
166
+ // the surface stack) paints above it. Defaults to layer 0 — the offset is 0,
167
+ // so a lone dialog's z is byte-for-byte unchanged.
168
+ const dialogZIndex = useMemo(() => createOverlayZIndex(layer ?? 0), [layer]);
169
+
153
170
  // Controlled mode is opt-in: when `open` is a boolean the host owns the
154
171
  // visible state; otherwise the legacy imperative `control` path drives it.
155
172
  const isControlled = controlledOpen !== undefined;
156
173
  const resolvedPlacement = placement;
157
174
 
158
- const [isOpen, setIsOpen] = useState(false);
175
+ // Seed the initial visible state so a branch that mounts while the surface is
176
+ // already open (the imperative `startOpen` intent) or under a truthy
177
+ // controlled `open` renders open on its FIRST commit — this is what lets a
178
+ // responsive Dialog survive a placement swap (centered card ↔ bottom sheet on
179
+ // resize) without going blank. `startOpen` never flips the close semantics;
180
+ // the imperative `control.close()` still drives the exit + post-exit onClose.
181
+ const [isOpen, setIsOpen] = useState(() => controlledOpen ?? startOpen ?? false);
159
182
  const [isClosing, setIsClosing] = useState(false);
160
183
  const closeCallbacksRef = useRef<(() => void)[]>([]);
161
184
  // Read the latest controlled flag inside stable callbacks without re-binding.
@@ -294,9 +317,13 @@ function CenterOrSideDialog({
294
317
  title={title}
295
318
  description={description}
296
319
  actions={actions}
320
+ header={header}
297
321
  style={style}
298
322
  maxWidth={maxWidth}
299
323
  contentPadding={contentPadding}
324
+ maxHeightRatio={maxHeightRatio}
325
+ scrollable={scrollable}
326
+ surfaceZIndex={dialogZIndex.surface}
300
327
  isClosing={isClosing}
301
328
  >
302
329
  {children}
@@ -319,12 +346,16 @@ function CenterOrSideDialog({
319
346
  title={title}
320
347
  description={description}
321
348
  actions={actions}
349
+ header={header}
350
+ scrollable={scrollable}
322
351
  placement={resolvedPlacement}
323
352
  shown={!isClosing}
324
353
  width={width}
325
354
  inset={inset}
326
355
  dismissOnBackdrop={dismissOnBackdrop}
327
356
  contentPadding={contentPadding}
357
+ backdropZIndex={dialogZIndex.backdrop}
358
+ surfaceZIndex={dialogZIndex.surface}
328
359
  onDismiss={close}
329
360
  panelStyle={panelStyle}
330
361
  panelClassName={panelClassName}
@@ -346,9 +377,13 @@ function DialogPanel({
346
377
  title,
347
378
  description,
348
379
  actions,
380
+ header,
349
381
  style,
350
382
  maxWidth,
351
383
  contentPadding,
384
+ maxHeightRatio,
385
+ scrollable,
386
+ surfaceZIndex,
352
387
  isClosing,
353
388
  children,
354
389
  }: {
@@ -357,15 +392,33 @@ function DialogPanel({
357
392
  title?: string;
358
393
  description?: string;
359
394
  actions?: DialogAction[];
395
+ header?: DialogProps['header'];
360
396
  style?: DialogProps['style'];
361
397
  maxWidth: number;
362
398
  contentPadding: number;
399
+ maxHeightRatio?: number;
400
+ scrollable?: boolean;
401
+ surfaceZIndex: number;
363
402
  isClosing: boolean;
364
403
  children?: React.ReactNode;
365
404
  }) {
366
405
  const theme = useTheme();
406
+ const { close } = useDialogContext();
367
407
  const titleId = useId();
368
408
  const descriptionId = useId();
409
+ const headerController = useDialogHeaderController();
410
+
411
+ const body = (
412
+ <DialogBody
413
+ titleId={titleId}
414
+ descriptionId={descriptionId}
415
+ title={title}
416
+ description={description}
417
+ actions={actions}
418
+ >
419
+ {children}
420
+ </DialogBody>
421
+ );
369
422
 
370
423
  return (
371
424
  <View
@@ -383,6 +436,13 @@ function DialogPanel({
383
436
  borderRadius: 20,
384
437
  width: '100%',
385
438
  maxWidth,
439
+ // The Dialog OWNS the size cap + scroll boundary (its content renders
440
+ // bare, edge-to-edge). Short content stays natural; tall content is
441
+ // capped at `maxHeightRatio` of the viewport and scrolls inside the
442
+ // rounded card (via the ScrollView below) — consumers never add their
443
+ // own height cap / ScrollView. `overflow: hidden` clips to the radius.
444
+ maxHeight: `${Math.round((maxHeightRatio ?? 0.9) * 100)}%`,
445
+ overflow: 'hidden',
386
446
  backgroundColor: theme.colors.background,
387
447
  borderWidth: 1,
388
448
  borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
@@ -390,8 +450,7 @@ function DialogPanel({
390
450
  shadowOpacity: theme.isDark ? 0.4 : 0.1,
391
451
  shadowRadius: 30,
392
452
  shadowOffset: { width: 0, height: 4 },
393
- padding: contentPadding,
394
- zIndex: dialogZIndex.surface,
453
+ zIndex: surfaceZIndex,
395
454
  },
396
455
  isClosing
397
456
  ? ({ animation: `bloomDialogZoomFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
@@ -399,15 +458,56 @@ function DialogPanel({
399
458
  style,
400
459
  ]}
401
460
  >
402
- <DialogBody
403
- titleId={titleId}
404
- descriptionId={descriptionId}
405
- title={title}
406
- description={description}
407
- actions={actions}
408
- >
409
- {children}
410
- </DialogBody>
461
+ {header ? (
462
+ // Nav-header mode: the Dialog OWNS a sticky gradient nav bar + a large
463
+ // collapsing title over its own scroll content (see `DialogHeader`). The
464
+ // bar overlays; the large title + screen body scroll under it, feeding
465
+ // the shared scroll offset that drives the small-title cross-fade.
466
+ <>
467
+ <DialogNavHeader
468
+ controller={headerController}
469
+ header={header}
470
+ onDismiss={close}
471
+ collapse={scrollable !== false}
472
+ />
473
+ {scrollable === false ? (
474
+ <View style={{ flex: 1, minHeight: 0 }}>
475
+ <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
476
+ <DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
477
+ </View>
478
+ ) : (
479
+ <ScrollView
480
+ style={{ flexShrink: 1 }}
481
+ contentContainerStyle={{ paddingBottom: contentPadding }}
482
+ showsVerticalScrollIndicator={false}
483
+ scrollEventThrottle={16}
484
+ onScroll={(e) => {
485
+ headerController.scrollY.value = e.nativeEvent.contentOffset.y;
486
+ }}
487
+ >
488
+ <DialogLargeTitle controller={headerController} header={header} />
489
+ <DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
490
+ </ScrollView>
491
+ )}
492
+ </>
493
+ ) : scrollable === false ? (
494
+ // The content owns its own scrolling (a FlatList / VirtualizedList or a
495
+ // nested ScrollView). Render it bare in a bounded flex box — the card's
496
+ // `maxHeight` + `overflow: hidden` cap it and `flex: 1` + `minHeight: 0`
497
+ // hand the child a bounded height to scroll into — with NO wrapping
498
+ // ScrollView, so a VirtualizedList never nests inside one.
499
+ <View style={{ flex: 1, minHeight: 0, padding: contentPadding }}>
500
+ {body}
501
+ </View>
502
+ ) : (
503
+ <ScrollView
504
+ style={{ flexShrink: 1 }}
505
+ contentContainerStyle={{ padding: contentPadding }}
506
+ showsVerticalScrollIndicator={false}
507
+ >
508
+ {body}
509
+ </ScrollView>
510
+ )}
411
511
  </View>
412
512
  );
413
513
  }
@@ -429,12 +529,16 @@ function SheetSurface({
429
529
  title,
430
530
  description,
431
531
  actions,
532
+ header,
533
+ scrollable,
432
534
  placement,
433
535
  shown,
434
536
  width,
435
537
  inset,
436
538
  dismissOnBackdrop,
437
539
  contentPadding,
540
+ backdropZIndex,
541
+ surfaceZIndex,
438
542
  onDismiss,
439
543
  panelStyle,
440
544
  panelClassName,
@@ -448,12 +552,16 @@ function SheetSurface({
448
552
  title?: string;
449
553
  description?: string;
450
554
  actions?: DialogAction[];
555
+ header?: DialogProps['header'];
556
+ scrollable?: boolean;
451
557
  placement: 'left' | 'right';
452
558
  shown: boolean;
453
559
  width: number;
454
560
  inset?: DialogInset;
455
561
  dismissOnBackdrop: boolean;
456
562
  contentPadding: number;
563
+ backdropZIndex: number;
564
+ surfaceZIndex: number;
457
565
  onDismiss: () => void;
458
566
  panelStyle?: StyleProp<ViewStyle>;
459
567
  panelClassName?: string;
@@ -465,6 +573,7 @@ function SheetSurface({
465
573
  const theme = useTheme();
466
574
  const titleId = useId();
467
575
  const descriptionId = useId();
576
+ const headerController = useDialogHeaderController();
468
577
  const { width: viewportWidth } = useWindowDimensions();
469
578
 
470
579
  // Defer the entry transition by a frame so the start state paints before the
@@ -532,7 +641,7 @@ function SheetSurface({
532
641
 
533
642
  return (
534
643
  <View
535
- style={[sheetStyles.root, containerStyle]}
644
+ style={[sheetStyles.root, { zIndex: backdropZIndex }, containerStyle]}
536
645
  {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
537
646
  pointerEvents="box-none"
538
647
  >
@@ -562,7 +671,11 @@ function SheetSurface({
562
671
  {...(panelClassName ? ({ className: panelClassName } as Record<string, string>) : {})}
563
672
  style={[
564
673
  sheetStyles.panel,
565
- { backgroundColor: theme.colors.background, shadowColor: theme.colors.shadow },
674
+ {
675
+ backgroundColor: theme.colors.background,
676
+ shadowColor: theme.colors.shadow,
677
+ zIndex: surfaceZIndex,
678
+ },
566
679
  panelGeometry,
567
680
  panelTransition,
568
681
  panelStyle,
@@ -570,17 +683,53 @@ function SheetSurface({
570
683
  ]}
571
684
  pointerEvents="auto"
572
685
  >
573
- <View style={{ padding: contentPadding }}>
574
- <DialogBody
575
- titleId={titleId}
576
- descriptionId={descriptionId}
577
- title={title}
578
- description={description}
579
- actions={actions}
580
- >
581
- {children}
582
- </DialogBody>
583
- </View>
686
+ {header ? (
687
+ // Nav-header mode on a side drawer: a static titled bar (the drawer
688
+ // body does not own a Dialog scroll offset to drive a collapse) over
689
+ // the content, which is inset below the bar.
690
+ <View style={{ flex: 1, minHeight: 0 }}>
691
+ <DialogNavHeader
692
+ controller={headerController}
693
+ header={header}
694
+ onDismiss={onDismiss}
695
+ collapse={false}
696
+ />
697
+ <ScrollView
698
+ style={{ flexShrink: 1 }}
699
+ contentContainerStyle={{ paddingTop: DIALOG_NAV_BAR_HEIGHT, paddingBottom: contentPadding }}
700
+ showsVerticalScrollIndicator={false}
701
+ scrollEventThrottle={16}
702
+ scrollEnabled={scrollable !== false}
703
+ onScroll={(e) => {
704
+ headerController.scrollY.value = e.nativeEvent.contentOffset.y;
705
+ }}
706
+ >
707
+ <DialogHeaderProvider controller={headerController}>
708
+ <DialogBody
709
+ titleId={titleId}
710
+ descriptionId={descriptionId}
711
+ title={title}
712
+ description={description}
713
+ actions={actions}
714
+ >
715
+ {children}
716
+ </DialogBody>
717
+ </DialogHeaderProvider>
718
+ </ScrollView>
719
+ </View>
720
+ ) : (
721
+ <View style={{ padding: contentPadding }}>
722
+ <DialogBody
723
+ titleId={titleId}
724
+ descriptionId={descriptionId}
725
+ title={title}
726
+ description={description}
727
+ actions={actions}
728
+ >
729
+ {children}
730
+ </DialogBody>
731
+ </View>
732
+ )}
584
733
  </View>
585
734
  </View>
586
735
  );
@@ -659,13 +808,15 @@ export function AutoMountedDialog({
659
808
  }
660
809
 
661
810
  const sheetStyles = {
811
+ // `zIndex` for the root (backdrop) and panel (surface) is applied inline from
812
+ // the per-layer `createOverlayZIndex(layer)` in `SheetSurface`, so a side
813
+ // dialog stacked on top of another (surface stack) paints above it.
662
814
  root: {
663
815
  position: 'fixed' as 'absolute',
664
816
  top: 0,
665
817
  left: 0,
666
818
  right: 0,
667
819
  bottom: 0,
668
- zIndex: dialogZIndex.backdrop,
669
820
  } as ViewStyle,
670
821
  backdrop: {
671
822
  position: 'absolute',
@@ -681,7 +832,6 @@ const sheetStyles = {
681
832
  shadowOpacity: 0.18,
682
833
  shadowRadius: 24,
683
834
  shadowOffset: { width: 0, height: 8 },
684
- zIndex: dialogZIndex.surface,
685
835
  } as ViewStyle,
686
836
  };
687
837