@oxyhq/bloom 0.32.1 → 0.33.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.
- package/lib/commonjs/hooks/index.js +13 -0
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/useImagePreload.js +52 -0
- package/lib/commonjs/hooks/useImagePreload.js.map +1 -0
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js +65 -0
- package/lib/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -0
- package/lib/commonjs/image-aspect-ratio-cache/index.js +37 -0
- package/lib/commonjs/image-aspect-ratio-cache/index.js.map +1 -0
- package/lib/commonjs/index.js +15 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +15 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js +664 -0
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/constants.js +41 -0
- package/lib/commonjs/zoomable-image-gallery/constants.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/index.js +19 -0
- package/lib/commonjs/zoomable-image-gallery/index.js.map +1 -0
- package/lib/commonjs/zoomable-image-gallery/types.js +2 -0
- package/lib/commonjs/zoomable-image-gallery/types.js.map +1 -0
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useImagePreload.js +48 -0
- package/lib/module/hooks/useImagePreload.js.map +1 -0
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js +57 -0
- package/lib/module/image-aspect-ratio-cache/imageAspectRatioCache.js.map +1 -0
- package/lib/module/image-aspect-ratio-cache/index.js +4 -0
- package/lib/module/image-aspect-ratio-cache/index.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js +659 -0
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +1 -0
- package/lib/module/zoomable-image-gallery/constants.js +38 -0
- package/lib/module/zoomable-image-gallery/constants.js.map +1 -0
- package/lib/module/zoomable-image-gallery/index.js +4 -0
- package/lib/module/zoomable-image-gallery/index.js.map +1 -0
- package/lib/module/zoomable-image-gallery/types.js +2 -0
- package/lib/module/zoomable-image-gallery/types.js.map +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/useImagePreload.d.ts +10 -0
- package/lib/typescript/commonjs/hooks/useImagePreload.d.ts.map +1 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +14 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts +2 -0
- package/lib/typescript/commonjs/image-aspect-ratio-cache/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +1 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +5 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/constants.d.ts +33 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +3 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +46 -0
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +1 -0
- package/lib/typescript/module/hooks/index.d.ts +1 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/useImagePreload.d.ts +10 -0
- package/lib/typescript/module/hooks/useImagePreload.d.ts.map +1 -0
- package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts +14 -0
- package/lib/typescript/module/image-aspect-ratio-cache/imageAspectRatioCache.d.ts.map +1 -0
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts +2 -0
- package/lib/typescript/module/image-aspect-ratio-cache/index.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +1 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +5 -0
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/constants.d.ts +33 -0
- package/lib/typescript/module/zoomable-image-gallery/constants.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts +3 -0
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts +46 -0
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +1 -0
- package/package.json +27 -1
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useImagePreload.ts +51 -0
- package/src/image-aspect-ratio-cache/imageAspectRatioCache.ts +62 -0
- package/src/image-aspect-ratio-cache/index.ts +7 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/zoomable-image-gallery/ZoomableImageGallery.tsx +695 -0
- package/src/zoomable-image-gallery/constants.ts +25 -0
- package/src/zoomable-image-gallery/index.ts +8 -0
- package/src/zoomable-image-gallery/types.ts +49 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = exports.ZoomableImageGallery = void 0;
|
|
7
|
+
var _react = _interopRequireWildcard(require("react"));
|
|
8
|
+
var _reactNative = require("react-native");
|
|
9
|
+
var _expoImage = require("expo-image");
|
|
10
|
+
var _expoBlur = require("expo-blur");
|
|
11
|
+
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
|
|
12
|
+
var _reactNativeGestureHandler = require("react-native-gesture-handler");
|
|
13
|
+
var _useTheme = require("../theme/use-theme.js");
|
|
14
|
+
var _portal = require("../portal");
|
|
15
|
+
var _zIndex = require("../styles/z-index.js");
|
|
16
|
+
var _index = require("../image-aspect-ratio-cache/index.js");
|
|
17
|
+
var _constants = require("./constants.js");
|
|
18
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
19
|
+
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
20
|
+
const AnimatedBlurView = _reactNativeReanimated.default.createAnimatedComponent(_expoBlur.BlurView);
|
|
21
|
+
const AnimatedImage = _reactNativeReanimated.default.createAnimatedComponent(_expoImage.Image);
|
|
22
|
+
|
|
23
|
+
// Web-only interaction hints (no-ops on native): the zoom surfaces are
|
|
24
|
+
// tap-to-dismiss (`cursor: pointer`) and must not select text or drag the
|
|
25
|
+
// image. `Platform.select` keeps them off native; the `as ViewStyle` cast
|
|
26
|
+
// mirrors Bloom's other web-only style casts (e.g. Dialog.web's `animation`).
|
|
27
|
+
const webPointerStyle = _reactNative.Platform.select({
|
|
28
|
+
web: {
|
|
29
|
+
userSelect: 'none',
|
|
30
|
+
cursor: 'pointer'
|
|
31
|
+
},
|
|
32
|
+
default: undefined
|
|
33
|
+
});
|
|
34
|
+
const webUserSelectNoneStyle = _reactNative.Platform.select({
|
|
35
|
+
web: {
|
|
36
|
+
userSelect: 'none'
|
|
37
|
+
},
|
|
38
|
+
default: undefined
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* Fullscreen, swipeable image viewer that replicates the profile avatar's
|
|
42
|
+
* measured-origin zoom transition (`ZoomableAvatar`) for rectangular post media:
|
|
43
|
+
*
|
|
44
|
+
* - Open/close feel is identical to the avatar (same spring configs, web
|
|
45
|
+
* timing/easing, blur backdrop, and the measure-origin technique). The viewer
|
|
46
|
+
* renders through the Bloom `Portal` on BOTH platforms (RN's `Modal` is not
|
|
47
|
+
* used on native — on the New Architecture / Fabric Android its host views
|
|
48
|
+
* mount full-screen but never composite, leaving the viewer invisible).
|
|
49
|
+
* - The OPENING (tapped) image animates from its measured rect to a centered,
|
|
50
|
+
* aspect-ratio-preserving fit within {@link FIT_FRACTION} of the screen. Once
|
|
51
|
+
* the open animation settles, a horizontal paging `ScrollView` mounts seeded at
|
|
52
|
+
* the tapped index so the user can swipe between every image in the post.
|
|
53
|
+
* - Gesture disambiguation: the pager owns horizontal swipes; a vertical-only
|
|
54
|
+
* `Gesture.Pan` (`activeOffsetY` + `failOffsetX`) owns drag-to-dismiss, so the
|
|
55
|
+
* two never fight.
|
|
56
|
+
*/
|
|
57
|
+
const ZoomableImageGalleryInner = /*#__PURE__*/_react.default.forwardRef(({
|
|
58
|
+
measureThumb,
|
|
59
|
+
cornerRadius = _constants.DEFAULT_CORNER_RADIUS
|
|
60
|
+
}, ref) => {
|
|
61
|
+
const theme = (0, _useTheme.useTheme)();
|
|
62
|
+
const {
|
|
63
|
+
width: SCREEN_WIDTH,
|
|
64
|
+
height: SCREEN_HEIGHT
|
|
65
|
+
} = (0, _reactNative.useWindowDimensions)();
|
|
66
|
+
const [isOpen, setIsOpen] = (0, _react.useState)(false);
|
|
67
|
+
// Once true, the swipeable pager is mounted and the single open-image hidden.
|
|
68
|
+
const [pagerReady, setPagerReady] = (0, _react.useState)(false);
|
|
69
|
+
const [images, setImages] = (0, _react.useState)([]);
|
|
70
|
+
const [activeIndex, setActiveIndex] = (0, _react.useState)(0);
|
|
71
|
+
// Aspect ratio of the OPENING image (drives the open-animation fit box).
|
|
72
|
+
const [openRatio, setOpenRatio] = (0, _react.useState)(_index.DEFAULT_ASPECT_RATIO);
|
|
73
|
+
// Per-image aspect ratios for the pager pages (index-aligned with `images`).
|
|
74
|
+
const [pageRatios, setPageRatios] = (0, _react.useState)({});
|
|
75
|
+
const scale = (0, _reactNativeReanimated.useSharedValue)(1);
|
|
76
|
+
const translateX = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
77
|
+
const translateY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
78
|
+
const opacity = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
79
|
+
|
|
80
|
+
// Origin (offset from screen center) of the tapped image.
|
|
81
|
+
const originX = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
82
|
+
const originY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
83
|
+
const startX = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
84
|
+
const startY = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
85
|
+
// Per-drag baseline captured at pan start (transient).
|
|
86
|
+
const startScale = (0, _reactNativeReanimated.useSharedValue)(1);
|
|
87
|
+
// Persistent scale of the open-image at its thumbnail origin (thumbWidth /
|
|
88
|
+
// fittedWidth). The open animation grows from this to 1; the close animation
|
|
89
|
+
// shrinks back to it. Never overwritten by the drag gesture.
|
|
90
|
+
const originScale = (0, _reactNativeReanimated.useSharedValue)(1);
|
|
91
|
+
const pagerRef = (0, _react.useRef)(null);
|
|
92
|
+
// Latch the index the pager must land on once it has mounted + laid out.
|
|
93
|
+
const pendingIndexRef = (0, _react.useRef)(0);
|
|
94
|
+
// Mirror of `activeIndex` readable synchronously from callbacks/worklets
|
|
95
|
+
// (`handleDismiss` runs via `runOnJS` and from `Pressable.onPress`, where the
|
|
96
|
+
// state closure can be stale). Kept in lockstep by `setActiveIndexBoth`.
|
|
97
|
+
const activeIndexRef = (0, _react.useRef)(0);
|
|
98
|
+
|
|
99
|
+
// Single writer for the current index: updates state (drives indicator + open
|
|
100
|
+
// image) and the synchronous mirror together, and only when it changes.
|
|
101
|
+
const setActiveIndexBoth = (0, _react.useCallback)(next => {
|
|
102
|
+
activeIndexRef.current = next;
|
|
103
|
+
setActiveIndex(prev => prev === next ? prev : next);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
// Box the fitted image must fit inside.
|
|
107
|
+
const fitBox = (0, _react.useMemo)(() => ({
|
|
108
|
+
width: SCREEN_WIDTH * _constants.FIT_FRACTION,
|
|
109
|
+
height: SCREEN_HEIGHT * _constants.FIT_FRACTION
|
|
110
|
+
}), [SCREEN_WIDTH, SCREEN_HEIGHT]);
|
|
111
|
+
|
|
112
|
+
// Compute the largest width/height for `ratio` that fits inside `fitBox`
|
|
113
|
+
// without cropping (contain).
|
|
114
|
+
const fitForRatio = (0, _react.useCallback)(ratio => {
|
|
115
|
+
const safeRatio = ratio > 0 && Number.isFinite(ratio) ? ratio : _index.DEFAULT_ASPECT_RATIO;
|
|
116
|
+
let width = fitBox.width;
|
|
117
|
+
let height = width / safeRatio;
|
|
118
|
+
if (height > fitBox.height) {
|
|
119
|
+
height = fitBox.height;
|
|
120
|
+
width = height * safeRatio;
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
width,
|
|
124
|
+
height
|
|
125
|
+
};
|
|
126
|
+
}, [fitBox]);
|
|
127
|
+
|
|
128
|
+
// Fitted size of the single open-image for the CURRENT page. On open this is
|
|
129
|
+
// the opened image's fit (active index == opened index, ratio == `openRatio`);
|
|
130
|
+
// after swiping it tracks the viewed image so the collapse-on-dismiss renders
|
|
131
|
+
// and flies back the image actually on screen.
|
|
132
|
+
const activeRatio = pageRatios[activeIndex] ?? openRatio;
|
|
133
|
+
const activeFit = (0, _react.useMemo)(() => fitForRatio(activeRatio), [fitForRatio, activeRatio]);
|
|
134
|
+
|
|
135
|
+
// Alt text (accessibility description) of the image currently on screen, shown
|
|
136
|
+
// as a caption at the bottom of the viewer (Bluesky-style lightbox footer).
|
|
137
|
+
const activeAlt = images[activeIndex]?.alt?.trim() || undefined;
|
|
138
|
+
const ensureRatio = (0, _react.useCallback)((index, uri) => {
|
|
139
|
+
const cached = (0, _index.getAspectRatio)(uri);
|
|
140
|
+
if (cached !== undefined) {
|
|
141
|
+
setPageRatios(prev => prev[index] === cached ? prev : {
|
|
142
|
+
...prev,
|
|
143
|
+
[index]: cached
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
void (0, _index.fetchAspectRatio)(uri).then(ratio => {
|
|
148
|
+
setPageRatios(prev => prev[index] === ratio ? prev : {
|
|
149
|
+
...prev,
|
|
150
|
+
[index]: ratio
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}, []);
|
|
154
|
+
|
|
155
|
+
// Unmount + reset all animation values to their neutral baseline. Shared by
|
|
156
|
+
// every dismiss path (fly-back and fade-out fallback).
|
|
157
|
+
const finalizeDismiss = (0, _react.useCallback)(() => {
|
|
158
|
+
setIsOpen(false);
|
|
159
|
+
scale.value = 1;
|
|
160
|
+
translateX.value = 0;
|
|
161
|
+
translateY.value = 0;
|
|
162
|
+
opacity.value = 0;
|
|
163
|
+
}, [opacity, scale, translateX, translateY]);
|
|
164
|
+
|
|
165
|
+
// Fly the (possibly dragged) image back toward `target` — the rect of the
|
|
166
|
+
// thumbnail currently being viewed — shrinking to its footprint. Uses the
|
|
167
|
+
// EXACT same close spring (native) / timing (web) as the avatar transition.
|
|
168
|
+
const flyBackTo = (0, _react.useCallback)(target => {
|
|
169
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
170
|
+
const duration = _constants.CLOSE_DURATION_WEB;
|
|
171
|
+
const easing = _reactNativeReanimated.Easing.in(_reactNativeReanimated.Easing.cubic);
|
|
172
|
+
scale.value = (0, _reactNativeReanimated.withTiming)(target.scale, {
|
|
173
|
+
duration,
|
|
174
|
+
easing
|
|
175
|
+
});
|
|
176
|
+
translateX.value = (0, _reactNativeReanimated.withTiming)(target.x, {
|
|
177
|
+
duration,
|
|
178
|
+
easing
|
|
179
|
+
});
|
|
180
|
+
translateY.value = (0, _reactNativeReanimated.withTiming)(target.y, {
|
|
181
|
+
duration,
|
|
182
|
+
easing
|
|
183
|
+
});
|
|
184
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
185
|
+
duration,
|
|
186
|
+
easing
|
|
187
|
+
});
|
|
188
|
+
setTimeout(finalizeDismiss, duration + 20);
|
|
189
|
+
} else {
|
|
190
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(target.scale, _constants.CLOSE_SPRING);
|
|
191
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(target.x, _constants.CLOSE_SPRING);
|
|
192
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(target.y, _constants.CLOSE_SPRING);
|
|
193
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
194
|
+
duration: _constants.OPACITY_DURATION
|
|
195
|
+
});
|
|
196
|
+
setTimeout(finalizeDismiss, _constants.CLOSE_DURATION_WEB);
|
|
197
|
+
}
|
|
198
|
+
}, [finalizeDismiss, opacity, scale, translateX, translateY]);
|
|
199
|
+
|
|
200
|
+
// Fallback when the current thumbnail cannot be measured (ref missing /
|
|
201
|
+
// unmounted / virtualized): a plain center fade-out with a slight scale-down,
|
|
202
|
+
// then unmount. Keeps the same web timing / native spring feel.
|
|
203
|
+
const fadeOutCenter = (0, _react.useCallback)(() => {
|
|
204
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
205
|
+
const duration = _constants.CLOSE_DURATION_WEB;
|
|
206
|
+
const easing = _reactNativeReanimated.Easing.in(_reactNativeReanimated.Easing.cubic);
|
|
207
|
+
scale.value = (0, _reactNativeReanimated.withTiming)(_constants.MIN_DRAG_SCALE, {
|
|
208
|
+
duration,
|
|
209
|
+
easing
|
|
210
|
+
});
|
|
211
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
212
|
+
duration,
|
|
213
|
+
easing
|
|
214
|
+
});
|
|
215
|
+
setTimeout(finalizeDismiss, duration + 20);
|
|
216
|
+
} else {
|
|
217
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(_constants.MIN_DRAG_SCALE, _constants.CLOSE_SPRING);
|
|
218
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
219
|
+
duration: _constants.OPACITY_DURATION
|
|
220
|
+
});
|
|
221
|
+
setTimeout(finalizeDismiss, _constants.CLOSE_DURATION_WEB);
|
|
222
|
+
}
|
|
223
|
+
}, [finalizeDismiss, opacity, scale]);
|
|
224
|
+
const handleDismiss = (0, _react.useCallback)(() => {
|
|
225
|
+
// Collapse the pager back to the single open-image so the fly-back animates
|
|
226
|
+
// one image (the current one) rather than the whole scrolled strip.
|
|
227
|
+
setPagerReady(false);
|
|
228
|
+
const index = activeIndexRef.current;
|
|
229
|
+
const current = images[index];
|
|
230
|
+
// Recompute the fly-back target from the CURRENT image: the live rect of its
|
|
231
|
+
// thumbnail + the same fitted box (`activeFit`) the open-image is rendered
|
|
232
|
+
// at, using the same screen-center math as `open`. Falls back to a center
|
|
233
|
+
// fade-out when the thumbnail can't be measured.
|
|
234
|
+
if (measureThumb && current) {
|
|
235
|
+
const centerX = SCREEN_WIDTH / 2;
|
|
236
|
+
const centerY = SCREEN_HEIGHT / 2;
|
|
237
|
+
void measureThumb(index).then(rect => {
|
|
238
|
+
if (rect && rect.width > 0 && activeFit.width > 0) {
|
|
239
|
+
flyBackTo({
|
|
240
|
+
x: rect.x + rect.width / 2 - centerX,
|
|
241
|
+
y: rect.y + rect.height / 2 - centerY,
|
|
242
|
+
scale: rect.width / activeFit.width
|
|
243
|
+
});
|
|
244
|
+
} else {
|
|
245
|
+
fadeOutCenter();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
fadeOutCenter();
|
|
251
|
+
}, [activeFit, fadeOutCenter, flyBackTo, images, measureThumb, SCREEN_HEIGHT, SCREEN_WIDTH]);
|
|
252
|
+
|
|
253
|
+
// Reveal the swipeable pager once the open animation has settled. The index it
|
|
254
|
+
// lands on is held in `pendingIndexRef` (set synchronously in `open`) and
|
|
255
|
+
// applied in `onPagerLayout`.
|
|
256
|
+
const revealPager = (0, _react.useCallback)(() => {
|
|
257
|
+
setPagerReady(true);
|
|
258
|
+
}, []);
|
|
259
|
+
const open = (0, _react.useCallback)((nextImages, index, rect) => {
|
|
260
|
+
if (isOpen || nextImages.length === 0) return;
|
|
261
|
+
const safeIndex = Math.min(Math.max(index, 0), nextImages.length - 1);
|
|
262
|
+
const target = nextImages[safeIndex];
|
|
263
|
+
if (!target) return;
|
|
264
|
+
const knownRatio = target.aspectRatio ?? (0, _index.getAspectRatio)(target.uri);
|
|
265
|
+
const ratio = knownRatio ?? _index.DEFAULT_ASPECT_RATIO;
|
|
266
|
+
setImages(nextImages);
|
|
267
|
+
setActiveIndexBoth(safeIndex);
|
|
268
|
+
setOpenRatio(ratio);
|
|
269
|
+
// Seed every page whose ratio is already known (consumer-provided metadata
|
|
270
|
+
// or a previously-cached probe) so swiping never hits the same snap — only
|
|
271
|
+
// a genuinely-unknown image falls through to `ensureRatio`'s async probe.
|
|
272
|
+
const initialRatios = {};
|
|
273
|
+
nextImages.forEach((img, i) => {
|
|
274
|
+
const r = img.aspectRatio ?? (0, _index.getAspectRatio)(img.uri);
|
|
275
|
+
if (r !== undefined) initialRatios[i] = r;
|
|
276
|
+
});
|
|
277
|
+
setPageRatios(initialRatios);
|
|
278
|
+
pendingIndexRef.current = safeIndex;
|
|
279
|
+
|
|
280
|
+
// Resolve the opening ratio if it was not yet known, then re-fit.
|
|
281
|
+
if (knownRatio === undefined) {
|
|
282
|
+
void (0, _index.fetchAspectRatio)(target.uri).then(resolved => {
|
|
283
|
+
setOpenRatio(resolved);
|
|
284
|
+
setPageRatios(prev => ({
|
|
285
|
+
...prev,
|
|
286
|
+
[safeIndex]: resolved
|
|
287
|
+
}));
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The open-image box is rendered at its FINAL fitted size; the open
|
|
292
|
+
// animation grows it from the thumbnail's footprint (scale < 1) up to 1,
|
|
293
|
+
// mirroring the avatar's small→big scale. The initial scale is the ratio
|
|
294
|
+
// of the thumbnail width to the fitted width.
|
|
295
|
+
const fitted = fitForRatio(ratio);
|
|
296
|
+
const centerX = SCREEN_WIDTH / 2;
|
|
297
|
+
const centerY = SCREEN_HEIGHT / 2;
|
|
298
|
+
if (rect && rect.width > 0) {
|
|
299
|
+
originX.value = rect.x + rect.width / 2 - centerX;
|
|
300
|
+
originY.value = rect.y + rect.height / 2 - centerY;
|
|
301
|
+
originScale.value = rect.width / fitted.width;
|
|
302
|
+
} else {
|
|
303
|
+
originX.value = 0;
|
|
304
|
+
originY.value = 0;
|
|
305
|
+
originScale.value = 1;
|
|
306
|
+
}
|
|
307
|
+
setIsOpen(true);
|
|
308
|
+
translateX.value = originX.value;
|
|
309
|
+
translateY.value = originY.value;
|
|
310
|
+
scale.value = originScale.value;
|
|
311
|
+
opacity.value = 0;
|
|
312
|
+
if (_reactNative.Platform.OS === 'web') {
|
|
313
|
+
requestAnimationFrame(() => {
|
|
314
|
+
const duration = _constants.OPEN_DURATION_WEB;
|
|
315
|
+
const easing = _reactNativeReanimated.Easing.out(_reactNativeReanimated.Easing.cubic);
|
|
316
|
+
scale.value = (0, _reactNativeReanimated.withTiming)(1, {
|
|
317
|
+
duration,
|
|
318
|
+
easing
|
|
319
|
+
});
|
|
320
|
+
translateX.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
321
|
+
duration,
|
|
322
|
+
easing
|
|
323
|
+
});
|
|
324
|
+
translateY.value = (0, _reactNativeReanimated.withTiming)(0, {
|
|
325
|
+
duration,
|
|
326
|
+
easing
|
|
327
|
+
});
|
|
328
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(1, {
|
|
329
|
+
duration,
|
|
330
|
+
easing
|
|
331
|
+
});
|
|
332
|
+
setTimeout(revealPager, duration);
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(1, _constants.OPEN_SPRING);
|
|
337
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(0, _constants.OPEN_SPRING);
|
|
338
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(0, _constants.OPEN_SPRING);
|
|
339
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(1, {
|
|
340
|
+
duration: _constants.OPACITY_DURATION
|
|
341
|
+
});
|
|
342
|
+
setTimeout(revealPager, _constants.OPEN_DURATION_WEB);
|
|
343
|
+
}, 0);
|
|
344
|
+
}
|
|
345
|
+
}, [isOpen, SCREEN_WIDTH, SCREEN_HEIGHT, fitForRatio, opacity, originScale, originX, originY, revealPager, scale, setActiveIndexBoth, translateX, translateY]);
|
|
346
|
+
_react.default.useImperativeHandle(ref, () => ({
|
|
347
|
+
open
|
|
348
|
+
}), [open]);
|
|
349
|
+
const panGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pan().enabled(isOpen)
|
|
350
|
+
// Only claim drags that are decided to be vertical; horizontal drags
|
|
351
|
+
// fall through to the pager's ScrollView so swiping changes pages.
|
|
352
|
+
.activeOffsetY([-_constants.AXIS_DECISION_OFFSET, _constants.AXIS_DECISION_OFFSET]).failOffsetX([-_constants.AXIS_DECISION_OFFSET, _constants.AXIS_DECISION_OFFSET]).onStart(() => {
|
|
353
|
+
startX.value = translateX.value;
|
|
354
|
+
startY.value = translateY.value;
|
|
355
|
+
startScale.value = scale.value;
|
|
356
|
+
}).onUpdate(event => {
|
|
357
|
+
translateX.value = startX.value + event.translationX;
|
|
358
|
+
translateY.value = startY.value + event.translationY;
|
|
359
|
+
const dragDistance = Math.sqrt(event.translationX ** 2 + event.translationY ** 2);
|
|
360
|
+
const maxDrag = SCREEN_HEIGHT * _constants.MAX_DRAG_FRACTION;
|
|
361
|
+
opacity.value = Math.max(0, 1 - dragDistance / maxDrag);
|
|
362
|
+
const scaleReduction = Math.max(_constants.MIN_DRAG_SCALE, 1 - dragDistance / (SCREEN_HEIGHT * _constants.SCALE_DRAG_FRACTION));
|
|
363
|
+
scale.value = startScale.value * scaleReduction;
|
|
364
|
+
}).onEnd(event => {
|
|
365
|
+
const dragDistance = Math.sqrt(event.translationX ** 2 + event.translationY ** 2);
|
|
366
|
+
if (dragDistance > SCREEN_HEIGHT * _constants.DISMISS_FRACTION) {
|
|
367
|
+
(0, _reactNativeReanimated.runOnJS)(handleDismiss)();
|
|
368
|
+
} else {
|
|
369
|
+
scale.value = (0, _reactNativeReanimated.withSpring)(1, _constants.SNAP_BACK_SPRING);
|
|
370
|
+
translateX.value = (0, _reactNativeReanimated.withSpring)(0, _constants.SNAP_BACK_SPRING);
|
|
371
|
+
translateY.value = (0, _reactNativeReanimated.withSpring)(0, _constants.SNAP_BACK_SPRING);
|
|
372
|
+
opacity.value = (0, _reactNativeReanimated.withTiming)(1, {
|
|
373
|
+
duration: _constants.OPACITY_DURATION
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}), [handleDismiss, isOpen, SCREEN_HEIGHT, opacity, scale, startScale, startX, startY, translateX, translateY]);
|
|
377
|
+
const backdropStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
378
|
+
opacity: opacity.value
|
|
379
|
+
}));
|
|
380
|
+
|
|
381
|
+
// The single open-image animates from origin → fitted center.
|
|
382
|
+
const openImageStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
383
|
+
transform: [{
|
|
384
|
+
translateX: translateX.value
|
|
385
|
+
}, {
|
|
386
|
+
translateY: translateY.value
|
|
387
|
+
}, {
|
|
388
|
+
scale: scale.value
|
|
389
|
+
}]
|
|
390
|
+
}));
|
|
391
|
+
|
|
392
|
+
// While dragging to dismiss, the whole pager follows the finger + fades.
|
|
393
|
+
const pagerContainerStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
394
|
+
opacity: opacity.value,
|
|
395
|
+
transform: [{
|
|
396
|
+
translateX: translateX.value
|
|
397
|
+
}, {
|
|
398
|
+
translateY: translateY.value
|
|
399
|
+
}, {
|
|
400
|
+
scale: scale.value
|
|
401
|
+
}]
|
|
402
|
+
}));
|
|
403
|
+
|
|
404
|
+
// Derive the current page from the scroll offset, clamp into range, and update
|
|
405
|
+
// `activeIndex` only when it actually changes (drives the live indicator + the
|
|
406
|
+
// close fly-back target). Shared by `onMomentumScrollEnd` (native) and `onScroll`
|
|
407
|
+
// (web, where paging may not fire a reliable momentum-end).
|
|
408
|
+
const updateIndexFromOffset = (0, _react.useCallback)(offsetX => {
|
|
409
|
+
const lastIndex = images.length - 1;
|
|
410
|
+
if (lastIndex < 0) return;
|
|
411
|
+
const next = Math.min(Math.max(Math.round(offsetX / SCREEN_WIDTH), 0), lastIndex);
|
|
412
|
+
if (next === activeIndexRef.current) return;
|
|
413
|
+
setActiveIndexBoth(next);
|
|
414
|
+
const img = images[next];
|
|
415
|
+
if (img) ensureRatio(next, img.uri);
|
|
416
|
+
}, [ensureRatio, images, setActiveIndexBoth, SCREEN_WIDTH]);
|
|
417
|
+
const onPagerScroll = (0, _react.useCallback)(event => {
|
|
418
|
+
updateIndexFromOffset(event.nativeEvent.contentOffset.x);
|
|
419
|
+
}, [updateIndexFromOffset]);
|
|
420
|
+
|
|
421
|
+
// When the pager mounts, jump it to the open index without animation so the
|
|
422
|
+
// swap from the open-image to the pager is seamless.
|
|
423
|
+
const onPagerLayout = (0, _react.useCallback)(() => {
|
|
424
|
+
const idx = pendingIndexRef.current;
|
|
425
|
+
if (idx > 0) {
|
|
426
|
+
pagerRef.current?.scrollTo({
|
|
427
|
+
x: idx * SCREEN_WIDTH,
|
|
428
|
+
animated: false
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}, [SCREEN_WIDTH]);
|
|
432
|
+
const renderContent = () => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeGestureHandler.GestureHandlerRootView, {
|
|
433
|
+
style: styles.modalContainer,
|
|
434
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
435
|
+
style: _reactNative.StyleSheet.absoluteFill,
|
|
436
|
+
onPress: handleDismiss,
|
|
437
|
+
hitSlop: 0,
|
|
438
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedBlurView, {
|
|
439
|
+
intensity: 80,
|
|
440
|
+
tint: theme.isDark ? 'dark' : 'light',
|
|
441
|
+
experimentalBlurMethod: "dimezisBlurView",
|
|
442
|
+
style: [_reactNative.StyleSheet.absoluteFill, backdropStyle],
|
|
443
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
444
|
+
style: [_reactNative.StyleSheet.absoluteFill, {
|
|
445
|
+
backgroundColor: theme.colors.overlay
|
|
446
|
+
}, backdropStyle]
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
|
|
450
|
+
gesture: panGesture,
|
|
451
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
|
|
452
|
+
style: [_reactNative.StyleSheet.absoluteFill, styles.zoomContainer, {
|
|
453
|
+
width: SCREEN_WIDTH,
|
|
454
|
+
height: SCREEN_HEIGHT
|
|
455
|
+
}, webUserSelectNoneStyle],
|
|
456
|
+
pointerEvents: "box-none",
|
|
457
|
+
children: [!pagerReady && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
458
|
+
onPress: handleDismiss,
|
|
459
|
+
style: webPointerStyle,
|
|
460
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedImage, {
|
|
461
|
+
source: {
|
|
462
|
+
uri: images[activeIndex]?.uri
|
|
463
|
+
},
|
|
464
|
+
contentFit: "contain",
|
|
465
|
+
style: [{
|
|
466
|
+
width: activeFit.width,
|
|
467
|
+
height: activeFit.height,
|
|
468
|
+
borderRadius: cornerRadius
|
|
469
|
+
}, openImageStyle],
|
|
470
|
+
transition: 0,
|
|
471
|
+
...(_reactNative.Platform.OS === 'web' ? {
|
|
472
|
+
draggable: false
|
|
473
|
+
} : {})
|
|
474
|
+
})
|
|
475
|
+
}), pagerReady && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
476
|
+
style: [_reactNative.StyleSheet.absoluteFill, pagerContainerStyle],
|
|
477
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
|
|
478
|
+
ref: pagerRef,
|
|
479
|
+
horizontal: true,
|
|
480
|
+
pagingEnabled: true,
|
|
481
|
+
showsHorizontalScrollIndicator: false,
|
|
482
|
+
contentOffset: {
|
|
483
|
+
x: pendingIndexRef.current * SCREEN_WIDTH,
|
|
484
|
+
y: 0
|
|
485
|
+
},
|
|
486
|
+
onLayout: onPagerLayout,
|
|
487
|
+
onMomentumScrollEnd: onPagerScroll,
|
|
488
|
+
...(_reactNative.Platform.OS === 'web' ? {
|
|
489
|
+
onScroll: onPagerScroll
|
|
490
|
+
} : {}),
|
|
491
|
+
scrollEventThrottle: 16,
|
|
492
|
+
style: _reactNative.StyleSheet.absoluteFill,
|
|
493
|
+
children: images.map((img, idx) => {
|
|
494
|
+
const ratio = pageRatios[idx] ?? (0, _index.getAspectRatio)(img.uri) ?? _index.DEFAULT_ASPECT_RATIO;
|
|
495
|
+
const fit = fitForRatio(ratio);
|
|
496
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
|
|
497
|
+
onPress: handleDismiss,
|
|
498
|
+
style: [styles.page, {
|
|
499
|
+
width: SCREEN_WIDTH,
|
|
500
|
+
height: SCREEN_HEIGHT
|
|
501
|
+
}, webPointerStyle],
|
|
502
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_expoImage.Image, {
|
|
503
|
+
source: {
|
|
504
|
+
uri: img.uri
|
|
505
|
+
},
|
|
506
|
+
contentFit: "contain",
|
|
507
|
+
style: {
|
|
508
|
+
width: fit.width,
|
|
509
|
+
height: fit.height,
|
|
510
|
+
borderRadius: cornerRadius
|
|
511
|
+
},
|
|
512
|
+
transition: 0,
|
|
513
|
+
...(_reactNative.Platform.OS === 'web' ? {
|
|
514
|
+
draggable: false
|
|
515
|
+
} : {})
|
|
516
|
+
})
|
|
517
|
+
}, `${img.uri}-${idx}`);
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
}), pagerReady && images.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
|
|
521
|
+
style: [styles.indicatorWrap, backdropStyle],
|
|
522
|
+
pointerEvents: "none",
|
|
523
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
524
|
+
style: styles.counterPill,
|
|
525
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
526
|
+
style: styles.counterText,
|
|
527
|
+
children: `${activeIndex + 1} / ${images.length}`
|
|
528
|
+
})
|
|
529
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
530
|
+
style: styles.dotsRow,
|
|
531
|
+
children: images.map((img, idx) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
532
|
+
style: [styles.dot, idx === activeIndex ? styles.dotActive : styles.dotInactive]
|
|
533
|
+
}, `dot-${img.uri}-${idx}`))
|
|
534
|
+
})]
|
|
535
|
+
}), activeAlt ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
|
|
536
|
+
style: [styles.altCaptionWrap, images.length > 1 && styles.altCaptionWrapWithIndicator, backdropStyle],
|
|
537
|
+
pointerEvents: "none",
|
|
538
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
539
|
+
style: styles.altCaptionPill,
|
|
540
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
541
|
+
style: styles.altCaptionText,
|
|
542
|
+
numberOfLines: 4,
|
|
543
|
+
children: activeAlt
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
}) : null]
|
|
547
|
+
})
|
|
548
|
+
})]
|
|
549
|
+
});
|
|
550
|
+
if (!isOpen) return null;
|
|
551
|
+
|
|
552
|
+
// Both platforms render through the Bloom `Portal` so the viewer overlays the
|
|
553
|
+
// whole app from the root `Outlet`. RN's `Modal` is intentionally NOT used on
|
|
554
|
+
// native: on the New Architecture (Fabric) Android its host views mount
|
|
555
|
+
// full-screen in the tree but never composite to the screen, leaving the
|
|
556
|
+
// entire viewer (blur backdrop + zoomed image) invisible — tapping appeared to
|
|
557
|
+
// "do nothing". The Portal path is the same one the working web build uses.
|
|
558
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_portal.Portal, {
|
|
559
|
+
children: renderContent()
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
ZoomableImageGalleryInner.displayName = 'ZoomableImageGallery';
|
|
563
|
+
const ZoomableImageGallery = exports.ZoomableImageGallery = ZoomableImageGalleryInner;
|
|
564
|
+
const styles = _reactNative.StyleSheet.create({
|
|
565
|
+
modalContainer: {
|
|
566
|
+
justifyContent: 'center',
|
|
567
|
+
alignItems: 'center',
|
|
568
|
+
..._reactNative.Platform.select({
|
|
569
|
+
web: {
|
|
570
|
+
// Full-screen fixed overlay (web). Mirrors Bloom's Portal/Dialog.web
|
|
571
|
+
// `position: 'fixed' as 'absolute'` pattern; the fullscreen z-index
|
|
572
|
+
// token keeps the viewer above the app chrome.
|
|
573
|
+
position: 'fixed',
|
|
574
|
+
top: 0,
|
|
575
|
+
left: 0,
|
|
576
|
+
right: 0,
|
|
577
|
+
bottom: 0,
|
|
578
|
+
zIndex: _zIndex.Z_INDEX.fullscreen
|
|
579
|
+
},
|
|
580
|
+
default: {
|
|
581
|
+
// Mounted through the Bloom Portal `Outlet` (which renders its content
|
|
582
|
+
// inline at the app root on native, NOT wrapped in a full-screen view),
|
|
583
|
+
// so the gallery root must absolutely fill the screen itself to overlay
|
|
584
|
+
// the app. RN's `Modal` is not used on native: on the New Architecture
|
|
585
|
+
// (Fabric) Android its host views mount full-screen but never paint
|
|
586
|
+
// (the entire viewer — backdrop + image — stays invisible).
|
|
587
|
+
position: 'absolute',
|
|
588
|
+
top: 0,
|
|
589
|
+
left: 0,
|
|
590
|
+
right: 0,
|
|
591
|
+
bottom: 0,
|
|
592
|
+
backgroundColor: 'transparent'
|
|
593
|
+
}
|
|
594
|
+
})
|
|
595
|
+
},
|
|
596
|
+
zoomContainer: {
|
|
597
|
+
justifyContent: 'center',
|
|
598
|
+
alignItems: 'center'
|
|
599
|
+
},
|
|
600
|
+
page: {
|
|
601
|
+
justifyContent: 'center',
|
|
602
|
+
alignItems: 'center'
|
|
603
|
+
},
|
|
604
|
+
indicatorWrap: {
|
|
605
|
+
position: 'absolute',
|
|
606
|
+
bottom: 48,
|
|
607
|
+
left: 0,
|
|
608
|
+
right: 0,
|
|
609
|
+
alignItems: 'center',
|
|
610
|
+
gap: 10
|
|
611
|
+
},
|
|
612
|
+
counterPill: {
|
|
613
|
+
paddingHorizontal: 12,
|
|
614
|
+
paddingVertical: 4,
|
|
615
|
+
borderRadius: 999,
|
|
616
|
+
backgroundColor: 'rgba(0,0,0,0.55)'
|
|
617
|
+
},
|
|
618
|
+
counterText: {
|
|
619
|
+
color: '#fff',
|
|
620
|
+
fontSize: 13,
|
|
621
|
+
fontWeight: '600'
|
|
622
|
+
},
|
|
623
|
+
dotsRow: {
|
|
624
|
+
flexDirection: 'row',
|
|
625
|
+
gap: 6
|
|
626
|
+
},
|
|
627
|
+
dot: {
|
|
628
|
+
width: 7,
|
|
629
|
+
height: 7,
|
|
630
|
+
borderRadius: 999
|
|
631
|
+
},
|
|
632
|
+
dotActive: {
|
|
633
|
+
backgroundColor: '#fff'
|
|
634
|
+
},
|
|
635
|
+
dotInactive: {
|
|
636
|
+
backgroundColor: 'rgba(255,255,255,0.4)'
|
|
637
|
+
},
|
|
638
|
+
altCaptionWrap: {
|
|
639
|
+
position: 'absolute',
|
|
640
|
+
bottom: 48,
|
|
641
|
+
left: 0,
|
|
642
|
+
right: 0,
|
|
643
|
+
alignItems: 'center',
|
|
644
|
+
paddingHorizontal: 24
|
|
645
|
+
},
|
|
646
|
+
// Lifted above the page counter + dots when the multi-image indicator shows.
|
|
647
|
+
altCaptionWrapWithIndicator: {
|
|
648
|
+
bottom: 110
|
|
649
|
+
},
|
|
650
|
+
altCaptionPill: {
|
|
651
|
+
maxWidth: 600,
|
|
652
|
+
paddingHorizontal: 14,
|
|
653
|
+
paddingVertical: 8,
|
|
654
|
+
borderRadius: 12,
|
|
655
|
+
backgroundColor: 'rgba(0,0,0,0.6)'
|
|
656
|
+
},
|
|
657
|
+
altCaptionText: {
|
|
658
|
+
color: '#fff',
|
|
659
|
+
fontSize: 13,
|
|
660
|
+
lineHeight: 18
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
var _default = exports.default = ZoomableImageGallery;
|
|
664
|
+
//# sourceMappingURL=ZoomableImageGallery.js.map
|