@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
@@ -1,9 +1,51 @@
1
+ import type { ReactNode } from 'react';
1
2
  import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native';
2
3
 
3
4
  import type { ResponsiveDialogPlacement } from './placement';
4
5
 
5
6
  export type { DialogPlacement, ResponsiveDialogPlacement } from './placement';
6
7
 
8
+ /**
9
+ * Config for the Dialog's OWN navigation header — a DISTINCT surface mode from
10
+ * the declarative `title`/`description`/`actions` centered-heading path. When
11
+ * present it turns the Dialog into a scrolling page with a sticky, gradient top
12
+ * bar (left + right slots) and a large collapsing title that lives in the
13
+ * Dialog's own scroll content; as the large title scrolls under the bar a small
14
+ * title cross-fades into the bar center. Screens render NO header of their own.
15
+ *
16
+ * A surface seeds this from its registry config; a mounted screen may override
17
+ * or augment any field at runtime via `useDialogHeader` (a dynamic title, a Save
18
+ * action on the right, etc.).
19
+ */
20
+ export interface DialogHeaderConfig {
21
+ /** Collapsing title — rendered large in-content and small in the nav bar. */
22
+ title?: string;
23
+ /** Supporting copy under the large title (and, optionally, in the bar). */
24
+ subtitle?: string;
25
+ /**
26
+ * Whether to render the large collapsing title in the scroll content.
27
+ * Defaults to `true`. When `false`, only the small nav-bar title shows (always
28
+ * visible, no collapse) and the content starts flush under the bar.
29
+ */
30
+ largeTitle?: boolean;
31
+ /** Custom left slot node. Overrides the default back button. */
32
+ left?: ReactNode;
33
+ /** Custom right slot node. Overrides the default close button. */
34
+ right?: ReactNode;
35
+ /**
36
+ * When set, the default left slot renders a back button that calls this. A
37
+ * custom `left` node still wins. Set by the surface host when the surface can
38
+ * navigate back.
39
+ */
40
+ onBack?: () => void;
41
+ /**
42
+ * Whether the default right slot renders a close button (dismiss). Defaults to
43
+ * `true`. A custom `right` node still wins. `false` suppresses the close
44
+ * affordance entirely (e.g. a blocking surface).
45
+ */
46
+ showClose?: boolean;
47
+ }
48
+
7
49
  /**
8
50
  * Side-sheet inset (px) from the root overlay's edges. Lets a host place a
9
51
  * `left`/`right` placement "inside its own shell" — e.g. offset from the top
@@ -134,6 +176,40 @@ export type DialogProps = React.PropsWithChildren<{
134
176
  inset?: DialogInset;
135
177
  /** Whether to render the drag handle in bottom-sheet mode. Defaults to `true`. */
136
178
  showHandle?: boolean;
179
+ /**
180
+ * Turns on the Dialog's OWN navigation header (see {@link DialogHeaderConfig}).
181
+ * When present the Dialog renders a sticky gradient nav bar + a large
182
+ * collapsing title over its own scroll content, and its content is inset below
183
+ * the bar — a DISTINCT mode from the declarative
184
+ * `title`/`description`/`actions` centered heading (a confirm/alert dialog
185
+ * passes NO `header` and is unchanged). Screens render no header of their own;
186
+ * the title/slots come from this config, refined at runtime by a mounted
187
+ * screen's `useDialogHeader` contribution.
188
+ */
189
+ header?: DialogHeaderConfig;
190
+ /**
191
+ * Open-INTENT seed for a freshly-mounted placement branch, distinct from the
192
+ * controlled `open` prop. It ONLY seeds a branch's initial visible state on
193
+ * mount — it never switches the dialog into controlled close semantics, so
194
+ * the imperative `control.close()` still owns the exit animation and fires
195
+ * `onClose` post-exit (no synchronous-`onClose` entry leak). Set by the
196
+ * surface stack so a responsive `Dialog` that swaps its inner component across
197
+ * a breakpoint (centered card ↔ bottom sheet on resize) re-mounts the new
198
+ * branch ALREADY OPEN instead of blank. Ignored when the controlled `open`
199
+ * prop is provided (that path seeds itself).
200
+ */
201
+ startOpen?: boolean;
202
+ /**
203
+ * Whether the dialog wraps its content in an internal scroll container.
204
+ * Defaults to `true`. Set `false` when the content owns its own scrolling
205
+ * primitive (a `FlatList` / `SectionList` / any VirtualizedList, or its own
206
+ * `ScrollView`): the surface then renders children WITHOUT a ScrollView wrap —
207
+ * a bounded, overflow-clipped card on the centered/side placements and a
208
+ * non-scrollable `BottomSheet` body on the bottom placement — so the child
209
+ * gets a bounded height and owns its scroll, and no "VirtualizedList inside a
210
+ * plain ScrollView" warning is emitted.
211
+ */
212
+ scrollable?: boolean;
137
213
  /**
138
214
  * Inner padding (px) of the dialog content container, applied uniformly across
139
215
  * every placement (centered panel, side-sheet body, bottom-sheet body).
@@ -163,6 +239,16 @@ export type DialogProps = React.PropsWithChildren<{
163
239
  containerClassName?: string;
164
240
  /** Accessibility label, applied to the dialog role on web. */
165
241
  label?: string;
242
+ /**
243
+ * Stacking layer index within a coordinated surface stack. Multiplies
244
+ * `Z_INDEX_LAYER_STEP` onto this dialog's overlay z-indices (backdrop +
245
+ * panel) so a dialog presented on top of another paints above it. Defaults to
246
+ * `0` — the single-dialog case, byte-for-byte unchanged. Set by the shared
247
+ * surface stack (`src/surfaces`); direct consumers rarely need it. No-op for
248
+ * the `bottom` placement, which already stacks in mount order (native RN
249
+ * `<Modal>` / the web portal), so `layer` is harmless there.
250
+ */
251
+ layer?: number;
166
252
  }>;
167
253
 
168
254
  /**
package/src/index.ts CHANGED
@@ -59,6 +59,7 @@ export {
59
59
  alert,
60
60
  useDialogContext,
61
61
  useDialogControl,
62
+ useDialogHeader,
62
63
  } from './dialog';
63
64
  export type {
64
65
  AlertButton,
@@ -67,11 +68,27 @@ export type {
67
68
  DialogActionColor,
68
69
  DialogContextProps,
69
70
  DialogControlProps,
71
+ DialogHeaderConfig,
70
72
  DialogInset,
71
73
  DialogPlacement,
72
74
  DialogProps,
73
75
  ResponsiveDialogPlacement,
74
76
  } from './dialog';
77
+ // Surface stack — the shared, coordinated overlay system. The imperative API is
78
+ // the `surfaces` object (its members would otherwise clash with alert-dialog's
79
+ // `confirm`); the individual functions are also named exports of
80
+ // `@oxyhq/bloom/surfaces`.
81
+ export { surfaces, SurfaceProvider, SurfaceHost, useSurface } from './surfaces';
82
+ export type {
83
+ SurfacePresentation,
84
+ PresentOptions,
85
+ SurfaceControls,
86
+ SurfaceRenderFn,
87
+ SurfaceStatus,
88
+ SurfaceEntry,
89
+ SurfaceConfirmOptions,
90
+ SurfacePromptOptions,
91
+ } from './surfaces';
75
92
  export * from './button';
76
93
  export { Fab } from './fab';
77
94
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab';
@@ -129,7 +146,7 @@ export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
129
146
  // Form components
130
147
  export * from './text-field';
131
148
  export * from './segmented-control';
132
- export { SearchInput } from './search-input';
149
+ export { Search } from './search';
133
150
  export { Label } from './label';
134
151
  export type { LabelProps } from './label';
135
152
  export { Field } from './field';
package/src/index.web.ts CHANGED
@@ -64,6 +64,7 @@ export {
64
64
  alert,
65
65
  useDialogContext,
66
66
  useDialogControl,
67
+ useDialogHeader,
67
68
  } from './dialog/index.web';
68
69
  export type {
69
70
  AlertButton,
@@ -72,11 +73,27 @@ export type {
72
73
  DialogActionColor,
73
74
  DialogContextProps,
74
75
  DialogControlProps,
76
+ DialogHeaderConfig,
75
77
  DialogInset,
76
78
  DialogPlacement,
77
79
  DialogProps,
78
80
  ResponsiveDialogPlacement,
79
81
  } from './dialog/index.web';
82
+ // Surface stack — the shared, coordinated overlay system. The imperative API is
83
+ // the `surfaces` object (its members would otherwise clash with alert-dialog's
84
+ // `confirm`); the individual functions are also named exports of
85
+ // `@oxyhq/bloom/surfaces`.
86
+ export { surfaces, SurfaceProvider, SurfaceHost, useSurface } from './surfaces/index.web';
87
+ export type {
88
+ SurfacePresentation,
89
+ PresentOptions,
90
+ SurfaceControls,
91
+ SurfaceRenderFn,
92
+ SurfaceStatus,
93
+ SurfaceEntry,
94
+ SurfaceConfirmOptions,
95
+ SurfacePromptOptions,
96
+ } from './surfaces/index.web';
80
97
  export * from './button/index.web';
81
98
  export { Fab } from './fab/index.web';
82
99
  export type { FabProps, FabVariant, FabSize, FabPlacement } from './fab/index.web';
@@ -134,7 +151,7 @@ export type { AnimatedCheckProps, AnimatedCheckRef } from './animated-check';
134
151
  // Form components
135
152
  export * from './text-field';
136
153
  export * from './segmented-control';
137
- export { SearchInput } from './search-input';
154
+ export { Search } from './search';
138
155
  export { Label } from './label';
139
156
  export type { LabelProps } from './label';
140
157
  export { Field } from './field';
@@ -21,7 +21,7 @@ export interface InputGroupAddonProps {
21
21
  export interface InputGroupProps {
22
22
  /**
23
23
  * Group children. Place `InputGroup.Addon` elements before/after the input
24
- * (a Bloom `TextFieldInput`, a `SearchInput`, or any control). The middle
24
+ * (a Bloom `TextFieldInput`, a `Search`, or any control). The middle
25
25
  * child stretches to fill.
26
26
  */
27
27
  children: React.ReactNode;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `className` support for React Native core components in Bloom.
3
+ *
4
+ * Bloom styles some primitives with NativeWind/`react-native-css` utility
5
+ * classes (`className="..."`). react-native-css applies them at runtime — on web
6
+ * as real CSS classes emitted by the consumer's Tailwind pipeline (`@source`-
7
+ * scanning Bloom's lib), on native as compiled styles. This ambient augmentation
8
+ * makes `className` type-valid on the RN primitives we set it on.
9
+ *
10
+ * Heritage-free (a plain interface-merge `declare module`, no `extends` / import
11
+ * of RN's own module graph) so it never drags a second `@types/react` copy into
12
+ * the RN 0.85 / React 19 tree — the same hazard the `mergeRefs` note calls out.
13
+ */
14
+ // `export {}` makes this file a MODULE, so the `declare module` below MERGES
15
+ // with react-native's real types (an augmentation) instead of replacing the
16
+ // module and hiding its exports.
17
+ export {};
18
+
19
+ declare module 'react-native' {
20
+ interface ViewProps {
21
+ className?: string;
22
+ }
23
+ interface TextProps {
24
+ className?: string;
25
+ }
26
+ interface ImageProps {
27
+ className?: string;
28
+ }
29
+ interface PressableProps {
30
+ className?: string;
31
+ }
32
+ interface ScrollViewProps {
33
+ className?: string;
34
+ }
35
+ interface TextInputProps {
36
+ className?: string;
37
+ }
38
+ }
@@ -8,7 +8,10 @@ import { TextField, TextFieldIcon, TextFieldInput, type TextFieldInputProps } fr
8
8
  import { MagnifyingGlass_Stroke2_Corner0_Rounded as MagnifyingGlassIcon } from '../icons/MagnifyingGlass';
9
9
  import { TimesLarge_Stroke2_Corner0_Rounded as X } from '../icons/Times';
10
10
 
11
- type SearchInputProps = Omit<TextFieldInputProps, 'label'> & {
11
+ /** Fully-rounded (pill) corner radius clamps to half the field height. */
12
+ const PILL_RADIUS = 999;
13
+
14
+ type SearchProps = Omit<TextFieldInputProps, 'label'> & {
12
15
  label?: TextFieldInputProps['label'];
13
16
  /**
14
17
  * Called when the user presses the clear (X) button.
@@ -16,14 +19,14 @@ type SearchInputProps = Omit<TextFieldInputProps, 'label'> & {
16
19
  onClearText?: () => void;
17
20
  };
18
21
 
19
- export const SearchInput = forwardRef<TextInput, SearchInputProps>(
20
- function SearchInput({ value, label = 'Search', onClearText, ...rest }, ref) {
22
+ export const Search = forwardRef<TextInput, SearchProps>(
23
+ function Search({ value, label = 'Search', onClearText, ...rest }, ref) {
21
24
  const theme = useTheme();
22
25
  const showClear = value != null && value.length > 0;
23
26
 
24
27
  return (
25
28
  <View style={[a.w_full, a.relative]}>
26
- <TextField>
29
+ <TextField radius={PILL_RADIUS}>
27
30
  <TextFieldIcon icon={MagnifyingGlassIcon} />
28
31
  <TextFieldInput
29
32
  inputRef={ref}
@@ -70,4 +73,4 @@ export const SearchInput = forwardRef<TextInput, SearchInputProps>(
70
73
  );
71
74
  },
72
75
  );
73
- SearchInput.displayName = 'SearchInput';
76
+ Search.displayName = 'Search';
@@ -243,7 +243,11 @@ const styles = StyleSheet.create({
243
243
  marginBottom: 16,
244
244
  },
245
245
  groupCard: {
246
- marginHorizontal: 16,
246
+ // No horizontal self-inset: a `SettingsListGroup` fills its parent's content
247
+ // width, so the single horizontal inset comes from the ONE content padding
248
+ // the screen/Dialog already applies (the same `px-screen-margin` that pads
249
+ // the title/search). Keeping a self-margin here double-inset grouped rows on
250
+ // every screen that also applies that padding.
247
251
  borderRadius: 16,
248
252
  overflow: 'hidden',
249
253
  },
@@ -0,0 +1,222 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useSyncExternalStore,
6
+ } from 'react';
7
+ import { BackHandler, Platform } from 'react-native';
8
+
9
+ import { useDialogControl } from '../dialog/context';
10
+ import type { DialogProps } from '../dialog/types';
11
+ import {
12
+ finalizeClose,
13
+ getSnapshot,
14
+ present,
15
+ requestDismiss,
16
+ resetSurfaces,
17
+ subscribe,
18
+ } from './surfaceStore';
19
+ import { SurfaceContext } from './useSurface';
20
+ import type { SurfaceControls, SurfaceEntry, SurfacePresentation } from './types';
21
+
22
+ type DialogComponent = React.ComponentType<DialogProps>;
23
+
24
+ /** Default placement — the ecosystem's responsive "sheet" (bottom → centered). */
25
+ const DEFAULT_PLACEMENT: SurfacePresentation['placement'] = {
26
+ base: 'bottom',
27
+ md: 'center',
28
+ };
29
+
30
+ /** Map a surface's presentation config onto the shared `Dialog` props. */
31
+ function placementFor(p: SurfacePresentation): Partial<DialogProps> {
32
+ return {
33
+ placement: p.placement ?? DEFAULT_PLACEMENT,
34
+ width: p.width,
35
+ maxWidth: p.maxWidth,
36
+ maxHeightRatio: p.maxHeightRatio,
37
+ inset: p.inset,
38
+ showHandle: p.showHandle,
39
+ dismissOnBackdrop: p.dismissOnBackdrop,
40
+ contentPadding: p.contentPadding,
41
+ header: p.header,
42
+ scrollable: p.scrollable,
43
+ style: p.style,
44
+ panelStyle: p.panelStyle,
45
+ panelClassName: p.panelClassName,
46
+ containerStyle: p.containerStyle,
47
+ containerClassName: p.containerClassName,
48
+ label: p.label,
49
+ testID: p.testID,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Build the `<SurfaceHost>` + `<SurfaceProvider>` bound to a platform `Dialog`.
55
+ *
56
+ * `Dialog` is platform-forked (native `BottomSheet`-backed vs. pure-DOM web
57
+ * overlay), so — like `createAlertDialog` — the platform entry files
58
+ * (`index.ts` / `index.web.ts`) inject the correct one. The stack logic below is
59
+ * single-source, no duplication.
60
+ */
61
+ export function createSurfaceHost(Dialog: DialogComponent) {
62
+ /**
63
+ * One stacked surface. Bridges a store entry onto an IMPERATIVE `Dialog`
64
+ * (`useDialogControl()` + `open()`/`close()`) — never the controlled
65
+ * `open`/`onClose` boolean path, which fires `onClose` synchronously as the
66
+ * dismiss REQUEST and has no post-exit callback, so it could never tell the
67
+ * store WHEN to splice the entry after the exit animation.
68
+ */
69
+ function SurfaceLayer({
70
+ entry,
71
+ index,
72
+ }: {
73
+ entry: SurfaceEntry;
74
+ index: number;
75
+ }) {
76
+ const control = useDialogControl();
77
+ const { id, status } = entry;
78
+
79
+ const controls = useMemo<SurfaceControls>(
80
+ () => ({
81
+ dismiss: (result?: unknown) => requestDismiss(id, result),
82
+ present: (render, opts) => present(render, opts),
83
+ }),
84
+ [id],
85
+ );
86
+
87
+ // Open imperatively on mount (mirrors `AlertDialog`'s canonical
88
+ // fresh-mount open-from-effect; `control` is stable for the layer's life).
89
+ useEffect(() => {
90
+ control.open();
91
+ }, [control]);
92
+
93
+ // When the store flips this entry to 'closing' (a programmatic dismiss,
94
+ // dismissAll, dismissToRoot, Escape, or Android-back), run the exit
95
+ // animation. A user backdrop/Escape handled by the Dialog itself instead
96
+ // drives its own internal close; either way `onClose` fires post-exit.
97
+ useEffect(() => {
98
+ if (status === 'closing') control.close();
99
+ }, [status, control]);
100
+
101
+ // Fires AFTER the exit animation settles (imperative mode), for ANY
102
+ // dismissal path — user backdrop/Escape/back OR a programmatic dismiss.
103
+ // `requestDismiss` resolves-once (a programmatic dismiss already resolved
104
+ // with its result; this is a no-op then), then `finalizeClose` splices the
105
+ // entry out. Guarded by unique ids + `'closing'` status in the store.
106
+ const handleClose = useCallback(() => {
107
+ requestDismiss(id);
108
+ finalizeClose(id);
109
+ }, [id]);
110
+
111
+ const placement = useMemo(
112
+ () => placementFor(entry.presentation),
113
+ [entry.presentation],
114
+ );
115
+
116
+ return (
117
+ // `startOpen` is the surface's open-INTENT (true while not closing). It
118
+ // ONLY seeds a freshly-mounted Dialog branch's visible state — the
119
+ // imperative `control` still owns open/close — so when a responsive Dialog
120
+ // swaps its inner component on resize (centered card ↔ bottom sheet) the
121
+ // new branch mounts STILL OPEN instead of dismissing the surface. A real
122
+ // dismiss (backdrop/Escape/back/programmatic) still resolves + splices
123
+ // exactly once via `control.close()` → `handleClose`, post-exit.
124
+ <Dialog
125
+ control={control}
126
+ onClose={handleClose}
127
+ layer={index}
128
+ startOpen={status !== 'closing'}
129
+ {...placement}
130
+ >
131
+ <SurfaceContext.Provider value={controls}>
132
+ {entry.render(controls)}
133
+ </SurfaceContext.Provider>
134
+ </Dialog>
135
+ );
136
+ }
137
+
138
+ function SurfaceHost() {
139
+ const stack = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
140
+
141
+ useAndroidBackDismissesTop();
142
+ useEscapeDismissesTop();
143
+
144
+ return (
145
+ <>
146
+ {stack.map((entry, index) => (
147
+ <SurfaceLayer key={entry.id} entry={entry} index={index} />
148
+ ))}
149
+ </>
150
+ );
151
+ }
152
+ SurfaceHost.displayName = 'SurfaceHost';
153
+
154
+ /**
155
+ * Mount ONCE near your app root — renders `children` plus the stack host.
156
+ * Requires the Bloom Portal provider to be mounted too (same as any `<Dialog>`
157
+ * usage): on web the Portal auto-creates `#bloom-portal-root`; on native mount
158
+ * `<Provider>`/`<Outlet>` from `@oxyhq/bloom/portal`. Can equally be folded
159
+ * next to that Portal provider — `<SurfaceHost>` used directly does the same
160
+ * job without wrapping `children`.
161
+ */
162
+ function SurfaceProvider({ children }: { children: React.ReactNode }) {
163
+ // Resolve any pending awaiters if the provider unmounts so no `present()`
164
+ // promise is stranded.
165
+ useEffect(() => () => resetSurfaces(), []);
166
+ return (
167
+ <>
168
+ {children}
169
+ <SurfaceHost />
170
+ </>
171
+ );
172
+ }
173
+ SurfaceProvider.displayName = 'SurfaceProvider';
174
+
175
+ return { SurfaceHost, SurfaceProvider };
176
+ }
177
+
178
+ /**
179
+ * ONE Android hardware-back handler for the whole stack → dismiss the TOP
180
+ * surface. Reads the live top from the store so a single registration always
181
+ * targets the current top. Returns `false` when the stack is empty so app-level
182
+ * back navigation is untouched.
183
+ */
184
+ function useAndroidBackDismissesTop(): void {
185
+ useEffect(() => {
186
+ if (Platform.OS !== 'android') return;
187
+ const sub = BackHandler.addEventListener('hardwareBackPress', () => {
188
+ const stack = getSnapshot();
189
+ const top = stack[stack.length - 1];
190
+ if (!top) return false;
191
+ if (top.presentation.dismissOnBackdrop === false) return true;
192
+ requestDismiss(top.id);
193
+ return true;
194
+ });
195
+ return () => sub.remove();
196
+ }, []);
197
+ }
198
+
199
+ /**
200
+ * Web-only: route Escape to the TOP surface only. A window-CAPTURE listener runs
201
+ * before each `Dialog`'s own document-level keydown, so `stopImmediatePropagation`
202
+ * prevents the lower stacked dialogs from ALSO handling Escape (they each add a
203
+ * document listener) — Escape dismisses strictly the top, LIFO. No-op on native.
204
+ */
205
+ function useEscapeDismissesTop(): void {
206
+ useEffect(() => {
207
+ if (Platform.OS !== 'web') return;
208
+ if (typeof window === 'undefined' || typeof document === 'undefined') return;
209
+ const handler = (e: KeyboardEvent) => {
210
+ if (e.key !== 'Escape') return;
211
+ const stack = getSnapshot();
212
+ const top = stack[stack.length - 1];
213
+ if (!top) return;
214
+ if (top.presentation.dismissOnBackdrop === false) return;
215
+ e.stopImmediatePropagation();
216
+ e.preventDefault();
217
+ requestDismiss(top.id);
218
+ };
219
+ window.addEventListener('keydown', handler, true);
220
+ return () => window.removeEventListener('keydown', handler, true);
221
+ }, []);
222
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ dismissAll,
3
+ dismissToRoot,
4
+ dismissTop,
5
+ present,
6
+ requestDismiss,
7
+ } from './surfaceStore';
8
+ import { confirm, prompt } from './prompts';
9
+ import type { PresentOptions, SurfaceRenderFn } from './types';
10
+
11
+ /**
12
+ * Dismiss the TOP surface, resolving its `present()` promise with `result`.
13
+ * The public counterpart of the store's `dismissTop`.
14
+ */
15
+ function dismiss(result?: unknown): void {
16
+ dismissTop(result);
17
+ }
18
+
19
+ /**
20
+ * Dismiss a SPECIFIC surface by id, resolving its `present()` promise with
21
+ * `result`. Rarely needed — most call sites use `dismiss()` (top) or the
22
+ * per-surface `SurfaceControls.dismiss`.
23
+ */
24
+ function dismissById(id: string, result?: unknown): void {
25
+ requestDismiss(id, result);
26
+ }
27
+
28
+ /**
29
+ * The shared imperative surface-stack API — a module singleton (mirrors
30
+ * `alert()`'s shape) so any consumer, in the SDK or an app, presents into the
31
+ * SAME coordinated Bloom stack from anywhere without threading a provider.
32
+ *
33
+ * Exposed at the package root as `surfaces` (avoids a bare `present`/`confirm`
34
+ * name clash with the existing `alert-dialog` `confirm`); the individual
35
+ * functions are also named exports of `@oxyhq/bloom/surfaces` for direct import.
36
+ */
37
+ export const surfaces = {
38
+ /** Present a new surface on top. Resolves with the dismissal result. */
39
+ present: <Result = unknown>(render: SurfaceRenderFn, opts?: PresentOptions) =>
40
+ present<Result>(render, opts),
41
+ /** Dismiss the top surface. */
42
+ dismiss,
43
+ /** Dismiss a specific surface by id. */
44
+ dismissById,
45
+ /** Dismiss everything above the root surface. */
46
+ dismissToRoot,
47
+ /** Dismiss the entire stack (resolves pending awaiters with `undefined`). */
48
+ dismissAll,
49
+ /** Present a confirm surface; resolves `true`/`false`. */
50
+ confirm,
51
+ /** Present a prompt surface; resolves the string or `null`. */
52
+ prompt,
53
+ } as const;
54
+
55
+ export { confirm, dismiss, dismissAll, dismissById, dismissToRoot, present, prompt };
@@ -0,0 +1,38 @@
1
+ import { Dialog } from '../dialog';
2
+ import { createSurfaceHost } from './SurfaceHost';
3
+
4
+ /**
5
+ * The shared, content-agnostic SURFACE STACK.
6
+ *
7
+ * `<SurfaceProvider>`/`<SurfaceHost>` render the coordinated stack; the
8
+ * imperative `surfaces` API (+ its named functions) present into it from
9
+ * anywhere; `useSurface()` gives a presented surface its own controls.
10
+ *
11
+ * `SurfaceHost`/`SurfaceProvider` are bound to the NATIVE `Dialog` here; the web
12
+ * fork (`./index.web`) binds the web `Dialog`. The store, imperative API, hook,
13
+ * and types are platform-agnostic and shared by both.
14
+ */
15
+ const { SurfaceHost, SurfaceProvider } = createSurfaceHost(Dialog);
16
+
17
+ export { SurfaceHost, SurfaceProvider };
18
+ export {
19
+ surfaces,
20
+ present,
21
+ dismiss,
22
+ dismissById,
23
+ dismissToRoot,
24
+ dismissAll,
25
+ confirm,
26
+ prompt,
27
+ } from './api';
28
+ export { useSurface } from './useSurface';
29
+ export type {
30
+ SurfacePresentation,
31
+ PresentOptions,
32
+ SurfaceControls,
33
+ SurfaceRenderFn,
34
+ SurfaceStatus,
35
+ SurfaceEntry,
36
+ SurfaceConfirmOptions,
37
+ SurfacePromptOptions,
38
+ } from './types';
@@ -0,0 +1,33 @@
1
+ import { Dialog } from '../dialog/index.web';
2
+ import { createSurfaceHost } from './SurfaceHost';
3
+
4
+ /**
5
+ * Web variant of the surface stack barrel. Identical surface area to the native
6
+ * `./index.ts` — it just binds the web-fork `Dialog` (`Dialog.web.tsx`, a
7
+ * pure-DOM overlay) instead of the native `BottomSheet`-backed one. The store,
8
+ * imperative API, hook, and types are shared, platform-agnostic modules.
9
+ */
10
+ const { SurfaceHost, SurfaceProvider } = createSurfaceHost(Dialog);
11
+
12
+ export { SurfaceHost, SurfaceProvider };
13
+ export {
14
+ surfaces,
15
+ present,
16
+ dismiss,
17
+ dismissById,
18
+ dismissToRoot,
19
+ dismissAll,
20
+ confirm,
21
+ prompt,
22
+ } from './api';
23
+ export { useSurface } from './useSurface';
24
+ export type {
25
+ SurfacePresentation,
26
+ PresentOptions,
27
+ SurfaceControls,
28
+ SurfaceRenderFn,
29
+ SurfaceStatus,
30
+ SurfaceEntry,
31
+ SurfaceConfirmOptions,
32
+ SurfacePromptOptions,
33
+ } from './types';