@oxyhq/bloom 0.46.0 → 0.49.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 (160) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +36 -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 +103 -8
  8. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  9. package/lib/commonjs/dialog/Dialog.web.js +189 -12
  10. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  11. package/lib/commonjs/dialog/DialogBottomSheet.js +111 -31
  12. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  13. package/lib/commonjs/dialog/DialogHeader.js +371 -0
  14. package/lib/commonjs/dialog/DialogHeader.js.map +1 -0
  15. package/lib/commonjs/dialog/DialogMorph.js +458 -0
  16. package/lib/commonjs/dialog/DialogMorph.js.map +1 -0
  17. package/lib/commonjs/dialog/index.js +14 -0
  18. package/lib/commonjs/dialog/index.js.map +1 -1
  19. package/lib/commonjs/dialog/index.web.js +14 -0
  20. package/lib/commonjs/dialog/index.web.js.map +1 -1
  21. package/lib/commonjs/index.js +11 -4
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/index.web.js +11 -4
  24. package/lib/commonjs/index.web.js.map +1 -1
  25. package/lib/commonjs/react-native-className.d.js +6 -0
  26. package/lib/commonjs/react-native-className.d.js.map +1 -0
  27. package/lib/commonjs/{search-input → search}/index.js +5 -3
  28. package/lib/commonjs/search/index.js.map +1 -0
  29. package/lib/commonjs/settings-list/SettingsList.js +6 -2
  30. package/lib/commonjs/settings-list/SettingsList.js.map +1 -1
  31. package/lib/commonjs/surfaces/SurfaceHost.js +23 -9
  32. package/lib/commonjs/surfaces/SurfaceHost.js.map +1 -1
  33. package/lib/commonjs/text-field/index.js +22 -6
  34. package/lib/commonjs/text-field/index.js.map +1 -1
  35. package/lib/module/bottom-sheet/BottomSheetBase.js +36 -12
  36. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  37. package/lib/module/combobox/Combobox.js +3 -3
  38. package/lib/module/combobox/Combobox.js.map +1 -1
  39. package/lib/module/command/Command.js +3 -3
  40. package/lib/module/command/Command.js.map +1 -1
  41. package/lib/module/dialog/Dialog.js +104 -9
  42. package/lib/module/dialog/Dialog.js.map +1 -1
  43. package/lib/module/dialog/Dialog.web.js +192 -16
  44. package/lib/module/dialog/Dialog.web.js.map +1 -1
  45. package/lib/module/dialog/DialogBottomSheet.js +113 -33
  46. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  47. package/lib/module/dialog/DialogHeader.js +362 -0
  48. package/lib/module/dialog/DialogHeader.js.map +1 -0
  49. package/lib/module/dialog/DialogMorph.js +451 -0
  50. package/lib/module/dialog/DialogMorph.js.map +1 -0
  51. package/lib/module/dialog/index.js +2 -0
  52. package/lib/module/dialog/index.js.map +1 -1
  53. package/lib/module/dialog/index.web.js +2 -0
  54. package/lib/module/dialog/index.web.js.map +1 -1
  55. package/lib/module/index.js +2 -2
  56. package/lib/module/index.js.map +1 -1
  57. package/lib/module/index.web.js +2 -2
  58. package/lib/module/index.web.js.map +1 -1
  59. package/lib/module/react-native-className.d.js +20 -0
  60. package/lib/module/react-native-className.d.js.map +1 -0
  61. package/lib/module/{search-input → search}/index.js +6 -2
  62. package/lib/module/search/index.js.map +1 -0
  63. package/lib/module/settings-list/SettingsList.js +6 -2
  64. package/lib/module/settings-list/SettingsList.js.map +1 -1
  65. package/lib/module/surfaces/SurfaceHost.js +23 -9
  66. package/lib/module/surfaces/SurfaceHost.js.map +1 -1
  67. package/lib/module/text-field/index.js +22 -6
  68. package/lib/module/text-field/index.js.map +1 -1
  69. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +31 -1
  70. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/combobox/Combobox.d.ts +1 -1
  72. package/lib/typescript/commonjs/command/Command.d.ts +1 -1
  73. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +2 -2
  76. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +86 -0
  78. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -0
  79. package/lib/typescript/commonjs/dialog/DialogMorph.d.ts +150 -0
  80. package/lib/typescript/commonjs/dialog/DialogMorph.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/dialog/index.d.ts +4 -1
  82. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/dialog/index.web.d.ts +4 -1
  84. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/dialog/types.d.ts +105 -0
  86. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/icons/common.d.ts +1 -0
  88. package/lib/typescript/commonjs/icons/common.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/index.d.ts +3 -3
  90. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/index.web.d.ts +3 -3
  92. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/input-group/types.d.ts +1 -1
  94. package/lib/typescript/commonjs/{search-input → search}/index.d.ts +1 -1
  95. package/lib/typescript/commonjs/search/index.d.ts.map +1 -0
  96. package/lib/typescript/commonjs/surfaces/SurfaceHost.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/surfaces/types.d.ts +26 -1
  98. package/lib/typescript/commonjs/surfaces/types.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/text-field/index.d.ts +2 -1
  100. package/lib/typescript/commonjs/text-field/index.d.ts.map +1 -1
  101. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +31 -1
  102. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  103. package/lib/typescript/module/combobox/Combobox.d.ts +1 -1
  104. package/lib/typescript/module/command/Command.d.ts +1 -1
  105. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  106. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  107. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +2 -2
  108. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  109. package/lib/typescript/module/dialog/DialogHeader.d.ts +86 -0
  110. package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -0
  111. package/lib/typescript/module/dialog/DialogMorph.d.ts +150 -0
  112. package/lib/typescript/module/dialog/DialogMorph.d.ts.map +1 -0
  113. package/lib/typescript/module/dialog/index.d.ts +4 -1
  114. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  115. package/lib/typescript/module/dialog/index.web.d.ts +4 -1
  116. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  117. package/lib/typescript/module/dialog/types.d.ts +105 -0
  118. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  119. package/lib/typescript/module/icons/common.d.ts +1 -0
  120. package/lib/typescript/module/icons/common.d.ts.map +1 -1
  121. package/lib/typescript/module/index.d.ts +3 -3
  122. package/lib/typescript/module/index.d.ts.map +1 -1
  123. package/lib/typescript/module/index.web.d.ts +3 -3
  124. package/lib/typescript/module/index.web.d.ts.map +1 -1
  125. package/lib/typescript/module/input-group/types.d.ts +1 -1
  126. package/lib/typescript/module/{search-input → search}/index.d.ts +1 -1
  127. package/lib/typescript/module/search/index.d.ts.map +1 -0
  128. package/lib/typescript/module/surfaces/SurfaceHost.d.ts.map +1 -1
  129. package/lib/typescript/module/surfaces/types.d.ts +26 -1
  130. package/lib/typescript/module/surfaces/types.d.ts.map +1 -1
  131. package/lib/typescript/module/text-field/index.d.ts +2 -1
  132. package/lib/typescript/module/text-field/index.d.ts.map +1 -1
  133. package/package.json +8 -8
  134. package/src/__tests__/BottomSheet.test.tsx +47 -0
  135. package/src/__tests__/DialogBottomSheet.test.tsx +30 -5
  136. package/src/__tests__/DialogMorph.test.tsx +230 -0
  137. package/src/bottom-sheet/BottomSheetBase.tsx +72 -11
  138. package/src/combobox/Combobox.tsx +3 -3
  139. package/src/command/Command.tsx +3 -3
  140. package/src/dialog/Dialog.tsx +111 -14
  141. package/src/dialog/Dialog.web.tsx +186 -25
  142. package/src/dialog/DialogBottomSheet.tsx +122 -30
  143. package/src/dialog/DialogHeader.tsx +429 -0
  144. package/src/dialog/DialogMorph.tsx +576 -0
  145. package/src/dialog/index.ts +4 -0
  146. package/src/dialog/index.web.ts +4 -0
  147. package/src/dialog/types.ts +106 -0
  148. package/src/index.ts +3 -1
  149. package/src/index.web.ts +3 -1
  150. package/src/input-group/types.ts +1 -1
  151. package/src/react-native-className.d.ts +38 -0
  152. package/src/{search-input → search}/index.tsx +8 -5
  153. package/src/settings-list/SettingsList.tsx +6 -2
  154. package/src/surfaces/SurfaceHost.tsx +17 -1
  155. package/src/surfaces/types.ts +26 -1
  156. package/src/text-field/index.tsx +24 -5
  157. package/lib/commonjs/search-input/index.js.map +0 -1
  158. package/lib/module/search-input/index.js.map +0 -1
  159. package/lib/typescript/commonjs/search-input/index.d.ts.map +0 -1
  160. package/lib/typescript/module/search-input/index.d.ts.map +0 -1
@@ -19,7 +19,7 @@ export interface InputGroupAddonProps {
19
19
  export interface InputGroupProps {
20
20
  /**
21
21
  * Group children. Place `InputGroup.Addon` elements before/after the input
22
- * (a Bloom `TextFieldInput`, a `SearchInput`, or any control). The middle
22
+ * (a Bloom `TextFieldInput`, a `Search`, or any control). The middle
23
23
  * child stretches to fill.
24
24
  */
25
25
  children: React.ReactNode;
@@ -1,6 +1,6 @@
1
1
  import { type TextInput } from 'react-native';
2
2
  import { type TextFieldInputProps } from '../text-field';
3
- export declare const SearchInput: import("react").ForwardRefExoticComponent<Omit<TextFieldInputProps, "label"> & {
3
+ export declare const Search: import("react").ForwardRefExoticComponent<Omit<TextFieldInputProps, "label"> & {
4
4
  label?: TextFieldInputProps["label"];
5
5
  /**
6
6
  * Called when the user presses the clear (X) button.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/search/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAAY,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAK9D,OAAO,EAA4C,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAenG,eAAO,MAAM,MAAM;YAPT,mBAAmB,CAAC,OAAO,CAAC;IACpC;;OAEG;kBACW,MAAM,IAAI;6CAwDzB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AA6BxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBAwFhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
1
+ {"version":3,"file":"SurfaceHost.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/SurfaceHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAKN,MAAM,OAAO,CAAC;AAIf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAYnD,KAAK,eAAe,GAAG,KAAK,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AAgCxD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe;;;;;;uBAqGhB;YAAE,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;SAAE;;;EAcrE"}
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { StyleProp, ViewStyle } from 'react-native';
3
- import type { DialogInset, ResponsiveDialogPlacement } from '../dialog/types';
3
+ import type { DialogHeaderConfig, DialogInset, ResponsiveDialogPlacement } from '../dialog/types';
4
4
  /**
5
5
  * How a surface renders — the placement + the `Dialog` sizing/chrome options a
6
6
  * surface needs. This is the CONTENT-AGNOSTIC presentation config: it never
@@ -37,6 +37,31 @@ export interface SurfacePresentation {
37
37
  dismissOnBackdrop?: boolean;
38
38
  /** Inner padding (px) of the surface content container. Forwarded to `Dialog`. */
39
39
  contentPadding?: number;
40
+ /**
41
+ * Turns on the Dialog's OWN navigation header for this surface (sticky gradient
42
+ * nav bar + large collapsing title over the surface's scroll content). Seeds
43
+ * the header content; a mounted screen refines it at runtime via
44
+ * `useDialogHeader`. Omit for a plain surface (a confirm/alert dialog). Forwarded
45
+ * to `Dialog`.
46
+ */
47
+ header?: DialogHeaderConfig;
48
+ /**
49
+ * Whether the surface wraps its content in an internal scroll container.
50
+ * Defaults to `true`. Set `false` when the surface's content owns its own
51
+ * scrolling primitive (a `FlatList` / `SectionList` / VirtualizedList, or its
52
+ * own `ScrollView`) so the host does not nest it in a ScrollView. Forwarded
53
+ * to `Dialog`.
54
+ */
55
+ scrollable?: boolean;
56
+ /**
57
+ * Whether the surface MORPHS its panel when its content is swapped in place —
58
+ * navigation WITHIN this surface, as opposed to presenting a new surface on
59
+ * top. Defaults to `true`. The content signals the swap through
60
+ * `useDialogFrame`; a surface whose content never declares a frame is
61
+ * unaffected. Set `false` for content that must never be resized into (a
62
+ * full-bleed picker). Forwarded to `Dialog`.
63
+ */
64
+ morph?: boolean;
40
65
  /** Style overrides applied to the inner content container. */
41
66
  style?: StyleProp<ViewStyle>;
42
67
  /** Style overrides for the side/bottom placement panel surface. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/surfaces/types.ts"],"names":[],"mappings":"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"}
@@ -3,8 +3,9 @@ import { type TextStyleProp, type ViewStyleProp } from '../styles';
3
3
  import { type Props as SVGIconProps } from '../icons/common';
4
4
  export type TextFieldProps = React.PropsWithChildren<{
5
5
  isInvalid?: boolean;
6
+ radius?: number;
6
7
  } & ViewStyleProp>;
7
- export declare function TextField({ children, isInvalid, style }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
8
+ export declare function TextField({ children, isInvalid, radius, style }: TextFieldProps): import("react/jsx-runtime").JSX.Element;
8
9
  export declare function useSharedInputStyles(): {
9
10
  chromeHover: ViewStyle;
10
11
  chromeFocus: ViewStyle;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/text-field/index.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,kBAAkB,EAIvB,SAAS,EACT,KAAK,cAAc,EAGnB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAMtB,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAkB,KAAK,KAAK,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAwB7E,MAAM,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,CAClD;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,aAAa,CACxC,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAiB,EAAE,KAAK,EAAE,EAAE,cAAc,2CA0C/E;AAED,wBAAgB,oBAAoB;;;;;EA0BnC;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,cAAc,EACd,OAAO,GAAG,cAAc,GAAG,aAAa,CACzC,GAAG;IACF,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,WAAW,EACX,KAAK,EACL,YAAY,EACZ,OAAO,EACP,MAAM,EACN,SAAS,EACT,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,GAAG,IAAI,EACR,EAAE,mBAAmB,2CA4HrB;AA4KD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,GACT,EAAE,KAAK,CAAC,iBAAiB,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,2CAchD;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,YAAY,CAAC,CAAA;CAAE,2CA8BxF;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,KAAK,EACL,iBAAiB,EACjB,KAAK,GACN,EAAE,KAAK,CAAC,iBAAiB,CACxB,aAAa,GAAG;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;CAC7D,CACF,2CAqBA;AAED,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,2CAkDA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/text-field/index.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,kBAAkB,EAIvB,SAAS,EACT,KAAK,cAAc,EAGnB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAMtB,OAAO,EAML,KAAK,aAAa,EAClB,KAAK,aAAa,EACnB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAkB,KAAK,KAAK,IAAI,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAwC7E,MAAM,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,CAClD;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CACzD,CAAC;AAEF,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAiB,EAAE,MAAW,EAAE,KAAK,EAAE,EAAE,cAAc,2CA2C5F;AAED,wBAAgB,oBAAoB;;;;;EA0BnC;AAED,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,cAAc,EACd,OAAO,GAAG,cAAc,GAAG,aAAa,CACzC,GAAG;IACF,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,WAAW,EACX,KAAK,EACL,YAAY,EACZ,OAAO,EACP,MAAM,EACN,SAAS,EACT,QAAQ,EACR,KAAK,EACL,aAAqB,EACrB,GAAG,IAAI,EACR,EAAE,mBAAmB,2CA6HrB;AA6KD,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,GACT,EAAE,KAAK,CAAC,iBAAiB,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,2CAchD;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,KAAK,CAAC,aAAa,CAAC,YAAY,CAAC,CAAA;CAAE,2CA8BxF;AAED,wBAAgB,eAAe,CAAC,EAC9B,QAAQ,EACR,KAAK,EACL,iBAAiB,EACjB,KAAK,GACN,EAAE,KAAK,CAAC,iBAAiB,CACxB,aAAa,GAAG;IACd,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,CAAC,EAAE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC;CAC7D,CACF,2CAqBA;AAED,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,KAAK,GACN,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,2CAkDA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.46.0",
3
+ "version": "0.49.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -617,15 +617,15 @@
617
617
  "default": "./lib/commonjs/segmented-control/index.js"
618
618
  }
619
619
  },
620
- "./search-input": {
621
- "react-native": "./src/search-input/index.tsx",
620
+ "./search": {
621
+ "react-native": "./src/search/index.tsx",
622
622
  "import": {
623
- "types": "./lib/typescript/module/search-input/index.d.ts",
624
- "default": "./lib/module/search-input/index.js"
623
+ "types": "./lib/typescript/module/search/index.d.ts",
624
+ "default": "./lib/module/search/index.js"
625
625
  },
626
626
  "require": {
627
- "types": "./lib/typescript/commonjs/search-input/index.d.ts",
628
- "default": "./lib/commonjs/search-input/index.js"
627
+ "types": "./lib/typescript/commonjs/search/index.d.ts",
628
+ "default": "./lib/commonjs/search/index.js"
629
629
  }
630
630
  },
631
631
  "./admonition": {
@@ -1043,7 +1043,7 @@
1043
1043
  "grid",
1044
1044
  "text-field",
1045
1045
  "segmented-control",
1046
- "search-input",
1046
+ "search",
1047
1047
  "admonition",
1048
1048
  "menu",
1049
1049
  "tooltip",
@@ -248,6 +248,53 @@ describe('BottomSheet', () => {
248
248
  });
249
249
  });
250
250
 
251
+ describe('unmount-while-open does not fire onDismiss (responsive placement swap)', () => {
252
+ it('skips onDismiss when a fully-open sheet is unmounted (yanked, not closing)', () => {
253
+ // A responsive Dialog crossing its breakpoint swaps its bottom-sheet branch
254
+ // for the centered-card branch, unmounting this sheet while it is still
255
+ // fully OPEN. That is NOT a dismissal — firing onDismiss here would tear the
256
+ // surface down mid-swap. The unmount-safety only flushes when a close was
257
+ // already underway (visible=false).
258
+ const onDismiss = jest.fn();
259
+ const ref = createRef<BottomSheetRef>();
260
+ const { unmount } = renderWithTheme(
261
+ <BottomSheet ref={ref} onDismiss={onDismiss}>
262
+ <React.Fragment>Open content</React.Fragment>
263
+ </BottomSheet>,
264
+ );
265
+ act(() => {
266
+ ref.current?.present();
267
+ });
268
+ act(() => {
269
+ unmount();
270
+ });
271
+ expect(onDismiss).not.toHaveBeenCalled();
272
+ });
273
+
274
+ it('still flushes onDismiss when unmounted mid-close (visible already false)', () => {
275
+ // The original guard's purpose: a sheet yanked mid-close-animation must
276
+ // still flush its onDismiss so queued post-close callbacks are not lost.
277
+ const onDismiss = jest.fn();
278
+ const ref = createRef<BottomSheetRef>();
279
+ const { unmount } = renderWithTheme(
280
+ <BottomSheet ref={ref} onDismiss={onDismiss}>
281
+ <React.Fragment>Closing content</React.Fragment>
282
+ </BottomSheet>,
283
+ );
284
+ act(() => {
285
+ ref.current?.present();
286
+ });
287
+ // Start the close (visible → false), then yank before the exit settles.
288
+ act(() => {
289
+ ref.current?.dismiss();
290
+ });
291
+ act(() => {
292
+ unmount();
293
+ });
294
+ expect(onDismiss).toHaveBeenCalledTimes(1);
295
+ });
296
+ });
297
+
251
298
  describe('close generation tracking', () => {
252
299
  it('re-opening the sheet after dismiss does not throw or get stuck', () => {
253
300
  // Regression test for the "tap to open does nothing" bug: when the
@@ -206,7 +206,7 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
206
206
  expect(flattened.padding).toBe(0);
207
207
  });
208
208
 
209
- it('does NOT wrap pure custom children in a second scroll container (no scroll-in-scroll)', () => {
209
+ it('wraps pure custom children in the internal scroll container by default (Dialog owns scroll)', () => {
210
210
  let control: ReturnType<typeof useDialogControl> | undefined;
211
211
  const result = renderWithTheme(
212
212
  <Harness>
@@ -214,7 +214,7 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
214
214
  control = c;
215
215
  return (
216
216
  <Dialog control={c} placement="bottom">
217
- <Text>Custom children own their own scrolling</Text>
217
+ <Text>Custom children scroll inside the sheet by default</Text>
218
218
  </Dialog>
219
219
  );
220
220
  }}
@@ -223,9 +223,34 @@ describe('Dialog bottom placement delegates to BottomSheet', () => {
223
223
  act(() => {
224
224
  control?.open();
225
225
  });
226
- // Pure custom children DialogBottomSheet passes `scrollable={false}` to
227
- // BottomSheet, so the sheet does NOT add its internal ScrollView. A child
228
- // that contains its own scroller is then the ONLY scroll container.
226
+ // Pure custom children default to `scrollable` (undefined → true), so the
227
+ // sheet DOES add its internal ScrollView and the screen — stripped of its
228
+ // own scroller scrolls inside it. The Dialog owns the scroll boundary.
229
+ const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
230
+ expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(1);
231
+ });
232
+
233
+ it('opts custom children OUT of the internal scroll container when scrollable={false}', () => {
234
+ let control: ReturnType<typeof useDialogControl> | undefined;
235
+ const result = renderWithTheme(
236
+ <Harness>
237
+ {(c) => {
238
+ control = c;
239
+ return (
240
+ <Dialog control={c} placement="bottom" scrollable={false}>
241
+ <Text>Owns its own FlatList</Text>
242
+ </Dialog>
243
+ );
244
+ }}
245
+ </Harness>,
246
+ );
247
+ act(() => {
248
+ control?.open();
249
+ });
250
+ // A surface whose content owns its own scroller (a FlatList / VirtualizedList)
251
+ // passes `scrollable={false}`, so the sheet does NOT add its internal
252
+ // ScrollView — the child is then the ONLY scroll container (no scroll-in-
253
+ // scroll, no "VirtualizedList inside a plain ScrollView" warning).
229
254
  const tree: RenderedNode | RenderedNode[] | null = result.toJSON();
230
255
  expect(countNodesByType(tree, 'Animated.ScrollView')).toBe(0);
231
256
  });
@@ -0,0 +1,230 @@
1
+ import React from 'react';
2
+ import { act, render } from '@testing-library/react-native';
3
+ import type { LayoutChangeEvent, ViewStyle } from 'react-native';
4
+
5
+ import {
6
+ DialogMorphContent,
7
+ useDialogFrame,
8
+ useDialogMorph,
9
+ } from '../dialog/DialogMorph';
10
+
11
+ /**
12
+ * Size morphing across an in-place content swap.
13
+ *
14
+ * These assert the STATE MACHINE — when the panel height is taken over and when
15
+ * it is handed back to natural sizing. The reanimated mock settles `withTiming`
16
+ * synchronously, so the frame-by-frame interpolation is NOT observable here; it
17
+ * is verified in a real browser, the only place reanimated's web mapper ticks.
18
+ */
19
+
20
+ const layout = (height: number): LayoutChangeEvent =>
21
+ ({
22
+ nativeEvent: { layout: { x: 0, y: 0, width: 320, height } },
23
+ }) as LayoutChangeEvent;
24
+
25
+ interface HarnessProps {
26
+ morphEnabled?: boolean;
27
+ measurable?: boolean;
28
+ frameKey: string;
29
+ frameMorph?: boolean;
30
+ onState: (state: {
31
+ panelStyle: ViewStyle;
32
+ onPanelLayout: (e: LayoutChangeEvent) => void;
33
+ onContentLayout: (e: LayoutChangeEvent) => void;
34
+ }) => void;
35
+ }
36
+
37
+ /** Drives one surface: measurement in, panel style + a frame-declaring child out. */
38
+ function Harness({
39
+ morphEnabled = true,
40
+ measurable = true,
41
+ frameKey,
42
+ frameMorph,
43
+ onState,
44
+ }: HarnessProps) {
45
+ const morph = useDialogMorph({ enabled: morphEnabled, measurable, maxHeight: 800 });
46
+ onState({
47
+ panelStyle: morph.panelStyle as ViewStyle,
48
+ onPanelLayout: morph.onPanelLayout,
49
+ onContentLayout: morph.onContentLayout,
50
+ });
51
+ return (
52
+ <DialogMorphContent morph={morph}>
53
+ <Frame frameKey={frameKey} frameMorph={frameMorph} />
54
+ </DialogMorphContent>
55
+ );
56
+ }
57
+
58
+ function Frame({ frameKey, frameMorph }: { frameKey: string; frameMorph?: boolean }) {
59
+ useDialogFrame({ key: frameKey, morph: frameMorph });
60
+ return null;
61
+ }
62
+
63
+ /** Mount the harness and return a handle for driving layout + swapping frames. */
64
+ function mountHarness(props: Omit<Partial<HarnessProps>, 'onState' | 'frameKey'> = {}) {
65
+ let state: Parameters<HarnessProps['onState']>[0] | null = null;
66
+ const onState: HarnessProps['onState'] = (next) => {
67
+ state = next;
68
+ };
69
+ const element = (frameKey: string, extra: Omit<Partial<HarnessProps>, 'onState'> = {}) => (
70
+ <Harness frameKey={frameKey} onState={onState} {...props} {...extra} />
71
+ );
72
+ const { rerender, unmount } = render(element('a'));
73
+ const read = () => {
74
+ if (!state) throw new Error('harness did not render');
75
+ return state;
76
+ };
77
+ return {
78
+ /** The panel height the surface currently resolves to. */
79
+ panelHeight: () => read().panelStyle.height,
80
+ /** Report the panel's and then the content's laid-out heights. */
81
+ measure(panelHeight: number, contentHeight: number) {
82
+ act(() => {
83
+ read().onPanelLayout(layout(panelHeight));
84
+ read().onContentLayout(layout(contentHeight));
85
+ });
86
+ },
87
+ /**
88
+ * Report only the content's natural height (the incoming frame settling).
89
+ * Followed by a re-render: the mock's `useAnimatedStyle` only re-evaluates
90
+ * on render, whereas the real one re-runs on the shared-value write.
91
+ */
92
+ measureContent(contentHeight: number, frameKey = 'b') {
93
+ act(() => read().onContentLayout(layout(contentHeight)));
94
+ rerender(element(frameKey));
95
+ },
96
+ /** Declare a frame, then re-render so the resulting panel style is observable. */
97
+ swapTo(frameKey: string, extra: Omit<Partial<HarnessProps>, 'onState'> = {}) {
98
+ rerender(element(frameKey, extra));
99
+ rerender(element(frameKey, extra));
100
+ },
101
+ unmount,
102
+ };
103
+ }
104
+
105
+ describe('Dialog size morphing', () => {
106
+ it('leaves the panel naturally sized until a frame actually changes', () => {
107
+ const h = mountHarness();
108
+ expect(h.panelHeight()).toBe('auto');
109
+
110
+ h.measure(300, 298);
111
+ expect(h.panelHeight()).toBe('auto');
112
+
113
+ // Re-declaring the SAME frame is not a swap.
114
+ h.swapTo('a');
115
+ expect(h.panelHeight()).toBe('auto');
116
+ h.unmount();
117
+ });
118
+
119
+ it('pins the panel at the outgoing size, then releases onto the incoming one', () => {
120
+ const h = mountHarness();
121
+ h.measure(300, 298);
122
+
123
+ h.swapTo('b');
124
+ // Held at the size the outgoing frame was painted at — no jump while the
125
+ // incoming content is still being measured.
126
+ expect(h.panelHeight()).toBe(300);
127
+
128
+ // The incoming frame reports 500 of natural height; the panel carries the
129
+ // 2px of chrome calibrated from the outgoing frame, then hands the height
130
+ // back so later content changes size the panel normally again.
131
+ h.measureContent(500);
132
+ expect(h.panelHeight()).toBe('auto');
133
+ h.unmount();
134
+ });
135
+
136
+ it('releases the pin even when the incoming frame is taller than the bound', () => {
137
+ const h = mountHarness();
138
+ h.measure(300, 300);
139
+ h.swapTo('b');
140
+ expect(h.panelHeight()).toBe(300);
141
+
142
+ h.measureContent(5000);
143
+ expect(h.panelHeight()).toBe('auto');
144
+ h.unmount();
145
+ });
146
+
147
+ it('releases without reshaping when the incoming frame fills the space it is given', () => {
148
+ const h = mountHarness();
149
+ h.measure(300, 300);
150
+ h.swapTo('b');
151
+ expect(h.panelHeight()).toBe(300);
152
+
153
+ // A child that flexes to its container measures ~0 inside a content-sized
154
+ // wrapper; collapsing the panel onto that would empty the surface.
155
+ h.measureContent(0);
156
+ expect(h.panelHeight()).toBe('auto');
157
+ h.unmount();
158
+ });
159
+
160
+ it('never pins when nothing has been measured yet', () => {
161
+ const h = mountHarness();
162
+ h.swapTo('b');
163
+ expect(h.panelHeight()).toBe('auto');
164
+ h.unmount();
165
+ });
166
+
167
+ it('honours the per-frame morph opt-out', () => {
168
+ const h = mountHarness();
169
+ h.measure(300, 300);
170
+ h.swapTo('b', { frameMorph: false });
171
+ expect(h.panelHeight()).toBe('auto');
172
+ h.unmount();
173
+ });
174
+
175
+ it('honours the surface-level morph opt-out', () => {
176
+ const h = mountHarness({ morphEnabled: false });
177
+ h.measure(300, 300);
178
+ h.swapTo('b');
179
+ expect(h.panelHeight()).toBe('auto');
180
+ h.unmount();
181
+ });
182
+
183
+ it('does not pin a surface whose content owns its own size', () => {
184
+ const h = mountHarness({ measurable: false });
185
+ h.measure(300, 300);
186
+ h.swapTo('b');
187
+ expect(h.panelHeight()).toBe('auto');
188
+ h.unmount();
189
+ });
190
+
191
+ it('drives maxWidth only on a placement that can vary it', () => {
192
+ let fixedWidth: ViewStyle | null = null;
193
+ let varyingWidth: ViewStyle | null = null;
194
+ render(
195
+ <WidthHarness
196
+ onStyle={(s) => {
197
+ fixedWidth = s;
198
+ }}
199
+ />,
200
+ );
201
+ render(
202
+ <WidthHarness
203
+ maxWidth={480}
204
+ onStyle={(s) => {
205
+ varyingWidth = s;
206
+ }}
207
+ />,
208
+ );
209
+ expect((fixedWidth as unknown as ViewStyle).maxWidth).toBeUndefined();
210
+ expect((varyingWidth as unknown as ViewStyle).maxWidth).toBe(480);
211
+ });
212
+ });
213
+
214
+ function WidthHarness({
215
+ maxWidth,
216
+ onStyle,
217
+ }: {
218
+ maxWidth?: number;
219
+ onStyle: (style: ViewStyle) => void;
220
+ }) {
221
+ const morph = useDialogMorph({ enabled: true, measurable: true, maxHeight: 800, maxWidth });
222
+ onStyle(morph.panelStyle as ViewStyle);
223
+ return null;
224
+ }
225
+
226
+ describe('useDialogFrame outside a morph-capable surface', () => {
227
+ it('is a no-op (a side sheet, or a plain confirm dialog)', () => {
228
+ expect(() => render(<Frame frameKey="only" />)).not.toThrow();
229
+ });
230
+ });
@@ -6,6 +6,7 @@ import {
6
6
  Pressable,
7
7
  Dimensions,
8
8
  Platform,
9
+ type LayoutChangeEvent,
9
10
  type ViewStyle,
10
11
  type StyleProp,
11
12
  } from 'react-native';
@@ -55,6 +56,16 @@ export interface BottomSheetRef {
55
56
 
56
57
  export interface BottomSheetProps {
57
58
  children: React.ReactNode;
59
+ /**
60
+ * Controlled open state. When provided, the sheet mounts already-open if
61
+ * `true` (seeding its internal visible/rendered state) instead of relying on
62
+ * an imperative `present()` after mount. This is what makes a responsive
63
+ * `Dialog` survive a placement swap (centered card ↔ bottom sheet on resize):
64
+ * the freshly-mounted sheet is visible on its first commit, with no
65
+ * present()-in-effect race that would otherwise leave it blank. Omit for the
66
+ * purely imperative `present()`/`dismiss()` API.
67
+ */
68
+ open?: boolean;
58
69
  onDismiss?: () => void;
59
70
  enablePanDownToClose?: boolean;
60
71
  backgroundComponent?: (props: { style?: StyleProp<ViewStyle> }) => React.ReactElement | null;
@@ -128,6 +139,26 @@ export interface BottomSheetProps {
128
139
  * `showHandle` is `true`.
129
140
  */
130
141
  handleComponent?: () => React.ReactNode;
142
+ /**
143
+ * External shared value the internal scroll handler mirrors the content
144
+ * offset into (in addition to its own gesture-gating offset). Lets a host —
145
+ * e.g. a Dialog nav-header — drive a collapse from the sheet's OWN scroller
146
+ * without owning the ScrollView. Optional; the standalone sheet ignores it.
147
+ */
148
+ scrollY?: SharedValue<number>;
149
+ /**
150
+ * Overlay slot rendered absolutely at the TOP of the sheet card, painted
151
+ * above the scrolling body (clipped to the sheet's rounded top). Used by the
152
+ * Dialog to float its sticky nav header over the sheet's scroll content.
153
+ */
154
+ headerOverlay?: React.ReactNode;
155
+ /**
156
+ * Layout callback for the SHEET CARD — the same box `style` targets. Lets a
157
+ * host observe the card's rendered size (the Dialog measures it to morph the
158
+ * sheet between two content frames). Fires on every size change, including
159
+ * while a host animates the card's height.
160
+ */
161
+ onLayout?: (event: LayoutChangeEvent) => void;
131
162
  }
132
163
 
133
164
  /**
@@ -169,6 +200,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
169
200
  const {
170
201
  Shell,
171
202
  children,
203
+ open,
172
204
  onDismiss,
173
205
  enablePanDownToClose = true,
174
206
  backgroundComponent,
@@ -183,14 +215,20 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
183
215
  manualActivation = false,
184
216
  dynamicBackdrop = false,
185
217
  handleComponent,
218
+ scrollY: externalScrollY,
219
+ headerOverlay,
220
+ onLayout,
186
221
  } = props;
187
222
 
188
223
  const insets = useSafeAreaInsets();
189
224
  const theme = useTheme();
190
225
  const { colors } = theme;
191
226
  const { height: screenHeight } = useScreenDimensions();
192
- const [visible, setVisible] = useState(false);
193
- const [rendered, setRendered] = useState(false); // keep mounted for exit animation
227
+ // Seed from the controlled `open` prop so a sheet that should be open is
228
+ // visible on its FIRST commit (no present()-in-effect race) this is what
229
+ // lets a responsive Dialog survive a resize placement swap without going blank.
230
+ const [visible, setVisible] = useState(() => open ?? false);
231
+ const [rendered, setRendered] = useState(() => open ?? false); // keep mounted for exit animation
194
232
  const closeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
195
233
  const hasClosedRef = useRef(false);
196
234
  const scrollViewRef = useRef<Animated.ScrollView>(null);
@@ -266,6 +304,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
266
304
  useEffect(() => {
267
305
  renderedRef.current = rendered;
268
306
  }, [rendered]);
307
+ // Mirror `visible` so the unmount cleanup can tell "closing" (visible=false,
308
+ // animating out) from "fully open" (visible=true) at teardown time.
309
+ const visibleRef = useRef(visible);
310
+ useEffect(() => {
311
+ visibleRef.current = visible;
312
+ }, [visible]);
269
313
 
270
314
  /**
271
315
  * Commit a close. Two guards prevent stale callbacks from firing:
@@ -325,18 +369,24 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
325
369
  }, [visible, rendered, finishClose, screenHeight, closeGeneration, opacity, translateY]);
326
370
 
327
371
  // On unmount: ensure pending close callbacks (e.g. consumer's `onDismiss`)
328
- // still fire if the BS is yanked mid-animation by a parent re-render.
329
- // Without this, `Dialog`'s `handleDismiss` never runs and queued
330
- // callbacks (post-close handlers) are silently lost.
331
- // Only fires when the sheet was actually rendered (open or closing) to
332
- // avoid spuriously calling onDismiss on bare unmount of a never-opened
333
- // sheet. Refs are read inside the cleanup, so latest values are captured.
372
+ // still fire if the BS is yanked mid-animation by a parent re-render while a
373
+ // close is IN PROGRESS. Without this, `Dialog`'s `handleDismiss` never runs
374
+ // and queued callbacks (post-close handlers) are silently lost.
375
+ //
376
+ // Guarded on `!visibleRef.current`: only flush when a close was already
377
+ // underway (visible=false closing/animating out). A sheet yanked while
378
+ // still FULLY OPEN (visible=true) is not closing — it is being swapped out by
379
+ // a parent, e.g. a responsive `Dialog` crossing its breakpoint between the
380
+ // bottom-sheet and centered-card branches. Firing onDismiss there would
381
+ // wrongly dismiss the surface mid-swap (there are no pending close callbacks
382
+ // to flush anyway), so the new branch can take over still-open. Refs are read
383
+ // inside the cleanup, so the latest values are captured.
334
384
  useEffect(() => () => {
335
385
  if (closeTimeoutRef.current) {
336
386
  clearTimeout(closeTimeoutRef.current);
337
387
  closeTimeoutRef.current = null;
338
388
  }
339
- if (renderedRef.current && !hasClosedRef.current) {
389
+ if (renderedRef.current && !hasClosedRef.current && !visibleRef.current) {
340
390
  hasClosedRef.current = true;
341
391
  safeCloseRef.current();
342
392
  }
@@ -639,8 +689,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
639
689
  onScroll: (event) => {
640
690
  scrollOffsetY.value = event.contentOffset.y;
641
691
  isScrollAtTop.value = event.contentOffset.y <= 0;
692
+ // Mirror into the host's collapse offset (a Dialog nav header) when
693
+ // provided. Listed in deps so the RN-Web (no worklets plugin) handler
694
+ // re-binds if the shared value identity changes.
695
+ if (externalScrollY) externalScrollY.value = event.contentOffset.y;
642
696
  },
643
- }, []);
697
+ }, [externalScrollY]);
644
698
 
645
699
  const dynamicStyles = useMemo(() => {
646
700
  return StyleSheet.create({
@@ -735,12 +789,19 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
735
789
  </Animated.View>
736
790
 
737
791
  <GestureDetector gesture={panGesture}>
738
- <Animated.View style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}>
792
+ <Animated.View
793
+ onLayout={onLayout}
794
+ style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}
795
+ >
739
796
  {backgroundComponent?.({ style: styles.background })}
740
797
 
741
798
  {handleSlot}
742
799
 
743
800
  {bodyContent}
801
+
802
+ {/* Sticky nav-header overlay (Dialog nav-header mode): floats
803
+ above the scroll body, clipped to the sheet's rounded top. */}
804
+ {headerOverlay}
744
805
  </Animated.View>
745
806
  </GestureDetector>
746
807
  </View>