@djangocfg/ui-tools 2.1.118 → 2.1.120
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/JsonTree-G2TPWQ4C.mjs +4 -0
- package/dist/{JsonTree-6RYAOPSS.mjs.map → JsonTree-G2TPWQ4C.mjs.map} +1 -1
- package/dist/JsonTree-TWXUBBIG.cjs +10 -0
- package/dist/{JsonTree-7OH6CIHT.cjs.map → JsonTree-TWXUBBIG.cjs.map} +1 -1
- package/dist/{Mermaid.client-PNXEC6YL.cjs → Mermaid.client-AF4WOQZR.cjs} +9 -11
- package/dist/Mermaid.client-AF4WOQZR.cjs.map +1 -0
- package/dist/{Mermaid.client-OKACITCW.mjs → Mermaid.client-W4QXJX7Q.mjs} +9 -11
- package/dist/Mermaid.client-W4QXJX7Q.mjs.map +1 -0
- package/dist/{PlaygroundLayout-SYMEAG3J.cjs → PlaygroundLayout-RZMJWH3Y.cjs} +25 -25
- package/dist/{PlaygroundLayout-SYMEAG3J.cjs.map → PlaygroundLayout-RZMJWH3Y.cjs.map} +1 -1
- package/dist/{PlaygroundLayout-UQRBU5RH.mjs → PlaygroundLayout-UQABCZ6K.mjs} +4 -4
- package/dist/{PlaygroundLayout-UQRBU5RH.mjs.map → PlaygroundLayout-UQABCZ6K.mjs.map} +1 -1
- package/dist/{chunk-UOMPPIED.mjs → chunk-4G4UGMOP.mjs} +3 -3
- package/dist/chunk-4G4UGMOP.mjs.map +1 -0
- package/dist/{chunk-47T5ECYV.cjs → chunk-CY3CQS26.cjs} +3 -3
- package/dist/chunk-CY3CQS26.cjs.map +1 -0
- package/dist/{chunk-5QT3QYFZ.cjs → chunk-EGYUND4E.cjs} +2 -2
- package/dist/chunk-EGYUND4E.cjs.map +1 -0
- package/dist/{chunk-DI3HUXHK.cjs → chunk-OYLQZT62.cjs} +2 -2
- package/dist/chunk-OYLQZT62.cjs.map +1 -0
- package/dist/{chunk-W6YHQI4F.mjs → chunk-OYYCGIBF.mjs} +2 -2
- package/dist/chunk-OYYCGIBF.mjs.map +1 -0
- package/dist/{chunk-G6PRZP5I.mjs → chunk-XZZ22EHP.mjs} +2 -2
- package/dist/chunk-XZZ22EHP.mjs.map +1 -0
- package/dist/{components-EASJYK45.mjs → components-4YSJ5ALL.mjs} +3 -3
- package/dist/{components-CJ2IB65O.cjs.map → components-4YSJ5ALL.mjs.map} +1 -1
- package/dist/{components-CJ2IB65O.cjs → components-BSTP3VLD.cjs} +7 -7
- package/dist/{components-EASJYK45.mjs.map → components-BSTP3VLD.cjs.map} +1 -1
- package/dist/index.cjs +27 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +10 -10
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -5
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
- package/src/tools/Gallery/Gallery.story.tsx +231 -0
- package/src/tools/Gallery/components/Gallery.tsx +3 -3
- package/src/tools/Gallery/components/compact/GalleryCompact.tsx +363 -0
- package/src/tools/Gallery/components/compact/index.ts +1 -0
- package/src/tools/Gallery/components/index.ts +17 -12
- package/src/tools/Gallery/components/{GalleryLightbox.tsx → lightbox/GalleryLightbox.tsx} +6 -6
- package/src/tools/Gallery/components/lightbox/index.ts +1 -0
- package/src/tools/Gallery/components/{GalleryImage.tsx → media/GalleryImage.tsx} +1 -1
- package/src/tools/Gallery/components/{GalleryMedia.tsx → media/GalleryMedia.tsx} +1 -1
- package/src/tools/Gallery/components/{GalleryVideo.tsx → media/GalleryVideo.tsx} +1 -1
- package/src/tools/Gallery/components/media/index.ts +3 -0
- package/src/tools/Gallery/components/{GalleryCarousel.tsx → preview/GalleryCarousel.tsx} +114 -6
- package/src/tools/Gallery/components/{GalleryGrid.tsx → preview/GalleryGrid.tsx} +1 -1
- package/src/tools/Gallery/components/preview/index.ts +2 -0
- package/src/tools/Gallery/components/{GalleryThumbnails.tsx → thumbnails/GalleryThumbnails.tsx} +1 -1
- package/src/tools/Gallery/components/{GalleryThumbnailsVirtual.tsx → thumbnails/GalleryThumbnailsVirtual.tsx} +2 -2
- package/src/tools/Gallery/components/thumbnails/index.ts +2 -0
- package/src/tools/JsonForm/JsonForm.story.tsx +134 -0
- package/src/tools/JsonTree/JsonTree.story.tsx +140 -0
- package/src/tools/JsonTree/index.tsx +1 -1
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +95 -0
- package/src/tools/Map/Map.story.tsx +300 -0
- package/src/tools/Mermaid/Mermaid.story.tsx +131 -0
- package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +2 -5
- package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +7 -1
- package/src/tools/Mermaid/hooks/useMermaidValidation.ts +4 -2
- package/src/tools/Mermaid/index.tsx +1 -1
- package/src/tools/PrettyCode/PrettyCode.story.tsx +116 -0
- package/src/tools/PrettyCode/index.tsx +1 -1
- package/src/tools/VideoPlayer/VideoPlayer.story.tsx +87 -0
- package/src/tools/VideoPlayer/utils/resolvers.ts +2 -2
- package/dist/JsonTree-6RYAOPSS.mjs +0 -4
- package/dist/JsonTree-7OH6CIHT.cjs +0 -10
- package/dist/Mermaid.client-OKACITCW.mjs.map +0 -1
- package/dist/Mermaid.client-PNXEC6YL.cjs.map +0 -1
- package/dist/chunk-47T5ECYV.cjs.map +0 -1
- package/dist/chunk-5QT3QYFZ.cjs.map +0 -1
- package/dist/chunk-DI3HUXHK.cjs.map +0 -1
- package/dist/chunk-G6PRZP5I.mjs.map +0 -1
- package/dist/chunk-UOMPPIED.mjs.map +0 -1
- package/dist/chunk-W6YHQI4F.mjs.map +0 -1
- package/src/tools/Gallery/components/GalleryCompact.tsx +0 -173
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { defineStory, useBoolean, useSelect, useNumber } from '@djangocfg/playground';
|
|
2
|
+
import { Gallery, GalleryCompact, type GalleryMediaItem } from './index';
|
|
3
|
+
|
|
4
|
+
// Sample images for testing
|
|
5
|
+
const SAMPLE_IMAGES: GalleryMediaItem[] = [
|
|
6
|
+
{
|
|
7
|
+
id: '1',
|
|
8
|
+
src: 'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=800',
|
|
9
|
+
thumbnail: 'https://images.unsplash.com/photo-1494976388531-d1058494cdd8?w=200',
|
|
10
|
+
alt: 'Classic car',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: '2',
|
|
14
|
+
src: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=800',
|
|
15
|
+
thumbnail: 'https://images.unsplash.com/photo-1503376780353-7e6692767b70?w=200',
|
|
16
|
+
alt: 'Porsche',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: '3',
|
|
20
|
+
src: 'https://images.unsplash.com/photo-1542362567-b07e54358753?w=800',
|
|
21
|
+
thumbnail: 'https://images.unsplash.com/photo-1542362567-b07e54358753?w=200',
|
|
22
|
+
alt: 'BMW',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: '4',
|
|
26
|
+
src: 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=800',
|
|
27
|
+
thumbnail: 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=200',
|
|
28
|
+
alt: 'Mercedes',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: '5',
|
|
32
|
+
src: 'https://images.unsplash.com/photo-1617531653332-bd46c24f2068?w=800',
|
|
33
|
+
thumbnail: 'https://images.unsplash.com/photo-1617531653332-bd46c24f2068?w=200',
|
|
34
|
+
alt: 'Tesla',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export default defineStory({
|
|
39
|
+
title: 'Tools/Gallery',
|
|
40
|
+
component: Gallery,
|
|
41
|
+
description: 'Full-featured gallery with lightbox, thumbnails, and keyboard navigation.',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const CarouselMode = () => (
|
|
45
|
+
<div className="max-w-4xl">
|
|
46
|
+
<Gallery
|
|
47
|
+
images={SAMPLE_IMAGES}
|
|
48
|
+
previewMode="carousel"
|
|
49
|
+
showThumbnails
|
|
50
|
+
showControls
|
|
51
|
+
showCounter
|
|
52
|
+
enableLightbox
|
|
53
|
+
enableKeyboard
|
|
54
|
+
aspectRatio={16 / 9}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export const GridMode = () => (
|
|
60
|
+
<div className="max-w-4xl">
|
|
61
|
+
<Gallery
|
|
62
|
+
images={SAMPLE_IMAGES}
|
|
63
|
+
previewMode="grid"
|
|
64
|
+
previewCount={5}
|
|
65
|
+
enableLightbox
|
|
66
|
+
aspectRatio={16 / 9}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export const WithoutThumbnails = () => (
|
|
72
|
+
<div className="max-w-4xl">
|
|
73
|
+
<Gallery
|
|
74
|
+
images={SAMPLE_IMAGES}
|
|
75
|
+
previewMode="carousel"
|
|
76
|
+
showThumbnails={false}
|
|
77
|
+
showControls
|
|
78
|
+
enableLightbox
|
|
79
|
+
aspectRatio={16 / 9}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const Compact = {
|
|
85
|
+
render: (args: {
|
|
86
|
+
showDots: boolean;
|
|
87
|
+
showArrows: boolean;
|
|
88
|
+
enableZoom: boolean;
|
|
89
|
+
showCounter: boolean;
|
|
90
|
+
}) => (
|
|
91
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
92
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
93
|
+
<div className="aspect-[4/3]">
|
|
94
|
+
<GalleryCompact
|
|
95
|
+
images={SAMPLE_IMAGES}
|
|
96
|
+
showDots={args.showDots}
|
|
97
|
+
showArrows={args.showArrows}
|
|
98
|
+
enableZoom={args.enableZoom}
|
|
99
|
+
showCounter={args.showCounter}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<div className="p-4">
|
|
103
|
+
<h3 className="font-medium">Vehicle Title</h3>
|
|
104
|
+
<p className="text-sm text-muted-foreground">$25,000</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
),
|
|
109
|
+
args: {
|
|
110
|
+
showDots: true,
|
|
111
|
+
showArrows: true,
|
|
112
|
+
enableZoom: true,
|
|
113
|
+
showCounter: false,
|
|
114
|
+
},
|
|
115
|
+
argTypes: {
|
|
116
|
+
showDots: { control: 'boolean', description: 'Show navigation dots' },
|
|
117
|
+
showArrows: { control: 'boolean', description: 'Show arrow buttons' },
|
|
118
|
+
enableZoom: { control: 'boolean', description: 'Enable zoom on hover' },
|
|
119
|
+
showCounter: { control: 'boolean', description: 'Show image counter' },
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const SingleImage = () => (
|
|
124
|
+
<div className="w-64">
|
|
125
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
126
|
+
<div className="aspect-[4/3]">
|
|
127
|
+
<GalleryCompact images={SAMPLE_IMAGES.slice(0, 1)} enableZoom />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
export const EmptyState = () => (
|
|
134
|
+
<div className="w-64">
|
|
135
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
136
|
+
<div className="aspect-[4/3]">
|
|
137
|
+
<GalleryCompact images={[]} />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Interactive story using fixture hooks
|
|
145
|
+
* Controls appear automatically in the right sidebar
|
|
146
|
+
*/
|
|
147
|
+
export const Interactive = () => {
|
|
148
|
+
// These hooks auto-register controls in the Properties panel
|
|
149
|
+
const [showDots] = useBoolean('showDots', {
|
|
150
|
+
defaultValue: true,
|
|
151
|
+
label: 'Show Dots',
|
|
152
|
+
description: 'Display navigation dots at the bottom',
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const [showArrows] = useBoolean('showArrows', {
|
|
156
|
+
defaultValue: true,
|
|
157
|
+
label: 'Show Arrows',
|
|
158
|
+
description: 'Display navigation arrows on hover',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const [enableZoom] = useBoolean('enableZoom', {
|
|
162
|
+
defaultValue: true,
|
|
163
|
+
label: 'Enable Zoom',
|
|
164
|
+
description: 'Zoom effect on image hover',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const [showCounter] = useBoolean('showCounter', {
|
|
168
|
+
defaultValue: false,
|
|
169
|
+
label: 'Show Counter',
|
|
170
|
+
description: 'Display image counter badge',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const [maxDots] = useNumber('maxDots', {
|
|
174
|
+
defaultValue: 5,
|
|
175
|
+
min: 1,
|
|
176
|
+
max: 10,
|
|
177
|
+
label: 'Max Dots',
|
|
178
|
+
description: 'Maximum number of dots to display',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const [previewMode] = useSelect('previewMode', {
|
|
182
|
+
options: ['carousel', 'grid'] as const,
|
|
183
|
+
defaultValue: 'carousel',
|
|
184
|
+
label: 'Preview Mode',
|
|
185
|
+
description: 'Gallery preview display mode',
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<div className="space-y-8">
|
|
190
|
+
{/* GalleryCompact */}
|
|
191
|
+
<div>
|
|
192
|
+
<h3 className="text-lg font-semibold mb-4">GalleryCompact</h3>
|
|
193
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
194
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
195
|
+
<div className="aspect-[4/3]">
|
|
196
|
+
<GalleryCompact
|
|
197
|
+
images={SAMPLE_IMAGES}
|
|
198
|
+
showDots={showDots}
|
|
199
|
+
showArrows={showArrows}
|
|
200
|
+
enableZoom={enableZoom}
|
|
201
|
+
showCounter={showCounter}
|
|
202
|
+
maxDots={maxDots}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
<div className="p-4">
|
|
206
|
+
<h3 className="font-medium">Interactive Card</h3>
|
|
207
|
+
<p className="text-sm text-muted-foreground">Use controls on the right →</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Full Gallery */}
|
|
214
|
+
<div>
|
|
215
|
+
<h3 className="text-lg font-semibold mb-4">Full Gallery ({previewMode})</h3>
|
|
216
|
+
<div className="max-w-4xl">
|
|
217
|
+
<Gallery
|
|
218
|
+
images={SAMPLE_IMAGES}
|
|
219
|
+
previewMode={previewMode}
|
|
220
|
+
showThumbnails
|
|
221
|
+
showControls
|
|
222
|
+
showCounter={showCounter}
|
|
223
|
+
enableLightbox
|
|
224
|
+
enableKeyboard
|
|
225
|
+
aspectRatio={16 / 9}
|
|
226
|
+
/>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
};
|
|
@@ -5,13 +5,13 @@ import { cn } from '@djangocfg/ui-core/lib'
|
|
|
5
5
|
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n'
|
|
6
6
|
import { ImageOff } from 'lucide-react'
|
|
7
7
|
import { useGallery } from '../hooks/useGallery'
|
|
8
|
-
import { GalleryGrid } from './
|
|
9
|
-
import { GalleryCarousel } from './
|
|
8
|
+
import { GalleryGrid } from './preview'
|
|
9
|
+
import { GalleryCarousel } from './preview'
|
|
10
10
|
import type { GalleryProps } from '../types'
|
|
11
11
|
|
|
12
12
|
// Lazy load lightbox - only loaded when user opens it
|
|
13
13
|
const GalleryLightbox = lazy(() =>
|
|
14
|
-
import('./
|
|
14
|
+
import('./lightbox').then((mod) => ({ default: mod.GalleryLightbox }))
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useEffect, useState, useMemo } 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 { ChevronLeft, ChevronRight, ImageOff } from 'lucide-react'
|
|
12
|
+
import { GalleryMedia } from '../media'
|
|
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
|
+
/** Show navigation arrows on hover (default: true) */
|
|
25
|
+
showArrows?: boolean
|
|
26
|
+
/** Enable zoom effect on hover (default: true) */
|
|
27
|
+
enableZoom?: boolean
|
|
28
|
+
/** On image click callback */
|
|
29
|
+
onClick?: () => void
|
|
30
|
+
/** Additional CSS class */
|
|
31
|
+
className?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* GalleryCompact - Minimal carousel for property/vehicle cards
|
|
36
|
+
*
|
|
37
|
+
* Features:
|
|
38
|
+
* - Simple swipe carousel
|
|
39
|
+
* - Dots indicator (mobile-style)
|
|
40
|
+
* - Navigation arrows on hover (desktop)
|
|
41
|
+
* - Hover zoom effect on images
|
|
42
|
+
* - Lazy loading - only loads active image + neighbors
|
|
43
|
+
* - Fills parent container
|
|
44
|
+
* - Stops event propagation on navigation
|
|
45
|
+
*/
|
|
46
|
+
export const GalleryCompact = memo(function GalleryCompact({
|
|
47
|
+
images,
|
|
48
|
+
showDots = true,
|
|
49
|
+
maxDots = 5,
|
|
50
|
+
showCounter = false,
|
|
51
|
+
showArrows = true,
|
|
52
|
+
enableZoom = true,
|
|
53
|
+
onClick,
|
|
54
|
+
className,
|
|
55
|
+
}: GalleryCompactProps) {
|
|
56
|
+
const [api, setApi] = useState<CarouselApi>()
|
|
57
|
+
const [currentIndex, setCurrentIndex] = useState(0)
|
|
58
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
59
|
+
// Track if component is mounted (client-side) to avoid hydration mismatches
|
|
60
|
+
const [isMounted, setIsMounted] = useState(false)
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
setIsMounted(true)
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
const total = images.length
|
|
67
|
+
const hasMultiple = total > 1
|
|
68
|
+
|
|
69
|
+
// Determine which images should be loaded (current + neighbors for smooth transition)
|
|
70
|
+
const loadedIndices = useMemo(() => {
|
|
71
|
+
if (total === 0) return new Set<number>()
|
|
72
|
+
const indices = new Set<number>()
|
|
73
|
+
// Always load first image
|
|
74
|
+
indices.add(0)
|
|
75
|
+
// Load current
|
|
76
|
+
indices.add(currentIndex)
|
|
77
|
+
// Load neighbors for smooth transitions
|
|
78
|
+
if (hasMultiple) {
|
|
79
|
+
indices.add((currentIndex - 1 + total) % total)
|
|
80
|
+
indices.add((currentIndex + 1) % total)
|
|
81
|
+
}
|
|
82
|
+
return indices
|
|
83
|
+
}, [currentIndex, total, hasMultiple])
|
|
84
|
+
|
|
85
|
+
// Listen to carousel changes
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!api) return
|
|
88
|
+
|
|
89
|
+
const onSelect = () => {
|
|
90
|
+
setCurrentIndex(api.selectedScrollSnap())
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
api.on('select', onSelect)
|
|
94
|
+
return () => {
|
|
95
|
+
api.off('select', onSelect)
|
|
96
|
+
}
|
|
97
|
+
}, [api])
|
|
98
|
+
|
|
99
|
+
// Navigation handlers - stop all propagation to parent Link
|
|
100
|
+
const stopEvent = useCallback((e: React.MouseEvent | React.PointerEvent) => {
|
|
101
|
+
e.preventDefault()
|
|
102
|
+
e.stopPropagation()
|
|
103
|
+
e.nativeEvent.stopImmediatePropagation()
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
const handlePrev = useCallback(
|
|
107
|
+
(e: React.MouseEvent) => {
|
|
108
|
+
stopEvent(e)
|
|
109
|
+
api?.scrollPrev()
|
|
110
|
+
},
|
|
111
|
+
[api, stopEvent]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const handleNext = useCallback(
|
|
115
|
+
(e: React.MouseEvent) => {
|
|
116
|
+
stopEvent(e)
|
|
117
|
+
api?.scrollNext()
|
|
118
|
+
},
|
|
119
|
+
[api, stopEvent]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Dot click handler
|
|
123
|
+
const handleDotClick = useCallback(
|
|
124
|
+
(e: React.MouseEvent, index: number) => {
|
|
125
|
+
e.preventDefault()
|
|
126
|
+
e.stopPropagation()
|
|
127
|
+
api?.scrollTo(index)
|
|
128
|
+
},
|
|
129
|
+
[api]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Handle container click
|
|
133
|
+
const handleClick = useCallback(
|
|
134
|
+
(e: React.MouseEvent) => {
|
|
135
|
+
// Don't trigger if clicking on navigation elements
|
|
136
|
+
if ((e.target as HTMLElement).closest('[data-nav]')) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
onClick?.()
|
|
140
|
+
},
|
|
141
|
+
[onClick]
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Empty state
|
|
145
|
+
if (total === 0) {
|
|
146
|
+
return (
|
|
147
|
+
<div className={cn('relative w-full h-full bg-muted flex items-center justify-center', className)}>
|
|
148
|
+
<ImageOff className="w-8 h-8 text-muted-foreground/50" />
|
|
149
|
+
</div>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Single image - no carousel needed
|
|
154
|
+
if (!hasMultiple) {
|
|
155
|
+
return (
|
|
156
|
+
<div
|
|
157
|
+
className={cn('relative w-full h-full overflow-hidden group', className)}
|
|
158
|
+
onClick={onClick}
|
|
159
|
+
>
|
|
160
|
+
<div className={cn(
|
|
161
|
+
'w-full h-full transition-transform duration-500 ease-out',
|
|
162
|
+
enableZoom && 'group-hover:scale-110'
|
|
163
|
+
)}>
|
|
164
|
+
<GalleryMedia
|
|
165
|
+
media={images[0]}
|
|
166
|
+
className="w-full h-full"
|
|
167
|
+
priority
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Calculate visible dots
|
|
175
|
+
const visibleDots = images.slice(0, maxDots).map((_, i) => i)
|
|
176
|
+
const remainingCount = total > maxDots ? total - maxDots : 0
|
|
177
|
+
|
|
178
|
+
// SSR fallback - render first image only to avoid hydration mismatch
|
|
179
|
+
// The carousel requires DOM access and will cause issues during SSR
|
|
180
|
+
if (!isMounted) {
|
|
181
|
+
return (
|
|
182
|
+
<div
|
|
183
|
+
className={cn('relative w-full h-full overflow-hidden', className)}
|
|
184
|
+
onClick={onClick}
|
|
185
|
+
>
|
|
186
|
+
<GalleryMedia
|
|
187
|
+
media={images[0]}
|
|
188
|
+
className="w-full h-full"
|
|
189
|
+
priority
|
|
190
|
+
/>
|
|
191
|
+
{/* Show dots indicator placeholder for consistent layout */}
|
|
192
|
+
{showDots && (
|
|
193
|
+
<div
|
|
194
|
+
className={cn(
|
|
195
|
+
'absolute bottom-3 left-1/2 -translate-x-1/2 z-10',
|
|
196
|
+
'flex items-center gap-1.5',
|
|
197
|
+
'px-2 py-1 rounded-full',
|
|
198
|
+
'bg-black/30 backdrop-blur-sm'
|
|
199
|
+
)}
|
|
200
|
+
>
|
|
201
|
+
{visibleDots.map((index) => (
|
|
202
|
+
<div
|
|
203
|
+
key={index}
|
|
204
|
+
className={cn(
|
|
205
|
+
'rounded-full',
|
|
206
|
+
index === 0
|
|
207
|
+
? 'w-2 h-2 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
208
|
+
: 'w-1.5 h-1.5 bg-white/50'
|
|
209
|
+
)}
|
|
210
|
+
/>
|
|
211
|
+
))}
|
|
212
|
+
{remainingCount > 0 && (
|
|
213
|
+
<span className="text-white/80 text-[10px] font-medium ml-0.5">
|
|
214
|
+
+{remainingCount}
|
|
215
|
+
</span>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
{/* Counter badge placeholder */}
|
|
220
|
+
{showCounter && (
|
|
221
|
+
<div className="absolute bottom-3 right-3 z-10 bg-black/50 backdrop-blur-sm text-white text-xs px-2 py-1 rounded-full font-medium">
|
|
222
|
+
1 / {total}
|
|
223
|
+
</div>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
className={cn('relative w-full h-full group/gallery', className)}
|
|
232
|
+
onClick={handleClick}
|
|
233
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
234
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
235
|
+
>
|
|
236
|
+
<Carousel
|
|
237
|
+
setApi={setApi}
|
|
238
|
+
opts={{
|
|
239
|
+
loop: true,
|
|
240
|
+
}}
|
|
241
|
+
className="w-full h-full"
|
|
242
|
+
>
|
|
243
|
+
<CarouselContent className="-ml-0 h-full">
|
|
244
|
+
{images.map((image, index) => {
|
|
245
|
+
const shouldLoad = loadedIndices.has(index)
|
|
246
|
+
const isActive = index === currentIndex
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<CarouselItem key={image.id} className="pl-0 h-full overflow-hidden">
|
|
250
|
+
<div className={cn(
|
|
251
|
+
'w-full h-full transition-transform duration-500 ease-out',
|
|
252
|
+
enableZoom && isActive && isHovered && 'scale-110'
|
|
253
|
+
)}>
|
|
254
|
+
{shouldLoad ? (
|
|
255
|
+
<GalleryMedia
|
|
256
|
+
media={image}
|
|
257
|
+
className="w-full h-full"
|
|
258
|
+
priority={index === 0}
|
|
259
|
+
/>
|
|
260
|
+
) : (
|
|
261
|
+
// Placeholder for unloaded images
|
|
262
|
+
<div className="w-full h-full bg-muted animate-pulse" />
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</CarouselItem>
|
|
266
|
+
)
|
|
267
|
+
})}
|
|
268
|
+
</CarouselContent>
|
|
269
|
+
</Carousel>
|
|
270
|
+
|
|
271
|
+
{/* Navigation arrows - always visible but subtle, more prominent on hover */}
|
|
272
|
+
{showArrows && (
|
|
273
|
+
<>
|
|
274
|
+
<button
|
|
275
|
+
data-nav
|
|
276
|
+
type="button"
|
|
277
|
+
className={cn(
|
|
278
|
+
'absolute left-2 top-1/2 -translate-y-1/2 z-10',
|
|
279
|
+
'w-7 h-7 rounded-full',
|
|
280
|
+
'bg-black/30 backdrop-blur-sm text-white/80',
|
|
281
|
+
'flex items-center justify-center',
|
|
282
|
+
'transition-all duration-200',
|
|
283
|
+
// Always visible but subtle, more prominent on hover
|
|
284
|
+
'opacity-60 group-hover/gallery:opacity-100',
|
|
285
|
+
'hover:bg-black/60 hover:text-white hover:scale-110',
|
|
286
|
+
'active:scale-95',
|
|
287
|
+
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
288
|
+
)}
|
|
289
|
+
onClick={handlePrev}
|
|
290
|
+
onMouseDown={stopEvent}
|
|
291
|
+
onPointerDown={stopEvent}
|
|
292
|
+
aria-label="Previous image"
|
|
293
|
+
>
|
|
294
|
+
<ChevronLeft className="w-4 h-4" />
|
|
295
|
+
</button>
|
|
296
|
+
<button
|
|
297
|
+
data-nav
|
|
298
|
+
type="button"
|
|
299
|
+
className={cn(
|
|
300
|
+
'absolute right-2 top-1/2 -translate-y-1/2 z-10',
|
|
301
|
+
'w-7 h-7 rounded-full',
|
|
302
|
+
'bg-black/30 backdrop-blur-sm text-white/80',
|
|
303
|
+
'flex items-center justify-center',
|
|
304
|
+
'transition-all duration-200',
|
|
305
|
+
// Always visible but subtle, more prominent on hover
|
|
306
|
+
'opacity-60 group-hover/gallery:opacity-100',
|
|
307
|
+
'hover:bg-black/60 hover:text-white hover:scale-110',
|
|
308
|
+
'active:scale-95',
|
|
309
|
+
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
310
|
+
)}
|
|
311
|
+
onClick={handleNext}
|
|
312
|
+
onMouseDown={stopEvent}
|
|
313
|
+
onPointerDown={stopEvent}
|
|
314
|
+
aria-label="Next image"
|
|
315
|
+
>
|
|
316
|
+
<ChevronRight className="w-4 h-4" />
|
|
317
|
+
</button>
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
|
|
321
|
+
{/* Dots indicator */}
|
|
322
|
+
{showDots && (
|
|
323
|
+
<div
|
|
324
|
+
data-nav
|
|
325
|
+
className={cn(
|
|
326
|
+
'absolute bottom-3 left-1/2 -translate-x-1/2 z-10',
|
|
327
|
+
'flex items-center gap-1.5',
|
|
328
|
+
'px-2 py-1 rounded-full',
|
|
329
|
+
'bg-black/30 backdrop-blur-sm'
|
|
330
|
+
)}
|
|
331
|
+
onClick={(e) => e.stopPropagation()}
|
|
332
|
+
>
|
|
333
|
+
{visibleDots.map((index) => (
|
|
334
|
+
<button
|
|
335
|
+
key={index}
|
|
336
|
+
type="button"
|
|
337
|
+
className={cn(
|
|
338
|
+
'rounded-full transition-all duration-200',
|
|
339
|
+
index === currentIndex
|
|
340
|
+
? 'w-2 h-2 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
341
|
+
: 'w-1.5 h-1.5 bg-white/50 hover:bg-white/80'
|
|
342
|
+
)}
|
|
343
|
+
onClick={(e) => handleDotClick(e, index)}
|
|
344
|
+
aria-label={`Go to image ${index + 1}`}
|
|
345
|
+
/>
|
|
346
|
+
))}
|
|
347
|
+
{remainingCount > 0 && (
|
|
348
|
+
<span className="text-white/80 text-[10px] font-medium ml-0.5">
|
|
349
|
+
+{remainingCount}
|
|
350
|
+
</span>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
)}
|
|
354
|
+
|
|
355
|
+
{/* Counter badge */}
|
|
356
|
+
{showCounter && (
|
|
357
|
+
<div className="absolute bottom-3 right-3 z-10 bg-black/50 backdrop-blur-sm text-white text-xs px-2 py-1 rounded-full font-medium">
|
|
358
|
+
{currentIndex + 1} / {total}
|
|
359
|
+
</div>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
)
|
|
363
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GalleryCompact, type GalleryCompactProps } from './GalleryCompact'
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
+
// Main Gallery component
|
|
1
2
|
export { Gallery } from './Gallery'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export { GalleryCompact } from './
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
export {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
|
|
4
|
+
// Compact mode (for cards)
|
|
5
|
+
export { GalleryCompact, type GalleryCompactProps } from './compact'
|
|
6
|
+
|
|
7
|
+
// Preview modes
|
|
8
|
+
export { GalleryCarousel } from './preview'
|
|
9
|
+
export { GalleryGrid, type GalleryGridProps, type GalleryGridLayout } from './preview'
|
|
10
|
+
|
|
11
|
+
// Lightbox
|
|
12
|
+
export { GalleryLightbox } from './lightbox'
|
|
13
|
+
|
|
14
|
+
// Media components
|
|
15
|
+
export { GalleryImage, GalleryVideo, GalleryMedia, type GalleryMediaProps } from './media'
|
|
16
|
+
|
|
17
|
+
// Thumbnails
|
|
18
|
+
export { GalleryThumbnails, GalleryThumbnailsVirtual } from './thumbnails'
|
|
@@ -5,12 +5,12 @@ import { createPortal } from 'react-dom'
|
|
|
5
5
|
import { cn } from '@djangocfg/ui-core/lib'
|
|
6
6
|
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n'
|
|
7
7
|
import { X, ChevronLeft, ChevronRight, Download, Share2, ZoomIn, ZoomOut } from 'lucide-react'
|
|
8
|
-
import { useSwipe } from '
|
|
9
|
-
import { usePreloadImages } from '
|
|
10
|
-
import { useZoom } from '
|
|
11
|
-
import { GalleryMedia } from '
|
|
12
|
-
import { GalleryThumbnails } from '
|
|
13
|
-
import type { GalleryLightboxProps } from '
|
|
8
|
+
import { useSwipe } from '../../hooks/useSwipe'
|
|
9
|
+
import { usePreloadImages } from '../../hooks/usePreloadImages'
|
|
10
|
+
import { useZoom } from '../../hooks/useZoom'
|
|
11
|
+
import { GalleryMedia } from '../media'
|
|
12
|
+
import { GalleryThumbnails } from '../thumbnails'
|
|
13
|
+
import type { GalleryLightboxProps } from '../../types'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* GalleryLightbox - Fullscreen image viewer with zoom and navigation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GalleryLightbox } from './GalleryLightbox'
|
|
@@ -5,7 +5,7 @@ import { cn } from '@djangocfg/ui-core/lib'
|
|
|
5
5
|
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n'
|
|
6
6
|
import { useImageLoader } from '@djangocfg/ui-core/hooks'
|
|
7
7
|
import { ImageOff } from 'lucide-react'
|
|
8
|
-
import type { GalleryImageProps } from '
|
|
8
|
+
import type { GalleryImageProps } from '../../types'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* GalleryImage - Single image with loading state and error handling
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { memo } from 'react'
|
|
4
4
|
import { GalleryImage as GalleryImageComponent } from './GalleryImage'
|
|
5
5
|
import { GalleryVideo } from './GalleryVideo'
|
|
6
|
-
import type { GalleryMediaItem } from '
|
|
6
|
+
import type { GalleryMediaItem } from '../../types'
|
|
7
7
|
|
|
8
8
|
export interface GalleryMediaProps {
|
|
9
9
|
/** Media data */
|