@oxyhq/bloom 0.15.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 (188) 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 +1 -2
  14. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  15. package/lib/commonjs/dialog/Dialog.web.js +9 -6
  16. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  17. package/lib/commonjs/dialog/index.js +0 -13
  18. package/lib/commonjs/dialog/index.js.map +1 -1
  19. package/lib/commonjs/dialog/index.web.js +0 -19
  20. package/lib/commonjs/dialog/index.web.js.map +1 -1
  21. package/lib/commonjs/index.js +0 -29
  22. package/lib/commonjs/index.js.map +1 -1
  23. package/lib/commonjs/index.web.js +6 -35
  24. package/lib/commonjs/index.web.js.map +1 -1
  25. package/lib/module/alert-dialog/AlertDialog.js +35 -27
  26. package/lib/module/alert-dialog/AlertDialog.js.map +1 -1
  27. package/lib/module/alert-dialog/index.js +2 -2
  28. package/lib/module/alert-dialog/index.js.map +1 -1
  29. package/lib/module/alert-dialog/index.web.js +2 -2
  30. package/lib/module/alert-dialog/index.web.js.map +1 -1
  31. package/lib/module/command/Command.js +27 -16
  32. package/lib/module/command/Command.js.map +1 -1
  33. package/lib/module/command/index.js +2 -2
  34. package/lib/module/command/index.js.map +1 -1
  35. package/lib/module/command/index.web.js +2 -2
  36. package/lib/module/command/index.web.js.map +1 -1
  37. package/lib/module/dialog/Dialog.js +1 -2
  38. package/lib/module/dialog/Dialog.js.map +1 -1
  39. package/lib/module/dialog/Dialog.web.js +9 -6
  40. package/lib/module/dialog/Dialog.web.js.map +1 -1
  41. package/lib/module/dialog/index.js +0 -1
  42. package/lib/module/dialog/index.js.map +1 -1
  43. package/lib/module/dialog/index.web.js +0 -1
  44. package/lib/module/dialog/index.web.js.map +1 -1
  45. package/lib/module/index.js +1 -3
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/index.web.js +1 -3
  48. package/lib/module/index.web.js.map +1 -1
  49. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts +13 -12
  50. package/lib/typescript/commonjs/alert-dialog/AlertDialog.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/alert-dialog/index.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/alert-dialog/index.web.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/command/Command.d.ts +8 -8
  54. package/lib/typescript/commonjs/command/Command.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/command/index.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/command/index.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/dialog/Dialog.d.ts +1 -2
  58. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +2 -2
  60. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  61. package/lib/typescript/commonjs/dialog/index.d.ts +0 -2
  62. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/dialog/index.web.d.ts +0 -2
  64. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/index.d.ts +2 -4
  66. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/index.web.d.ts +2 -4
  68. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  69. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts +13 -12
  70. package/lib/typescript/module/alert-dialog/AlertDialog.d.ts.map +1 -1
  71. package/lib/typescript/module/alert-dialog/index.d.ts.map +1 -1
  72. package/lib/typescript/module/alert-dialog/index.web.d.ts.map +1 -1
  73. package/lib/typescript/module/command/Command.d.ts +8 -8
  74. package/lib/typescript/module/command/Command.d.ts.map +1 -1
  75. package/lib/typescript/module/command/index.d.ts.map +1 -1
  76. package/lib/typescript/module/command/index.web.d.ts.map +1 -1
  77. package/lib/typescript/module/dialog/Dialog.d.ts +1 -2
  78. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  79. package/lib/typescript/module/dialog/Dialog.web.d.ts +2 -2
  80. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  81. package/lib/typescript/module/dialog/index.d.ts +0 -2
  82. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  83. package/lib/typescript/module/dialog/index.web.d.ts +0 -2
  84. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  85. package/lib/typescript/module/index.d.ts +2 -4
  86. package/lib/typescript/module/index.d.ts.map +1 -1
  87. package/lib/typescript/module/index.web.d.ts +2 -4
  88. package/lib/typescript/module/index.web.d.ts.map +1 -1
  89. package/package.json +1 -18
  90. package/src/__tests__/AlertDialog.test.tsx +122 -0
  91. package/src/__tests__/Command.test.tsx +107 -0
  92. package/src/alert-dialog/AlertDialog.tsx +46 -36
  93. package/src/alert-dialog/index.ts +2 -2
  94. package/src/alert-dialog/index.web.ts +2 -2
  95. package/src/command/Command.tsx +30 -19
  96. package/src/command/index.ts +2 -2
  97. package/src/command/index.web.ts +2 -2
  98. package/src/dialog/Dialog.tsx +1 -2
  99. package/src/dialog/Dialog.web.tsx +9 -6
  100. package/src/dialog/index.ts +0 -2
  101. package/src/dialog/index.web.ts +0 -6
  102. package/src/index.ts +0 -11
  103. package/src/index.web.ts +0 -11
  104. package/lib/commonjs/dialog/CenteredDialog.js +0 -252
  105. package/lib/commonjs/dialog/CenteredDialog.js.map +0 -1
  106. package/lib/commonjs/dialog/CenteredDialog.web.js +0 -330
  107. package/lib/commonjs/dialog/CenteredDialog.web.js.map +0 -1
  108. package/lib/commonjs/dialog/centered-dialog-tokens.js +0 -62
  109. package/lib/commonjs/dialog/centered-dialog-tokens.js.map +0 -1
  110. package/lib/commonjs/dialog/centered-dialog-types.js +0 -6
  111. package/lib/commonjs/dialog/centered-dialog-types.js.map +0 -1
  112. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js +0 -97
  113. package/lib/commonjs/responsive-sheet/ResponsiveSheet.js.map +0 -1
  114. package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js +0 -84
  115. package/lib/commonjs/responsive-sheet/ResponsiveSheet.web.js.map +0 -1
  116. package/lib/commonjs/responsive-sheet/index.js +0 -19
  117. package/lib/commonjs/responsive-sheet/index.js.map +0 -1
  118. package/lib/commonjs/responsive-sheet/index.web.js +0 -19
  119. package/lib/commonjs/responsive-sheet/index.web.js.map +0 -1
  120. package/lib/commonjs/responsive-sheet/types.js +0 -6
  121. package/lib/commonjs/responsive-sheet/types.js.map +0 -1
  122. package/lib/module/dialog/CenteredDialog.js +0 -246
  123. package/lib/module/dialog/CenteredDialog.js.map +0 -1
  124. package/lib/module/dialog/CenteredDialog.web.js +0 -324
  125. package/lib/module/dialog/CenteredDialog.web.js.map +0 -1
  126. package/lib/module/dialog/centered-dialog-tokens.js +0 -58
  127. package/lib/module/dialog/centered-dialog-tokens.js.map +0 -1
  128. package/lib/module/dialog/centered-dialog-types.js +0 -4
  129. package/lib/module/dialog/centered-dialog-types.js.map +0 -1
  130. package/lib/module/responsive-sheet/ResponsiveSheet.js +0 -90
  131. package/lib/module/responsive-sheet/ResponsiveSheet.js.map +0 -1
  132. package/lib/module/responsive-sheet/ResponsiveSheet.web.js +0 -77
  133. package/lib/module/responsive-sheet/ResponsiveSheet.web.js.map +0 -1
  134. package/lib/module/responsive-sheet/index.js +0 -4
  135. package/lib/module/responsive-sheet/index.js.map +0 -1
  136. package/lib/module/responsive-sheet/index.web.js +0 -12
  137. package/lib/module/responsive-sheet/index.web.js.map +0 -1
  138. package/lib/module/responsive-sheet/types.js +0 -4
  139. package/lib/module/responsive-sheet/types.js.map +0 -1
  140. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts +0 -22
  141. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts.map +0 -1
  142. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts +0 -31
  143. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +0 -1
  144. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts +0 -47
  145. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts.map +0 -1
  146. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts +0 -74
  147. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts.map +0 -1
  148. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts +0 -22
  149. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  150. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts +0 -16
  151. package/lib/typescript/commonjs/responsive-sheet/ResponsiveSheet.web.d.ts.map +0 -1
  152. package/lib/typescript/commonjs/responsive-sheet/index.d.ts +0 -3
  153. package/lib/typescript/commonjs/responsive-sheet/index.d.ts.map +0 -1
  154. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts +0 -3
  155. package/lib/typescript/commonjs/responsive-sheet/index.web.d.ts.map +0 -1
  156. package/lib/typescript/commonjs/responsive-sheet/types.d.ts +0 -58
  157. package/lib/typescript/commonjs/responsive-sheet/types.d.ts.map +0 -1
  158. package/lib/typescript/module/dialog/CenteredDialog.d.ts +0 -22
  159. package/lib/typescript/module/dialog/CenteredDialog.d.ts.map +0 -1
  160. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts +0 -31
  161. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +0 -1
  162. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts +0 -47
  163. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts.map +0 -1
  164. package/lib/typescript/module/dialog/centered-dialog-types.d.ts +0 -74
  165. package/lib/typescript/module/dialog/centered-dialog-types.d.ts.map +0 -1
  166. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts +0 -22
  167. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.d.ts.map +0 -1
  168. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts +0 -16
  169. package/lib/typescript/module/responsive-sheet/ResponsiveSheet.web.d.ts.map +0 -1
  170. package/lib/typescript/module/responsive-sheet/index.d.ts +0 -3
  171. package/lib/typescript/module/responsive-sheet/index.d.ts.map +0 -1
  172. package/lib/typescript/module/responsive-sheet/index.web.d.ts +0 -3
  173. package/lib/typescript/module/responsive-sheet/index.web.d.ts.map +0 -1
  174. package/lib/typescript/module/responsive-sheet/types.d.ts +0 -58
  175. package/lib/typescript/module/responsive-sheet/types.d.ts.map +0 -1
  176. package/src/__tests__/CenteredDialog.test.tsx +0 -108
  177. package/src/__tests__/ResponsiveSheet.test.tsx +0 -88
  178. package/src/dialog/CenteredDialog.stories.tsx +0 -125
  179. package/src/dialog/CenteredDialog.tsx +0 -305
  180. package/src/dialog/CenteredDialog.web.tsx +0 -374
  181. package/src/dialog/centered-dialog-tokens.ts +0 -68
  182. package/src/dialog/centered-dialog-types.ts +0 -74
  183. package/src/responsive-sheet/ResponsiveSheet.stories.tsx +0 -54
  184. package/src/responsive-sheet/ResponsiveSheet.tsx +0 -88
  185. package/src/responsive-sheet/ResponsiveSheet.web.tsx +0 -76
  186. package/src/responsive-sheet/index.ts +0 -6
  187. package/src/responsive-sheet/index.web.ts +0 -14
  188. package/src/responsive-sheet/types.ts +0 -60
@@ -1,374 +0,0 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
2
- import { Pressable, ScrollView, StyleSheet, View, type ViewStyle } from 'react-native';
3
- import { RemoveScrollBar } from 'react-remove-scroll-bar';
4
-
5
- import { useInteractionStates } from '../hooks/useInteractionState';
6
- import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '../icons/Times';
7
- import { Portal } from '../portal/index.web';
8
- import { createOverlayZIndex } from '../styles/z-index';
9
- import { useTheme } from '../theme/use-theme';
10
- import { Text } from '../typography';
11
- import type { CenteredDialogProps } from './centered-dialog-types';
12
- import {
13
- CARD_RADIUS,
14
- DEFAULT_MAX_WIDTH,
15
- MAX_HEIGHT_FRACTION,
16
- resolveSpacing,
17
- VIEWPORT_GUTTER,
18
- } from './centered-dialog-tokens';
19
-
20
- const FADE_OUT_DURATION = 150;
21
- const CLOSE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
22
- const dialogZIndex = createOverlayZIndex();
23
-
24
- /** Stable testID for the dimmed backdrop. Overridden when a `testID` is set. */
25
- export const CENTERED_DIALOG_BACKDROP_TESTID = 'bloom-centered-dialog-backdrop';
26
-
27
- const stopPropagation = (e: { stopPropagation: () => void }) => e.stopPropagation();
28
-
29
- /**
30
- * @deprecated Use `<Dialog placement="center">` instead.
31
- *
32
- * Web variant of `<CenteredDialog>`.
33
- *
34
- * A centered modal card rendered into the Bloom Portal at the end of
35
- * `document.body`. Identical prop surface to native — `visible`, `onClose`,
36
- * `title`, `compact`, etc. — so call sites are platform agnostic.
37
- *
38
- * Differences from the native fork (necessary, not behavioural):
39
- * - Portal + `RemoveScrollBar` lock the underlying page while open.
40
- * - `Escape` dismisses (when `dismissible`).
41
- * - A `role="dialog"` panel with `aria-label`/`aria-modal` for a11y.
42
- * - A CSS fade/zoom keyframe matches the native modal's `fade` animation;
43
- * the keyframes are injected once (idempotent) into `<head>`.
44
- *
45
- * The backdrop is a sibling element BEHIND the card, and the card stops
46
- * click/press propagation so a tap on the card never dismisses.
47
- */
48
- export function CenteredDialog({
49
- visible,
50
- onClose,
51
- title,
52
- dismissible = true,
53
- maxWidth = DEFAULT_MAX_WIDTH,
54
- compact = true,
55
- showClose,
56
- children,
57
- footer,
58
- accessibilityLabel,
59
- closeAccessibilityLabel = 'Close dialog',
60
- backdropAccessibilityLabel = 'Dismiss dialog',
61
- cardStyle,
62
- contentStyle,
63
- testID,
64
- }: CenteredDialogProps) {
65
- const theme = useTheme();
66
- // `rendered` keeps the dialog mounted through its fade-out so a controlled
67
- // `visible` flip to `false` still plays an exit animation (matching the
68
- // native `Modal animationType="fade"`, which animates both directions).
69
- const [rendered, setRendered] = useState(visible);
70
- const isClosing = rendered && !visible;
71
-
72
- useKeyframes();
73
-
74
- // Mount immediately when opened; defer unmount until the exit animation
75
- // has played when closed.
76
- useEffect(() => {
77
- if (visible) {
78
- setRendered(true);
79
- return;
80
- }
81
- if (!rendered) return;
82
- const timer = setTimeout(() => setRendered(false), FADE_OUT_DURATION);
83
- return () => clearTimeout(timer);
84
- }, [visible, rendered]);
85
-
86
- // Escape-to-close while open. Scoped to the open lifetime so stacked
87
- // dialogs don't fight for the keydown — the top-most one wins via
88
- // document-level event order.
89
- useEffect(() => {
90
- if (!visible || !dismissible || typeof document === 'undefined') return;
91
- const handler = (e: KeyboardEvent) => {
92
- if (e.key === 'Escape') {
93
- e.stopPropagation();
94
- onClose();
95
- }
96
- };
97
- document.addEventListener('keydown', handler);
98
- return () => document.removeEventListener('keydown', handler);
99
- }, [visible, dismissible, onClose]);
100
-
101
- const spacing = useMemo(() => resolveSpacing(compact), [compact]);
102
- const resolvedShowClose = showClose ?? Boolean(title);
103
- const hasHeader = Boolean(title) || resolvedShowClose;
104
-
105
- if (!rendered) return null;
106
-
107
- // While closing, the backdrop must not re-trigger onClose (it's mid-exit).
108
- const handleBackdropPress = dismissible && !isClosing ? onClose : undefined;
109
-
110
- return (
111
- <Portal>
112
- <RemoveScrollBar />
113
- {/* `pointerEvents: 'auto'` opts back in from the Portal root's
114
- `pointer-events: none` (set so the idle portal doesn't intercept
115
- clicks on the underlying app). */}
116
- <View style={[styles.root, { padding: VIEWPORT_GUTTER }]}>
117
- <Pressable
118
- testID={testID ? `${testID}-backdrop` : CENTERED_DIALOG_BACKDROP_TESTID}
119
- onPress={handleBackdropPress}
120
- disabled={!dismissible}
121
- accessibilityRole="button"
122
- accessibilityLabel={backdropAccessibilityLabel}
123
- style={[
124
- styles.backdrop,
125
- { backgroundColor: theme.colors.overlay },
126
- backdropAnimation(isClosing),
127
- ]}
128
- />
129
-
130
- <View
131
- testID={testID}
132
- role="dialog"
133
- aria-modal
134
- aria-label={accessibilityLabel ?? title}
135
- onStartShouldSetResponder={returnsTrue}
136
- onResponderRelease={stopPropagation}
137
- {...({ onClick: stopPropagation } as Record<string, unknown>)}
138
- style={[
139
- styles.card,
140
- {
141
- maxWidth,
142
- maxHeight: `${MAX_HEIGHT_FRACTION * 100}%`,
143
- backgroundColor: theme.colors.card,
144
- borderColor: theme.colors.border,
145
- shadowColor: theme.colors.shadow,
146
- },
147
- cardAnimation(isClosing),
148
- cardStyle,
149
- ]}
150
- >
151
- {hasHeader ? (
152
- <DialogHeader
153
- title={title}
154
- showClose={resolvedShowClose}
155
- padding={spacing.pad}
156
- gap={spacing.headerGap}
157
- closeAccessibilityLabel={closeAccessibilityLabel}
158
- onClose={onClose}
159
- />
160
- ) : null}
161
-
162
- <ScrollView
163
- style={styles.bodyScroll}
164
- contentContainerStyle={[
165
- {
166
- padding: spacing.pad,
167
- paddingTop: hasHeader ? spacing.headerGap : spacing.pad,
168
- gap: spacing.contentGap,
169
- },
170
- contentStyle,
171
- ]}
172
- showsVerticalScrollIndicator={false}
173
- keyboardShouldPersistTaps="handled"
174
- >
175
- {children}
176
- </ScrollView>
177
-
178
- {footer ? (
179
- <View
180
- style={[
181
- styles.footer,
182
- {
183
- padding: spacing.pad,
184
- paddingTop: spacing.footerGap,
185
- borderTopColor: theme.colors.borderLight,
186
- },
187
- ]}
188
- >
189
- {footer}
190
- </View>
191
- ) : null}
192
- </View>
193
- </View>
194
- </Portal>
195
- );
196
- }
197
-
198
- function DialogHeader({
199
- title,
200
- showClose,
201
- padding,
202
- gap,
203
- closeAccessibilityLabel,
204
- onClose,
205
- }: {
206
- title?: string;
207
- showClose: boolean;
208
- padding: number;
209
- gap: number;
210
- closeAccessibilityLabel: string;
211
- onClose: () => void;
212
- }) {
213
- const theme = useTheme();
214
-
215
- return (
216
- <View
217
- style={[
218
- styles.header,
219
- { paddingHorizontal: padding, paddingTop: padding, gap },
220
- ]}
221
- >
222
- {title ? (
223
- <Text
224
- accessibilityRole="header"
225
- numberOfLines={2}
226
- style={[styles.title, { color: theme.colors.text }]}
227
- >
228
- {title}
229
- </Text>
230
- ) : (
231
- <View style={styles.titleSpacer} />
232
- )}
233
- {showClose ? (
234
- <CloseButton
235
- accessibilityLabel={closeAccessibilityLabel}
236
- onPress={onClose}
237
- />
238
- ) : null}
239
- </View>
240
- );
241
- }
242
-
243
- function CloseButton({
244
- accessibilityLabel,
245
- onPress,
246
- }: {
247
- accessibilityLabel: string;
248
- onPress: () => void;
249
- }) {
250
- const theme = useTheme();
251
- const { hovered, pressed, hoverHandlers, pressHandlers } = useInteractionStates();
252
-
253
- return (
254
- <Pressable
255
- onPress={onPress}
256
- {...hoverHandlers}
257
- {...pressHandlers}
258
- hitSlop={CLOSE_HIT_SLOP}
259
- accessibilityRole="button"
260
- accessibilityLabel={accessibilityLabel}
261
- style={[
262
- styles.closeButton,
263
- { backgroundColor: theme.colors.contrast50 },
264
- (hovered || pressed) && styles.closeButtonActive,
265
- ]}
266
- >
267
- <CloseIcon size="sm" fill={theme.colors.textSecondary} />
268
- </Pressable>
269
- );
270
- }
271
-
272
- const returnsTrue = () => true;
273
-
274
- function backdropAnimation(isClosing: boolean): ViewStyle {
275
- return isClosing
276
- ? ({ animation: `bloomCenteredDialogFadeOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
277
- : ({ animation: 'bloomCenteredDialogFadeIn ease-out 150ms' } as ViewStyle);
278
- }
279
-
280
- function cardAnimation(isClosing: boolean): ViewStyle {
281
- return isClosing
282
- ? ({ animation: `bloomCenteredDialogZoomOut ease-in ${FADE_OUT_DURATION}ms forwards` } as ViewStyle)
283
- : ({ animation: 'bloomCenteredDialogZoomIn cubic-bezier(0.16, 1, 0.3, 1) 200ms' } as ViewStyle);
284
- }
285
-
286
- const KEYFRAMES_ID = 'bloom-centered-dialog-keyframes';
287
-
288
- /**
289
- * CSS keyframes powering the web enter/exit animation. Injected once into
290
- * `<head>` on first mount (keyed by id so re-mounts and multiple dialogs
291
- * don't duplicate the rule). Mirrors the native modal's `fade` feel with a
292
- * subtle zoom on the card.
293
- */
294
- export const BLOOM_CENTERED_DIALOG_CSS = `
295
- @keyframes bloomCenteredDialogFadeIn { from { opacity: 0; } to { opacity: 1; } }
296
- @keyframes bloomCenteredDialogFadeOut { from { opacity: 1; } to { opacity: 0; } }
297
- @keyframes bloomCenteredDialogZoomIn { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } }
298
- @keyframes bloomCenteredDialogZoomOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.96); } }
299
- `;
300
-
301
- function useKeyframes(): void {
302
- useEffect(() => {
303
- if (typeof document === 'undefined') return;
304
- if (document.getElementById(KEYFRAMES_ID)) return;
305
- const style = document.createElement('style');
306
- style.id = KEYFRAMES_ID;
307
- style.textContent = BLOOM_CENTERED_DIALOG_CSS;
308
- document.head.appendChild(style);
309
- }, []);
310
- }
311
-
312
- const styles = StyleSheet.create({
313
- root: {
314
- position: 'fixed' as 'absolute',
315
- top: 0,
316
- left: 0,
317
- right: 0,
318
- bottom: 0,
319
- zIndex: dialogZIndex.backdrop,
320
- alignItems: 'center',
321
- justifyContent: 'center',
322
- pointerEvents: 'auto',
323
- },
324
- backdrop: {
325
- position: 'fixed' as 'absolute',
326
- top: 0,
327
- left: 0,
328
- right: 0,
329
- bottom: 0,
330
- },
331
- card: {
332
- position: 'relative',
333
- width: '100%',
334
- borderRadius: CARD_RADIUS,
335
- borderWidth: 1,
336
- overflow: 'hidden',
337
- shadowOpacity: 0.18,
338
- shadowRadius: 24,
339
- shadowOffset: { width: 0, height: 8 },
340
- zIndex: dialogZIndex.surface,
341
- },
342
- header: {
343
- flexDirection: 'row',
344
- alignItems: 'flex-start',
345
- justifyContent: 'space-between',
346
- },
347
- title: {
348
- flex: 1,
349
- fontSize: 17,
350
- fontWeight: '700',
351
- lineHeight: 22,
352
- },
353
- titleSpacer: {
354
- flex: 1,
355
- },
356
- closeButton: {
357
- width: 28,
358
- height: 28,
359
- borderRadius: 14,
360
- alignItems: 'center',
361
- justifyContent: 'center',
362
- cursor: 'pointer',
363
- },
364
- closeButtonActive: {
365
- opacity: 0.7,
366
- },
367
- bodyScroll: {
368
- flexGrow: 0,
369
- flexShrink: 1,
370
- },
371
- footer: {
372
- borderTopWidth: 1,
373
- },
374
- });
@@ -1,68 +0,0 @@
1
- import { borderRadius, space } from '../styles/tokens';
2
-
3
- /**
4
- * Shared layout constants for `<CenteredDialog>`, kept in one module so the
5
- * native and web forks render identically. All values are Bloom theme tokens
6
- * (`styles/tokens`) — no ad-hoc magic numbers.
7
- *
8
- * COMPACT is the priority: the `compact` spacing set is the default and is
9
- * deliberately tight (≈`space.md`), so the card reads snug out of the box.
10
- * The `cozy` set is the opt-out (`compact={false}`) for roomier dialogs.
11
- */
12
-
13
- /** Default max card width — comfortable for a compact dialog. */
14
- export const DEFAULT_MAX_WIDTH = 440;
15
-
16
- /** Card height is capped at this fraction of the viewport; body scrolls. */
17
- export const MAX_HEIGHT_FRACTION = 0.8;
18
-
19
- /** Card corner radius (token: `borderRadius.lg` = 16). */
20
- export const CARD_RADIUS = borderRadius.lg;
21
-
22
- /** Outer gutter between the card edge and the viewport edge (token `space.lg`). */
23
- export const VIEWPORT_GUTTER = space.lg;
24
-
25
- export type DialogSpacing = {
26
- /** Card inner padding (horizontal + vertical). */
27
- pad: number;
28
- /** Gap between the header title row and the body. */
29
- headerGap: number;
30
- /** Gap between the body and the footer. */
31
- footerGap: number;
32
- /** Vertical gap between stacked body items (the content container `gap`). */
33
- contentGap: number;
34
- };
35
-
36
- /**
37
- * Compact (default) — tight, snug spacing built on `space.md` (12).
38
- *
39
- * pad = space.md (12)
40
- * headerGap = space.sm (8)
41
- * footerGap = space.md (12)
42
- * contentGap = space.sm (8)
43
- */
44
- export const COMPACT_SPACING: DialogSpacing = {
45
- pad: space.md,
46
- headerGap: space.sm,
47
- footerGap: space.md,
48
- contentGap: space.sm,
49
- };
50
-
51
- /**
52
- * Cozy (`compact={false}`) — roomier spacing built on `space.xl` (20).
53
- *
54
- * pad = space.xl (20)
55
- * headerGap = space.md (12)
56
- * footerGap = space.lg (16)
57
- * contentGap = space.md (12)
58
- */
59
- export const COZY_SPACING: DialogSpacing = {
60
- pad: space.xl,
61
- headerGap: space.md,
62
- footerGap: space.lg,
63
- contentGap: space.md,
64
- };
65
-
66
- export function resolveSpacing(compact: boolean): DialogSpacing {
67
- return compact ? COMPACT_SPACING : COZY_SPACING;
68
- }
@@ -1,74 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { StyleProp, ViewStyle } from 'react-native';
3
-
4
- /**
5
- * @deprecated Use `<Dialog placement="center">` instead. `CenteredDialog` is
6
- * superseded by the centered placement of the canonical `<Dialog>` and is
7
- * slated for removal in a future major.
8
- *
9
- * Props for the declarative `<CenteredDialog>` primitive.
10
- *
11
- * This is the simple, controlled centered-modal surface — distinct from the
12
- * imperative `useDialogControl()` + `<Dialog control={…}>` API in the same
13
- * module. Reach for `<CenteredDialog>` when you already own the open/closed
14
- * boolean (a `useState`, a query flag, a route param) and just want Bloom to
15
- * render the dimmed backdrop + a snug, theme-styled centered card.
16
- *
17
- * All spacing/colour/radius come from Bloom theme tokens — the card reads
18
- * compact out of the box (`compact` defaults to `true`).
19
- */
20
- export type CenteredDialogProps = {
21
- /** Whether the dialog is mounted + visible. */
22
- visible: boolean;
23
- /**
24
- * Close request — fired on backdrop tap (when `dismissible`), the close
25
- * button, the hardware back button (Android), and `Escape` (web). The
26
- * caller owns the visible state, so it must flip `visible` to `false` here.
27
- */
28
- onClose: () => void;
29
- /**
30
- * Optional header title. When set (or when `showClose` is `true`) Bloom
31
- * renders a tight header row above the content.
32
- */
33
- title?: string;
34
- /**
35
- * Tap-outside / back-button / Escape dismisses the dialog. Defaults to
36
- * `true`. Set `false` for blocking flows (e.g. a destructive confirm that
37
- * must be answered via an explicit action).
38
- */
39
- dismissible?: boolean;
40
- /** Max card width in px. Defaults to {@link DEFAULT_MAX_WIDTH} (440). */
41
- maxWidth?: number;
42
- /**
43
- * Tight, snug spacing (the Bloom default). Set `false` for a roomier card
44
- * with larger padding and header/footer gaps. Defaults to `true`.
45
- */
46
- compact?: boolean;
47
- /**
48
- * Render a close (✕) icon button in the header. Defaults to `true` when a
49
- * `title` is provided, otherwise `false` (a chromeless card owns its own
50
- * dismissal affordances).
51
- */
52
- showClose?: boolean;
53
- /** Card body. Scrolls within the height cap when it overflows. */
54
- children?: ReactNode;
55
- /**
56
- * Optional footer slot for actions (e.g. a Bloom `<Button>` row). Rendered
57
- * below the scrollable body with a hairline separator.
58
- */
59
- footer?: ReactNode;
60
- /**
61
- * Accessibility label for the dialog surface. Falls back to `title` when
62
- * omitted.
63
- */
64
- accessibilityLabel?: string;
65
- /** Accessibility label for the header close (✕) button. */
66
- closeAccessibilityLabel?: string;
67
- /** Accessibility label for the dimmed backdrop's tap-to-dismiss affordance. */
68
- backdropAccessibilityLabel?: string;
69
- /** Style overrides merged onto the centered card container. */
70
- cardStyle?: StyleProp<ViewStyle>;
71
- /** Style overrides merged onto the scrollable content container. */
72
- contentStyle?: StyleProp<ViewStyle>;
73
- testID?: string;
74
- };
@@ -1,54 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { View } from 'react-native';
3
- import type { Meta, StoryObj } from '@storybook/react-vite';
4
-
5
- import { ResponsiveSheet } from './index';
6
- import type { ResponsiveSheetSide } from './types';
7
- import { Button } from '../button';
8
- import { Text } from '../typography';
9
-
10
- const meta: Meta = {
11
- title: 'Overlays/ResponsiveSheet',
12
- };
13
-
14
- export default meta;
15
-
16
- type Story = StoryObj;
17
-
18
- function Demo({ side }: { side: ResponsiveSheetSide }) {
19
- const [open, setOpen] = useState(false);
20
-
21
- return (
22
- <View style={{ padding: 24, minHeight: 480 }}>
23
- <Button onPress={() => setOpen(true)}>Open sheet ({side})</Button>
24
- <ResponsiveSheet
25
- open={open}
26
- onClose={() => setOpen(false)}
27
- side={side}
28
- width={420}
29
- inset={{ top: 16, bottom: 16, left: 16 }}
30
- label="Demo sheet"
31
- >
32
- <View style={{ padding: 20, gap: 12 }}>
33
- <Text style={{ fontSize: 20, fontWeight: '700' }}>Responsive sheet</Text>
34
- <Text>
35
- On a wide viewport this is an anchored side-sheet; below the
36
- breakpoint it becomes a bottom-sheet with a drag handle. Resize the
37
- preview to switch modes.
38
- </Text>
39
- <Button variant="secondary" onPress={() => setOpen(false)}>
40
- Close
41
- </Button>
42
- </View>
43
- </ResponsiveSheet>
44
- </View>
45
- );
46
- }
47
-
48
- export const SideLeft: Story = {
49
- render: () => <Demo side="left" />,
50
- };
51
-
52
- export const SideRight: Story = {
53
- render: () => <Demo side="right" />,
54
- };
@@ -1,88 +0,0 @@
1
- import React from 'react';
2
-
3
- import { Dialog } from '../dialog';
4
- import {
5
- BREAKPOINT_LG,
6
- BREAKPOINT_MD,
7
- BREAKPOINT_SM,
8
- BREAKPOINT_XL,
9
- DIALOG_SHEET_BACKDROP_TESTID,
10
- type ResponsiveDialogPlacement,
11
- } from '../dialog/placement';
12
- import type { ResponsiveSheetProps } from './types';
13
-
14
- /**
15
- * @deprecated Use `<Dialog placement={{ base: 'bottom', md: 'left' }}>` instead.
16
- *
17
- * `ResponsiveSheet` is now a thin wrapper that configures the canonical
18
- * `<Dialog>` — an anchored side-sheet on wide screens collapsing to a
19
- * bottom-sheet below the breakpoint. It holds ZERO motion/layout logic of its
20
- * own; it maps its props onto a responsive `Dialog` `placement`. The
21
- * `@oxyhq/bloom/responsive-sheet` subpath is retained for the migration window.
22
- *
23
- * This file is BOTH the package default entry (resolved by downstream `tsc`
24
- * and non-Metro web bundlers via the `browser` condition through `index.web`)
25
- * AND a clean delegation, so it carries no platform-specific imports.
26
- */
27
- export function ResponsiveSheet({
28
- open,
29
- onClose,
30
- side = 'left',
31
- breakpoint = BREAKPOINT_MD,
32
- width,
33
- maxHeightRatio,
34
- inset,
35
- showHandle,
36
- dismissOnBackdrop,
37
- panelStyle,
38
- panelClassName,
39
- containerStyle,
40
- containerClassName,
41
- label,
42
- children,
43
- }: ResponsiveSheetProps) {
44
- return (
45
- <Dialog
46
- open={open}
47
- onClose={onClose}
48
- placement={placementForSide(side, breakpoint)}
49
- width={width}
50
- maxHeightRatio={maxHeightRatio}
51
- inset={inset}
52
- showHandle={showHandle}
53
- dismissOnBackdrop={dismissOnBackdrop}
54
- panelStyle={panelStyle}
55
- panelClassName={panelClassName}
56
- containerStyle={containerStyle}
57
- containerClassName={containerClassName}
58
- label={label}
59
- >
60
- {children}
61
- </Dialog>
62
- );
63
- }
64
-
65
- /**
66
- * Build the `Dialog` `placement` map for a `ResponsiveSheet`: a bottom-sheet
67
- * below `breakpoint`, the anchored `side` at and above it. The `breakpoint`
68
- * (px) is bucketed to the nearest Tailwind key so the side-sheet engages at the
69
- * matching width.
70
- */
71
- function placementForSide(
72
- side: 'left' | 'right',
73
- breakpoint: number,
74
- ): ResponsiveDialogPlacement {
75
- if (breakpoint <= BREAKPOINT_SM) return { base: 'bottom', sm: side };
76
- if (breakpoint <= BREAKPOINT_MD) return { base: 'bottom', md: side };
77
- if (breakpoint <= BREAKPOINT_LG) return { base: 'bottom', lg: side };
78
- if (breakpoint <= BREAKPOINT_XL) return { base: 'bottom', xl: side };
79
- return { base: 'bottom', xl: side };
80
- }
81
-
82
- /**
83
- * @deprecated Stable testID for the dimmed backdrop dismiss button — now
84
- * provided by the underlying `<Dialog>`'s side/bottom backdrop.
85
- */
86
- export const RESPONSIVE_SHEET_BACKDROP_TESTID = DIALOG_SHEET_BACKDROP_TESTID;
87
-
88
- export default ResponsiveSheet;