react-media-lightbox 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -69,6 +69,8 @@ function getEmbedURL(url, provider, autoplay) {
69
69
  }
70
70
  return null;
71
71
  }
72
+ var SWIPE_THRESHOLD = 50;
73
+ var CLOSE_THRESHOLD = 110;
72
74
  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
75
  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
76
  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" }) });
@@ -145,6 +147,10 @@ function MediaLightbox({
145
147
  }) {
146
148
  const count = items?.length ?? 0;
147
149
  const [currentIndex, setCurrentIndex] = react.useState(initialIndex);
150
+ const [drag, setDrag] = react.useState(null);
151
+ const touchRef = react.useRef(
152
+ null
153
+ );
148
154
  const handlePrev = react.useCallback(() => {
149
155
  setCurrentIndex((prev) => {
150
156
  const next = prev === 0 ? loop ? count - 1 : 0 : prev - 1;
@@ -159,6 +165,39 @@ function MediaLightbox({
159
165
  return next;
160
166
  });
161
167
  }, [count, loop, onIndexChange]);
168
+ const handleTouchStart = react.useCallback((e) => {
169
+ if (e.touches.length !== 1) return;
170
+ const t = e.touches[0];
171
+ touchRef.current = { x: t.clientX, y: t.clientY, axis: null };
172
+ }, []);
173
+ const handleTouchMove = react.useCallback((e) => {
174
+ const start = touchRef.current;
175
+ if (!start || e.touches.length !== 1) return;
176
+ const t = e.touches[0];
177
+ const dx = t.clientX - start.x;
178
+ const dy = t.clientY - start.y;
179
+ if (!start.axis && Math.abs(dx) + Math.abs(dy) > 10) {
180
+ start.axis = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
181
+ }
182
+ if (start.axis === "x") {
183
+ setDrag({ x: dx, y: 0 });
184
+ } else if (start.axis === "y" && dy > 0) {
185
+ setDrag({ x: 0, y: dy });
186
+ }
187
+ }, []);
188
+ const handleTouchEnd = react.useCallback(() => {
189
+ const start = touchRef.current;
190
+ const offset = drag;
191
+ touchRef.current = null;
192
+ setDrag(null);
193
+ if (!start || !offset) return;
194
+ if (start.axis === "x" && Math.abs(offset.x) > SWIPE_THRESHOLD && count > 1) {
195
+ if (offset.x < 0) handleNext();
196
+ else handlePrev();
197
+ } else if (start.axis === "y" && offset.y > CLOSE_THRESHOLD) {
198
+ onClose();
199
+ }
200
+ }, [drag, count, handleNext, handlePrev, onClose]);
162
201
  react.useEffect(() => {
163
202
  const handleKeyDown = (e) => {
164
203
  if (e.key === "ArrowLeft") handlePrev();
@@ -186,6 +225,11 @@ function MediaLightbox({
186
225
  onClose();
187
226
  }
188
227
  };
228
+ const contentStyle = drag ? {
229
+ transform: `translate3d(${drag.x}px, ${drag.y}px, 0)`,
230
+ transition: "none"
231
+ } : {};
232
+ const overlayStyle = drag && drag.y > 0 ? { opacity: Math.max(0.3, 1 - drag.y / 400) } : {};
189
233
  return /* @__PURE__ */ jsxRuntime.jsxs(
190
234
  "div",
191
235
  {
@@ -193,6 +237,7 @@ function MediaLightbox({
193
237
  role: "dialog",
194
238
  "aria-modal": "true",
195
239
  onClick: handleOverlayClick,
240
+ style: overlayStyle,
196
241
  children: [
197
242
  /* @__PURE__ */ jsxRuntime.jsx("button", { className: "rml-closeBtn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {}) }),
198
243
  count > 1 && /* @__PURE__ */ jsxRuntime.jsx(
@@ -204,15 +249,27 @@ function MediaLightbox({
204
249
  children: /* @__PURE__ */ jsxRuntime.jsx(PrevIcon, {})
205
250
  }
206
251
  ),
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,
252
+ /* @__PURE__ */ jsxRuntime.jsx(
253
+ "div",
209
254
  {
210
- src: current.src,
211
- alt: current.alt || current.title || `Item ${safeIndex + 1}`,
212
- fallbackSrc
213
- },
214
- safeIndex
215
- ) }),
255
+ className: "rml-content",
256
+ onClick: handleOverlayClick,
257
+ onTouchStart: handleTouchStart,
258
+ onTouchMove: handleTouchMove,
259
+ onTouchEnd: handleTouchEnd,
260
+ onTouchCancel: handleTouchEnd,
261
+ style: contentStyle,
262
+ children: current.type === "video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoSlide, { item: current }) : /* @__PURE__ */ jsxRuntime.jsx(
263
+ ImageSlide,
264
+ {
265
+ src: current.src,
266
+ alt: current.alt || current.title || `Item ${safeIndex + 1}`,
267
+ fallbackSrc
268
+ },
269
+ safeIndex
270
+ )
271
+ }
272
+ ),
216
273
  count > 1 && /* @__PURE__ */ jsxRuntime.jsx(
217
274
  "button",
218
275
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/video.ts","../src/MediaLightbox.tsx"],"names":["jsx","useState","jsxs","useCallback","useEffect"],"mappings":"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","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"]}
1
+ {"version":3,"sources":["../src/video.ts","../src/MediaLightbox.tsx"],"names":["jsx","useState","jsxs","useRef","useCallback","useEffect"],"mappings":"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","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, useRef, useState } from \"react\";\nimport type { MediaItem, MediaLightboxProps, VideoItem } from \"./types\";\nimport { detectProvider, getEmbedURL } from \"./video\";\n\n/** Minimum px travelled before a gesture is treated as a swipe. */\nconst SWIPE_THRESHOLD = 50;\n/** Vertical px travelled before a downward swipe closes the lightbox. */\nconst CLOSE_THRESHOLD = 110;\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 // Live drag offset (in px) used to follow the finger during a swipe.\n const [drag, setDrag] = useState<{ x: number; y: number } | null>(null);\n const touchRef = useRef<{ x: number; y: number; axis: \"x\" | \"y\" | null } | null>(\n null\n );\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 const handleTouchStart = useCallback((e: React.TouchEvent) => {\n if (e.touches.length !== 1) return;\n const t = e.touches[0];\n touchRef.current = { x: t.clientX, y: t.clientY, axis: null };\n }, []);\n\n const handleTouchMove = useCallback((e: React.TouchEvent) => {\n const start = touchRef.current;\n if (!start || e.touches.length !== 1) return;\n const t = e.touches[0];\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n\n // Lock the gesture to a single axis once movement is clearly intentional.\n if (!start.axis && Math.abs(dx) + Math.abs(dy) > 10) {\n start.axis = Math.abs(dx) > Math.abs(dy) ? \"x\" : \"y\";\n }\n\n if (start.axis === \"x\") {\n setDrag({ x: dx, y: 0 });\n } else if (start.axis === \"y\" && dy > 0) {\n // Only follow downward drags (swipe-to-close).\n setDrag({ x: 0, y: dy });\n }\n }, []);\n\n const handleTouchEnd = useCallback(() => {\n const start = touchRef.current;\n const offset = drag;\n touchRef.current = null;\n setDrag(null);\n if (!start || !offset) return;\n\n if (start.axis === \"x\" && Math.abs(offset.x) > SWIPE_THRESHOLD && count > 1) {\n if (offset.x < 0) handleNext();\n else handlePrev();\n } else if (start.axis === \"y\" && offset.y > CLOSE_THRESHOLD) {\n onClose();\n }\n }, [drag, count, handleNext, handlePrev, onClose]);\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 const contentStyle: React.CSSProperties = drag\n ? {\n transform: `translate3d(${drag.x}px, ${drag.y}px, 0)`,\n transition: \"none\",\n }\n : {};\n\n // Dim the backdrop as the user drags down to close.\n const overlayStyle: React.CSSProperties =\n drag && drag.y > 0\n ? { opacity: Math.max(0.3, 1 - drag.y / 400) }\n : {};\n\n return (\n <div\n className={`rml-overlay${className ? ` ${className}` : \"\"}`}\n role=\"dialog\"\n aria-modal=\"true\"\n onClick={handleOverlayClick}\n style={overlayStyle}\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\n className=\"rml-content\"\n onClick={handleOverlayClick}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n onTouchCancel={handleTouchEnd}\n style={contentStyle}\n >\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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from 'react';
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
3
 
4
4
  // src/MediaLightbox.tsx
@@ -65,6 +65,8 @@ function getEmbedURL(url, provider, autoplay) {
65
65
  }
66
66
  return null;
67
67
  }
68
+ var SWIPE_THRESHOLD = 50;
69
+ var CLOSE_THRESHOLD = 110;
68
70
  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
71
  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
72
  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" }) });
@@ -141,6 +143,10 @@ function MediaLightbox({
141
143
  }) {
142
144
  const count = items?.length ?? 0;
143
145
  const [currentIndex, setCurrentIndex] = useState(initialIndex);
146
+ const [drag, setDrag] = useState(null);
147
+ const touchRef = useRef(
148
+ null
149
+ );
144
150
  const handlePrev = useCallback(() => {
145
151
  setCurrentIndex((prev) => {
146
152
  const next = prev === 0 ? loop ? count - 1 : 0 : prev - 1;
@@ -155,6 +161,39 @@ function MediaLightbox({
155
161
  return next;
156
162
  });
157
163
  }, [count, loop, onIndexChange]);
164
+ const handleTouchStart = useCallback((e) => {
165
+ if (e.touches.length !== 1) return;
166
+ const t = e.touches[0];
167
+ touchRef.current = { x: t.clientX, y: t.clientY, axis: null };
168
+ }, []);
169
+ const handleTouchMove = useCallback((e) => {
170
+ const start = touchRef.current;
171
+ if (!start || e.touches.length !== 1) return;
172
+ const t = e.touches[0];
173
+ const dx = t.clientX - start.x;
174
+ const dy = t.clientY - start.y;
175
+ if (!start.axis && Math.abs(dx) + Math.abs(dy) > 10) {
176
+ start.axis = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
177
+ }
178
+ if (start.axis === "x") {
179
+ setDrag({ x: dx, y: 0 });
180
+ } else if (start.axis === "y" && dy > 0) {
181
+ setDrag({ x: 0, y: dy });
182
+ }
183
+ }, []);
184
+ const handleTouchEnd = useCallback(() => {
185
+ const start = touchRef.current;
186
+ const offset = drag;
187
+ touchRef.current = null;
188
+ setDrag(null);
189
+ if (!start || !offset) return;
190
+ if (start.axis === "x" && Math.abs(offset.x) > SWIPE_THRESHOLD && count > 1) {
191
+ if (offset.x < 0) handleNext();
192
+ else handlePrev();
193
+ } else if (start.axis === "y" && offset.y > CLOSE_THRESHOLD) {
194
+ onClose();
195
+ }
196
+ }, [drag, count, handleNext, handlePrev, onClose]);
158
197
  useEffect(() => {
159
198
  const handleKeyDown = (e) => {
160
199
  if (e.key === "ArrowLeft") handlePrev();
@@ -182,6 +221,11 @@ function MediaLightbox({
182
221
  onClose();
183
222
  }
184
223
  };
224
+ const contentStyle = drag ? {
225
+ transform: `translate3d(${drag.x}px, ${drag.y}px, 0)`,
226
+ transition: "none"
227
+ } : {};
228
+ const overlayStyle = drag && drag.y > 0 ? { opacity: Math.max(0.3, 1 - drag.y / 400) } : {};
185
229
  return /* @__PURE__ */ jsxs(
186
230
  "div",
187
231
  {
@@ -189,6 +233,7 @@ function MediaLightbox({
189
233
  role: "dialog",
190
234
  "aria-modal": "true",
191
235
  onClick: handleOverlayClick,
236
+ style: overlayStyle,
192
237
  children: [
193
238
  /* @__PURE__ */ jsx("button", { className: "rml-closeBtn", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) }),
194
239
  count > 1 && /* @__PURE__ */ jsx(
@@ -200,15 +245,27 @@ function MediaLightbox({
200
245
  children: /* @__PURE__ */ jsx(PrevIcon, {})
201
246
  }
202
247
  ),
203
- /* @__PURE__ */ jsx("div", { className: "rml-content", onClick: handleOverlayClick, children: current.type === "video" ? /* @__PURE__ */ jsx(VideoSlide, { item: current }) : /* @__PURE__ */ jsx(
204
- ImageSlide,
248
+ /* @__PURE__ */ jsx(
249
+ "div",
205
250
  {
206
- src: current.src,
207
- alt: current.alt || current.title || `Item ${safeIndex + 1}`,
208
- fallbackSrc
209
- },
210
- safeIndex
211
- ) }),
251
+ className: "rml-content",
252
+ onClick: handleOverlayClick,
253
+ onTouchStart: handleTouchStart,
254
+ onTouchMove: handleTouchMove,
255
+ onTouchEnd: handleTouchEnd,
256
+ onTouchCancel: handleTouchEnd,
257
+ style: contentStyle,
258
+ children: current.type === "video" ? /* @__PURE__ */ jsx(VideoSlide, { item: current }) : /* @__PURE__ */ jsx(
259
+ ImageSlide,
260
+ {
261
+ src: current.src,
262
+ alt: current.alt || current.title || `Item ${safeIndex + 1}`,
263
+ fallbackSrc
264
+ },
265
+ safeIndex
266
+ )
267
+ }
268
+ ),
212
269
  count > 1 && /* @__PURE__ */ jsx(
213
270
  "button",
214
271
  {
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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, useRef, useState } from \"react\";\nimport type { MediaItem, MediaLightboxProps, VideoItem } from \"./types\";\nimport { detectProvider, getEmbedURL } from \"./video\";\n\n/** Minimum px travelled before a gesture is treated as a swipe. */\nconst SWIPE_THRESHOLD = 50;\n/** Vertical px travelled before a downward swipe closes the lightbox. */\nconst CLOSE_THRESHOLD = 110;\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 // Live drag offset (in px) used to follow the finger during a swipe.\n const [drag, setDrag] = useState<{ x: number; y: number } | null>(null);\n const touchRef = useRef<{ x: number; y: number; axis: \"x\" | \"y\" | null } | null>(\n null\n );\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 const handleTouchStart = useCallback((e: React.TouchEvent) => {\n if (e.touches.length !== 1) return;\n const t = e.touches[0];\n touchRef.current = { x: t.clientX, y: t.clientY, axis: null };\n }, []);\n\n const handleTouchMove = useCallback((e: React.TouchEvent) => {\n const start = touchRef.current;\n if (!start || e.touches.length !== 1) return;\n const t = e.touches[0];\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n\n // Lock the gesture to a single axis once movement is clearly intentional.\n if (!start.axis && Math.abs(dx) + Math.abs(dy) > 10) {\n start.axis = Math.abs(dx) > Math.abs(dy) ? \"x\" : \"y\";\n }\n\n if (start.axis === \"x\") {\n setDrag({ x: dx, y: 0 });\n } else if (start.axis === \"y\" && dy > 0) {\n // Only follow downward drags (swipe-to-close).\n setDrag({ x: 0, y: dy });\n }\n }, []);\n\n const handleTouchEnd = useCallback(() => {\n const start = touchRef.current;\n const offset = drag;\n touchRef.current = null;\n setDrag(null);\n if (!start || !offset) return;\n\n if (start.axis === \"x\" && Math.abs(offset.x) > SWIPE_THRESHOLD && count > 1) {\n if (offset.x < 0) handleNext();\n else handlePrev();\n } else if (start.axis === \"y\" && offset.y > CLOSE_THRESHOLD) {\n onClose();\n }\n }, [drag, count, handleNext, handlePrev, onClose]);\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 const contentStyle: React.CSSProperties = drag\n ? {\n transform: `translate3d(${drag.x}px, ${drag.y}px, 0)`,\n transition: \"none\",\n }\n : {};\n\n // Dim the backdrop as the user drags down to close.\n const overlayStyle: React.CSSProperties =\n drag && drag.y > 0\n ? { opacity: Math.max(0.3, 1 - drag.y / 400) }\n : {};\n\n return (\n <div\n className={`rml-overlay${className ? ` ${className}` : \"\"}`}\n role=\"dialog\"\n aria-modal=\"true\"\n onClick={handleOverlayClick}\n style={overlayStyle}\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\n className=\"rml-content\"\n onClick={handleOverlayClick}\n onTouchStart={handleTouchStart}\n onTouchMove={handleTouchMove}\n onTouchEnd={handleTouchEnd}\n onTouchCancel={handleTouchEnd}\n style={contentStyle}\n >\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 CHANGED
@@ -10,6 +10,9 @@
10
10
  -webkit-backdrop-filter: blur(12px);
11
11
  animation: rml-fadeIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
12
12
  user-select: none;
13
+ -webkit-tap-highlight-color: transparent;
14
+ touch-action: none;
15
+ overscroll-behavior: contain;
13
16
  }
14
17
 
15
18
  .rml-content {
@@ -20,6 +23,9 @@
20
23
  align-items: center;
21
24
  justify-content: center;
22
25
  outline: none;
26
+ /* Smoothly snap back into place when a swipe is released. */
27
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
28
+ will-change: transform;
23
29
  }
24
30
 
25
31
  /* ---------- Image slide ---------- */
@@ -30,6 +36,8 @@
30
36
  display: flex;
31
37
  align-items: center;
32
38
  justify-content: center;
39
+ /* Let JS own horizontal/vertical swipe gestures over the image. */
40
+ touch-action: none;
33
41
  }
34
42
 
35
43
  .rml-image {
@@ -77,8 +85,8 @@
77
85
  /* ---------- Controls ---------- */
78
86
  .rml-closeBtn {
79
87
  position: absolute;
80
- top: 30px;
81
- right: 30px;
88
+ top: max(30px, env(safe-area-inset-top));
89
+ right: max(30px, env(safe-area-inset-right));
82
90
  background: rgba(255, 255, 255, 0.08);
83
91
  border: 1px solid rgba(255, 255, 255, 0.1);
84
92
  color: #fff;
@@ -94,10 +102,17 @@
94
102
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
95
103
  }
96
104
 
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);
105
+ @media (hover: hover) {
106
+ .rml-closeBtn:hover {
107
+ background: rgba(255, 255, 255, 0.2);
108
+ border-color: rgba(255, 255, 255, 0.3);
109
+ transform: scale(1.05);
110
+ }
111
+ }
112
+
113
+ .rml-closeBtn:active {
114
+ background: rgba(255, 255, 255, 0.28);
115
+ transform: scale(0.92);
101
116
  }
102
117
 
103
118
  .rml-closeBtn svg {
@@ -125,10 +140,17 @@
125
140
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
126
141
  }
127
142
 
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);
143
+ @media (hover: hover) {
144
+ .rml-navBtn:hover {
145
+ background: rgba(255, 255, 255, 0.18);
146
+ border-color: rgba(255, 255, 255, 0.25);
147
+ transform: translateY(-50%) scale(1.05);
148
+ }
149
+ }
150
+
151
+ .rml-navBtn:active {
152
+ background: rgba(255, 255, 255, 0.28);
153
+ transform: translateY(-50%) scale(0.92);
132
154
  }
133
155
 
134
156
  .rml-prevBtn {
@@ -148,7 +170,7 @@
148
170
  /* ---------- Footer ---------- */
149
171
  .rml-footer {
150
172
  position: absolute;
151
- bottom: 30px;
173
+ bottom: max(30px, env(safe-area-inset-bottom));
152
174
  left: 0;
153
175
  right: 0;
154
176
  text-align: center;
@@ -212,41 +234,98 @@
212
234
 
213
235
  /* ---------- Responsive ---------- */
214
236
  @media (max-width: 768px) {
215
- .rml-closeBtn {
216
- top: 15px;
217
- right: 15px;
218
- width: 40px;
219
- height: 40px;
237
+ .rml-content {
238
+ width: 100vw;
239
+ height: 100dvh;
220
240
  }
221
241
 
222
- .rml-navBtn {
242
+ .rml-closeBtn {
243
+ top: max(12px, env(safe-area-inset-top));
244
+ right: max(12px, env(safe-area-inset-right));
223
245
  width: 44px;
224
246
  height: 44px;
225
247
  }
226
248
 
249
+ /* Bottom-center the nav buttons so they're reachable with the thumbs. */
250
+ .rml-navBtn {
251
+ top: auto;
252
+ bottom: max(74px, calc(env(safe-area-inset-bottom) + 64px));
253
+ transform: none;
254
+ width: 48px;
255
+ height: 48px;
256
+ }
257
+
258
+ .rml-navBtn:active {
259
+ transform: scale(0.9);
260
+ }
261
+
227
262
  .rml-prevBtn {
228
- left: 15px;
263
+ left: 50%;
264
+ margin-left: -64px;
229
265
  }
230
266
 
231
267
  .rml-nextBtn {
232
- right: 15px;
268
+ right: 50%;
269
+ margin-right: -64px;
233
270
  }
234
271
 
235
272
  .rml-navBtn svg {
236
- width: 18px;
237
- height: 18px;
273
+ width: 22px;
274
+ height: 22px;
275
+ }
276
+
277
+ .rml-footer {
278
+ bottom: max(20px, calc(env(safe-area-inset-bottom) + 12px));
279
+ padding: 0 20px;
238
280
  }
239
281
 
240
282
  .rml-title {
241
283
  font-size: 15px;
242
- max-width: 80%;
284
+ max-width: 86%;
243
285
  }
244
286
 
245
287
  .rml-image {
246
- max-height: 65vh;
288
+ max-width: 96vw;
289
+ max-height: 82dvh;
290
+ border-radius: 4px;
247
291
  }
248
292
 
249
293
  .rml-playerContainer {
250
- width: 95vw;
294
+ width: 96vw;
295
+ border-radius: 8px;
296
+ }
297
+ }
298
+
299
+ /* Compact landscape phones: keep nav buttons on the sides instead. */
300
+ @media (max-width: 900px) and (orientation: landscape) {
301
+ .rml-navBtn {
302
+ top: 50%;
303
+ bottom: auto;
304
+ transform: translateY(-50%);
305
+ width: 44px;
306
+ height: 44px;
307
+ }
308
+
309
+ .rml-prevBtn {
310
+ left: max(10px, env(safe-area-inset-left));
311
+ margin-left: 0;
312
+ }
313
+
314
+ .rml-nextBtn {
315
+ right: max(10px, env(safe-area-inset-right));
316
+ margin-right: 0;
317
+ }
318
+
319
+ .rml-image {
320
+ max-height: 88dvh;
321
+ }
322
+ }
323
+
324
+ @media (prefers-reduced-motion: reduce) {
325
+ .rml-overlay,
326
+ .rml-content,
327
+ .rml-image {
328
+ animation: none;
329
+ transition: none;
251
330
  }
252
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-media-lightbox",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A lightweight, dependency-free React lightbox for images and video (YouTube, Vimeo, and self-hosted files).",
5
5
  "keywords": [
6
6
  "react",
@@ -15,6 +15,14 @@
15
15
  ],
16
16
  "license": "MIT",
17
17
  "author": "Sreekanth Ramachandran <tech.sreekanth@gmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/techsreekanth/react-media-lightbox.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/techsreekanth/react-media-lightbox/issues"
24
+ },
25
+ "homepage": "https://github.com/techsreekanth/react-media-lightbox#readme",
18
26
  "type": "module",
19
27
  "sideEffects": [
20
28
  "**/*.css"