uix-image-magnify 0.2.2 → 0.2.4

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.
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { CSSProperties, ImgHTMLAttributes } from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes, ReactNode } from 'react';
3
3
 
4
4
  type ZoomType = "hover" | "click" | "double-click" | "hold" | "wheel" | "none";
5
5
  type PanePosition = "left" | "right" | "top" | "bottom";
@@ -7,35 +7,20 @@ type LensShape = "circle" | "square";
7
7
  type MagnifyMode = "lens" | "side-by-side" | "inner" | "follow";
8
8
  type DragMode = "follow" | "pan";
9
9
  interface UseImageMagnifyOptions {
10
- /** Base image URL (required when used standalone). */
11
10
  src?: string;
12
- /** High-resolution image used for the zoomed view. Defaults to `src`. */
13
11
  zoomSrc?: string;
14
- /** Magnification factor (2 = 200%). */
15
12
  zoomScale?: number;
16
- /** Minimum scale for wheel/keyboard zoom. Default `1.5`. */
17
13
  minZoomScale?: number;
18
- /** Maximum scale for wheel/keyboard zoom. Default `5`. */
19
14
  maxZoomScale?: number;
20
- /** Interaction that activates zoom. */
21
15
  zoomType?: ZoomType;
22
- /** How the zoomed content is displayed. */
23
16
  mode?: MagnifyMode;
24
- /** Drag interaction for `inner` mode. */
25
17
  dragMode?: DragMode;
26
- /** Where the side-by-side pane sits relative to the image. */
27
18
  panePosition?: PanePosition;
28
- /** Diameter / side length of the lens in px. */
29
19
  lensSize?: number;
30
- /** Lens shape. */
31
20
  lensShape?: LensShape;
32
- /** Offset (both axes, px) for `mode="follow"`. */
33
21
  followOffset?: number;
34
- /** Disable all interaction. */
35
22
  disabled?: boolean;
36
- /** Preload the high-res image to avoid a flash. */
37
23
  zoomPreload?: boolean;
38
- /** Reset zoom when the pointer leaves the container. */
39
24
  resetOnLeave?: boolean;
40
25
  onZoomStart?: () => void;
41
26
  onZoomEnd?: () => void;
@@ -79,10 +64,42 @@ interface ImageMagnifyProps extends UseImageMagnifyOptions {
79
64
  paneWidth?: number | string;
80
65
  paneHeight?: number | string;
81
66
  lazy?: boolean;
82
- imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style">;
67
+ imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style" | "className" | "onLoad" | "onError">;
83
68
  onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
84
69
  onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
85
70
  }
86
71
  declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
87
72
 
88
- export { type DragMode as D, type ImageMagnifyProps as I, type LensShape as L, type MagnifyMode as M, type PanePosition as P, type UseImageMagnifyOptions as U, type ZoomType as Z, ImageMagnify as a, useImageMagnify as u };
73
+ interface ImageMagnifyGalleryProps extends Omit<ImageMagnifyProps, "src" | "onLoad" | "onError"> {
74
+ images: string[];
75
+ alt: string;
76
+ activeIndex?: number;
77
+ defaultIndex?: number;
78
+ onIndexChange?: (index: number) => void;
79
+ showArrows?: boolean;
80
+ showThumbnails?: boolean;
81
+ showCounter?: boolean;
82
+ loop?: boolean;
83
+ enableKeyboard?: boolean;
84
+ preloadAdjacent?: boolean;
85
+ className?: string;
86
+ style?: CSSProperties;
87
+ viewportClassName?: string;
88
+ viewportStyle?: CSSProperties;
89
+ swiperStyle?: CSSProperties;
90
+ thumbnailContainerClassName?: string;
91
+ thumbnailContainerStyle?: CSSProperties;
92
+ thumbnailClassName?: string;
93
+ thumbnailActiveClassName?: string;
94
+ thumbnailStyle?: CSSProperties;
95
+ thumbnailActiveStyle?: CSSProperties;
96
+ arrowClassName?: string;
97
+ arrowStyle?: CSSProperties;
98
+ thumbnailSize?: number;
99
+ thumbnailGap?: number;
100
+ prevArrow?: ReactNode;
101
+ nextArrow?: ReactNode;
102
+ }
103
+ declare const ImageMagnifyGallery: React.ForwardRefExoticComponent<ImageMagnifyGalleryProps & React.RefAttributes<HTMLDivElement>>;
104
+
105
+ export { type DragMode as D, ImageMagnify as I, type LensShape as L, type MagnifyMode as M, type PanePosition as P, type UseImageMagnifyOptions as U, type ZoomType as Z, ImageMagnifyGallery as a, type ImageMagnifyGalleryProps as b, type ImageMagnifyProps as c, useImageMagnify as u };
package/dist/index.cjs CHANGED
@@ -6,7 +6,6 @@ var react$1 = require('swiper/react');
6
6
  var modules = require('swiper/modules');
7
7
  require('swiper/css');
8
8
  require('swiper/css/navigation');
9
- require('swiper/css/thumbs');
10
9
 
11
10
  // src/components/ImageMagnify.tsx
12
11
  var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
@@ -82,6 +81,15 @@ function useImageMagnify(options = {}) {
82
81
  isZoomedRef.current = z;
83
82
  setIsZoomed(z);
84
83
  if (z) {
84
+ if (modeRef.current === "inner" && dragModeRef.current === "pan") {
85
+ const container = containerRef.current;
86
+ if (container) {
87
+ const rect = container.getBoundingClientRect();
88
+ const s = scaleRef.current;
89
+ txRef.current = rect.width * (1 - s) / 2;
90
+ tyRef.current = rect.height * (1 - s) / 2;
91
+ }
92
+ }
85
93
  onZoomStart?.();
86
94
  } else {
87
95
  if (zoomType === "wheel") {
@@ -288,8 +296,6 @@ function useImageMagnify(options = {}) {
288
296
  setZoomed(!isZoomedRef.current);
289
297
  } else if (zoomType === "click") {
290
298
  setZoomed(!isZoomedRef.current);
291
- } else if (zoomType === "wheel") {
292
- setZoomed(!isZoomedRef.current);
293
299
  }
294
300
  schedule();
295
301
  },
@@ -506,7 +512,8 @@ var ImageMagnify = react.forwardRef(
506
512
  const showLens = (mode === "lens" || mode === "follow") && isZoomed;
507
513
  const showPane = mode === "side-by-side" && isZoomed;
508
514
  const showCursor = mode === "side-by-side" && isZoomed;
509
- const cursorStyle = disabled ? "default" : zoomType === "hover" || zoomType === "hold" || zoomType === "wheel" ? "crosshair" : isZoomed ? "zoom-out" : "zoom-in";
515
+ const isInteractive = !disabled && zoomType !== "none";
516
+ const cursorStyle = !isInteractive ? "default" : zoomType === "hover" || zoomType === "hold" || zoomType === "wheel" ? "crosshair" : isZoomed ? "zoom-out" : "zoom-in";
510
517
  const outerWidth = isMobile ? "100%" : width;
511
518
  return /* @__PURE__ */ jsxRuntime.jsxs(
512
519
  "div",
@@ -526,7 +533,7 @@ var ImageMagnify = react.forwardRef(
526
533
  "div",
527
534
  {
528
535
  ref: containerRef,
529
- tabIndex: 0,
536
+ tabIndex: isInteractive ? 0 : -1,
530
537
  role: "img",
531
538
  "aria-label": alt,
532
539
  "aria-disabled": disabled || void 0,
@@ -693,6 +700,23 @@ var defaultArrowBaseStyle = {
693
700
  backdropFilter: "blur(4px)",
694
701
  WebkitBackdropFilter: "blur(4px)"
695
702
  };
703
+ var defaultThumbBaseStyle = {
704
+ flexShrink: 0,
705
+ padding: 0,
706
+ border: "2px solid #e5e5e5",
707
+ background: "#fff",
708
+ cursor: "pointer",
709
+ overflow: "hidden",
710
+ transition: "border-color 0.15s ease, opacity 0.15s ease",
711
+ borderRadius: 2,
712
+ display: "inline-flex",
713
+ alignItems: "center",
714
+ justifyContent: "center",
715
+ boxSizing: "border-box"
716
+ };
717
+ var defaultThumbActiveStyle = {
718
+ borderColor: "#111"
719
+ };
696
720
  var visuallyHidden = {
697
721
  position: "absolute",
698
722
  width: 1,
@@ -721,6 +745,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
721
745
  style,
722
746
  viewportClassName,
723
747
  viewportStyle,
748
+ swiperStyle,
724
749
  thumbnailContainerClassName,
725
750
  thumbnailContainerStyle,
726
751
  thumbnailClassName,
@@ -737,13 +762,17 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
737
762
  ...magnifyProps
738
763
  } = props;
739
764
  const [swiper, setSwiper] = react.useState(null);
740
- const [thumbsSwiper, setThumbsSwiper] = react.useState(null);
741
- const [localIndex, setLocalIndex] = react.useState(defaultIndex);
765
+ const [localIndex, setLocalIndex] = react.useState(() => {
766
+ const safeLength = Math.max(1, images.length);
767
+ return Math.max(0, Math.min(safeLength - 1, defaultIndex));
768
+ });
742
769
  const prevRef = react.useRef(null);
743
770
  const nextRef = react.useRef(null);
744
771
  const isControlled = controlledActiveIndex !== void 0;
745
- const activeIndex = isControlled ? controlledActiveIndex : localIndex;
772
+ const rawIndex = isControlled ? controlledActiveIndex : localIndex;
773
+ const activeIndex = images.length ? Math.max(0, Math.min(images.length - 1, rawIndex)) : 0;
746
774
  const hasMultiple = images.length > 1;
775
+ const modules$1 = react.useMemo(() => [modules.Navigation, modules.Keyboard, modules.A11y], []);
747
776
  react.useEffect(() => {
748
777
  if (!swiper || swiper.destroyed) return;
749
778
  if (isControlled && swiper.activeIndex !== controlledActiveIndex) {
@@ -753,22 +782,31 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
753
782
  react.useEffect(() => {
754
783
  if (!preloadAdjacent || typeof window === "undefined" || images.length <= 1)
755
784
  return;
756
- const targets = /* @__PURE__ */ new Set([
757
- (activeIndex + 1) % images.length,
758
- (activeIndex - 1 + images.length) % images.length
759
- ]);
785
+ const targets = /* @__PURE__ */ new Set();
786
+ if (loop) {
787
+ targets.add((activeIndex + 1) % images.length);
788
+ targets.add((activeIndex - 1 + images.length) % images.length);
789
+ } else {
790
+ if (activeIndex < images.length - 1) targets.add(activeIndex + 1);
791
+ if (activeIndex > 0) targets.add(activeIndex - 1);
792
+ }
760
793
  targets.forEach((i) => {
761
- if (images[i]) {
794
+ const src = images[i];
795
+ if (src) {
762
796
  const img = new Image();
763
- img.src = images[i];
797
+ img.src = src;
764
798
  }
765
799
  });
766
- }, [activeIndex, images, preloadAdjacent]);
767
- const modules$1 = react.useMemo(() => {
768
- const m = [modules.Navigation, modules.Thumbs];
769
- if (enableKeyboard) m.push(modules.Keyboard);
770
- return m;
771
- }, [enableKeyboard]);
800
+ }, [activeIndex, images, preloadAdjacent, loop]);
801
+ const handleSlideChange = (s) => {
802
+ const idx = s.activeIndex;
803
+ if (!isControlled) setLocalIndex(idx);
804
+ onIndexChange?.(idx);
805
+ };
806
+ const handleThumbClick = (idx) => {
807
+ if (!swiper || swiper.destroyed) return;
808
+ swiper.slideTo(idx);
809
+ };
772
810
  if (images.length === 0) {
773
811
  return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, className, style });
774
812
  }
@@ -794,36 +832,29 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
794
832
  modules: modules$1,
795
833
  onSwiper: setSwiper,
796
834
  initialSlide: isControlled ? controlledActiveIndex : defaultIndex,
797
- onSlideChange: (s) => {
798
- if (!isControlled) setLocalIndex(s.activeIndex);
799
- onIndexChange?.(s.activeIndex);
800
- },
835
+ onSlideChange: handleSlideChange,
801
836
  onBeforeInit: (s) => {
802
837
  if (!showArrows || !hasMultiple) return;
803
- if (s.params.navigation && typeof s.params.navigation === "object") {
804
- s.params.navigation.prevEl = prevRef.current;
805
- s.params.navigation.nextEl = nextRef.current;
838
+ const nav = s.params.navigation;
839
+ if (nav && typeof nav === "object") {
840
+ nav.prevEl = prevRef.current;
841
+ nav.nextEl = nextRef.current;
806
842
  }
807
843
  },
808
844
  navigation: showArrows && hasMultiple ? {} : false,
809
- keyboard: enableKeyboard ? { enabled: true } : void 0,
810
- thumbs: {
811
- swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
812
- },
845
+ keyboard: enableKeyboard ? { enabled: true } : false,
813
846
  loop: loop && hasMultiple,
814
847
  autoHeight: true,
815
848
  spaceBetween: 10,
816
- style: {
817
- width: "100%",
818
- "--swiper-navigation-size": "20px",
819
- ...arrowStyle
820
- },
849
+ observer: true,
850
+ observeParents: true,
851
+ style: { width: "100%", ...swiperStyle },
821
852
  children: images.map((src, idx) => /* @__PURE__ */ jsxRuntime.jsx(react$1.SwiperSlide, { children: /* @__PURE__ */ jsxRuntime.jsx(
822
853
  ImageMagnify,
823
854
  {
824
855
  ...magnifyProps,
825
856
  src,
826
- alt,
857
+ alt: `${alt} \u2014 image ${idx + 1} of ${images.length}`,
827
858
  width: "100%"
828
859
  }
829
860
  ) }, idx))
@@ -836,7 +867,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
836
867
  ref: prevRef,
837
868
  type: "button",
838
869
  className: arrowClassName,
839
- style: { ...defaultArrowBaseStyle, left: 12 },
870
+ style: { ...defaultArrowBaseStyle, left: 12, ...arrowStyle },
840
871
  "aria-label": "Previous image",
841
872
  children: prevArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon, {})
842
873
  }
@@ -847,7 +878,7 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
847
878
  ref: nextRef,
848
879
  type: "button",
849
880
  className: arrowClassName,
850
- style: { ...defaultArrowBaseStyle, right: 12 },
881
+ style: { ...defaultArrowBaseStyle, right: 12, ...arrowStyle },
851
882
  "aria-label": "Next image",
852
883
  children: nextArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowRightIcon, {})
853
884
  }
@@ -887,41 +918,62 @@ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
887
918
  }
888
919
  ),
889
920
  showThumbnails && hasMultiple && /* @__PURE__ */ jsxRuntime.jsx(
890
- react$1.Swiper,
921
+ "div",
891
922
  {
892
- modules: modules$1,
893
- onSwiper: setSwiper,
894
- initialSlide: isControlled ? controlledActiveIndex : defaultIndex,
895
- onSlideChange: (s) => {
896
- if (!isControlled) setLocalIndex(s.activeIndex);
897
- onIndexChange?.(s.activeIndex);
898
- },
899
- onBeforeInit: (s) => {
900
- if (!showArrows || !hasMultiple) return;
901
- const nav = s.params.navigation;
902
- if (nav && typeof nav === "object") {
903
- nav.prevEl = prevRef.current;
904
- nav.nextEl = nextRef.current;
905
- }
906
- },
907
- navigation: showArrows && hasMultiple ? {} : false,
908
- keyboard: enableKeyboard ? { enabled: true } : void 0,
909
- thumbs: {
910
- swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null
923
+ className: thumbnailContainerClassName,
924
+ role: "group",
925
+ "aria-label": "Image thumbnails",
926
+ style: {
927
+ display: "flex",
928
+ gap: thumbnailGap,
929
+ marginTop: 12,
930
+ overflowX: "auto",
931
+ overflowY: "hidden",
932
+ paddingBottom: 4,
933
+ scrollbarWidth: "thin",
934
+ ...thumbnailContainerStyle
911
935
  },
912
- loop: loop && hasMultiple,
913
- autoHeight: true,
914
- spaceBetween: 10,
915
- style: { width: "100%" },
916
- children: images.map((src, idx) => /* @__PURE__ */ jsxRuntime.jsx(react$1.SwiperSlide, { children: /* @__PURE__ */ jsxRuntime.jsx(
917
- ImageMagnify,
918
- {
919
- ...magnifyProps,
920
- src,
921
- alt,
922
- width: "100%"
923
- }
924
- ) }, idx))
936
+ children: images.map((src, idx) => {
937
+ const isActive = idx === activeIndex;
938
+ const mergedClassName = [
939
+ thumbnailClassName,
940
+ isActive ? thumbnailActiveClassName : void 0
941
+ ].filter(Boolean).join(" ") || void 0;
942
+ return /* @__PURE__ */ jsxRuntime.jsx(
943
+ "button",
944
+ {
945
+ type: "button",
946
+ onClick: () => handleThumbClick(idx),
947
+ className: mergedClassName,
948
+ style: {
949
+ ...defaultThumbBaseStyle,
950
+ width: thumbnailSize,
951
+ height: thumbnailSize,
952
+ ...thumbnailStyle,
953
+ ...isActive ? { ...defaultThumbActiveStyle, ...thumbnailActiveStyle } : void 0
954
+ },
955
+ "aria-label": `View image ${idx + 1}`,
956
+ "aria-current": isActive ? "true" : void 0,
957
+ children: /* @__PURE__ */ jsxRuntime.jsx(
958
+ "img",
959
+ {
960
+ src,
961
+ alt: "",
962
+ loading: "lazy",
963
+ draggable: false,
964
+ style: {
965
+ width: "100%",
966
+ height: "100%",
967
+ objectFit: "cover",
968
+ display: "block",
969
+ pointerEvents: "none"
970
+ }
971
+ }
972
+ )
973
+ },
974
+ idx
975
+ );
976
+ })
925
977
  }
926
978
  )
927
979
  ]
@@ -936,7 +988,10 @@ function useImageSlider({
936
988
  onIndexChange,
937
989
  loop = true
938
990
  }) {
939
- const [internalIndex, setInternalIndex] = react.useState(defaultIndex);
991
+ const [internalIndex, setInternalIndex] = react.useState(() => {
992
+ const safeLength2 = Math.max(1, length);
993
+ return Math.max(0, Math.min(safeLength2 - 1, defaultIndex));
994
+ });
940
995
  const safeLength = Math.max(1, length);
941
996
  const rawIndex = controlledIndex ?? internalIndex;
942
997
  const index = Math.max(0, Math.min(safeLength - 1, rawIndex));