uix-image-magnify 0.2.0 → 0.2.2

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.js CHANGED
@@ -1,5 +1,10 @@
1
- import { forwardRef, useRef, useState, useEffect, useCallback, useMemo, useLayoutEffect } from 'react';
2
- import { jsxs, jsx } from 'react/jsx-runtime';
1
+ import { forwardRef, useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from 'react';
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import { Swiper, SwiperSlide } from 'swiper/react';
4
+ import { Navigation, Thumbs, Keyboard } from 'swiper/modules';
5
+ import 'swiper/css';
6
+ import 'swiper/css/navigation';
7
+ import 'swiper/css/thumbs';
3
8
 
4
9
  // src/components/ImageMagnify.tsx
5
10
  var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
@@ -411,6 +416,19 @@ function useImageMagnify(options = {}) {
411
416
  handlers
412
417
  };
413
418
  }
419
+ var useIsoLayoutEffect2 = typeof window !== "undefined" ? useLayoutEffect : useEffect;
420
+ function useMediaQuery(query) {
421
+ const [matches, setMatches] = useState(false);
422
+ useIsoLayoutEffect2(() => {
423
+ const media = window.matchMedia(query);
424
+ const listener = () => setMatches(media.matches);
425
+ listener();
426
+ media.addEventListener("change", listener);
427
+ return () => media.removeEventListener("change", listener);
428
+ }, [query]);
429
+ return matches;
430
+ }
431
+ var MOBILE_QUERY = "(max-width: 768px)";
414
432
  var ImageMagnify = forwardRef(
415
433
  (props, ref) => {
416
434
  const {
@@ -439,6 +457,8 @@ var ImageMagnify = forwardRef(
439
457
  onError,
440
458
  ...hookOptions
441
459
  } = props;
460
+ const isMobile = useMediaQuery(MOBILE_QUERY);
461
+ const responsivePanePosition = isMobile ? "bottom" : panePosition;
442
462
  const {
443
463
  isZoomed,
444
464
  bgImage,
@@ -456,25 +476,36 @@ var ImageMagnify = forwardRef(
456
476
  disabled,
457
477
  ...hookOptions
458
478
  });
459
- const paneOffset = {};
460
- if (panePosition === "right") {
461
- paneOffset.left = "calc(100% + 12px)";
462
- paneOffset.top = 0;
463
- } else if (panePosition === "left") {
464
- paneOffset.right = "calc(100% + 12px)";
465
- paneOffset.top = 0;
466
- } else if (panePosition === "top") {
467
- paneOffset.left = 0;
468
- paneOffset.bottom = "calc(100% + 12px)";
479
+ const panePositionStyle = {};
480
+ if (responsivePanePosition === "right") {
481
+ panePositionStyle.position = "absolute";
482
+ panePositionStyle.left = "calc(100% + 12px)";
483
+ panePositionStyle.top = 0;
484
+ } else if (responsivePanePosition === "left") {
485
+ panePositionStyle.position = "absolute";
486
+ panePositionStyle.right = "calc(100% + 12px)";
487
+ panePositionStyle.top = 0;
488
+ } else if (responsivePanePosition === "top") {
489
+ panePositionStyle.position = "absolute";
490
+ panePositionStyle.left = 0;
491
+ panePositionStyle.bottom = "calc(100% + 12px)";
492
+ } else if (isMobile) {
493
+ panePositionStyle.position = "static";
494
+ panePositionStyle.display = "block";
495
+ panePositionStyle.marginTop = 12;
496
+ panePositionStyle.width = "100%";
497
+ panePositionStyle.boxSizing = "border-box";
469
498
  } else {
470
- paneOffset.left = 0;
471
- paneOffset.top = "calc(100% + 12px)";
499
+ panePositionStyle.position = "absolute";
500
+ panePositionStyle.left = 0;
501
+ panePositionStyle.top = "calc(100% + 12px)";
472
502
  }
473
503
  const isInner = mode === "inner";
474
504
  const showLens = (mode === "lens" || mode === "follow") && isZoomed;
475
505
  const showPane = mode === "side-by-side" && isZoomed;
476
506
  const showCursor = mode === "side-by-side" && isZoomed;
477
507
  const cursorStyle = disabled ? "default" : zoomType === "hover" || zoomType === "hold" || zoomType === "wheel" ? "crosshair" : isZoomed ? "zoom-out" : "zoom-in";
508
+ const outerWidth = isMobile ? "100%" : width;
478
509
  return /* @__PURE__ */ jsxs(
479
510
  "div",
480
511
  {
@@ -484,7 +515,8 @@ var ImageMagnify = forwardRef(
484
515
  position: "relative",
485
516
  display: "inline-block",
486
517
  lineHeight: 0,
487
- width,
518
+ width: outerWidth,
519
+ maxWidth: "100%",
488
520
  ...style
489
521
  },
490
522
  children: [
@@ -501,7 +533,6 @@ var ImageMagnify = forwardRef(
501
533
  overflow: "hidden",
502
534
  width: "100%",
503
535
  cursor: cursorStyle,
504
- // Only block scrolling while zoomed so page scrolling still works.
505
536
  touchAction: isZoomed ? "none" : "pan-y",
506
537
  outline: "none",
507
538
  lineHeight: 0
@@ -588,16 +619,17 @@ var ImageMagnify = forwardRef(
588
619
  className: paneClassName,
589
620
  "aria-hidden": "true",
590
621
  style: {
591
- position: "absolute",
592
- width: paneWidth,
622
+ width: isMobile && responsivePanePosition === "bottom" ? "100%" : paneWidth,
593
623
  height: paneHeight,
624
+ maxWidth: "100%",
625
+ boxSizing: "border-box",
594
626
  backgroundImage: bgImage ? `url("${bgImage}")` : void 0,
595
627
  backgroundRepeat: "no-repeat",
596
628
  border: "1px solid #e5e5e5",
597
629
  borderRadius: 4,
598
630
  boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
599
631
  pointerEvents: "none",
600
- ...paneOffset,
632
+ ...panePositionStyle,
601
633
  ...paneStyle
602
634
  }
603
635
  }
@@ -608,45 +640,6 @@ var ImageMagnify = forwardRef(
608
640
  }
609
641
  );
610
642
  ImageMagnify.displayName = "ImageMagnify";
611
- function useImageSlider({
612
- length,
613
- defaultIndex = 0,
614
- index: controlledIndex,
615
- onIndexChange,
616
- loop = true
617
- }) {
618
- const [internalIndex, setInternalIndex] = useState(defaultIndex);
619
- const safeLength = Math.max(1, length);
620
- const rawIndex = controlledIndex ?? internalIndex;
621
- const index = Math.max(0, Math.min(safeLength - 1, rawIndex));
622
- const setIndex = useCallback(
623
- (next2) => {
624
- if (length <= 0) return;
625
- let resolved;
626
- if (loop) {
627
- resolved = (next2 % safeLength + safeLength) % safeLength;
628
- } else {
629
- resolved = Math.max(0, Math.min(safeLength - 1, next2));
630
- }
631
- if (resolved === index) return;
632
- if (controlledIndex === void 0) {
633
- setInternalIndex(resolved);
634
- }
635
- onIndexChange?.(resolved);
636
- },
637
- [length, safeLength, loop, index, controlledIndex, onIndexChange]
638
- );
639
- const prev = useCallback(() => setIndex(index - 1), [setIndex, index]);
640
- const next = useCallback(() => setIndex(index + 1), [setIndex, index]);
641
- return {
642
- index,
643
- canPrev: loop || index > 0,
644
- canNext: loop || index < safeLength - 1,
645
- setIndex,
646
- prev,
647
- next
648
- };
649
- }
650
643
  var ArrowLeftIcon = () => /* @__PURE__ */ jsx(
651
644
  "svg",
652
645
  {
@@ -698,30 +691,6 @@ var defaultArrowBaseStyle = {
698
691
  backdropFilter: "blur(4px)",
699
692
  WebkitBackdropFilter: "blur(4px)"
700
693
  };
701
- var defaultThumbStripStyle = {
702
- display: "flex",
703
- overflowX: "auto",
704
- overflowY: "hidden",
705
- paddingBottom: 4,
706
- scrollbarWidth: "thin",
707
- WebkitOverflowScrolling: "touch"
708
- };
709
- var defaultThumbBaseStyle = {
710
- flexShrink: 0,
711
- padding: 0,
712
- border: "2px solid #e5e5e5",
713
- background: "#fff",
714
- cursor: "pointer",
715
- overflow: "hidden",
716
- transition: "border-color 0.15s ease",
717
- borderRadius: 2,
718
- display: "inline-flex",
719
- alignItems: "center",
720
- justifyContent: "center"
721
- };
722
- var defaultThumbActiveStyle = {
723
- borderColor: "#111"
724
- };
725
694
  var visuallyHidden = {
726
695
  position: "absolute",
727
696
  width: 1,
@@ -762,70 +731,51 @@ var ImageMagnifyGallery = forwardRef((props, ref) => {
762
731
  thumbnailGap = 8,
763
732
  prevArrow,
764
733
  nextArrow,
734
+ width,
765
735
  ...magnifyProps
766
736
  } = props;
767
- const { index, canPrev, canNext, setIndex, prev, next } = useImageSlider({
768
- length: images.length,
769
- defaultIndex,
770
- index: controlledActiveIndex,
771
- onIndexChange,
772
- loop
773
- });
774
- const currentSrc = images[index] ?? "";
775
- const thumbStripRef = useRef(null);
776
- const [hoveredThumb, setHoveredThumb] = useState(null);
737
+ const [swiper, setSwiper] = useState(null);
738
+ const [thumbsSwiper, setThumbsSwiper] = useState(null);
739
+ const [localIndex, setLocalIndex] = useState(defaultIndex);
740
+ const prevRef = useRef(null);
741
+ const nextRef = useRef(null);
742
+ const isControlled = controlledActiveIndex !== void 0;
743
+ const activeIndex = isControlled ? controlledActiveIndex : localIndex;
744
+ const hasMultiple = images.length > 1;
777
745
  useEffect(() => {
778
- if (!preloadAdjacent || typeof window === "undefined") return;
779
- if (images.length <= 1) return;
780
- const targets = /* @__PURE__ */ new Set();
781
- targets.add((index + 1) % images.length);
782
- targets.add((index - 1 + images.length) % images.length);
746
+ if (!swiper || swiper.destroyed) return;
747
+ if (isControlled && swiper.activeIndex !== controlledActiveIndex) {
748
+ swiper.slideTo(controlledActiveIndex);
749
+ }
750
+ }, [isControlled, controlledActiveIndex, swiper]);
751
+ useEffect(() => {
752
+ if (!preloadAdjacent || typeof window === "undefined" || images.length <= 1)
753
+ return;
754
+ const targets = /* @__PURE__ */ new Set([
755
+ (activeIndex + 1) % images.length,
756
+ (activeIndex - 1 + images.length) % images.length
757
+ ]);
783
758
  targets.forEach((i) => {
784
759
  if (images[i]) {
785
760
  const img = new Image();
786
761
  img.src = images[i];
787
762
  }
788
763
  });
789
- }, [index, images, preloadAdjacent]);
790
- useEffect(() => {
791
- const strip = thumbStripRef.current;
792
- if (!strip) return;
793
- const active = strip.children[index];
794
- if (!active) return;
795
- const sr = strip.getBoundingClientRect();
796
- const ar = active.getBoundingClientRect();
797
- if (ar.left < sr.left) {
798
- strip.scrollBy({ left: ar.left - sr.left, behavior: "smooth" });
799
- } else if (ar.right > sr.right) {
800
- strip.scrollBy({ left: ar.right - sr.right, behavior: "smooth" });
801
- }
802
- }, [index]);
803
- const handleKeyDown = useCallback(
804
- (e) => {
805
- if (!enableKeyboard) return;
806
- if (e.key === "ArrowLeft") {
807
- if (!canPrev) return;
808
- e.preventDefault();
809
- prev();
810
- } else if (e.key === "ArrowRight") {
811
- if (!canNext) return;
812
- e.preventDefault();
813
- next();
814
- }
815
- },
816
- [enableKeyboard, canPrev, canNext, prev, next]
817
- );
818
- const hasMultiple = images.length > 1;
819
- const liveText = useMemo(
820
- () => `Image ${index + 1} of ${images.length}`,
821
- [index, images.length]
822
- );
764
+ }, [activeIndex, images, preloadAdjacent]);
765
+ const modules = useMemo(() => {
766
+ const m = [Navigation, Thumbs];
767
+ if (enableKeyboard) m.push(Keyboard);
768
+ return m;
769
+ }, [enableKeyboard]);
770
+ if (images.length === 0) {
771
+ return /* @__PURE__ */ jsx("div", { ref, className, style });
772
+ }
823
773
  return /* @__PURE__ */ jsxs(
824
774
  "div",
825
775
  {
826
776
  ref,
827
777
  className,
828
- style,
778
+ style: { maxWidth: "100%", width, ...style },
829
779
  role: "region",
830
780
  "aria-roledescription": "carousel",
831
781
  "aria-label": `${alt} \u2014 image gallery`,
@@ -834,53 +784,80 @@ var ImageMagnifyGallery = forwardRef((props, ref) => {
834
784
  "div",
835
785
  {
836
786
  className: viewportClassName,
837
- style: {
838
- position: "relative",
839
- width: "fit-content",
840
- maxWidth: "100%",
841
- ...viewportStyle
842
- },
843
- tabIndex: enableKeyboard && hasMultiple ? 0 : void 0,
844
- onKeyDown: handleKeyDown,
787
+ style: { position: "relative", width: "100%", ...viewportStyle },
845
788
  children: [
846
- /* @__PURE__ */ jsx(ImageMagnify, { ...magnifyProps, src: currentSrc, alt }),
847
- showArrows && hasMultiple && canPrev && /* @__PURE__ */ jsx(
848
- "button",
789
+ /* @__PURE__ */ jsx(
790
+ Swiper,
849
791
  {
850
- type: "button",
851
- onClick: prev,
852
- className: arrowClassName,
853
- style: {
854
- ...defaultArrowBaseStyle,
855
- left: 12,
856
- ...arrowStyle
792
+ modules,
793
+ onSwiper: setSwiper,
794
+ initialSlide: isControlled ? controlledActiveIndex : defaultIndex,
795
+ onSlideChange: (s) => {
796
+ if (!isControlled) setLocalIndex(s.activeIndex);
797
+ onIndexChange?.(s.activeIndex);
857
798
  },
858
- "aria-label": "Previous image",
859
- children: prevArrow ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
860
- }
861
- ),
862
- showArrows && hasMultiple && canNext && /* @__PURE__ */ jsx(
863
- "button",
864
- {
865
- type: "button",
866
- onClick: next,
867
- className: arrowClassName,
799
+ onBeforeInit: (s) => {
800
+ if (!showArrows || !hasMultiple) return;
801
+ if (s.params.navigation && typeof s.params.navigation === "object") {
802
+ s.params.navigation.prevEl = prevRef.current;
803
+ s.params.navigation.nextEl = nextRef.current;
804
+ }
805
+ },
806
+ navigation: showArrows && hasMultiple ? {} : false,
807
+ keyboard: enableKeyboard ? { enabled: true } : void 0,
808
+ thumbs: {
809
+ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
810
+ },
811
+ loop: loop && hasMultiple,
812
+ autoHeight: true,
813
+ spaceBetween: 10,
868
814
  style: {
869
- ...defaultArrowBaseStyle,
870
- right: 12,
815
+ width: "100%",
816
+ "--swiper-navigation-size": "20px",
871
817
  ...arrowStyle
872
818
  },
873
- "aria-label": "Next image",
874
- children: nextArrow ?? /* @__PURE__ */ jsx(ArrowRightIcon, {})
819
+ children: images.map((src, idx) => /* @__PURE__ */ jsx(SwiperSlide, { children: /* @__PURE__ */ jsx(
820
+ ImageMagnify,
821
+ {
822
+ ...magnifyProps,
823
+ src,
824
+ alt,
825
+ width: "100%"
826
+ }
827
+ ) }, idx))
875
828
  }
876
829
  ),
830
+ showArrows && hasMultiple && /* @__PURE__ */ jsxs(Fragment, { children: [
831
+ /* @__PURE__ */ jsx(
832
+ "button",
833
+ {
834
+ ref: prevRef,
835
+ type: "button",
836
+ className: arrowClassName,
837
+ style: { ...defaultArrowBaseStyle, left: 12 },
838
+ "aria-label": "Previous image",
839
+ children: prevArrow ?? /* @__PURE__ */ jsx(ArrowLeftIcon, {})
840
+ }
841
+ ),
842
+ /* @__PURE__ */ jsx(
843
+ "button",
844
+ {
845
+ ref: nextRef,
846
+ type: "button",
847
+ className: arrowClassName,
848
+ style: { ...defaultArrowBaseStyle, right: 12 },
849
+ "aria-label": "Next image",
850
+ children: nextArrow ?? /* @__PURE__ */ jsx(ArrowRightIcon, {})
851
+ }
852
+ )
853
+ ] }),
877
854
  showCounter && hasMultiple && /* @__PURE__ */ jsxs(
878
855
  "div",
879
856
  {
880
857
  "aria-hidden": "true",
881
858
  style: {
882
859
  position: "absolute",
883
- bottom: 12,
860
+ top: 12,
884
861
  right: 12,
885
862
  background: "rgba(0,0,0,0.6)",
886
863
  color: "#fff",
@@ -892,74 +869,57 @@ var ImageMagnifyGallery = forwardRef((props, ref) => {
892
869
  pointerEvents: "none"
893
870
  },
894
871
  children: [
895
- index + 1,
872
+ activeIndex + 1,
896
873
  " / ",
897
874
  images.length
898
875
  ]
899
876
  }
900
877
  ),
901
- /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-atomic": "true", style: visuallyHidden, children: liveText })
878
+ /* @__PURE__ */ jsxs("div", { "aria-live": "polite", "aria-atomic": "true", style: visuallyHidden, children: [
879
+ "Image ",
880
+ activeIndex + 1,
881
+ " of ",
882
+ images.length
883
+ ] })
902
884
  ]
903
885
  }
904
886
  ),
905
887
  showThumbnails && hasMultiple && /* @__PURE__ */ jsx(
906
- "div",
888
+ Swiper,
907
889
  {
908
- ref: thumbStripRef,
909
- className: thumbnailContainerClassName,
910
- role: "tablist",
911
- "aria-label": "Choose image",
912
- style: {
913
- ...defaultThumbStripStyle,
914
- gap: thumbnailGap,
915
- marginTop: 12,
916
- ...thumbnailContainerStyle
890
+ modules,
891
+ onSwiper: setSwiper,
892
+ initialSlide: isControlled ? controlledActiveIndex : defaultIndex,
893
+ onSlideChange: (s) => {
894
+ if (!isControlled) setLocalIndex(s.activeIndex);
895
+ onIndexChange?.(s.activeIndex);
917
896
  },
918
- children: images.map((img, idx) => {
919
- const isActive = idx === index;
920
- const isHovered = idx === hoveredThumb;
921
- const cls = isActive ? thumbnailActiveClassName : thumbnailClassName;
922
- return /* @__PURE__ */ jsx(
923
- "button",
924
- {
925
- type: "button",
926
- role: "tab",
927
- onClick: () => setIndex(idx),
928
- onMouseEnter: () => setHoveredThumb(idx),
929
- onMouseLeave: () => setHoveredThumb(null),
930
- className: cls,
931
- style: {
932
- ...defaultThumbBaseStyle,
933
- width: thumbnailSize,
934
- height: thumbnailSize,
935
- ...thumbnailStyle,
936
- ...isActive ? defaultThumbActiveStyle : void 0,
937
- ...isActive ? thumbnailActiveStyle : void 0,
938
- ...isHovered && !isActive ? { borderColor: "#999" } : void 0
939
- },
940
- "aria-label": `View image ${idx + 1} of ${images.length}`,
941
- "aria-selected": isActive,
942
- children: /* @__PURE__ */ jsx(
943
- "img",
944
- {
945
- src: img,
946
- alt: "",
947
- loading: "lazy",
948
- draggable: false,
949
- style: {
950
- width: "100%",
951
- height: "100%",
952
- objectFit: "contain",
953
- display: "block",
954
- pointerEvents: "none",
955
- userSelect: "none"
956
- }
957
- }
958
- )
959
- },
960
- idx
961
- );
962
- })
897
+ onBeforeInit: (s) => {
898
+ if (!showArrows || !hasMultiple) return;
899
+ const nav = s.params.navigation;
900
+ if (nav && typeof nav === "object") {
901
+ nav.prevEl = prevRef.current;
902
+ nav.nextEl = nextRef.current;
903
+ }
904
+ },
905
+ navigation: showArrows && hasMultiple ? {} : false,
906
+ keyboard: enableKeyboard ? { enabled: true } : void 0,
907
+ thumbs: {
908
+ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
909
+ },
910
+ loop: loop && hasMultiple,
911
+ autoHeight: true,
912
+ spaceBetween: 10,
913
+ style: { width: "100%" },
914
+ children: images.map((src, idx) => /* @__PURE__ */ jsx(SwiperSlide, { children: /* @__PURE__ */ jsx(
915
+ ImageMagnify,
916
+ {
917
+ ...magnifyProps,
918
+ src,
919
+ alt,
920
+ width: "100%"
921
+ }
922
+ ) }, idx))
963
923
  }
964
924
  )
965
925
  ]
@@ -967,6 +927,45 @@ var ImageMagnifyGallery = forwardRef((props, ref) => {
967
927
  );
968
928
  });
969
929
  ImageMagnifyGallery.displayName = "ImageMagnifyGallery";
930
+ function useImageSlider({
931
+ length,
932
+ defaultIndex = 0,
933
+ index: controlledIndex,
934
+ onIndexChange,
935
+ loop = true
936
+ }) {
937
+ const [internalIndex, setInternalIndex] = useState(defaultIndex);
938
+ const safeLength = Math.max(1, length);
939
+ const rawIndex = controlledIndex ?? internalIndex;
940
+ const index = Math.max(0, Math.min(safeLength - 1, rawIndex));
941
+ const setIndex = useCallback(
942
+ (next2) => {
943
+ if (length <= 0) return;
944
+ let resolved;
945
+ if (loop) {
946
+ resolved = (next2 % safeLength + safeLength) % safeLength;
947
+ } else {
948
+ resolved = Math.max(0, Math.min(safeLength - 1, next2));
949
+ }
950
+ if (resolved === index) return;
951
+ if (controlledIndex === void 0) {
952
+ setInternalIndex(resolved);
953
+ }
954
+ onIndexChange?.(resolved);
955
+ },
956
+ [length, safeLength, loop, index, controlledIndex, onIndexChange]
957
+ );
958
+ const prev = useCallback(() => setIndex(index - 1), [setIndex, index]);
959
+ const next = useCallback(() => setIndex(index + 1), [setIndex, index]);
960
+ return {
961
+ index,
962
+ canPrev: loop || index > 0,
963
+ canNext: loop || index < safeLength - 1,
964
+ setIndex,
965
+ prev,
966
+ next
967
+ };
968
+ }
970
969
 
971
970
  export { ImageMagnify, ImageMagnifyGallery, useImageMagnify, useImageSlider };
972
971
  //# sourceMappingURL=index.js.map