@oxyhq/bloom 0.16.0 → 0.16.2

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 (53) hide show
  1. package/lib/commonjs/dialog/Dialog.js +45 -157
  2. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +64 -198
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/dialog/DialogBottomSheet.js +216 -0
  6. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -0
  7. package/lib/commonjs/dialog/DialogContent.js +155 -0
  8. package/lib/commonjs/dialog/DialogContent.js.map +1 -0
  9. package/lib/commonjs/dialog/placement.js +12 -1
  10. package/lib/commonjs/dialog/placement.js.map +1 -1
  11. package/lib/module/dialog/Dialog.js +48 -160
  12. package/lib/module/dialog/Dialog.js.map +1 -1
  13. package/lib/module/dialog/Dialog.web.js +66 -200
  14. package/lib/module/dialog/Dialog.web.js.map +1 -1
  15. package/lib/module/dialog/DialogBottomSheet.js +211 -0
  16. package/lib/module/dialog/DialogBottomSheet.js.map +1 -0
  17. package/lib/module/dialog/DialogContent.js +148 -0
  18. package/lib/module/dialog/DialogContent.js.map +1 -0
  19. package/lib/module/dialog/placement.js +11 -0
  20. package/lib/module/dialog/placement.js.map +1 -1
  21. package/lib/typescript/commonjs/dialog/Dialog.d.ts +1 -1
  22. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts +11 -8
  24. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts +29 -0
  26. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/dialog/DialogContent.d.ts +32 -0
  28. package/lib/typescript/commonjs/dialog/DialogContent.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/dialog/placement.d.ts +10 -0
  30. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/dialog/types.d.ts +12 -0
  32. package/lib/typescript/commonjs/dialog/types.d.ts.map +1 -1
  33. package/lib/typescript/module/dialog/Dialog.d.ts +1 -1
  34. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  35. package/lib/typescript/module/dialog/Dialog.web.d.ts +11 -8
  36. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  37. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts +29 -0
  38. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -0
  39. package/lib/typescript/module/dialog/DialogContent.d.ts +32 -0
  40. package/lib/typescript/module/dialog/DialogContent.d.ts.map +1 -0
  41. package/lib/typescript/module/dialog/placement.d.ts +10 -0
  42. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  43. package/lib/typescript/module/dialog/types.d.ts +12 -0
  44. package/lib/typescript/module/dialog/types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/__tests__/BottomSheetGesture.test.tsx +135 -0
  47. package/src/__tests__/DialogBottomSheet.test.tsx +259 -0
  48. package/src/dialog/Dialog.tsx +41 -171
  49. package/src/dialog/Dialog.web.tsx +56 -211
  50. package/src/dialog/DialogBottomSheet.tsx +250 -0
  51. package/src/dialog/DialogContent.tsx +157 -0
  52. package/src/dialog/placement.ts +11 -0
  53. package/src/dialog/types.ts +12 -0
@@ -10,11 +10,8 @@ import React, {
10
10
  import {
11
11
  Pressable,
12
12
  StyleSheet,
13
- Text,
14
- TouchableOpacity,
15
13
  useWindowDimensions,
16
14
  View,
17
- type GestureResponderEvent,
18
15
  type StyleProp,
19
16
  type ViewStyle,
20
17
  } from 'react-native';
@@ -29,12 +26,13 @@ import Animated, {
29
26
 
30
27
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
31
28
  import { Z_INDEX } from '../styles/z-index';
32
- import type { ThemeColors } from '../theme/types';
33
29
  import { useTheme } from '../theme/use-theme';
34
- import { Context, useDialogContext, useDialogControl } from './context';
30
+ import { Context, useDialogControl } from './context';
31
+ import { DialogBody } from './DialogContent';
32
+ import { DialogBottomSheet } from './DialogBottomSheet';
35
33
  import {
36
34
  ANIMATION_DURATION,
37
- DEFAULT_MAX_HEIGHT_RATIO,
35
+ DEFAULT_DIALOG_CONTENT_PADDING,
38
36
  DEFAULT_SIDE_WIDTH,
39
37
  DIALOG_SHEET_BACKDROP_TESTID,
40
38
  PANEL_RADIUS,
@@ -44,7 +42,6 @@ import {
44
42
  } from './placement';
45
43
  import type {
46
44
  DialogAction,
47
- DialogActionColor,
48
45
  DialogControlProps,
49
46
  DialogInset,
50
47
  DialogProps,
@@ -74,6 +71,31 @@ export { DIALOG_SHEET_BACKDROP_TESTID };
74
71
  * 3. Pure children — no `title`/`description`/`actions`.
75
72
  */
76
73
  export function Dialog({
74
+ placement,
75
+ ...rest
76
+ }: DialogProps) {
77
+ const resolvedPlacement = useResolvedPlacement(placement);
78
+
79
+ // `bottom` routes to the shared cross-platform `BottomSheet` surface — one
80
+ // code path with drag-to-dismiss on web and native. `center` (default) and
81
+ // the side-sheets keep their existing native lifecycle below. Each branch is
82
+ // its own component so the dispatcher only ever calls `useResolvedPlacement`,
83
+ // keeping the rules-of-hooks contract intact across a responsive placement
84
+ // change.
85
+ if (resolvedPlacement === 'bottom') {
86
+ return <DialogBottomSheet {...rest} />;
87
+ }
88
+ return <CenteredOrSideDialog {...rest} placement={resolvedPlacement} />;
89
+ }
90
+
91
+ /**
92
+ * Native `center` and `left`/`right` placements. `center` uses bloom's
93
+ * `BottomSheet` in `detached` mode (a floating, content-hugging card that
94
+ * degrades from sheet to centered card via the 500px cap); the side placements
95
+ * use a reanimated drawer. These share one open/close lifecycle. The `bottom`
96
+ * placement is handled separately by `DialogBottomSheet`.
97
+ */
98
+ function CenteredOrSideDialog({
77
99
  control,
78
100
  open: controlledOpen,
79
101
  onClose,
@@ -83,10 +105,9 @@ export function Dialog({
83
105
  actions,
84
106
  placement,
85
107
  width = DEFAULT_SIDE_WIDTH,
86
- maxHeightRatio = DEFAULT_MAX_HEIGHT_RATIO,
87
108
  inset,
88
- showHandle = true,
89
109
  dismissOnBackdrop = true,
110
+ contentPadding = DEFAULT_DIALOG_CONTENT_PADDING,
90
111
  style,
91
112
  panelStyle,
92
113
  panelClassName,
@@ -94,10 +115,9 @@ export function Dialog({
94
115
  containerClassName,
95
116
  label,
96
117
  children,
97
- }: DialogProps) {
118
+ }: Omit<DialogProps, 'placement'> & { placement: 'center' | 'left' | 'right' }) {
98
119
  const isControlled = controlledOpen !== undefined;
99
- const resolvedPlacement = useResolvedPlacement(placement);
100
- const isSide = resolvedPlacement === 'left' || resolvedPlacement === 'right';
120
+ const isSide = placement === 'left' || placement === 'right';
101
121
 
102
122
  // Imperative open state for the side placement (the BottomSheet path owns its
103
123
  // own visibility via its ref instead).
@@ -220,10 +240,11 @@ export function Dialog({
220
240
  <SideSheet
221
241
  open={sideOpen}
222
242
  onDismiss={handleDismiss}
223
- side={resolvedPlacement}
243
+ side={placement}
224
244
  width={width}
225
245
  inset={inset}
226
246
  dismissOnBackdrop={dismissOnBackdrop}
247
+ contentPadding={contentPadding}
227
248
  testID={testID}
228
249
  label={label}
229
250
  title={title}
@@ -242,19 +263,17 @@ export function Dialog({
242
263
  );
243
264
  }
244
265
 
245
- const isBottom = resolvedPlacement === 'bottom';
246
-
247
266
  return (
248
267
  <BottomSheet
249
268
  ref={ref}
250
269
  onDismiss={handleDismiss}
251
270
  enablePanDownToClose
252
- detached={!isBottom}
253
- showHandle={isBottom ? showHandle : true}
271
+ detached
272
+ showHandle
254
273
  // Stronger dim when a Dialog is stacked over another sheet so the
255
274
  // underlying sheet's handle/content doesn't bleed through.
256
275
  backdropOpacity={0.7}
257
- style={isBottom ? bottomSheetStyle(theme.colors.background, maxHeightRatio) : sheetStyle}
276
+ style={sheetStyle}
258
277
  >
259
278
  <Context.Provider value={context}>
260
279
  <View
@@ -267,7 +286,7 @@ export function Dialog({
267
286
  // Detached BottomSheet already adds `marginBottom: insets.bottom + 16`
268
287
  // to the sheet container — the floating card sits ABOVE the
269
288
  // system gesture bar, so we don't add `insets.bottom` here.
270
- { paddingTop: 20, paddingHorizontal: 20, paddingBottom: 20 },
289
+ { padding: contentPadding },
271
290
  { backgroundColor: theme.colors.background },
272
291
  style,
273
292
  panelStyle,
@@ -280,71 +299,6 @@ export function Dialog({
280
299
  );
281
300
  }
282
301
 
283
- function bottomSheetStyle(background: string, maxHeightRatio: number): ViewStyle {
284
- return {
285
- backgroundColor: background,
286
- borderTopLeftRadius: PANEL_RADIUS + 4,
287
- borderTopRightRadius: PANEL_RADIUS + 4,
288
- maxHeight: `${Math.round(maxHeightRatio * 100)}%`,
289
- };
290
- }
291
-
292
- /**
293
- * Renders the dialog's body: optional declarative title + description, any
294
- * `children`, then the action row. Shared by all placements. The
295
- * `titleId`/`descriptionId` are wired by the caller for accessibility.
296
- */
297
- function DialogBody({
298
- titleId,
299
- descriptionId,
300
- title,
301
- description,
302
- actions,
303
- children,
304
- }: {
305
- titleId: string;
306
- descriptionId: string;
307
- title?: string;
308
- description?: string;
309
- actions?: DialogAction[];
310
- children?: React.ReactNode;
311
- }) {
312
- const theme = useTheme();
313
- return (
314
- <>
315
- {title ? (
316
- <Text
317
- nativeID={titleId}
318
- style={{
319
- fontSize: 22,
320
- fontWeight: '600',
321
- color: theme.colors.text,
322
- paddingBottom: description ? 4 : 16,
323
- lineHeight: 30,
324
- }}
325
- >
326
- {title}
327
- </Text>
328
- ) : null}
329
- {description ? (
330
- <Text
331
- nativeID={descriptionId}
332
- style={{
333
- fontSize: 16,
334
- color: theme.colors.textSecondary,
335
- paddingBottom: 16,
336
- lineHeight: 22,
337
- }}
338
- >
339
- {description}
340
- </Text>
341
- ) : null}
342
- {children}
343
- {actions && actions.length > 0 ? <ActionRow actions={actions} /> : null}
344
- </>
345
- );
346
- }
347
-
348
302
  /**
349
303
  * Reanimated side-sheet/drawer for the `left`/`right` placements (native).
350
304
  *
@@ -360,6 +314,7 @@ function SideSheet({
360
314
  width,
361
315
  inset,
362
316
  dismissOnBackdrop,
317
+ contentPadding,
363
318
  testID,
364
319
  label,
365
320
  title,
@@ -379,6 +334,7 @@ function SideSheet({
379
334
  width: number;
380
335
  inset?: DialogInset;
381
336
  dismissOnBackdrop: boolean;
337
+ contentPadding: number;
382
338
  testID?: string;
383
339
  label?: string;
384
340
  title?: string;
@@ -510,95 +466,12 @@ function SideSheet({
510
466
  ]}
511
467
  pointerEvents="auto"
512
468
  >
513
- <View style={sideStyles.body}>{children}</View>
469
+ <View style={{ padding: contentPadding }}>{children}</View>
514
470
  </Animated.View>
515
471
  </View>
516
472
  );
517
473
  }
518
474
 
519
- function ActionRow({ actions }: { actions: DialogAction[] }) {
520
- return (
521
- <View style={{ width: '100%', gap: 8, justifyContent: 'flex-end' }}>
522
- {actions.map((action, idx) => (
523
- <ActionButton
524
- key={`${action.label}-${idx}`}
525
- action={action}
526
- />
527
- ))}
528
- </View>
529
- );
530
- }
531
-
532
- function ActionButton({ action }: { action: DialogAction }) {
533
- const { close } = useDialogContext();
534
- const theme = useTheme();
535
- const color: DialogActionColor = action.color ?? 'default';
536
- const shouldCloseOnPress = action.shouldCloseOnPress ?? true;
537
-
538
- const { background, foreground } = getActionPalette(color, theme.colors);
539
-
540
- const handlePress = useCallback(
541
- (e: GestureResponderEvent) => {
542
- const onPress = action.onPress;
543
- if (color === 'cancel') {
544
- // Cancel always dismisses; consumer's onPress (rare) runs after.
545
- close(onPress ? () => onPress(e) : undefined);
546
- return;
547
- }
548
- if (shouldCloseOnPress) {
549
- close(onPress ? () => onPress(e) : undefined);
550
- } else {
551
- onPress?.(e);
552
- }
553
- },
554
- [action.onPress, close, color, shouldCloseOnPress],
555
- );
556
-
557
- return (
558
- <TouchableOpacity
559
- style={{
560
- borderRadius: 9999,
561
- alignItems: 'center',
562
- justifyContent: 'center',
563
- backgroundColor: background,
564
- opacity: action.disabled ? 0.5 : 1,
565
- paddingVertical: 12,
566
- paddingHorizontal: 24,
567
- }}
568
- onPress={handlePress}
569
- disabled={action.disabled}
570
- activeOpacity={0.7}
571
- testID={action.testID}
572
- >
573
- <Text style={{ fontSize: 16, fontWeight: '500', color: foreground }}>
574
- {action.label}
575
- </Text>
576
- </TouchableOpacity>
577
- );
578
- }
579
-
580
- function getActionPalette(
581
- color: DialogActionColor,
582
- colors: ThemeColors,
583
- ): { background: string; foreground: string } {
584
- switch (color) {
585
- case 'destructive':
586
- return {
587
- background: colors.negative,
588
- foreground: colors.negativeForeground,
589
- };
590
- case 'cancel':
591
- return { background: colors.contrast50, foreground: colors.text };
592
- case 'default':
593
- return { background: colors.primary, foreground: colors.primaryForeground };
594
- /* c8 ignore next 2 -- TS exhaustiveness check guards this branch */
595
- default: {
596
- const _exhaustive: never = color;
597
- return { background: colors.primary, foreground: colors.primaryForeground };
598
- }
599
- }
600
- }
601
-
602
475
  const sideStyles = StyleSheet.create({
603
476
  root: {
604
477
  ...StyleSheet.absoluteFill,
@@ -616,9 +489,6 @@ const sideStyles = StyleSheet.create({
616
489
  shadowOffset: { width: 0, height: 8 },
617
490
  elevation: 12,
618
491
  },
619
- body: {
620
- padding: 20,
621
- },
622
492
  });
623
493
 
624
494
  /**