uix-image-magnify 0.1.4 → 0.2.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.
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes } from 'react';
3
+
4
+ type ZoomType = "hover" | "click" | "double-click" | "hold" | "wheel" | "none";
5
+ type PanePosition = "left" | "right" | "top" | "bottom";
6
+ type LensShape = "circle" | "square";
7
+ type MagnifyMode = "lens" | "side-by-side" | "inner" | "follow";
8
+ type DragMode = "follow" | "pan";
9
+ interface UseImageMagnifyOptions {
10
+ /** Base image URL (required when used standalone). */
11
+ src?: string;
12
+ /** High-resolution image used for the zoomed view. Defaults to `src`. */
13
+ zoomSrc?: string;
14
+ /** Magnification factor (2 = 200%). */
15
+ zoomScale?: number;
16
+ /** Minimum scale for wheel/keyboard zoom. Default `1.5`. */
17
+ minZoomScale?: number;
18
+ /** Maximum scale for wheel/keyboard zoom. Default `5`. */
19
+ maxZoomScale?: number;
20
+ /** Interaction that activates zoom. */
21
+ zoomType?: ZoomType;
22
+ /** How the zoomed content is displayed. */
23
+ mode?: MagnifyMode;
24
+ /** Drag interaction for `inner` mode. */
25
+ dragMode?: DragMode;
26
+ /** Where the side-by-side pane sits relative to the image. */
27
+ panePosition?: PanePosition;
28
+ /** Diameter / side length of the lens in px. */
29
+ lensSize?: number;
30
+ /** Lens shape. */
31
+ lensShape?: LensShape;
32
+ /** Offset (both axes, px) for `mode="follow"`. */
33
+ followOffset?: number;
34
+ /** Disable all interaction. */
35
+ disabled?: boolean;
36
+ /** Preload the high-res image to avoid a flash. */
37
+ zoomPreload?: boolean;
38
+ /** Reset zoom when the pointer leaves the container. */
39
+ resetOnLeave?: boolean;
40
+ onZoomStart?: () => void;
41
+ onZoomEnd?: () => void;
42
+ onZoomChange?: (scale: number) => void;
43
+ }
44
+ declare function useImageMagnify(options?: UseImageMagnifyOptions): {
45
+ isZoomed: boolean;
46
+ zoomScale: number;
47
+ bgImage: string | undefined;
48
+ containerRef: React.RefObject<HTMLDivElement>;
49
+ imageRef: React.RefObject<HTMLImageElement>;
50
+ lensRef: React.RefObject<HTMLDivElement>;
51
+ paneRef: React.RefObject<HTMLDivElement>;
52
+ cursorRef: React.RefObject<HTMLDivElement>;
53
+ handlers: {
54
+ onPointerEnter: (e: React.PointerEvent) => void;
55
+ onPointerLeave: (e: React.PointerEvent) => void;
56
+ onPointerDown: (e: React.PointerEvent) => void;
57
+ onPointerMove: (e: React.PointerEvent) => void;
58
+ onPointerUp: (e: React.PointerEvent) => void;
59
+ onPointerCancel: () => void;
60
+ onDoubleClick: (e: React.MouseEvent) => void;
61
+ onKeyDown: (e: React.KeyboardEvent) => void;
62
+ onFocus: () => void;
63
+ onBlur: () => void;
64
+ };
65
+ };
66
+
67
+ interface ImageMagnifyProps extends UseImageMagnifyOptions {
68
+ src: string;
69
+ alt: string;
70
+ /** Rendered width of the image (height follows aspect ratio). */
71
+ width?: number | string;
72
+ className?: string;
73
+ style?: CSSProperties;
74
+ imageClassName?: string;
75
+ imageStyle?: CSSProperties;
76
+ lensClassName?: string;
77
+ lensStyle?: CSSProperties;
78
+ paneClassName?: string;
79
+ paneStyle?: CSSProperties;
80
+ paneWidth?: number;
81
+ paneHeight?: number;
82
+ lazy?: boolean;
83
+ /** Extra attributes forwarded to the underlying <img>. */
84
+ imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style">;
85
+ onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
86
+ onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
87
+ }
88
+ declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
89
+
90
+ 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 };
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+ import { CSSProperties, ImgHTMLAttributes } from 'react';
3
+
4
+ type ZoomType = "hover" | "click" | "double-click" | "hold" | "wheel" | "none";
5
+ type PanePosition = "left" | "right" | "top" | "bottom";
6
+ type LensShape = "circle" | "square";
7
+ type MagnifyMode = "lens" | "side-by-side" | "inner" | "follow";
8
+ type DragMode = "follow" | "pan";
9
+ interface UseImageMagnifyOptions {
10
+ /** Base image URL (required when used standalone). */
11
+ src?: string;
12
+ /** High-resolution image used for the zoomed view. Defaults to `src`. */
13
+ zoomSrc?: string;
14
+ /** Magnification factor (2 = 200%). */
15
+ zoomScale?: number;
16
+ /** Minimum scale for wheel/keyboard zoom. Default `1.5`. */
17
+ minZoomScale?: number;
18
+ /** Maximum scale for wheel/keyboard zoom. Default `5`. */
19
+ maxZoomScale?: number;
20
+ /** Interaction that activates zoom. */
21
+ zoomType?: ZoomType;
22
+ /** How the zoomed content is displayed. */
23
+ mode?: MagnifyMode;
24
+ /** Drag interaction for `inner` mode. */
25
+ dragMode?: DragMode;
26
+ /** Where the side-by-side pane sits relative to the image. */
27
+ panePosition?: PanePosition;
28
+ /** Diameter / side length of the lens in px. */
29
+ lensSize?: number;
30
+ /** Lens shape. */
31
+ lensShape?: LensShape;
32
+ /** Offset (both axes, px) for `mode="follow"`. */
33
+ followOffset?: number;
34
+ /** Disable all interaction. */
35
+ disabled?: boolean;
36
+ /** Preload the high-res image to avoid a flash. */
37
+ zoomPreload?: boolean;
38
+ /** Reset zoom when the pointer leaves the container. */
39
+ resetOnLeave?: boolean;
40
+ onZoomStart?: () => void;
41
+ onZoomEnd?: () => void;
42
+ onZoomChange?: (scale: number) => void;
43
+ }
44
+ declare function useImageMagnify(options?: UseImageMagnifyOptions): {
45
+ isZoomed: boolean;
46
+ zoomScale: number;
47
+ bgImage: string | undefined;
48
+ containerRef: React.RefObject<HTMLDivElement>;
49
+ imageRef: React.RefObject<HTMLImageElement>;
50
+ lensRef: React.RefObject<HTMLDivElement>;
51
+ paneRef: React.RefObject<HTMLDivElement>;
52
+ cursorRef: React.RefObject<HTMLDivElement>;
53
+ handlers: {
54
+ onPointerEnter: (e: React.PointerEvent) => void;
55
+ onPointerLeave: (e: React.PointerEvent) => void;
56
+ onPointerDown: (e: React.PointerEvent) => void;
57
+ onPointerMove: (e: React.PointerEvent) => void;
58
+ onPointerUp: (e: React.PointerEvent) => void;
59
+ onPointerCancel: () => void;
60
+ onDoubleClick: (e: React.MouseEvent) => void;
61
+ onKeyDown: (e: React.KeyboardEvent) => void;
62
+ onFocus: () => void;
63
+ onBlur: () => void;
64
+ };
65
+ };
66
+
67
+ interface ImageMagnifyProps extends UseImageMagnifyOptions {
68
+ src: string;
69
+ alt: string;
70
+ /** Rendered width of the image (height follows aspect ratio). */
71
+ width?: number | string;
72
+ className?: string;
73
+ style?: CSSProperties;
74
+ imageClassName?: string;
75
+ imageStyle?: CSSProperties;
76
+ lensClassName?: string;
77
+ lensStyle?: CSSProperties;
78
+ paneClassName?: string;
79
+ paneStyle?: CSSProperties;
80
+ paneWidth?: number;
81
+ paneHeight?: number;
82
+ lazy?: boolean;
83
+ /** Extra attributes forwarded to the underlying <img>. */
84
+ imageProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "style">;
85
+ onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
86
+ onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
87
+ }
88
+ declare const ImageMagnify: React.ForwardRefExoticComponent<ImageMagnifyProps & React.RefAttributes<HTMLDivElement>>;
89
+
90
+ 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 };
package/dist/index.cjs CHANGED
@@ -610,8 +610,369 @@ var ImageMagnify = react.forwardRef(
610
610
  }
611
611
  );
612
612
  ImageMagnify.displayName = "ImageMagnify";
613
+ function useImageSlider({
614
+ length,
615
+ defaultIndex = 0,
616
+ index: controlledIndex,
617
+ onIndexChange,
618
+ loop = true
619
+ }) {
620
+ const [internalIndex, setInternalIndex] = react.useState(defaultIndex);
621
+ const safeLength = Math.max(1, length);
622
+ const rawIndex = controlledIndex ?? internalIndex;
623
+ const index = Math.max(0, Math.min(safeLength - 1, rawIndex));
624
+ const setIndex = react.useCallback(
625
+ (next2) => {
626
+ if (length <= 0) return;
627
+ let resolved;
628
+ if (loop) {
629
+ resolved = (next2 % safeLength + safeLength) % safeLength;
630
+ } else {
631
+ resolved = Math.max(0, Math.min(safeLength - 1, next2));
632
+ }
633
+ if (resolved === index) return;
634
+ if (controlledIndex === void 0) {
635
+ setInternalIndex(resolved);
636
+ }
637
+ onIndexChange?.(resolved);
638
+ },
639
+ [length, safeLength, loop, index, controlledIndex, onIndexChange]
640
+ );
641
+ const prev = react.useCallback(() => setIndex(index - 1), [setIndex, index]);
642
+ const next = react.useCallback(() => setIndex(index + 1), [setIndex, index]);
643
+ return {
644
+ index,
645
+ canPrev: loop || index > 0,
646
+ canNext: loop || index < safeLength - 1,
647
+ setIndex,
648
+ prev,
649
+ next
650
+ };
651
+ }
652
+ var ArrowLeftIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
653
+ "svg",
654
+ {
655
+ viewBox: "0 0 24 24",
656
+ width: "20",
657
+ height: "20",
658
+ fill: "none",
659
+ stroke: "currentColor",
660
+ strokeWidth: "2",
661
+ strokeLinecap: "round",
662
+ strokeLinejoin: "round",
663
+ "aria-hidden": "true",
664
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 18 9 12 15 6" })
665
+ }
666
+ );
667
+ var ArrowRightIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
668
+ "svg",
669
+ {
670
+ viewBox: "0 0 24 24",
671
+ width: "20",
672
+ height: "20",
673
+ fill: "none",
674
+ stroke: "currentColor",
675
+ strokeWidth: "2",
676
+ strokeLinecap: "round",
677
+ strokeLinejoin: "round",
678
+ "aria-hidden": "true",
679
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" })
680
+ }
681
+ );
682
+ var defaultArrowBaseStyle = {
683
+ position: "absolute",
684
+ top: "50%",
685
+ transform: "translateY(-50%)",
686
+ width: 40,
687
+ height: 40,
688
+ display: "inline-flex",
689
+ alignItems: "center",
690
+ justifyContent: "center",
691
+ background: "rgba(255,255,255,0.85)",
692
+ border: "1px solid rgba(0,0,0,0.12)",
693
+ borderRadius: 9999,
694
+ boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
695
+ cursor: "pointer",
696
+ zIndex: 10,
697
+ color: "#111",
698
+ padding: 0,
699
+ transition: "background 0.15s ease",
700
+ backdropFilter: "blur(4px)",
701
+ WebkitBackdropFilter: "blur(4px)"
702
+ };
703
+ var defaultThumbStripStyle = {
704
+ display: "flex",
705
+ overflowX: "auto",
706
+ overflowY: "hidden",
707
+ paddingBottom: 4,
708
+ scrollbarWidth: "thin",
709
+ WebkitOverflowScrolling: "touch"
710
+ };
711
+ var defaultThumbBaseStyle = {
712
+ flexShrink: 0,
713
+ padding: 0,
714
+ border: "2px solid #e5e5e5",
715
+ background: "#fff",
716
+ cursor: "pointer",
717
+ overflow: "hidden",
718
+ transition: "border-color 0.15s ease",
719
+ borderRadius: 2,
720
+ display: "inline-flex",
721
+ alignItems: "center",
722
+ justifyContent: "center"
723
+ };
724
+ var defaultThumbActiveStyle = {
725
+ borderColor: "#111"
726
+ };
727
+ var visuallyHidden = {
728
+ position: "absolute",
729
+ width: 1,
730
+ height: 1,
731
+ padding: 0,
732
+ margin: -1,
733
+ overflow: "hidden",
734
+ clip: "rect(0,0,0,0)",
735
+ whiteSpace: "nowrap",
736
+ border: 0
737
+ };
738
+ var ImageMagnifyGallery = react.forwardRef((props, ref) => {
739
+ const {
740
+ images,
741
+ alt,
742
+ activeIndex: controlledActiveIndex,
743
+ defaultIndex = 0,
744
+ onIndexChange,
745
+ showArrows = true,
746
+ showThumbnails = true,
747
+ showCounter = false,
748
+ loop = true,
749
+ enableKeyboard = true,
750
+ preloadAdjacent = true,
751
+ className,
752
+ style,
753
+ viewportClassName,
754
+ viewportStyle,
755
+ thumbnailContainerClassName,
756
+ thumbnailContainerStyle,
757
+ thumbnailClassName,
758
+ thumbnailActiveClassName,
759
+ thumbnailStyle,
760
+ thumbnailActiveStyle,
761
+ arrowClassName,
762
+ arrowStyle,
763
+ thumbnailSize = 80,
764
+ thumbnailGap = 8,
765
+ prevArrow,
766
+ nextArrow,
767
+ ...magnifyProps
768
+ } = props;
769
+ const { index, canPrev, canNext, setIndex, prev, next } = useImageSlider({
770
+ length: images.length,
771
+ defaultIndex,
772
+ index: controlledActiveIndex,
773
+ onIndexChange,
774
+ loop
775
+ });
776
+ const currentSrc = images[index] ?? "";
777
+ const thumbStripRef = react.useRef(null);
778
+ const [hoveredThumb, setHoveredThumb] = react.useState(null);
779
+ react.useEffect(() => {
780
+ if (!preloadAdjacent || typeof window === "undefined") return;
781
+ if (images.length <= 1) return;
782
+ const targets = /* @__PURE__ */ new Set();
783
+ targets.add((index + 1) % images.length);
784
+ targets.add((index - 1 + images.length) % images.length);
785
+ targets.forEach((i) => {
786
+ if (images[i]) {
787
+ const img = new Image();
788
+ img.src = images[i];
789
+ }
790
+ });
791
+ }, [index, images, preloadAdjacent]);
792
+ react.useEffect(() => {
793
+ const strip = thumbStripRef.current;
794
+ if (!strip) return;
795
+ const active = strip.children[index];
796
+ if (!active) return;
797
+ const sr = strip.getBoundingClientRect();
798
+ const ar = active.getBoundingClientRect();
799
+ if (ar.left < sr.left) {
800
+ strip.scrollBy({ left: ar.left - sr.left, behavior: "smooth" });
801
+ } else if (ar.right > sr.right) {
802
+ strip.scrollBy({ left: ar.right - sr.right, behavior: "smooth" });
803
+ }
804
+ }, [index]);
805
+ const handleKeyDown = react.useCallback(
806
+ (e) => {
807
+ if (!enableKeyboard) return;
808
+ if (e.key === "ArrowLeft") {
809
+ if (!canPrev) return;
810
+ e.preventDefault();
811
+ prev();
812
+ } else if (e.key === "ArrowRight") {
813
+ if (!canNext) return;
814
+ e.preventDefault();
815
+ next();
816
+ }
817
+ },
818
+ [enableKeyboard, canPrev, canNext, prev, next]
819
+ );
820
+ const hasMultiple = images.length > 1;
821
+ const liveText = react.useMemo(
822
+ () => `Image ${index + 1} of ${images.length}`,
823
+ [index, images.length]
824
+ );
825
+ return /* @__PURE__ */ jsxRuntime.jsxs(
826
+ "div",
827
+ {
828
+ ref,
829
+ className,
830
+ style,
831
+ role: "region",
832
+ "aria-roledescription": "carousel",
833
+ "aria-label": `${alt} \u2014 image gallery`,
834
+ children: [
835
+ /* @__PURE__ */ jsxRuntime.jsxs(
836
+ "div",
837
+ {
838
+ className: viewportClassName,
839
+ style: {
840
+ position: "relative",
841
+ width: "fit-content",
842
+ maxWidth: "100%",
843
+ ...viewportStyle
844
+ },
845
+ tabIndex: enableKeyboard && hasMultiple ? 0 : void 0,
846
+ onKeyDown: handleKeyDown,
847
+ children: [
848
+ /* @__PURE__ */ jsxRuntime.jsx(ImageMagnify, { ...magnifyProps, src: currentSrc, alt }),
849
+ showArrows && hasMultiple && canPrev && /* @__PURE__ */ jsxRuntime.jsx(
850
+ "button",
851
+ {
852
+ type: "button",
853
+ onClick: prev,
854
+ className: arrowClassName,
855
+ style: {
856
+ ...defaultArrowBaseStyle,
857
+ left: 12,
858
+ ...arrowStyle
859
+ },
860
+ "aria-label": "Previous image",
861
+ children: prevArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowLeftIcon, {})
862
+ }
863
+ ),
864
+ showArrows && hasMultiple && canNext && /* @__PURE__ */ jsxRuntime.jsx(
865
+ "button",
866
+ {
867
+ type: "button",
868
+ onClick: next,
869
+ className: arrowClassName,
870
+ style: {
871
+ ...defaultArrowBaseStyle,
872
+ right: 12,
873
+ ...arrowStyle
874
+ },
875
+ "aria-label": "Next image",
876
+ children: nextArrow ?? /* @__PURE__ */ jsxRuntime.jsx(ArrowRightIcon, {})
877
+ }
878
+ ),
879
+ showCounter && hasMultiple && /* @__PURE__ */ jsxRuntime.jsxs(
880
+ "div",
881
+ {
882
+ "aria-hidden": "true",
883
+ style: {
884
+ position: "absolute",
885
+ bottom: 12,
886
+ right: 12,
887
+ background: "rgba(0,0,0,0.6)",
888
+ color: "#fff",
889
+ fontSize: 12,
890
+ fontWeight: 600,
891
+ padding: "4px 8px",
892
+ borderRadius: 4,
893
+ zIndex: 10,
894
+ pointerEvents: "none"
895
+ },
896
+ children: [
897
+ index + 1,
898
+ " / ",
899
+ images.length
900
+ ]
901
+ }
902
+ ),
903
+ /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-live": "polite", "aria-atomic": "true", style: visuallyHidden, children: liveText })
904
+ ]
905
+ }
906
+ ),
907
+ showThumbnails && hasMultiple && /* @__PURE__ */ jsxRuntime.jsx(
908
+ "div",
909
+ {
910
+ ref: thumbStripRef,
911
+ className: thumbnailContainerClassName,
912
+ role: "tablist",
913
+ "aria-label": "Choose image",
914
+ style: {
915
+ ...defaultThumbStripStyle,
916
+ gap: thumbnailGap,
917
+ marginTop: 12,
918
+ ...thumbnailContainerStyle
919
+ },
920
+ children: images.map((img, idx) => {
921
+ const isActive = idx === index;
922
+ const isHovered = idx === hoveredThumb;
923
+ const cls = isActive ? thumbnailActiveClassName : thumbnailClassName;
924
+ return /* @__PURE__ */ jsxRuntime.jsx(
925
+ "button",
926
+ {
927
+ type: "button",
928
+ role: "tab",
929
+ onClick: () => setIndex(idx),
930
+ onMouseEnter: () => setHoveredThumb(idx),
931
+ onMouseLeave: () => setHoveredThumb(null),
932
+ className: cls,
933
+ style: {
934
+ ...defaultThumbBaseStyle,
935
+ width: thumbnailSize,
936
+ height: thumbnailSize,
937
+ ...thumbnailStyle,
938
+ ...isActive ? defaultThumbActiveStyle : void 0,
939
+ ...isActive ? thumbnailActiveStyle : void 0,
940
+ ...isHovered && !isActive ? { borderColor: "#999" } : void 0
941
+ },
942
+ "aria-label": `View image ${idx + 1} of ${images.length}`,
943
+ "aria-selected": isActive,
944
+ children: /* @__PURE__ */ jsxRuntime.jsx(
945
+ "img",
946
+ {
947
+ src: img,
948
+ alt: "",
949
+ loading: "lazy",
950
+ draggable: false,
951
+ style: {
952
+ width: "100%",
953
+ height: "100%",
954
+ objectFit: "contain",
955
+ display: "block",
956
+ pointerEvents: "none",
957
+ userSelect: "none"
958
+ }
959
+ }
960
+ )
961
+ },
962
+ idx
963
+ );
964
+ })
965
+ }
966
+ )
967
+ ]
968
+ }
969
+ );
970
+ });
971
+ ImageMagnifyGallery.displayName = "ImageMagnifyGallery";
613
972
 
614
973
  exports.ImageMagnify = ImageMagnify;
974
+ exports.ImageMagnifyGallery = ImageMagnifyGallery;
615
975
  exports.useImageMagnify = useImageMagnify;
976
+ exports.useImageSlider = useImageSlider;
616
977
  //# sourceMappingURL=index.cjs.map
617
978
  //# sourceMappingURL=index.cjs.map