@djangocfg/ui-tools 2.1.110 → 2.1.112
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,222 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useMemo, useRef, useState } from 'react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n'
|
|
6
|
+
import { Play, Pause, Volume2, VolumeX, Maximize, AlertCircle } from 'lucide-react'
|
|
7
|
+
import type { GalleryMediaItem } from '../types'
|
|
8
|
+
|
|
9
|
+
export interface GalleryVideoProps {
|
|
10
|
+
/** Video data */
|
|
11
|
+
media: GalleryMediaItem
|
|
12
|
+
/** Whether video should autoplay */
|
|
13
|
+
autoPlay?: boolean
|
|
14
|
+
/** Whether video should be muted by default */
|
|
15
|
+
muted?: boolean
|
|
16
|
+
/** Whether video should loop */
|
|
17
|
+
loop?: boolean
|
|
18
|
+
/** Show controls overlay */
|
|
19
|
+
showControls?: boolean
|
|
20
|
+
/** Additional CSS class */
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* GalleryVideo - Video player for gallery items
|
|
26
|
+
*/
|
|
27
|
+
export const GalleryVideo = memo(function GalleryVideo({
|
|
28
|
+
media,
|
|
29
|
+
autoPlay = false,
|
|
30
|
+
muted = true,
|
|
31
|
+
loop = true,
|
|
32
|
+
showControls = true,
|
|
33
|
+
className,
|
|
34
|
+
}: GalleryVideoProps) {
|
|
35
|
+
const t = useTypedT<I18nTranslations>()
|
|
36
|
+
const videoRef = useRef<HTMLVideoElement>(null)
|
|
37
|
+
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
|
38
|
+
const [isMuted, setIsMuted] = useState(muted)
|
|
39
|
+
const [showOverlay, setShowOverlay] = useState(true)
|
|
40
|
+
const [hasError, setHasError] = useState(false)
|
|
41
|
+
|
|
42
|
+
const labels = useMemo(() => ({
|
|
43
|
+
play: t('tools.video.play'),
|
|
44
|
+
fullscreen: t('tools.video.fullscreen'),
|
|
45
|
+
unavailable: t('tools.video.unavailable'),
|
|
46
|
+
}), [t])
|
|
47
|
+
|
|
48
|
+
const handlePlayPause = useCallback((e?: React.MouseEvent) => {
|
|
49
|
+
e?.stopPropagation()
|
|
50
|
+
const video = videoRef.current
|
|
51
|
+
if (!video) return
|
|
52
|
+
|
|
53
|
+
if (video.paused) {
|
|
54
|
+
video.play()
|
|
55
|
+
setIsPlaying(true)
|
|
56
|
+
} else {
|
|
57
|
+
video.pause()
|
|
58
|
+
setIsPlaying(false)
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
const handleMuteToggle = useCallback((e: React.MouseEvent) => {
|
|
63
|
+
e.stopPropagation()
|
|
64
|
+
const video = videoRef.current
|
|
65
|
+
if (!video) return
|
|
66
|
+
|
|
67
|
+
video.muted = !video.muted
|
|
68
|
+
setIsMuted(video.muted)
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
const handleFullscreen = useCallback((e: React.MouseEvent) => {
|
|
72
|
+
e.stopPropagation()
|
|
73
|
+
const video = videoRef.current
|
|
74
|
+
if (!video) return
|
|
75
|
+
|
|
76
|
+
if (video.requestFullscreen) {
|
|
77
|
+
video.requestFullscreen()
|
|
78
|
+
}
|
|
79
|
+
}, [])
|
|
80
|
+
|
|
81
|
+
const handleVideoClick = useCallback((e: React.MouseEvent) => {
|
|
82
|
+
e.stopPropagation()
|
|
83
|
+
handlePlayPause()
|
|
84
|
+
}, [handlePlayPause])
|
|
85
|
+
|
|
86
|
+
const handleMouseEnter = useCallback(() => {
|
|
87
|
+
setShowOverlay(true)
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
90
|
+
const handleMouseLeave = useCallback(() => {
|
|
91
|
+
if (isPlaying) {
|
|
92
|
+
setShowOverlay(false)
|
|
93
|
+
}
|
|
94
|
+
}, [isPlaying])
|
|
95
|
+
|
|
96
|
+
const handleError = useCallback(() => {
|
|
97
|
+
setHasError(true)
|
|
98
|
+
setIsPlaying(false)
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
const handleContainerClick = useCallback((e: React.MouseEvent) => {
|
|
102
|
+
e.stopPropagation()
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
if (!media.videoSrc) {
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Error state - show poster with error overlay
|
|
110
|
+
if (hasError) {
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
className={cn('relative w-full h-full bg-black', className)}
|
|
114
|
+
onClick={handleContainerClick}
|
|
115
|
+
>
|
|
116
|
+
{media.src && (
|
|
117
|
+
<img
|
|
118
|
+
src={media.src}
|
|
119
|
+
alt={media.alt || 'Video thumbnail'}
|
|
120
|
+
className="w-full h-full object-contain opacity-50"
|
|
121
|
+
/>
|
|
122
|
+
)}
|
|
123
|
+
<div className="absolute inset-0 flex flex-col items-center justify-center bg-black/50">
|
|
124
|
+
<AlertCircle className="w-12 h-12 text-white/70 mb-2" />
|
|
125
|
+
<span className="text-white/70 text-sm">{labels.unavailable}</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="absolute top-3 left-3 px-2 py-1 bg-black/60 rounded text-white text-xs font-medium">
|
|
128
|
+
Video
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
className={cn('relative w-full h-full bg-black', className)}
|
|
137
|
+
onMouseEnter={handleMouseEnter}
|
|
138
|
+
onMouseLeave={handleMouseLeave}
|
|
139
|
+
onClick={handleContainerClick}
|
|
140
|
+
>
|
|
141
|
+
<video
|
|
142
|
+
ref={videoRef}
|
|
143
|
+
className="w-full h-full object-contain"
|
|
144
|
+
src={media.videoSrc}
|
|
145
|
+
poster={media.src}
|
|
146
|
+
autoPlay={autoPlay}
|
|
147
|
+
muted={muted}
|
|
148
|
+
loop={loop}
|
|
149
|
+
playsInline
|
|
150
|
+
onClick={handleVideoClick}
|
|
151
|
+
onError={handleError}
|
|
152
|
+
>
|
|
153
|
+
{media.videoType && (
|
|
154
|
+
<source src={media.videoSrc} type={media.videoType} />
|
|
155
|
+
)}
|
|
156
|
+
</video>
|
|
157
|
+
|
|
158
|
+
{/* Play button overlay */}
|
|
159
|
+
{!isPlaying && (
|
|
160
|
+
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
className="w-16 h-16 rounded-full bg-white/90 flex items-center justify-center shadow-lg hover:bg-white transition-colors"
|
|
164
|
+
onClick={handlePlayPause}
|
|
165
|
+
aria-label={labels.play}
|
|
166
|
+
>
|
|
167
|
+
<Play className="w-8 h-8 text-black ml-1" />
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{/* Controls overlay */}
|
|
173
|
+
{showControls && showOverlay && isPlaying && (
|
|
174
|
+
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/60 to-transparent">
|
|
175
|
+
<div className="flex items-center gap-3">
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className="p-2 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
|
|
179
|
+
onClick={handlePlayPause}
|
|
180
|
+
aria-label={isPlaying ? 'Pause' : 'Play'}
|
|
181
|
+
>
|
|
182
|
+
{isPlaying ? (
|
|
183
|
+
<Pause className="w-5 h-5" />
|
|
184
|
+
) : (
|
|
185
|
+
<Play className="w-5 h-5" />
|
|
186
|
+
)}
|
|
187
|
+
</button>
|
|
188
|
+
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="p-2 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
|
|
192
|
+
onClick={handleMuteToggle}
|
|
193
|
+
aria-label={isMuted ? 'Unmute' : 'Mute'}
|
|
194
|
+
>
|
|
195
|
+
{isMuted ? (
|
|
196
|
+
<VolumeX className="w-5 h-5" />
|
|
197
|
+
) : (
|
|
198
|
+
<Volume2 className="w-5 h-5" />
|
|
199
|
+
)}
|
|
200
|
+
</button>
|
|
201
|
+
|
|
202
|
+
<div className="flex-1" />
|
|
203
|
+
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
className="p-2 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
|
|
207
|
+
onClick={handleFullscreen}
|
|
208
|
+
aria-label={labels.fullscreen}
|
|
209
|
+
>
|
|
210
|
+
<Maximize className="w-5 h-5" />
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{/* Video indicator badge */}
|
|
217
|
+
<div className="absolute top-3 left-3 px-2 py-1 bg-black/60 rounded text-white text-xs font-medium">
|
|
218
|
+
Video
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)
|
|
222
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Gallery } from './Gallery'
|
|
2
|
+
export { GalleryCarousel } from './GalleryCarousel'
|
|
3
|
+
export type { GalleryCarouselProps } from './GalleryCarousel'
|
|
4
|
+
export { GalleryCompact } from './GalleryCompact'
|
|
5
|
+
export type { GalleryCompactProps } from './GalleryCompact'
|
|
6
|
+
export { GalleryGrid } from './GalleryGrid'
|
|
7
|
+
export type { GalleryGridProps, GalleryGridLayout } from './GalleryGrid'
|
|
8
|
+
export { GalleryImage } from './GalleryImage'
|
|
9
|
+
export { GalleryVideo } from './GalleryVideo'
|
|
10
|
+
export { GalleryMedia } from './GalleryMedia'
|
|
11
|
+
export { GalleryThumbnails } from './GalleryThumbnails'
|
|
12
|
+
export { GalleryThumbnailsVirtual } from './GalleryThumbnailsVirtual'
|
|
13
|
+
export { GalleryLightbox } from './GalleryLightbox'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export { useGallery } from './useGallery'
|
|
2
|
+
export type { UseGalleryOptions } from './useGallery'
|
|
3
|
+
|
|
4
|
+
export { useSwipe } from './useSwipe'
|
|
5
|
+
export type { UseSwipeOptions } from './useSwipe'
|
|
6
|
+
|
|
7
|
+
export { usePreloadImages, preloadImage, preloadImages } from './usePreloadImages'
|
|
8
|
+
|
|
9
|
+
export { useZoom } from './useZoom'
|
|
10
|
+
export type { ZoomState, UseZoomOptions, UseZoomReturn } from './useZoom'
|
|
11
|
+
|
|
12
|
+
export { usePinchZoom } from './usePinchZoom'
|
|
13
|
+
export type { PinchZoomState, UsePinchZoomOptions, UsePinchZoomReturn } from './usePinchZoom'
|
|
14
|
+
|
|
15
|
+
export { useVirtualList } from './useVirtualList'
|
|
16
|
+
export type { VirtualListOptions, VirtualListReturn, VirtualItem } from './useVirtualList'
|
|
17
|
+
|
|
18
|
+
export { useImageDimensions, clearDimensionsCache, getCachedDimensions } from './useImageDimensions'
|
|
19
|
+
export type {
|
|
20
|
+
ImageWithDimensions,
|
|
21
|
+
UseImageDimensionsResult,
|
|
22
|
+
UseImageDimensionsOptions,
|
|
23
|
+
} from './useImageDimensions'
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useMemo, useEffect } from 'react'
|
|
4
|
+
import type { GalleryMediaItem, GalleryContextValue } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface UseGalleryOptions {
|
|
7
|
+
/** Array of images */
|
|
8
|
+
images: GalleryMediaItem[]
|
|
9
|
+
/** Initial selected index */
|
|
10
|
+
initialIndex?: number
|
|
11
|
+
/** Loop navigation */
|
|
12
|
+
loop?: boolean
|
|
13
|
+
/** Callback when image changes */
|
|
14
|
+
onImageChange?: (index: number, image: GalleryMediaItem) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook for managing gallery state
|
|
19
|
+
*/
|
|
20
|
+
export function useGallery({
|
|
21
|
+
images,
|
|
22
|
+
initialIndex = 0,
|
|
23
|
+
loop = true,
|
|
24
|
+
onImageChange,
|
|
25
|
+
}: UseGalleryOptions): GalleryContextValue {
|
|
26
|
+
const [currentIndex, setCurrentIndex] = useState(initialIndex)
|
|
27
|
+
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
|
|
28
|
+
const [isZoomed, setIsZoomed] = useState(false)
|
|
29
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
30
|
+
|
|
31
|
+
const total = images.length
|
|
32
|
+
const hasMultiple = total > 1
|
|
33
|
+
const currentImage = images[currentIndex] ?? null
|
|
34
|
+
|
|
35
|
+
// Reset state when images change
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (currentIndex >= total) {
|
|
38
|
+
setCurrentIndex(Math.max(0, total - 1))
|
|
39
|
+
}
|
|
40
|
+
}, [total, currentIndex])
|
|
41
|
+
|
|
42
|
+
// Reset zoom when image changes
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setIsZoomed(false)
|
|
45
|
+
setIsLoading(true)
|
|
46
|
+
}, [currentIndex])
|
|
47
|
+
|
|
48
|
+
const goTo = useCallback(
|
|
49
|
+
(index: number) => {
|
|
50
|
+
if (index < 0 || index >= total) return
|
|
51
|
+
setCurrentIndex(index)
|
|
52
|
+
onImageChange?.(index, images[index]!)
|
|
53
|
+
},
|
|
54
|
+
[total, images, onImageChange]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const next = useCallback(() => {
|
|
58
|
+
if (!hasMultiple) return
|
|
59
|
+
const nextIndex = loop
|
|
60
|
+
? (currentIndex + 1) % total
|
|
61
|
+
: Math.min(currentIndex + 1, total - 1)
|
|
62
|
+
if (nextIndex !== currentIndex) {
|
|
63
|
+
goTo(nextIndex)
|
|
64
|
+
}
|
|
65
|
+
}, [currentIndex, total, hasMultiple, loop, goTo])
|
|
66
|
+
|
|
67
|
+
const prev = useCallback(() => {
|
|
68
|
+
if (!hasMultiple) return
|
|
69
|
+
const prevIndex = loop
|
|
70
|
+
? (currentIndex - 1 + total) % total
|
|
71
|
+
: Math.max(currentIndex - 1, 0)
|
|
72
|
+
if (prevIndex !== currentIndex) {
|
|
73
|
+
goTo(prevIndex)
|
|
74
|
+
}
|
|
75
|
+
}, [currentIndex, total, hasMultiple, loop, goTo])
|
|
76
|
+
|
|
77
|
+
const openLightbox = useCallback((index?: number) => {
|
|
78
|
+
if (index !== undefined) {
|
|
79
|
+
setCurrentIndex(index)
|
|
80
|
+
}
|
|
81
|
+
setIsLightboxOpen(true)
|
|
82
|
+
setIsZoomed(false)
|
|
83
|
+
}, [])
|
|
84
|
+
|
|
85
|
+
const closeLightbox = useCallback(() => {
|
|
86
|
+
setIsLightboxOpen(false)
|
|
87
|
+
setIsZoomed(false)
|
|
88
|
+
}, [])
|
|
89
|
+
|
|
90
|
+
const toggleZoom = useCallback(() => {
|
|
91
|
+
setIsZoomed((prev) => !prev)
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
const setLoadingState = useCallback((loading: boolean) => {
|
|
95
|
+
setIsLoading(loading)
|
|
96
|
+
}, [])
|
|
97
|
+
|
|
98
|
+
return useMemo(
|
|
99
|
+
() => ({
|
|
100
|
+
// State
|
|
101
|
+
currentIndex,
|
|
102
|
+
isLightboxOpen,
|
|
103
|
+
isZoomed,
|
|
104
|
+
isLoading,
|
|
105
|
+
// Computed
|
|
106
|
+
images,
|
|
107
|
+
currentImage,
|
|
108
|
+
total,
|
|
109
|
+
hasMultiple,
|
|
110
|
+
// Actions
|
|
111
|
+
goTo,
|
|
112
|
+
next,
|
|
113
|
+
prev,
|
|
114
|
+
openLightbox,
|
|
115
|
+
closeLightbox,
|
|
116
|
+
toggleZoom,
|
|
117
|
+
setLoading: setLoadingState,
|
|
118
|
+
}),
|
|
119
|
+
[
|
|
120
|
+
currentIndex,
|
|
121
|
+
isLightboxOpen,
|
|
122
|
+
isZoomed,
|
|
123
|
+
isLoading,
|
|
124
|
+
images,
|
|
125
|
+
currentImage,
|
|
126
|
+
total,
|
|
127
|
+
hasMultiple,
|
|
128
|
+
goTo,
|
|
129
|
+
next,
|
|
130
|
+
prev,
|
|
131
|
+
openLightbox,
|
|
132
|
+
closeLightbox,
|
|
133
|
+
toggleZoom,
|
|
134
|
+
setLoadingState,
|
|
135
|
+
]
|
|
136
|
+
)
|
|
137
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
4
|
+
import type { GalleryMediaItem } from '../types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Image with loaded dimensions
|
|
8
|
+
*/
|
|
9
|
+
export interface ImageWithDimensions extends GalleryMediaItem {
|
|
10
|
+
width: number
|
|
11
|
+
height: number
|
|
12
|
+
loaded: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of useImageDimensions hook
|
|
17
|
+
*/
|
|
18
|
+
export interface UseImageDimensionsResult {
|
|
19
|
+
/** Images with dimensions (loaded or from props) */
|
|
20
|
+
images: ImageWithDimensions[]
|
|
21
|
+
/** Whether all images have dimensions */
|
|
22
|
+
isReady: boolean
|
|
23
|
+
/** Whether currently loading */
|
|
24
|
+
isLoading: boolean
|
|
25
|
+
/** Number of images loaded */
|
|
26
|
+
loadedCount: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Options for useImageDimensions
|
|
31
|
+
*/
|
|
32
|
+
export interface UseImageDimensionsOptions {
|
|
33
|
+
/** Only load dimensions for first N images */
|
|
34
|
+
maxToLoad?: number
|
|
35
|
+
/** Skip loading if dimensions already present */
|
|
36
|
+
skipIfPresent?: boolean
|
|
37
|
+
/** Timeout per image in ms */
|
|
38
|
+
timeout?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Cache for loaded dimensions
|
|
42
|
+
const dimensionsCache = new Map<string, { width: number; height: number }>()
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load image dimensions
|
|
46
|
+
*/
|
|
47
|
+
function loadImageDimensions(
|
|
48
|
+
src: string,
|
|
49
|
+
timeout: number = 5000
|
|
50
|
+
): Promise<{ width: number; height: number }> {
|
|
51
|
+
// Check cache
|
|
52
|
+
const cached = dimensionsCache.get(src)
|
|
53
|
+
if (cached) return Promise.resolve(cached)
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const img = new Image()
|
|
57
|
+
const timeoutId = setTimeout(() => {
|
|
58
|
+
reject(new Error('Timeout loading image'))
|
|
59
|
+
}, timeout)
|
|
60
|
+
|
|
61
|
+
img.onload = () => {
|
|
62
|
+
clearTimeout(timeoutId)
|
|
63
|
+
const dims = { width: img.naturalWidth, height: img.naturalHeight }
|
|
64
|
+
dimensionsCache.set(src, dims)
|
|
65
|
+
resolve(dims)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
img.onerror = () => {
|
|
69
|
+
clearTimeout(timeoutId)
|
|
70
|
+
reject(new Error('Failed to load image'))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
img.src = src
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Hook to load image dimensions for gallery images
|
|
79
|
+
*
|
|
80
|
+
* Uses thumbnail URL when available (faster to load)
|
|
81
|
+
* Caches results to avoid reloading
|
|
82
|
+
*/
|
|
83
|
+
export function useImageDimensions(
|
|
84
|
+
images: GalleryMediaItem[],
|
|
85
|
+
options: UseImageDimensionsOptions = {}
|
|
86
|
+
): UseImageDimensionsResult {
|
|
87
|
+
const {
|
|
88
|
+
maxToLoad = 5,
|
|
89
|
+
skipIfPresent = true,
|
|
90
|
+
timeout = 5000,
|
|
91
|
+
} = options
|
|
92
|
+
|
|
93
|
+
// Create stable key from image IDs
|
|
94
|
+
const imageIds = useMemo(() => {
|
|
95
|
+
return images.slice(0, maxToLoad).map(img => img.id).join(',')
|
|
96
|
+
}, [images, maxToLoad])
|
|
97
|
+
|
|
98
|
+
// Track dimensions in a map by ID (stable across re-renders)
|
|
99
|
+
const dimensionsRef = useRef(new Map<string, { width: number; height: number; loaded: boolean }>())
|
|
100
|
+
const [version, setVersion] = useState(0) // Force re-render when dimensions load
|
|
101
|
+
const [isLoading, setIsLoading] = useState(true)
|
|
102
|
+
const loadingRef = useRef(false)
|
|
103
|
+
const prevImageIdsRef = useRef('')
|
|
104
|
+
|
|
105
|
+
// Initialize dimensions from props or cache
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
const targetImages = images.slice(0, maxToLoad)
|
|
108
|
+
const idsChanged = prevImageIdsRef.current !== imageIds
|
|
109
|
+
prevImageIdsRef.current = imageIds
|
|
110
|
+
|
|
111
|
+
// Update dimensions map with any new images
|
|
112
|
+
for (const img of targetImages) {
|
|
113
|
+
if (!dimensionsRef.current.has(img.id)) {
|
|
114
|
+
// Check cache first
|
|
115
|
+
const src = img.thumbnail || img.src
|
|
116
|
+
const cached = dimensionsCache.get(src)
|
|
117
|
+
|
|
118
|
+
if (cached) {
|
|
119
|
+
dimensionsRef.current.set(img.id, { ...cached, loaded: true })
|
|
120
|
+
} else if (img.width && img.height && skipIfPresent) {
|
|
121
|
+
dimensionsRef.current.set(img.id, { width: img.width, height: img.height, loaded: true })
|
|
122
|
+
} else {
|
|
123
|
+
dimensionsRef.current.set(img.id, { width: 0, height: 0, loaded: false })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find images that need loading
|
|
129
|
+
const toLoad = targetImages.filter(img => {
|
|
130
|
+
const dims = dimensionsRef.current.get(img.id)
|
|
131
|
+
return !dims?.loaded
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Nothing to load
|
|
135
|
+
if (toLoad.length === 0) {
|
|
136
|
+
setIsLoading(false)
|
|
137
|
+
if (idsChanged) setVersion(v => v + 1)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Already loading
|
|
142
|
+
if (loadingRef.current) return
|
|
143
|
+
|
|
144
|
+
loadingRef.current = true
|
|
145
|
+
setIsLoading(true)
|
|
146
|
+
|
|
147
|
+
// Load dimensions
|
|
148
|
+
Promise.all(
|
|
149
|
+
toLoad.map(async (img) => {
|
|
150
|
+
try {
|
|
151
|
+
const src = img.thumbnail || img.src
|
|
152
|
+
const dims = await loadImageDimensions(src, timeout)
|
|
153
|
+
dimensionsRef.current.set(img.id, { ...dims, loaded: true })
|
|
154
|
+
return { id: img.id, success: true }
|
|
155
|
+
} catch {
|
|
156
|
+
// Use default dimensions on error
|
|
157
|
+
dimensionsRef.current.set(img.id, { width: 1600, height: 900, loaded: true })
|
|
158
|
+
return { id: img.id, success: false }
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
).then(() => {
|
|
162
|
+
loadingRef.current = false
|
|
163
|
+
setIsLoading(false)
|
|
164
|
+
setVersion(v => v + 1)
|
|
165
|
+
})
|
|
166
|
+
}, [imageIds, images, maxToLoad, skipIfPresent, timeout])
|
|
167
|
+
|
|
168
|
+
// Build result array
|
|
169
|
+
const result = useMemo((): ImageWithDimensions[] => {
|
|
170
|
+
return images.slice(0, maxToLoad).map(img => {
|
|
171
|
+
const dims = dimensionsRef.current.get(img.id)
|
|
172
|
+
return {
|
|
173
|
+
...img,
|
|
174
|
+
width: dims?.width || img.width || 0,
|
|
175
|
+
height: dims?.height || img.height || 0,
|
|
176
|
+
loaded: dims?.loaded || false,
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
180
|
+
}, [imageIds, version])
|
|
181
|
+
|
|
182
|
+
// Also return all images (not just maxToLoad) with available dimensions
|
|
183
|
+
const allImages = useMemo((): ImageWithDimensions[] => {
|
|
184
|
+
return images.map(img => {
|
|
185
|
+
const dims = dimensionsRef.current.get(img.id)
|
|
186
|
+
return {
|
|
187
|
+
...img,
|
|
188
|
+
width: dims?.width || img.width || 0,
|
|
189
|
+
height: dims?.height || img.height || 0,
|
|
190
|
+
loaded: dims?.loaded || Boolean(img.width && img.height),
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
194
|
+
}, [images, version])
|
|
195
|
+
|
|
196
|
+
const loadedCount = result.filter(img => img.loaded).length
|
|
197
|
+
const targetCount = Math.min(images.length, maxToLoad)
|
|
198
|
+
|
|
199
|
+
const isReady = !isLoading && loadedCount >= targetCount
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
images: allImages,
|
|
203
|
+
isReady,
|
|
204
|
+
isLoading,
|
|
205
|
+
loadedCount,
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Clear dimensions cache
|
|
211
|
+
*/
|
|
212
|
+
export function clearDimensionsCache(): void {
|
|
213
|
+
dimensionsCache.clear()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get cached dimensions for an image
|
|
218
|
+
*/
|
|
219
|
+
export function getCachedDimensions(
|
|
220
|
+
src: string
|
|
221
|
+
): { width: number; height: number } | null {
|
|
222
|
+
return dimensionsCache.get(src) || null
|
|
223
|
+
}
|