@oxyhq/bloom 0.60.0 → 0.62.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 (51) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +24 -9
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +14 -8
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js +18 -1
  6. package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -1
  7. package/lib/commonjs/image-aspect-ratio-cache/index.js +6 -0
  8. package/lib/commonjs/image-aspect-ratio-cache/index.js.map +1 -1
  9. package/lib/commonjs/overlay/index.js +33 -19
  10. package/lib/commonjs/overlay/index.js.map +1 -1
  11. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +17 -10
  12. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  13. package/lib/module/bottom-sheet/BottomSheetBase.js +25 -10
  14. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  15. package/lib/module/dialog/Dialog.web.js +14 -8
  16. package/lib/module/dialog/Dialog.web.js.map +1 -1
  17. package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js +16 -0
  18. package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -1
  19. package/lib/module/image-aspect-ratio-cache/index.js +1 -1
  20. package/lib/module/image-aspect-ratio-cache/index.js.map +1 -1
  21. package/lib/module/overlay/index.js +33 -21
  22. package/lib/module/overlay/index.js.map +1 -1
  23. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +18 -11
  24. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  25. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +12 -0
  28. package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts +2 -1
  30. package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/overlay/index.d.ts +21 -3
  32. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  34. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  35. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  36. package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +12 -0
  37. package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -1
  38. package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts +2 -1
  39. package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts.map +1 -1
  40. package/lib/typescript/module/overlay/index.d.ts +21 -3
  41. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  42. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/__tests__/imageIntrinsicSize.test.ts +47 -0
  45. package/src/__tests__/overlay-pointer-events.test.tsx +17 -16
  46. package/src/bottom-sheet/BottomSheetBase.tsx +24 -9
  47. package/src/dialog/Dialog.web.tsx +16 -18
  48. package/src/image-aspect-ratio-cache/imageAspectRatioCache.ts +18 -0
  49. package/src/image-aspect-ratio-cache/index.ts +2 -0
  50. package/src/overlay/index.tsx +59 -30
  51. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +25 -10
@@ -39,7 +39,7 @@
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 { memo, type ReactNode } from 'react';
43
43
  import { BlurView } from 'expo-blur';
44
44
  import {
45
45
  Platform,
@@ -49,9 +49,12 @@ import {
49
49
  type StyleProp,
50
50
  type ViewStyle,
51
51
  } from 'react-native';
52
+ import Animated, { useAnimatedStyle, type SharedValue } from 'react-native-reanimated';
52
53
 
53
54
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
54
55
 
56
+ const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
57
+
55
58
  /**
56
59
  * One blur radius for every Bloom overlay. Surfaces differ in what they show,
57
60
  * not in how the app behind them recedes.
@@ -85,6 +88,12 @@ OverlayRoot.displayName = 'OverlayRoot';
85
88
  export interface BackdropProps {
86
89
  /** Dismiss handler. Omit (or pass `disabled`) for a backdrop that only dims. */
87
90
  onPress?: () => void;
91
+ /**
92
+ * Fade driver, 0 (hidden) → 1 (fully shown). The backdrop applies it to its
93
+ * OWN layers — see the note on `style` for why the caller must not animate
94
+ * opacity from the outside.
95
+ */
96
+ progress?: SharedValue<number>;
88
97
  /** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
89
98
  disabled?: boolean;
90
99
  /** Blur radius behind the dim. `0` renders the dim alone. */
@@ -95,8 +104,19 @@ export interface BackdropProps {
95
104
  dimColor?: string;
96
105
  /** Dim opacity, 0–1. */
97
106
  dimOpacity?: number;
98
- /** Extra style on the press target — animated opacity, insets, z-index. */
107
+ /**
108
+ * Geometry for the press target: insets, z-index, layout. NOT opacity —
109
+ * `backdrop-filter` samples nothing under an ancestor with `opacity < 1`
110
+ * (the group composites in isolation), so a fade applied here silently kills
111
+ * the blur. An `opacity` found in this style is redirected onto the layers;
112
+ * animate the fade through `progress` instead.
113
+ */
99
114
  style?: StyleProp<ViewStyle>;
115
+ /**
116
+ * Extra style for the blur + dim LAYERS, for fades a shared value can't
117
+ * express — the web dialog's CSS keyframes, for instance.
118
+ */
119
+ layerStyle?: StyleProp<ViewStyle>;
100
120
  /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
101
121
  children?: ReactNode;
102
122
  accessibilityLabel?: string;
@@ -108,31 +128,40 @@ export interface BackdropProps {
108
128
  * pointer events (that is its whole job), so anything that must stay pressable
109
129
  * goes INSIDE it as `children`, never as a sibling rendered over it.
110
130
  */
111
- export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
112
- {
113
- onPress,
114
- disabled = false,
115
- blurIntensity = BACKDROP_BLUR_INTENSITY,
116
- blurTint = 'dark',
117
- dimColor = '#000',
118
- dimOpacity = BACKDROP_DIM_OPACITY,
119
- style,
120
- children,
121
- accessibilityLabel = 'Dismiss',
122
- testID,
123
- },
124
- ref,
125
- ) {
131
+ export const Backdrop = memo(function Backdrop({
132
+ onPress,
133
+ progress,
134
+ disabled = false,
135
+ blurIntensity = BACKDROP_BLUR_INTENSITY,
136
+ blurTint = 'dark',
137
+ dimColor = '#000',
138
+ dimOpacity = BACKDROP_DIM_OPACITY,
139
+ style,
140
+ layerStyle,
141
+ children,
142
+ accessibilityLabel = 'Dismiss',
143
+ testID,
144
+ }: BackdropProps) {
126
145
  const inert = disabled || !onPress;
146
+
147
+ // An `opacity` handed in through `style` would sit on the ancestor of the
148
+ // blur and kill it; hoist it onto the layers instead of honouring it there.
149
+ const flat: ViewStyle = StyleSheet.flatten(style) ?? {};
150
+ const { opacity: styleOpacity, ...rootStyle } = flat;
151
+ const staticOpacity = typeof styleOpacity === 'number' ? styleOpacity : 1;
152
+
153
+ // The fade lives on each LAYER, never on their shared ancestor.
154
+ const blurFade = useAnimatedStyle(
155
+ () => ({ opacity: (progress ? progress.value : 1) * staticOpacity }),
156
+ [progress, staticOpacity],
157
+ );
158
+ const dimFade = useAnimatedStyle(
159
+ () => ({ opacity: (progress ? progress.value : 1) * staticOpacity * dimOpacity }),
160
+ [progress, staticOpacity, dimOpacity],
161
+ );
162
+
127
163
  return (
128
164
  <Pressable
129
- // MUST forward the ref: surfaces animate the backdrop through
130
- // `Animated.createAnimatedComponent(Backdrop)`, and reanimated applies
131
- // those updates by reaching the underlying host view. With the ref
132
- // swallowed the animated style never lands, so a backdrop whose opacity
133
- // is animated from 0 stays at 0 — fully transparent, with the surface
134
- // floating over an undimmed app.
135
- ref={ref}
136
165
  pointerEvents="auto"
137
166
  onPress={inert ? undefined : onPress}
138
167
  disabled={inert}
@@ -143,27 +172,27 @@ export const Backdrop = memo(forwardRef<View, BackdropProps>(function Backdrop(
143
172
  accessibilityRole={inert ? undefined : 'button'}
144
173
  accessibilityLabel={inert ? undefined : accessibilityLabel}
145
174
  testID={testID}
146
- style={[StyleSheet.absoluteFill, style]}
175
+ style={[StyleSheet.absoluteFill, rootStyle]}
147
176
  >
148
177
  {blurIntensity > 0 ? (
149
- <BlurView
178
+ <AnimatedBlurView
150
179
  intensity={blurIntensity}
151
180
  tint={blurTint}
152
181
  // Android's default blur is a no-op on many devices; this is the
153
182
  // implementation that actually renders there.
154
183
  experimentalBlurMethod="dimezisBlurView"
155
184
  pointerEvents="none"
156
- style={StyleSheet.absoluteFill}
185
+ style={[StyleSheet.absoluteFill, layerStyle, blurFade]}
157
186
  />
158
187
  ) : null}
159
- <View
188
+ <Animated.View
160
189
  pointerEvents="none"
161
- style={[StyleSheet.absoluteFill, { backgroundColor: dimColor, opacity: dimOpacity }]}
190
+ style={[StyleSheet.absoluteFill, { backgroundColor: dimColor }, layerStyle, dimFade]}
162
191
  />
163
192
  {children}
164
193
  </Pressable>
165
194
  );
166
- }));
195
+ });
167
196
 
168
197
  Backdrop.displayName = 'Backdrop';
169
198
 
@@ -40,6 +40,7 @@ import { Z_INDEX } from '../styles/z-index';
40
40
  import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
41
41
  import {
42
42
  getAspectRatio,
43
+ getIntrinsicSize,
43
44
  fetchAspectRatio,
44
45
  DEFAULT_ASPECT_RATIO,
45
46
  } from '../image-aspect-ratio-cache';
@@ -73,8 +74,6 @@ import type {
73
74
  } from './types';
74
75
 
75
76
  const AnimatedImage = Animated.createAnimatedComponent(Image);
76
- // The shared overlay backdrop, driven by the viewer's open/drag progress.
77
- const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
78
77
 
79
78
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
80
79
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
@@ -216,9 +215,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
216
215
  [SCREEN_WIDTH, SCREEN_HEIGHT]
217
216
  );
218
217
 
219
- // Compute the largest width/height for `ratio` that fits inside `fitBox`
220
- // without cropping (contain).
221
- const fitForRatio = useCallback((ratio: number): FittedSize => {
218
+ // Largest width/height for `ratio` that fits inside `fitBox` without cropping
219
+ // (contain), never exceeding the image's own resolution: filling the viewport
220
+ // with a 512px avatar or a small thumbnail just renders it soft. `uri` is what
221
+ // lets us look the intrinsic size up; without it (not measured yet) the fit
222
+ // box wins and the clamp applies on the next frame.
223
+ const fitForRatio = useCallback((ratio: number, uri?: string): FittedSize => {
222
224
  const safeRatio = ratio > 0 && Number.isFinite(ratio) ? ratio : DEFAULT_ASPECT_RATIO;
223
225
  let width = fitBox.width;
224
226
  let height = width / safeRatio;
@@ -226,6 +228,11 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
226
228
  height = fitBox.height;
227
229
  width = height * safeRatio;
228
230
  }
231
+ const intrinsic = uri ? getIntrinsicSize(uri) : undefined;
232
+ if (intrinsic && intrinsic.width > 0 && width > intrinsic.width) {
233
+ width = intrinsic.width;
234
+ height = width / safeRatio;
235
+ }
229
236
  return { width, height };
230
237
  }, [fitBox]);
231
238
 
@@ -234,7 +241,11 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
234
241
  // after swiping it tracks the viewed image so the collapse-on-dismiss renders
235
242
  // and flies back the image actually on screen.
236
243
  const activeRatio = pageRatios[activeIndex] ?? openRatio;
237
- const activeFit = useMemo(() => fitForRatio(activeRatio), [fitForRatio, activeRatio]);
244
+ const activeUri = images[activeIndex]?.uri;
245
+ const activeFit = useMemo(
246
+ () => fitForRatio(activeRatio, activeUri),
247
+ [fitForRatio, activeRatio, activeUri],
248
+ );
238
249
 
239
250
  // Alt text (accessibility description) of the image currently on screen, shown
240
251
  // as a caption at the bottom of the viewer (Bluesky-style lightbox footer).
@@ -391,7 +402,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
391
402
  // animation grows it from the thumbnail's footprint (scale < 1) up to 1,
392
403
  // mirroring the avatar's small→big scale. The initial scale is the ratio
393
404
  // of the thumbnail width to the fitted width.
394
- const fitted = fitForRatio(ratio);
405
+ const fitted = fitForRatio(ratio, target.uri);
395
406
  const centerX = SCREEN_WIDTH / 2;
396
407
  const centerY = SCREEN_HEIGHT / 2;
397
408
  if (rect && rect.width > 0) {
@@ -778,11 +789,15 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
778
789
  did nothing on web while Escape still closed. `Backdrop` opts back
779
790
  in via the `pointerEvents` PROP, the only form that reaches the DOM
780
791
  (see `src/overlay`). */}
781
- <AnimatedBackdrop
792
+ {/* `progress` rather than an animated style: an opacity on the
793
+ backdrop's root would sit ABOVE the blur layer and neutralise it —
794
+ `backdrop-filter` samples nothing under an ancestor that composites
795
+ in isolation. */}
796
+ <Backdrop
782
797
  onPress={handleDismiss}
783
798
  accessibilityLabel="Close image viewer"
784
799
  dimColor={theme.colors.overlay}
785
- style={backdropStyle}
800
+ progress={opacity}
786
801
  />
787
802
 
788
803
  <GestureDetector gesture={panGesture}>
@@ -829,7 +844,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
829
844
  >
830
845
  {images.map((img, idx) => {
831
846
  const ratio = pageRatios[idx] ?? getAspectRatio(img.uri) ?? DEFAULT_ASPECT_RATIO;
832
- const fit = fitForRatio(ratio);
847
+ const fit = fitForRatio(ratio, img.uri);
833
848
  // Only the active page is zoomable + gesture-wrapped (double-tap
834
849
  // / single-tap dismiss / pinch / pan). Off-screen pages stay a
835
850
  // plain image; the active image handles its own tap-to-dismiss