@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.
@@ -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 };