@jekrch/react-viewport-lightbox 0.2.0 → 0.3.1
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/README.md +30 -22
- package/dist/index.cjs +88 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -1
- package/dist/index.d.ts +58 -1
- package/dist/index.js +88 -14
- package/dist/index.js.map +1 -1
- package/dist/styles.css +64 -7
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -50,6 +50,12 @@ interface ViewerContext<TData = unknown> {
|
|
|
50
50
|
index: number;
|
|
51
51
|
item: ViewerItem<TData>;
|
|
52
52
|
total: number;
|
|
53
|
+
/**
|
|
54
|
+
* True once the exit animation has started (after a close was requested,
|
|
55
|
+
* before `onClose` fires and the viewer unmounts). Lets overlay content fade
|
|
56
|
+
* out in step with the closing chrome instead of vanishing on unmount.
|
|
57
|
+
*/
|
|
58
|
+
closing: boolean;
|
|
53
59
|
hasPrev: boolean;
|
|
54
60
|
hasNext: boolean;
|
|
55
61
|
goPrev: () => void;
|
|
@@ -90,6 +96,14 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
90
96
|
onNavigate?: (direction: "prev" | "next") => void;
|
|
91
97
|
/** Called AFTER the exit animation completes. */
|
|
92
98
|
onClose: () => void;
|
|
99
|
+
/**
|
|
100
|
+
* Called when the user presses Escape, before the viewer closes. Return
|
|
101
|
+
* `true` to mark the key handled and veto the default close — e.g. to dismiss
|
|
102
|
+
* a consumer overlay (drawer/graph) first, closing the viewer only on a
|
|
103
|
+
* second press. Return `false`/`undefined` to fall through to the default
|
|
104
|
+
* close.
|
|
105
|
+
*/
|
|
106
|
+
onEscape?: () => boolean;
|
|
93
107
|
/**
|
|
94
108
|
* Enables a shared-element "zoom from thumbnail" open/close transition. Given
|
|
95
109
|
* the active index, return the on-screen rect of the source element (e.g. the
|
|
@@ -109,6 +123,20 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
109
123
|
zoomToCursor?: boolean;
|
|
110
124
|
/** Show the `index / total` counter. Default `true`. */
|
|
111
125
|
showCounter?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Show the built-in zoom in/out/reset buttons in the top bar. Independent of
|
|
128
|
+
* `zoom` (which governs the gesture behavior): set this `false` to keep
|
|
129
|
+
* zoom/pan gestures while a consumer overlay (e.g. an open graph/drawer that
|
|
130
|
+
* covers the image) temporarily owns the chrome. Default `true`.
|
|
131
|
+
*/
|
|
132
|
+
showZoomControls?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Suppress built-in arrow-key navigation (and the swipe commit) without
|
|
135
|
+
* tearing the viewer down. Useful while an overlay that has its own
|
|
136
|
+
* left/right handling is open. Does not hide the on-screen nav buttons.
|
|
137
|
+
* Default `false`.
|
|
138
|
+
*/
|
|
139
|
+
disableNavigation?: boolean;
|
|
112
140
|
/** Wrap around at the ends. Default `false`. */
|
|
113
141
|
loop?: boolean;
|
|
114
142
|
/**
|
|
@@ -129,6 +157,35 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
129
157
|
renderNavStart?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
130
158
|
/** Pinned to the RIGHT edge of the nav row; mirror of `renderNavStart`. */
|
|
131
159
|
renderNavEnd?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
160
|
+
/**
|
|
161
|
+
* Where the `renderNavStart` / `renderNavEnd` slots sit relative to the
|
|
162
|
+
* prev/counter/next group:
|
|
163
|
+
* - `"edge"` (default): pinned to the left/right edges of the nav row (max
|
|
164
|
+
* 42rem), keeping the nav group optically centered regardless of slot width.
|
|
165
|
+
* - `"inline"`: placed directly flanking the nav group as one centered
|
|
166
|
+
* cluster, so a details/info toggle hugs the arrows.
|
|
167
|
+
*/
|
|
168
|
+
navSlotPlacement?: "edge" | "inline";
|
|
169
|
+
/**
|
|
170
|
+
* Size of the prev/next nav arrows (the bottom nav controls). A number is
|
|
171
|
+
* treated as pixels; a string is used verbatim (e.g. `"1.5rem"`). Sets the
|
|
172
|
+
* `--rvl-nav-height` custom property, so it can equally be themed in CSS.
|
|
173
|
+
* Defaults to `2.375rem` (38px) to match the comic-snaps viewer.
|
|
174
|
+
*/
|
|
175
|
+
navHeight?: number | string;
|
|
176
|
+
/**
|
|
177
|
+
* Gap between the bottom nav controls and the viewport's bottom edge. A number
|
|
178
|
+
* is treated as pixels; a string is used verbatim (e.g. `"2rem"`). Sets the
|
|
179
|
+
* `--rvl-nav-inset` custom property and is floored by the device safe-area
|
|
180
|
+
* inset. Defaults to `1.3rem`.
|
|
181
|
+
*/
|
|
182
|
+
navInset?: number | string;
|
|
183
|
+
/**
|
|
184
|
+
* Counter font size. By default the counter scales with `navHeight` (≈0.29×);
|
|
185
|
+
* set this to override that ratio with a fixed size. A number is treated as
|
|
186
|
+
* pixels; a string is used verbatim. Sets `--rvl-counter-font-size`.
|
|
187
|
+
*/
|
|
188
|
+
counterFontSize?: number | string;
|
|
132
189
|
/** Content below the nav row. */
|
|
133
190
|
renderFooter?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
134
191
|
/** Drawers/graphs layered over the image. */
|
|
@@ -144,7 +201,7 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
144
201
|
* `onIndexChange`; mount it when open and it runs its own enter/exit animation,
|
|
145
202
|
* calling `onClose` after the exit completes.
|
|
146
203
|
*/
|
|
147
|
-
declare function ImageViewer<TData = unknown>({ items, index, onIndexChange, onNavigate, onClose, getOriginRect, zoom, zoomToCursor, showCounter, loop, closeOnBackdropClick, renderHeader, renderHeaderActions, renderNavStart, renderNavEnd, renderFooter, renderOverlay, classNames, icons, ariaLabel, }: ImageViewerProps<TData>): react.JSX.Element | null;
|
|
204
|
+
declare function ImageViewer<TData = unknown>({ items, index, onIndexChange, onNavigate, onClose, onEscape, getOriginRect, zoom, zoomToCursor, showCounter, showZoomControls, disableNavigation, loop, closeOnBackdropClick, renderHeader, renderHeaderActions, renderNavStart, renderNavEnd, navSlotPlacement, navHeight, navInset, counterFontSize, renderFooter, renderOverlay, classNames, icons, ariaLabel, }: ImageViewerProps<TData>): react.JSX.Element | null;
|
|
148
205
|
|
|
149
206
|
interface NavButtonProps {
|
|
150
207
|
direction: "prev" | "next";
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ interface ViewerContext<TData = unknown> {
|
|
|
50
50
|
index: number;
|
|
51
51
|
item: ViewerItem<TData>;
|
|
52
52
|
total: number;
|
|
53
|
+
/**
|
|
54
|
+
* True once the exit animation has started (after a close was requested,
|
|
55
|
+
* before `onClose` fires and the viewer unmounts). Lets overlay content fade
|
|
56
|
+
* out in step with the closing chrome instead of vanishing on unmount.
|
|
57
|
+
*/
|
|
58
|
+
closing: boolean;
|
|
53
59
|
hasPrev: boolean;
|
|
54
60
|
hasNext: boolean;
|
|
55
61
|
goPrev: () => void;
|
|
@@ -90,6 +96,14 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
90
96
|
onNavigate?: (direction: "prev" | "next") => void;
|
|
91
97
|
/** Called AFTER the exit animation completes. */
|
|
92
98
|
onClose: () => void;
|
|
99
|
+
/**
|
|
100
|
+
* Called when the user presses Escape, before the viewer closes. Return
|
|
101
|
+
* `true` to mark the key handled and veto the default close — e.g. to dismiss
|
|
102
|
+
* a consumer overlay (drawer/graph) first, closing the viewer only on a
|
|
103
|
+
* second press. Return `false`/`undefined` to fall through to the default
|
|
104
|
+
* close.
|
|
105
|
+
*/
|
|
106
|
+
onEscape?: () => boolean;
|
|
93
107
|
/**
|
|
94
108
|
* Enables a shared-element "zoom from thumbnail" open/close transition. Given
|
|
95
109
|
* the active index, return the on-screen rect of the source element (e.g. the
|
|
@@ -109,6 +123,20 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
109
123
|
zoomToCursor?: boolean;
|
|
110
124
|
/** Show the `index / total` counter. Default `true`. */
|
|
111
125
|
showCounter?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Show the built-in zoom in/out/reset buttons in the top bar. Independent of
|
|
128
|
+
* `zoom` (which governs the gesture behavior): set this `false` to keep
|
|
129
|
+
* zoom/pan gestures while a consumer overlay (e.g. an open graph/drawer that
|
|
130
|
+
* covers the image) temporarily owns the chrome. Default `true`.
|
|
131
|
+
*/
|
|
132
|
+
showZoomControls?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Suppress built-in arrow-key navigation (and the swipe commit) without
|
|
135
|
+
* tearing the viewer down. Useful while an overlay that has its own
|
|
136
|
+
* left/right handling is open. Does not hide the on-screen nav buttons.
|
|
137
|
+
* Default `false`.
|
|
138
|
+
*/
|
|
139
|
+
disableNavigation?: boolean;
|
|
112
140
|
/** Wrap around at the ends. Default `false`. */
|
|
113
141
|
loop?: boolean;
|
|
114
142
|
/**
|
|
@@ -129,6 +157,35 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
129
157
|
renderNavStart?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
130
158
|
/** Pinned to the RIGHT edge of the nav row; mirror of `renderNavStart`. */
|
|
131
159
|
renderNavEnd?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
160
|
+
/**
|
|
161
|
+
* Where the `renderNavStart` / `renderNavEnd` slots sit relative to the
|
|
162
|
+
* prev/counter/next group:
|
|
163
|
+
* - `"edge"` (default): pinned to the left/right edges of the nav row (max
|
|
164
|
+
* 42rem), keeping the nav group optically centered regardless of slot width.
|
|
165
|
+
* - `"inline"`: placed directly flanking the nav group as one centered
|
|
166
|
+
* cluster, so a details/info toggle hugs the arrows.
|
|
167
|
+
*/
|
|
168
|
+
navSlotPlacement?: "edge" | "inline";
|
|
169
|
+
/**
|
|
170
|
+
* Size of the prev/next nav arrows (the bottom nav controls). A number is
|
|
171
|
+
* treated as pixels; a string is used verbatim (e.g. `"1.5rem"`). Sets the
|
|
172
|
+
* `--rvl-nav-height` custom property, so it can equally be themed in CSS.
|
|
173
|
+
* Defaults to `2.375rem` (38px) to match the comic-snaps viewer.
|
|
174
|
+
*/
|
|
175
|
+
navHeight?: number | string;
|
|
176
|
+
/**
|
|
177
|
+
* Gap between the bottom nav controls and the viewport's bottom edge. A number
|
|
178
|
+
* is treated as pixels; a string is used verbatim (e.g. `"2rem"`). Sets the
|
|
179
|
+
* `--rvl-nav-inset` custom property and is floored by the device safe-area
|
|
180
|
+
* inset. Defaults to `1.3rem`.
|
|
181
|
+
*/
|
|
182
|
+
navInset?: number | string;
|
|
183
|
+
/**
|
|
184
|
+
* Counter font size. By default the counter scales with `navHeight` (≈0.29×);
|
|
185
|
+
* set this to override that ratio with a fixed size. A number is treated as
|
|
186
|
+
* pixels; a string is used verbatim. Sets `--rvl-counter-font-size`.
|
|
187
|
+
*/
|
|
188
|
+
counterFontSize?: number | string;
|
|
132
189
|
/** Content below the nav row. */
|
|
133
190
|
renderFooter?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
134
191
|
/** Drawers/graphs layered over the image. */
|
|
@@ -144,7 +201,7 @@ interface ImageViewerProps<TData = unknown> {
|
|
|
144
201
|
* `onIndexChange`; mount it when open and it runs its own enter/exit animation,
|
|
145
202
|
* calling `onClose` after the exit completes.
|
|
146
203
|
*/
|
|
147
|
-
declare function ImageViewer<TData = unknown>({ items, index, onIndexChange, onNavigate, onClose, getOriginRect, zoom, zoomToCursor, showCounter, loop, closeOnBackdropClick, renderHeader, renderHeaderActions, renderNavStart, renderNavEnd, renderFooter, renderOverlay, classNames, icons, ariaLabel, }: ImageViewerProps<TData>): react.JSX.Element | null;
|
|
204
|
+
declare function ImageViewer<TData = unknown>({ items, index, onIndexChange, onNavigate, onClose, onEscape, getOriginRect, zoom, zoomToCursor, showCounter, showZoomControls, disableNavigation, loop, closeOnBackdropClick, renderHeader, renderHeaderActions, renderNavStart, renderNavEnd, navSlotPlacement, navHeight, navInset, counterFontSize, renderFooter, renderOverlay, classNames, icons, ariaLabel, }: ImageViewerProps<TData>): react.JSX.Element | null;
|
|
148
205
|
|
|
149
206
|
interface NavButtonProps {
|
|
150
207
|
direction: "prev" | "next";
|
package/dist/index.js
CHANGED
|
@@ -590,16 +590,29 @@ function useBarMeasure(topBarRef, bottomBarRef, measureKey) {
|
|
|
590
590
|
var lockCount = 0;
|
|
591
591
|
var previousOverflow = "";
|
|
592
592
|
var previousPaddingRight = "";
|
|
593
|
+
var previousPosition = "";
|
|
594
|
+
var previousTop = "";
|
|
595
|
+
var previousWidth = "";
|
|
596
|
+
var lockedScrollY = 0;
|
|
593
597
|
function useBodyScrollLock(isLocked) {
|
|
594
598
|
useEffect(() => {
|
|
595
599
|
if (!isLocked) return;
|
|
596
600
|
if (typeof document === "undefined") return;
|
|
597
601
|
if (lockCount === 0) {
|
|
598
602
|
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
603
|
+
const rootGutter = window.getComputedStyle(document.documentElement).scrollbarGutter;
|
|
604
|
+
const reservesGutter = typeof rootGutter === "string" && rootGutter.includes("stable");
|
|
605
|
+
lockedScrollY = window.scrollY;
|
|
599
606
|
previousOverflow = document.body.style.overflow;
|
|
600
607
|
previousPaddingRight = document.body.style.paddingRight;
|
|
608
|
+
previousPosition = document.body.style.position;
|
|
609
|
+
previousTop = document.body.style.top;
|
|
610
|
+
previousWidth = document.body.style.width;
|
|
601
611
|
document.body.style.overflow = "hidden";
|
|
602
|
-
|
|
612
|
+
document.body.style.position = "fixed";
|
|
613
|
+
document.body.style.top = `-${lockedScrollY}px`;
|
|
614
|
+
document.body.style.width = "100%";
|
|
615
|
+
if (scrollbarWidth > 0 && !reservesGutter) {
|
|
603
616
|
const currentPaddingRight = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
|
|
604
617
|
document.body.style.paddingRight = `${currentPaddingRight + scrollbarWidth}px`;
|
|
605
618
|
}
|
|
@@ -610,6 +623,10 @@ function useBodyScrollLock(isLocked) {
|
|
|
610
623
|
if (lockCount === 0) {
|
|
611
624
|
document.body.style.overflow = previousOverflow;
|
|
612
625
|
document.body.style.paddingRight = previousPaddingRight;
|
|
626
|
+
document.body.style.position = previousPosition;
|
|
627
|
+
document.body.style.top = previousTop;
|
|
628
|
+
document.body.style.width = previousWidth;
|
|
629
|
+
window.scrollTo(0, lockedScrollY);
|
|
613
630
|
}
|
|
614
631
|
};
|
|
615
632
|
}, [isLocked]);
|
|
@@ -722,6 +739,7 @@ function NavButton({ direction, enabled, onClick, icon, className }) {
|
|
|
722
739
|
}
|
|
723
740
|
var ANIM_MS = 250;
|
|
724
741
|
var IMG_PADDING = 44;
|
|
742
|
+
var GHOST_CLICK_MS = 700;
|
|
725
743
|
var ZOOM_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
|
|
726
744
|
function prefersReducedMotion() {
|
|
727
745
|
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
@@ -747,16 +765,23 @@ function ImageViewer({
|
|
|
747
765
|
onIndexChange,
|
|
748
766
|
onNavigate,
|
|
749
767
|
onClose,
|
|
768
|
+
onEscape,
|
|
750
769
|
getOriginRect,
|
|
751
770
|
zoom = true,
|
|
752
771
|
zoomToCursor = true,
|
|
753
772
|
showCounter = true,
|
|
773
|
+
showZoomControls = true,
|
|
774
|
+
disableNavigation = false,
|
|
754
775
|
loop = false,
|
|
755
776
|
closeOnBackdropClick = false,
|
|
756
777
|
renderHeader,
|
|
757
778
|
renderHeaderActions,
|
|
758
779
|
renderNavStart,
|
|
759
780
|
renderNavEnd,
|
|
781
|
+
navSlotPlacement = "edge",
|
|
782
|
+
navHeight,
|
|
783
|
+
navInset,
|
|
784
|
+
counterFontSize,
|
|
760
785
|
renderFooter,
|
|
761
786
|
renderOverlay,
|
|
762
787
|
classNames,
|
|
@@ -768,6 +793,11 @@ function ImageViewer({
|
|
|
768
793
|
const [collapsing, setCollapsing] = useState(false);
|
|
769
794
|
const [isTouchDevice, setIsTouchDevice] = useState(false);
|
|
770
795
|
const [contentShift, setContentShiftState] = useState({ transform: null, animate: true });
|
|
796
|
+
const openedAtRef = useRef(Date.now());
|
|
797
|
+
const isGhostMouseEvent = useCallback(
|
|
798
|
+
() => Date.now() - openedAtRef.current < GHOST_CLICK_MS,
|
|
799
|
+
[]
|
|
800
|
+
);
|
|
771
801
|
const containerRef = useRef(null);
|
|
772
802
|
const imgWrapperRef = useRef(null);
|
|
773
803
|
const topBarRef = useRef(null);
|
|
@@ -801,7 +831,15 @@ function ImageViewer({
|
|
|
801
831
|
const { slideTrackRef, slideActive, slideAnimating, swipeOffset, commitSlide } = slide;
|
|
802
832
|
const gestures = useGestureHandler(zoomPan, slide, hasPrev, hasNext, zoom, zoomToCursor);
|
|
803
833
|
useEffect(() => {
|
|
804
|
-
|
|
834
|
+
if (typeof window === "undefined" || !window.matchMedia) {
|
|
835
|
+
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const mq = window.matchMedia("(hover: none) and (pointer: coarse)");
|
|
839
|
+
const update = () => setIsTouchDevice(mq.matches);
|
|
840
|
+
update();
|
|
841
|
+
mq.addEventListener("change", update);
|
|
842
|
+
return () => mq.removeEventListener("change", update);
|
|
805
843
|
}, []);
|
|
806
844
|
useEffect(() => {
|
|
807
845
|
const onResize = () => setViewportWidth(window.innerWidth);
|
|
@@ -907,6 +945,33 @@ function ImageViewer({
|
|
|
907
945
|
}
|
|
908
946
|
setTimeout(onClose, reduce ? 0 : ANIM_MS);
|
|
909
947
|
}, [onClose, getOriginRect, index, isZoomed, imgRef, imgWrapperRef]);
|
|
948
|
+
const handleBackdropTouchEnd = useCallback(
|
|
949
|
+
(e) => {
|
|
950
|
+
if (e.target !== e.currentTarget) return;
|
|
951
|
+
e.preventDefault();
|
|
952
|
+
handleClose();
|
|
953
|
+
},
|
|
954
|
+
[handleClose]
|
|
955
|
+
);
|
|
956
|
+
const handleDoubleClickGuarded = useCallback(
|
|
957
|
+
(e) => {
|
|
958
|
+
if (isGhostMouseEvent()) return;
|
|
959
|
+
handleDoubleClick(e);
|
|
960
|
+
},
|
|
961
|
+
[handleDoubleClick, isGhostMouseEvent]
|
|
962
|
+
);
|
|
963
|
+
const handleBackdropClick = useCallback(() => {
|
|
964
|
+
if (isGhostMouseEvent()) return;
|
|
965
|
+
handleClose();
|
|
966
|
+
}, [handleClose, isGhostMouseEvent]);
|
|
967
|
+
const handleStageClick = useCallback(
|
|
968
|
+
(e) => {
|
|
969
|
+
if (e.target !== e.currentTarget) return;
|
|
970
|
+
if (isGhostMouseEvent()) return;
|
|
971
|
+
handleClose();
|
|
972
|
+
},
|
|
973
|
+
[handleClose, isGhostMouseEvent]
|
|
974
|
+
);
|
|
910
975
|
const navigate = useCallback(
|
|
911
976
|
(dir) => {
|
|
912
977
|
if (dir === "prev") {
|
|
@@ -920,16 +985,17 @@ function ImageViewer({
|
|
|
920
985
|
useEffect(() => {
|
|
921
986
|
const handler = (e) => {
|
|
922
987
|
if (e.key === "Escape") {
|
|
988
|
+
if (onEscape?.()) return;
|
|
923
989
|
handleClose();
|
|
924
990
|
return;
|
|
925
991
|
}
|
|
926
|
-
if (displayScale > 1) return;
|
|
992
|
+
if (disableNavigation || displayScale > 1) return;
|
|
927
993
|
if (e.key === "ArrowLeft" && hasPrev) navigate("prev");
|
|
928
994
|
if (e.key === "ArrowRight" && hasNext) navigate("next");
|
|
929
995
|
};
|
|
930
996
|
window.addEventListener("keydown", handler);
|
|
931
997
|
return () => window.removeEventListener("keydown", handler);
|
|
932
|
-
}, [handleClose, hasPrev, hasNext, displayScale, navigate]);
|
|
998
|
+
}, [handleClose, hasPrev, hasNext, displayScale, navigate, onEscape, disableNavigation]);
|
|
933
999
|
const setContentShift = useCallback((transform, animate = true) => {
|
|
934
1000
|
setContentShiftState({ transform, animate });
|
|
935
1001
|
}, []);
|
|
@@ -938,6 +1004,7 @@ function ImageViewer({
|
|
|
938
1004
|
index,
|
|
939
1005
|
item,
|
|
940
1006
|
total: items.length,
|
|
1007
|
+
closing,
|
|
941
1008
|
hasPrev,
|
|
942
1009
|
hasNext,
|
|
943
1010
|
goPrev: () => navigate("prev"),
|
|
@@ -970,6 +1037,12 @@ function ImageViewer({
|
|
|
970
1037
|
const reservedH = bottomBarH + IMG_PADDING * 2;
|
|
971
1038
|
const imgMaxHeight = `calc(100vh - ${reservedH}px)`;
|
|
972
1039
|
const imgStyle = { maxHeight: imgMaxHeight };
|
|
1040
|
+
const dim = (v) => typeof v === "number" ? `${v}px` : v;
|
|
1041
|
+
const rootStyle = {
|
|
1042
|
+
...navHeight != null && { "--rvl-nav-height": dim(navHeight) },
|
|
1043
|
+
...navInset != null && { "--rvl-nav-inset": dim(navInset) },
|
|
1044
|
+
...counterFontSize != null && { "--rvl-counter-font-size": dim(counterFontSize) }
|
|
1045
|
+
};
|
|
973
1046
|
if (gateEntry && !fullLoaded) imgStyle.opacity = 0;
|
|
974
1047
|
const totalDigits = String(items.length).length;
|
|
975
1048
|
const counterMinWidth = `${totalDigits * 2 * 0.6 + 1.5}em`;
|
|
@@ -979,7 +1052,7 @@ function ImageViewer({
|
|
|
979
1052
|
const nextItem = nextIndex >= 0 ? items[nextIndex] : null;
|
|
980
1053
|
const showAdjacent = slideActive || slideAnimating || swipeOffset !== 0;
|
|
981
1054
|
const adjacentOpacity = Math.min(1, Math.abs(swipeOffset) / (viewportWidth * 0.8 || 1));
|
|
982
|
-
const
|
|
1055
|
+
const showZoomCtrls = zoom && !isTouchDevice && showZoomControls && !contentShift.transform;
|
|
983
1056
|
const headerActions = renderHeaderActions?.(ctx);
|
|
984
1057
|
const navStart = renderNavStart?.(ctx);
|
|
985
1058
|
const navEnd = renderNavEnd?.(ctx);
|
|
@@ -994,12 +1067,14 @@ function ImageViewer({
|
|
|
994
1067
|
"aria-modal": "true",
|
|
995
1068
|
"aria-label": ariaLabel ?? item.alt ?? "Image viewer",
|
|
996
1069
|
tabIndex: -1,
|
|
1070
|
+
style: rootStyle,
|
|
997
1071
|
children: [
|
|
998
1072
|
/* @__PURE__ */ jsx(
|
|
999
1073
|
"div",
|
|
1000
1074
|
{
|
|
1001
1075
|
className: cx("rvl-backdrop", cn("backdrop")),
|
|
1002
|
-
onClick: closeOnBackdropClick ?
|
|
1076
|
+
onClick: closeOnBackdropClick ? handleBackdropClick : void 0,
|
|
1077
|
+
onTouchEnd: closeOnBackdropClick ? handleBackdropTouchEnd : void 0,
|
|
1003
1078
|
"aria-hidden": "true"
|
|
1004
1079
|
}
|
|
1005
1080
|
),
|
|
@@ -1007,7 +1082,7 @@ function ImageViewer({
|
|
|
1007
1082
|
/* @__PURE__ */ jsx("div", { className: cx("rvl-header", cn("topBar")), children: renderHeader?.(ctx) }),
|
|
1008
1083
|
/* @__PURE__ */ jsxs("div", { className: "rvl-header-actions", children: [
|
|
1009
1084
|
headerActions,
|
|
1010
|
-
|
|
1085
|
+
showZoomCtrls && isZoomed && /* @__PURE__ */ jsxs(
|
|
1011
1086
|
"button",
|
|
1012
1087
|
{
|
|
1013
1088
|
type: "button",
|
|
@@ -1024,7 +1099,7 @@ function ImageViewer({
|
|
|
1024
1099
|
]
|
|
1025
1100
|
}
|
|
1026
1101
|
),
|
|
1027
|
-
|
|
1102
|
+
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1028
1103
|
"button",
|
|
1029
1104
|
{
|
|
1030
1105
|
type: "button",
|
|
@@ -1038,7 +1113,7 @@ function ImageViewer({
|
|
|
1038
1113
|
children: mergedIcons.zoomIn
|
|
1039
1114
|
}
|
|
1040
1115
|
),
|
|
1041
|
-
|
|
1116
|
+
showZoomCtrls && /* @__PURE__ */ jsx(
|
|
1042
1117
|
"button",
|
|
1043
1118
|
{
|
|
1044
1119
|
type: "button",
|
|
@@ -1072,9 +1147,8 @@ function ImageViewer({
|
|
|
1072
1147
|
"div",
|
|
1073
1148
|
{
|
|
1074
1149
|
className: "rvl-stage",
|
|
1075
|
-
onClick: closeOnBackdropClick ?
|
|
1076
|
-
|
|
1077
|
-
} : void 0,
|
|
1150
|
+
onClick: closeOnBackdropClick ? handleStageClick : void 0,
|
|
1151
|
+
onTouchEnd: closeOnBackdropClick ? handleBackdropTouchEnd : void 0,
|
|
1078
1152
|
style: {
|
|
1079
1153
|
transform: contentShift.transform ?? "translateY(0)",
|
|
1080
1154
|
// animate=false snaps with no transition (overrides the CSS transition)
|
|
@@ -1117,7 +1191,7 @@ function ImageViewer({
|
|
|
1117
1191
|
ref: imgWrapperRef,
|
|
1118
1192
|
className: "rvl-img-wrapper",
|
|
1119
1193
|
onClick: (e) => e.stopPropagation(),
|
|
1120
|
-
onDoubleClick:
|
|
1194
|
+
onDoubleClick: handleDoubleClickGuarded,
|
|
1121
1195
|
onPointerDown: gestures.handlePointerDown,
|
|
1122
1196
|
onPointerMove: gestures.handlePointerMove,
|
|
1123
1197
|
onPointerUp: gestures.handlePointerUp,
|
|
@@ -1164,7 +1238,7 @@ function ImageViewer({
|
|
|
1164
1238
|
}
|
|
1165
1239
|
),
|
|
1166
1240
|
/* @__PURE__ */ jsxs("div", { ref: bottomBarRef, className: cx("rvl-bar", "rvl-bottom-bar", cn("bottomBar")), children: [
|
|
1167
|
-
showNavRow && /* @__PURE__ */ jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxs("div", { className: "rvl-nav-inner", children: [
|
|
1241
|
+
showNavRow && /* @__PURE__ */ jsx("div", { className: "rvl-nav-row", children: /* @__PURE__ */ jsxs("div", { className: cx("rvl-nav-inner", navSlotPlacement === "inline" && "rvl-nav-inline"), children: [
|
|
1168
1242
|
navStart != null && /* @__PURE__ */ jsx("div", { className: cx("rvl-nav-start", cn("navStart")), children: navStart }),
|
|
1169
1243
|
hasNavGroup && /* @__PURE__ */ jsxs("div", { className: "rvl-nav-group", children: [
|
|
1170
1244
|
/* @__PURE__ */ jsx(
|