@oxyhq/bloom 0.57.0 → 0.58.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 (41) hide show
  1. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +15 -9
  2. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  3. package/lib/commonjs/dialog/Dialog.web.js +17 -23
  4. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  5. package/lib/commonjs/overlay/index.js +50 -14
  6. package/lib/commonjs/overlay/index.js.map +1 -1
  7. package/lib/commonjs/provider/index.js +6 -0
  8. package/lib/commonjs/provider/index.js.map +1 -1
  9. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +27 -16
  10. package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  11. package/lib/module/bottom-sheet/BottomSheetBase.js +16 -10
  12. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  13. package/lib/module/dialog/Dialog.web.js +17 -23
  14. package/lib/module/dialog/Dialog.web.js.map +1 -1
  15. package/lib/module/overlay/index.js +51 -14
  16. package/lib/module/overlay/index.js.map +1 -1
  17. package/lib/module/provider/index.js +6 -0
  18. package/lib/module/provider/index.js.map +1 -1
  19. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +27 -16
  20. package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -1
  21. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  23. package/lib/typescript/commonjs/overlay/index.d.ts +31 -6
  24. package/lib/typescript/commonjs/overlay/index.d.ts.map +1 -1
  25. package/lib/typescript/commonjs/provider/index.d.ts +6 -0
  26. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  28. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  29. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  30. package/lib/typescript/module/overlay/index.d.ts +31 -6
  31. package/lib/typescript/module/overlay/index.d.ts.map +1 -1
  32. package/lib/typescript/module/provider/index.d.ts +6 -0
  33. package/lib/typescript/module/provider/index.d.ts.map +1 -1
  34. package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/__tests__/overlay-pointer-events.test.tsx +17 -0
  37. package/src/bottom-sheet/BottomSheetBase.tsx +18 -9
  38. package/src/dialog/Dialog.web.tsx +20 -24
  39. package/src/overlay/index.tsx +66 -10
  40. package/src/provider/index.tsx +6 -0
  41. package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +32 -17
@@ -2,7 +2,8 @@
2
2
  * The shared plumbing every portaled surface needs: an interactive ROOT and a
3
3
  * press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
4
4
  * toasts each used to hand-roll both, and each one got the web contract subtly
5
- * wrong in its own way.
5
+ * wrong in its own way — and looked different while doing it (the image viewer
6
+ * blurred, everything else only dimmed).
6
7
  *
7
8
  * ## The contract these two components encode
8
9
  *
@@ -26,11 +27,27 @@
26
27
  * which is what makes it look like a dismissal bug rather than a hit-testing
27
28
  * one.
28
29
  *
30
+ * A backdrop also only dismisses what is actually ON TOP of it: a full-screen
31
+ * layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
32
+ * press first and swallows it. Either that layer opts out (`pointerEvents
33
+ *="box-none"`) or it owns the dismiss itself — the backdrop being present is
34
+ * not enough.
35
+ *
36
+ * EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
37
+ * two components are universal.
38
+ *
29
39
  * Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
30
40
  * dimming layer; do not re-implement either with raw `View`s.
31
41
  */
32
42
  import { type ReactNode } from 'react';
33
43
  import { type StyleProp, type ViewStyle } from 'react-native';
44
+ /**
45
+ * One blur radius for every Bloom overlay. Surfaces differ in what they show,
46
+ * not in how the app behind them recedes.
47
+ */
48
+ export declare const BACKDROP_BLUR_INTENSITY = 40;
49
+ /** Dim laid over the blur — the blur alone does not give enough contrast. */
50
+ export declare const BACKDROP_DIM_OPACITY = 0.45;
34
51
  export interface OverlayRootProps {
35
52
  children?: ReactNode;
36
53
  style?: StyleProp<ViewStyle>;
@@ -51,17 +68,25 @@ export interface BackdropProps {
51
68
  onPress?: () => void;
52
69
  /** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
53
70
  disabled?: boolean;
54
- /** Extra style (dim colour, animated opacity). Rendered over `absoluteFill`. */
71
+ /** Blur radius behind the dim. `0` renders the dim alone. */
72
+ blurIntensity?: number;
73
+ /** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
74
+ blurTint?: 'light' | 'dark' | 'default';
75
+ /** Dim colour over the blur. */
76
+ dimColor?: string;
77
+ /** Dim opacity, 0–1. */
78
+ dimOpacity?: number;
79
+ /** Extra style on the press target — animated opacity, insets, z-index. */
55
80
  style?: StyleProp<ViewStyle>;
56
- /** Rendered inside the backdrop e.g. a blur layer that must dim with it. */
81
+ /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
57
82
  children?: ReactNode;
58
83
  accessibilityLabel?: string;
59
84
  testID?: string;
60
85
  }
61
86
  /**
62
- * Full-bleed dimming layer that dismisses the surface when pressed. Always
63
- * takes pointer events (that is its whole job), so it must render UNDER the
64
- * surface's panel in the tree, never over it.
87
+ * Full-bleed blur + dim that dismisses the surface when pressed. Always takes
88
+ * pointer events (that is its whole job), so anything that must stay pressable
89
+ * goes INSIDE it as `children`, never as a sibling rendered over it.
65
90
  */
66
91
  export declare const Backdrop: import("react").NamedExoticComponent<BackdropProps>;
67
92
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAyC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIrG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA0BnB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACxC,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA6CnB,CAAC"}
@@ -18,6 +18,12 @@
18
18
  * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
19
  * longer a per-app decision.
20
20
  *
21
+ * EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
22
+ * `expo-router` (its web implementation keys offsets on the focused route), so
23
+ * a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
24
+ * apps keep mounting `BloomThemeProvider` (and any other piece they need)
25
+ * directly — that is not a lesser path, just the one without a router.
26
+ *
21
27
  * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
28
  * in the tree is a real app decision (z-order, safe areas, and mounting a
23
29
  * second one duplicates every surface it renders):
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAu4BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAs5BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAMH,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AAItB,OAAiB,EACb,KAAK,aAAa,EAGlB,KAAK,WAAW,EAMnB,MAAM,yBAAyB,CAAC;AAwBjC,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC7F,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IACjH;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5C,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IAClC,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B;;;OAGG;IACH,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC1D,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACrD;AAED,eAAO,MAAM,eAAe,6FAsmB1B,CAAC"}
1
+ {"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../../src/bottom-sheet/BottomSheetBase.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAMH,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,cAAc,CAAC;AAItB,OAAiB,EACb,KAAK,aAAa,EAGlB,KAAK,WAAW,EAMnB,MAAM,yBAAyB,CAAC;AAyBjC,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IAC7F,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC;IACjH;;;;;OAKG;IACH,KAAK,CAAC,EAAE,SAAS,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5C,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;;;;;;;OAcG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IACxC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACjD;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,qBAAqB;IAClC,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,gFAAgF;IAChF,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B;;;OAGG;IACH,cAAc,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,oBAAqB,SAAQ,gBAAgB;IAC1D,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;CACrD;AAID,eAAO,MAAM,eAAe,6FA4mB1B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAOL,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;AAsDjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AA4ZD,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAiQxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AAiCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
1
+ {"version":3,"file":"Dialog.web.d.ts","sourceRoot":"","sources":["../../../../src/dialog/Dialog.web.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAOL,4BAA4B,EAM7B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EACV,YAAY,EAGZ,WAAW,EACZ,MAAM,SAAS,CAAC;AA4DjB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,WAAW,2CAazD;AA+ZD,OAAO,EAAE,4BAA4B,EAAE,CAAC;AAoPxC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,SAAS,GACV,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB,2CAgBA;AAiCD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,mZAK5B,CAAC"}
@@ -2,7 +2,8 @@
2
2
  * The shared plumbing every portaled surface needs: an interactive ROOT and a
3
3
  * press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
4
4
  * toasts each used to hand-roll both, and each one got the web contract subtly
5
- * wrong in its own way.
5
+ * wrong in its own way — and looked different while doing it (the image viewer
6
+ * blurred, everything else only dimmed).
6
7
  *
7
8
  * ## The contract these two components encode
8
9
  *
@@ -26,11 +27,27 @@
26
27
  * which is what makes it look like a dismissal bug rather than a hit-testing
27
28
  * one.
28
29
  *
30
+ * A backdrop also only dismisses what is actually ON TOP of it: a full-screen
31
+ * layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
32
+ * press first and swallows it. Either that layer opts out (`pointerEvents
33
+ *="box-none"`) or it owns the dismiss itself — the backdrop being present is
34
+ * not enough.
35
+ *
36
+ * EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
37
+ * two components are universal.
38
+ *
29
39
  * Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
30
40
  * dimming layer; do not re-implement either with raw `View`s.
31
41
  */
32
42
  import { type ReactNode } from 'react';
33
43
  import { type StyleProp, type ViewStyle } from 'react-native';
44
+ /**
45
+ * One blur radius for every Bloom overlay. Surfaces differ in what they show,
46
+ * not in how the app behind them recedes.
47
+ */
48
+ export declare const BACKDROP_BLUR_INTENSITY = 40;
49
+ /** Dim laid over the blur — the blur alone does not give enough contrast. */
50
+ export declare const BACKDROP_DIM_OPACITY = 0.45;
34
51
  export interface OverlayRootProps {
35
52
  children?: ReactNode;
36
53
  style?: StyleProp<ViewStyle>;
@@ -51,17 +68,25 @@ export interface BackdropProps {
51
68
  onPress?: () => void;
52
69
  /** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
53
70
  disabled?: boolean;
54
- /** Extra style (dim colour, animated opacity). Rendered over `absoluteFill`. */
71
+ /** Blur radius behind the dim. `0` renders the dim alone. */
72
+ blurIntensity?: number;
73
+ /** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
74
+ blurTint?: 'light' | 'dark' | 'default';
75
+ /** Dim colour over the blur. */
76
+ dimColor?: string;
77
+ /** Dim opacity, 0–1. */
78
+ dimOpacity?: number;
79
+ /** Extra style on the press target — animated opacity, insets, z-index. */
55
80
  style?: StyleProp<ViewStyle>;
56
- /** Rendered inside the backdrop e.g. a blur layer that must dim with it. */
81
+ /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
57
82
  children?: ReactNode;
58
83
  accessibilityLabel?: string;
59
84
  testID?: string;
60
85
  }
61
86
  /**
62
- * Full-bleed dimming layer that dismisses the surface when pressed. Always
63
- * takes pointer events (that is its whole job), so it must render UNDER the
64
- * surface's panel in the tree, never over it.
87
+ * Full-bleed blur + dim that dismisses the surface when pressed. Always takes
88
+ * pointer events (that is its whole job), so anything that must stay pressable
89
+ * goes INSIDE it as `children`, never as a sibling rendered over it.
65
90
  */
66
91
  export declare const Backdrop: import("react").NamedExoticComponent<BackdropProps>;
67
92
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAC7C,OAAO,EAAyC,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIrG,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA0BnB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/overlay/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAKL,KAAK,SAAS,EACd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAItB;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,6EAA6E;AAC7E,eAAO,MAAM,oBAAoB,OAAO,CAAC;AAEzC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAMxE;yBANe,WAAW;;;AAU3B,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IACxC,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wBAAwB;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,eAAO,MAAM,QAAQ,qDA6CnB,CAAC"}
@@ -18,6 +18,12 @@
18
18
  * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
19
  * longer a per-app decision.
20
20
  *
21
+ * EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
22
+ * `expo-router` (its web implementation keys offsets on the focused route), so
23
+ * a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
24
+ * apps keep mounting `BloomThemeProvider` (and any other piece they need)
25
+ * directly — that is not a lesser path, just the one without a router.
26
+ *
21
27
  * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
28
  * in the tree is a real app decision (z-order, safe areas, and mounting a
23
29
  * second one duplicates every surface it renders):
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/provider/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAG9E,OAAO,EAAsB,KAAK,uBAAuB,EAAE,MAAM,UAAU,CAAC;AAG5E,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC;IACnF,QAAQ,EAAE,SAAS,CAAC;IACpB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,wFAAwF;IACxF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,aAAa,EACb,OAAc,EACd,GAAG,UAAU,EACd,EAAE,kBAAkB,2CAcpB;yBAnBe,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAu4BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ZoomableImageGallery.d.ts","sourceRoot":"","sources":["../../../../src/zoomable-image-gallery/ZoomableImageGallery.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAmEjF,OAAO,KAAK,EAGV,0BAA0B,EAC1B,yBAAyB,EAC1B,MAAM,SAAS,CAAC;AAs5BjB,eAAO,MAAM,oBAAoB,8GAA4B,CAAC;AAyK9D,eAAe,oBAAoB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/bloom",
3
- "version": "0.57.0",
3
+ "version": "0.58.0",
4
4
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -106,6 +106,23 @@ describe('overlay pointer-events contract (web)', () => {
106
106
  container.remove();
107
107
  });
108
108
 
109
+ // Every Bloom surface dims the app the same way: the blur is part of the
110
+ // shared backdrop, not something each surface opts into (it used to be — the
111
+ // image viewer blurred, dialogs and sheets only dimmed).
112
+ it('blurs by default and drops the blur layer only when asked', () => {
113
+ const withBlur = render(createElement(Backdrop, { onPress: () => {}, testID: 'a' }));
114
+ expect(withBlur.container.getElementsByTagName('blurview').length).toBe(1);
115
+ act(() => withBlur.root.unmount());
116
+ withBlur.container.remove();
117
+
118
+ const noBlur = render(
119
+ createElement(Backdrop, { onPress: () => {}, blurIntensity: 0, testID: 'b' }),
120
+ );
121
+ expect(noBlur.container.getElementsByTagName('blurview').length).toBe(0);
122
+ act(() => noBlur.root.unmount());
123
+ noBlur.container.remove();
124
+ });
125
+
109
126
  it('a disabled Backdrop dims without dismissing', () => {
110
127
  const onPress = jest.fn();
111
128
  const { root, container } = render(
@@ -25,6 +25,7 @@ import Animated, {
25
25
  withTiming,
26
26
  } from 'react-native-reanimated';
27
27
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
+ import { Backdrop } from '../overlay';
28
29
  import { useTheme } from '../theme/use-theme';
29
30
 
30
31
  /** Hook that returns current screen dimensions and updates on rotation/resize. */
@@ -198,6 +199,8 @@ export interface BottomSheetBaseProps extends BottomSheetProps {
198
199
  Shell: React.ComponentType<BottomSheetShellProps>;
199
200
  }
200
201
 
202
+ const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
203
+
201
204
  export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: React.ForwardedRef<BottomSheetRef>) => {
202
205
  const {
203
206
  Shell,
@@ -783,15 +786,21 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
783
786
  return (
784
787
  <Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
785
788
  <View style={StyleSheet.absoluteFill}>
786
- <Animated.View style={[styles.backdrop, backdropStyle]}>
787
- {backdropComponent ? (
788
- backdropComponent({ onPress: handleBackdropPress })
789
- ) : (
790
- <Pressable style={styles.backdropTouchable} onPress={handleBackdropPress}>
791
- <View style={StyleSheet.absoluteFill} />
792
- </Pressable>
793
- )}
794
- </Animated.View>
789
+ {backdropComponent ? (
790
+ <Animated.View style={[StyleSheet.absoluteFill, backdropStyle]}>
791
+ {backdropComponent({ onPress: handleBackdropPress })}
792
+ </Animated.View>
793
+ ) : (
794
+ // 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.
798
+ <AnimatedBackdrop
799
+ onPress={handleBackdropPress}
800
+ dimOpacity={1}
801
+ style={[styles.backdrop, backdropStyle]}
802
+ />
803
+ )}
795
804
 
796
805
  <GestureDetector gesture={panGesture}>
797
806
  <Animated.View
@@ -60,6 +60,12 @@ import type {
60
60
 
61
61
  const FADE_OUT_DURATION = CENTER_FADE_OUT_DURATION;
62
62
 
63
+ /**
64
+ * The centered card's scrim reads darker than a sheet's because the card floats
65
+ * in the middle of it; the shared blur underneath is identical.
66
+ */
67
+ const DIALOG_BACKDROP_DIM_OPACITY = 0.6;
68
+
63
69
  /**
64
70
  * The four CSS `animation` shorthands driving the centered card and its backdrop.
65
71
  * `animation` has no `ViewStyle` key at all — react-native-web forwards it to the
@@ -325,15 +331,18 @@ function CenterOrSideDialog({
325
331
  onPress={() => close()}
326
332
  disabled={!dismissOnBackdrop}
327
333
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
328
- style={{
329
- position: WEB_POSITION_FIXED,
330
- zIndex: dialogZIndex.backdrop,
331
- alignItems: 'center',
332
- justifyContent: 'center',
333
- paddingHorizontal: 20,
334
- }}
334
+ dimOpacity={DIALOG_BACKDROP_DIM_OPACITY}
335
+ style={[
336
+ {
337
+ position: WEB_POSITION_FIXED,
338
+ zIndex: dialogZIndex.backdrop,
339
+ alignItems: 'center',
340
+ justifyContent: 'center',
341
+ paddingHorizontal: 20,
342
+ },
343
+ isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
344
+ ]}
335
345
  >
336
- <DialogBackdrop isClosing={isClosing} />
337
346
  <DialogPanel
338
347
  testID={testID}
339
348
  label={label}
@@ -693,6 +702,9 @@ function SheetSurface({
693
702
  accessibilityLabel={label ? `Dismiss ${label}` : 'Dismiss dialog'}
694
703
  onPress={handleBackdropPress}
695
704
  disabled={!dismissOnBackdrop}
705
+ // The dim rides on the element's own animated opacity here, so the
706
+ // shared component's dim layer stays fully opaque under it.
707
+ dimOpacity={1}
696
708
  style={[
697
709
  sheetStyles.backdrop,
698
710
  backdropTransition,
@@ -799,22 +811,6 @@ function cancelFrame(token: FrameToken): void {
799
811
  clearTimeout(token.timer);
800
812
  }
801
813
 
802
- function DialogBackdrop({ isClosing }: { isClosing: boolean }) {
803
- const style: ViewStyle[] = [
804
- {
805
- position: WEB_POSITION_FIXED,
806
- top: 0,
807
- left: 0,
808
- right: 0,
809
- bottom: 0,
810
- backgroundColor: 'rgba(0,0,0,0.8)',
811
- },
812
- isClosing ? BACKDROP_FADE_OUT : BACKDROP_FADE_IN,
813
- ];
814
-
815
- return <View style={style} />;
816
- }
817
-
818
814
  /**
819
815
  * Inline imperative dialog used by `alert()`. Mounts and immediately
820
816
  * presents; resolves the host's `onResolve` once the dialog has finished
@@ -2,7 +2,8 @@
2
2
  * The shared plumbing every portaled surface needs: an interactive ROOT and a
3
3
  * press-to-dismiss BACKDROP. Dialog, BottomSheet, the image gallery, menus and
4
4
  * toasts each used to hand-roll both, and each one got the web contract subtly
5
- * wrong in its own way.
5
+ * wrong in its own way — and looked different while doing it (the image viewer
6
+ * blurred, everything else only dimmed).
6
7
  *
7
8
  * ## The contract these two components encode
8
9
  *
@@ -26,14 +27,39 @@
26
27
  * which is what makes it look like a dismissal bug rather than a hit-testing
27
28
  * one.
28
29
  *
30
+ * A backdrop also only dismisses what is actually ON TOP of it: a full-screen
31
+ * layer rendered ABOVE the backdrop (a pager, a zoom container) receives the
32
+ * press first and swallows it. Either that layer opts out (`pointerEvents
33
+ *="box-none"`) or it owns the dismiss itself — the backdrop being present is
34
+ * not enough.
35
+ *
36
+ * EXPO/EXPO-ROUTER APPS ONLY for `BloomProvider` — see `src/provider`; these
37
+ * two components are universal.
38
+ *
29
39
  * Use `<OverlayRoot>` for the surface's outermost node and `<Backdrop>` for its
30
40
  * dimming layer; do not re-implement either with raw `View`s.
31
41
  */
32
42
  import { memo, type ReactNode } from 'react';
33
- import { Platform, Pressable, StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
43
+ import { BlurView } from 'expo-blur';
44
+ import {
45
+ Platform,
46
+ Pressable,
47
+ StyleSheet,
48
+ View,
49
+ type StyleProp,
50
+ type ViewStyle,
51
+ } from 'react-native';
34
52
 
35
53
  import { WEB_POSITION_FIXED } from '../styles/web-view-style';
36
54
 
55
+ /**
56
+ * One blur radius for every Bloom overlay. Surfaces differ in what they show,
57
+ * not in how the app behind them recedes.
58
+ */
59
+ export const BACKDROP_BLUR_INTENSITY = 40;
60
+ /** Dim laid over the blur — the blur alone does not give enough contrast. */
61
+ export const BACKDROP_DIM_OPACITY = 0.45;
62
+
37
63
  export interface OverlayRootProps {
38
64
  children?: ReactNode;
39
65
  style?: StyleProp<ViewStyle>;
@@ -61,22 +87,34 @@ export interface BackdropProps {
61
87
  onPress?: () => void;
62
88
  /** `true` keeps the dim but makes it inert — a blocking dialog, a busy state. */
63
89
  disabled?: boolean;
64
- /** Extra style (dim colour, animated opacity). Rendered over `absoluteFill`. */
90
+ /** Blur radius behind the dim. `0` renders the dim alone. */
91
+ blurIntensity?: number;
92
+ /** Blur tint. Backdrops dim the app, so `dark` is the default on every theme. */
93
+ blurTint?: 'light' | 'dark' | 'default';
94
+ /** Dim colour over the blur. */
95
+ dimColor?: string;
96
+ /** Dim opacity, 0–1. */
97
+ dimOpacity?: number;
98
+ /** Extra style on the press target — animated opacity, insets, z-index. */
65
99
  style?: StyleProp<ViewStyle>;
66
- /** Rendered inside the backdrop e.g. a blur layer that must dim with it. */
100
+ /** Rendered ON TOP of the dim, inside the press target (Dialog's panel does this). */
67
101
  children?: ReactNode;
68
102
  accessibilityLabel?: string;
69
103
  testID?: string;
70
104
  }
71
105
 
72
106
  /**
73
- * Full-bleed dimming layer that dismisses the surface when pressed. Always
74
- * takes pointer events (that is its whole job), so it must render UNDER the
75
- * surface's panel in the tree, never over it.
107
+ * Full-bleed blur + dim that dismisses the surface when pressed. Always takes
108
+ * pointer events (that is its whole job), so anything that must stay pressable
109
+ * goes INSIDE it as `children`, never as a sibling rendered over it.
76
110
  */
77
111
  export const Backdrop = memo(function Backdrop({
78
112
  onPress,
79
113
  disabled = false,
114
+ blurIntensity = BACKDROP_BLUR_INTENSITY,
115
+ blurTint = 'dark',
116
+ dimColor = '#000',
117
+ dimOpacity = BACKDROP_DIM_OPACITY,
80
118
  style,
81
119
  children,
82
120
  accessibilityLabel = 'Dismiss',
@@ -97,6 +135,21 @@ export const Backdrop = memo(function Backdrop({
97
135
  testID={testID}
98
136
  style={[StyleSheet.absoluteFill, style]}
99
137
  >
138
+ {blurIntensity > 0 ? (
139
+ <BlurView
140
+ intensity={blurIntensity}
141
+ tint={blurTint}
142
+ // Android's default blur is a no-op on many devices; this is the
143
+ // implementation that actually renders there.
144
+ experimentalBlurMethod="dimezisBlurView"
145
+ pointerEvents="none"
146
+ style={StyleSheet.absoluteFill}
147
+ />
148
+ ) : null}
149
+ <View
150
+ pointerEvents="none"
151
+ style={[StyleSheet.absoluteFill, { backgroundColor: dimColor, opacity: dimOpacity }]}
152
+ />
100
153
  {children}
101
154
  </Pressable>
102
155
  );
@@ -108,8 +161,11 @@ const styles = StyleSheet.create({
108
161
  // it anchors to the document flow in some browsers once the page scrolls —
109
162
  // pin this box to the viewport itself. Native: absolute fill inside the
110
163
  // Outlet, which is already full-screen.
111
- ...(Platform.OS === 'web'
112
- ? { position: WEB_POSITION_FIXED, top: 0, left: 0, right: 0, bottom: 0 }
113
- : StyleSheet.absoluteFillObject),
164
+ // `absoluteFillObject` was removed in RN 0.85 the inset is written out.
165
+ position: Platform.OS === 'web' ? WEB_POSITION_FIXED : 'absolute',
166
+ top: 0,
167
+ left: 0,
168
+ right: 0,
169
+ bottom: 0,
114
170
  },
115
171
  });
@@ -18,6 +18,12 @@
18
18
  * Nesting extra contexts costs nothing at runtime — the win is that scope is no
19
19
  * longer a per-app decision.
20
20
  *
21
+ * EXPO/EXPO-ROUTER APPS ONLY. The scroll-restoration provider it mounts imports
22
+ * `expo-router` (its web implementation keys offsets on the focused route), so
23
+ * a Vite/SPA consumer that has no expo-router cannot resolve this module. Those
24
+ * apps keep mounting `BloomThemeProvider` (and any other piece they need)
25
+ * directly — that is not a lesser path, just the one without a router.
26
+ *
21
27
  * NOT included, on purpose — these are OUTLETS, not state, and their placement
22
28
  * in the tree is a real app decision (z-order, safe areas, and mounting a
23
29
  * second one duplicates every surface it renders):
@@ -72,8 +72,9 @@ import type {
72
72
  ZoomableImageGalleryProps,
73
73
  } from './types';
74
74
 
75
- const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
76
75
  const AnimatedImage = Animated.createAnimatedComponent(Image);
76
+ // The shared overlay backdrop, driven by the viewer's open/drag progress.
77
+ const AnimatedBackdrop = Animated.createAnimatedComponent(Backdrop);
77
78
 
78
79
  // Web-only interaction hints (no-ops on native): the zoom surfaces are
79
80
  // tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
@@ -172,6 +173,8 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
172
173
  // (`handleDismiss` runs via `runOnJS` and from `Pressable.onPress`, where the
173
174
  // state closure can be stale). Kept in lockstep by `setActiveIndexBoth`.
174
175
  const activeIndexRef = useRef(0);
176
+ // True from the first dismiss request until the viewer is actually unmounted.
177
+ const dismissingRef = useRef(false);
175
178
 
176
179
  // Single writer for the current index: updates state (drives indicator + open
177
180
  // image) and the synchronous mirror together, and only when it changes.
@@ -238,6 +241,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
238
241
  // every dismiss path (fly-back and fade-out fallback).
239
242
  const finalizeDismiss = useCallback(() => {
240
243
  setIsOpen(false);
244
+ dismissingRef.current = false;
241
245
  scale.value = 1;
242
246
  translateX.value = 0;
243
247
  translateY.value = 0;
@@ -286,6 +290,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
286
290
  }, [finalizeDismiss, opacity, scale]);
287
291
 
288
292
  const handleDismiss = useCallback(() => {
293
+ // A press on the image reaches BOTH its tap gesture and the page beneath it
294
+ // (which owns the dismiss for the empty area around the image), so this can
295
+ // fire twice for one click. Latch it: a second call would start a second
296
+ // fly-back from an already-moving image.
297
+ if (dismissingRef.current) return;
298
+ dismissingRef.current = true;
289
299
  // Collapse the pager back to the single open-image so the fly-back animates
290
300
  // one image (the current one) rather than the whole scrolled strip.
291
301
  setPagerReady(false);
@@ -334,6 +344,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
334
344
  const open = useCallback(
335
345
  (nextImages: GalleryImage[], index: number, rect?: MeasuredRect) => {
336
346
  if (isOpen || nextImages.length === 0) return;
347
+ dismissingRef.current = false;
337
348
  const safeIndex = Math.min(Math.max(index, 0), nextImages.length - 1);
338
349
  const target = nextImages[safeIndex];
339
350
  if (!target) return;
@@ -753,18 +764,12 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
753
764
  did nothing on web while Escape still closed. `Backdrop` opts back
754
765
  in via the `pointerEvents` PROP, the only form that reaches the DOM
755
766
  (see `src/overlay`). */}
756
- <Backdrop onPress={handleDismiss} accessibilityLabel="Close image viewer">
757
- <AnimatedBlurView
758
- intensity={80}
759
- tint={theme.isDark ? 'dark' : 'light'}
760
- experimentalBlurMethod="dimezisBlurView"
761
- style={[StyleSheet.absoluteFill, backdropStyle]}
762
- >
763
- <Animated.View
764
- style={[StyleSheet.absoluteFill, { backgroundColor: theme.colors.overlay }, backdropStyle]}
765
- />
766
- </AnimatedBlurView>
767
- </Backdrop>
767
+ <AnimatedBackdrop
768
+ onPress={handleDismiss}
769
+ accessibilityLabel="Close image viewer"
770
+ dimColor={theme.colors.overlay}
771
+ style={backdropStyle}
772
+ />
768
773
 
769
774
  <GestureDetector gesture={panGesture}>
770
775
  <Animated.View
@@ -817,8 +822,16 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
817
822
  // through the gesture, so it isn't wrapped in a Pressable.
818
823
  if (idx === activeIndex) {
819
824
  return (
820
- <View
825
+ <Pressable
821
826
  key={`${img.uri}-${idx}`}
827
+ // The page fills the screen ON TOP of the backdrop (the
828
+ // pager below it needs pointer events to swipe), so a
829
+ // press on the empty area around the image can never
830
+ // reach the backdrop — the page dismisses it itself.
831
+ // Same outcome as tapping the image, which already
832
+ // dismisses through `singleTapDismissGesture`.
833
+ onPress={handleDismiss}
834
+ accessibilityLabel="Close image viewer"
822
835
  style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }, webPointerStyle]}
823
836
  >
824
837
  <GestureDetector gesture={activePageGesture}>
@@ -833,12 +846,14 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
833
846
  {...(Platform.OS === 'web' ? { draggable: false } : {})}
834
847
  />
835
848
  </GestureDetector>
836
- </View>
849
+ </Pressable>
837
850
  );
838
851
  }
839
852
  return (
840
- <View
853
+ <Pressable
841
854
  key={`${img.uri}-${idx}`}
855
+ onPress={handleDismiss}
856
+ accessibilityLabel="Close image viewer"
842
857
  style={[styles.page, { width: SCREEN_WIDTH, height: SCREEN_HEIGHT }]}
843
858
  >
844
859
  <Image
@@ -848,7 +863,7 @@ const ZoomableImageGalleryInner = React.forwardRef<ZoomableImageGalleryHandle, Z
848
863
  transition={0}
849
864
  {...(Platform.OS === 'web' ? { draggable: false } : {})}
850
865
  />
851
- </View>
866
+ </Pressable>
852
867
  );
853
868
  })}
854
869
  </ScrollView>