react-media-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 +115 -0
- package/dist/index.cjs +242 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +233 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +252 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sreekanth Ramachandran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# react-media-lightbox
|
|
2
|
+
|
|
3
|
+
A lightweight, dependency-free React lightbox for **images and video** in a single component. Supports YouTube, Vimeo, and self-hosted video files, with keyboard navigation, looping, a loading spinner, image fallbacks, and a clean, responsive UI.
|
|
4
|
+
|
|
5
|
+
- Zero runtime dependencies (only `react` as a peer dependency)
|
|
6
|
+
- One unified `<MediaLightbox>` for mixed image + video galleries
|
|
7
|
+
- YouTube, Vimeo, and self-hosted (`<video>`) sources
|
|
8
|
+
- Keyboard nav (←/→/Esc), click-outside-to-close, body-scroll lock
|
|
9
|
+
- ESM + CJS builds with TypeScript types
|
|
10
|
+
- Works with React 18 and 19 (incl. Next.js App Router — ships `"use client"`)
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install react-media-lightbox
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
> `react` and `react-dom` are peer dependencies (>= 18).
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Import the component **and the stylesheet** once:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { useState } from "react";
|
|
26
|
+
import { MediaLightbox, type MediaItem } from "react-media-lightbox";
|
|
27
|
+
import "react-media-lightbox/styles.css";
|
|
28
|
+
|
|
29
|
+
const items: MediaItem[] = [
|
|
30
|
+
{ type: "image", src: "https://picsum.photos/id/1015/1200/800", title: "A river" },
|
|
31
|
+
{ type: "video", src: "https://youtu.be/dQw4w9WgXcQ", title: "YouTube clip" },
|
|
32
|
+
{ type: "video", src: "https://vimeo.com/76979871", title: "Vimeo clip" },
|
|
33
|
+
{ type: "video", src: "https://example.com/clip.mp4", provider: "file", title: "Self-hosted", poster: "https://picsum.photos/id/1016/1200/800" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
export default function Gallery() {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const [index, setIndex] = useState(0);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<button onClick={() => { setIndex(0); setOpen(true); }}>Open gallery</button>
|
|
43
|
+
|
|
44
|
+
{open && (
|
|
45
|
+
<MediaLightbox
|
|
46
|
+
items={items}
|
|
47
|
+
initialIndex={index}
|
|
48
|
+
onClose={() => setOpen(false)}
|
|
49
|
+
fallbackSrc="/no_image.png"
|
|
50
|
+
/>
|
|
51
|
+
)}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The lightbox is **controlled by the parent**: render it conditionally and supply an `onClose` handler. It does not manage its own visibility.
|
|
58
|
+
|
|
59
|
+
## Props
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
| --- | --- | --- | --- |
|
|
63
|
+
| `items` | `MediaItem[]` | — | Media to display (images and videos). |
|
|
64
|
+
| `initialIndex` | `number` | `0` | Index opened first. |
|
|
65
|
+
| `onClose` | `() => void` | — | Called on Esc, close button, or overlay click. |
|
|
66
|
+
| `fallbackSrc` | `string` | — | Image shown when an image fails to load. |
|
|
67
|
+
| `showCounter` | `boolean` | `true` | Show the `N / total` counter. |
|
|
68
|
+
| `loop` | `boolean` | `true` | Wrap navigation at the ends. |
|
|
69
|
+
| `closeOnOverlayClick` | `boolean` | `true` | Close when the dimmed background is clicked. |
|
|
70
|
+
| `lockBodyScroll` | `boolean` | `true` | Lock page scroll while open. |
|
|
71
|
+
| `className` | `string` | — | Extra class on the overlay root. |
|
|
72
|
+
| `onIndexChange` | `(index: number) => void` | — | Fired when the active index changes. |
|
|
73
|
+
|
|
74
|
+
## Item shapes
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
type ImageItem = {
|
|
78
|
+
type: "image";
|
|
79
|
+
src: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
alt?: string; // falls back to title
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type VideoItem = {
|
|
85
|
+
type: "video";
|
|
86
|
+
src: string; // page/embed URL or direct file URL
|
|
87
|
+
provider?: "youtube" | "vimeo" | "file" | "auto"; // default: "auto" (detected from src)
|
|
88
|
+
title?: string;
|
|
89
|
+
poster?: string; // for "file" videos
|
|
90
|
+
autoplay?: boolean; // default true
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type MediaItem = ImageItem | VideoItem;
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
When `provider` is omitted or `"auto"`, the provider is detected from the URL:
|
|
97
|
+
- `youtube.com` / `youtu.be` / `youtube-nocookie.com` → YouTube iframe
|
|
98
|
+
- `vimeo.com` → Vimeo iframe
|
|
99
|
+
- anything else → self-hosted `<video>`
|
|
100
|
+
|
|
101
|
+
## Helpers
|
|
102
|
+
|
|
103
|
+
The URL utilities are exported if you need them directly:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import { detectProvider, getEmbedURL, getYouTubeId, getVimeoId } from "react-media-lightbox";
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Styling
|
|
110
|
+
|
|
111
|
+
All class names are prefixed with `rml-` to avoid collisions. Override them in your own CSS after importing `react-media-lightbox/styles.css`, or pass `className` for the overlay root.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/MediaLightbox.tsx
|
|
9
|
+
|
|
10
|
+
// src/video.ts
|
|
11
|
+
function detectProvider(url) {
|
|
12
|
+
if (!url) return "file";
|
|
13
|
+
if (/(?:youtube\.com|youtu\.be|youtube-nocookie\.com)/i.test(url)) {
|
|
14
|
+
return "youtube";
|
|
15
|
+
}
|
|
16
|
+
if (/vimeo\.com/i.test(url)) {
|
|
17
|
+
return "vimeo";
|
|
18
|
+
}
|
|
19
|
+
return "file";
|
|
20
|
+
}
|
|
21
|
+
function getYouTubeId(url) {
|
|
22
|
+
if (!url) return null;
|
|
23
|
+
try {
|
|
24
|
+
if (url.includes("youtube.com/embed/")) {
|
|
25
|
+
return url.split("youtube.com/embed/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
26
|
+
}
|
|
27
|
+
if (url.includes("youtube-nocookie.com/embed/")) {
|
|
28
|
+
return url.split("youtube-nocookie.com/embed/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
if (url.includes("youtube.com/v/")) {
|
|
31
|
+
return url.split("youtube.com/v/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
if (url.includes("youtu.be/")) {
|
|
34
|
+
return url.split("youtu.be/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (url.includes("youtube.com/watch")) {
|
|
37
|
+
const params = new URLSearchParams(new URL(url).search);
|
|
38
|
+
return params.get("v");
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function getVimeoId(url) {
|
|
46
|
+
if (!url) return null;
|
|
47
|
+
if (url.includes("player.vimeo.com/video/")) {
|
|
48
|
+
return url.split("player.vimeo.com/video/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
49
|
+
}
|
|
50
|
+
const match = url.match(/vimeo\.com\/(?:.*\/)?(\d+)/i);
|
|
51
|
+
return match ? match[1] : null;
|
|
52
|
+
}
|
|
53
|
+
function getEmbedURL(url, provider, autoplay) {
|
|
54
|
+
if (provider === "youtube") {
|
|
55
|
+
const id = getYouTubeId(url);
|
|
56
|
+
if (id) {
|
|
57
|
+
const params = autoplay ? "?autoplay=1" : "";
|
|
58
|
+
return `https://www.youtube-nocookie.com/embed/${id}${params}`;
|
|
59
|
+
}
|
|
60
|
+
return url.replace("youtube.com", "youtube-nocookie.com");
|
|
61
|
+
}
|
|
62
|
+
if (provider === "vimeo") {
|
|
63
|
+
const id = getVimeoId(url);
|
|
64
|
+
if (id) {
|
|
65
|
+
const params = autoplay ? "?autoplay=1" : "";
|
|
66
|
+
return `https://player.vimeo.com/video/${id}${params}`;
|
|
67
|
+
}
|
|
68
|
+
return url;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
var CloseIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.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" }) });
|
|
73
|
+
var PrevIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) });
|
|
74
|
+
var NextIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" }) });
|
|
75
|
+
function ImageSlide({
|
|
76
|
+
src,
|
|
77
|
+
alt,
|
|
78
|
+
fallbackSrc
|
|
79
|
+
}) {
|
|
80
|
+
const [isLoaded, setIsLoaded] = react.useState(false);
|
|
81
|
+
const [renderedSrc, setRenderedSrc] = react.useState(src);
|
|
82
|
+
if (renderedSrc !== src) {
|
|
83
|
+
setRenderedSrc(src);
|
|
84
|
+
setIsLoaded(false);
|
|
85
|
+
}
|
|
86
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rml-imageContainer", children: [
|
|
87
|
+
!isLoaded && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rml-spinner" }),
|
|
88
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
89
|
+
"img",
|
|
90
|
+
{
|
|
91
|
+
className: `rml-image${isLoaded ? " rml-imageLoaded" : ""}`,
|
|
92
|
+
src,
|
|
93
|
+
alt,
|
|
94
|
+
draggable: false,
|
|
95
|
+
onLoad: () => setIsLoaded(true),
|
|
96
|
+
onError: (e) => {
|
|
97
|
+
setIsLoaded(true);
|
|
98
|
+
if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {
|
|
99
|
+
e.currentTarget.src = fallbackSrc;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
] });
|
|
105
|
+
}
|
|
106
|
+
function VideoSlide({ item }) {
|
|
107
|
+
const autoplay = item.autoplay ?? true;
|
|
108
|
+
const provider = !item.provider || item.provider === "auto" ? detectProvider(item.src) : item.provider;
|
|
109
|
+
if (provider === "file") {
|
|
110
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rml-playerContainer rml-fileContainer", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
111
|
+
"video",
|
|
112
|
+
{
|
|
113
|
+
className: "rml-videoPlayer",
|
|
114
|
+
src: item.src,
|
|
115
|
+
poster: item.poster,
|
|
116
|
+
controls: true,
|
|
117
|
+
autoPlay: autoplay,
|
|
118
|
+
playsInline: true
|
|
119
|
+
}
|
|
120
|
+
) });
|
|
121
|
+
}
|
|
122
|
+
const embedUrl = getEmbedURL(item.src, provider, autoplay) ?? item.src;
|
|
123
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rml-playerContainer", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
124
|
+
"iframe",
|
|
125
|
+
{
|
|
126
|
+
className: "rml-iframePlayer",
|
|
127
|
+
src: embedUrl,
|
|
128
|
+
title: item.title || "Video player",
|
|
129
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
130
|
+
allowFullScreen: true
|
|
131
|
+
}
|
|
132
|
+
) });
|
|
133
|
+
}
|
|
134
|
+
function MediaLightbox({
|
|
135
|
+
items,
|
|
136
|
+
initialIndex = 0,
|
|
137
|
+
onClose,
|
|
138
|
+
fallbackSrc,
|
|
139
|
+
showCounter = true,
|
|
140
|
+
loop = true,
|
|
141
|
+
closeOnOverlayClick = true,
|
|
142
|
+
lockBodyScroll = true,
|
|
143
|
+
className,
|
|
144
|
+
onIndexChange
|
|
145
|
+
}) {
|
|
146
|
+
const count = items?.length ?? 0;
|
|
147
|
+
const [currentIndex, setCurrentIndex] = react.useState(initialIndex);
|
|
148
|
+
const handlePrev = react.useCallback(() => {
|
|
149
|
+
setCurrentIndex((prev) => {
|
|
150
|
+
const next = prev === 0 ? loop ? count - 1 : 0 : prev - 1;
|
|
151
|
+
onIndexChange?.(next);
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
}, [count, loop, onIndexChange]);
|
|
155
|
+
const handleNext = react.useCallback(() => {
|
|
156
|
+
setCurrentIndex((prev) => {
|
|
157
|
+
const next = prev === count - 1 ? loop ? 0 : count - 1 : prev + 1;
|
|
158
|
+
onIndexChange?.(next);
|
|
159
|
+
return next;
|
|
160
|
+
});
|
|
161
|
+
}, [count, loop, onIndexChange]);
|
|
162
|
+
react.useEffect(() => {
|
|
163
|
+
const handleKeyDown = (e) => {
|
|
164
|
+
if (e.key === "ArrowLeft") handlePrev();
|
|
165
|
+
else if (e.key === "ArrowRight") handleNext();
|
|
166
|
+
else if (e.key === "Escape") onClose();
|
|
167
|
+
};
|
|
168
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
169
|
+
let previousOverflow = "";
|
|
170
|
+
if (lockBodyScroll) {
|
|
171
|
+
previousOverflow = document.body.style.overflow;
|
|
172
|
+
document.body.style.overflow = "hidden";
|
|
173
|
+
}
|
|
174
|
+
return () => {
|
|
175
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
176
|
+
if (lockBodyScroll) {
|
|
177
|
+
document.body.style.overflow = previousOverflow;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [handlePrev, handleNext, onClose, lockBodyScroll]);
|
|
181
|
+
if (!items || count === 0) return null;
|
|
182
|
+
const safeIndex = Math.min(Math.max(currentIndex, 0), count - 1);
|
|
183
|
+
const current = items[safeIndex];
|
|
184
|
+
const handleOverlayClick = (e) => {
|
|
185
|
+
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
186
|
+
onClose();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
190
|
+
"div",
|
|
191
|
+
{
|
|
192
|
+
className: `rml-overlay${className ? ` ${className}` : ""}`,
|
|
193
|
+
role: "dialog",
|
|
194
|
+
"aria-modal": "true",
|
|
195
|
+
onClick: handleOverlayClick,
|
|
196
|
+
children: [
|
|
197
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "rml-closeBtn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {}) }),
|
|
198
|
+
count > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
className: "rml-navBtn rml-prevBtn",
|
|
202
|
+
onClick: handlePrev,
|
|
203
|
+
"aria-label": "Previous",
|
|
204
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(PrevIcon, {})
|
|
205
|
+
}
|
|
206
|
+
),
|
|
207
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "rml-content", onClick: handleOverlayClick, children: current.type === "video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoSlide, { item: current }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
208
|
+
ImageSlide,
|
|
209
|
+
{
|
|
210
|
+
src: current.src,
|
|
211
|
+
alt: current.alt || current.title || `Item ${safeIndex + 1}`,
|
|
212
|
+
fallbackSrc
|
|
213
|
+
},
|
|
214
|
+
safeIndex
|
|
215
|
+
) }),
|
|
216
|
+
count > 1 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
217
|
+
"button",
|
|
218
|
+
{
|
|
219
|
+
className: "rml-navBtn rml-nextBtn",
|
|
220
|
+
onClick: handleNext,
|
|
221
|
+
"aria-label": "Next",
|
|
222
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(NextIcon, {})
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rml-footer", children: [
|
|
226
|
+
showCounter && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rml-counter", children: `${safeIndex + 1} / ${count}` }),
|
|
227
|
+
current.title && /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "rml-title", children: current.title })
|
|
228
|
+
] })
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
var MediaLightbox_default = MediaLightbox;
|
|
234
|
+
|
|
235
|
+
exports.MediaLightbox = MediaLightbox;
|
|
236
|
+
exports.default = MediaLightbox_default;
|
|
237
|
+
exports.detectProvider = detectProvider;
|
|
238
|
+
exports.getEmbedURL = getEmbedURL;
|
|
239
|
+
exports.getVimeoId = getVimeoId;
|
|
240
|
+
exports.getYouTubeId = getYouTubeId;
|
|
241
|
+
//# sourceMappingURL=index.cjs.map
|
|
242
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/video.ts","../src/MediaLightbox.tsx"],"names":["jsx","useState","jsxs","useCallback","useEffect"],"mappings":";;;;;;;;;;AAGO,SAAS,eAAe,GAAA,EAA4B;AACzD,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,mDAAA,CAAoD,IAAA,CAAK,GAAG,CAAA,EAAG;AACjE,IAAA,OAAO,SAAA;AAAA,EACT;AACA,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,GAAG,CAAA,EAAG;AAC3B,IAAA,OAAO,OAAA;AAAA,EACT;AACA,EAAA,OAAO,MAAA;AACT;AAGO,SAAS,aAAa,GAAA,EAA4B;AACvD,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,IAAI;AACF,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,oBAAoB,CAAA,EAAG;AACtC,MAAA,OAAO,GAAA,CAAI,KAAA,CAAM,oBAAoB,CAAA,CAAE,CAAC,GAAG,KAAA,CAAM,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,IAClE;AACA,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,6BAA6B,CAAA,EAAG;AAC/C,MAAA,OAAO,GAAA,CAAI,KAAA,CAAM,6BAA6B,CAAA,CAAE,CAAC,GAAG,KAAA,CAAM,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,IAC3E;AACA,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,gBAAgB,CAAA,EAAG;AAClC,MAAA,OAAO,GAAA,CAAI,KAAA,CAAM,gBAAgB,CAAA,CAAE,CAAC,GAAG,KAAA,CAAM,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,IAC9D;AACA,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,WAAW,CAAA,EAAG;AAC7B,MAAA,OAAO,GAAA,CAAI,KAAA,CAAM,WAAW,CAAA,CAAE,CAAC,GAAG,KAAA,CAAM,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,IACzD;AACA,IAAA,IAAI,GAAA,CAAI,QAAA,CAAS,mBAAmB,CAAA,EAAG;AACrC,MAAA,MAAM,SAAS,IAAI,eAAA,CAAgB,IAAI,GAAA,CAAI,GAAG,EAAE,MAAM,CAAA;AACtD,MAAA,OAAO,MAAA,CAAO,IAAI,GAAG,CAAA;AAAA,IACvB;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA;AACT;AAGO,SAAS,WAAW,GAAA,EAA4B;AACrD,EAAA,IAAI,CAAC,KAAK,OAAO,IAAA;AACjB,EAAA,IAAI,GAAA,CAAI,QAAA,CAAS,yBAAyB,CAAA,EAAG;AAC3C,IAAA,OAAO,GAAA,CAAI,KAAA,CAAM,yBAAyB,CAAA,CAAE,CAAC,GAAG,KAAA,CAAM,OAAO,CAAA,CAAE,CAAC,CAAA,IAAK,IAAA;AAAA,EACvE;AACA,EAAA,MAAM,KAAA,GAAQ,GAAA,CAAI,KAAA,CAAM,6BAA6B,CAAA;AACrD,EAAA,OAAO,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAI,IAAA;AAC5B;AAMO,SAAS,WAAA,CACd,GAAA,EACA,QAAA,EACA,QAAA,EACe;AACf,EAAA,IAAI,aAAa,SAAA,EAAW;AAC1B,IAAA,MAAM,EAAA,GAAK,aAAa,GAAG,CAAA;AAC3B,IAAA,IAAI,EAAA,EAAI;AACN,MAAA,MAAM,MAAA,GAAS,WAAW,aAAA,GAAgB,EAAA;AAC1C,MAAA,OAAO,CAAA,uCAAA,EAA0C,EAAE,CAAA,EAAG,MAAM,CAAA,CAAA;AAAA,IAC9D;AACA,IAAA,OAAO,GAAA,CAAI,OAAA,CAAQ,aAAA,EAAe,sBAAsB,CAAA;AAAA,EAC1D;AACA,EAAA,IAAI,aAAa,OAAA,EAAS;AACxB,IAAA,MAAM,EAAA,GAAK,WAAW,GAAG,CAAA;AACzB,IAAA,IAAI,EAAA,EAAI;AACN,MAAA,MAAM,MAAA,GAAS,WAAW,aAAA,GAAgB,EAAA;AAC1C,MAAA,OAAO,CAAA,+BAAA,EAAkC,EAAE,CAAA,EAAG,MAAM,CAAA,CAAA;AAAA,IACtD;AACA,IAAA,OAAO,GAAA;AAAA,EACT;AACA,EAAA,OAAO,IAAA;AACT;ACtEA,IAAM,SAAA,GAAY,sBAChBA,cAAA,CAAC,KAAA,EAAA,EAAI,OAAA,EAAQ,WAAA,EAAY,aAAA,EAAY,MAAA,EACnC,QAAA,kBAAAA,cAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,uGAAA,EAAwG,CAAA,EAClH,CAAA;AAGF,IAAM,QAAA,GAAW,sBACfA,cAAA,CAAC,KAAA,EAAA,EAAI,OAAA,EAAQ,WAAA,EAAY,aAAA,EAAY,MAAA,EACnC,QAAA,kBAAAA,cAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,+CAAA,EAAgD,CAAA,EAC1D,CAAA;AAGF,IAAM,QAAA,GAAW,sBACfA,cAAA,CAAC,KAAA,EAAA,EAAI,OAAA,EAAQ,WAAA,EAAY,aAAA,EAAY,MAAA,EACnC,QAAA,kBAAAA,cAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,gDAAA,EAAiD,CAAA,EAC3D,CAAA;AAGF,SAAS,UAAA,CAAW;AAAA,EAClB,GAAA;AAAA,EACA,GAAA;AAAA,EACA;AACF,CAAA,EAIG;AACD,EAAA,MAAM,CAAC,QAAA,EAAU,WAAW,CAAA,GAAIC,eAAS,KAAK,CAAA;AAI9C,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAIA,eAAS,GAAG,CAAA;AAClD,EAAA,IAAI,gBAAgB,GAAA,EAAK;AACvB,IAAA,cAAA,CAAe,GAAG,CAAA;AAClB,IAAA,WAAA,CAAY,KAAK,CAAA;AAAA,EACnB;AAEA,EAAA,uBACEC,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oBAAA,EACZ,QAAA,EAAA;AAAA,IAAA,CAAC,QAAA,oBAAYF,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,aAAA,EAAc,CAAA;AAAA,oBAE3CA,cAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAW,CAAA,SAAA,EAAY,QAAA,GAAW,kBAAA,GAAqB,EAAE,CAAA,CAAA;AAAA,QACzD,GAAA;AAAA,QACA,GAAA;AAAA,QACA,SAAA,EAAW,KAAA;AAAA,QACX,MAAA,EAAQ,MAAM,WAAA,CAAY,IAAI,CAAA;AAAA,QAC9B,OAAA,EAAS,CAAC,CAAA,KAAM;AACd,UAAA,WAAA,CAAY,IAAI,CAAA;AAChB,UAAA,IAAI,WAAA,IAAe,CAAA,CAAE,aAAA,CAAc,GAAA,KAAQ,WAAA,EAAa;AACtD,YAAA,CAAA,CAAE,cAAc,GAAA,GAAM,WAAA;AAAA,UACxB;AAAA,QACF;AAAA;AAAA;AACF,GAAA,EACF,CAAA;AAEJ;AAEA,SAAS,UAAA,CAAW,EAAE,IAAA,EAAK,EAAwB;AACjD,EAAA,MAAM,QAAA,GAAW,KAAK,QAAA,IAAY,IAAA;AAClC,EAAA,MAAM,QAAA,GACJ,CAAC,IAAA,CAAK,QAAA,IAAY,IAAA,CAAK,QAAA,KAAa,MAAA,GAChC,cAAA,CAAe,IAAA,CAAK,GAAG,CAAA,GACvB,IAAA,CAAK,QAAA;AAEX,EAAA,IAAI,aAAa,MAAA,EAAQ;AACvB,IAAA,uBACEA,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uCAAA,EACb,QAAA,kBAAAA,cAAA;AAAA,MAAC,OAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,iBAAA;AAAA,QACV,KAAK,IAAA,CAAK,GAAA;AAAA,QACV,QAAQ,IAAA,CAAK,MAAA;AAAA,QACb,QAAA,EAAQ,IAAA;AAAA,QACR,QAAA,EAAU,QAAA;AAAA,QACV,WAAA,EAAW;AAAA;AAAA,KACb,EACF,CAAA;AAAA,EAEJ;AAEA,EAAA,MAAM,WAAW,WAAA,CAAY,IAAA,CAAK,KAAK,QAAA,EAAU,QAAQ,KAAK,IAAA,CAAK,GAAA;AAEnE,EAAA,uBACEA,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBAAA,EACb,QAAA,kBAAAA,cAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAU,kBAAA;AAAA,MACV,GAAA,EAAK,QAAA;AAAA,MACL,KAAA,EAAO,KAAK,KAAA,IAAS,cAAA;AAAA,MACrB,KAAA,EAAM,qGAAA;AAAA,MACN,eAAA,EAAe;AAAA;AAAA,GACjB,EACF,CAAA;AAEJ;AAEO,SAAS,aAAA,CAAc;AAAA,EAC5B,KAAA;AAAA,EACA,YAAA,GAAe,CAAA;AAAA,EACf,OAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA,GAAc,IAAA;AAAA,EACd,IAAA,GAAO,IAAA;AAAA,EACP,mBAAA,GAAsB,IAAA;AAAA,EACtB,cAAA,GAAiB,IAAA;AAAA,EACjB,SAAA;AAAA,EACA;AACF,CAAA,EAAuB;AACrB,EAAA,MAAM,KAAA,GAAQ,OAAO,MAAA,IAAU,CAAA;AAC/B,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAIC,eAAS,YAAY,CAAA;AAE7D,EAAA,MAAM,UAAA,GAAaE,kBAAY,MAAM;AACnC,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAS;AACxB,MAAA,MAAM,OAAO,IAAA,KAAS,CAAA,GAAK,OAAO,KAAA,GAAQ,CAAA,GAAI,IAAK,IAAA,GAAO,CAAA;AAC1D,MAAA,aAAA,GAAgB,IAAI,CAAA;AACpB,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,KAAA,EAAO,IAAA,EAAM,aAAa,CAAC,CAAA;AAE/B,EAAA,MAAM,UAAA,GAAaA,kBAAY,MAAM;AACnC,IAAA,eAAA,CAAgB,CAAC,IAAA,KAAS;AACxB,MAAA,MAAM,IAAA,GAAO,SAAS,KAAA,GAAQ,CAAA,GAAK,OAAO,CAAA,GAAI,KAAA,GAAQ,IAAK,IAAA,GAAO,CAAA;AAClE,MAAA,aAAA,GAAgB,IAAI,CAAA;AACpB,MAAA,OAAO,IAAA;AAAA,IACT,CAAC,CAAA;AAAA,EACH,CAAA,EAAG,CAAC,KAAA,EAAO,IAAA,EAAM,aAAa,CAAC,CAAA;AAE/B,EAAAC,eAAA,CAAU,MAAM;AACd,IAAA,MAAM,aAAA,GAAgB,CAAC,CAAA,KAAqB;AAC1C,MAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,WAAA,EAAa,UAAA,EAAW;AAAA,WAAA,IAC7B,CAAA,CAAE,GAAA,KAAQ,YAAA,EAAc,UAAA,EAAW;AAAA,WAAA,IACnC,CAAA,CAAE,GAAA,KAAQ,QAAA,EAAU,OAAA,EAAQ;AAAA,IACvC,CAAA;AAEA,IAAA,MAAA,CAAO,gBAAA,CAAiB,WAAW,aAAa,CAAA;AAChD,IAAA,IAAI,gBAAA,GAAmB,EAAA;AACvB,IAAA,IAAI,cAAA,EAAgB;AAClB,MAAA,gBAAA,GAAmB,QAAA,CAAS,KAAK,KAAA,CAAM,QAAA;AACvC,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAAA,IACjC;AAEA,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,CAAO,mBAAA,CAAoB,WAAW,aAAa,CAAA;AACnD,MAAA,IAAI,cAAA,EAAgB;AAClB,QAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,gBAAA;AAAA,MACjC;AAAA,IACF,CAAA;AAAA,EACF,GAAG,CAAC,UAAA,EAAY,UAAA,EAAY,OAAA,EAAS,cAAc,CAAC,CAAA;AAEpD,EAAA,IAAI,CAAC,KAAA,IAAS,KAAA,KAAU,CAAA,EAAG,OAAO,IAAA;AAElC,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,CAAI,IAAA,CAAK,IAAI,YAAA,EAAc,CAAC,CAAA,EAAG,KAAA,GAAQ,CAAC,CAAA;AAC/D,EAAA,MAAM,OAAA,GAAqB,MAAM,SAAS,CAAA;AAE1C,EAAA,MAAM,kBAAA,GAAqB,CAAC,CAAA,KAAwB;AAClD,IAAA,IAAI,mBAAA,IAAuB,CAAA,CAAE,MAAA,KAAW,CAAA,CAAE,aAAA,EAAe;AACvD,MAAA,OAAA,EAAQ;AAAA,IACV;AAAA,EACF,CAAA;AAEA,EAAA,uBACEF,eAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,WAAW,CAAA,WAAA,EAAc,SAAA,GAAY,CAAA,CAAA,EAAI,SAAS,KAAK,EAAE,CAAA,CAAA;AAAA,MACzD,IAAA,EAAK,QAAA;AAAA,MACL,YAAA,EAAW,MAAA;AAAA,MACX,OAAA,EAAS,kBAAA;AAAA,MAET,QAAA,EAAA;AAAA,wBAAAF,cAAA,CAAC,QAAA,EAAA,EAAO,WAAU,cAAA,EAAe,OAAA,EAAS,SAAS,YAAA,EAAW,OAAA,EAC5D,QAAA,kBAAAA,cAAA,CAAC,SAAA,EAAA,EAAU,CAAA,EACb,CAAA;AAAA,QAEC,QAAQ,CAAA,oBACPA,cAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,wBAAA;AAAA,YACV,OAAA,EAAS,UAAA;AAAA,YACT,YAAA,EAAW,UAAA;AAAA,YAEX,yCAAC,QAAA,EAAA,EAAS;AAAA;AAAA,SACZ;AAAA,wBAGFA,cAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,aAAA,EAAc,OAAA,EAAS,kBAAA,EACnC,QAAA,EAAA,OAAA,CAAQ,IAAA,KAAS,OAAA,mBAChBA,cAAA,CAAC,UAAA,EAAA,EAAW,IAAA,EAAM,SAAS,CAAA,mBAE3BA,cAAA;AAAA,UAAC,UAAA;AAAA,UAAA;AAAA,YAEC,KAAK,OAAA,CAAQ,GAAA;AAAA,YACb,KAAK,OAAA,CAAQ,GAAA,IAAO,QAAQ,KAAA,IAAS,CAAA,KAAA,EAAQ,YAAY,CAAC,CAAA,CAAA;AAAA,YAC1D;AAAA,WAAA;AAAA,UAHK;AAAA,SAIP,EAEJ,CAAA;AAAA,QAEC,QAAQ,CAAA,oBACPA,cAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,wBAAA;AAAA,YACV,OAAA,EAAS,UAAA;AAAA,YACT,YAAA,EAAW,MAAA;AAAA,YAEX,yCAAC,QAAA,EAAA,EAAS;AAAA;AAAA,SACZ;AAAA,wBAGFE,eAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,YAAA,EACZ,QAAA,EAAA;AAAA,UAAA,WAAA,oBACCF,cAAA,CAAC,UAAK,SAAA,EAAU,aAAA,EAAe,aAAG,SAAA,GAAY,CAAC,CAAA,GAAA,EAAM,KAAK,CAAA,CAAA,EAAG,CAAA;AAAA,UAE9D,QAAQ,KAAA,oBAASA,cAAA,CAAC,QAAG,SAAA,EAAU,WAAA,EAAa,kBAAQ,KAAA,EAAM;AAAA,SAAA,EAC7D;AAAA;AAAA;AAAA,GACF;AAEJ;AAEA,IAAO,qBAAA,GAAQ","file":"index.cjs","sourcesContent":["import type { VideoProvider } from \"./types\";\n\n/** Detect a video provider from its URL. */\nexport function detectProvider(url: string): VideoProvider {\n if (!url) return \"file\";\n if (/(?:youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/i.test(url)) {\n return \"youtube\";\n }\n if (/vimeo\\.com/i.test(url)) {\n return \"vimeo\";\n }\n return \"file\";\n}\n\n/** Extract the YouTube video id from any common YouTube URL shape. */\nexport function getYouTubeId(url: string): string | null {\n if (!url) return null;\n try {\n if (url.includes(\"youtube.com/embed/\")) {\n return url.split(\"youtube.com/embed/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube-nocookie.com/embed/\")) {\n return url.split(\"youtube-nocookie.com/embed/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube.com/v/\")) {\n return url.split(\"youtube.com/v/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtu.be/\")) {\n return url.split(\"youtu.be/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube.com/watch\")) {\n const params = new URLSearchParams(new URL(url).search);\n return params.get(\"v\");\n }\n } catch {\n return null;\n }\n return null;\n}\n\n/** Extract the numeric Vimeo id from any common Vimeo URL shape. */\nexport function getVimeoId(url: string): string | null {\n if (!url) return null;\n if (url.includes(\"player.vimeo.com/video/\")) {\n return url.split(\"player.vimeo.com/video/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n const match = url.match(/vimeo\\.com\\/(?:.*\\/)?(\\d+)/i);\n return match ? match[1] : null;\n}\n\n/**\n * Resolve a video URL to an embeddable `<iframe>` src.\n * Returns `null` if the URL is not an iframe-embeddable provider (e.g. a file).\n */\nexport function getEmbedURL(\n url: string,\n provider: VideoProvider,\n autoplay: boolean\n): string | null {\n if (provider === \"youtube\") {\n const id = getYouTubeId(url);\n if (id) {\n const params = autoplay ? \"?autoplay=1\" : \"\";\n return `https://www.youtube-nocookie.com/embed/${id}${params}`;\n }\n return url.replace(\"youtube.com\", \"youtube-nocookie.com\");\n }\n if (provider === \"vimeo\") {\n const id = getVimeoId(url);\n if (id) {\n const params = autoplay ? \"?autoplay=1\" : \"\";\n return `https://player.vimeo.com/video/${id}${params}`;\n }\n return url;\n }\n return null;\n}\n","\"use client\";\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport type { MediaItem, MediaLightboxProps, VideoItem } from \"./types\";\nimport { detectProvider, getEmbedURL } from \"./video\";\n\nconst CloseIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <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\" />\n </svg>\n);\n\nconst PrevIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n);\n\nconst NextIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n);\n\nfunction ImageSlide({\n src,\n alt,\n fallbackSrc,\n}: {\n src: string;\n alt: string;\n fallbackSrc?: string;\n}) {\n const [isLoaded, setIsLoaded] = useState(false);\n\n // Reset the loaded flag when the source changes (React-recommended derived\n // state via render-time comparison instead of an effect).\n const [renderedSrc, setRenderedSrc] = useState(src);\n if (renderedSrc !== src) {\n setRenderedSrc(src);\n setIsLoaded(false);\n }\n\n return (\n <div className=\"rml-imageContainer\">\n {!isLoaded && <div className=\"rml-spinner\" />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className={`rml-image${isLoaded ? \" rml-imageLoaded\" : \"\"}`}\n src={src}\n alt={alt}\n draggable={false}\n onLoad={() => setIsLoaded(true)}\n onError={(e) => {\n setIsLoaded(true);\n if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {\n e.currentTarget.src = fallbackSrc;\n }\n }}\n />\n </div>\n );\n}\n\nfunction VideoSlide({ item }: { item: VideoItem }) {\n const autoplay = item.autoplay ?? true;\n const provider =\n !item.provider || item.provider === \"auto\"\n ? detectProvider(item.src)\n : item.provider;\n\n if (provider === \"file\") {\n return (\n <div className=\"rml-playerContainer rml-fileContainer\">\n <video\n className=\"rml-videoPlayer\"\n src={item.src}\n poster={item.poster}\n controls\n autoPlay={autoplay}\n playsInline\n />\n </div>\n );\n }\n\n const embedUrl = getEmbedURL(item.src, provider, autoplay) ?? item.src;\n\n return (\n <div className=\"rml-playerContainer\">\n <iframe\n className=\"rml-iframePlayer\"\n src={embedUrl}\n title={item.title || \"Video player\"}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n );\n}\n\nexport function MediaLightbox({\n items,\n initialIndex = 0,\n onClose,\n fallbackSrc,\n showCounter = true,\n loop = true,\n closeOnOverlayClick = true,\n lockBodyScroll = true,\n className,\n onIndexChange,\n}: MediaLightboxProps) {\n const count = items?.length ?? 0;\n const [currentIndex, setCurrentIndex] = useState(initialIndex);\n\n const handlePrev = useCallback(() => {\n setCurrentIndex((prev) => {\n const next = prev === 0 ? (loop ? count - 1 : 0) : prev - 1;\n onIndexChange?.(next);\n return next;\n });\n }, [count, loop, onIndexChange]);\n\n const handleNext = useCallback(() => {\n setCurrentIndex((prev) => {\n const next = prev === count - 1 ? (loop ? 0 : count - 1) : prev + 1;\n onIndexChange?.(next);\n return next;\n });\n }, [count, loop, onIndexChange]);\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowLeft\") handlePrev();\n else if (e.key === \"ArrowRight\") handleNext();\n else if (e.key === \"Escape\") onClose();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n let previousOverflow = \"\";\n if (lockBodyScroll) {\n previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n if (lockBodyScroll) {\n document.body.style.overflow = previousOverflow;\n }\n };\n }, [handlePrev, handleNext, onClose, lockBodyScroll]);\n\n if (!items || count === 0) return null;\n\n const safeIndex = Math.min(Math.max(currentIndex, 0), count - 1);\n const current: MediaItem = items[safeIndex];\n\n const handleOverlayClick = (e: React.MouseEvent) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n onClose();\n }\n };\n\n return (\n <div\n className={`rml-overlay${className ? ` ${className}` : \"\"}`}\n role=\"dialog\"\n aria-modal=\"true\"\n onClick={handleOverlayClick}\n >\n <button className=\"rml-closeBtn\" onClick={onClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n\n {count > 1 && (\n <button\n className=\"rml-navBtn rml-prevBtn\"\n onClick={handlePrev}\n aria-label=\"Previous\"\n >\n <PrevIcon />\n </button>\n )}\n\n <div className=\"rml-content\" onClick={handleOverlayClick}>\n {current.type === \"video\" ? (\n <VideoSlide item={current} />\n ) : (\n <ImageSlide\n key={safeIndex}\n src={current.src}\n alt={current.alt || current.title || `Item ${safeIndex + 1}`}\n fallbackSrc={fallbackSrc}\n />\n )}\n </div>\n\n {count > 1 && (\n <button\n className=\"rml-navBtn rml-nextBtn\"\n onClick={handleNext}\n aria-label=\"Next\"\n >\n <NextIcon />\n </button>\n )}\n\n <div className=\"rml-footer\">\n {showCounter && (\n <span className=\"rml-counter\">{`${safeIndex + 1} / ${count}`}</span>\n )}\n {current.title && <h4 className=\"rml-title\">{current.title}</h4>}\n </div>\n </div>\n );\n}\n\nexport default MediaLightbox;\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type VideoProvider = "youtube" | "vimeo" | "file";
|
|
4
|
+
interface ImageItem {
|
|
5
|
+
type: "image";
|
|
6
|
+
/** Image source URL. */
|
|
7
|
+
src: string;
|
|
8
|
+
/** Optional caption shown in the footer; also used as the `alt` text. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Explicit alt text. Falls back to `title`. */
|
|
11
|
+
alt?: string;
|
|
12
|
+
}
|
|
13
|
+
interface VideoItem {
|
|
14
|
+
type: "video";
|
|
15
|
+
/**
|
|
16
|
+
* Video source. For `youtube`/`vimeo` this is the page or embed URL; for
|
|
17
|
+
* `file` this is a direct media URL (mp4/webm/etc.).
|
|
18
|
+
*/
|
|
19
|
+
src: string;
|
|
20
|
+
/**
|
|
21
|
+
* Video provider. When omitted (or "auto"), the provider is detected from
|
|
22
|
+
* the `src` URL.
|
|
23
|
+
*/
|
|
24
|
+
provider?: VideoProvider | "auto";
|
|
25
|
+
/** Optional caption shown in the footer. */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Poster image for self-hosted (`file`) videos. */
|
|
28
|
+
poster?: string;
|
|
29
|
+
/** Autoplay when the slide becomes active. Defaults to `true`. */
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
}
|
|
32
|
+
type MediaItem = ImageItem | VideoItem;
|
|
33
|
+
interface MediaLightboxProps {
|
|
34
|
+
/** The media items to display. */
|
|
35
|
+
items: MediaItem[];
|
|
36
|
+
/** Index of the item to open first. Defaults to `0`. */
|
|
37
|
+
initialIndex?: number;
|
|
38
|
+
/** Called when the lightbox requests to close (Esc, close button, overlay click). */
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
/** Fallback image URL used when an image fails to load. */
|
|
41
|
+
fallbackSrc?: string;
|
|
42
|
+
/** Show the "N / total" counter in the footer. Defaults to `true`. */
|
|
43
|
+
showCounter?: boolean;
|
|
44
|
+
/** Wrap around when navigating past the first/last item. Defaults to `true`. */
|
|
45
|
+
loop?: boolean;
|
|
46
|
+
/** Close the lightbox when the dimmed overlay (background) is clicked. Defaults to `true`. */
|
|
47
|
+
closeOnOverlayClick?: boolean;
|
|
48
|
+
/** Lock body scroll while the lightbox is open. Defaults to `true`. */
|
|
49
|
+
lockBodyScroll?: boolean;
|
|
50
|
+
/** Extra class name applied to the overlay root element. */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Fired whenever the active index changes. */
|
|
53
|
+
onIndexChange?: (index: number) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function MediaLightbox({ items, initialIndex, onClose, fallbackSrc, showCounter, loop, closeOnOverlayClick, lockBodyScroll, className, onIndexChange, }: MediaLightboxProps): React.JSX.Element | null;
|
|
57
|
+
|
|
58
|
+
/** Detect a video provider from its URL. */
|
|
59
|
+
declare function detectProvider(url: string): VideoProvider;
|
|
60
|
+
/** Extract the YouTube video id from any common YouTube URL shape. */
|
|
61
|
+
declare function getYouTubeId(url: string): string | null;
|
|
62
|
+
/** Extract the numeric Vimeo id from any common Vimeo URL shape. */
|
|
63
|
+
declare function getVimeoId(url: string): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a video URL to an embeddable `<iframe>` src.
|
|
66
|
+
* Returns `null` if the URL is not an iframe-embeddable provider (e.g. a file).
|
|
67
|
+
*/
|
|
68
|
+
declare function getEmbedURL(url: string, provider: VideoProvider, autoplay: boolean): string | null;
|
|
69
|
+
|
|
70
|
+
export { type ImageItem, type MediaItem, MediaLightbox, type MediaLightboxProps, type VideoItem, type VideoProvider, MediaLightbox as default, detectProvider, getEmbedURL, getVimeoId, getYouTubeId };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
type VideoProvider = "youtube" | "vimeo" | "file";
|
|
4
|
+
interface ImageItem {
|
|
5
|
+
type: "image";
|
|
6
|
+
/** Image source URL. */
|
|
7
|
+
src: string;
|
|
8
|
+
/** Optional caption shown in the footer; also used as the `alt` text. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** Explicit alt text. Falls back to `title`. */
|
|
11
|
+
alt?: string;
|
|
12
|
+
}
|
|
13
|
+
interface VideoItem {
|
|
14
|
+
type: "video";
|
|
15
|
+
/**
|
|
16
|
+
* Video source. For `youtube`/`vimeo` this is the page or embed URL; for
|
|
17
|
+
* `file` this is a direct media URL (mp4/webm/etc.).
|
|
18
|
+
*/
|
|
19
|
+
src: string;
|
|
20
|
+
/**
|
|
21
|
+
* Video provider. When omitted (or "auto"), the provider is detected from
|
|
22
|
+
* the `src` URL.
|
|
23
|
+
*/
|
|
24
|
+
provider?: VideoProvider | "auto";
|
|
25
|
+
/** Optional caption shown in the footer. */
|
|
26
|
+
title?: string;
|
|
27
|
+
/** Poster image for self-hosted (`file`) videos. */
|
|
28
|
+
poster?: string;
|
|
29
|
+
/** Autoplay when the slide becomes active. Defaults to `true`. */
|
|
30
|
+
autoplay?: boolean;
|
|
31
|
+
}
|
|
32
|
+
type MediaItem = ImageItem | VideoItem;
|
|
33
|
+
interface MediaLightboxProps {
|
|
34
|
+
/** The media items to display. */
|
|
35
|
+
items: MediaItem[];
|
|
36
|
+
/** Index of the item to open first. Defaults to `0`. */
|
|
37
|
+
initialIndex?: number;
|
|
38
|
+
/** Called when the lightbox requests to close (Esc, close button, overlay click). */
|
|
39
|
+
onClose: () => void;
|
|
40
|
+
/** Fallback image URL used when an image fails to load. */
|
|
41
|
+
fallbackSrc?: string;
|
|
42
|
+
/** Show the "N / total" counter in the footer. Defaults to `true`. */
|
|
43
|
+
showCounter?: boolean;
|
|
44
|
+
/** Wrap around when navigating past the first/last item. Defaults to `true`. */
|
|
45
|
+
loop?: boolean;
|
|
46
|
+
/** Close the lightbox when the dimmed overlay (background) is clicked. Defaults to `true`. */
|
|
47
|
+
closeOnOverlayClick?: boolean;
|
|
48
|
+
/** Lock body scroll while the lightbox is open. Defaults to `true`. */
|
|
49
|
+
lockBodyScroll?: boolean;
|
|
50
|
+
/** Extra class name applied to the overlay root element. */
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Fired whenever the active index changes. */
|
|
53
|
+
onIndexChange?: (index: number) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function MediaLightbox({ items, initialIndex, onClose, fallbackSrc, showCounter, loop, closeOnOverlayClick, lockBodyScroll, className, onIndexChange, }: MediaLightboxProps): React.JSX.Element | null;
|
|
57
|
+
|
|
58
|
+
/** Detect a video provider from its URL. */
|
|
59
|
+
declare function detectProvider(url: string): VideoProvider;
|
|
60
|
+
/** Extract the YouTube video id from any common YouTube URL shape. */
|
|
61
|
+
declare function getYouTubeId(url: string): string | null;
|
|
62
|
+
/** Extract the numeric Vimeo id from any common Vimeo URL shape. */
|
|
63
|
+
declare function getVimeoId(url: string): string | null;
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a video URL to an embeddable `<iframe>` src.
|
|
66
|
+
* Returns `null` if the URL is not an iframe-embeddable provider (e.g. a file).
|
|
67
|
+
*/
|
|
68
|
+
declare function getEmbedURL(url: string, provider: VideoProvider, autoplay: boolean): string | null;
|
|
69
|
+
|
|
70
|
+
export { type ImageItem, type MediaItem, MediaLightbox, type MediaLightboxProps, type VideoItem, type VideoProvider, MediaLightbox as default, detectProvider, getEmbedURL, getVimeoId, getYouTubeId };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
|
+
|
|
4
|
+
// src/MediaLightbox.tsx
|
|
5
|
+
|
|
6
|
+
// src/video.ts
|
|
7
|
+
function detectProvider(url) {
|
|
8
|
+
if (!url) return "file";
|
|
9
|
+
if (/(?:youtube\.com|youtu\.be|youtube-nocookie\.com)/i.test(url)) {
|
|
10
|
+
return "youtube";
|
|
11
|
+
}
|
|
12
|
+
if (/vimeo\.com/i.test(url)) {
|
|
13
|
+
return "vimeo";
|
|
14
|
+
}
|
|
15
|
+
return "file";
|
|
16
|
+
}
|
|
17
|
+
function getYouTubeId(url) {
|
|
18
|
+
if (!url) return null;
|
|
19
|
+
try {
|
|
20
|
+
if (url.includes("youtube.com/embed/")) {
|
|
21
|
+
return url.split("youtube.com/embed/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
22
|
+
}
|
|
23
|
+
if (url.includes("youtube-nocookie.com/embed/")) {
|
|
24
|
+
return url.split("youtube-nocookie.com/embed/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
25
|
+
}
|
|
26
|
+
if (url.includes("youtube.com/v/")) {
|
|
27
|
+
return url.split("youtube.com/v/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
28
|
+
}
|
|
29
|
+
if (url.includes("youtu.be/")) {
|
|
30
|
+
return url.split("youtu.be/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
31
|
+
}
|
|
32
|
+
if (url.includes("youtube.com/watch")) {
|
|
33
|
+
const params = new URLSearchParams(new URL(url).search);
|
|
34
|
+
return params.get("v");
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function getVimeoId(url) {
|
|
42
|
+
if (!url) return null;
|
|
43
|
+
if (url.includes("player.vimeo.com/video/")) {
|
|
44
|
+
return url.split("player.vimeo.com/video/")[1]?.split(/[?&/]/)[0] ?? null;
|
|
45
|
+
}
|
|
46
|
+
const match = url.match(/vimeo\.com\/(?:.*\/)?(\d+)/i);
|
|
47
|
+
return match ? match[1] : null;
|
|
48
|
+
}
|
|
49
|
+
function getEmbedURL(url, provider, autoplay) {
|
|
50
|
+
if (provider === "youtube") {
|
|
51
|
+
const id = getYouTubeId(url);
|
|
52
|
+
if (id) {
|
|
53
|
+
const params = autoplay ? "?autoplay=1" : "";
|
|
54
|
+
return `https://www.youtube-nocookie.com/embed/${id}${params}`;
|
|
55
|
+
}
|
|
56
|
+
return url.replace("youtube.com", "youtube-nocookie.com");
|
|
57
|
+
}
|
|
58
|
+
if (provider === "vimeo") {
|
|
59
|
+
const id = getVimeoId(url);
|
|
60
|
+
if (id) {
|
|
61
|
+
const params = autoplay ? "?autoplay=1" : "";
|
|
62
|
+
return `https://player.vimeo.com/video/${id}${params}`;
|
|
63
|
+
}
|
|
64
|
+
return url;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
var CloseIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ 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" }) });
|
|
69
|
+
var PrevIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" }) });
|
|
70
|
+
var NextIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" }) });
|
|
71
|
+
function ImageSlide({
|
|
72
|
+
src,
|
|
73
|
+
alt,
|
|
74
|
+
fallbackSrc
|
|
75
|
+
}) {
|
|
76
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
77
|
+
const [renderedSrc, setRenderedSrc] = useState(src);
|
|
78
|
+
if (renderedSrc !== src) {
|
|
79
|
+
setRenderedSrc(src);
|
|
80
|
+
setIsLoaded(false);
|
|
81
|
+
}
|
|
82
|
+
return /* @__PURE__ */ jsxs("div", { className: "rml-imageContainer", children: [
|
|
83
|
+
!isLoaded && /* @__PURE__ */ jsx("div", { className: "rml-spinner" }),
|
|
84
|
+
/* @__PURE__ */ jsx(
|
|
85
|
+
"img",
|
|
86
|
+
{
|
|
87
|
+
className: `rml-image${isLoaded ? " rml-imageLoaded" : ""}`,
|
|
88
|
+
src,
|
|
89
|
+
alt,
|
|
90
|
+
draggable: false,
|
|
91
|
+
onLoad: () => setIsLoaded(true),
|
|
92
|
+
onError: (e) => {
|
|
93
|
+
setIsLoaded(true);
|
|
94
|
+
if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {
|
|
95
|
+
e.currentTarget.src = fallbackSrc;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
] });
|
|
101
|
+
}
|
|
102
|
+
function VideoSlide({ item }) {
|
|
103
|
+
const autoplay = item.autoplay ?? true;
|
|
104
|
+
const provider = !item.provider || item.provider === "auto" ? detectProvider(item.src) : item.provider;
|
|
105
|
+
if (provider === "file") {
|
|
106
|
+
return /* @__PURE__ */ jsx("div", { className: "rml-playerContainer rml-fileContainer", children: /* @__PURE__ */ jsx(
|
|
107
|
+
"video",
|
|
108
|
+
{
|
|
109
|
+
className: "rml-videoPlayer",
|
|
110
|
+
src: item.src,
|
|
111
|
+
poster: item.poster,
|
|
112
|
+
controls: true,
|
|
113
|
+
autoPlay: autoplay,
|
|
114
|
+
playsInline: true
|
|
115
|
+
}
|
|
116
|
+
) });
|
|
117
|
+
}
|
|
118
|
+
const embedUrl = getEmbedURL(item.src, provider, autoplay) ?? item.src;
|
|
119
|
+
return /* @__PURE__ */ jsx("div", { className: "rml-playerContainer", children: /* @__PURE__ */ jsx(
|
|
120
|
+
"iframe",
|
|
121
|
+
{
|
|
122
|
+
className: "rml-iframePlayer",
|
|
123
|
+
src: embedUrl,
|
|
124
|
+
title: item.title || "Video player",
|
|
125
|
+
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
|
|
126
|
+
allowFullScreen: true
|
|
127
|
+
}
|
|
128
|
+
) });
|
|
129
|
+
}
|
|
130
|
+
function MediaLightbox({
|
|
131
|
+
items,
|
|
132
|
+
initialIndex = 0,
|
|
133
|
+
onClose,
|
|
134
|
+
fallbackSrc,
|
|
135
|
+
showCounter = true,
|
|
136
|
+
loop = true,
|
|
137
|
+
closeOnOverlayClick = true,
|
|
138
|
+
lockBodyScroll = true,
|
|
139
|
+
className,
|
|
140
|
+
onIndexChange
|
|
141
|
+
}) {
|
|
142
|
+
const count = items?.length ?? 0;
|
|
143
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
|
144
|
+
const handlePrev = useCallback(() => {
|
|
145
|
+
setCurrentIndex((prev) => {
|
|
146
|
+
const next = prev === 0 ? loop ? count - 1 : 0 : prev - 1;
|
|
147
|
+
onIndexChange?.(next);
|
|
148
|
+
return next;
|
|
149
|
+
});
|
|
150
|
+
}, [count, loop, onIndexChange]);
|
|
151
|
+
const handleNext = useCallback(() => {
|
|
152
|
+
setCurrentIndex((prev) => {
|
|
153
|
+
const next = prev === count - 1 ? loop ? 0 : count - 1 : prev + 1;
|
|
154
|
+
onIndexChange?.(next);
|
|
155
|
+
return next;
|
|
156
|
+
});
|
|
157
|
+
}, [count, loop, onIndexChange]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const handleKeyDown = (e) => {
|
|
160
|
+
if (e.key === "ArrowLeft") handlePrev();
|
|
161
|
+
else if (e.key === "ArrowRight") handleNext();
|
|
162
|
+
else if (e.key === "Escape") onClose();
|
|
163
|
+
};
|
|
164
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
165
|
+
let previousOverflow = "";
|
|
166
|
+
if (lockBodyScroll) {
|
|
167
|
+
previousOverflow = document.body.style.overflow;
|
|
168
|
+
document.body.style.overflow = "hidden";
|
|
169
|
+
}
|
|
170
|
+
return () => {
|
|
171
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
172
|
+
if (lockBodyScroll) {
|
|
173
|
+
document.body.style.overflow = previousOverflow;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}, [handlePrev, handleNext, onClose, lockBodyScroll]);
|
|
177
|
+
if (!items || count === 0) return null;
|
|
178
|
+
const safeIndex = Math.min(Math.max(currentIndex, 0), count - 1);
|
|
179
|
+
const current = items[safeIndex];
|
|
180
|
+
const handleOverlayClick = (e) => {
|
|
181
|
+
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
182
|
+
onClose();
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return /* @__PURE__ */ jsxs(
|
|
186
|
+
"div",
|
|
187
|
+
{
|
|
188
|
+
className: `rml-overlay${className ? ` ${className}` : ""}`,
|
|
189
|
+
role: "dialog",
|
|
190
|
+
"aria-modal": "true",
|
|
191
|
+
onClick: handleOverlayClick,
|
|
192
|
+
children: [
|
|
193
|
+
/* @__PURE__ */ jsx("button", { className: "rml-closeBtn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) }),
|
|
194
|
+
count > 1 && /* @__PURE__ */ jsx(
|
|
195
|
+
"button",
|
|
196
|
+
{
|
|
197
|
+
className: "rml-navBtn rml-prevBtn",
|
|
198
|
+
onClick: handlePrev,
|
|
199
|
+
"aria-label": "Previous",
|
|
200
|
+
children: /* @__PURE__ */ jsx(PrevIcon, {})
|
|
201
|
+
}
|
|
202
|
+
),
|
|
203
|
+
/* @__PURE__ */ jsx("div", { className: "rml-content", onClick: handleOverlayClick, children: current.type === "video" ? /* @__PURE__ */ jsx(VideoSlide, { item: current }) : /* @__PURE__ */ jsx(
|
|
204
|
+
ImageSlide,
|
|
205
|
+
{
|
|
206
|
+
src: current.src,
|
|
207
|
+
alt: current.alt || current.title || `Item ${safeIndex + 1}`,
|
|
208
|
+
fallbackSrc
|
|
209
|
+
},
|
|
210
|
+
safeIndex
|
|
211
|
+
) }),
|
|
212
|
+
count > 1 && /* @__PURE__ */ jsx(
|
|
213
|
+
"button",
|
|
214
|
+
{
|
|
215
|
+
className: "rml-navBtn rml-nextBtn",
|
|
216
|
+
onClick: handleNext,
|
|
217
|
+
"aria-label": "Next",
|
|
218
|
+
children: /* @__PURE__ */ jsx(NextIcon, {})
|
|
219
|
+
}
|
|
220
|
+
),
|
|
221
|
+
/* @__PURE__ */ jsxs("div", { className: "rml-footer", children: [
|
|
222
|
+
showCounter && /* @__PURE__ */ jsx("span", { className: "rml-counter", children: `${safeIndex + 1} / ${count}` }),
|
|
223
|
+
current.title && /* @__PURE__ */ jsx("h4", { className: "rml-title", children: current.title })
|
|
224
|
+
] })
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
var MediaLightbox_default = MediaLightbox;
|
|
230
|
+
|
|
231
|
+
export { MediaLightbox, MediaLightbox_default as default, detectProvider, getEmbedURL, getVimeoId, getYouTubeId };
|
|
232
|
+
//# sourceMappingURL=index.js.map
|
|
233
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/video.ts","../src/MediaLightbox.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { VideoProvider } from \"./types\";\n\n/** Detect a video provider from its URL. */\nexport function detectProvider(url: string): VideoProvider {\n if (!url) return \"file\";\n if (/(?:youtube\\.com|youtu\\.be|youtube-nocookie\\.com)/i.test(url)) {\n return \"youtube\";\n }\n if (/vimeo\\.com/i.test(url)) {\n return \"vimeo\";\n }\n return \"file\";\n}\n\n/** Extract the YouTube video id from any common YouTube URL shape. */\nexport function getYouTubeId(url: string): string | null {\n if (!url) return null;\n try {\n if (url.includes(\"youtube.com/embed/\")) {\n return url.split(\"youtube.com/embed/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube-nocookie.com/embed/\")) {\n return url.split(\"youtube-nocookie.com/embed/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube.com/v/\")) {\n return url.split(\"youtube.com/v/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtu.be/\")) {\n return url.split(\"youtu.be/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n if (url.includes(\"youtube.com/watch\")) {\n const params = new URLSearchParams(new URL(url).search);\n return params.get(\"v\");\n }\n } catch {\n return null;\n }\n return null;\n}\n\n/** Extract the numeric Vimeo id from any common Vimeo URL shape. */\nexport function getVimeoId(url: string): string | null {\n if (!url) return null;\n if (url.includes(\"player.vimeo.com/video/\")) {\n return url.split(\"player.vimeo.com/video/\")[1]?.split(/[?&/]/)[0] ?? null;\n }\n const match = url.match(/vimeo\\.com\\/(?:.*\\/)?(\\d+)/i);\n return match ? match[1] : null;\n}\n\n/**\n * Resolve a video URL to an embeddable `<iframe>` src.\n * Returns `null` if the URL is not an iframe-embeddable provider (e.g. a file).\n */\nexport function getEmbedURL(\n url: string,\n provider: VideoProvider,\n autoplay: boolean\n): string | null {\n if (provider === \"youtube\") {\n const id = getYouTubeId(url);\n if (id) {\n const params = autoplay ? \"?autoplay=1\" : \"\";\n return `https://www.youtube-nocookie.com/embed/${id}${params}`;\n }\n return url.replace(\"youtube.com\", \"youtube-nocookie.com\");\n }\n if (provider === \"vimeo\") {\n const id = getVimeoId(url);\n if (id) {\n const params = autoplay ? \"?autoplay=1\" : \"\";\n return `https://player.vimeo.com/video/${id}${params}`;\n }\n return url;\n }\n return null;\n}\n","\"use client\";\n\nimport React, { useCallback, useEffect, useState } from \"react\";\nimport type { MediaItem, MediaLightboxProps, VideoItem } from \"./types\";\nimport { detectProvider, getEmbedURL } from \"./video\";\n\nconst CloseIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <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\" />\n </svg>\n);\n\nconst PrevIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z\" />\n </svg>\n);\n\nconst NextIcon = () => (\n <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\n <path d=\"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z\" />\n </svg>\n);\n\nfunction ImageSlide({\n src,\n alt,\n fallbackSrc,\n}: {\n src: string;\n alt: string;\n fallbackSrc?: string;\n}) {\n const [isLoaded, setIsLoaded] = useState(false);\n\n // Reset the loaded flag when the source changes (React-recommended derived\n // state via render-time comparison instead of an effect).\n const [renderedSrc, setRenderedSrc] = useState(src);\n if (renderedSrc !== src) {\n setRenderedSrc(src);\n setIsLoaded(false);\n }\n\n return (\n <div className=\"rml-imageContainer\">\n {!isLoaded && <div className=\"rml-spinner\" />}\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n className={`rml-image${isLoaded ? \" rml-imageLoaded\" : \"\"}`}\n src={src}\n alt={alt}\n draggable={false}\n onLoad={() => setIsLoaded(true)}\n onError={(e) => {\n setIsLoaded(true);\n if (fallbackSrc && e.currentTarget.src !== fallbackSrc) {\n e.currentTarget.src = fallbackSrc;\n }\n }}\n />\n </div>\n );\n}\n\nfunction VideoSlide({ item }: { item: VideoItem }) {\n const autoplay = item.autoplay ?? true;\n const provider =\n !item.provider || item.provider === \"auto\"\n ? detectProvider(item.src)\n : item.provider;\n\n if (provider === \"file\") {\n return (\n <div className=\"rml-playerContainer rml-fileContainer\">\n <video\n className=\"rml-videoPlayer\"\n src={item.src}\n poster={item.poster}\n controls\n autoPlay={autoplay}\n playsInline\n />\n </div>\n );\n }\n\n const embedUrl = getEmbedURL(item.src, provider, autoplay) ?? item.src;\n\n return (\n <div className=\"rml-playerContainer\">\n <iframe\n className=\"rml-iframePlayer\"\n src={embedUrl}\n title={item.title || \"Video player\"}\n allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n allowFullScreen\n />\n </div>\n );\n}\n\nexport function MediaLightbox({\n items,\n initialIndex = 0,\n onClose,\n fallbackSrc,\n showCounter = true,\n loop = true,\n closeOnOverlayClick = true,\n lockBodyScroll = true,\n className,\n onIndexChange,\n}: MediaLightboxProps) {\n const count = items?.length ?? 0;\n const [currentIndex, setCurrentIndex] = useState(initialIndex);\n\n const handlePrev = useCallback(() => {\n setCurrentIndex((prev) => {\n const next = prev === 0 ? (loop ? count - 1 : 0) : prev - 1;\n onIndexChange?.(next);\n return next;\n });\n }, [count, loop, onIndexChange]);\n\n const handleNext = useCallback(() => {\n setCurrentIndex((prev) => {\n const next = prev === count - 1 ? (loop ? 0 : count - 1) : prev + 1;\n onIndexChange?.(next);\n return next;\n });\n }, [count, loop, onIndexChange]);\n\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"ArrowLeft\") handlePrev();\n else if (e.key === \"ArrowRight\") handleNext();\n else if (e.key === \"Escape\") onClose();\n };\n\n window.addEventListener(\"keydown\", handleKeyDown);\n let previousOverflow = \"\";\n if (lockBodyScroll) {\n previousOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n return () => {\n window.removeEventListener(\"keydown\", handleKeyDown);\n if (lockBodyScroll) {\n document.body.style.overflow = previousOverflow;\n }\n };\n }, [handlePrev, handleNext, onClose, lockBodyScroll]);\n\n if (!items || count === 0) return null;\n\n const safeIndex = Math.min(Math.max(currentIndex, 0), count - 1);\n const current: MediaItem = items[safeIndex];\n\n const handleOverlayClick = (e: React.MouseEvent) => {\n if (closeOnOverlayClick && e.target === e.currentTarget) {\n onClose();\n }\n };\n\n return (\n <div\n className={`rml-overlay${className ? ` ${className}` : \"\"}`}\n role=\"dialog\"\n aria-modal=\"true\"\n onClick={handleOverlayClick}\n >\n <button className=\"rml-closeBtn\" onClick={onClose} aria-label=\"Close\">\n <CloseIcon />\n </button>\n\n {count > 1 && (\n <button\n className=\"rml-navBtn rml-prevBtn\"\n onClick={handlePrev}\n aria-label=\"Previous\"\n >\n <PrevIcon />\n </button>\n )}\n\n <div className=\"rml-content\" onClick={handleOverlayClick}>\n {current.type === \"video\" ? (\n <VideoSlide item={current} />\n ) : (\n <ImageSlide\n key={safeIndex}\n src={current.src}\n alt={current.alt || current.title || `Item ${safeIndex + 1}`}\n fallbackSrc={fallbackSrc}\n />\n )}\n </div>\n\n {count > 1 && (\n <button\n className=\"rml-navBtn rml-nextBtn\"\n onClick={handleNext}\n aria-label=\"Next\"\n >\n <NextIcon />\n </button>\n )}\n\n <div className=\"rml-footer\">\n {showCounter && (\n <span className=\"rml-counter\">{`${safeIndex + 1} / ${count}`}</span>\n )}\n {current.title && <h4 className=\"rml-title\">{current.title}</h4>}\n </div>\n </div>\n );\n}\n\nexport default MediaLightbox;\n"]}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
.rml-overlay {
|
|
2
|
+
position: fixed;
|
|
3
|
+
inset: 0;
|
|
4
|
+
background-color: rgba(10, 10, 10, 0.9);
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
z-index: 99999;
|
|
9
|
+
backdrop-filter: blur(12px);
|
|
10
|
+
-webkit-backdrop-filter: blur(12px);
|
|
11
|
+
animation: rml-fadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
|
12
|
+
user-select: none;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.rml-content {
|
|
16
|
+
position: relative;
|
|
17
|
+
width: 90vw;
|
|
18
|
+
height: 80vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
outline: none;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* ---------- Image slide ---------- */
|
|
26
|
+
.rml-imageContainer {
|
|
27
|
+
position: relative;
|
|
28
|
+
max-width: 100%;
|
|
29
|
+
max-height: 100%;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.rml-image {
|
|
36
|
+
max-width: 100%;
|
|
37
|
+
max-height: 75vh;
|
|
38
|
+
object-fit: contain;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6);
|
|
41
|
+
transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
42
|
+
opacity: 0;
|
|
43
|
+
transform: scale(0.97);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.rml-imageLoaded {
|
|
47
|
+
opacity: 1;
|
|
48
|
+
transform: scale(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ---------- Video slide ---------- */
|
|
52
|
+
.rml-playerContainer {
|
|
53
|
+
position: relative;
|
|
54
|
+
width: 90vw;
|
|
55
|
+
max-width: 1000px;
|
|
56
|
+
aspect-ratio: 16 / 9;
|
|
57
|
+
background-color: #000;
|
|
58
|
+
border-radius: 12px;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
61
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.rml-iframePlayer,
|
|
65
|
+
.rml-videoPlayer {
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
border: 0;
|
|
69
|
+
display: block;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.rml-videoPlayer {
|
|
73
|
+
object-fit: contain;
|
|
74
|
+
background-color: #000;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* ---------- Controls ---------- */
|
|
78
|
+
.rml-closeBtn {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: 30px;
|
|
81
|
+
right: 30px;
|
|
82
|
+
background: rgba(255, 255, 255, 0.08);
|
|
83
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
84
|
+
color: #fff;
|
|
85
|
+
width: 46px;
|
|
86
|
+
height: 46px;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
display: flex;
|
|
90
|
+
align-items: center;
|
|
91
|
+
justify-content: center;
|
|
92
|
+
transition: all 0.2s ease;
|
|
93
|
+
z-index: 100002;
|
|
94
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.rml-closeBtn:hover {
|
|
98
|
+
background: rgba(255, 255, 255, 0.2);
|
|
99
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
100
|
+
transform: scale(1.05);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.rml-closeBtn svg {
|
|
104
|
+
width: 20px;
|
|
105
|
+
height: 20px;
|
|
106
|
+
fill: currentColor;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.rml-navBtn {
|
|
110
|
+
position: absolute;
|
|
111
|
+
top: 50%;
|
|
112
|
+
transform: translateY(-50%);
|
|
113
|
+
background: rgba(255, 255, 255, 0.06);
|
|
114
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
115
|
+
color: #fff;
|
|
116
|
+
width: 56px;
|
|
117
|
+
height: 56px;
|
|
118
|
+
border-radius: 50%;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
transition: all 0.2s ease;
|
|
124
|
+
z-index: 100001;
|
|
125
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.rml-navBtn:hover {
|
|
129
|
+
background: rgba(255, 255, 255, 0.18);
|
|
130
|
+
border-color: rgba(255, 255, 255, 0.25);
|
|
131
|
+
transform: translateY(-50%) scale(1.05);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.rml-prevBtn {
|
|
135
|
+
left: 30px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.rml-nextBtn {
|
|
139
|
+
right: 30px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.rml-navBtn svg {
|
|
143
|
+
width: 24px;
|
|
144
|
+
height: 24px;
|
|
145
|
+
fill: currentColor;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ---------- Footer ---------- */
|
|
149
|
+
.rml-footer {
|
|
150
|
+
position: absolute;
|
|
151
|
+
bottom: 30px;
|
|
152
|
+
left: 0;
|
|
153
|
+
right: 0;
|
|
154
|
+
text-align: center;
|
|
155
|
+
color: #fff;
|
|
156
|
+
padding: 0 40px;
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
z-index: 100000;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 6px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.rml-counter {
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
color: rgba(255, 255, 255, 0.5);
|
|
169
|
+
letter-spacing: 1.5px;
|
|
170
|
+
text-transform: uppercase;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.rml-title {
|
|
174
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
175
|
+
font-size: 18px;
|
|
176
|
+
font-weight: 500;
|
|
177
|
+
margin: 0;
|
|
178
|
+
letter-spacing: 0.5px;
|
|
179
|
+
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.6);
|
|
180
|
+
max-width: 600px;
|
|
181
|
+
white-space: nowrap;
|
|
182
|
+
overflow: hidden;
|
|
183
|
+
text-overflow: ellipsis;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ---------- Spinner ---------- */
|
|
187
|
+
.rml-spinner {
|
|
188
|
+
position: absolute;
|
|
189
|
+
width: 40px;
|
|
190
|
+
height: 40px;
|
|
191
|
+
border: 3px solid rgba(255, 255, 255, 0.1);
|
|
192
|
+
border-radius: 50%;
|
|
193
|
+
border-top-color: #fff;
|
|
194
|
+
animation: rml-spin 0.8s ease-in-out infinite;
|
|
195
|
+
z-index: 99999;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes rml-spin {
|
|
199
|
+
to {
|
|
200
|
+
transform: rotate(360deg);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes rml-fadeIn {
|
|
205
|
+
from {
|
|
206
|
+
opacity: 0;
|
|
207
|
+
}
|
|
208
|
+
to {
|
|
209
|
+
opacity: 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ---------- Responsive ---------- */
|
|
214
|
+
@media (max-width: 768px) {
|
|
215
|
+
.rml-closeBtn {
|
|
216
|
+
top: 15px;
|
|
217
|
+
right: 15px;
|
|
218
|
+
width: 40px;
|
|
219
|
+
height: 40px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.rml-navBtn {
|
|
223
|
+
width: 44px;
|
|
224
|
+
height: 44px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.rml-prevBtn {
|
|
228
|
+
left: 15px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.rml-nextBtn {
|
|
232
|
+
right: 15px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.rml-navBtn svg {
|
|
236
|
+
width: 18px;
|
|
237
|
+
height: 18px;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.rml-title {
|
|
241
|
+
font-size: 15px;
|
|
242
|
+
max-width: 80%;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.rml-image {
|
|
246
|
+
max-height: 65vh;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.rml-playerContainer {
|
|
250
|
+
width: 95vw;
|
|
251
|
+
}
|
|
252
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-media-lightbox",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A lightweight, dependency-free React lightbox for images and video (YouTube, Vimeo, and self-hosted files).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"lightbox",
|
|
8
|
+
"gallery",
|
|
9
|
+
"image",
|
|
10
|
+
"video",
|
|
11
|
+
"youtube",
|
|
12
|
+
"vimeo",
|
|
13
|
+
"modal",
|
|
14
|
+
"carousel"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "Sreekanth Ramachandran <tech.sreekanth@gmail.com>",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"**/*.css"
|
|
21
|
+
],
|
|
22
|
+
"main": "./dist/index.cjs",
|
|
23
|
+
"module": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"require": "./dist/index.cjs"
|
|
30
|
+
},
|
|
31
|
+
"./styles.css": "./dist/styles.css"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"dev": "tsup --watch",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": ">=18",
|
|
44
|
+
"react-dom": ">=18"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/react": "^19.0.0",
|
|
48
|
+
"@types/react-dom": "^19.0.0",
|
|
49
|
+
"react": "^19.0.0",
|
|
50
|
+
"react-dom": "^19.0.0",
|
|
51
|
+
"tsup": "^8.3.5",
|
|
52
|
+
"typescript": "^5.7.2"
|
|
53
|
+
}
|
|
54
|
+
}
|