yet-another-react-lightbox-lite 1.0.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 ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Igor Danchenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,425 @@
1
+ # Yet Another React Lightbox Lite
2
+
3
+ Lightweight React lightbox component. This is a trimmed-down version of the
4
+ [yet-another-react-lightbox](https://github.com/igordanchenko/yet-another-react-lightbox)
5
+ that provides essential lightbox features and slick UX with just 2.8KB bundle
6
+ size.
7
+
8
+ ## Overview
9
+
10
+ [![NPM Version](https://img.shields.io/npm/v/yet-another-react-lightbox-lite.svg?color=blue)](https://www.npmjs.com/package/yet-another-react-lightbox)
11
+ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/yet-another-react-lightbox-lite.svg?color=blue)](https://bundlephobia.com/package/yet-another-react-lightbox)
12
+ [![License MIT](https://img.shields.io/npm/l/yet-another-react-lightbox-lite.svg?color=blue)](https://github.com/igordanchenko/yet-another-react-lightbox/blob/main/LICENSE)
13
+
14
+ - **Built for React:** works with React 18+
15
+ - **UX:** supports keyboard, mouse, touchpad and touchscreen navigation
16
+ - **Performance:** preloads a fixed number of images without compromising
17
+ performance or UX
18
+ - **Responsive:** responsive images with automatic resolution switching are
19
+ supported out of the box
20
+ - **Customization:** customize any UI element or add your own custom slides
21
+ - **No bloat:** supports only essential lightbox features
22
+ - **TypeScript:** type definitions come built-in in the package
23
+
24
+ ## Documentation
25
+
26
+ [https://github.com/igordanchenko/yet-another-react-lightbox-lite](https://github.com/igordanchenko/yet-another-react-lightbox-lite)
27
+
28
+ ## Live Demo
29
+
30
+ [https://stackblitz.com/edit/yet-another-react-lightbox-lite](https://stackblitz.com/edit/yet-another-react-lightbox-lite)
31
+
32
+ ## Changelog
33
+
34
+ [https://github.com/igordanchenko/yet-another-react-lightbox-lite/releases](https://github.com/igordanchenko/yet-another-react-lightbox-lite/releases)
35
+
36
+ ## Requirements
37
+
38
+ - React 18+
39
+ - Node 18+
40
+ - modern ESM-compatible bundler
41
+
42
+ ## Installation
43
+
44
+ ```shell
45
+ npm install yet-another-react-lightbox-lite
46
+ ```
47
+
48
+ ## Minimal Setup Example
49
+
50
+ ```tsx
51
+ import { useState } from "react";
52
+ import Lightbox from "yet-another-react-lightbox-lite";
53
+ import "yet-another-react-lightbox-lite/styles.css";
54
+
55
+ export default function App() {
56
+ const [index, setIndex] = useState<number>();
57
+
58
+ return (
59
+ <>
60
+ <button type="button" onClick={() => setIndex(0)}>
61
+ Open Lightbox
62
+ </button>
63
+
64
+ <Lightbox
65
+ slides={[
66
+ { src: "/image1.jpg" },
67
+ { src: "/image2.jpg" },
68
+ { src: "/image3.jpg" },
69
+ ]}
70
+ index={index}
71
+ setIndex={setIndex}
72
+ />
73
+ </>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ## Responsive Images
79
+
80
+ To utilize responsive images with automatic resolution switching, provide
81
+ `srcset` images in the slide `srcSet` array.
82
+
83
+ ```tsx
84
+ <Lightbox
85
+ slides={[
86
+ {
87
+ src: "/image1x3840.jpg",
88
+ srcSet: [
89
+ { src: "/image1x320.jpg", width: 320, height: 213 },
90
+ { src: "/image1x640.jpg", width: 640, height: 427 },
91
+ { src: "/image1x1200.jpg", width: 1200, height: 800 },
92
+ { src: "/image1x2048.jpg", width: 2048, height: 1365 },
93
+ { src: "/image1x3840.jpg", width: 3840, height: 2560 },
94
+ ],
95
+ },
96
+ // ...
97
+ ]}
98
+ // ...
99
+ />
100
+ ```
101
+
102
+ ## Next.js Image
103
+
104
+ If your project is based on [Next.js](https://nextjs.org/), you may want to take
105
+ advantage of the
106
+ [next/image](https://nextjs.org/docs/pages/api-reference/components/image)
107
+ component. The `next/image` component provides a more efficient way to handle
108
+ images in your Next.js project. You can replace the standard `<img>` element
109
+ with `next/image` with the following `render.slide` render function.
110
+
111
+ ```tsx
112
+ <Lightbox
113
+ // ...
114
+ render={{
115
+ slide: ({ slide, rect }) => {
116
+ const width =
117
+ slide.width && slide.height
118
+ ? Math.round(
119
+ Math.min(rect.width, (rect.height / slide.height) * slide.width),
120
+ )
121
+ : rect.width;
122
+
123
+ const height =
124
+ slide.width && slide.height
125
+ ? Math.round(
126
+ Math.min(rect.height, (rect.width / slide.width) * slide.height),
127
+ )
128
+ : rect.height;
129
+
130
+ return (
131
+ <Image
132
+ src={slide.src}
133
+ alt={slide.alt || ""}
134
+ width={width}
135
+ height={height}
136
+ loading="eager"
137
+ draggable={false}
138
+ blurDataURL={(slide as any).blurDataURL}
139
+ style={{
140
+ minWidth: 0,
141
+ minHeight: 0,
142
+ maxWidth: "100%",
143
+ maxHeight: "100%",
144
+ objectFit: "contain",
145
+ }}
146
+ />
147
+ );
148
+ },
149
+ }}
150
+ />
151
+ ```
152
+
153
+ ## API
154
+
155
+ Yet Another React Lightbox Lite comes with CSS stylesheet that needs to be
156
+ imported in your app.
157
+
158
+ ```tsx
159
+ import "yet-another-react-lightbox-lite/styles.css";
160
+ ```
161
+
162
+ The lightbox component accepts the following props.
163
+
164
+ ### slides
165
+
166
+ Type: `Slide[]`
167
+
168
+ An array of slides to display in the lightbox. This prop is required. By
169
+ default, the lightbox supports only image slides. You can add support for custom
170
+ slides through a custom render function (see example below).
171
+
172
+ Image slide props:
173
+
174
+ - `src` - image source (required)
175
+ - `alt` - image `alt` attribute
176
+
177
+ ### index
178
+
179
+ Type: `number | undefined`
180
+
181
+ Current slide index. This prop is required.
182
+
183
+ ### setIndex
184
+
185
+ Type: `(index: number | undefined) => void`
186
+
187
+ A callback to update current slide index state. This prop is required.
188
+
189
+ ### labels
190
+
191
+ Type: `{ [key: string]: string }`
192
+
193
+ Custom UI labels / translations.
194
+
195
+ ```tsx
196
+ <Lightbox
197
+ labels={{
198
+ Previous: t("Previous"),
199
+ Next: t("Next"),
200
+ Close: t("Close"),
201
+ }}
202
+ // ...
203
+ />
204
+ ```
205
+
206
+ ### render
207
+
208
+ Type: `object`
209
+
210
+ An object providing custom render functions.
211
+
212
+ ```tsx
213
+ <Lightbox
214
+ render={{
215
+ slide: ({ slide, rect, current }) => (
216
+ <CustomSlide {...{ slide, rect, current }} />
217
+ ),
218
+ slideHeader: ({ slide, rect, current }) => (
219
+ <SlideHeader {...{ slide, rect, current }} />
220
+ ),
221
+ slideFooter: ({ slide, rect, current }) => (
222
+ <SlideFooter {...{ slide, rect, current }} />
223
+ ),
224
+ controls: () => <CustomControls />,
225
+ iconPrev: () => <IconPrev />,
226
+ iconNext: () => <IconNext />,
227
+ iconClose: () => <IconClose />,
228
+ }}
229
+ // ...
230
+ />
231
+ ```
232
+
233
+ #### slide: ({ slide, rect, current }) => ReactNode
234
+
235
+ Render custom slide type, or override the default image slide implementation.
236
+
237
+ #### slideHeader: ({ slide, rect, current }) => ReactNode
238
+
239
+ Render custom elements above each slide.
240
+
241
+ #### slideFooter: ({ slide, rect, current }) => ReactNode
242
+
243
+ Render custom elements below or over each slide. By default, the content is
244
+ rendered right under the slide. Alternatively, you can use
245
+ `position: "absolute"` to position the extra elements relative to the slide.
246
+
247
+ For example, you can use the `slideFooter` render function to add slides
248
+ descriptions.
249
+
250
+ ```tsx
251
+ <Lightbox
252
+ render={{
253
+ slideFooter: ({ slide }) => (
254
+ <div style={{ marginBlockStart: 16 }}>{slide.description}</div>
255
+ ),
256
+ }}
257
+ // ...
258
+ />
259
+ ```
260
+
261
+ #### controls: () => ReactNode
262
+
263
+ Render custom controls or additional elements in the lightbox (use absolute
264
+ positioning).
265
+
266
+ For example, you can use the `render.controls` render function to implement
267
+ slides counter.
268
+
269
+ ```tsx
270
+ <Lightbox
271
+ // ...
272
+ render={{
273
+ controls: () =>
274
+ index !== undefined && (
275
+ <div style={{ position: "absolute", top: 16, left: 16 }}>
276
+ {index + 1} of {slides.length}
277
+ </div>
278
+ ),
279
+ }}
280
+ />
281
+ ```
282
+
283
+ You can also use the `render.controls` render function to add custom buttons to
284
+ the toolbar.
285
+
286
+ ```tsx
287
+ <Lightbox
288
+ // ...
289
+ render={{
290
+ controls: () => (
291
+ <button
292
+ type="button"
293
+ className="yarll__button"
294
+ style={{ position: "absolute", top: 8, right: 64 }}
295
+ onClick={() => {
296
+ // ...
297
+ }}
298
+ >
299
+ <ButtonIcon />
300
+ </button>
301
+ ),
302
+ }}
303
+ />
304
+ ```
305
+
306
+ #### iconPrev: () => ReactNode
307
+
308
+ Render custom `Previous` icon.
309
+
310
+ #### iconNext: () => ReactNode
311
+
312
+ Render custom `Next` icon.
313
+
314
+ #### iconClose: () => ReactNode
315
+
316
+ Render custom `Close` icon.
317
+
318
+ ## Custom Slide Attributes
319
+
320
+ You can add custom slide attributes with the following module augmentation.
321
+
322
+ ```tsx
323
+ declare module "yet-another-react-lightbox-lite" {
324
+ interface GenericSlide {
325
+ description?: string;
326
+ }
327
+ }
328
+ ```
329
+
330
+ ## Custom Slides
331
+
332
+ You can add custom slide types through module augmentation and render them with
333
+ the `render.slide` render function.
334
+
335
+ Here is an example demonstrating video slides implementation.
336
+
337
+ ```tsx
338
+ declare module "yet-another-react-lightbox-lite" {
339
+ interface SlideVideo extends GenericSlide {
340
+ type: "video";
341
+ src: string;
342
+ poster: string;
343
+ width: number;
344
+ height: number;
345
+ }
346
+
347
+ interface SlideTypes {
348
+ video: SlideVideo;
349
+ }
350
+ }
351
+
352
+ // ...
353
+
354
+ <Lightbox
355
+ slides={[
356
+ {
357
+ type: "video",
358
+ src: "/media/video.mp4",
359
+ poster: "/media/poster.jpg",
360
+ width: 1280,
361
+ height: 720,
362
+ },
363
+ ]}
364
+ render={{
365
+ slide: ({ slide }) =>
366
+ slide.type === "video" ? (
367
+ <video
368
+ controls
369
+ playsInline
370
+ poster={slide.poster}
371
+ width={slide.width}
372
+ height={slide.height}
373
+ style={{ maxWidth: "100%", maxHeight: "100%" }}
374
+ >
375
+ <source type="video/mp4" src={slide.src} />
376
+ </video>
377
+ ) : undefined,
378
+ }}
379
+ // ...
380
+ />;
381
+ ```
382
+
383
+ ## Code Splitting (Suspense)
384
+
385
+ ```tsx
386
+ // Lightbox.tsx
387
+ import LightboxComponent, {
388
+ LightboxProps,
389
+ } from "yet-another-react-lightbox-lite";
390
+ import "yet-another-react-lightbox-lite/styles.css";
391
+
392
+ export default function Lightbox(props: LightboxProps) {
393
+ return <LightboxComponent {...props} />;
394
+ }
395
+ ```
396
+
397
+ ```tsx
398
+ // App.tsx
399
+ import { lazy, Suspense, useState } from "react";
400
+ import slides from "./slides";
401
+
402
+ const Lightbox = lazy(() => import("./Lightbox"));
403
+
404
+ export default function App() {
405
+ const [index, setIndex] = useState<number>();
406
+
407
+ return (
408
+ <>
409
+ <button type="button" onClick={() => setIndex(0)}>
410
+ Open Lightbox
411
+ </button>
412
+
413
+ {index !== undefined && (
414
+ <Suspense>
415
+ <Lightbox slides={slides} index={index} setIndex={setIndex} />
416
+ </Suspense>
417
+ )}
418
+ </>
419
+ );
420
+ }
421
+ ```
422
+
423
+ ## License
424
+
425
+ MIT © 2024 [Igor Danchenko](https://github.com/igordanchenko)
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+
4
+ /** Lightbox props */
5
+ interface LightboxProps {
6
+ /** slides to display in the lightbox */
7
+ slides: Slide[];
8
+ /** slide index */
9
+ index: number | undefined;
10
+ /** slide index change callback */
11
+ setIndex: (index: number | undefined) => void;
12
+ /** custom UI labels / translations */
13
+ labels?: Labels;
14
+ /** custom render functions */
15
+ render?: Render;
16
+ }
17
+ /** Slide */
18
+ type Slide = SlideTypes[SlideTypeKey];
19
+ /** Supported slide types */
20
+ interface SlideTypes {
21
+ /** image slide type */
22
+ image: SlideImage;
23
+ }
24
+ /** Slide type key */
25
+ type SlideTypeKey = keyof SlideTypes;
26
+ /** Generic slide */
27
+ interface GenericSlide {
28
+ /** slide key */
29
+ key?: React.Key;
30
+ /** slide type */
31
+ type?: SlideTypeKey;
32
+ }
33
+ /** Image slide properties */
34
+ interface SlideImage extends GenericSlide {
35
+ /** image slide type */
36
+ type?: "image";
37
+ /** image URL */
38
+ src: string;
39
+ /** image width in pixels */
40
+ width?: number;
41
+ /** image height in pixels */
42
+ height?: number;
43
+ /** image 'alt' attribute */
44
+ alt?: string;
45
+ /** alternative images to be passed to the 'srcSet' */
46
+ srcSet?: ImageSource[];
47
+ }
48
+ /** Image source */
49
+ interface ImageSource {
50
+ /** image URL */
51
+ src: string;
52
+ /** image width in pixels */
53
+ width: number;
54
+ /** image height in pixels */
55
+ height: number;
56
+ }
57
+ /** Custom UI labels / translations */
58
+ interface Labels {
59
+ Previous?: string;
60
+ Next?: string;
61
+ Close?: string;
62
+ }
63
+ /** Label key */
64
+ type Label = keyof Labels;
65
+ /** Custom render functions. */
66
+ interface Render {
67
+ /** render custom slide type, or override the default image slide */
68
+ slide?: RenderFunction<RenderSlideProps>;
69
+ /** render custom elements above each slide */
70
+ slideHeader?: RenderFunction<RenderSlideProps>;
71
+ /** render custom elements below or over each slide */
72
+ slideFooter?: RenderFunction<RenderSlideProps>;
73
+ /** render custom controls or additional elements in the lightbox (use absolute positioning) */
74
+ controls?: RenderFunction;
75
+ /** render custom Prev icon */
76
+ iconPrev?: RenderFunction;
77
+ /** render custom Next icon */
78
+ iconNext?: RenderFunction;
79
+ /** render custom Close icon */
80
+ iconClose?: RenderFunction;
81
+ }
82
+ /** `render.slide` render function props */
83
+ interface RenderSlideProps {
84
+ /** slide */
85
+ slide: Slide;
86
+ /** slide */
87
+ rect: Rect;
88
+ /** if `true`, the slide is the current slide in the viewport */
89
+ current: boolean;
90
+ }
91
+ /** Rect */
92
+ type Rect = {
93
+ width: number;
94
+ height: number;
95
+ };
96
+ /** Generic callback function */
97
+ type Callback<T = void> = () => T;
98
+ /** Render function */
99
+ type RenderFunction<T = void> = [T] extends [void] ? () => React.ReactNode : (props: T) => React.ReactNode;
100
+
101
+ declare function Lightbox({ slides, index, setIndex, ...rest }: LightboxProps): react_jsx_runtime.JSX.Element | null;
102
+
103
+ export { type Callback, type GenericSlide, type ImageSource, type Label, type Labels, type LightboxProps, type Rect, type Render, type RenderFunction, type RenderSlideProps, type Slide, type SlideImage, type SlideTypeKey, type SlideTypes, Lightbox as default };
package/dist/index.js ADDED
@@ -0,0 +1,324 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { useContext, createContext, useState, useRef, useCallback, useMemo, useEffect } from 'react';
3
+ import { flushSync, createPortal } from 'react-dom';
4
+
5
+ const cssPrefix = "yarll__";
6
+ function cssClass(name) {
7
+ return `${cssPrefix}${name}`;
8
+ }
9
+ function cssVar(name) {
10
+ return `--${cssPrefix}${name}`;
11
+ }
12
+ function clsx(...classes) {
13
+ return [...classes].filter(Boolean).join(" ");
14
+ }
15
+ function transition(callback) {
16
+ if (document.startViewTransition) {
17
+ document.startViewTransition(() => {
18
+ flushSync(callback);
19
+ });
20
+ }
21
+ else {
22
+ callback();
23
+ }
24
+ }
25
+ function translateLabel(labels, label) {
26
+ return labels?.[label] ?? label;
27
+ }
28
+ function makeUseContext(context) {
29
+ return () => {
30
+ const ctx = useContext(context);
31
+ if (!ctx)
32
+ throw new Error();
33
+ return ctx;
34
+ };
35
+ }
36
+
37
+ const LightboxContext = createContext(null);
38
+ const useLightboxContext = makeUseContext(LightboxContext);
39
+ function LightboxContextProvider({ children, ...props }) {
40
+ return jsx(LightboxContext.Provider, { value: props, children: children });
41
+ }
42
+
43
+ function ImageSlide({ slide, rect }) {
44
+ const { width, height } = slide.srcSet?.[0] ?? slide;
45
+ const imageAspectRatio = width && height ? width / height : undefined;
46
+ const srcSet = slide.srcSet
47
+ ?.sort((a, b) => a.width - b.width)
48
+ .map((image) => `${image.src} ${image.width}w`)
49
+ .join(", ");
50
+ const sizes = imageAspectRatio
51
+ ? `${imageAspectRatio < rect.width / rect.height ? Math.round(imageAspectRatio * rect.height) : rect.width}px`
52
+ : undefined;
53
+ return (jsx("img", { draggable: false, className: cssClass("slide_image"), srcSet: srcSet, sizes: sizes, src: slide.src, alt: slide.alt }));
54
+ }
55
+
56
+ function Carousel() {
57
+ const { slides, index, render: { slide: renderSlide, slideHeader, slideFooter } = {} } = useLightboxContext();
58
+ const [rect, setRect] = useState();
59
+ const observer = useRef();
60
+ const handleRef = useCallback((node) => {
61
+ observer.current?.disconnect();
62
+ observer.current = undefined;
63
+ const updateRect = () => setRect(node ? { width: node.clientWidth, height: node.clientHeight } : undefined);
64
+ if (node && typeof ResizeObserver !== "undefined") {
65
+ observer.current = new ResizeObserver(updateRect);
66
+ observer.current.observe(node);
67
+ }
68
+ else {
69
+ updateRect();
70
+ }
71
+ }, []);
72
+ return (jsx("div", { ref: handleRef, className: cssClass("carousel"), children: rect &&
73
+ Array.from({ length: 5 }).map((_, i) => {
74
+ const slideIndex = index - 2 + i;
75
+ if (slideIndex < 0 || slideIndex >= slides.length)
76
+ return null;
77
+ const slide = slides[slideIndex];
78
+ const current = slideIndex === index;
79
+ const context = { slide, rect, current };
80
+ return (jsxs("div", { role: "group", "aria-roledescription": "slide", className: cssClass("slide"), hidden: !current, children: [slideHeader?.(context), renderSlide?.(context) ?? jsx(ImageSlide, { ...context }), slideFooter?.(context)] }, slide.key ?? `${slideIndex}-${slide.src}`));
81
+ }) }));
82
+ }
83
+
84
+ const ControllerContext = createContext(null);
85
+ const useController = makeUseContext(ControllerContext);
86
+ function Controller({ setIndex, children }) {
87
+ const { slides, index } = useLightboxContext();
88
+ const exitHooks = useRef([]);
89
+ const context = useMemo(() => {
90
+ const prev = () => {
91
+ if (index > 0)
92
+ transition(() => setIndex(index - 1));
93
+ };
94
+ const next = () => {
95
+ if (index < slides.length - 1)
96
+ transition(() => setIndex(index + 1));
97
+ };
98
+ const close = () => {
99
+ Promise.all(exitHooks.current.map((hook) => hook()))
100
+ .catch(() => { })
101
+ .then(() => {
102
+ exitHooks.current = [];
103
+ setIndex(-1);
104
+ });
105
+ };
106
+ const addExitHook = (hook) => {
107
+ exitHooks.current.push(hook);
108
+ return () => {
109
+ exitHooks.current.splice(exitHooks.current.indexOf(hook), 1);
110
+ };
111
+ };
112
+ return { prev, next, close, addExitHook };
113
+ }, [slides.length, index, setIndex]);
114
+ return jsx(ControllerContext.Provider, { value: context, children: children });
115
+ }
116
+
117
+ function Button({ icon: Icon, renderIcon, label, onClick, disabled, className }) {
118
+ const { labels } = useLightboxContext();
119
+ const buttonLabel = translateLabel(labels, label);
120
+ return (jsx("button", { type: "button", title: buttonLabel, "aria-label": buttonLabel, onClick: onClick, disabled: disabled, className: clsx(cssClass("button"), className), children: renderIcon?.() ?? jsx(Icon, { className: cssClass("icon") }) }));
121
+ }
122
+
123
+ function svgIcon(name, children) {
124
+ const icon = (props) => (jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", width: "24", height: "24", "aria-hidden": "true", focusable: "false", ...props, children: children }));
125
+ icon.displayName = name;
126
+ return icon;
127
+ }
128
+ function createIcon(name, glyph) {
129
+ return svgIcon(name, jsxs("g", { fill: "currentColor", children: [jsx("path", { d: "M0 0h24v24H0z", fill: "none" }), glyph] }));
130
+ }
131
+
132
+ const Close = createIcon("Close", jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }));
133
+
134
+ const Next = createIcon("Next", jsx("path", { d: "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" }));
135
+
136
+ const Previous = createIcon("Previous", jsx("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }));
137
+
138
+ function Navigation() {
139
+ const { slides, index, render: { iconPrev, iconNext, iconClose, controls } = {} } = useLightboxContext();
140
+ const { prev, next, close } = useController();
141
+ return (jsxs(Fragment, { children: [slides.length > 1 && (jsxs(Fragment, { children: [jsx(Button, { label: "Previous", icon: Previous, renderIcon: iconPrev, onClick: prev, className: cssClass("button_prev"), disabled: index <= 0 }), jsx(Button, { label: "Next", icon: Next, renderIcon: iconNext, onClick: next, className: cssClass("button_next"), disabled: index >= slides.length - 1 })] })), jsx(Button, { label: "Close", icon: Close, renderIcon: iconClose, onClick: close, className: cssClass("button_close") }), controls?.()] }));
142
+ }
143
+
144
+ function useSensors() {
145
+ const wheelEvents = useRef([]);
146
+ const wheelCooldown = useRef(null);
147
+ const wheelCooldownMomentum = useRef(null);
148
+ const activePointer = useRef(null);
149
+ const { prev, next, close } = useController();
150
+ return useMemo(() => {
151
+ const onKeyDown = (event) => {
152
+ switch (event.key) {
153
+ case "Escape":
154
+ close();
155
+ break;
156
+ case "ArrowLeft":
157
+ prev();
158
+ break;
159
+ case "ArrowRight":
160
+ next();
161
+ break;
162
+ }
163
+ };
164
+ const onPointerDown = (event) => {
165
+ if (!activePointer.current) {
166
+ event.persist();
167
+ activePointer.current = event;
168
+ }
169
+ else {
170
+ activePointer.current = null;
171
+ }
172
+ };
173
+ const onPointerUp = (event) => {
174
+ if (event.pointerId === activePointer.current?.pointerId) {
175
+ const dx = event.clientX - activePointer.current.clientX;
176
+ const deltaX = Math.abs(dx);
177
+ const deltaY = Math.abs(event.clientY - activePointer.current.clientY);
178
+ if (deltaX > 50 && deltaX > 1.2 * deltaY) {
179
+ if (dx > 0) {
180
+ prev();
181
+ }
182
+ else {
183
+ next();
184
+ }
185
+ }
186
+ else if ((deltaY > 50 && deltaY > 1.2 * deltaX) ||
187
+ (activePointer.current.target instanceof HTMLElement &&
188
+ activePointer.current.target.className.split(" ").includes(cssClass("slide")))) {
189
+ close();
190
+ }
191
+ activePointer.current = null;
192
+ }
193
+ };
194
+ const onWheel = (event) => {
195
+ if (wheelCooldown.current && wheelCooldownMomentum.current) {
196
+ if (event.deltaX * wheelCooldownMomentum.current > 0 &&
197
+ (event.timeStamp <= wheelCooldown.current + 500 ||
198
+ (event.timeStamp <= wheelCooldown.current + 1000 &&
199
+ Math.abs(event.deltaX) < 1.2 * Math.abs(wheelCooldownMomentum.current)))) {
200
+ wheelCooldownMomentum.current = event.deltaX;
201
+ return;
202
+ }
203
+ wheelCooldown.current = null;
204
+ wheelCooldownMomentum.current = null;
205
+ }
206
+ event.persist();
207
+ wheelEvents.current = wheelEvents.current.filter((e) => e.timeStamp > event.timeStamp - 3000);
208
+ wheelEvents.current.push(event);
209
+ const dx = wheelEvents.current.map((e) => e.deltaX).reduce((a, b) => a + b, 0);
210
+ const deltaX = Math.abs(dx);
211
+ const deltaY = Math.abs(wheelEvents.current.map((e) => e.deltaY).reduce((a, b) => a + b, 0));
212
+ if (deltaX > 100 && deltaX > 1.2 * deltaY) {
213
+ if (dx < 0) {
214
+ prev();
215
+ }
216
+ else {
217
+ next();
218
+ }
219
+ wheelEvents.current = [];
220
+ wheelCooldown.current = event.timeStamp;
221
+ wheelCooldownMomentum.current = event.deltaX;
222
+ }
223
+ };
224
+ return {
225
+ onKeyDown,
226
+ onPointerDown,
227
+ onPointerUp,
228
+ onPointerLeave: onPointerUp,
229
+ onPointerCancel: onPointerUp,
230
+ onWheel,
231
+ };
232
+ }, [prev, next, close]);
233
+ }
234
+
235
+ function setAttribute(element, attribute, value) {
236
+ const previousValue = element.getAttribute(attribute);
237
+ element.setAttribute(attribute, value);
238
+ return () => {
239
+ if (previousValue) {
240
+ element.setAttribute(attribute, previousValue);
241
+ }
242
+ else {
243
+ element.removeAttribute(attribute);
244
+ }
245
+ };
246
+ }
247
+ function Portal({ children }) {
248
+ const cleanup = useRef([]);
249
+ const [mounted, setMounted] = useState(false);
250
+ const [visible, setVisible] = useState(false);
251
+ const onTransitionEnd = useRef();
252
+ const restoreFocus = useRef(null);
253
+ const sensors = useSensors();
254
+ const { addExitHook } = useController();
255
+ const handleCleanup = useCallback(() => {
256
+ cleanup.current.forEach((cleaner) => cleaner());
257
+ cleanup.current = [];
258
+ }, []);
259
+ useEffect(() => addExitHook(() => new Promise((resolve) => {
260
+ onTransitionEnd.current = () => {
261
+ onTransitionEnd.current = undefined;
262
+ resolve();
263
+ };
264
+ handleCleanup();
265
+ setVisible(false);
266
+ })), [addExitHook, handleCleanup]);
267
+ useEffect(() => {
268
+ const property = cssVar("scrollbar-width");
269
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
270
+ if (scrollbarWidth > 0) {
271
+ document.documentElement.style.setProperty(property, `${scrollbarWidth}px`);
272
+ }
273
+ return () => {
274
+ if (scrollbarWidth > 0) {
275
+ document.documentElement.style.removeProperty(property);
276
+ }
277
+ };
278
+ }, []);
279
+ useEffect(() => {
280
+ setMounted(true);
281
+ return () => setMounted(false);
282
+ }, []);
283
+ const handleRef = useCallback((node) => {
284
+ if (node) {
285
+ node.focus();
286
+ const preventWheelDefaults = (event) => event.preventDefault();
287
+ node.addEventListener("wheel", preventWheelDefaults, { passive: false });
288
+ cleanup.current.push(() => {
289
+ node.removeEventListener("wheel", preventWheelDefaults);
290
+ });
291
+ const elements = node.parentNode?.children ?? [];
292
+ for (let i = 0; i < elements.length; i += 1) {
293
+ const element = elements[i];
294
+ if (!["TEMPLATE", "SCRIPT", "STYLE"].includes(element.tagName) && element !== node) {
295
+ cleanup.current.push(setAttribute(element, "inert", "true"));
296
+ cleanup.current.push(setAttribute(element, "aria-hidden", "true"));
297
+ }
298
+ }
299
+ cleanup.current.push(() => {
300
+ restoreFocus.current?.focus?.();
301
+ restoreFocus.current = null;
302
+ });
303
+ setVisible(true);
304
+ }
305
+ else {
306
+ handleCleanup();
307
+ }
308
+ }, [handleCleanup]);
309
+ return mounted
310
+ ? createPortal(jsx("div", { "aria-modal": true, role: "dialog", "aria-roledescription": "carousel", tabIndex: -1, ref: handleRef, className: clsx(cssClass("portal"), !visible && cssClass("portal_closed")), onTransitionEnd: onTransitionEnd.current, onFocus: (event) => {
311
+ if (!restoreFocus.current) {
312
+ restoreFocus.current = event.relatedTarget;
313
+ }
314
+ }, ...sensors, children: children }), document.body)
315
+ : null;
316
+ }
317
+
318
+ function Lightbox({ slides, index, setIndex, ...rest }) {
319
+ if (!Array.isArray(slides) || index === undefined || index < 0 || index >= slides.length)
320
+ return null;
321
+ return (jsx(LightboxContextProvider, { slides, index, ...rest, children: jsx(Controller, { setIndex, children: jsxs(Portal, { children: [jsx(Carousel, {}), jsx(Navigation, {})] }) }) }));
322
+ }
323
+
324
+ export { Lightbox as default };
@@ -0,0 +1 @@
1
+ body:has(>.yarll__portal){overscroll-behavior:none}body:has(>.yarll__portal):not(.yarll__no_scroll_lock){height:100%;overflow:hidden;padding-right:var(--yarll__scrollbar-width,0)}.yarll__portal{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;outline:none;overflow:hidden;overscroll-behavior:none;position:fixed;touch-action:none;-moz-user-select:none;user-select:none;-webkit-user-select:none;z-index:var(--yarll__portal_zindex,9999);-webkit-touch-callout:none;background-color:var(--yarll__backdrop_color,#000);color:var(--yarll__color,#fff);opacity:1;transition:var(--yarll__fade_transition,opacity .3s ease)}.yarll__portal_closed{opacity:0}.yarll__portal *{box-sizing:border-box}.yarll__carousel{align-self:stretch;flex:1;margin:var(--yarll__carousel_margin,16px);position:relative}.yarll__slide{align-items:center;display:flex;flex-direction:column;inset:0;justify-content:center;position:absolute}.yarll__slide[hidden]{display:none}.yarll__slide_image{display:block;flex:1;max-height:100%;max-width:100%;min-height:0;min-width:0;-o-object-fit:contain;object-fit:contain}.yarll__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarll__button_background_color,transparent);border:0;color:var(--yarll__button_color,var(--yarll__button_color,hsla(0,0%,100%,.8)));cursor:pointer;filter:var(--yarll__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));margin:0;padding:var(--yarll__button_padding,8px)}.yarll__button:focus-visible{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}@supports not selector(:focus-visible){.yarll__button:focus{box-shadow:var(--yarll__button_focus_box_shadow,0 0 0 4px #fff);color:var(--yarll__button_color_active,#fff);outline:var(--yarll__button_focus_outline,6px double #000)}}@media (hover:hover){.yarll__button:focus-visible:hover,.yarll__button:focus:hover,.yarll__button:hover{color:var(--yarll__button_color_active,#fff)}}.yarll__button_close{position:absolute;right:8px;top:8px}.yarll__button_prev{left:8px}.yarll__button_next{right:8px}.yarll__button_next,.yarll__button_prev{padding:var(--yarll__navigation_button_padding,24px 8px);position:absolute}.yarll__button:disabled{color:var(--yarll__button_color_disabled,var(--yarll__button_color_disabled,hsla(0,0%,100%,.4)));cursor:default}.yarll__icon{display:block;height:var(--yarll__icon_size,32px);width:var(--yarll__icon_size,32px)}
@@ -0,0 +1,2 @@
1
+ declare const styles: unknown;
2
+ export default styles;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "yet-another-react-lightbox-lite",
3
+ "version": "1.0.0",
4
+ "description": "Lightweight React lightbox component",
5
+ "author": "Igor Danchenko",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./styles.css": {
16
+ "types": "./dist/styles.css.d.ts",
17
+ "default": "./dist/styles.css"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "sideEffects": [
24
+ "*.css"
25
+ ],
26
+ "homepage": "https://github.com/igordanchenko/yet-another-react-lightbox-lite",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/igordanchenko/yet-another-react-lightbox-lite.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/igordanchenko/yet-another-react-lightbox-lite/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public",
39
+ "provenance": true
40
+ },
41
+ "peerDependencies": {
42
+ "@types/react": ">=18",
43
+ "@types/react-dom": ">=18",
44
+ "react": ">=18",
45
+ "react-dom": ">=18"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@types/react": {
49
+ "optional": true
50
+ },
51
+ "@types/react-dom": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "keywords": [
56
+ "react",
57
+ "lightbox",
58
+ "react lightbox",
59
+ "react lightbox lite",
60
+ "lightweight react lightbox"
61
+ ]
62
+ }