@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,182 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useMemo, Suspense, lazy } from 'react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n'
|
|
6
|
+
import { ImageOff } from 'lucide-react'
|
|
7
|
+
import { useGallery } from '../hooks/useGallery'
|
|
8
|
+
import { GalleryGrid } from './GalleryGrid'
|
|
9
|
+
import { GalleryCarousel } from './GalleryCarousel'
|
|
10
|
+
import type { GalleryProps } from '../types'
|
|
11
|
+
|
|
12
|
+
// Lazy load lightbox - only loaded when user opens it
|
|
13
|
+
const GalleryLightbox = lazy(() =>
|
|
14
|
+
import('./GalleryLightbox').then((mod) => ({ default: mod.GalleryLightbox }))
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Gallery - Complete image gallery with carousel/grid preview and lightbox
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Two preview modes: carousel (default) or grid
|
|
22
|
+
* - Smooth slide transitions (embla-carousel)
|
|
23
|
+
* - Touch/swipe support
|
|
24
|
+
* - Keyboard navigation
|
|
25
|
+
* - Fullscreen lightbox
|
|
26
|
+
* - Dark overlay for controls
|
|
27
|
+
*/
|
|
28
|
+
export const Gallery = memo(function Gallery({
|
|
29
|
+
images,
|
|
30
|
+
initialIndex = 0,
|
|
31
|
+
previewMode = 'carousel',
|
|
32
|
+
previewCount = 5,
|
|
33
|
+
showThumbnails = true,
|
|
34
|
+
showControls = true,
|
|
35
|
+
showCounter = true,
|
|
36
|
+
aspectRatio = 16 / 9,
|
|
37
|
+
enableLightbox = true,
|
|
38
|
+
enableKeyboard = true,
|
|
39
|
+
onImageChange,
|
|
40
|
+
onLightboxOpen,
|
|
41
|
+
onLightboxClose,
|
|
42
|
+
emptyState,
|
|
43
|
+
className,
|
|
44
|
+
}: GalleryProps) {
|
|
45
|
+
const t = useTypedT<I18nTranslations>()
|
|
46
|
+
const noImagesText = useMemo(() => t('tools.gallery.noImages'), [t])
|
|
47
|
+
|
|
48
|
+
const gallery = useGallery({
|
|
49
|
+
images,
|
|
50
|
+
initialIndex,
|
|
51
|
+
loop: true,
|
|
52
|
+
onImageChange,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const {
|
|
56
|
+
currentIndex,
|
|
57
|
+
total,
|
|
58
|
+
hasMultiple,
|
|
59
|
+
isLightboxOpen,
|
|
60
|
+
goTo,
|
|
61
|
+
openLightbox,
|
|
62
|
+
closeLightbox,
|
|
63
|
+
setLoading,
|
|
64
|
+
} = gallery
|
|
65
|
+
|
|
66
|
+
// Lightbox callbacks
|
|
67
|
+
const handleLightboxOpen = useCallback(() => {
|
|
68
|
+
openLightbox()
|
|
69
|
+
onLightboxOpen?.()
|
|
70
|
+
}, [openLightbox, onLightboxOpen])
|
|
71
|
+
|
|
72
|
+
const handleLightboxClose = useCallback(() => {
|
|
73
|
+
closeLightbox()
|
|
74
|
+
onLightboxClose?.()
|
|
75
|
+
}, [closeLightbox, onLightboxClose])
|
|
76
|
+
|
|
77
|
+
// Open lightbox at specific index (for grid mode)
|
|
78
|
+
const handleGridImageClick = useCallback(
|
|
79
|
+
(index: number) => {
|
|
80
|
+
goTo(index)
|
|
81
|
+
openLightbox()
|
|
82
|
+
onLightboxOpen?.()
|
|
83
|
+
},
|
|
84
|
+
[goTo, openLightbox, onLightboxOpen]
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Current image load handler
|
|
88
|
+
const handleCurrentImageLoad = useCallback(() => {
|
|
89
|
+
setLoading(false)
|
|
90
|
+
}, [setLoading])
|
|
91
|
+
|
|
92
|
+
// Empty state
|
|
93
|
+
if (images.length === 0) {
|
|
94
|
+
if (emptyState) {
|
|
95
|
+
return <>{emptyState}</>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div
|
|
100
|
+
className={cn(
|
|
101
|
+
'bg-muted rounded-xl flex items-center justify-center',
|
|
102
|
+
className
|
|
103
|
+
)}
|
|
104
|
+
style={{ aspectRatio }}
|
|
105
|
+
>
|
|
106
|
+
<div className="text-center text-muted-foreground p-8">
|
|
107
|
+
<ImageOff className="w-12 h-12 mx-auto mb-3 opacity-50" />
|
|
108
|
+
<p>{noImagesText}</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Grid mode
|
|
115
|
+
if (previewMode === 'grid') {
|
|
116
|
+
return (
|
|
117
|
+
<div className={className}>
|
|
118
|
+
<GalleryGrid
|
|
119
|
+
images={images}
|
|
120
|
+
maxVisible={previewCount}
|
|
121
|
+
aspectRatio={aspectRatio}
|
|
122
|
+
gap={2}
|
|
123
|
+
rounded="xl"
|
|
124
|
+
onImageClick={enableLightbox ? handleGridImageClick : undefined}
|
|
125
|
+
showMoreBadge
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
{/* Lightbox - lazy loaded */}
|
|
129
|
+
{enableLightbox && isLightboxOpen && (
|
|
130
|
+
<Suspense fallback={null}>
|
|
131
|
+
<GalleryLightbox
|
|
132
|
+
open={isLightboxOpen}
|
|
133
|
+
onClose={handleLightboxClose}
|
|
134
|
+
images={images}
|
|
135
|
+
currentIndex={currentIndex}
|
|
136
|
+
onIndexChange={goTo}
|
|
137
|
+
showThumbnails={showThumbnails}
|
|
138
|
+
/>
|
|
139
|
+
</Suspense>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Carousel mode (default)
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<GalleryCarousel
|
|
149
|
+
images={images}
|
|
150
|
+
currentIndex={currentIndex}
|
|
151
|
+
total={total}
|
|
152
|
+
hasMultiple={hasMultiple}
|
|
153
|
+
initialIndex={initialIndex}
|
|
154
|
+
aspectRatio={aspectRatio}
|
|
155
|
+
showControls={showControls}
|
|
156
|
+
showCounter={showCounter}
|
|
157
|
+
showThumbnails={showThumbnails}
|
|
158
|
+
enableLightbox={enableLightbox}
|
|
159
|
+
enableKeyboard={enableKeyboard}
|
|
160
|
+
isLightboxOpen={isLightboxOpen}
|
|
161
|
+
onIndexChange={goTo}
|
|
162
|
+
onLightboxOpen={handleLightboxOpen}
|
|
163
|
+
onImageLoad={handleCurrentImageLoad}
|
|
164
|
+
className={className}
|
|
165
|
+
/>
|
|
166
|
+
|
|
167
|
+
{/* Lightbox - lazy loaded */}
|
|
168
|
+
{enableLightbox && isLightboxOpen && (
|
|
169
|
+
<Suspense fallback={null}>
|
|
170
|
+
<GalleryLightbox
|
|
171
|
+
open={isLightboxOpen}
|
|
172
|
+
onClose={handleLightboxClose}
|
|
173
|
+
images={images}
|
|
174
|
+
currentIndex={currentIndex}
|
|
175
|
+
onIndexChange={goTo}
|
|
176
|
+
showThumbnails={showThumbnails}
|
|
177
|
+
/>
|
|
178
|
+
</Suspense>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
)
|
|
182
|
+
})
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useMemo } from 'react'
|
|
5
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
6
|
+
import {
|
|
7
|
+
Carousel,
|
|
8
|
+
CarouselContent,
|
|
9
|
+
CarouselItem,
|
|
10
|
+
CarouselPrevious,
|
|
11
|
+
CarouselNext,
|
|
12
|
+
type CarouselApi,
|
|
13
|
+
} from '@djangocfg/ui-core/components'
|
|
14
|
+
import { ZoomIn } from 'lucide-react'
|
|
15
|
+
import { GalleryMedia } from './GalleryMedia'
|
|
16
|
+
import { GalleryThumbnails } from './GalleryThumbnails'
|
|
17
|
+
import type { GalleryMediaItem } from '../types'
|
|
18
|
+
|
|
19
|
+
export interface GalleryCarouselProps {
|
|
20
|
+
images: GalleryMediaItem[]
|
|
21
|
+
currentIndex: number
|
|
22
|
+
total: number
|
|
23
|
+
hasMultiple: boolean
|
|
24
|
+
initialIndex?: number
|
|
25
|
+
aspectRatio?: number
|
|
26
|
+
showControls?: boolean
|
|
27
|
+
showCounter?: boolean
|
|
28
|
+
showThumbnails?: boolean
|
|
29
|
+
enableLightbox?: boolean
|
|
30
|
+
enableKeyboard?: boolean
|
|
31
|
+
isLightboxOpen?: boolean
|
|
32
|
+
onApiChange?: (api: CarouselApi | undefined) => void
|
|
33
|
+
onIndexChange: (index: number) => void
|
|
34
|
+
onLightboxOpen?: () => void
|
|
35
|
+
onImageLoad?: () => void
|
|
36
|
+
className?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* GalleryCarousel - Embla-based carousel for gallery images
|
|
41
|
+
*
|
|
42
|
+
* Features:
|
|
43
|
+
* - Smooth slide transitions
|
|
44
|
+
* - Touch/swipe support
|
|
45
|
+
* - Keyboard navigation
|
|
46
|
+
* - Mobile dots + desktop thumbnails
|
|
47
|
+
*/
|
|
48
|
+
export const GalleryCarousel = memo(function GalleryCarousel({
|
|
49
|
+
images,
|
|
50
|
+
currentIndex,
|
|
51
|
+
total,
|
|
52
|
+
hasMultiple,
|
|
53
|
+
initialIndex = 0,
|
|
54
|
+
aspectRatio = 16 / 9,
|
|
55
|
+
showControls = true,
|
|
56
|
+
showCounter = true,
|
|
57
|
+
showThumbnails = true,
|
|
58
|
+
enableLightbox = true,
|
|
59
|
+
enableKeyboard = true,
|
|
60
|
+
isLightboxOpen = false,
|
|
61
|
+
onApiChange,
|
|
62
|
+
onIndexChange,
|
|
63
|
+
onLightboxOpen,
|
|
64
|
+
onImageLoad,
|
|
65
|
+
className,
|
|
66
|
+
}: GalleryCarouselProps) {
|
|
67
|
+
const [api, setApi] = React.useState<CarouselApi>()
|
|
68
|
+
|
|
69
|
+
// Notify parent when API is ready
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
onApiChange?.(api)
|
|
72
|
+
}, [api, onApiChange])
|
|
73
|
+
|
|
74
|
+
// Mobile dots (max 5)
|
|
75
|
+
const mobileDots = useMemo(() => {
|
|
76
|
+
return images.slice(0, 5).map((_, index) => index)
|
|
77
|
+
}, [images])
|
|
78
|
+
|
|
79
|
+
const remainingCount = useMemo(() => {
|
|
80
|
+
return images.length > 5 ? images.length - 5 : 0
|
|
81
|
+
}, [images.length])
|
|
82
|
+
|
|
83
|
+
// Sync carousel with external state
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (!api) return
|
|
86
|
+
api.scrollTo(currentIndex)
|
|
87
|
+
}, [api, currentIndex])
|
|
88
|
+
|
|
89
|
+
// Listen to carousel changes
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!api) return
|
|
92
|
+
|
|
93
|
+
const onSelect = () => {
|
|
94
|
+
const index = api.selectedScrollSnap()
|
|
95
|
+
if (index !== currentIndex) {
|
|
96
|
+
onIndexChange(index)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
api.on('select', onSelect)
|
|
101
|
+
return () => {
|
|
102
|
+
api.off('select', onSelect)
|
|
103
|
+
}
|
|
104
|
+
}, [api, currentIndex, onIndexChange])
|
|
105
|
+
|
|
106
|
+
// Keyboard navigation
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (!enableKeyboard || isLightboxOpen || !api) return
|
|
109
|
+
|
|
110
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
111
|
+
if (e.key === 'ArrowLeft' && hasMultiple) {
|
|
112
|
+
api.scrollPrev()
|
|
113
|
+
} else if (e.key === 'ArrowRight' && hasMultiple) {
|
|
114
|
+
api.scrollNext()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
119
|
+
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
120
|
+
}, [enableKeyboard, isLightboxOpen, hasMultiple, api])
|
|
121
|
+
|
|
122
|
+
// Dot click handler
|
|
123
|
+
const handleDotClick = useCallback(
|
|
124
|
+
(e: React.MouseEvent, index: number) => {
|
|
125
|
+
e.stopPropagation()
|
|
126
|
+
api?.scrollTo(index)
|
|
127
|
+
},
|
|
128
|
+
[api]
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// Scroll to index
|
|
132
|
+
const handleScrollTo = useCallback(
|
|
133
|
+
(index: number) => {
|
|
134
|
+
api?.scrollTo(index)
|
|
135
|
+
},
|
|
136
|
+
[api]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div className={cn('space-y-3', className)}>
|
|
141
|
+
<Carousel
|
|
142
|
+
setApi={setApi}
|
|
143
|
+
opts={{
|
|
144
|
+
loop: true,
|
|
145
|
+
startIndex: initialIndex,
|
|
146
|
+
}}
|
|
147
|
+
className="relative rounded-xl overflow-hidden"
|
|
148
|
+
>
|
|
149
|
+
<CarouselContent className="-ml-0">
|
|
150
|
+
{images.map((image, index) => (
|
|
151
|
+
<CarouselItem key={image.id} className="pl-0">
|
|
152
|
+
<div
|
|
153
|
+
className="relative bg-muted cursor-pointer"
|
|
154
|
+
style={{ aspectRatio }}
|
|
155
|
+
onClick={enableLightbox ? onLightboxOpen : undefined}
|
|
156
|
+
>
|
|
157
|
+
<GalleryMedia
|
|
158
|
+
media={image}
|
|
159
|
+
className="w-full h-full"
|
|
160
|
+
onLoad={index === currentIndex ? onImageLoad : undefined}
|
|
161
|
+
priority={index === 0}
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
{/* Dark overlay */}
|
|
165
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-black/20 pointer-events-none" />
|
|
166
|
+
|
|
167
|
+
{/* Lightbox indicator */}
|
|
168
|
+
{enableLightbox && (
|
|
169
|
+
<div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
|
170
|
+
<div className="bg-black/60 backdrop-blur-sm rounded-full p-3 shadow-lg border border-white/20">
|
|
171
|
+
<ZoomIn className="w-6 h-6 text-white" />
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</CarouselItem>
|
|
177
|
+
))}
|
|
178
|
+
</CarouselContent>
|
|
179
|
+
|
|
180
|
+
{/* Navigation Controls */}
|
|
181
|
+
{showControls && hasMultiple && (
|
|
182
|
+
<>
|
|
183
|
+
<CarouselPrevious
|
|
184
|
+
className={cn(
|
|
185
|
+
'absolute left-3 top-1/2 -translate-y-1/2',
|
|
186
|
+
'w-10 h-10 rounded-full',
|
|
187
|
+
'bg-black/50 hover:bg-black/70 text-white backdrop-blur-sm border-white/20',
|
|
188
|
+
'disabled:opacity-30'
|
|
189
|
+
)}
|
|
190
|
+
/>
|
|
191
|
+
<CarouselNext
|
|
192
|
+
className={cn(
|
|
193
|
+
'absolute right-3 top-1/2 -translate-y-1/2',
|
|
194
|
+
'w-10 h-10 rounded-full',
|
|
195
|
+
'bg-black/50 hover:bg-black/70 text-white backdrop-blur-sm border-white/20',
|
|
196
|
+
'disabled:opacity-30'
|
|
197
|
+
)}
|
|
198
|
+
/>
|
|
199
|
+
</>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Counter */}
|
|
203
|
+
{showCounter && hasMultiple && (
|
|
204
|
+
<div
|
|
205
|
+
className={cn(
|
|
206
|
+
'absolute bottom-3 right-3 z-10',
|
|
207
|
+
'bg-black/60 backdrop-blur-sm text-white px-3 py-1.5 rounded-full',
|
|
208
|
+
'text-sm font-medium'
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
{currentIndex + 1} / {total}
|
|
212
|
+
</div>
|
|
213
|
+
)}
|
|
214
|
+
|
|
215
|
+
{/* Mobile dots */}
|
|
216
|
+
{hasMultiple && (
|
|
217
|
+
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 md:hidden z-10">
|
|
218
|
+
{mobileDots.map((index) => (
|
|
219
|
+
<button
|
|
220
|
+
key={index}
|
|
221
|
+
type="button"
|
|
222
|
+
className={cn(
|
|
223
|
+
'rounded-full transition-all',
|
|
224
|
+
index === currentIndex
|
|
225
|
+
? 'w-2.5 h-2.5 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
226
|
+
: 'w-2 h-2 bg-white/40'
|
|
227
|
+
)}
|
|
228
|
+
onClick={(e) => handleDotClick(e, index)}
|
|
229
|
+
aria-label={`Go to image ${index + 1}`}
|
|
230
|
+
/>
|
|
231
|
+
))}
|
|
232
|
+
{remainingCount > 0 && (
|
|
233
|
+
<span className="text-white/70 text-xs ml-1">+{remainingCount}</span>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</Carousel>
|
|
238
|
+
|
|
239
|
+
{/* Thumbnails */}
|
|
240
|
+
{showThumbnails && hasMultiple && (
|
|
241
|
+
<GalleryThumbnails
|
|
242
|
+
images={images}
|
|
243
|
+
currentIndex={currentIndex}
|
|
244
|
+
onSelect={handleScrollTo}
|
|
245
|
+
size="md"
|
|
246
|
+
className="hidden md:flex"
|
|
247
|
+
/>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
)
|
|
251
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib'
|
|
5
|
+
import {
|
|
6
|
+
Carousel,
|
|
7
|
+
CarouselContent,
|
|
8
|
+
CarouselItem,
|
|
9
|
+
type CarouselApi,
|
|
10
|
+
} from '@djangocfg/ui-core/components'
|
|
11
|
+
import { ImageOff } from 'lucide-react'
|
|
12
|
+
import { GalleryMedia } from './GalleryMedia'
|
|
13
|
+
import type { GalleryMediaItem } from '../types'
|
|
14
|
+
|
|
15
|
+
export interface GalleryCompactProps {
|
|
16
|
+
/** Array of images to display */
|
|
17
|
+
images: GalleryMediaItem[]
|
|
18
|
+
/** Show dots indicator (default: true) */
|
|
19
|
+
showDots?: boolean
|
|
20
|
+
/** Max dots to show (default: 5) */
|
|
21
|
+
maxDots?: number
|
|
22
|
+
/** Show counter badge (default: false) */
|
|
23
|
+
showCounter?: boolean
|
|
24
|
+
/** On image click callback */
|
|
25
|
+
onClick?: () => void
|
|
26
|
+
/** Additional CSS class */
|
|
27
|
+
className?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* GalleryCompact - Minimal carousel for property cards
|
|
32
|
+
*
|
|
33
|
+
* Features:
|
|
34
|
+
* - Simple swipe carousel
|
|
35
|
+
* - Dots indicator (mobile-style)
|
|
36
|
+
* - No arrows, thumbnails, or lightbox
|
|
37
|
+
* - Fills parent container
|
|
38
|
+
* - Stops event propagation on navigation
|
|
39
|
+
*/
|
|
40
|
+
export const GalleryCompact = memo(function GalleryCompact({
|
|
41
|
+
images,
|
|
42
|
+
showDots = true,
|
|
43
|
+
maxDots = 5,
|
|
44
|
+
showCounter = false,
|
|
45
|
+
onClick,
|
|
46
|
+
className,
|
|
47
|
+
}: GalleryCompactProps) {
|
|
48
|
+
const [api, setApi] = useState<CarouselApi>()
|
|
49
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
50
|
+
|
|
51
|
+
const total = images.length
|
|
52
|
+
const hasMultiple = total > 1
|
|
53
|
+
|
|
54
|
+
// Listen to carousel changes
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!api) return
|
|
57
|
+
|
|
58
|
+
const onSelect = () => {
|
|
59
|
+
setCurrentIndex(api.selectedScrollSnap())
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
api.on('select', onSelect)
|
|
63
|
+
return () => {
|
|
64
|
+
api.off('select', onSelect)
|
|
65
|
+
}
|
|
66
|
+
}, [api])
|
|
67
|
+
|
|
68
|
+
// Dot click handler - stops propagation to prevent card click
|
|
69
|
+
const handleDotClick = useCallback(
|
|
70
|
+
(e: React.MouseEvent, index: number) => {
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
e.stopPropagation()
|
|
73
|
+
api?.scrollTo(index)
|
|
74
|
+
},
|
|
75
|
+
[api]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
// Handle container click
|
|
79
|
+
const handleClick = useCallback(
|
|
80
|
+
(e: React.MouseEvent) => {
|
|
81
|
+
// Don't trigger if clicking on dots
|
|
82
|
+
if ((e.target as HTMLElement).closest('[data-dots]')) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
onClick?.()
|
|
86
|
+
},
|
|
87
|
+
[onClick]
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Empty state
|
|
91
|
+
if (total === 0) {
|
|
92
|
+
return (
|
|
93
|
+
<div className={cn('relative w-full h-full bg-muted flex items-center justify-center', className)}>
|
|
94
|
+
<ImageOff className="w-8 h-8 text-muted-foreground/50" />
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Single image - no carousel needed
|
|
100
|
+
if (!hasMultiple) {
|
|
101
|
+
return (
|
|
102
|
+
<div className={cn('relative w-full h-full', className)} onClick={onClick}>
|
|
103
|
+
<GalleryMedia
|
|
104
|
+
media={images[0]}
|
|
105
|
+
className="w-full h-full"
|
|
106
|
+
priority
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Calculate visible dots
|
|
113
|
+
const visibleDots = images.slice(0, maxDots).map((_, i) => i)
|
|
114
|
+
const remainingCount = total > maxDots ? total - maxDots : 0
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={cn('relative w-full h-full', className)} onClick={handleClick}>
|
|
118
|
+
<Carousel
|
|
119
|
+
setApi={setApi}
|
|
120
|
+
opts={{
|
|
121
|
+
loop: true,
|
|
122
|
+
}}
|
|
123
|
+
className="w-full h-full"
|
|
124
|
+
>
|
|
125
|
+
<CarouselContent className="-ml-0 h-full">
|
|
126
|
+
{images.map((image, index) => (
|
|
127
|
+
<CarouselItem key={image.id} className="pl-0 h-full">
|
|
128
|
+
<GalleryMedia
|
|
129
|
+
media={image}
|
|
130
|
+
className="w-full h-full"
|
|
131
|
+
priority={index === 0}
|
|
132
|
+
/>
|
|
133
|
+
</CarouselItem>
|
|
134
|
+
))}
|
|
135
|
+
</CarouselContent>
|
|
136
|
+
</Carousel>
|
|
137
|
+
|
|
138
|
+
{/* Dots indicator */}
|
|
139
|
+
{showDots && (
|
|
140
|
+
<div
|
|
141
|
+
data-dots
|
|
142
|
+
className="absolute bottom-3 left-1/2 -translate-x-1/2 flex items-center gap-1.5 z-10"
|
|
143
|
+
onClick={(e) => e.stopPropagation()}
|
|
144
|
+
>
|
|
145
|
+
{visibleDots.map((index) => (
|
|
146
|
+
<button
|
|
147
|
+
key={index}
|
|
148
|
+
type="button"
|
|
149
|
+
className={cn(
|
|
150
|
+
'rounded-full transition-all',
|
|
151
|
+
index === currentIndex
|
|
152
|
+
? 'w-2 h-2 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
153
|
+
: 'w-1.5 h-1.5 bg-white/50 hover:bg-white/70'
|
|
154
|
+
)}
|
|
155
|
+
onClick={(e) => handleDotClick(e, index)}
|
|
156
|
+
aria-label={`Go to image ${index + 1}`}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
{remainingCount > 0 && (
|
|
160
|
+
<span className="text-white/70 text-[10px] ml-0.5">+{remainingCount}</span>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{/* Counter badge */}
|
|
166
|
+
{showCounter && (
|
|
167
|
+
<div className="absolute bottom-3 right-3 z-10 bg-black/60 backdrop-blur-sm text-white text-xs px-2 py-1 rounded-full">
|
|
168
|
+
{currentIndex + 1} / {total}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
)
|
|
173
|
+
})
|