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