@jekrch/react-viewport-lightbox 0.1.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/LICENSE +21 -0
- package/README.md +321 -0
- package/dist/index.cjs +1078 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +277 -0
- package/dist/index.d.ts +277 -0
- package/dist/index.js +1059 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +344 -0
- package/package.json +89 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode, SVGProps, RefObject } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A single image in the viewer. Neutral replacement for app-specific item
|
|
6
|
+
* types (e.g. `Panel` / `Organism`).
|
|
7
|
+
*
|
|
8
|
+
* `TData` is an optional per-slide payload (caption, credit, links, anything).
|
|
9
|
+
* It travels with the item and is surfaced as `ctx.item.data` in every render
|
|
10
|
+
* slot, so details stay paired with their image without a parallel lookup.
|
|
11
|
+
*/
|
|
12
|
+
interface ViewerItem<TData = unknown> {
|
|
13
|
+
id: string;
|
|
14
|
+
/** FINAL url — the consumer resolves any base path before passing it in. */
|
|
15
|
+
src: string;
|
|
16
|
+
alt?: string;
|
|
17
|
+
/** Optional thumbnail url; falls back to `src`. */
|
|
18
|
+
thumbnail?: string;
|
|
19
|
+
/** Arbitrary per-slide payload, surfaced as `ctx.item.data` in render slots. */
|
|
20
|
+
data?: TData;
|
|
21
|
+
}
|
|
22
|
+
/** Named, themeable regions of the viewer that accept a `className` override. */
|
|
23
|
+
type ViewerSlot = "root" | "backdrop" | "topBar" | "bottomBar" | "image" | "button" | "counter" | "navButton" | "navStart" | "navEnd" | "overlay";
|
|
24
|
+
/** Overridable control icons. Each is a React node rendered inside its button. */
|
|
25
|
+
interface ViewerIcons {
|
|
26
|
+
close: ReactNode;
|
|
27
|
+
zoomIn: ReactNode;
|
|
28
|
+
zoomOut: ReactNode;
|
|
29
|
+
prev: ReactNode;
|
|
30
|
+
next: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Context object handed to every render slot. Exposes navigation, zoom, and
|
|
34
|
+
* layout-measurement state so slot content (info drawers, graphs, custom
|
|
35
|
+
* headers) can coordinate with the viewer.
|
|
36
|
+
*/
|
|
37
|
+
interface ViewerContext<TData = unknown> {
|
|
38
|
+
items: ViewerItem<TData>[];
|
|
39
|
+
index: number;
|
|
40
|
+
item: ViewerItem<TData>;
|
|
41
|
+
total: number;
|
|
42
|
+
hasPrev: boolean;
|
|
43
|
+
hasNext: boolean;
|
|
44
|
+
goPrev: () => void;
|
|
45
|
+
goNext: () => void;
|
|
46
|
+
goTo: (index: number) => void;
|
|
47
|
+
close: () => void;
|
|
48
|
+
isZoomed: boolean;
|
|
49
|
+
displayScale: number;
|
|
50
|
+
zoomIn: () => void;
|
|
51
|
+
zoomOut: () => void;
|
|
52
|
+
resetZoom: () => void;
|
|
53
|
+
isTouchDevice: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Measured bar heights so overlays (info drawers) can size themselves
|
|
56
|
+
* between the bars.
|
|
57
|
+
*/
|
|
58
|
+
topBarHeight: number;
|
|
59
|
+
bottomBarHeight: number;
|
|
60
|
+
/**
|
|
61
|
+
* Lets an overlay push the image track up/down (drawer-open behavior).
|
|
62
|
+
* Pass a CSS transform string, or `null` to reset. `animate` defaults to
|
|
63
|
+
* `true`; pass `false` to apply the shift instantly with no transition — e.g.
|
|
64
|
+
* to snap the image back to center on navigation so it slides in horizontally
|
|
65
|
+
* instead of dropping down.
|
|
66
|
+
*/
|
|
67
|
+
setContentShift: (transform: string | null, animate?: boolean) => void;
|
|
68
|
+
}
|
|
69
|
+
interface ImageViewerProps<TData = unknown> {
|
|
70
|
+
items: ViewerItem<TData>[];
|
|
71
|
+
/** Controlled index of the active item. */
|
|
72
|
+
index: number;
|
|
73
|
+
onIndexChange: (index: number) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Fired when a slide STARTS (button, key, or swipe), before the animation and
|
|
76
|
+
* the resulting `onIndexChange`. Lets overlays (e.g. an info drawer) animate
|
|
77
|
+
* out in sync with the image. `direction` is the swipe direction.
|
|
78
|
+
*/
|
|
79
|
+
onNavigate?: (direction: "prev" | "next") => void;
|
|
80
|
+
/** Called AFTER the exit animation completes. */
|
|
81
|
+
onClose: () => void;
|
|
82
|
+
/** Enable zoom/pan (wheel, pinch, double-tap). Default `true`. */
|
|
83
|
+
zoom?: boolean;
|
|
84
|
+
/** Show the `index / total` counter. Default `true`. */
|
|
85
|
+
showCounter?: boolean;
|
|
86
|
+
/** Wrap around at the ends. Default `false`. */
|
|
87
|
+
loop?: boolean;
|
|
88
|
+
/** Top-left title area. */
|
|
89
|
+
renderHeader?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
90
|
+
/** Extra top-right buttons, rendered before the close button. */
|
|
91
|
+
renderHeaderActions?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
92
|
+
/**
|
|
93
|
+
* Pinned to the LEFT edge of the nav row, vertically centered alongside the
|
|
94
|
+
* prev/counter/next group (which stays optically centered). Ideal for an
|
|
95
|
+
* info/details toggle that should not cost an extra row of vertical space.
|
|
96
|
+
*/
|
|
97
|
+
renderNavStart?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
98
|
+
/** Pinned to the RIGHT edge of the nav row; mirror of `renderNavStart`. */
|
|
99
|
+
renderNavEnd?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
100
|
+
/** Content below the nav row. */
|
|
101
|
+
renderFooter?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
102
|
+
/** Drawers/graphs layered over the image. */
|
|
103
|
+
renderOverlay?: (ctx: ViewerContext<TData>) => ReactNode;
|
|
104
|
+
classNames?: Partial<Record<ViewerSlot, string>>;
|
|
105
|
+
icons?: Partial<ViewerIcons>;
|
|
106
|
+
ariaLabel?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Batteries-included fullscreen image viewer: zoom, pan, pinch, and swipe
|
|
111
|
+
* navigation with themeable chrome and render slots. Controlled via `index` /
|
|
112
|
+
* `onIndexChange`; mount it when open and it runs its own enter/exit animation,
|
|
113
|
+
* calling `onClose` after the exit completes.
|
|
114
|
+
*/
|
|
115
|
+
declare function ImageViewer<TData = unknown>({ items, index, onIndexChange, onNavigate, onClose, zoom, showCounter, loop, renderHeader, renderHeaderActions, renderNavStart, renderNavEnd, renderFooter, renderOverlay, classNames, icons, ariaLabel, }: ImageViewerProps<TData>): react.JSX.Element | null;
|
|
116
|
+
|
|
117
|
+
interface NavButtonProps {
|
|
118
|
+
direction: "prev" | "next";
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
onClick: () => void;
|
|
121
|
+
icon: ReactNode;
|
|
122
|
+
className?: string;
|
|
123
|
+
}
|
|
124
|
+
declare function NavButton({ direction, enabled, onClick, icon, className }: NavButtonProps): react.JSX.Element;
|
|
125
|
+
|
|
126
|
+
declare function CloseIcon(props: SVGProps<SVGSVGElement>): react.JSX.Element;
|
|
127
|
+
declare function ZoomInIcon(props: SVGProps<SVGSVGElement>): react.JSX.Element;
|
|
128
|
+
declare function ZoomOutIcon(props: SVGProps<SVGSVGElement>): react.JSX.Element;
|
|
129
|
+
declare function ChevronLeftIcon(props: SVGProps<SVGSVGElement>): react.JSX.Element;
|
|
130
|
+
declare function ChevronRightIcon(props: SVGProps<SVGSVGElement>): react.JSX.Element;
|
|
131
|
+
/** The full default icon set, merged with any consumer overrides. */
|
|
132
|
+
declare const defaultIcons: ViewerIcons;
|
|
133
|
+
|
|
134
|
+
declare const MIN_SCALE = 1;
|
|
135
|
+
declare const MAX_SCALE = 5;
|
|
136
|
+
interface ImageTransform {
|
|
137
|
+
scale: number;
|
|
138
|
+
x: number;
|
|
139
|
+
y: number;
|
|
140
|
+
}
|
|
141
|
+
interface ImageZoomPanState {
|
|
142
|
+
imgRef: RefObject<HTMLImageElement | null>;
|
|
143
|
+
displayScale: number;
|
|
144
|
+
isZoomed: boolean;
|
|
145
|
+
transformRef: React.MutableRefObject<ImageTransform>;
|
|
146
|
+
/** Base (unscaled) image dimensions for clamp calculations */
|
|
147
|
+
baseDimsRef: React.MutableRefObject<{
|
|
148
|
+
width: number;
|
|
149
|
+
height: number;
|
|
150
|
+
}>;
|
|
151
|
+
resetTransform: () => void;
|
|
152
|
+
setTransform: (t: ImageTransform, animate?: boolean) => void;
|
|
153
|
+
applyTransform: (t: ImageTransform, animate?: boolean) => void;
|
|
154
|
+
clampTranslate: (x: number, y: number, scale: number) => {
|
|
155
|
+
x: number;
|
|
156
|
+
y: number;
|
|
157
|
+
};
|
|
158
|
+
measureBaseDims: () => void;
|
|
159
|
+
handleDoubleClick: (e: React.MouseEvent) => void;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Manages zoom/pan state for an image viewer.
|
|
163
|
+
*
|
|
164
|
+
* Applies scale + translate transforms to the **wrapper** element rather
|
|
165
|
+
* than the image itself. This avoids an iOS Safari compositing bug where
|
|
166
|
+
* CSS scale() on an element clips its painted output to the element's
|
|
167
|
+
* original layout bounds.
|
|
168
|
+
*
|
|
169
|
+
* When zoomed, the wrapper is positioned absolute inset-0 (full viewport),
|
|
170
|
+
* so its layout bounds already match the viewport and scaling it won't clip.
|
|
171
|
+
* The image stays at its natural constrained size, centered via flexbox.
|
|
172
|
+
*/
|
|
173
|
+
declare function useImageZoomPan(imgWrapperRef: RefObject<HTMLDivElement | null>, currentIndex: number,
|
|
174
|
+
/** When false, wheel-zoom and double-click-zoom are disabled. Default true. */
|
|
175
|
+
enabled?: boolean): ImageZoomPanState;
|
|
176
|
+
|
|
177
|
+
interface SlideNavigationState {
|
|
178
|
+
slideTrackRef: React.RefObject<HTMLDivElement | null>;
|
|
179
|
+
slideActive: boolean;
|
|
180
|
+
slideAnimating: boolean;
|
|
181
|
+
swipeOffset: number;
|
|
182
|
+
swipeOffsetRef: React.MutableRefObject<number>;
|
|
183
|
+
commitLockRef: React.MutableRefObject<boolean>;
|
|
184
|
+
applySlideOffset: (offset: number, animate?: boolean) => void;
|
|
185
|
+
commitSlide: (direction: "prev" | "next") => void;
|
|
186
|
+
snapBack: () => void;
|
|
187
|
+
resolveSlide: (gestureStartTime: number) => void;
|
|
188
|
+
setSlideActive: React.Dispatch<React.SetStateAction<boolean>>;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Manages the three-slot slide carousel: swipe offset tracking, animated
|
|
192
|
+
* commit/snap-back, and DOM resets on navigation.
|
|
193
|
+
*
|
|
194
|
+
* `items[i].src` is treated as a final, ready-to-load url — the next image is
|
|
195
|
+
* preloaded/decoded before navigation commits so the swipe lands on a painted
|
|
196
|
+
* frame.
|
|
197
|
+
*/
|
|
198
|
+
declare function useSlideNavigation(items: ViewerItem[], currentIndex: number, onNavigate: (index: number) => void, onSlideStart?: (direction: "prev" | "next") => void): SlideNavigationState;
|
|
199
|
+
|
|
200
|
+
interface GestureHandlers {
|
|
201
|
+
handlePointerDown: (e: React.PointerEvent) => void;
|
|
202
|
+
handlePointerMove: (e: React.PointerEvent) => void;
|
|
203
|
+
handlePointerUp: (e: React.PointerEvent) => void;
|
|
204
|
+
handleTouchStart: (e: React.TouchEvent) => void;
|
|
205
|
+
handleTouchMove: (e: React.TouchEvent) => void;
|
|
206
|
+
handleTouchEnd: (e: React.TouchEvent) => void;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Coordinates zoom/pan and slide gestures, routing pointer and touch events
|
|
210
|
+
* to the appropriate behavior based on current zoom state.
|
|
211
|
+
*
|
|
212
|
+
* When zoomed (scale > 1): pointer/touch drags are pans.
|
|
213
|
+
* When unzoomed (scale === 1): pointer/touch drags are slide-to-navigate.
|
|
214
|
+
* Two-finger touch is always a pinch-zoom.
|
|
215
|
+
*/
|
|
216
|
+
declare function useGestureHandler(zoomPan: ImageZoomPanState, slide: SlideNavigationState, hasPrev: boolean, hasNext: boolean,
|
|
217
|
+
/** When false, pinch-zoom and double-tap-zoom are disabled. Default true. */
|
|
218
|
+
zoomEnabled?: boolean): GestureHandlers;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Measures the height of the top and bottom bars so the image area
|
|
222
|
+
* can be constrained to fit between them.
|
|
223
|
+
*/
|
|
224
|
+
declare function useBarMeasure(topBarRef: RefObject<HTMLDivElement | null>, bottomBarRef: RefObject<HTMLDivElement | null>,
|
|
225
|
+
/** Re-measure whenever this key changes (e.g. currentIndex) */
|
|
226
|
+
measureKey: unknown): {
|
|
227
|
+
topBarH: number;
|
|
228
|
+
bottomBarH: number;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
declare function useBodyScrollLock(isLocked: boolean): void;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pure geometry/threshold helpers shared by the interaction hooks. Kept free of
|
|
235
|
+
* React and DOM globals so they can be unit-tested in isolation.
|
|
236
|
+
*/
|
|
237
|
+
interface Dims {
|
|
238
|
+
width: number;
|
|
239
|
+
height: number;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Clamp a pan translation so the scaled image edge can't move past the
|
|
243
|
+
* viewport edge. Returns `{ x: 0, y: 0 }` when not zoomed or when base
|
|
244
|
+
* dimensions are unknown.
|
|
245
|
+
*/
|
|
246
|
+
declare function clampTranslate(x: number, y: number, scale: number, baseDims: Dims, viewport: Dims): {
|
|
247
|
+
x: number;
|
|
248
|
+
y: number;
|
|
249
|
+
};
|
|
250
|
+
type SlideAction = "prev" | "next" | "snap";
|
|
251
|
+
interface ResolveSlideArgs {
|
|
252
|
+
/** Current horizontal swipe offset in px (positive = dragged right). */
|
|
253
|
+
offset: number;
|
|
254
|
+
/** Elapsed time of the gesture in ms (used for fling velocity). */
|
|
255
|
+
elapsedMs: number;
|
|
256
|
+
viewportWidth: number;
|
|
257
|
+
hasPrev: boolean;
|
|
258
|
+
hasNext: boolean;
|
|
259
|
+
/** Fraction of viewport width past which a drag commits. Default 0.25. */
|
|
260
|
+
distanceThreshold?: number;
|
|
261
|
+
/** px/ms past which a fast fling commits regardless of distance. Default 0.4. */
|
|
262
|
+
velocityThreshold?: number;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Decide whether a released swipe should navigate `prev`/`next` or `snap` back,
|
|
266
|
+
* based on distance and fling velocity.
|
|
267
|
+
*/
|
|
268
|
+
declare function resolveSlideDirection({ offset, elapsedMs, viewportWidth, hasPrev, hasNext, distanceThreshold, velocityThreshold, }: ResolveSlideArgs): SlideAction;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Traps Tab focus within `containerRef` while `active`, and restores focus to
|
|
272
|
+
* the previously-focused element on deactivate/unmount. SSR-safe (effect only
|
|
273
|
+
* runs in the browser).
|
|
274
|
+
*/
|
|
275
|
+
declare function useFocusTrap(containerRef: RefObject<HTMLElement | null>, active: boolean): void;
|
|
276
|
+
|
|
277
|
+
export { ChevronLeftIcon, ChevronRightIcon, CloseIcon, type Dims, type ImageTransform, ImageViewer, type ImageViewerProps, type ImageZoomPanState, MAX_SCALE, MIN_SCALE, NavButton, type ResolveSlideArgs, type SlideAction, type SlideNavigationState, type ViewerContext, type ViewerIcons, type ViewerItem, type ViewerSlot, ZoomInIcon, ZoomOutIcon, clampTranslate, defaultIcons, resolveSlideDirection, useBarMeasure, useBodyScrollLock, useFocusTrap, useGestureHandler, useImageZoomPan, useSlideNavigation };
|