@oxyhq/bloom 0.6.9 → 0.6.10

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 (75) hide show
  1. package/lib/commonjs/dialog/CenteredDialog.js +242 -0
  2. package/lib/commonjs/dialog/CenteredDialog.js.map +1 -0
  3. package/lib/commonjs/dialog/CenteredDialog.web.js +326 -0
  4. package/lib/commonjs/dialog/CenteredDialog.web.js.map +1 -0
  5. package/lib/commonjs/dialog/centered-dialog-tokens.js +62 -0
  6. package/lib/commonjs/dialog/centered-dialog-tokens.js.map +1 -0
  7. package/lib/commonjs/dialog/centered-dialog-types.js +6 -0
  8. package/lib/commonjs/dialog/centered-dialog-types.js.map +1 -0
  9. package/lib/commonjs/dialog/index.js +13 -0
  10. package/lib/commonjs/dialog/index.js.map +1 -1
  11. package/lib/commonjs/dialog/index.web.js +19 -0
  12. package/lib/commonjs/dialog/index.web.js.map +1 -1
  13. package/lib/commonjs/index.js +14 -0
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +14 -0
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/module/dialog/CenteredDialog.js +236 -0
  18. package/lib/module/dialog/CenteredDialog.js.map +1 -0
  19. package/lib/module/dialog/CenteredDialog.web.js +320 -0
  20. package/lib/module/dialog/CenteredDialog.web.js.map +1 -0
  21. package/lib/module/dialog/centered-dialog-tokens.js +58 -0
  22. package/lib/module/dialog/centered-dialog-tokens.js.map +1 -0
  23. package/lib/module/dialog/centered-dialog-types.js +4 -0
  24. package/lib/module/dialog/centered-dialog-types.js.map +1 -0
  25. package/lib/module/dialog/index.js +1 -0
  26. package/lib/module/dialog/index.js.map +1 -1
  27. package/lib/module/dialog/index.web.js +1 -0
  28. package/lib/module/dialog/index.web.js.map +1 -1
  29. package/lib/module/index.js +1 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/index.web.js +1 -1
  32. package/lib/module/index.web.js.map +1 -1
  33. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts +20 -0
  34. package/lib/typescript/commonjs/dialog/CenteredDialog.d.ts.map +1 -0
  35. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts +29 -0
  36. package/lib/typescript/commonjs/dialog/CenteredDialog.web.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts +47 -0
  38. package/lib/typescript/commonjs/dialog/centered-dialog-tokens.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts +70 -0
  40. package/lib/typescript/commonjs/dialog/centered-dialog-types.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/dialog/index.d.ts +2 -0
  42. package/lib/typescript/commonjs/dialog/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/dialog/index.web.d.ts +2 -0
  44. package/lib/typescript/commonjs/dialog/index.web.d.ts.map +1 -1
  45. package/lib/typescript/commonjs/index.d.ts +2 -2
  46. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/index.web.d.ts +2 -2
  48. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  49. package/lib/typescript/module/dialog/CenteredDialog.d.ts +20 -0
  50. package/lib/typescript/module/dialog/CenteredDialog.d.ts.map +1 -0
  51. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts +29 -0
  52. package/lib/typescript/module/dialog/CenteredDialog.web.d.ts.map +1 -0
  53. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts +47 -0
  54. package/lib/typescript/module/dialog/centered-dialog-tokens.d.ts.map +1 -0
  55. package/lib/typescript/module/dialog/centered-dialog-types.d.ts +70 -0
  56. package/lib/typescript/module/dialog/centered-dialog-types.d.ts.map +1 -0
  57. package/lib/typescript/module/dialog/index.d.ts +2 -0
  58. package/lib/typescript/module/dialog/index.d.ts.map +1 -1
  59. package/lib/typescript/module/dialog/index.web.d.ts +2 -0
  60. package/lib/typescript/module/dialog/index.web.d.ts.map +1 -1
  61. package/lib/typescript/module/index.d.ts +2 -2
  62. package/lib/typescript/module/index.d.ts.map +1 -1
  63. package/lib/typescript/module/index.web.d.ts +2 -2
  64. package/lib/typescript/module/index.web.d.ts.map +1 -1
  65. package/package.json +1 -1
  66. package/src/__tests__/CenteredDialog.test.tsx +108 -0
  67. package/src/dialog/CenteredDialog.stories.tsx +125 -0
  68. package/src/dialog/CenteredDialog.tsx +295 -0
  69. package/src/dialog/CenteredDialog.web.tsx +370 -0
  70. package/src/dialog/centered-dialog-tokens.ts +68 -0
  71. package/src/dialog/centered-dialog-types.ts +70 -0
  72. package/src/dialog/index.ts +2 -0
  73. package/src/dialog/index.web.ts +6 -0
  74. package/src/index.ts +3 -0
  75. package/src/index.web.ts +3 -0
@@ -0,0 +1,125 @@
1
+ import React, { useState } from 'react';
2
+ import { Text, View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { Button } from '../button';
6
+ import { CenteredDialog } from './CenteredDialog';
7
+
8
+ const meta: Meta<typeof CenteredDialog> = {
9
+ title: 'Components/CenteredDialog',
10
+ component: CenteredDialog,
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof CenteredDialog>;
16
+
17
+ function CompactDemo() {
18
+ const [visible, setVisible] = useState(false);
19
+ return (
20
+ <>
21
+ <Button onPress={() => setVisible(true)}>Open compact dialog</Button>
22
+ <CenteredDialog
23
+ visible={visible}
24
+ onClose={() => setVisible(false)}
25
+ title="Delete project?"
26
+ footer={
27
+ <View style={{ flexDirection: 'row', justifyContent: 'flex-end', gap: 8 }}>
28
+ <Button variant="ghost" size="small" onPress={() => setVisible(false)}>
29
+ Cancel
30
+ </Button>
31
+ <Button variant="primary" size="small" onPress={() => setVisible(false)}>
32
+ Delete
33
+ </Button>
34
+ </View>
35
+ }
36
+ >
37
+ <Text>This action cannot be undone. The project and all its data will be permanently removed.</Text>
38
+ </CenteredDialog>
39
+ </>
40
+ );
41
+ }
42
+
43
+ function CozyDemo() {
44
+ const [visible, setVisible] = useState(false);
45
+ return (
46
+ <>
47
+ <Button onPress={() => setVisible(true)}>Open cozy dialog</Button>
48
+ <CenteredDialog
49
+ visible={visible}
50
+ onClose={() => setVisible(false)}
51
+ title="Roomier layout"
52
+ compact={false}
53
+ >
54
+ <Text>compact={'{false}'} relaxes the padding and header/footer gaps for content-heavy dialogs.</Text>
55
+ </CenteredDialog>
56
+ </>
57
+ );
58
+ }
59
+
60
+ function ChromelessDemo() {
61
+ const [visible, setVisible] = useState(false);
62
+ return (
63
+ <>
64
+ <Button onPress={() => setVisible(true)}>Open chromeless dialog</Button>
65
+ <CenteredDialog visible={visible} onClose={() => setVisible(false)}>
66
+ <View style={{ gap: 12 }}>
67
+ <Text>No title, no close button — the card owns every pixel.</Text>
68
+ <Button variant="secondary" size="small" onPress={() => setVisible(false)}>
69
+ Done
70
+ </Button>
71
+ </View>
72
+ </CenteredDialog>
73
+ </>
74
+ );
75
+ }
76
+
77
+ function NonDismissibleDemo() {
78
+ const [visible, setVisible] = useState(false);
79
+ return (
80
+ <>
81
+ <Button onPress={() => setVisible(true)}>Open blocking dialog</Button>
82
+ <CenteredDialog
83
+ visible={visible}
84
+ onClose={() => setVisible(false)}
85
+ title="Action required"
86
+ dismissible={false}
87
+ showClose={false}
88
+ footer={
89
+ <Button size="small" onPress={() => setVisible(false)}>
90
+ Acknowledge
91
+ </Button>
92
+ }
93
+ >
94
+ <Text>Backdrop tap and Escape are disabled — the user must choose an action.</Text>
95
+ </CenteredDialog>
96
+ </>
97
+ );
98
+ }
99
+
100
+ export const Compact: Story = {
101
+ render: () => <CompactDemo />,
102
+ };
103
+
104
+ export const Cozy: Story = {
105
+ render: () => <CozyDemo />,
106
+ };
107
+
108
+ export const Chromeless: Story = {
109
+ render: () => <ChromelessDemo />,
110
+ };
111
+
112
+ export const NonDismissible: Story = {
113
+ render: () => <NonDismissibleDemo />,
114
+ };
115
+
116
+ export const Gallery: Story = {
117
+ render: () => (
118
+ <View style={{ gap: 12, alignItems: 'flex-start' }}>
119
+ <CompactDemo />
120
+ <CozyDemo />
121
+ <ChromelessDemo />
122
+ <NonDismissibleDemo />
123
+ </View>
124
+ ),
125
+ };
@@ -0,0 +1,295 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ Modal,
4
+ Pressable,
5
+ ScrollView,
6
+ StyleSheet,
7
+ View,
8
+ } from 'react-native';
9
+
10
+ import { useInteractionState } from '../hooks/useInteractionState';
11
+ import { TimesLarge_Stroke2_Corner0_Rounded as CloseIcon } from '../icons/Times';
12
+ import { useTheme } from '../theme/use-theme';
13
+ import { Text } from '../typography';
14
+ import type { CenteredDialogProps } from './centered-dialog-types';
15
+ import {
16
+ CARD_RADIUS,
17
+ DEFAULT_MAX_WIDTH,
18
+ MAX_HEIGHT_FRACTION,
19
+ resolveSpacing,
20
+ VIEWPORT_GUTTER,
21
+ } from './centered-dialog-tokens';
22
+
23
+ const CLOSE_HIT_SLOP = { top: 8, bottom: 8, left: 8, right: 8 } as const;
24
+
25
+ /** Stable testID for the dimmed backdrop. Overridden when a `testID` is set. */
26
+ export const CENTERED_DIALOG_BACKDROP_TESTID = 'bloom-centered-dialog-backdrop';
27
+
28
+ /**
29
+ * Native variant of `<CenteredDialog>`.
30
+ *
31
+ * A controlled, centered modal: a transparent RN `Modal` (fade animation,
32
+ * own native window) hosting a dimmed full-screen backdrop and a centered,
33
+ * rounded card styled entirely from Bloom theme tokens.
34
+ *
35
+ * The backdrop is a sibling `Pressable` rendered BEHIND the card (absolute
36
+ * filled), never a parent of it — so a tap on the card never bubbles to the
37
+ * backdrop's close handler and the card never ends up nested inside the
38
+ * backdrop's pressable on web.
39
+ *
40
+ * Dismissal: backdrop tap (when `dismissible`), the header close button, and
41
+ * the Android hardware back button (`onRequestClose`) all call `onClose`.
42
+ */
43
+ export function CenteredDialog({
44
+ visible,
45
+ onClose,
46
+ title,
47
+ dismissible = true,
48
+ maxWidth = DEFAULT_MAX_WIDTH,
49
+ compact = true,
50
+ showClose,
51
+ children,
52
+ footer,
53
+ accessibilityLabel,
54
+ closeAccessibilityLabel = 'Close dialog',
55
+ backdropAccessibilityLabel = 'Dismiss dialog',
56
+ cardStyle,
57
+ contentStyle,
58
+ testID,
59
+ }: CenteredDialogProps) {
60
+ const theme = useTheme();
61
+ const backdropPress = useInteractionState();
62
+
63
+ const spacing = useMemo(() => resolveSpacing(compact), [compact]);
64
+ const resolvedShowClose = showClose ?? Boolean(title);
65
+ const hasHeader = Boolean(title) || resolvedShowClose;
66
+
67
+ const handleBackdropPress = dismissible ? onClose : undefined;
68
+ // Android's hardware back must always be answerable; when the dialog is
69
+ // non-dismissible we still need a handler (RN requires one for transparent
70
+ // modals) — make it a no-op so back does nothing rather than crash.
71
+ const handleRequestClose = dismissible ? onClose : noop;
72
+
73
+ return (
74
+ <Modal
75
+ visible={visible}
76
+ transparent
77
+ animationType="fade"
78
+ statusBarTranslucent
79
+ onRequestClose={handleRequestClose}
80
+ >
81
+ <View style={[styles.root, { padding: VIEWPORT_GUTTER }]}>
82
+ <Pressable
83
+ testID={testID ? `${testID}-backdrop` : CENTERED_DIALOG_BACKDROP_TESTID}
84
+ style={[
85
+ styles.backdrop,
86
+ { backgroundColor: theme.colors.overlay },
87
+ backdropPress.state && styles.backdropPressed,
88
+ ]}
89
+ onPress={handleBackdropPress}
90
+ onPressIn={backdropPress.onIn}
91
+ onPressOut={backdropPress.onOut}
92
+ disabled={!dismissible}
93
+ accessibilityRole="button"
94
+ accessibilityLabel={backdropAccessibilityLabel}
95
+ />
96
+
97
+ {/* RN's `Modal` already isolates its contents in a separate
98
+ accessibility window, so an explicit `accessibilityViewIsModal`
99
+ on the card is redundant here (and would mark the sibling
100
+ backdrop inaccessible). The web fork uses `aria-modal` instead. */}
101
+ <View
102
+ testID={testID}
103
+ accessibilityLabel={accessibilityLabel ?? title}
104
+ style={[
105
+ styles.card,
106
+ {
107
+ maxWidth,
108
+ maxHeight: `${MAX_HEIGHT_FRACTION * 100}%`,
109
+ backgroundColor: theme.colors.card,
110
+ borderColor: theme.colors.border,
111
+ shadowColor: theme.colors.shadow,
112
+ },
113
+ cardStyle,
114
+ ]}
115
+ >
116
+ {hasHeader ? (
117
+ <DialogHeader
118
+ title={title}
119
+ showClose={resolvedShowClose}
120
+ padding={spacing.pad}
121
+ gap={spacing.headerGap}
122
+ closeAccessibilityLabel={closeAccessibilityLabel}
123
+ onClose={onClose}
124
+ />
125
+ ) : null}
126
+
127
+ <ScrollView
128
+ style={styles.bodyScroll}
129
+ contentContainerStyle={[
130
+ {
131
+ padding: spacing.pad,
132
+ paddingTop: hasHeader ? spacing.headerGap : spacing.pad,
133
+ gap: spacing.contentGap,
134
+ },
135
+ contentStyle,
136
+ ]}
137
+ showsVerticalScrollIndicator={false}
138
+ keyboardShouldPersistTaps="handled"
139
+ >
140
+ {children}
141
+ </ScrollView>
142
+
143
+ {footer ? (
144
+ <View
145
+ style={[
146
+ styles.footer,
147
+ {
148
+ padding: spacing.pad,
149
+ paddingTop: spacing.footerGap,
150
+ borderTopColor: theme.colors.borderLight,
151
+ },
152
+ ]}
153
+ >
154
+ {footer}
155
+ </View>
156
+ ) : null}
157
+ </View>
158
+ </View>
159
+ </Modal>
160
+ );
161
+ }
162
+
163
+ function DialogHeader({
164
+ title,
165
+ showClose,
166
+ padding,
167
+ gap,
168
+ closeAccessibilityLabel,
169
+ onClose,
170
+ }: {
171
+ title?: string;
172
+ showClose: boolean;
173
+ padding: number;
174
+ gap: number;
175
+ closeAccessibilityLabel: string;
176
+ onClose: () => void;
177
+ }) {
178
+ const theme = useTheme();
179
+
180
+ return (
181
+ <View
182
+ style={[
183
+ styles.header,
184
+ { paddingHorizontal: padding, paddingTop: padding, gap },
185
+ ]}
186
+ >
187
+ {title ? (
188
+ <Text
189
+ accessibilityRole="header"
190
+ numberOfLines={2}
191
+ style={[styles.title, { color: theme.colors.text }]}
192
+ >
193
+ {title}
194
+ </Text>
195
+ ) : (
196
+ <View style={styles.titleSpacer} />
197
+ )}
198
+ {showClose ? (
199
+ <CloseButton
200
+ accessibilityLabel={closeAccessibilityLabel}
201
+ onPress={onClose}
202
+ />
203
+ ) : null}
204
+ </View>
205
+ );
206
+ }
207
+
208
+ function CloseButton({
209
+ accessibilityLabel,
210
+ onPress,
211
+ }: {
212
+ accessibilityLabel: string;
213
+ onPress: () => void;
214
+ }) {
215
+ const theme = useTheme();
216
+ const press = useInteractionState();
217
+
218
+ return (
219
+ <Pressable
220
+ onPress={onPress}
221
+ onPressIn={press.onIn}
222
+ onPressOut={press.onOut}
223
+ hitSlop={CLOSE_HIT_SLOP}
224
+ accessibilityRole="button"
225
+ accessibilityLabel={accessibilityLabel}
226
+ style={[
227
+ styles.closeButton,
228
+ { backgroundColor: theme.colors.contrast50 },
229
+ press.state && styles.closeButtonPressed,
230
+ ]}
231
+ >
232
+ <CloseIcon size="sm" fill={theme.colors.textSecondary} />
233
+ </Pressable>
234
+ );
235
+ }
236
+
237
+ function noop(): void {
238
+ /* non-dismissible: hardware back is intentionally inert */
239
+ }
240
+
241
+ const styles = StyleSheet.create({
242
+ root: {
243
+ flex: 1,
244
+ alignItems: 'center',
245
+ justifyContent: 'center',
246
+ },
247
+ backdrop: {
248
+ ...StyleSheet.absoluteFillObject,
249
+ },
250
+ // A subtle deepening on press confirms the tap-to-dismiss affordance.
251
+ backdropPressed: {
252
+ opacity: 0.92,
253
+ },
254
+ card: {
255
+ width: '100%',
256
+ borderRadius: CARD_RADIUS,
257
+ borderWidth: StyleSheet.hairlineWidth,
258
+ overflow: 'hidden',
259
+ shadowOpacity: 0.18,
260
+ shadowRadius: 24,
261
+ shadowOffset: { width: 0, height: 8 },
262
+ elevation: 12,
263
+ },
264
+ header: {
265
+ flexDirection: 'row',
266
+ alignItems: 'flex-start',
267
+ justifyContent: 'space-between',
268
+ },
269
+ title: {
270
+ flex: 1,
271
+ fontSize: 17,
272
+ fontWeight: '700',
273
+ lineHeight: 22,
274
+ },
275
+ titleSpacer: {
276
+ flex: 1,
277
+ },
278
+ closeButton: {
279
+ width: 28,
280
+ height: 28,
281
+ borderRadius: 14,
282
+ alignItems: 'center',
283
+ justifyContent: 'center',
284
+ },
285
+ closeButtonPressed: {
286
+ opacity: 0.7,
287
+ },
288
+ bodyScroll: {
289
+ flexGrow: 0,
290
+ flexShrink: 1,
291
+ },
292
+ footer: {
293
+ borderTopWidth: StyleSheet.hairlineWidth,
294
+ },
295
+ });