@dmsi/wedgekit-react 0.0.369 → 0.0.371
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/{chunk-RLLQRVM7.js → chunk-2H35FETR.js} +18 -10
- package/dist/chunk-2IKT6IHB.js +190 -0
- package/dist/chunk-4UNWXB4A.js +89 -0
- package/dist/chunk-5IFPG6TS.js +17 -0
- package/dist/{chunk-6GAYJCFE.js → chunk-6DPFKSCT.js} +1 -1
- package/dist/{chunk-ZFOANBWG.js → chunk-AG43RS4Q.js} +2 -1
- package/dist/chunk-AJ5M6MVX.js +7 -0
- package/dist/chunk-AT4AWD6B.js +44 -0
- package/dist/chunk-BQNPOGD5.js +105 -0
- package/dist/chunk-CQFPNZTN.js +172 -0
- package/dist/chunk-EJSPFQCY.js +29 -0
- package/dist/chunk-ER6RCOH3.js +97 -0
- package/dist/{chunk-4VER5OEU.js → chunk-FBE2HGEF.js} +35 -11
- package/dist/chunk-HPQWEZJL.js +45 -0
- package/dist/{chunk-URCLLHO5.js → chunk-IBX6DVHU.js} +376 -102
- package/dist/{chunk-I3WFZOFY.js → chunk-J5V2JRIK.js} +1 -1
- package/dist/chunk-JGJUVJKD.js +283 -0
- package/dist/chunk-KEMCFN4U.js +78 -0
- package/dist/chunk-M6TSTDNZ.js +22 -0
- package/dist/chunk-M7INAUAJ.js +140 -0
- package/dist/chunk-MBZ55T2D.js +51 -0
- package/dist/chunk-N6PNLLNS.js +77 -0
- package/dist/{chunk-ZA5E7ZYM.js → chunk-NXGUDYRR.js} +1 -1
- package/dist/chunk-P36QKH26.js +143 -0
- package/dist/chunk-PTRZHGHA.js +89 -0
- package/dist/chunk-QVWYTQKL.js +29 -0
- package/dist/{chunk-6CPGOW6J.js → chunk-T36HX6QY.js} +6 -4
- package/dist/chunk-U6PUOGG4.js +114 -0
- package/dist/{chunk-NQXZBWDZ.js → chunk-V6U7LU6M.js} +15 -6
- package/dist/chunk-VJVY6NPF.js +32 -0
- package/dist/chunk-VVXPGI2P.js +61 -0
- package/dist/{chunk-ARQBSR3I.js → chunk-YCKRVNJ3.js} +4 -4
- package/dist/chunk-YYHQLQDQ.js +68 -0
- package/dist/components/Accordion.cjs +47 -14
- package/dist/components/Accordion.js +2 -2
- package/dist/components/CalendarRange.cjs +700 -46
- package/dist/components/CalendarRange.css +186 -3
- package/dist/components/CalendarRange.js +43 -11
- package/dist/components/CompactImagesPreview.cjs +485 -0
- package/dist/components/CompactImagesPreview.js +13 -0
- package/dist/components/ContentTabs.cjs +3 -2
- package/dist/components/ContentTabs.js +3 -2
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/ColumnSelectorMenuOption.cjs +4687 -0
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/ColumnSelectorMenuOption.css +5051 -0
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/ColumnSelectorMenuOption.js +62 -0
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/index.cjs +4687 -0
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/index.css +5051 -0
- package/dist/components/DataGrid/ColumnSelectorHeaderCell/index.js +62 -0
- package/dist/components/DataGrid/PinnedColumns.cjs +4687 -0
- package/dist/components/DataGrid/PinnedColumns.css +5051 -0
- package/dist/components/DataGrid/PinnedColumns.js +62 -0
- package/dist/components/DataGrid/TableBody/LoadingCell.cjs +4689 -0
- package/dist/components/DataGrid/TableBody/LoadingCell.css +5051 -0
- package/dist/components/DataGrid/TableBody/LoadingCell.js +62 -0
- package/dist/components/DataGrid/TableBody/TableBodyRow.cjs +4689 -0
- package/dist/components/DataGrid/TableBody/TableBodyRow.css +5051 -0
- package/dist/components/DataGrid/TableBody/TableBodyRow.js +62 -0
- package/dist/components/DataGrid/TableBody/index.cjs +4689 -0
- package/dist/components/DataGrid/TableBody/index.css +5051 -0
- package/dist/components/DataGrid/TableBody/index.js +62 -0
- package/dist/components/DataGrid/index.cjs +4692 -0
- package/dist/components/DataGrid/index.css +5051 -0
- package/dist/components/DataGrid/index.js +65 -0
- package/dist/components/DataGrid/utils.cjs +4687 -0
- package/dist/components/DataGrid/utils.css +5051 -0
- package/dist/components/DataGrid/utils.js +62 -0
- package/dist/components/DataGridCell.js +6 -6
- package/dist/components/DateInput.cjs +721 -67
- package/dist/components/DateInput.css +186 -3
- package/dist/components/DateInput.js +45 -13
- package/dist/components/DateRangeInput.cjs +721 -67
- package/dist/components/DateRangeInput.css +186 -3
- package/dist/components/DateRangeInput.js +45 -13
- package/dist/components/FilterGroup.js +3 -3
- package/dist/components/Grid.cjs +3 -1
- package/dist/components/Grid.js +3 -92
- package/dist/components/ImagePlaceholder.cjs +65 -0
- package/dist/components/ImagePlaceholder.js +7 -0
- package/dist/components/Input.js +2 -2
- package/dist/components/MenuOption.js +2 -2
- package/dist/components/MobileDataGrid/ColumnList.cjs +845 -0
- package/dist/components/MobileDataGrid/ColumnList.js +17 -0
- package/dist/components/MobileDataGrid/ColumnSelector/index.cjs +4797 -0
- package/dist/components/MobileDataGrid/ColumnSelector/index.css +5051 -0
- package/dist/components/MobileDataGrid/ColumnSelector/index.js +62 -0
- package/dist/components/MobileDataGrid/GridContextProvider/GridContext.cjs +31 -0
- package/dist/components/MobileDataGrid/GridContextProvider/GridContext.js +7 -0
- package/dist/components/MobileDataGrid/GridContextProvider/index.cjs +177 -0
- package/dist/components/MobileDataGrid/GridContextProvider/index.js +8 -0
- package/dist/components/MobileDataGrid/MobileDataGridCard/MobileDataGridColumn.cjs +269 -0
- package/dist/components/MobileDataGrid/MobileDataGridCard/MobileDataGridColumn.js +9 -0
- package/dist/components/MobileDataGrid/MobileDataGridCard/index.cjs +790 -0
- package/dist/components/MobileDataGrid/MobileDataGridCard/index.js +16 -0
- package/dist/components/MobileDataGrid/MobileDataGridHeader.cjs +5059 -0
- package/dist/components/MobileDataGrid/MobileDataGridHeader.css +5051 -0
- package/dist/components/MobileDataGrid/MobileDataGridHeader.js +62 -0
- package/dist/components/MobileDataGrid/RowDetailModalProvider/ModalContent.cjs +509 -0
- package/dist/components/MobileDataGrid/RowDetailModalProvider/ModalContent.js +13 -0
- package/dist/components/MobileDataGrid/RowDetailModalProvider/index.cjs +1261 -0
- package/dist/components/MobileDataGrid/RowDetailModalProvider/index.js +27 -0
- package/dist/components/MobileDataGrid/index.cjs +5521 -0
- package/dist/components/MobileDataGrid/index.css +5051 -0
- package/dist/components/MobileDataGrid/index.js +62 -0
- package/dist/components/Modal.cjs +24 -13
- package/dist/components/Modal.js +3 -3
- package/dist/components/ModalHeader.cjs +6 -4
- package/dist/components/ModalHeader.js +1 -1
- package/dist/components/ModalScrim.cjs +2 -1
- package/dist/components/ModalScrim.js +1 -1
- package/dist/components/NestedMenu.js +4 -4
- package/dist/components/Notification.cjs +15 -6
- package/dist/components/Notification.js +1 -1
- package/dist/components/PDFViewer/DownloadIcon.cjs +394 -0
- package/dist/components/PDFViewer/DownloadIcon.js +10 -0
- package/dist/components/PDFViewer/PDFElement.cjs +515 -0
- package/dist/components/PDFViewer/PDFElement.js +11 -0
- package/dist/components/{MobileDataGrid.cjs → PDFViewer/PDFNavigation.cjs} +318 -402
- package/dist/components/PDFViewer/PDFNavigation.js +13 -0
- package/dist/components/PDFViewer/PDFPage.cjs +56 -0
- package/dist/components/PDFViewer/PDFPage.js +7 -0
- package/dist/components/{PDFViewer.cjs → PDFViewer/index.cjs} +290 -202
- package/dist/components/PDFViewer/index.js +29 -0
- package/dist/components/Password.js +2 -2
- package/dist/components/ProductImagePreview/CarouselPagination.cjs +75 -0
- package/dist/components/ProductImagePreview/CarouselPagination.js +7 -0
- package/dist/components/ProductImagePreview/MobileImageCarousel.cjs +214 -0
- package/dist/components/ProductImagePreview/MobileImageCarousel.js +7 -0
- package/dist/components/ProductImagePreview/ProductPrimaryImage.cjs +255 -0
- package/dist/components/ProductImagePreview/ProductPrimaryImage.js +9 -0
- package/dist/components/ProductImagePreview/Thumbnail.cjs +105 -0
- package/dist/components/ProductImagePreview/Thumbnail.js +8 -0
- package/dist/components/ProductImagePreview/ZoomWindow.cjs +198 -0
- package/dist/components/ProductImagePreview/ZoomWindow.js +8 -0
- package/dist/components/ProductImagePreview/index.cjs +1369 -0
- package/dist/components/ProductImagePreview/index.js +22 -0
- package/dist/components/Search.js +3 -3
- package/dist/components/Select.js +3 -3
- package/dist/components/SideMenuGroup.cjs +15 -6
- package/dist/components/SideMenuGroup.js +1 -1
- package/dist/components/SideMenuItem.cjs +15 -6
- package/dist/components/SideMenuItem.js +1 -1
- package/dist/components/SkeletonParagraph.cjs +33 -0
- package/dist/components/SkeletonParagraph.js +10 -0
- package/dist/components/Stack.cjs +15 -6
- package/dist/components/Stack.js +1 -1
- package/dist/components/Stepper.cjs +61 -53
- package/dist/components/Stepper.js +63 -55
- package/dist/components/Surface.js +3 -39
- package/dist/components/Swatch.cjs +15 -6
- package/dist/components/Swatch.js +4 -4
- package/dist/components/Time.cjs +15 -6
- package/dist/components/Time.js +5 -5
- package/dist/components/Upload.cjs +15 -6
- package/dist/components/Upload.js +1 -1
- package/dist/components/index.cjs +2559 -14
- package/dist/components/index.css +186 -3
- package/dist/components/index.js +57 -14
- package/dist/index.css +186 -3
- package/package.json +1 -1
- package/src/components/Accordion.tsx +23 -4
- package/src/components/CompactImagesPreview.tsx +99 -0
- package/src/components/ContentTabs.tsx +3 -1
- package/src/components/DataGrid/types.ts +5 -0
- package/src/components/Grid.tsx +2 -0
- package/src/components/ImagePlaceholder.tsx +22 -0
- package/src/components/MobileDataGrid/ColumnList.tsx +66 -0
- package/src/components/MobileDataGrid/ColumnSelector/index.tsx +97 -0
- package/src/components/MobileDataGrid/GridContextProvider/GridContext.tsx +25 -0
- package/src/components/MobileDataGrid/GridContextProvider/index.tsx +132 -0
- package/src/components/MobileDataGrid/GridContextProvider/useGridContext.ts +10 -0
- package/src/components/MobileDataGrid/MobileDataGridCard/MobileDataGridColumn.tsx +20 -0
- package/src/components/MobileDataGrid/MobileDataGridCard/index.tsx +129 -0
- package/src/components/MobileDataGrid/MobileDataGridHeader.tsx +80 -0
- package/src/components/MobileDataGrid/RowDetailModalProvider/ModalContent.tsx +42 -0
- package/src/components/MobileDataGrid/RowDetailModalProvider/index.tsx +68 -0
- package/src/components/MobileDataGrid/dataGridReducer.ts +55 -0
- package/src/components/MobileDataGrid/index.tsx +92 -0
- package/src/components/MobileDataGrid/types.ts +4 -0
- package/src/components/Modal.tsx +31 -12
- package/src/components/ModalButtons.tsx +1 -1
- package/src/components/ModalHeader.tsx +5 -2
- package/src/components/ModalScrim.tsx +3 -2
- package/src/components/PDFViewer/DownloadIcon.tsx +22 -0
- package/src/components/PDFViewer/PDFElement.tsx +90 -0
- package/src/components/PDFViewer/PDFNavigation.tsx +68 -0
- package/src/components/PDFViewer/PDFPage.tsx +34 -0
- package/src/components/PDFViewer/index.tsx +128 -0
- package/src/components/ProductImagePreview/CarouselPagination.tsx +54 -0
- package/src/components/ProductImagePreview/MobileImageCarousel.tsx +226 -0
- package/src/components/ProductImagePreview/ProductPrimaryImage.tsx +218 -0
- package/src/components/ProductImagePreview/Thumbnail.tsx +49 -0
- package/src/components/ProductImagePreview/ZoomWindow.tsx +136 -0
- package/src/components/ProductImagePreview/index.tsx +182 -0
- package/src/components/ProductImagePreview/useProductImagePreview.ts +211 -0
- package/src/components/SkeletonParagraph.tsx +5 -0
- package/src/components/Stack.tsx +29 -6
- package/src/components/Stepper.tsx +5 -1
- package/src/components/index.ts +4 -0
- package/src/types.ts +2 -1
- package/dist/components/MobileDataGrid.js +0 -150
- package/dist/components/PDFViewer.js +0 -250
- package/src/components/MobileDataGrid.tsx +0 -163
- package/src/components/PDFViewer.tsx +0 -264
- package/dist/{chunk-OXSBIBGT.js → chunk-CKQNJNU3.js} +3 -3
- package/dist/{chunk-RJUN52HJ.js → chunk-ZL5X7KP6.js} +3 -3
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useRef, useEffect, useState, useCallback } from "react";
|
|
2
|
+
import type { ProductPreviewImage } from "./index";
|
|
3
|
+
|
|
4
|
+
export type MobileImageCarouselProps = {
|
|
5
|
+
images: ProductPreviewImage[];
|
|
6
|
+
currentIndex: number;
|
|
7
|
+
width?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
onChangeIndex: (index: number) => void;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function MobileImageCarousel({
|
|
14
|
+
images,
|
|
15
|
+
currentIndex,
|
|
16
|
+
width = 483,
|
|
17
|
+
height = 483,
|
|
18
|
+
onChangeIndex,
|
|
19
|
+
className = "",
|
|
20
|
+
}: MobileImageCarouselProps) {
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
22
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
23
|
+
const [startX, setStartX] = useState(0);
|
|
24
|
+
const [currentTranslate, setCurrentTranslate] = useState(0);
|
|
25
|
+
const [prevTranslate, setPrevTranslate] = useState(0);
|
|
26
|
+
const [containerWidth, setContainerWidth] = useState(width);
|
|
27
|
+
|
|
28
|
+
const imageSize = Math.min(containerWidth * 0.6, height * 0.6);
|
|
29
|
+
const gap = 16;
|
|
30
|
+
|
|
31
|
+
// Calculate translate position to center current image in the reel
|
|
32
|
+
const getTranslateX = useCallback(
|
|
33
|
+
(index: number) => {
|
|
34
|
+
const containerCenter = containerWidth / 2;
|
|
35
|
+
const imageCenter = imageSize / 2;
|
|
36
|
+
const totalOffset = index * (imageSize + gap);
|
|
37
|
+
return containerCenter - imageCenter - totalOffset;
|
|
38
|
+
},
|
|
39
|
+
[containerWidth, imageSize, gap],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const translateX = getTranslateX(currentIndex);
|
|
44
|
+
setCurrentTranslate(translateX);
|
|
45
|
+
setPrevTranslate(translateX);
|
|
46
|
+
}, [currentIndex, getTranslateX]);
|
|
47
|
+
|
|
48
|
+
// Update container width based on actual DOM element
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const updateContainerWidth = () => {
|
|
51
|
+
if (containerRef.current) {
|
|
52
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
53
|
+
setContainerWidth(rect.width);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
updateContainerWidth();
|
|
58
|
+
|
|
59
|
+
const resizeObserver = new ResizeObserver(updateContainerWidth);
|
|
60
|
+
if (containerRef.current) {
|
|
61
|
+
resizeObserver.observe(containerRef.current);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return () => resizeObserver.disconnect();
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Handle touch/mouse events for swiping
|
|
68
|
+
const handleStart = useCallback((clientX: number) => {
|
|
69
|
+
setIsDragging(true);
|
|
70
|
+
setStartX(clientX);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleMove = useCallback(
|
|
74
|
+
(clientX: number) => {
|
|
75
|
+
if (!isDragging) return;
|
|
76
|
+
|
|
77
|
+
const currentPosition = clientX;
|
|
78
|
+
const diff = currentPosition - startX;
|
|
79
|
+
setCurrentTranslate(prevTranslate + diff);
|
|
80
|
+
},
|
|
81
|
+
[isDragging, startX, prevTranslate],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const handleEnd = useCallback(() => {
|
|
85
|
+
if (!isDragging) return;
|
|
86
|
+
|
|
87
|
+
setIsDragging(false);
|
|
88
|
+
|
|
89
|
+
// Determine swipe direction and snap to nearest image
|
|
90
|
+
const moved = currentTranslate - prevTranslate;
|
|
91
|
+
const threshold = imageSize / 3;
|
|
92
|
+
|
|
93
|
+
let newIndex = currentIndex;
|
|
94
|
+
|
|
95
|
+
if (moved > threshold && currentIndex > 0) {
|
|
96
|
+
newIndex = currentIndex - 1;
|
|
97
|
+
} else if (moved < -threshold && currentIndex < images.length - 1) {
|
|
98
|
+
newIndex = currentIndex + 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (newIndex !== currentIndex) {
|
|
102
|
+
onChangeIndex(newIndex);
|
|
103
|
+
} else {
|
|
104
|
+
setCurrentTranslate(prevTranslate);
|
|
105
|
+
}
|
|
106
|
+
}, [
|
|
107
|
+
isDragging,
|
|
108
|
+
currentTranslate,
|
|
109
|
+
prevTranslate,
|
|
110
|
+
currentIndex,
|
|
111
|
+
imageSize,
|
|
112
|
+
images.length,
|
|
113
|
+
onChangeIndex,
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const handleMouseDown = (e: React.MouseEvent) => {
|
|
117
|
+
e.preventDefault();
|
|
118
|
+
handleStart(e.clientX);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
handleMove(e.clientX);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleMouseUp = () => {
|
|
127
|
+
handleEnd();
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const handleMouseLeave = () => {
|
|
131
|
+
handleEnd();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const handleTouchStart = (e: React.TouchEvent) => {
|
|
135
|
+
handleStart(e.touches[0].clientX);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const handleTouchMove = (e: React.TouchEvent) => {
|
|
139
|
+
handleMove(e.touches[0].clientX);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleTouchEnd = () => {
|
|
143
|
+
handleEnd();
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const handleImageClick = useCallback(
|
|
147
|
+
(index: number) => {
|
|
148
|
+
if (!isDragging && index !== currentIndex) {
|
|
149
|
+
onChangeIndex(index);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
[isDragging, currentIndex, onChangeIndex],
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
if (!images.length) return null;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className={`md:hidden w-full ${className}`}>
|
|
159
|
+
<div
|
|
160
|
+
ref={containerRef}
|
|
161
|
+
className="relative overflow-hidden cursor-grab active:cursor-grabbing select-none w-full"
|
|
162
|
+
style={{
|
|
163
|
+
height: imageSize,
|
|
164
|
+
}}
|
|
165
|
+
onMouseDown={handleMouseDown}
|
|
166
|
+
onMouseMove={handleMouseMove}
|
|
167
|
+
onMouseUp={handleMouseUp}
|
|
168
|
+
onMouseLeave={handleMouseLeave}
|
|
169
|
+
onTouchStart={handleTouchStart}
|
|
170
|
+
onTouchMove={handleTouchMove}
|
|
171
|
+
onTouchEnd={handleTouchEnd}
|
|
172
|
+
>
|
|
173
|
+
<div
|
|
174
|
+
className="flex items-center"
|
|
175
|
+
style={{
|
|
176
|
+
transform: `translateX(${currentTranslate}px)`,
|
|
177
|
+
transition: isDragging ? "none" : "transform 0.3s ease-out",
|
|
178
|
+
gap: `${gap}px`,
|
|
179
|
+
}}
|
|
180
|
+
>
|
|
181
|
+
{images.map((image, index) => {
|
|
182
|
+
const isActive = index === currentIndex;
|
|
183
|
+
const distance = Math.abs(index - currentIndex);
|
|
184
|
+
|
|
185
|
+
// Only render visible images for performance
|
|
186
|
+
const shouldRender = distance <= 2;
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
key={image.src + index}
|
|
191
|
+
className="flex-shrink-0 transition-opacity duration-300"
|
|
192
|
+
style={{
|
|
193
|
+
width: imageSize,
|
|
194
|
+
height: imageSize,
|
|
195
|
+
opacity: isActive ? 1 : Math.max(0.3, 1 - distance * 0.3),
|
|
196
|
+
}}
|
|
197
|
+
onClick={() => handleImageClick(index)}
|
|
198
|
+
>
|
|
199
|
+
{shouldRender ? (
|
|
200
|
+
<img
|
|
201
|
+
src={image.src}
|
|
202
|
+
alt={image.alt || `Product image ${index + 1}`}
|
|
203
|
+
className="w-full h-full object-cover"
|
|
204
|
+
draggable={false}
|
|
205
|
+
loading={distance <= 1 ? "eager" : "lazy"}
|
|
206
|
+
style={{
|
|
207
|
+
aspectRatio: "1 / 1",
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<div
|
|
212
|
+
className="w-full h-full bg-neutral-100 rounded-md border border-gray-200"
|
|
213
|
+
style={{
|
|
214
|
+
aspectRatio: "1 / 1",
|
|
215
|
+
}}
|
|
216
|
+
aria-hidden="true"
|
|
217
|
+
/>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useMemo, useEffect } from "react";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import { Spinner } from "../Spinner";
|
|
4
|
+
import type { ProductPreviewImage } from "./index";
|
|
5
|
+
import { ImagePlaceholder } from "../ImagePlaceholder";
|
|
6
|
+
|
|
7
|
+
export type ProductPrimaryImageProps = {
|
|
8
|
+
image?: ProductPreviewImage;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
zoomEnabled?: boolean;
|
|
12
|
+
zoomLensSize?: number;
|
|
13
|
+
scrollToZoomEnabled?: boolean;
|
|
14
|
+
className?: string;
|
|
15
|
+
isPlaceholder?: boolean;
|
|
16
|
+
onZoomPositionChange?: (
|
|
17
|
+
data: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
w: number;
|
|
21
|
+
h: number;
|
|
22
|
+
lensSize: number;
|
|
23
|
+
} | null,
|
|
24
|
+
active: boolean,
|
|
25
|
+
) => void;
|
|
26
|
+
onScrollZoom?: (delta: number) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Primary product image with optional zoom lens that tracks cursor movement */
|
|
30
|
+
export function ProductPrimaryImage({
|
|
31
|
+
image,
|
|
32
|
+
width,
|
|
33
|
+
height,
|
|
34
|
+
zoomEnabled = false,
|
|
35
|
+
zoomLensSize = 140,
|
|
36
|
+
scrollToZoomEnabled = false,
|
|
37
|
+
className = "",
|
|
38
|
+
isPlaceholder = false,
|
|
39
|
+
onZoomPositionChange,
|
|
40
|
+
onScrollZoom,
|
|
41
|
+
}: ProductPrimaryImageProps) {
|
|
42
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
43
|
+
const lastPointRef = useRef<{ x: number; y: number } | null>(null);
|
|
44
|
+
const rafRef = useRef<number | null>(null);
|
|
45
|
+
const [active, setActive] = useState(false);
|
|
46
|
+
const [, forceRerender] = useState(0);
|
|
47
|
+
const [imageLoading, setImageLoading] = useState(true);
|
|
48
|
+
const [imageError, setImageError] = useState(false);
|
|
49
|
+
|
|
50
|
+
// Memoize image src to prevent unnecessary re-requests
|
|
51
|
+
const imageSrc = useMemo(() => image?.src, [image?.src]);
|
|
52
|
+
|
|
53
|
+
const schedule = () => {
|
|
54
|
+
if (rafRef.current != null) return;
|
|
55
|
+
rafRef.current = requestAnimationFrame(() => {
|
|
56
|
+
rafRef.current = null;
|
|
57
|
+
forceRerender((n) => n + 1);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handlePointerEnter = useCallback(() => {
|
|
62
|
+
if (!zoomEnabled) return;
|
|
63
|
+
setActive(true);
|
|
64
|
+
const el = containerRef.current;
|
|
65
|
+
if (el) {
|
|
66
|
+
const r = el.getBoundingClientRect();
|
|
67
|
+
const pt = lastPointRef.current;
|
|
68
|
+
onZoomPositionChange?.(
|
|
69
|
+
pt ? { ...pt, w: r.width, h: r.height, lensSize: zoomLensSize } : null,
|
|
70
|
+
true,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}, [zoomEnabled, onZoomPositionChange, zoomLensSize]);
|
|
74
|
+
|
|
75
|
+
const handlePointerLeave = useCallback(() => {
|
|
76
|
+
if (!zoomEnabled) return;
|
|
77
|
+
setActive(false);
|
|
78
|
+
lastPointRef.current = null;
|
|
79
|
+
onZoomPositionChange?.(null, false);
|
|
80
|
+
}, [zoomEnabled, onZoomPositionChange]);
|
|
81
|
+
|
|
82
|
+
const handlePointerMove = useCallback<
|
|
83
|
+
React.PointerEventHandler<HTMLDivElement>
|
|
84
|
+
>(
|
|
85
|
+
(e) => {
|
|
86
|
+
if (!zoomEnabled || !active) return;
|
|
87
|
+
|
|
88
|
+
// Ignore touch events to prevent zoom on mobile
|
|
89
|
+
if (e.pointerType === "touch") return;
|
|
90
|
+
|
|
91
|
+
const el = containerRef.current;
|
|
92
|
+
if (!el) return;
|
|
93
|
+
const rect = el.getBoundingClientRect();
|
|
94
|
+
const rawX = (e.clientX - rect.left) / rect.width;
|
|
95
|
+
const rawY = (e.clientY - rect.top) / rect.height;
|
|
96
|
+
const size = zoomLensSize ?? 140;
|
|
97
|
+
// Constrain lens position within image boundaries
|
|
98
|
+
const left = Math.max(
|
|
99
|
+
0,
|
|
100
|
+
Math.min(rect.width - size, rawX * rect.width - size / 2),
|
|
101
|
+
);
|
|
102
|
+
const top = Math.max(
|
|
103
|
+
0,
|
|
104
|
+
Math.min(rect.height - size, rawY * rect.height - size / 2),
|
|
105
|
+
);
|
|
106
|
+
// Convert pixel position back to normalized coordinates
|
|
107
|
+
const centerXNorm = (left + size / 2) / rect.width;
|
|
108
|
+
const centerYNorm = (top + size / 2) / rect.height;
|
|
109
|
+
lastPointRef.current = {
|
|
110
|
+
x: centerXNorm,
|
|
111
|
+
y: centerYNorm,
|
|
112
|
+
};
|
|
113
|
+
schedule();
|
|
114
|
+
onZoomPositionChange?.(
|
|
115
|
+
lastPointRef.current
|
|
116
|
+
? {
|
|
117
|
+
...lastPointRef.current,
|
|
118
|
+
w: rect.width,
|
|
119
|
+
h: rect.height,
|
|
120
|
+
lensSize: zoomLensSize,
|
|
121
|
+
}
|
|
122
|
+
: null,
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
[zoomEnabled, active, onZoomPositionChange, zoomLensSize],
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Set up non-passive wheel event listener to properly prevent default
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const container = containerRef.current;
|
|
132
|
+
if (!container || !scrollToZoomEnabled) return;
|
|
133
|
+
|
|
134
|
+
const handleNativeWheel = (e: WheelEvent) => {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
|
|
138
|
+
if (!zoomEnabled || !active) return;
|
|
139
|
+
|
|
140
|
+
// Calculate zoom delta based on wheel direction
|
|
141
|
+
const delta = e.deltaY > 0 ? -0.2 : 0.2; // Zoom out on scroll down, zoom in on scroll up
|
|
142
|
+
onScrollZoom?.(delta);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
container.addEventListener("wheel", handleNativeWheel, { passive: false });
|
|
146
|
+
|
|
147
|
+
return () => {
|
|
148
|
+
container.removeEventListener("wheel", handleNativeWheel);
|
|
149
|
+
};
|
|
150
|
+
}, [scrollToZoomEnabled, zoomEnabled, active, onScrollZoom]);
|
|
151
|
+
|
|
152
|
+
if (!image && !isPlaceholder) return null;
|
|
153
|
+
|
|
154
|
+
const pt = lastPointRef.current;
|
|
155
|
+
let lensStyle: CSSProperties | undefined;
|
|
156
|
+
if (pt && active && zoomEnabled) {
|
|
157
|
+
const size = zoomLensSize;
|
|
158
|
+
const leftRaw = pt.x * width - size / 2;
|
|
159
|
+
const topRaw = pt.y * height - size / 2;
|
|
160
|
+
// Position lens overlay with boundary constraints
|
|
161
|
+
lensStyle = {
|
|
162
|
+
width: size,
|
|
163
|
+
height: size,
|
|
164
|
+
left: Math.max(0, Math.min(width - size, leftRaw)),
|
|
165
|
+
top: Math.max(0, Math.min(height - size, topRaw)),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
ref={containerRef}
|
|
172
|
+
className={[
|
|
173
|
+
"relative overflow-hidden bg-white rounded flex items-center justify-center select-none",
|
|
174
|
+
zoomEnabled ? "cursor-crosshair" : "",
|
|
175
|
+
className,
|
|
176
|
+
].join(" ")}
|
|
177
|
+
style={{ maxWidth: width, maxHeight: height, aspectRatio: "1 / 1" }}
|
|
178
|
+
onPointerEnter={handlePointerEnter}
|
|
179
|
+
onPointerLeave={handlePointerLeave}
|
|
180
|
+
onPointerMove={handlePointerMove}
|
|
181
|
+
>
|
|
182
|
+
{imageLoading && !imageError && !isPlaceholder && (
|
|
183
|
+
<div className="absolute inset-0 flex items-center justify-center bg-neutral-50">
|
|
184
|
+
<Spinner size="small" />
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{isPlaceholder ? (
|
|
188
|
+
<ImagePlaceholder width={width} height={height} />
|
|
189
|
+
) : (
|
|
190
|
+
<img
|
|
191
|
+
key={imageSrc}
|
|
192
|
+
src={imageSrc}
|
|
193
|
+
alt={image?.alt || "Product image"}
|
|
194
|
+
className="object-cover w-full h-full select-none"
|
|
195
|
+
draggable={false}
|
|
196
|
+
loading="lazy"
|
|
197
|
+
onLoad={() => setImageLoading(false)}
|
|
198
|
+
onError={() => {
|
|
199
|
+
setImageLoading(false);
|
|
200
|
+
setImageError(true);
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
)}
|
|
204
|
+
{zoomEnabled && active && lensStyle && (
|
|
205
|
+
<div
|
|
206
|
+
aria-hidden
|
|
207
|
+
className="absolute pointer-events-none border border-white/70 shadow-[0_0_0_1px_rgba(0,0,0,0.15)] rounded-md bg-white/10 backdrop-blur-[1px]"
|
|
208
|
+
style={lensStyle}
|
|
209
|
+
/>
|
|
210
|
+
)}
|
|
211
|
+
{/* {scrollToZoomEnabled && zoomEnabled && active && (
|
|
212
|
+
<div className="absolute top-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded pointer-events-none">
|
|
213
|
+
Scroll to zoom
|
|
214
|
+
</div>
|
|
215
|
+
)} */}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ImagePlaceholder } from "../ImagePlaceholder";
|
|
2
|
+
|
|
3
|
+
export type ThumbnailProps = {
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
src: string;
|
|
7
|
+
alt?: string;
|
|
8
|
+
isActive?: boolean;
|
|
9
|
+
isPlaceholder?: boolean;
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Thumbnail({
|
|
14
|
+
width,
|
|
15
|
+
height,
|
|
16
|
+
src,
|
|
17
|
+
alt,
|
|
18
|
+
isActive,
|
|
19
|
+
onClick,
|
|
20
|
+
isPlaceholder = false,
|
|
21
|
+
}: ThumbnailProps) {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
onClick={onClick}
|
|
26
|
+
className={[
|
|
27
|
+
"cursor-pointer relative overflow-hidden rounded aspect-square w-full", // base radius, square when no explicit size
|
|
28
|
+
"focus:outline-none",
|
|
29
|
+
isActive &&
|
|
30
|
+
!isPlaceholder &&
|
|
31
|
+
"ring-[3px] ring-offset-1 ring-border-action-normal ring-offset-white opacity-70",
|
|
32
|
+
].join(" ")}
|
|
33
|
+
style={{ maxWidth: width, maxHeight: height, aspectRatio: "1 / 1" }}
|
|
34
|
+
aria-pressed={isActive && !isPlaceholder ? "true" : "false"}
|
|
35
|
+
>
|
|
36
|
+
{isPlaceholder ? (
|
|
37
|
+
<ImagePlaceholder width={115} height={115} />
|
|
38
|
+
) : (
|
|
39
|
+
<img
|
|
40
|
+
src={src}
|
|
41
|
+
alt={alt}
|
|
42
|
+
className="object-cover w-full h-full select-none"
|
|
43
|
+
draggable={false}
|
|
44
|
+
loading="lazy"
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { Surface } from "../Surface";
|
|
4
|
+
import type { ProductPreviewImage } from "./index";
|
|
5
|
+
|
|
6
|
+
export type ZoomWindowProps = {
|
|
7
|
+
image?: ProductPreviewImage;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
pointer: {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
w: number;
|
|
14
|
+
h: number;
|
|
15
|
+
lensSize: number;
|
|
16
|
+
} | null;
|
|
17
|
+
active: boolean;
|
|
18
|
+
zoomFactor?: number;
|
|
19
|
+
className?: string;
|
|
20
|
+
scaleFactor?: number;
|
|
21
|
+
primaryImagePosition?: DOMRect | null;
|
|
22
|
+
offset?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Floating zoom window that follows the lens cursor and shows magnified image content */
|
|
26
|
+
export function ZoomWindow({
|
|
27
|
+
image,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
pointer,
|
|
31
|
+
active,
|
|
32
|
+
zoomFactor = 2,
|
|
33
|
+
scaleFactor = 2,
|
|
34
|
+
primaryImagePosition,
|
|
35
|
+
offset = 10,
|
|
36
|
+
className = "",
|
|
37
|
+
}: ZoomWindowProps) {
|
|
38
|
+
// Memoize image src to prevent unnecessary re-requests
|
|
39
|
+
const imageSrc = useMemo(() => image?.src, [image?.src]);
|
|
40
|
+
|
|
41
|
+
if (!image || !active || !pointer) return null;
|
|
42
|
+
|
|
43
|
+
const zoomWindowSize = pointer.lensSize * scaleFactor;
|
|
44
|
+
|
|
45
|
+
// Calculate image scaling and positioning for zoom effect
|
|
46
|
+
const baseW = pointer.w || width;
|
|
47
|
+
const baseH = pointer.h || height;
|
|
48
|
+
const scaledW = baseW * zoomFactor;
|
|
49
|
+
const scaledH = baseH * zoomFactor;
|
|
50
|
+
const centerX = pointer.x * baseW * zoomFactor;
|
|
51
|
+
const centerY = pointer.y * baseH * zoomFactor;
|
|
52
|
+
const imageOffsetX = zoomWindowSize / 2 - centerX;
|
|
53
|
+
const imageOffsetY = zoomWindowSize / 2 - centerY;
|
|
54
|
+
|
|
55
|
+
// Calculate lens position and smart window positioning
|
|
56
|
+
const calculatePosition = (): CSSProperties => {
|
|
57
|
+
if (!primaryImagePosition) {
|
|
58
|
+
return {
|
|
59
|
+
position: "fixed",
|
|
60
|
+
zIndex: 999,
|
|
61
|
+
top: "50%",
|
|
62
|
+
left: "50%",
|
|
63
|
+
transform: "translate(-50%, -50%)",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lensLeft =
|
|
68
|
+
primaryImagePosition.left +
|
|
69
|
+
pointer.x * primaryImagePosition.width -
|
|
70
|
+
pointer.lensSize / 2;
|
|
71
|
+
const lensTop =
|
|
72
|
+
primaryImagePosition.top +
|
|
73
|
+
pointer.y * primaryImagePosition.height -
|
|
74
|
+
pointer.lensSize / 2;
|
|
75
|
+
|
|
76
|
+
let left = lensLeft + pointer.lensSize + offset;
|
|
77
|
+
let top = lensTop;
|
|
78
|
+
|
|
79
|
+
const { innerWidth, innerHeight } = window;
|
|
80
|
+
|
|
81
|
+
// Smart positioning: adjust if exceeds viewport bounds
|
|
82
|
+
if (left + zoomWindowSize > innerWidth) {
|
|
83
|
+
left = lensLeft - zoomWindowSize - offset;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (top + zoomWindowSize > innerHeight) {
|
|
87
|
+
top = lensTop + pointer.lensSize - zoomWindowSize;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (top < 0) {
|
|
91
|
+
top = lensTop;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (left < 0) {
|
|
95
|
+
left = lensLeft + pointer.lensSize + offset;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
position: "fixed",
|
|
100
|
+
zIndex: 999,
|
|
101
|
+
left,
|
|
102
|
+
top,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
return (
|
|
106
|
+
<Surface
|
|
107
|
+
elevation={16}
|
|
108
|
+
className={className}
|
|
109
|
+
style={{
|
|
110
|
+
width: zoomWindowSize,
|
|
111
|
+
height: zoomWindowSize,
|
|
112
|
+
position: "fixed",
|
|
113
|
+
zIndex: 999,
|
|
114
|
+
overflow: "hidden",
|
|
115
|
+
pointerEvents: "none",
|
|
116
|
+
userSelect: "none",
|
|
117
|
+
...calculatePosition(),
|
|
118
|
+
}}
|
|
119
|
+
aria-hidden
|
|
120
|
+
>
|
|
121
|
+
<img
|
|
122
|
+
key={imageSrc}
|
|
123
|
+
src={imageSrc}
|
|
124
|
+
alt=""
|
|
125
|
+
className="pointer-events-none select-none max-w-none object-cover"
|
|
126
|
+
style={{
|
|
127
|
+
width: scaledW,
|
|
128
|
+
height: scaledH,
|
|
129
|
+
transform: `translate(${imageOffsetX}px, ${imageOffsetY}px)`,
|
|
130
|
+
}}
|
|
131
|
+
draggable={false}
|
|
132
|
+
loading="lazy"
|
|
133
|
+
/>
|
|
134
|
+
</Surface>
|
|
135
|
+
);
|
|
136
|
+
}
|