@djangocfg/ui-tools 2.1.109 → 2.1.111
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/README.md +242 -49
- package/dist/JsonSchemaForm-65NLLK56.mjs +4 -0
- package/dist/JsonSchemaForm-65NLLK56.mjs.map +1 -0
- package/dist/JsonSchemaForm-PY6DH3HE.cjs +13 -0
- package/dist/JsonSchemaForm-PY6DH3HE.cjs.map +1 -0
- package/dist/JsonTree-6RYAOPSS.mjs +4 -0
- package/dist/JsonTree-6RYAOPSS.mjs.map +1 -0
- package/dist/JsonTree-7OH6CIHT.cjs +10 -0
- package/dist/JsonTree-7OH6CIHT.cjs.map +1 -0
- package/dist/MapContainer-GXQLP5WY.mjs +214 -0
- package/dist/MapContainer-GXQLP5WY.mjs.map +1 -0
- package/dist/MapContainer-RYG4HPH4.cjs +221 -0
- package/dist/MapContainer-RYG4HPH4.cjs.map +1 -0
- package/dist/{Mermaid.client-4OCKJ6QD.mjs → Mermaid.client-OKACITCW.mjs} +16 -7
- package/dist/Mermaid.client-OKACITCW.mjs.map +1 -0
- package/dist/{Mermaid.client-ZP6OE46Z.cjs → Mermaid.client-PNXEC6YL.cjs} +16 -7
- package/dist/Mermaid.client-PNXEC6YL.cjs.map +1 -0
- package/dist/{PlaygroundLayout-XXVBU4WZ.cjs → PlaygroundLayout-SYMEAG3J.cjs} +25 -24
- package/dist/PlaygroundLayout-SYMEAG3J.cjs.map +1 -0
- package/dist/{PlaygroundLayout-LMQTVXSP.mjs → PlaygroundLayout-UQRBU5RH.mjs} +4 -3
- package/dist/PlaygroundLayout-UQRBU5RH.mjs.map +1 -0
- package/dist/{PrettyCode.client-2CLSV2VD.cjs → PrettyCode.client-DANYYQYO.cjs} +11 -4
- package/dist/PrettyCode.client-DANYYQYO.cjs.map +1 -0
- package/dist/{PrettyCode.client-Y2BVON7R.mjs → PrettyCode.client-RS5ZTNBT.mjs} +11 -4
- package/dist/PrettyCode.client-RS5ZTNBT.mjs.map +1 -0
- package/dist/chunk-2DSR7V2L.mjs +561 -0
- package/dist/chunk-2DSR7V2L.mjs.map +1 -0
- package/dist/chunk-47T5ECYV.cjs +1357 -0
- package/dist/chunk-47T5ECYV.cjs.map +1 -0
- package/dist/chunk-5QT3QYFZ.cjs +189 -0
- package/dist/chunk-5QT3QYFZ.cjs.map +1 -0
- package/dist/chunk-7IIRYG4S.mjs +1057 -0
- package/dist/chunk-7IIRYG4S.mjs.map +1 -0
- package/dist/{chunk-FB5QBSI3.cjs → chunk-DI3HUXHK.cjs} +15 -195
- package/dist/chunk-DI3HUXHK.cjs.map +1 -0
- package/dist/chunk-EVGWYASL.cjs +1528 -0
- package/dist/chunk-EVGWYASL.cjs.map +1 -0
- package/dist/chunk-F2N7P5XU.cjs +30 -0
- package/dist/chunk-F2N7P5XU.cjs.map +1 -0
- package/dist/{chunk-L6UHASYQ.mjs → chunk-G6PRZP5I.mjs} +7 -186
- package/dist/chunk-G6PRZP5I.mjs.map +1 -0
- package/dist/chunk-JWB2EWQO.mjs +5 -0
- package/dist/chunk-JWB2EWQO.mjs.map +1 -0
- package/dist/chunk-LTJX2JXE.mjs +338 -0
- package/dist/chunk-LTJX2JXE.mjs.map +1 -0
- package/dist/chunk-OVNC4KW6.mjs +1494 -0
- package/dist/chunk-OVNC4KW6.mjs.map +1 -0
- package/dist/chunk-PNZSJN6T.cjs +1086 -0
- package/dist/chunk-PNZSJN6T.cjs.map +1 -0
- package/dist/chunk-TEFRA7GW.cjs +565 -0
- package/dist/chunk-TEFRA7GW.cjs.map +1 -0
- package/dist/chunk-UOMPPIED.mjs +1343 -0
- package/dist/chunk-UOMPPIED.mjs.map +1 -0
- package/dist/chunk-W6YHQI4F.mjs +187 -0
- package/dist/chunk-W6YHQI4F.mjs.map +1 -0
- package/dist/chunk-XTBRWVIV.cjs +346 -0
- package/dist/chunk-XTBRWVIV.cjs.map +1 -0
- package/dist/components-C7ZL7OMY.mjs +5 -0
- package/dist/components-C7ZL7OMY.mjs.map +1 -0
- package/dist/components-CJ2IB65O.cjs +27 -0
- package/dist/components-CJ2IB65O.cjs.map +1 -0
- package/dist/components-EASJYK45.mjs +6 -0
- package/dist/components-EASJYK45.mjs.map +1 -0
- package/dist/components-LDRFDV4A.cjs +22 -0
- package/dist/components-LDRFDV4A.cjs.map +1 -0
- package/dist/components-VZKUTDJK.mjs +5 -0
- package/dist/components-VZKUTDJK.mjs.map +1 -0
- package/dist/components-Y64GTIMQ.cjs +42 -0
- package/dist/components-Y64GTIMQ.cjs.map +1 -0
- package/dist/index.cjs +701 -4813
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1274 -1026
- package/dist/index.d.ts +1274 -1026
- package/dist/index.mjs +358 -4730
- package/dist/index.mjs.map +1 -1
- package/package.json +27 -4
- package/src/components/index.ts +17 -0
- package/src/components/lazy-wrapper.tsx +281 -0
- package/src/index.ts +92 -7
- package/src/tools/AudioPlayer/components/HybridAudioPlayer.tsx +14 -5
- package/src/tools/AudioPlayer/lazy.tsx +85 -0
- package/src/tools/Gallery/components/Gallery.tsx +182 -0
- package/src/tools/Gallery/components/GalleryCarousel.tsx +251 -0
- package/src/tools/Gallery/components/GalleryCompact.tsx +173 -0
- package/src/tools/Gallery/components/GalleryGrid.tsx +493 -0
- package/src/tools/Gallery/components/GalleryImage.tsx +66 -0
- package/src/tools/Gallery/components/GalleryLightbox.tsx +331 -0
- package/src/tools/Gallery/components/GalleryMedia.tsx +66 -0
- package/src/tools/Gallery/components/GalleryThumbnails.tsx +173 -0
- package/src/tools/Gallery/components/GalleryThumbnailsVirtual.tsx +138 -0
- package/src/tools/Gallery/components/GalleryVideo.tsx +222 -0
- package/src/tools/Gallery/components/index.ts +13 -0
- package/src/tools/Gallery/hooks/index.ts +23 -0
- package/src/tools/Gallery/hooks/useGallery.ts +137 -0
- package/src/tools/Gallery/hooks/useImageDimensions.ts +223 -0
- package/src/tools/Gallery/hooks/usePinchZoom.ts +234 -0
- package/src/tools/Gallery/hooks/usePreloadImages.ts +71 -0
- package/src/tools/Gallery/hooks/useSwipe.ts +86 -0
- package/src/tools/Gallery/hooks/useVirtualList.ts +129 -0
- package/src/tools/Gallery/hooks/useZoom.ts +316 -0
- package/src/tools/Gallery/index.ts +66 -0
- package/src/tools/Gallery/types.ts +183 -0
- package/src/tools/Gallery/utils/imageAnalysis.ts +52 -0
- package/src/tools/Gallery/utils/index.ts +11 -0
- package/src/tools/ImageViewer/components/ImageToolbar.tsx +20 -8
- package/src/tools/ImageViewer/components/ImageViewer.tsx +12 -4
- package/src/tools/ImageViewer/lazy.tsx +37 -0
- package/src/tools/JsonForm/lazy.tsx +43 -0
- package/src/tools/JsonForm/widgets/ColorWidget.tsx +4 -1
- package/src/tools/JsonTree/lazy.tsx +45 -0
- package/src/tools/LottiePlayer/lazy.tsx +57 -0
- package/src/tools/Map/components/CustomOverlay.tsx +54 -0
- package/src/tools/Map/components/DrawControl.tsx +36 -0
- package/src/tools/Map/components/GeocoderControl.tsx +70 -0
- package/src/tools/Map/components/LayerSwitcher.tsx +225 -0
- package/src/tools/Map/components/MapCluster.tsx +273 -0
- package/src/tools/Map/components/MapContainer.tsx +191 -0
- package/src/tools/Map/components/MapControls.tsx +44 -0
- package/src/tools/Map/components/MapLegend.tsx +161 -0
- package/src/tools/Map/components/MapMarker.tsx +102 -0
- package/src/tools/Map/components/MapPopup.tsx +46 -0
- package/src/tools/Map/components/MapSource.tsx +30 -0
- package/src/tools/Map/components/index.ts +20 -0
- package/src/tools/Map/context/MapContext.tsx +89 -0
- package/src/tools/Map/context/index.ts +2 -0
- package/src/tools/Map/hooks/index.ts +9 -0
- package/src/tools/Map/hooks/useMap.ts +11 -0
- package/src/tools/Map/hooks/useMapControl.ts +99 -0
- package/src/tools/Map/hooks/useMapEvents.ts +147 -0
- package/src/tools/Map/hooks/useMapLayers.ts +83 -0
- package/src/tools/Map/hooks/useMapViewport.ts +62 -0
- package/src/tools/Map/hooks/useMarkers.ts +85 -0
- package/src/tools/Map/index.ts +116 -0
- package/src/tools/Map/layers/cluster.ts +94 -0
- package/src/tools/Map/layers/index.ts +15 -0
- package/src/tools/Map/layers/line.ts +93 -0
- package/src/tools/Map/layers/point.ts +61 -0
- package/src/tools/Map/layers/polygon.ts +73 -0
- package/src/tools/Map/lazy.tsx +56 -0
- package/src/tools/Map/styles/index.ts +15 -0
- package/src/tools/Map/types.ts +259 -0
- package/src/tools/Map/utils/geo.ts +88 -0
- package/src/tools/Map/utils/index.ts +16 -0
- package/src/tools/Map/utils/transform.ts +107 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +12 -4
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +6 -2
- package/src/tools/Mermaid/lazy.tsx +46 -0
- package/src/tools/OpenapiViewer/lazy.tsx +72 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +10 -3
- package/src/tools/PrettyCode/lazy.tsx +64 -0
- package/src/tools/VideoPlayer/lazy.tsx +63 -0
- package/dist/Mermaid.client-4OCKJ6QD.mjs.map +0 -1
- package/dist/Mermaid.client-ZP6OE46Z.cjs.map +0 -1
- package/dist/PlaygroundLayout-LMQTVXSP.mjs.map +0 -1
- package/dist/PlaygroundLayout-XXVBU4WZ.cjs.map +0 -1
- package/dist/PrettyCode.client-2CLSV2VD.cjs.map +0 -1
- package/dist/PrettyCode.client-Y2BVON7R.mjs.map +0 -1
- package/dist/chunk-FB5QBSI3.cjs.map +0 -1
- package/dist/chunk-L6UHASYQ.mjs.map +0 -1
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface ZoomState {
|
|
6
|
+
scale: number
|
|
7
|
+
x: number
|
|
8
|
+
y: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UseZoomOptions {
|
|
12
|
+
minScale?: number
|
|
13
|
+
maxScale?: number
|
|
14
|
+
desktopScale?: number
|
|
15
|
+
onZoomChange?: (zoomed: boolean) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseZoomReturn {
|
|
19
|
+
state: ZoomState
|
|
20
|
+
isZoomed: boolean
|
|
21
|
+
isDragging: boolean
|
|
22
|
+
handlers: {
|
|
23
|
+
onTouchStart: (e: React.TouchEvent) => void
|
|
24
|
+
onTouchMove: (e: React.TouchEvent) => void
|
|
25
|
+
onTouchEnd: (e: React.TouchEvent) => void
|
|
26
|
+
onClick: (e: React.MouseEvent) => void
|
|
27
|
+
onMouseDown: (e: React.MouseEvent) => void
|
|
28
|
+
onMouseMove: (e: React.MouseEvent) => void
|
|
29
|
+
onMouseUp: () => void
|
|
30
|
+
onMouseLeave: () => void
|
|
31
|
+
}
|
|
32
|
+
reset: () => void
|
|
33
|
+
zoomTo: (scale: number, x?: number, y?: number) => void
|
|
34
|
+
toggleZoom: () => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const INITIAL_STATE: ZoomState = { scale: 1, x: 0, y: 0 }
|
|
38
|
+
const ZOOM_THRESHOLD = 1.05
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unified zoom hook for both desktop (click/drag) and mobile (pinch/tap)
|
|
42
|
+
*/
|
|
43
|
+
export function useZoom(options: UseZoomOptions = {}): UseZoomReturn {
|
|
44
|
+
const { minScale = 1, maxScale = 3, desktopScale = 2, onZoomChange } = options
|
|
45
|
+
|
|
46
|
+
const [state, setState] = useState<ZoomState>(INITIAL_STATE)
|
|
47
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
48
|
+
|
|
49
|
+
const touchRef = useRef<{
|
|
50
|
+
// Touch tracking
|
|
51
|
+
initialDistance: number
|
|
52
|
+
initialScale: number
|
|
53
|
+
initialX: number
|
|
54
|
+
initialY: number
|
|
55
|
+
centerX: number
|
|
56
|
+
centerY: number
|
|
57
|
+
isPinching: boolean
|
|
58
|
+
lastTap: number
|
|
59
|
+
// Container dimensions
|
|
60
|
+
containerWidth: number
|
|
61
|
+
containerHeight: number
|
|
62
|
+
// Mouse drag tracking
|
|
63
|
+
dragStartX: number
|
|
64
|
+
dragStartY: number
|
|
65
|
+
}>({
|
|
66
|
+
initialDistance: 0,
|
|
67
|
+
initialScale: 1,
|
|
68
|
+
initialX: 0,
|
|
69
|
+
initialY: 0,
|
|
70
|
+
centerX: 0,
|
|
71
|
+
centerY: 0,
|
|
72
|
+
isPinching: false,
|
|
73
|
+
lastTap: 0,
|
|
74
|
+
containerWidth: 0,
|
|
75
|
+
containerHeight: 0,
|
|
76
|
+
dragStartX: 0,
|
|
77
|
+
dragStartY: 0,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const isZoomed = state.scale > ZOOM_THRESHOLD
|
|
81
|
+
|
|
82
|
+
// Calculate distance between two touch points
|
|
83
|
+
const getDistance = useCallback((touches: React.TouchList): number => {
|
|
84
|
+
if (touches.length < 2) return 0
|
|
85
|
+
const dx = touches[0].clientX - touches[1].clientX
|
|
86
|
+
const dy = touches[0].clientY - touches[1].clientY
|
|
87
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
90
|
+
// Get center point between two touches
|
|
91
|
+
const getCenter = useCallback((touches: React.TouchList): { x: number; y: number } => {
|
|
92
|
+
if (touches.length < 2) {
|
|
93
|
+
return { x: touches[0].clientX, y: touches[0].clientY }
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
x: (touches[0].clientX + touches[1].clientX) / 2,
|
|
97
|
+
y: (touches[0].clientY + touches[1].clientY) / 2,
|
|
98
|
+
}
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
// Calculate max pan based on container size and scale
|
|
102
|
+
const getMaxPan = useCallback((scale: number) => {
|
|
103
|
+
const { containerWidth, containerHeight } = touchRef.current
|
|
104
|
+
return {
|
|
105
|
+
x: (containerWidth * (scale - 1)) / 2,
|
|
106
|
+
y: (containerHeight * (scale - 1)) / 2,
|
|
107
|
+
}
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
// Clamp pan position within bounds
|
|
111
|
+
const clampPan = useCallback((x: number, y: number, scale: number) => {
|
|
112
|
+
const maxPan = getMaxPan(scale)
|
|
113
|
+
return {
|
|
114
|
+
x: Math.max(-maxPan.x, Math.min(maxPan.x, x)),
|
|
115
|
+
y: Math.max(-maxPan.y, Math.min(maxPan.y, y)),
|
|
116
|
+
}
|
|
117
|
+
}, [getMaxPan])
|
|
118
|
+
|
|
119
|
+
// ========== TOUCH HANDLERS (MOBILE) ==========
|
|
120
|
+
|
|
121
|
+
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
|
122
|
+
const touches = e.touches
|
|
123
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
124
|
+
touchRef.current.containerWidth = rect.width
|
|
125
|
+
touchRef.current.containerHeight = rect.height
|
|
126
|
+
|
|
127
|
+
// Double tap to zoom
|
|
128
|
+
if (touches.length === 1) {
|
|
129
|
+
const now = Date.now()
|
|
130
|
+
const timeSinceLastTap = now - touchRef.current.lastTap
|
|
131
|
+
|
|
132
|
+
if (timeSinceLastTap < 300) {
|
|
133
|
+
e.preventDefault()
|
|
134
|
+
if (isZoomed) {
|
|
135
|
+
setState(INITIAL_STATE)
|
|
136
|
+
onZoomChange?.(false)
|
|
137
|
+
} else {
|
|
138
|
+
const x = touches[0].clientX - rect.left - rect.width / 2
|
|
139
|
+
const y = touches[0].clientY - rect.top - rect.height / 2
|
|
140
|
+
setState({ scale: desktopScale, x: -x * 0.5, y: -y * 0.5 })
|
|
141
|
+
onZoomChange?.(true)
|
|
142
|
+
}
|
|
143
|
+
touchRef.current.lastTap = 0
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
touchRef.current.lastTap = now
|
|
148
|
+
|
|
149
|
+
// Single finger - start pan (if zoomed)
|
|
150
|
+
if (isZoomed) {
|
|
151
|
+
touchRef.current.initialX = touches[0].clientX - state.x
|
|
152
|
+
touchRef.current.initialY = touches[0].clientY - state.y
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Two finger pinch
|
|
157
|
+
if (touches.length === 2) {
|
|
158
|
+
e.preventDefault()
|
|
159
|
+
touchRef.current.isPinching = true
|
|
160
|
+
touchRef.current.initialDistance = getDistance(touches)
|
|
161
|
+
touchRef.current.initialScale = state.scale
|
|
162
|
+
|
|
163
|
+
const center = getCenter(touches)
|
|
164
|
+
touchRef.current.centerX = center.x
|
|
165
|
+
touchRef.current.centerY = center.y
|
|
166
|
+
touchRef.current.initialX = state.x
|
|
167
|
+
touchRef.current.initialY = state.y
|
|
168
|
+
}
|
|
169
|
+
}, [isZoomed, state, getDistance, getCenter, desktopScale, onZoomChange])
|
|
170
|
+
|
|
171
|
+
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
|
172
|
+
const touches = e.touches
|
|
173
|
+
|
|
174
|
+
// Pinch zoom
|
|
175
|
+
if (touches.length === 2 && touchRef.current.isPinching) {
|
|
176
|
+
e.preventDefault()
|
|
177
|
+
|
|
178
|
+
const currentDistance = getDistance(touches)
|
|
179
|
+
const scaleDelta = currentDistance / touchRef.current.initialDistance
|
|
180
|
+
let newScale = touchRef.current.initialScale * scaleDelta
|
|
181
|
+
newScale = Math.max(minScale, Math.min(maxScale, newScale))
|
|
182
|
+
|
|
183
|
+
const center = getCenter(touches)
|
|
184
|
+
const dx = center.x - touchRef.current.centerX
|
|
185
|
+
const dy = center.y - touchRef.current.centerY
|
|
186
|
+
|
|
187
|
+
const { x: clampedX, y: clampedY } = clampPan(
|
|
188
|
+
touchRef.current.initialX + dx,
|
|
189
|
+
touchRef.current.initialY + dy,
|
|
190
|
+
newScale
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
setState({ scale: newScale, x: clampedX, y: clampedY })
|
|
194
|
+
|
|
195
|
+
if (newScale > ZOOM_THRESHOLD !== isZoomed) {
|
|
196
|
+
onZoomChange?.(newScale > ZOOM_THRESHOLD)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Pan when zoomed (single finger)
|
|
201
|
+
if (touches.length === 1 && isZoomed && !touchRef.current.isPinching) {
|
|
202
|
+
e.preventDefault()
|
|
203
|
+
|
|
204
|
+
const newX = touches[0].clientX - touchRef.current.initialX
|
|
205
|
+
const newY = touches[0].clientY - touchRef.current.initialY
|
|
206
|
+
const { x: clampedX, y: clampedY } = clampPan(newX, newY, state.scale)
|
|
207
|
+
|
|
208
|
+
setState((prev) => ({ ...prev, x: clampedX, y: clampedY }))
|
|
209
|
+
}
|
|
210
|
+
}, [isZoomed, state.scale, minScale, maxScale, getDistance, getCenter, clampPan, onZoomChange])
|
|
211
|
+
|
|
212
|
+
const handleTouchEnd = useCallback((e: React.TouchEvent) => {
|
|
213
|
+
if (e.touches.length < 2) {
|
|
214
|
+
touchRef.current.isPinching = false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Snap to 1 if close
|
|
218
|
+
if (state.scale < 1.1 && state.scale > 0.9) {
|
|
219
|
+
setState(INITIAL_STATE)
|
|
220
|
+
onZoomChange?.(false)
|
|
221
|
+
}
|
|
222
|
+
}, [state.scale, onZoomChange])
|
|
223
|
+
|
|
224
|
+
// ========== MOUSE HANDLERS (DESKTOP) ==========
|
|
225
|
+
|
|
226
|
+
const handleClick = useCallback((e: React.MouseEvent) => {
|
|
227
|
+
// Skip if we were dragging
|
|
228
|
+
if (isDragging) return
|
|
229
|
+
|
|
230
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
231
|
+
touchRef.current.containerWidth = rect.width
|
|
232
|
+
touchRef.current.containerHeight = rect.height
|
|
233
|
+
|
|
234
|
+
if (isZoomed) {
|
|
235
|
+
setState(INITIAL_STATE)
|
|
236
|
+
onZoomChange?.(false)
|
|
237
|
+
} else {
|
|
238
|
+
// Zoom to click position
|
|
239
|
+
const x = ((e.clientX - rect.left) / rect.width - 0.5) * -100
|
|
240
|
+
const y = ((e.clientY - rect.top) / rect.height - 0.5) * -100
|
|
241
|
+
setState({ scale: desktopScale, x, y })
|
|
242
|
+
onZoomChange?.(true)
|
|
243
|
+
}
|
|
244
|
+
}, [isZoomed, isDragging, desktopScale, onZoomChange])
|
|
245
|
+
|
|
246
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
247
|
+
if (!isZoomed) return
|
|
248
|
+
e.preventDefault()
|
|
249
|
+
setIsDragging(true)
|
|
250
|
+
touchRef.current.dragStartX = e.clientX - state.x
|
|
251
|
+
touchRef.current.dragStartY = e.clientY - state.y
|
|
252
|
+
|
|
253
|
+
const rect = e.currentTarget.getBoundingClientRect()
|
|
254
|
+
touchRef.current.containerWidth = rect.width
|
|
255
|
+
touchRef.current.containerHeight = rect.height
|
|
256
|
+
}, [isZoomed, state.x, state.y])
|
|
257
|
+
|
|
258
|
+
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
|
259
|
+
if (!isDragging) return
|
|
260
|
+
|
|
261
|
+
const newX = e.clientX - touchRef.current.dragStartX
|
|
262
|
+
const newY = e.clientY - touchRef.current.dragStartY
|
|
263
|
+
const { x: clampedX, y: clampedY } = clampPan(newX, newY, state.scale)
|
|
264
|
+
|
|
265
|
+
setState((prev) => ({ ...prev, x: clampedX, y: clampedY }))
|
|
266
|
+
}, [isDragging, state.scale, clampPan])
|
|
267
|
+
|
|
268
|
+
const handleMouseUp = useCallback(() => {
|
|
269
|
+
// Small delay to prevent click from triggering after drag
|
|
270
|
+
setTimeout(() => setIsDragging(false), 10)
|
|
271
|
+
}, [])
|
|
272
|
+
|
|
273
|
+
// ========== COMMON ACTIONS ==========
|
|
274
|
+
|
|
275
|
+
const reset = useCallback(() => {
|
|
276
|
+
setState(INITIAL_STATE)
|
|
277
|
+
setIsDragging(false)
|
|
278
|
+
onZoomChange?.(false)
|
|
279
|
+
}, [onZoomChange])
|
|
280
|
+
|
|
281
|
+
const zoomTo = useCallback((scale: number, x = 0, y = 0) => {
|
|
282
|
+
const clampedScale = Math.max(minScale, Math.min(maxScale, scale))
|
|
283
|
+
const { x: clampedX, y: clampedY } = clampPan(x, y, clampedScale)
|
|
284
|
+
setState({ scale: clampedScale, x: clampedX, y: clampedY })
|
|
285
|
+
onZoomChange?.(clampedScale > ZOOM_THRESHOLD)
|
|
286
|
+
}, [minScale, maxScale, clampPan, onZoomChange])
|
|
287
|
+
|
|
288
|
+
const toggleZoom = useCallback(() => {
|
|
289
|
+
if (isZoomed) {
|
|
290
|
+
setState(INITIAL_STATE)
|
|
291
|
+
onZoomChange?.(false)
|
|
292
|
+
} else {
|
|
293
|
+
setState({ scale: desktopScale, x: 0, y: 0 })
|
|
294
|
+
onZoomChange?.(true)
|
|
295
|
+
}
|
|
296
|
+
}, [isZoomed, desktopScale, onZoomChange])
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
state,
|
|
300
|
+
isZoomed,
|
|
301
|
+
isDragging,
|
|
302
|
+
handlers: {
|
|
303
|
+
onTouchStart: handleTouchStart,
|
|
304
|
+
onTouchMove: handleTouchMove,
|
|
305
|
+
onTouchEnd: handleTouchEnd,
|
|
306
|
+
onClick: handleClick,
|
|
307
|
+
onMouseDown: handleMouseDown,
|
|
308
|
+
onMouseMove: handleMouseMove,
|
|
309
|
+
onMouseUp: handleMouseUp,
|
|
310
|
+
onMouseLeave: handleMouseUp,
|
|
311
|
+
},
|
|
312
|
+
reset,
|
|
313
|
+
zoomTo,
|
|
314
|
+
toggleZoom,
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export {
|
|
3
|
+
Gallery,
|
|
4
|
+
GalleryCompact,
|
|
5
|
+
GalleryGrid,
|
|
6
|
+
GalleryImage,
|
|
7
|
+
GalleryVideo,
|
|
8
|
+
GalleryMedia,
|
|
9
|
+
GalleryThumbnails,
|
|
10
|
+
GalleryThumbnailsVirtual,
|
|
11
|
+
GalleryLightbox,
|
|
12
|
+
} from './components'
|
|
13
|
+
export type { GalleryCompactProps, GalleryGridProps, GalleryGridLayout } from './components'
|
|
14
|
+
|
|
15
|
+
// Hooks
|
|
16
|
+
export {
|
|
17
|
+
useGallery,
|
|
18
|
+
useSwipe,
|
|
19
|
+
usePreloadImages,
|
|
20
|
+
preloadImage,
|
|
21
|
+
preloadImages,
|
|
22
|
+
usePinchZoom,
|
|
23
|
+
useVirtualList,
|
|
24
|
+
useImageDimensions,
|
|
25
|
+
clearDimensionsCache,
|
|
26
|
+
getCachedDimensions,
|
|
27
|
+
} from './hooks'
|
|
28
|
+
export type {
|
|
29
|
+
UseGalleryOptions,
|
|
30
|
+
UseSwipeOptions,
|
|
31
|
+
PinchZoomState,
|
|
32
|
+
UsePinchZoomOptions,
|
|
33
|
+
UsePinchZoomReturn,
|
|
34
|
+
VirtualListOptions,
|
|
35
|
+
VirtualListReturn,
|
|
36
|
+
VirtualItem,
|
|
37
|
+
ImageWithDimensions,
|
|
38
|
+
UseImageDimensionsResult,
|
|
39
|
+
UseImageDimensionsOptions,
|
|
40
|
+
} from './hooks'
|
|
41
|
+
|
|
42
|
+
// Utils
|
|
43
|
+
export {
|
|
44
|
+
getOrientation,
|
|
45
|
+
getAspectRatio,
|
|
46
|
+
analyzeImage,
|
|
47
|
+
analyzeImages,
|
|
48
|
+
} from './utils'
|
|
49
|
+
export type {
|
|
50
|
+
ImageOrientation,
|
|
51
|
+
AnalyzedImage,
|
|
52
|
+
} from './utils'
|
|
53
|
+
|
|
54
|
+
// Types
|
|
55
|
+
export type {
|
|
56
|
+
GalleryMediaType,
|
|
57
|
+
GalleryPreviewMode,
|
|
58
|
+
GalleryMediaItem,
|
|
59
|
+
GalleryState,
|
|
60
|
+
GalleryActions,
|
|
61
|
+
GalleryContextValue,
|
|
62
|
+
GalleryProps,
|
|
63
|
+
GalleryImageProps,
|
|
64
|
+
GalleryThumbnailsProps,
|
|
65
|
+
GalleryLightboxProps,
|
|
66
|
+
} from './types'
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Media type for gallery items
|
|
5
|
+
*/
|
|
6
|
+
export type GalleryMediaType = 'image' | 'video'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single item in the gallery (image or video)
|
|
10
|
+
*/
|
|
11
|
+
export interface GalleryMediaItem {
|
|
12
|
+
/** Unique identifier */
|
|
13
|
+
id: string
|
|
14
|
+
/** Full-size image/poster URL */
|
|
15
|
+
src: string
|
|
16
|
+
/** Thumbnail URL (optional, falls back to src) */
|
|
17
|
+
thumbnail?: string
|
|
18
|
+
/** Alt text for accessibility */
|
|
19
|
+
alt?: string
|
|
20
|
+
/** Image width (optional, for aspect ratio) */
|
|
21
|
+
width?: number
|
|
22
|
+
/** Image height (optional, for aspect ratio) */
|
|
23
|
+
height?: number
|
|
24
|
+
/** Media type (default: 'image') */
|
|
25
|
+
type?: GalleryMediaType
|
|
26
|
+
/** Video URL (required if type is 'video') */
|
|
27
|
+
videoSrc?: string
|
|
28
|
+
/** Video MIME type */
|
|
29
|
+
videoType?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gallery state
|
|
34
|
+
*/
|
|
35
|
+
export interface GalleryState {
|
|
36
|
+
/** Currently selected image index */
|
|
37
|
+
currentIndex: number
|
|
38
|
+
/** Whether lightbox is open */
|
|
39
|
+
isLightboxOpen: boolean
|
|
40
|
+
/** Whether image is zoomed in lightbox */
|
|
41
|
+
isZoomed: boolean
|
|
42
|
+
/** Whether current image is loading */
|
|
43
|
+
isLoading: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Gallery actions
|
|
48
|
+
*/
|
|
49
|
+
export interface GalleryActions {
|
|
50
|
+
/** Go to specific image by index */
|
|
51
|
+
goTo: (index: number) => void
|
|
52
|
+
/** Go to next image */
|
|
53
|
+
next: () => void
|
|
54
|
+
/** Go to previous image */
|
|
55
|
+
prev: () => void
|
|
56
|
+
/** Open lightbox at specific index */
|
|
57
|
+
openLightbox: (index?: number) => void
|
|
58
|
+
/** Close lightbox */
|
|
59
|
+
closeLightbox: () => void
|
|
60
|
+
/** Toggle zoom in lightbox */
|
|
61
|
+
toggleZoom: () => void
|
|
62
|
+
/** Set loading state */
|
|
63
|
+
setLoading: (loading: boolean) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gallery context value
|
|
68
|
+
*/
|
|
69
|
+
export interface GalleryContextValue extends GalleryState, GalleryActions {
|
|
70
|
+
/** All images in gallery */
|
|
71
|
+
images: GalleryMediaItem[]
|
|
72
|
+
/** Current image */
|
|
73
|
+
currentImage: GalleryMediaItem | null
|
|
74
|
+
/** Total number of images */
|
|
75
|
+
total: number
|
|
76
|
+
/** Whether there are multiple images */
|
|
77
|
+
hasMultiple: boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Preview mode for gallery
|
|
82
|
+
*/
|
|
83
|
+
export type GalleryPreviewMode = 'carousel' | 'grid'
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Props for Gallery component
|
|
87
|
+
*/
|
|
88
|
+
export interface GalleryProps {
|
|
89
|
+
/** Array of images to display */
|
|
90
|
+
images: GalleryMediaItem[]
|
|
91
|
+
/** Initial image index */
|
|
92
|
+
initialIndex?: number
|
|
93
|
+
/** Preview mode: carousel (default) or grid */
|
|
94
|
+
previewMode?: GalleryPreviewMode
|
|
95
|
+
/** Number of images to show in grid preview (default: 5) */
|
|
96
|
+
previewCount?: number
|
|
97
|
+
/** Show thumbnail strip (carousel mode only) */
|
|
98
|
+
showThumbnails?: boolean
|
|
99
|
+
/** Show navigation controls (carousel mode only) */
|
|
100
|
+
showControls?: boolean
|
|
101
|
+
/** Show image counter */
|
|
102
|
+
showCounter?: boolean
|
|
103
|
+
/** Aspect ratio for main image (e.g., 16/9, 4/3) */
|
|
104
|
+
aspectRatio?: number
|
|
105
|
+
/** Enable lightbox on click */
|
|
106
|
+
enableLightbox?: boolean
|
|
107
|
+
/** Enable keyboard navigation */
|
|
108
|
+
enableKeyboard?: boolean
|
|
109
|
+
/** Callback when image changes */
|
|
110
|
+
onImageChange?: (index: number, image: GalleryMediaItem) => void
|
|
111
|
+
/** Callback when lightbox opens */
|
|
112
|
+
onLightboxOpen?: () => void
|
|
113
|
+
/** Callback when lightbox closes */
|
|
114
|
+
onLightboxClose?: () => void
|
|
115
|
+
/** Custom empty state */
|
|
116
|
+
emptyState?: ReactNode
|
|
117
|
+
/** Custom loading placeholder */
|
|
118
|
+
loadingPlaceholder?: ReactNode
|
|
119
|
+
/** Additional CSS class */
|
|
120
|
+
className?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Props for GalleryImage component
|
|
125
|
+
*/
|
|
126
|
+
export interface GalleryImageProps {
|
|
127
|
+
/** Image data */
|
|
128
|
+
image: GalleryMediaItem
|
|
129
|
+
/** Whether to show loading skeleton */
|
|
130
|
+
showLoading?: boolean
|
|
131
|
+
/** On image load callback */
|
|
132
|
+
onLoad?: () => void
|
|
133
|
+
/** On image error callback */
|
|
134
|
+
onError?: () => void
|
|
135
|
+
/** On click callback */
|
|
136
|
+
onClick?: () => void
|
|
137
|
+
/** Priority loading */
|
|
138
|
+
priority?: boolean
|
|
139
|
+
/** Additional CSS class */
|
|
140
|
+
className?: string
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Props for GalleryThumbnails component
|
|
145
|
+
*/
|
|
146
|
+
export interface GalleryThumbnailsProps {
|
|
147
|
+
/** All images */
|
|
148
|
+
images: GalleryMediaItem[]
|
|
149
|
+
/** Currently selected index */
|
|
150
|
+
currentIndex: number
|
|
151
|
+
/** On thumbnail click */
|
|
152
|
+
onSelect: (index: number) => void
|
|
153
|
+
/** Thumbnail size */
|
|
154
|
+
size?: 'sm' | 'md' | 'lg'
|
|
155
|
+
/** Additional CSS class */
|
|
156
|
+
className?: string
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Props for GalleryLightbox component
|
|
161
|
+
*/
|
|
162
|
+
export interface GalleryLightboxProps {
|
|
163
|
+
/** Whether lightbox is open */
|
|
164
|
+
open: boolean
|
|
165
|
+
/** On close callback */
|
|
166
|
+
onClose: () => void
|
|
167
|
+
/** All images */
|
|
168
|
+
images: GalleryMediaItem[]
|
|
169
|
+
/** Current image index */
|
|
170
|
+
currentIndex: number
|
|
171
|
+
/** On index change */
|
|
172
|
+
onIndexChange: (index: number) => void
|
|
173
|
+
/** Show thumbnails in lightbox */
|
|
174
|
+
showThumbnails?: boolean
|
|
175
|
+
/** Enable zoom */
|
|
176
|
+
enableZoom?: boolean
|
|
177
|
+
/** Enable download button */
|
|
178
|
+
enableDownload?: boolean
|
|
179
|
+
/** Enable share button */
|
|
180
|
+
enableShare?: boolean
|
|
181
|
+
/** Title to display */
|
|
182
|
+
title?: string
|
|
183
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { GalleryMediaItem } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Image orientation types
|
|
5
|
+
*/
|
|
6
|
+
export type ImageOrientation = 'landscape' | 'portrait' | 'square'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Analyzed image with orientation info
|
|
10
|
+
*/
|
|
11
|
+
export interface AnalyzedImage extends GalleryMediaItem {
|
|
12
|
+
orientation: ImageOrientation
|
|
13
|
+
aspectRatio: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get orientation from width/height
|
|
18
|
+
*/
|
|
19
|
+
export function getOrientation(width?: number, height?: number): ImageOrientation {
|
|
20
|
+
if (!width || !height) return 'landscape' // default assumption
|
|
21
|
+
|
|
22
|
+
const ratio = width / height
|
|
23
|
+
if (ratio > 1.2) return 'landscape'
|
|
24
|
+
if (ratio < 0.8) return 'portrait'
|
|
25
|
+
return 'square'
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Calculate aspect ratio from width/height
|
|
30
|
+
*/
|
|
31
|
+
export function getAspectRatio(width?: number, height?: number): number {
|
|
32
|
+
if (!width || !height) return 16 / 9 // default
|
|
33
|
+
return width / height
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Analyze single image
|
|
38
|
+
*/
|
|
39
|
+
export function analyzeImage(image: GalleryMediaItem): AnalyzedImage {
|
|
40
|
+
return {
|
|
41
|
+
...image,
|
|
42
|
+
orientation: getOrientation(image.width, image.height),
|
|
43
|
+
aspectRatio: getAspectRatio(image.width, image.height),
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Analyze array of images
|
|
49
|
+
*/
|
|
50
|
+
export function analyzeImages(images: GalleryMediaItem[]): AnalyzedImage[] {
|
|
51
|
+
return images.map(analyzeImage)
|
|
52
|
+
}
|