@oxyhq/bloom 0.59.1 → 0.59.3

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 (68) hide show
  1. package/README.md +4 -3
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +38 -23
  3. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  4. package/lib/commonjs/dialog/Dialog.js +2 -5
  5. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  6. package/lib/commonjs/dialog/DialogBottomSheet.js +0 -4
  7. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  8. package/lib/commonjs/dialog/SheetShell.js +2 -2
  9. package/lib/commonjs/dialog/SheetShell.js.map +1 -1
  10. package/lib/commonjs/overlay/blur-target.js +53 -0
  11. package/lib/commonjs/overlay/blur-target.js.map +1 -0
  12. package/lib/commonjs/overlay/index.js +15 -5
  13. package/lib/commonjs/overlay/index.js.map +1 -1
  14. package/lib/commonjs/provider/index.js +4 -6
  15. package/lib/commonjs/provider/index.js.map +1 -1
  16. package/lib/commonjs/styles/radii.js +9 -0
  17. package/lib/commonjs/styles/radii.js.map +1 -0
  18. package/lib/module/bottom-sheet/BottomSheetBase.js +40 -25
  19. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  20. package/lib/module/dialog/Dialog.js +2 -5
  21. package/lib/module/dialog/Dialog.js.map +1 -1
  22. package/lib/module/dialog/DialogBottomSheet.js +1 -5
  23. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  24. package/lib/module/dialog/SheetShell.js +2 -2
  25. package/lib/module/dialog/SheetShell.js.map +1 -1
  26. package/lib/module/overlay/blur-target.js +48 -0
  27. package/lib/module/overlay/blur-target.js.map +1 -0
  28. package/lib/module/overlay/index.js +14 -5
  29. package/lib/module/overlay/index.js.map +1 -1
  30. package/lib/module/provider/index.js +4 -6
  31. package/lib/module/provider/index.js.map +1 -1
  32. package/lib/module/styles/radii.js +5 -0
  33. package/lib/module/styles/radii.js.map +1 -0
  34. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts +4 -5
  35. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/dialog/SheetShell.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/overlay/blur-target.d.ts +21 -0
  40. package/lib/typescript/commonjs/overlay/blur-target.d.ts.map +1 -0
  41. package/lib/typescript/commonjs/overlay/index.d.ts +7 -2
  42. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  43. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/styles/radii.d.ts +3 -0
  45. package/lib/typescript/commonjs/styles/radii.d.ts.map +1 -0
  46. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts +4 -5
  47. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  48. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  49. package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
  50. package/lib/typescript/module/dialog/SheetShell.d.ts.map +1 -1
  51. package/lib/typescript/module/overlay/blur-target.d.ts +21 -0
  52. package/lib/typescript/module/overlay/blur-target.d.ts.map +1 -0
  53. package/lib/typescript/module/overlay/index.d.ts +7 -2
  54. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  55. package/lib/typescript/module/provider/index.d.ts.map +1 -1
  56. package/lib/typescript/module/styles/radii.d.ts +3 -0
  57. package/lib/typescript/module/styles/radii.d.ts.map +1 -0
  58. package/package.json +1 -1
  59. package/src/__tests__/BottomSheet.test.tsx +46 -1
  60. package/src/__tests__/BottomSheetGesture.test.tsx +21 -3
  61. package/src/bottom-sheet/BottomSheetBase.tsx +46 -29
  62. package/src/dialog/Dialog.tsx +2 -4
  63. package/src/dialog/DialogBottomSheet.tsx +0 -4
  64. package/src/dialog/SheetShell.tsx +2 -2
  65. package/src/overlay/blur-target.tsx +45 -0
  66. package/src/overlay/index.tsx +21 -5
  67. package/src/provider/index.tsx +14 -11
  68. package/src/styles/radii.ts +2 -0
@@ -20,12 +20,15 @@ import Animated, {
20
20
  type SharedValue,
21
21
  useAnimatedScrollHandler,
22
22
  useAnimatedStyle,
23
+ useDerivedValue,
23
24
  useSharedValue,
24
25
  withSpring,
25
26
  withTiming,
26
27
  } from 'react-native-reanimated';
27
28
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
- import { Backdrop } from '../overlay';
29
+ import { Backdrop, BACKDROP_DIM_OPACITY } from '../overlay';
30
+ import { useBloomBlurTarget } from '../overlay/blur-target';
31
+ import { DETACHED_SHEET_RADIUS } from '../styles/radii';
29
32
  import { useTheme } from '../theme/use-theme';
30
33
 
31
34
  /** Hook that returns current screen dimensions and updates on rotation/resize. */
@@ -125,12 +128,11 @@ export interface BottomSheetProps {
125
128
  */
126
129
  manualActivation?: boolean;
127
130
  /**
128
- * When `true`, the backdrop dims proportionally with drag distance — the
129
- * overlay fades from full opacity (sheet at rest) to 30% as the sheet is
130
- * pulled down 40% of the screen height. iOS Photos style. The base
131
- * `backdropOpacity` still controls the resting dim level.
131
+ * When `true`, the backdrop and native blur fade proportionally with drag
132
+ * distance fully visible at rest and fully gone once the sheet leaves the
133
+ * viewport. The base `backdropOpacity` still controls the resting dim level.
132
134
  *
133
- * Defaults to `false` (constant opacity during drag).
135
+ * Defaults to `true`. Set to `false` only for a deliberately fixed backdrop.
134
136
  */
135
137
  dynamicBackdrop?: boolean;
136
138
  /**
@@ -215,10 +217,10 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
215
217
  onDismissAttempt,
216
218
  detached = false,
217
219
  showHandle = true,
218
- backdropOpacity = 0.5,
220
+ backdropOpacity = BACKDROP_DIM_OPACITY,
219
221
  scrollable = true,
220
222
  manualActivation = false,
221
- dynamicBackdrop = false,
223
+ dynamicBackdrop = true,
222
224
  handleComponent,
223
225
  scrollY: externalScrollY,
224
226
  headerOverlay,
@@ -226,6 +228,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
226
228
  } = props;
227
229
 
228
230
  const insets = useSafeAreaInsets();
231
+ const blurTarget = useBloomBlurTarget();
229
232
  const theme = useTheme();
230
233
  const { colors } = theme;
231
234
  const { height: screenHeight } = useScreenDimensions();
@@ -255,6 +258,10 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
255
258
  }, [screenHeight, screenHeightSV]);
256
259
 
257
260
  const translateY = useSharedValue(screenHeight);
261
+ // The blur must reach zero when the CARD itself has moved fully below the
262
+ // viewport. A short detached dialog can be far smaller than the screen, so
263
+ // screen height is not a valid denominator for this interaction.
264
+ const sheetHeight = useSharedValue(screenHeight);
258
265
  const opacity = useSharedValue(0);
259
266
  const scrollOffsetY = useSharedValue(0);
260
267
  const isScrollAtTop = useSharedValue(true);
@@ -637,24 +644,30 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
637
644
  // Native (with the plugin) auto-tracks and ignores the extra deps, so this
638
645
  // is safe on both platforms. Do NOT strip the shared values from these deps.
639
646
  //
640
- // `opacity.value` drives the fade in/out (0 -> 1). `backdropOpacity` is the
641
- // final dim level once fully visible. We multiply so the consumer-provided
642
- // dim opacity applies smoothly across the animation. When
643
- // `dynamicBackdrop` is enabled, the dim also fades proportionally with
644
- // drag distance (iOS Photos style).
645
- const backdropStyle = useAnimatedStyle(() => {
647
+ // `opacity.value` drives the whole blur+dim layer in/out (0 -> 1).
648
+ // `backdropOpacity` belongs ONLY to Backdrop's black dim layer below; if it
649
+ // is multiplied into this group opacity instead, the opaque black child
650
+ // hides the BlurView and Android looks like a plain dark scrim.
651
+ // `dynamicBackdrop` fades the composed blur+dim proportionally with drag.
652
+ // The SAME progress drives BlurView's native `intensity` prop below:
653
+ // Android's RenderEffect can remain visibly blurred when only an ancestor's
654
+ // alpha changes, so reducing the radius itself is required for the content
655
+ // behind the sheet to become genuinely sharp during the gesture.
656
+ const backdropProgress = useDerivedValue(() => {
646
657
  const dragFactor = dynamicBackdrop
647
658
  ? interpolate(
648
659
  translateY.value,
649
- [0, screenHeightSV.value * 0.4],
650
- [1, 0.3],
660
+ [0, sheetHeight.value],
661
+ [1, 0],
651
662
  'clamp',
652
663
  )
653
664
  : 1;
654
- return {
655
- opacity: opacity.value * backdropOpacity * dragFactor,
656
- };
657
- }, [backdropOpacity, dynamicBackdrop, opacity, translateY, screenHeightSV]);
665
+ return opacity.value * dragFactor;
666
+ }, [dynamicBackdrop, opacity, translateY, sheetHeight]);
667
+
668
+ const backdropStyle = useAnimatedStyle(() => ({
669
+ opacity: backdropProgress.value,
670
+ }), [backdropProgress]);
658
671
 
659
672
  const sheetStyle = useAnimatedStyle(() => {
660
673
  const scale = interpolate(translateY.value, [0, screenHeightSV.value], [1, 0.95]);
@@ -690,6 +703,12 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
690
703
  dismiss();
691
704
  }, [onDismissAttempt, dismiss]);
692
705
 
706
+ const handleSheetLayout = useCallback((event: LayoutChangeEvent) => {
707
+ const measuredHeight = event.nativeEvent.layout.height;
708
+ if (measuredHeight > 0) sheetHeight.value = measuredHeight;
709
+ onLayout?.(event);
710
+ }, [onLayout, sheetHeight]);
711
+
693
712
  const scrollHandler = useAnimatedScrollHandler({
694
713
  onScroll: (event) => {
695
714
  scrollOffsetY.value = event.contentOffset.y;
@@ -792,19 +811,20 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
792
811
  </Animated.View>
793
812
  ) : (
794
813
  // The ONE Bloom backdrop (blur + dim + press-to-dismiss).
795
- // `backdropStyle` drives the fade; the dim level rides on
796
- // `backdropOpacity`, so the shared component's own dim is
797
- // switched off here to keep a single source of dimming.
814
+ // `backdropStyle` drives the entrance/drag fade; the black
815
+ // dim stays translucent so the BlurView remains visible.
798
816
  <AnimatedBackdrop
799
817
  onPress={handleBackdropPress}
800
- dimOpacity={1}
818
+ dimOpacity={backdropOpacity}
819
+ blurTarget={blurTarget ?? undefined}
820
+ blurProgress={backdropProgress}
801
821
  style={[styles.backdrop, backdropStyle]}
802
822
  />
803
823
  )}
804
824
 
805
825
  <GestureDetector gesture={panGesture}>
806
826
  <Animated.View
807
- onLayout={onLayout}
827
+ onLayout={handleSheetLayout}
808
828
  style={[dynamicStyles.sheet, sheetMarginStyle, sheetStyle, sheetHeightStyle, style]}
809
829
  >
810
830
  {backgroundComponent?.({ style: styles.background })}
@@ -827,10 +847,7 @@ BottomSheetBase.displayName = 'BottomSheetBase';
827
847
 
828
848
  const styles = StyleSheet.create({
829
849
  backdrop: {
830
- // Color is solid black; the dim level is driven by `backdropOpacity` via
831
- // the animated `opacity` style on the wrapping <Animated.View>.
832
850
  flex: 1,
833
- backgroundColor: '#000',
834
851
  },
835
852
  backdropTouchable: {
836
853
  flex: 1,
@@ -846,7 +863,7 @@ const styles = StyleSheet.create({
846
863
  sheetDetached: {
847
864
  left: 16,
848
865
  right: 16,
849
- borderRadius: 24,
866
+ borderRadius: DETACHED_SHEET_RADIUS,
850
867
  },
851
868
  sheetNormal: {
852
869
  left: 0,
@@ -26,6 +26,7 @@ import Animated, {
26
26
  } from 'react-native-reanimated';
27
27
 
28
28
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
29
+ import { DETACHED_SHEET_RADIUS } from '../styles/radii';
29
30
  import { Z_INDEX, Z_INDEX_LAYER_STEP } from '../styles/z-index';
30
31
  import { useTheme } from '../theme/use-theme';
31
32
  import { Context, useDialogControl } from './context';
@@ -251,7 +252,7 @@ function CenteredOrSideDialog({
251
252
  // All four corners rounded — bloom's BottomSheet defaults to top-only
252
253
  // radius in flush mode, but we use `detached` so the whole card is
253
254
  // floating and rounded uniformly.
254
- borderRadius: 20,
255
+ borderRadius: DETACHED_SHEET_RADIUS,
255
256
  },
256
257
  // Drives the card's `height` only while a morph is in flight; at rest it
257
258
  // resolves to `height: 'auto'` — the card's normal content sizing.
@@ -344,9 +345,6 @@ function CenteredOrSideDialog({
344
345
  />
345
346
  ) : undefined
346
347
  }
347
- // Stronger dim when a Dialog is stacked over another sheet so the
348
- // underlying sheet's handle/content doesn't bleed through.
349
- backdropOpacity={0.7}
350
348
  // Measures the floating card — the size a morph starts from.
351
349
  onLayout={morphState.onPanelLayout}
352
350
  style={sheetStyle}
@@ -30,7 +30,6 @@ import {
30
30
  DEFAULT_DIALOG_CONTENT_PADDING,
31
31
  DEFAULT_MAX_HEIGHT_RATIO,
32
32
  PANEL_RADIUS,
33
- SHEET_BACKDROP_OPACITY,
34
33
  } from './placement';
35
34
  import type { DialogControlProps, DialogProps } from './types';
36
35
 
@@ -284,9 +283,6 @@ export function DialogBottomSheet({
284
283
  />
285
284
  ) : undefined
286
285
  }
287
- // Stronger dim than a lone sheet so an underlying sheet's handle/content
288
- // doesn't bleed through when a Dialog is stacked over one.
289
- backdropOpacity={SHEET_BACKDROP_OPACITY + 0.3}
290
286
  // Measures the sheet card — the size a morph starts from.
291
287
  onLayout={morphState.onPanelLayout}
292
288
  style={sheetStyle}
@@ -14,6 +14,7 @@ import React, { useCallback, useImperativeHandle, useMemo, useRef } from 'react'
14
14
  import { Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
15
15
 
16
16
  import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
17
+ import { DETACHED_SHEET_RADIUS } from '../styles/radii';
17
18
  import { Z_INDEX } from '../styles/z-index';
18
19
  import { useTheme } from '../theme/use-theme';
19
20
  import { Context } from './context';
@@ -87,7 +88,7 @@ export function SheetShell({
87
88
  () => ({
88
89
  maxWidth: 500,
89
90
  backgroundColor: theme.colors.background,
90
- borderRadius: 20,
91
+ borderRadius: DETACHED_SHEET_RADIUS,
91
92
  }),
92
93
  [theme.colors.background],
93
94
  );
@@ -98,7 +99,6 @@ export function SheetShell({
98
99
  onDismiss={handleDismiss}
99
100
  enablePanDownToClose
100
101
  detached
101
- backdropOpacity={0.7}
102
102
  style={sheetStyle}
103
103
  >
104
104
  <Context.Provider value={context}>
@@ -0,0 +1,45 @@
1
+ import { createContext, useContext, useRef, type ReactNode, type RefObject } from 'react';
2
+ import { BlurTargetView } from 'expo-blur';
3
+ import { Platform, StyleSheet, type View } from 'react-native';
4
+
5
+ type BlurTargetRef = RefObject<View | null>;
6
+
7
+ const BlurTargetContext = createContext<BlurTargetRef | null>(null);
8
+
9
+ /**
10
+ * Gives Android overlay blurs a real view to sample.
11
+ *
12
+ * Expo Blur 56+ intentionally disables the Dimezis implementation unless a
13
+ * `BlurTargetView` is supplied. Bloom's sheets render their backdrop inside a
14
+ * native `Modal`, so the target must live around the app content in the
15
+ * underlying window and travel to the modal through React context.
16
+ *
17
+ * Only modal backdrops consume this target. Ordinary in-tree BlurViews never
18
+ * target their own ancestor, avoiding the recursive capture/crash that causes.
19
+ */
20
+ export function BloomBlurTargetProvider({ children }: { children: ReactNode }) {
21
+ const targetRef = useRef<View>(null);
22
+
23
+ if (Platform.OS !== 'android') {
24
+ return <>{children}</>;
25
+ }
26
+
27
+ return (
28
+ <BlurTargetContext.Provider value={targetRef}>
29
+ <BlurTargetView ref={targetRef} style={styles.target}>
30
+ {children}
31
+ </BlurTargetView>
32
+ </BlurTargetContext.Provider>
33
+ );
34
+ }
35
+
36
+ /** Internal target used only by Bloom's native modal backdrops. */
37
+ export function useBloomBlurTarget(): BlurTargetRef | null {
38
+ return useContext(BlurTargetContext);
39
+ }
40
+
41
+ const styles = StyleSheet.create({
42
+ target: {
43
+ flex: 1,
44
+ },
45
+ });
@@ -39,14 +39,16 @@
39
39
  * Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
40
40
  * dimming layer; do not re-implement either with raw `View`s.
41
41
  */
42
- import { forwardRef, memo, type ReactNode } from 'react';
42
+ import { forwardRef, memo, type ReactNode, type RefObject } from 'react';
43
43
  import { BlurView } from 'expo-blur';
44
+ import Animated, { type SharedValue, useAnimatedProps } from 'react-native-reanimated';
44
45
  import {
45
46
  Platform,
46
47
  Pressable,
47
48
  StyleSheet,
48
49
  View,
49
50
  type StyleProp,
51
+ type View as NativeView,
50
52
  type ViewStyle,
51
53
  } from 'react-native';
52
54
 
@@ -60,6 +62,8 @@ export const BACKDROP_BLUR_INTENSITY = 40;
60
62
  /** Dim laid over the blur — the blur alone does not give enough contrast. */
61
63
  export const BACKDROP_DIM_OPACITY = 0.45;
62
64
 
65
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
66
+
63
67
  export interface OverlayRootProps {
64
68
  children?: ReactNode;
65
69
  style?: StyleProp<ViewStyle>;
@@ -95,6 +99,10 @@ export interface BackdropProps {
95
99
  dimColor?: string;
96
100
  /** Dim opacity, 0–1. */
97
101
  dimOpacity?: number;
102
+ /** Android view sampled by the native blur. Modal surfaces receive this from BloomProvider. */
103
+ blurTarget?: RefObject<NativeView | null>;
104
+ /** Live visibility progress used to reduce native blur while a surface leaves. */
105
+ blurProgress?: SharedValue<number>;
98
106
  /** Extra style on the press target — animated opacity, insets, z-index. */
99
107
  style?: StyleProp<ViewStyle>;
100
108
  /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
@@ -116,6 +124,8 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
116
124
  blurTint = 'dark',
117
125
  dimColor = '#000',
118
126
  dimOpacity = BACKDROP_DIM_OPACITY,
127
+ blurTarget,
128
+ blurProgress,
119
129
  style,
120
130
  children,
121
131
  accessibilityLabel = 'Dismiss',
@@ -124,6 +134,10 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
124
134
  ref,
125
135
  ) {
126
136
  const inert = disabled || !onPress;
137
+ const animatedBlurProps = useAnimatedProps(() => ({
138
+ intensity: Math.max(0, Math.min(100, blurIntensity * (blurProgress?.value ?? 1))),
139
+ }), [blurIntensity, blurProgress]);
140
+
127
141
  return (
128
142
  <Pressable
129
143
  // MUST forward the ref: surfaces animate the backdrop through
@@ -146,12 +160,14 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
146
160
  style={[StyleSheet.absoluteFill, style]}
147
161
  >
148
162
  {blurIntensity > 0 ? (
149
- <BlurView
163
+ <AnimatedBlurView
150
164
  intensity={blurIntensity}
165
+ animatedProps={animatedBlurProps}
151
166
  tint={blurTint}
152
- // Android's default blur is a no-op on many devices; this is the
153
- // implementation that actually renders there.
154
- experimentalBlurMethod="dimezisBlurView"
167
+ blurTarget={blurTarget}
168
+ // Expo disables Android blur without an explicit target. Sheets get
169
+ // one from BloomProvider; targetless surfaces degrade without warning.
170
+ blurMethod={Platform.OS === 'android' && blurTarget ? 'dimezisBlurViewSdk31Plus' : undefined}
155
171
  pointerEvents="none"
156
172
  style={StyleSheet.absoluteFill}
157
173
  />
@@ -34,6 +34,7 @@ import type { ReactNode } from 'react';
34
34
 
35
35
  import { ImageResolverProvider, type ImageResolver } from '../image-resolver';
36
36
  import { BloomHapticsProvider } from '../hooks/useHaptics';
37
+ import { BloomBlurTargetProvider } from '../overlay/blur-target';
37
38
  import { TabBarMinimizeProvider } from '../tab-bar/minimize-context';
38
39
  import { BloomThemeProvider, type BloomThemeProviderProps } from '../theme';
39
40
  import { ScrollRestorationProvider } from './scroll-provider';
@@ -57,17 +58,19 @@ export function BloomProvider({
57
58
  ...themeProps
58
59
  }: BloomProviderProps) {
59
60
  return (
60
- // `value` is passed unconditionally (null when unset) so toggling a resolver
61
- // never changes the tree shape and remounts everything below it.
62
- <ImageResolverProvider value={imageResolver ?? null}>
63
- <BloomThemeProvider {...themeProps}>
64
- <ScrollRestorationProvider>
65
- <BloomHapticsProvider enabled={haptics}>
66
- <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
67
- </BloomHapticsProvider>
68
- </ScrollRestorationProvider>
69
- </BloomThemeProvider>
70
- </ImageResolverProvider>
61
+ <BloomBlurTargetProvider>
62
+ {/* `value` is passed unconditionally (null when unset) so toggling a resolver
63
+ never changes the tree shape and remounts everything below it. */}
64
+ <ImageResolverProvider value={imageResolver ?? null}>
65
+ <BloomThemeProvider {...themeProps}>
66
+ <ScrollRestorationProvider>
67
+ <BloomHapticsProvider enabled={haptics}>
68
+ <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
69
+ </BloomHapticsProvider>
70
+ </ScrollRestorationProvider>
71
+ </BloomThemeProvider>
72
+ </ImageResolverProvider>
73
+ </BloomBlurTargetProvider>
71
74
  );
72
75
  }
73
76
 
@@ -0,0 +1,2 @@
1
+ /** Radius for floating bottom sheets and native detached dialog cards. */
2
+ export const DETACHED_SHEET_RADIUS = 32;