@oxyhq/bloom 0.14.0 → 0.16.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 (212) hide show
  1. package/lib/commonjs/alert-dialog/AlertDialog.js +35 -27
  2. package/lib/commonjs/alert-dialog/AlertDialog.js.map +1 -1
  3. package/lib/commonjs/alert-dialog/index.js +1 -1
  4. package/lib/commonjs/alert-dialog/index.js.map +1 -1
  5. package/lib/commonjs/alert-dialog/index.web.js +1 -1
  6. package/lib/commonjs/alert-dialog/index.web.js.map +1 -1
  7. package/lib/commonjs/command/Command.js +27 -16
  8. package/lib/commonjs/command/Command.js.map +1 -1
  9. package/lib/commonjs/command/index.js +1 -1
  10. package/lib/commonjs/command/index.js.map +1 -1
  11. package/lib/commonjs/command/index.web.js +1 -1
  12. package/lib/commonjs/command/index.web.js.map +1 -1
  13. package/lib/commonjs/dialog/Dialog.js +336 -43
  14. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  15. package/lib/commonjs/dialog/Dialog.web.js +420 -66
  16. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  17. package/lib/commonjs/dialog/index.js +2 -9
  18. package/lib/commonjs/dialog/index.js.map +1 -1
  19. package/lib/commonjs/dialog/index.web.js +2 -15
  20. package/lib/commonjs/dialog/index.web.js.map +1 -1
  21. package/lib/commonjs/dialog/placement.js +104 -0
  22. package/lib/commonjs/dialog/placement.js.map +1 -0
  23. package/lib/commonjs/index.js +7 -29
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/index.web.js +13 -35
  26. package/lib/commonjs/index.web.js.map +1 -1
  27. package/lib/module/alert-dialog/AlertDialog.js +35 -27
  28. package/lib/module/alert-dialog/AlertDialog.js.map +1 -1
  29. package/lib/module/alert-dialog/index.js +2 -2
  30. package/lib/module/alert-dialog/index.js.map +1 -1
  31. package/lib/module/alert-dialog/index.web.js +2 -2
  32. package/lib/module/alert-dialog/index.web.js.map +1 -1
  33. package/lib/module/command/Command.js +27 -16
  34. package/lib/module/command/Command.js.map +1 -1
  35. package/lib/module/command/index.js +2 -2
  36. package/lib/module/command/index.js.map +1 -1
  37. package/lib/module/command/index.web.js +2 -2
  38. package/lib/module/command/index.web.js.map +1 -1
  39. package/lib/module/dialog/Dialog.js +335 -46
  40. package/lib/module/dialog/Dialog.js.map +1 -1
  41. package/lib/module/dialog/Dialog.web.js +418 -68
  42. package/lib/module/dialog/Dialog.web.js.map +1 -1
  43. package/lib/module/dialog/index.js +1 -2
  44. package/lib/module/dialog/index.js.map +1 -1
  45. package/lib/module/dialog/index.web.js +1 -2
  46. package/lib/module/dialog/index.web.js.map +1 -1
  47. package/lib/module/dialog/placement.js +100 -0
  48. package/lib/module/dialog/placement.js.map +1 -0
  49. package/lib/module/index.js +1 -3
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/index.web.js +1 -3
  52. package/lib/module/index.web.js.map +1 -1
  53. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts +13 -12
  54. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/alert-dialog/index.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/alert-dialog/index.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/command/Command.d.ts +8 -8
  58. package/lib/typescript/commonjs/command/Command.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/command/index.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/command/index.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/dialog/Dialog.d.ts +17 -15
  62. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +18 -5
  64. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/dialog/index.d.ts +2 -4
  66. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -4
  68. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/dialog/placement.d.ts +71 -0
  70. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -0
  71. package/lib/typescript/commonjs/dialog/types.d.ts +66 -3
  72. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/index.d.ts +2 -4
  74. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/index.web.d.ts +2 -4
  76. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  77. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts +13 -12
  78. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts.map +1 -1
  79. package/lib/typescript/module/alert-dialog/index.d.ts.map +1 -1
  80. package/lib/typescript/module/alert-dialog/index.web.d.ts.map +1 -1
  81. package/lib/typescript/module/command/Command.d.ts +8 -8
  82. package/lib/typescript/module/command/Command.d.ts.map +1 -1
  83. package/lib/typescript/module/command/index.d.ts.map +1 -1
  84. package/lib/typescript/module/command/index.web.d.ts.map +1 -1
  85. package/lib/typescript/module/dialog/Dialog.d.ts +17 -15
  86. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  87. package/lib/typescript/module/dialog/Dialog.web.d.ts +18 -5
  88. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  89. package/lib/typescript/module/dialog/index.d.ts +2 -4
  90. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  91. package/lib/typescript/module/dialog/index.web.d.ts +2 -4
  92. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  93. package/lib/typescript/module/dialog/placement.d.ts +71 -0
  94. package/lib/typescript/module/dialog/placement.d.ts.map +1 -0
  95. package/lib/typescript/module/dialog/types.d.ts +66 -3
  96. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  97. package/lib/typescript/module/index.d.ts +2 -4
  98. package/lib/typescript/module/index.d.ts.map +1 -1
  99. package/lib/typescript/module/index.web.d.ts +2 -4
  100. package/lib/typescript/module/index.web.d.ts.map +1 -1
  101. package/package.json +1 -18
  102. package/src/__tests__/AlertDialog.test.tsx +122 -0
  103. package/src/__tests__/Command.test.tsx +107 -0
  104. package/src/__tests__/DialogPlacement.test.tsx +144 -0
  105. package/src/alert-dialog/AlertDialog.tsx +46 -36
  106. package/src/alert-dialog/index.ts +2 -2
  107. package/src/alert-dialog/index.web.ts +2 -2
  108. package/src/command/Command.tsx +30 -19
  109. package/src/command/index.ts +2 -2
  110. package/src/command/index.web.ts +2 -2
  111. package/src/dialog/Dialog.tsx +406 -50
  112. package/src/dialog/Dialog.web.tsx +496 -78
  113. package/src/dialog/index.ts +4 -3
  114. package/src/dialog/index.web.ts +4 -7
  115. package/src/dialog/placement.ts +113 -0
  116. package/src/dialog/types.ts +69 -3
  117. package/src/index.ts +4 -11
  118. package/src/index.web.ts +4 -11
  119. package/lib/commonjs/dialog/CenteredDialog.js +0 -250
  120. package/lib/commonjs/dialog/CenteredDialog.js.map +0 -1
  121. package/lib/commonjs/dialog/CenteredDialog.web.js +0 -328
  122. package/lib/commonjs/dialog/CenteredDialog.web.js.map +0 -1
  123. package/lib/commonjs/dialog/centered-dialog-tokens.js +0 -62
  124. package/lib/commonjs/dialog/centered-dialog-tokens.js.map +0 -1
  125. package/lib/commonjs/dialog/centered-dialog-types.js +0 -6
  126. package/lib/commonjs/dialog/centered-dialog-types.js.map +0 -1
  127. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +0 -300
  128. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +0 -1
  129. package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js +0 -226
  130. package/lib/commonjs/responsive-sheet/ResponsiveSheet.native.js.map +0 -1
  131. package/lib/commonjs/responsive-sheet/const.js +0 -60
  132. package/lib/commonjs/responsive-sheet/const.js.map +0 -1
  133. package/lib/commonjs/responsive-sheet/index.js +0 -19
  134. package/lib/commonjs/responsive-sheet/index.js.map +0 -1
  135. package/lib/commonjs/responsive-sheet/index.web.js +0 -19
  136. package/lib/commonjs/responsive-sheet/index.web.js.map +0 -1
  137. package/lib/commonjs/responsive-sheet/types.js +0 -6
  138. package/lib/commonjs/responsive-sheet/types.js.map +0 -1
  139. package/lib/module/dialog/CenteredDialog.js +0 -244
  140. package/lib/module/dialog/CenteredDialog.js.map +0 -1
  141. package/lib/module/dialog/CenteredDialog.web.js +0 -322
  142. package/lib/module/dialog/CenteredDialog.web.js.map +0 -1
  143. package/lib/module/dialog/centered-dialog-tokens.js +0 -58
  144. package/lib/module/dialog/centered-dialog-tokens.js.map +0 -1
  145. package/lib/module/dialog/centered-dialog-types.js +0 -4
  146. package/lib/module/dialog/centered-dialog-types.js.map +0 -1
  147. package/lib/module/responsive-sheet/ResponsiveSheet.js +0 -293
  148. package/lib/module/responsive-sheet/ResponsiveSheet.js.map +0 -1
  149. package/lib/module/responsive-sheet/ResponsiveSheet.native.js +0 -219
  150. package/lib/module/responsive-sheet/ResponsiveSheet.native.js.map +0 -1
  151. package/lib/module/responsive-sheet/const.js +0 -56
  152. package/lib/module/responsive-sheet/const.js.map +0 -1
  153. package/lib/module/responsive-sheet/index.js +0 -4
  154. package/lib/module/responsive-sheet/index.js.map +0 -1
  155. package/lib/module/responsive-sheet/index.web.js +0 -14
  156. package/lib/module/responsive-sheet/index.web.js.map +0 -1
  157. package/lib/module/responsive-sheet/types.js +0 -4
  158. package/lib/module/responsive-sheet/types.js.map +0 -1
  159. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts +0 -20
  160. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts.map +0 -1
  161. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts +0 -29
  162. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +0 -1
  163. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts +0 -47
  164. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts.map +0 -1
  165. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts +0 -70
  166. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts.map +0 -1
  167. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +0 -26
  168. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  169. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts +0 -20
  170. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.native.d.ts.map +0 -1
  171. package/lib/typescript/commonjs/responsive-sheet/const.d.ts +0 -43
  172. package/lib/typescript/commonjs/responsive-sheet/const.d.ts.map +0 -1
  173. package/lib/typescript/commonjs/responsive-sheet/index.d.ts +0 -3
  174. package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +0 -1
  175. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +0 -3
  176. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +0 -1
  177. package/lib/typescript/commonjs/responsive-sheet/types.d.ts +0 -55
  178. package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +0 -1
  179. package/lib/typescript/module/dialog/CenteredDialog.d.ts +0 -20
  180. package/lib/typescript/module/dialog/CenteredDialog.d.ts.map +0 -1
  181. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts +0 -29
  182. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +0 -1
  183. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts +0 -47
  184. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts.map +0 -1
  185. package/lib/typescript/module/dialog/centered-dialog-types.d.ts +0 -70
  186. package/lib/typescript/module/dialog/centered-dialog-types.d.ts.map +0 -1
  187. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +0 -26
  188. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  189. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts +0 -20
  190. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.native.d.ts.map +0 -1
  191. package/lib/typescript/module/responsive-sheet/const.d.ts +0 -43
  192. package/lib/typescript/module/responsive-sheet/const.d.ts.map +0 -1
  193. package/lib/typescript/module/responsive-sheet/index.d.ts +0 -3
  194. package/lib/typescript/module/responsive-sheet/index.d.ts.map +0 -1
  195. package/lib/typescript/module/responsive-sheet/index.web.d.ts +0 -3
  196. package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +0 -1
  197. package/lib/typescript/module/responsive-sheet/types.d.ts +0 -55
  198. package/lib/typescript/module/responsive-sheet/types.d.ts.map +0 -1
  199. package/src/__tests__/CenteredDialog.test.tsx +0 -108
  200. package/src/__tests__/ResponsiveSheet.test.tsx +0 -104
  201. package/src/dialog/CenteredDialog.stories.tsx +0 -125
  202. package/src/dialog/CenteredDialog.tsx +0 -303
  203. package/src/dialog/CenteredDialog.web.tsx +0 -372
  204. package/src/dialog/centered-dialog-tokens.ts +0 -68
  205. package/src/dialog/centered-dialog-types.ts +0 -70
  206. package/src/responsive-sheet/ResponsiveSheet.native.tsx +0 -257
  207. package/src/responsive-sheet/ResponsiveSheet.stories.tsx +0 -54
  208. package/src/responsive-sheet/ResponsiveSheet.tsx +0 -327
  209. package/src/responsive-sheet/const.ts +0 -53
  210. package/src/responsive-sheet/index.ts +0 -6
  211. package/src/responsive-sheet/index.web.ts +0 -16
  212. package/src/responsive-sheet/types.ts +0 -57
@@ -5,6 +5,12 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.AutoMountedDialog = AutoMountedDialog;
7
7
  exports.BLOOM_DIALOG_CSS = void 0;
8
+ Object.defineProperty(exports, "DIALOG_SHEET_BACKDROP_TESTID", {
9
+ enumerable: true,
10
+ get: function () {
11
+ return _placement.DIALOG_SHEET_BACKDROP_TESTID;
12
+ }
13
+ });
8
14
  exports.Dialog = Dialog;
9
15
  var _react = _interopRequireWildcard(require("react"));
10
16
  var _reactNative = require("react-native");
@@ -13,9 +19,10 @@ var _indexWeb = require("../portal/index.web.js");
13
19
  var _zIndex = require("../styles/z-index.js");
14
20
  var _useTheme = require("../theme/use-theme.js");
15
21
  var _context = require("./context.js");
22
+ var _placement = require("./placement.js");
16
23
  var _jsxRuntime = require("react/jsx-runtime");
17
24
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
18
- const FADE_OUT_DURATION = 150;
25
+ const FADE_OUT_DURATION = _placement.CENTER_FADE_OUT_DURATION;
19
26
  const dialogZIndex = (0, _zIndex.createOverlayZIndex)();
20
27
  const stopPropagation = e => e.stopPropagation();
21
28
  const ClosingContext = /*#__PURE__*/(0, _react.createContext)(false);
@@ -23,40 +30,94 @@ const ClosingContext = /*#__PURE__*/(0, _react.createContext)(false);
23
30
  /**
24
31
  * Web variant of `<Dialog>`.
25
32
  *
26
- * A centered modal card rendered into the bloom Portal at the end of
27
- * `document.body`. Same prop API as native`title`, `description`,
28
- * `actions`, or arbitrary `children` so call sites are platform
29
- * agnostic.
33
+ * A centered modal card (default) rendered into the bloom Portal at the end of
34
+ * `document.body`, or when `placement` is `left`/`right`/`bottom` an
35
+ * anchored side-sheet or bottom-sheet. Same prop API as native, so call sites
36
+ * are platform agnostic.
37
+ *
38
+ * Open/close is driven by EITHER the imperative `control` (legacy) OR the
39
+ * controlled `open` prop. When `open` is provided it wins.
30
40
  *
31
41
  * Accessibility: the panel has `role="dialog"` and the `title`/`description`
32
42
  * props (when provided) become the `aria-labelledby` / `aria-describedby`
33
43
  * targets. Pressing the backdrop dismisses. Pressing `Escape` dismisses.
34
44
  * Focus is locked inside the dialog while it's open.
45
+ *
46
+ * Web motion never uses reanimated `exiting` (which throws `removeChild` on
47
+ * concurrent React unmounts): the centered card uses CSS keyframes and the
48
+ * side/bottom modes use self-contained CSS transitions on a
49
+ * mounted-through-exit node.
35
50
  */
36
51
  function Dialog({
37
52
  control,
53
+ open: controlledOpen,
38
54
  onClose,
39
55
  testID,
40
56
  title,
41
57
  description,
42
58
  actions,
59
+ placement,
60
+ width = _placement.DEFAULT_SIDE_WIDTH,
61
+ maxWidth = _placement.DEFAULT_CENTER_MAX_WIDTH,
62
+ maxHeightRatio = _placement.DEFAULT_MAX_HEIGHT_RATIO,
63
+ inset,
64
+ showHandle = true,
65
+ dismissOnBackdrop = true,
43
66
  style,
67
+ panelStyle,
68
+ panelClassName,
69
+ containerStyle,
70
+ containerClassName,
44
71
  label,
45
72
  children
46
73
  }) {
74
+ // Controlled mode is opt-in: when `open` is a boolean the host owns the
75
+ // visible state; otherwise the legacy imperative `control` path drives it.
76
+ const isControlled = controlledOpen !== undefined;
77
+ const resolvedPlacement = (0, _placement.useResolvedPlacement)(placement);
47
78
  const [isOpen, setIsOpen] = (0, _react.useState)(false);
48
79
  const [isClosing, setIsClosing] = (0, _react.useState)(false);
49
80
  const closeCallbacksRef = (0, _react.useRef)([]);
81
+ // Read the latest controlled flag inside stable callbacks without re-binding.
82
+ const isControlledRef = (0, _react.useRef)(isControlled);
83
+ isControlledRef.current = isControlled;
50
84
  const open = (0, _react.useCallback)(() => {
51
85
  setIsClosing(false);
52
86
  setIsOpen(true);
53
87
  }, []);
88
+
89
+ // A dismissal request (backdrop, Escape, action button). In controlled mode
90
+ // it asks the host to close via `onClose` (the host flips `open`); in
91
+ // imperative mode it starts the exit animation directly. Either way an
92
+ // optional callback runs after the dialog has finished closing.
54
93
  const close = (0, _react.useCallback)(cb => {
55
94
  if (typeof cb === 'function') {
56
95
  closeCallbacksRef.current.push(cb);
57
96
  }
97
+ if (isControlledRef.current) {
98
+ onCloseRef.current?.();
99
+ return;
100
+ }
58
101
  setIsClosing(true);
59
102
  }, []);
103
+
104
+ // `onClose` mirrored into a ref so `close` stays referentially stable (the
105
+ // dialog context + imperative handle depend on it).
106
+ const onCloseRef = (0, _react.useRef)(onClose);
107
+ onCloseRef.current = onClose;
108
+
109
+ // In controlled mode, mirror the `open` prop into the internal open/closing
110
+ // state machine so both modes share the same exit-animation lifecycle.
111
+ (0, _react.useEffect)(() => {
112
+ if (!isControlled) return;
113
+ if (controlledOpen) {
114
+ setIsClosing(false);
115
+ setIsOpen(true);
116
+ } else {
117
+ setIsClosing(true);
118
+ }
119
+ }, [isControlled, controlledOpen]);
120
+ const exitDuration = resolvedPlacement === 'center' ? FADE_OUT_DURATION : _placement.ANIMATION_DURATION;
60
121
  (0, _react.useEffect)(() => {
61
122
  if (!isClosing) return;
62
123
  const timer = setTimeout(() => {
@@ -73,16 +134,22 @@ function Dialog({
73
134
  }
74
135
  }
75
136
  }
76
- onClose?.();
77
- }, FADE_OUT_DURATION);
137
+ // Imperative mode fires `onClose` when the exit settles. Controlled mode
138
+ // already requested the close through `onClose` (the host then flipped
139
+ // `open`), so it must NOT fire it a second time here.
140
+ if (!isControlledRef.current) onCloseRef.current?.();
141
+ }, exitDuration);
78
142
  return () => clearTimeout(timer);
79
- }, [isClosing, onClose]);
143
+ }, [isClosing, exitDuration]);
80
144
 
81
145
  // Escape-to-close while open. The listener is intentionally scoped to the
82
146
  // open lifetime so stacked dialogs don't fight for the keydown — the
83
- // top-most one wins via document-level event order.
147
+ // top-most one wins via document-level event order. Escape honors
148
+ // `dismissOnBackdrop`: a blocking dialog (e.g. an unanswered confirm) is not
149
+ // dismissible by Escape, matching the backdrop's behavior. The default
150
+ // (`dismissOnBackdrop` true) is unchanged — Escape still closes.
84
151
  (0, _react.useEffect)(() => {
85
- if (!isOpen || typeof document === 'undefined') return;
152
+ if (!isOpen || !dismissOnBackdrop || typeof document === 'undefined') return;
86
153
  const handler = e => {
87
154
  if (e.key === 'Escape') {
88
155
  e.stopPropagation();
@@ -91,8 +158,8 @@ function Dialog({
91
158
  };
92
159
  document.addEventListener('keydown', handler);
93
160
  return () => document.removeEventListener('keydown', handler);
94
- }, [close, isOpen]);
95
- (0, _react.useImperativeHandle)(control.ref, () => ({
161
+ }, [close, dismissOnBackdrop, isOpen]);
162
+ (0, _react.useImperativeHandle)(control?.ref, () => ({
96
163
  open,
97
164
  close
98
165
  }), [open, close]);
@@ -101,46 +168,121 @@ function Dialog({
101
168
  isWithinDialog: true
102
169
  }), [close]);
103
170
  if (!isOpen) return null;
171
+ if (resolvedPlacement === 'center') {
172
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_indexWeb.Portal, {
173
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.Context.Provider, {
174
+ value: context,
175
+ children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(ClosingContext.Provider, {
176
+ value: isClosing,
177
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRemoveScrollBar.RemoveScrollBar, {}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
178
+ onPress: dismissOnBackdrop ? () => close() : undefined,
179
+ disabled: !dismissOnBackdrop
180
+ // `pointerEvents: 'auto'` opts back in from the Portal root's
181
+ // `pointer-events: none`, which is set so the idle portal
182
+ // doesn't intercept clicks on the underlying app.
183
+ ,
184
+ style: {
185
+ position: 'fixed',
186
+ top: 0,
187
+ left: 0,
188
+ right: 0,
189
+ bottom: 0,
190
+ zIndex: dialogZIndex.backdrop,
191
+ alignItems: 'center',
192
+ justifyContent: 'center',
193
+ paddingHorizontal: 20,
194
+ pointerEvents: 'auto'
195
+ },
196
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(DialogBackdrop, {
197
+ isClosing: isClosing
198
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(DialogPanel, {
199
+ testID: testID,
200
+ label: label,
201
+ title: title,
202
+ description: description,
203
+ actions: actions,
204
+ style: style,
205
+ maxWidth: maxWidth,
206
+ isClosing: isClosing,
207
+ children: children
208
+ })]
209
+ })]
210
+ })
211
+ })
212
+ });
213
+ }
104
214
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_indexWeb.Portal, {
105
215
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.Context.Provider, {
106
216
  value: context,
107
217
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(ClosingContext.Provider, {
108
218
  value: isClosing,
109
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRemoveScrollBar.RemoveScrollBar, {}), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
110
- onPress: () => close()
111
- // `pointerEvents: 'auto'` opts back in from the Portal root's
112
- // `pointer-events: none`, which is set so the idle portal
113
- // doesn't intercept clicks on the underlying app.
114
- ,
115
- style: {
116
- position: 'fixed',
117
- top: 0,
118
- left: 0,
119
- right: 0,
120
- bottom: 0,
121
- zIndex: dialogZIndex.backdrop,
122
- alignItems: 'center',
123
- justifyContent: 'center',
124
- paddingHorizontal: 20,
125
- pointerEvents: 'auto'
126
- },
127
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(DialogBackdrop, {
128
- isClosing: isClosing
129
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(DialogPanel, {
130
- testID: testID,
131
- label: label,
132
- title: title,
133
- description: description,
134
- actions: actions,
135
- style: style,
136
- isClosing: isClosing,
137
- children: children
138
- })]
219
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactRemoveScrollBar.RemoveScrollBar, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)(SheetSurface, {
220
+ testID: testID,
221
+ label: label,
222
+ title: title,
223
+ description: description,
224
+ actions: actions,
225
+ placement: resolvedPlacement,
226
+ shown: !isClosing,
227
+ width: width,
228
+ maxHeightRatio: maxHeightRatio,
229
+ inset: inset,
230
+ showHandle: showHandle,
231
+ dismissOnBackdrop: dismissOnBackdrop,
232
+ onDismiss: close,
233
+ panelStyle: panelStyle,
234
+ panelClassName: panelClassName,
235
+ containerStyle: containerStyle,
236
+ containerClassName: containerClassName,
237
+ style: style,
238
+ children: children
139
239
  })]
140
240
  })
141
241
  })
142
242
  });
143
243
  }
244
+
245
+ /**
246
+ * Renders the dialog's body: optional declarative title + description, any
247
+ * `children`, then the action row. Shared by the centered panel and the
248
+ * side/bottom sheet so all placements present identical content. The
249
+ * `titleId`/`descriptionId` are wired by the caller onto the `role="dialog"`
250
+ * element for `aria-labelledby` / `aria-describedby`.
251
+ */
252
+ function DialogBody({
253
+ titleId,
254
+ descriptionId,
255
+ title,
256
+ description,
257
+ actions,
258
+ children
259
+ }) {
260
+ const theme = (0, _useTheme.useTheme)();
261
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
262
+ children: [title ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
263
+ nativeID: titleId,
264
+ style: {
265
+ fontSize: 22,
266
+ fontWeight: '600',
267
+ color: theme.colors.text,
268
+ paddingBottom: description ? 4 : 16,
269
+ lineHeight: 30
270
+ },
271
+ children: title
272
+ }) : null, description ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
273
+ nativeID: descriptionId,
274
+ style: {
275
+ fontSize: 16,
276
+ color: theme.colors.textSecondary,
277
+ paddingBottom: 16,
278
+ lineHeight: 22
279
+ },
280
+ children: description
281
+ }) : null, children, actions && actions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ActionRow, {
282
+ actions: actions
283
+ }) : null]
284
+ });
285
+ }
144
286
  function DialogPanel({
145
287
  testID,
146
288
  label,
@@ -148,13 +290,14 @@ function DialogPanel({
148
290
  description,
149
291
  actions,
150
292
  style,
293
+ maxWidth,
151
294
  isClosing,
152
295
  children
153
296
  }) {
154
297
  const theme = (0, _useTheme.useTheme)();
155
298
  const titleId = (0, _react.useId)();
156
299
  const descriptionId = (0, _react.useId)();
157
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
300
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
158
301
  role: "dialog",
159
302
  "aria-label": label,
160
303
  "aria-labelledby": title ? titleId : undefined,
@@ -167,7 +310,7 @@ function DialogPanel({
167
310
  position: 'relative',
168
311
  borderRadius: 20,
169
312
  width: '100%',
170
- maxWidth: 480,
313
+ maxWidth,
171
314
  backgroundColor: theme.colors.background,
172
315
  borderWidth: 1,
173
316
  borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
@@ -185,30 +328,194 @@ function DialogPanel({
185
328
  } : {
186
329
  animation: 'bloomDialogZoomFadeIn cubic-bezier(0.16, 1, 0.3, 1) 0.3s'
187
330
  }, style],
188
- children: [title ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
189
- nativeID: titleId,
190
- style: {
191
- fontSize: 22,
192
- fontWeight: '600',
193
- color: theme.colors.text,
194
- paddingBottom: description ? 4 : 16,
195
- lineHeight: 30
196
- },
197
- children: title
198
- }) : null, description ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
199
- nativeID: descriptionId,
200
- style: {
201
- fontSize: 16,
202
- color: theme.colors.textSecondary,
203
- paddingBottom: 16,
204
- lineHeight: 22
205
- },
206
- children: description
207
- }) : null, children, actions && actions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(ActionRow, {
208
- actions: actions
209
- }) : null]
331
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(DialogBody, {
332
+ titleId: titleId,
333
+ descriptionId: descriptionId,
334
+ title: title,
335
+ description: description,
336
+ actions: actions,
337
+ children: children
338
+ })
339
+ });
340
+ }
341
+ /**
342
+ * Side-sheet / bottom-sheet surface for the `left`/`right`/`bottom`
343
+ * placements. Pure CSS transitions: the
344
+ * node stays mounted through the exit while a `shown` flag drives the
345
+ * transform/opacity, so both directions animate without reanimated `exiting`.
346
+ */
347
+ function SheetSurface({
348
+ testID,
349
+ label,
350
+ title,
351
+ description,
352
+ actions,
353
+ placement,
354
+ shown,
355
+ width,
356
+ maxHeightRatio,
357
+ inset,
358
+ showHandle,
359
+ dismissOnBackdrop,
360
+ onDismiss,
361
+ panelStyle,
362
+ panelClassName,
363
+ containerStyle,
364
+ containerClassName,
365
+ style,
366
+ children
367
+ }) {
368
+ const theme = (0, _useTheme.useTheme)();
369
+ const titleId = (0, _react.useId)();
370
+ const descriptionId = (0, _react.useId)();
371
+ const {
372
+ width: viewportWidth,
373
+ height: viewportHeight
374
+ } = (0, _reactNative.useWindowDimensions)();
375
+
376
+ // Defer the entry transition by a frame so the start state paints before the
377
+ // browser animates to `shown`. `entered` is false on the first committed
378
+ // frame, then flips true so the CSS transition runs.
379
+ const frameRef = (0, _react.useRef)(null);
380
+ const [entered, setEntered] = (0, _react.useState)(false);
381
+ (0, _react.useEffect)(() => {
382
+ frameRef.current = scheduleFrame(() => {
383
+ frameRef.current = scheduleFrame(() => setEntered(true));
384
+ });
385
+ return () => {
386
+ if (frameRef.current !== null) cancelFrame(frameRef.current);
387
+ frameRef.current = null;
388
+ };
389
+ }, []);
390
+ const visible = shown && entered;
391
+ const isBottom = placement === 'bottom';
392
+ const panelTransition = (0, _react.useMemo)(() => ({
393
+ transitionProperty: 'transform, opacity',
394
+ transitionDuration: `${_placement.ANIMATION_DURATION}ms`,
395
+ transitionTimingFunction: _placement.EASE_OUT
396
+ }), []);
397
+ const backdropTransition = (0, _react.useMemo)(() => ({
398
+ transitionProperty: 'opacity',
399
+ transitionDuration: `${_placement.ANIMATION_DURATION}ms`,
400
+ transitionTimingFunction: _placement.EASE_OUT
401
+ }), []);
402
+ const panelGeometry = (0, _react.useMemo)(() => {
403
+ if (isBottom) {
404
+ return {
405
+ left: 0,
406
+ right: 0,
407
+ bottom: 0,
408
+ maxHeight: Math.round(viewportHeight * maxHeightRatio),
409
+ borderTopLeftRadius: _placement.PANEL_RADIUS,
410
+ borderTopRightRadius: _placement.PANEL_RADIUS,
411
+ transform: [{
412
+ translateY: visible ? 0 : '100%'
413
+ }],
414
+ opacity: visible ? 1 : 0
415
+ };
416
+ }
417
+ const insetTop = inset?.top ?? 0;
418
+ const insetBottom = inset?.bottom ?? 0;
419
+ const insetLeft = inset?.left ?? 0;
420
+ const insetRight = inset?.right ?? 0;
421
+ const anchorInset = placement === 'left' ? insetLeft : insetRight;
422
+ const oppositeInset = placement === 'left' ? insetRight : insetLeft;
423
+ const available = viewportWidth - anchorInset - oppositeInset - _placement.SIDE_SHEET_MIN_GUTTER;
424
+ const cappedWidth = Math.max(0, Math.min(width, available));
425
+ const hiddenSign = placement === 'left' ? '-100%' : '100%';
426
+ return {
427
+ top: insetTop,
428
+ bottom: insetBottom,
429
+ [placement]: anchorInset,
430
+ width: cappedWidth,
431
+ borderRadius: _placement.PANEL_RADIUS,
432
+ transform: [{
433
+ translateX: visible ? 0 : hiddenSign
434
+ }],
435
+ opacity: visible ? 1 : 0
436
+ };
437
+ }, [isBottom, visible, placement, width, inset, viewportWidth, viewportHeight, maxHeightRatio]);
438
+ const handleBackdropPress = (0, _react.useCallback)(() => {
439
+ if (dismissOnBackdrop) onDismiss();
440
+ }, [dismissOnBackdrop, onDismiss]);
441
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
442
+ style: [sheetStyles.root, containerStyle],
443
+ ...(containerClassName ? {
444
+ className: containerClassName
445
+ } : {}),
446
+ pointerEvents: "box-none",
447
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
448
+ testID: testID ? `${testID}-backdrop` : _placement.DIALOG_SHEET_BACKDROP_TESTID,
449
+ accessibilityRole: "button",
450
+ accessibilityLabel: label ? `Dismiss ${label}` : 'Dismiss dialog',
451
+ onPress: handleBackdropPress,
452
+ disabled: !dismissOnBackdrop,
453
+ style: [sheetStyles.backdrop, backdropTransition, {
454
+ opacity: visible ? _placement.SHEET_BACKDROP_OPACITY : 0
455
+ }]
456
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
457
+ role: "dialog",
458
+ "aria-label": label,
459
+ "aria-labelledby": title ? titleId : undefined,
460
+ "aria-describedby": description ? descriptionId : undefined,
461
+ accessibilityViewIsModal: true,
462
+ testID: testID,
463
+ onStartShouldSetResponder: () => true,
464
+ onResponderRelease: stopPropagation,
465
+ onClick: stopPropagation,
466
+ ...(panelClassName ? {
467
+ className: panelClassName
468
+ } : {}),
469
+ style: [sheetStyles.panel, {
470
+ backgroundColor: theme.colors.background,
471
+ shadowColor: theme.colors.shadow
472
+ }, panelGeometry, panelTransition, panelStyle, style],
473
+ pointerEvents: "auto",
474
+ children: [isBottom && showHandle ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
475
+ style: sheetStyles.handleRow,
476
+ pointerEvents: "none",
477
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
478
+ style: [sheetStyles.handle, {
479
+ backgroundColor: theme.colors.border
480
+ }]
481
+ })
482
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
483
+ style: sheetStyles.body,
484
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(DialogBody, {
485
+ titleId: titleId,
486
+ descriptionId: descriptionId,
487
+ title: title,
488
+ description: description,
489
+ actions: actions,
490
+ children: children
491
+ })
492
+ })]
493
+ })]
210
494
  });
211
495
  }
496
+
497
+ /**
498
+ * Opaque token returned by `scheduleFrame`. `requestAnimationFrame` returns a
499
+ * number; the `setTimeout` fallback returns a timer handle.
500
+ */
501
+
502
+ function scheduleFrame(cb) {
503
+ if (typeof requestAnimationFrame === 'function') {
504
+ return {
505
+ raf: requestAnimationFrame(cb)
506
+ };
507
+ }
508
+ return {
509
+ timer: setTimeout(cb, 0)
510
+ };
511
+ }
512
+ function cancelFrame(token) {
513
+ if ('raf' in token) {
514
+ if (typeof cancelAnimationFrame === 'function') cancelAnimationFrame(token.raf);
515
+ return;
516
+ }
517
+ clearTimeout(token.timer);
518
+ }
212
519
  function ActionRow({
213
520
  actions
214
521
  }) {
@@ -343,6 +650,50 @@ function AutoMountedDialog({
343
650
  onClose: onResolve
344
651
  });
345
652
  }
653
+ const sheetStyles = {
654
+ root: {
655
+ position: 'fixed',
656
+ top: 0,
657
+ left: 0,
658
+ right: 0,
659
+ bottom: 0,
660
+ zIndex: dialogZIndex.backdrop
661
+ },
662
+ backdrop: {
663
+ position: 'absolute',
664
+ top: 0,
665
+ left: 0,
666
+ right: 0,
667
+ bottom: 0,
668
+ backgroundColor: '#000'
669
+ },
670
+ panel: {
671
+ position: 'absolute',
672
+ overflow: 'hidden',
673
+ shadowOpacity: 0.18,
674
+ shadowRadius: 24,
675
+ shadowOffset: {
676
+ width: 0,
677
+ height: 8
678
+ },
679
+ zIndex: dialogZIndex.surface
680
+ },
681
+ handleRow: {
682
+ width: '100%',
683
+ alignItems: 'center',
684
+ paddingTop: 8,
685
+ paddingBottom: 4
686
+ },
687
+ handle: {
688
+ width: _placement.HANDLE_WIDTH,
689
+ height: _placement.HANDLE_HEIGHT,
690
+ borderRadius: _placement.HANDLE_RADIUS,
691
+ opacity: 0.5
692
+ },
693
+ body: {
694
+ padding: 20
695
+ }
696
+ };
346
697
 
347
698
  /**
348
699
  * CSS keyframes required for web dialog animations.
@@ -354,6 +705,9 @@ function AutoMountedDialog({
354
705
  * @keyframes bloomDialogZoomFadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
355
706
  * @keyframes bloomDialogZoomFadeOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.95); } }
356
707
  * ```
708
+ *
709
+ * The `left`/`right`/`bottom` placements do NOT depend on this — they animate
710
+ * via self-contained inline CSS transitions and need no keyframe injection.
357
711
  */
358
712
  const BLOOM_DIALOG_CSS = exports.BLOOM_DIALOG_CSS = `
359
713
  @keyframes bloomDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }