@djangocfg/ui-tools 2.1.119 → 2.1.121
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 +9 -12
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
- package/src/tools/Gallery/Gallery.story.tsx +237 -0
- package/src/tools/Gallery/components/compact/GalleryCompact.tsx +105 -10
- package/src/tools/Gallery/components/compact/index.ts +1 -1
- package/src/tools/Gallery/components/index.ts +1 -1
- package/src/tools/Gallery/components/media/GalleryImage.tsx +2 -2
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +86 -1
- package/src/tools/Gallery/index.ts +1 -1
- 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
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { defineStory, useBoolean, useSelect, useNumber } from '@djangocfg/playground';
|
|
2
|
+
import { Gallery, GalleryCompact, type GalleryMediaItem, type GalleryAspectRatio } 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
|
+
const [aspectRatio] = useSelect('aspectRatio', {
|
|
189
|
+
options: ['auto', '16/9', '4/3', '3/2', '1/1'] as const,
|
|
190
|
+
defaultValue: '4/3' as GalleryAspectRatio,
|
|
191
|
+
label: 'Aspect Ratio',
|
|
192
|
+
description: 'Image container aspect ratio',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className="space-y-8">
|
|
197
|
+
{/* GalleryCompact */}
|
|
198
|
+
<div>
|
|
199
|
+
<h3 className="text-lg font-semibold mb-4">GalleryCompact</h3>
|
|
200
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
201
|
+
<div className="rounded-xl border border-border overflow-hidden">
|
|
202
|
+
<GalleryCompact
|
|
203
|
+
images={SAMPLE_IMAGES}
|
|
204
|
+
aspectRatio={aspectRatio}
|
|
205
|
+
showDots={showDots}
|
|
206
|
+
showArrows={showArrows}
|
|
207
|
+
enableZoom={enableZoom}
|
|
208
|
+
showCounter={showCounter}
|
|
209
|
+
maxDots={maxDots}
|
|
210
|
+
/>
|
|
211
|
+
<div className="p-4">
|
|
212
|
+
<h3 className="font-medium">Interactive Card</h3>
|
|
213
|
+
<p className="text-sm text-muted-foreground">Use controls on the right →</p>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Full Gallery */}
|
|
220
|
+
<div>
|
|
221
|
+
<h3 className="text-lg font-semibold mb-4">Full Gallery ({previewMode})</h3>
|
|
222
|
+
<div className="max-w-4xl">
|
|
223
|
+
<Gallery
|
|
224
|
+
images={SAMPLE_IMAGES}
|
|
225
|
+
previewMode={previewMode}
|
|
226
|
+
showThumbnails
|
|
227
|
+
showControls
|
|
228
|
+
showCounter={showCounter}
|
|
229
|
+
enableLightbox
|
|
230
|
+
enableKeyboard
|
|
231
|
+
aspectRatio={16 / 9}
|
|
232
|
+
/>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
};
|
|
@@ -12,9 +12,14 @@ import { ChevronLeft, ChevronRight, ImageOff } from 'lucide-react'
|
|
|
12
12
|
import { GalleryMedia } from '../media'
|
|
13
13
|
import type { GalleryMediaItem } from '../../types'
|
|
14
14
|
|
|
15
|
+
/** Preset aspect ratios for common use cases */
|
|
16
|
+
export type GalleryAspectRatio = '16/9' | '4/3' | '3/2' | '1/1' | 'auto'
|
|
17
|
+
|
|
15
18
|
export interface GalleryCompactProps {
|
|
16
19
|
/** Array of images to display */
|
|
17
20
|
images: GalleryMediaItem[]
|
|
21
|
+
/** Aspect ratio preset or 'auto' to fill parent (default: 'auto') */
|
|
22
|
+
aspectRatio?: GalleryAspectRatio
|
|
18
23
|
/** Show dots indicator (default: true) */
|
|
19
24
|
showDots?: boolean
|
|
20
25
|
/** Max dots to show (default: 5) */
|
|
@@ -43,8 +48,18 @@ export interface GalleryCompactProps {
|
|
|
43
48
|
* - Fills parent container
|
|
44
49
|
* - Stops event propagation on navigation
|
|
45
50
|
*/
|
|
51
|
+
/** Map aspect ratio presets to CSS values */
|
|
52
|
+
const aspectRatioMap: Record<GalleryAspectRatio, string | undefined> = {
|
|
53
|
+
'16/9': '16 / 9',
|
|
54
|
+
'4/3': '4 / 3',
|
|
55
|
+
'3/2': '3 / 2',
|
|
56
|
+
'1/1': '1 / 1',
|
|
57
|
+
'auto': undefined,
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
export const GalleryCompact = memo(function GalleryCompact({
|
|
47
61
|
images,
|
|
62
|
+
aspectRatio = 'auto',
|
|
48
63
|
showDots = true,
|
|
49
64
|
maxDots = 5,
|
|
50
65
|
showCounter = false,
|
|
@@ -56,9 +71,23 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
56
71
|
const [api, setApi] = useState<CarouselApi>()
|
|
57
72
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
58
73
|
const [isHovered, setIsHovered] = useState(false)
|
|
74
|
+
// Track if component is mounted (client-side) to avoid hydration mismatches
|
|
75
|
+
const [isMounted, setIsMounted] = useState(false)
|
|
76
|
+
|
|
77
|
+
// Compute aspect ratio style - if set, we control height via aspect-ratio, otherwise fill parent
|
|
78
|
+
const aspectRatioStyle = aspectRatioMap[aspectRatio]
|
|
79
|
+
const containerStyle = aspectRatioStyle ? { aspectRatio: aspectRatioStyle } : undefined
|
|
80
|
+
const hasFixedAspect = aspectRatio !== 'auto'
|
|
81
|
+
const containerClass = hasFixedAspect ? 'relative w-full' : 'relative w-full h-full'
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
setIsMounted(true)
|
|
85
|
+
}, [])
|
|
59
86
|
|
|
60
87
|
const total = images.length
|
|
61
88
|
const hasMultiple = total > 1
|
|
89
|
+
// Stable key for carousel reset on images change
|
|
90
|
+
const carouselKey = images[0]?.id ?? 'empty'
|
|
62
91
|
|
|
63
92
|
// Determine which images should be loaded (current + neighbors for smooth transition)
|
|
64
93
|
const loadedIndices = useMemo(() => {
|
|
@@ -90,23 +119,27 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
90
119
|
}
|
|
91
120
|
}, [api])
|
|
92
121
|
|
|
93
|
-
// Navigation handlers
|
|
122
|
+
// Navigation handlers - stop all propagation to parent Link
|
|
123
|
+
const stopEvent = useCallback((e: React.MouseEvent | React.PointerEvent) => {
|
|
124
|
+
e.preventDefault()
|
|
125
|
+
e.stopPropagation()
|
|
126
|
+
e.nativeEvent.stopImmediatePropagation()
|
|
127
|
+
}, [])
|
|
128
|
+
|
|
94
129
|
const handlePrev = useCallback(
|
|
95
130
|
(e: React.MouseEvent) => {
|
|
96
|
-
e
|
|
97
|
-
e.stopPropagation()
|
|
131
|
+
stopEvent(e)
|
|
98
132
|
api?.scrollPrev()
|
|
99
133
|
},
|
|
100
|
-
[api]
|
|
134
|
+
[api, stopEvent]
|
|
101
135
|
)
|
|
102
136
|
|
|
103
137
|
const handleNext = useCallback(
|
|
104
138
|
(e: React.MouseEvent) => {
|
|
105
|
-
e
|
|
106
|
-
e.stopPropagation()
|
|
139
|
+
stopEvent(e)
|
|
107
140
|
api?.scrollNext()
|
|
108
141
|
},
|
|
109
|
-
[api]
|
|
142
|
+
[api, stopEvent]
|
|
110
143
|
)
|
|
111
144
|
|
|
112
145
|
// Dot click handler
|
|
@@ -134,7 +167,10 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
134
167
|
// Empty state
|
|
135
168
|
if (total === 0) {
|
|
136
169
|
return (
|
|
137
|
-
<div
|
|
170
|
+
<div
|
|
171
|
+
className={cn(containerClass, 'bg-muted flex items-center justify-center', className)}
|
|
172
|
+
style={containerStyle}
|
|
173
|
+
>
|
|
138
174
|
<ImageOff className="w-8 h-8 text-muted-foreground/50" />
|
|
139
175
|
</div>
|
|
140
176
|
)
|
|
@@ -144,7 +180,8 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
144
180
|
if (!hasMultiple) {
|
|
145
181
|
return (
|
|
146
182
|
<div
|
|
147
|
-
className={cn('
|
|
183
|
+
className={cn(containerClass, 'overflow-hidden group', className)}
|
|
184
|
+
style={containerStyle}
|
|
148
185
|
onClick={onClick}
|
|
149
186
|
>
|
|
150
187
|
<div className={cn(
|
|
@@ -165,14 +202,68 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
165
202
|
const visibleDots = images.slice(0, maxDots).map((_, i) => i)
|
|
166
203
|
const remainingCount = total > maxDots ? total - maxDots : 0
|
|
167
204
|
|
|
205
|
+
// SSR fallback - render first image only to avoid hydration mismatch
|
|
206
|
+
// The carousel requires DOM access and will cause issues during SSR
|
|
207
|
+
if (!isMounted) {
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
className={cn(containerClass, 'overflow-hidden', className)}
|
|
211
|
+
style={containerStyle}
|
|
212
|
+
onClick={onClick}
|
|
213
|
+
>
|
|
214
|
+
<GalleryMedia
|
|
215
|
+
media={images[0]}
|
|
216
|
+
className="w-full h-full"
|
|
217
|
+
priority
|
|
218
|
+
/>
|
|
219
|
+
{/* Show dots indicator placeholder for consistent layout */}
|
|
220
|
+
{showDots && (
|
|
221
|
+
<div
|
|
222
|
+
className={cn(
|
|
223
|
+
'absolute bottom-3 left-1/2 -translate-x-1/2 z-10',
|
|
224
|
+
'flex items-center gap-1.5',
|
|
225
|
+
'px-2 py-1 rounded-full',
|
|
226
|
+
'bg-black/30 backdrop-blur-sm'
|
|
227
|
+
)}
|
|
228
|
+
>
|
|
229
|
+
{visibleDots.map((index) => (
|
|
230
|
+
<div
|
|
231
|
+
key={index}
|
|
232
|
+
className={cn(
|
|
233
|
+
'rounded-full',
|
|
234
|
+
index === 0
|
|
235
|
+
? 'w-2 h-2 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
236
|
+
: 'w-1.5 h-1.5 bg-white/50'
|
|
237
|
+
)}
|
|
238
|
+
/>
|
|
239
|
+
))}
|
|
240
|
+
{remainingCount > 0 && (
|
|
241
|
+
<span className="text-white/80 text-[10px] font-medium ml-0.5">
|
|
242
|
+
+{remainingCount}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
{/* Counter badge placeholder */}
|
|
248
|
+
{showCounter && (
|
|
249
|
+
<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">
|
|
250
|
+
1 / {total}
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
168
257
|
return (
|
|
169
258
|
<div
|
|
170
|
-
className={cn('
|
|
259
|
+
className={cn(containerClass, 'group/gallery', className)}
|
|
260
|
+
style={containerStyle}
|
|
171
261
|
onClick={handleClick}
|
|
172
262
|
onMouseEnter={() => setIsHovered(true)}
|
|
173
263
|
onMouseLeave={() => setIsHovered(false)}
|
|
174
264
|
>
|
|
175
265
|
<Carousel
|
|
266
|
+
key={carouselKey}
|
|
176
267
|
setApi={setApi}
|
|
177
268
|
opts={{
|
|
178
269
|
loop: true,
|
|
@@ -226,6 +317,8 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
226
317
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
227
318
|
)}
|
|
228
319
|
onClick={handlePrev}
|
|
320
|
+
onMouseDown={stopEvent}
|
|
321
|
+
onPointerDown={stopEvent}
|
|
229
322
|
aria-label="Previous image"
|
|
230
323
|
>
|
|
231
324
|
<ChevronLeft className="w-4 h-4" />
|
|
@@ -246,6 +339,8 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
246
339
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
247
340
|
)}
|
|
248
341
|
onClick={handleNext}
|
|
342
|
+
onMouseDown={stopEvent}
|
|
343
|
+
onPointerDown={stopEvent}
|
|
249
344
|
aria-label="Next image"
|
|
250
345
|
>
|
|
251
346
|
<ChevronRight className="w-4 h-4" />
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { GalleryCompact, type GalleryCompactProps } from './GalleryCompact'
|
|
1
|
+
export { GalleryCompact, type GalleryCompactProps, type GalleryAspectRatio } from './GalleryCompact'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
export { Gallery } from './Gallery'
|
|
3
3
|
|
|
4
4
|
// Compact mode (for cards)
|
|
5
|
-
export { GalleryCompact, type GalleryCompactProps } from './compact'
|
|
5
|
+
export { GalleryCompact, type GalleryCompactProps, type GalleryAspectRatio } from './compact'
|
|
6
6
|
|
|
7
7
|
// Preview modes
|
|
8
8
|
export { GalleryCarousel } from './preview'
|
|
@@ -43,7 +43,7 @@ export const GalleryImage = memo(function GalleryImage({
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<div
|
|
46
|
-
className={cn('relative overflow-hidden', className)}
|
|
46
|
+
className={cn('relative w-full h-full overflow-hidden', className)}
|
|
47
47
|
onClick={onClick}
|
|
48
48
|
>
|
|
49
49
|
{/* Loading skeleton */}
|
|
@@ -56,7 +56,7 @@ export const GalleryImage = memo(function GalleryImage({
|
|
|
56
56
|
<img
|
|
57
57
|
src={image.src}
|
|
58
58
|
alt={image.alt || 'Gallery image'}
|
|
59
|
-
className="w-full h-full object-cover animate-in fade-in-0 duration-300"
|
|
59
|
+
className="absolute inset-0 w-full h-full object-cover animate-in fade-in-0 duration-300"
|
|
60
60
|
loading={priority ? 'eager' : 'lazy'}
|
|
61
61
|
decoding="async"
|
|
62
62
|
/>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
import { memo, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
4
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
import { cn } from '@djangocfg/ui-core/lib'
|
|
6
6
|
import {
|
|
7
7
|
Carousel,
|
|
@@ -65,6 +65,14 @@ export const GalleryCarousel = memo(function GalleryCarousel({
|
|
|
65
65
|
className,
|
|
66
66
|
}: GalleryCarouselProps) {
|
|
67
67
|
const [api, setApi] = React.useState<CarouselApi>()
|
|
68
|
+
// Track if component is mounted (client-side) to avoid hydration mismatches
|
|
69
|
+
const [isMounted, setIsMounted] = useState(false)
|
|
70
|
+
// Stable key for carousel reset on images change
|
|
71
|
+
const carouselKey = images[0]?.id ?? 'empty'
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
setIsMounted(true)
|
|
75
|
+
}, [])
|
|
68
76
|
|
|
69
77
|
// Notify parent when API is ready
|
|
70
78
|
useEffect(() => {
|
|
@@ -161,9 +169,86 @@ export const GalleryCarousel = memo(function GalleryCarousel({
|
|
|
161
169
|
pointerStartRef.current = null
|
|
162
170
|
}, [enableLightbox, onLightboxOpen])
|
|
163
171
|
|
|
172
|
+
// SSR fallback - render first image only to avoid hydration mismatch
|
|
173
|
+
// The carousel requires DOM access and will cause issues during SSR
|
|
174
|
+
if (!isMounted) {
|
|
175
|
+
return (
|
|
176
|
+
<div className={cn('space-y-3', className)}>
|
|
177
|
+
<div className="relative rounded-xl overflow-hidden">
|
|
178
|
+
<div
|
|
179
|
+
className="relative bg-muted"
|
|
180
|
+
style={{ aspectRatio }}
|
|
181
|
+
>
|
|
182
|
+
<GalleryMedia
|
|
183
|
+
media={images[0]}
|
|
184
|
+
className="w-full h-full"
|
|
185
|
+
priority
|
|
186
|
+
/>
|
|
187
|
+
{/* Dark overlay */}
|
|
188
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-black/20 pointer-events-none" />
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Counter */}
|
|
192
|
+
{showCounter && hasMultiple && (
|
|
193
|
+
<div
|
|
194
|
+
className={cn(
|
|
195
|
+
'absolute bottom-3 right-3 z-10',
|
|
196
|
+
'bg-black/60 backdrop-blur-sm text-white px-3 py-1.5 rounded-full',
|
|
197
|
+
'text-sm font-medium'
|
|
198
|
+
)}
|
|
199
|
+
>
|
|
200
|
+
1 / {total}
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
|
|
204
|
+
{/* Mobile dots */}
|
|
205
|
+
{hasMultiple && (
|
|
206
|
+
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 md:hidden z-10">
|
|
207
|
+
{mobileDots.map((index) => (
|
|
208
|
+
<div
|
|
209
|
+
key={index}
|
|
210
|
+
className={cn(
|
|
211
|
+
'rounded-full',
|
|
212
|
+
index === 0
|
|
213
|
+
? 'w-2.5 h-2.5 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
214
|
+
: 'w-2 h-2 bg-white/40'
|
|
215
|
+
)}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
{remainingCount > 0 && (
|
|
219
|
+
<span className="text-white/70 text-xs ml-1">+{remainingCount}</span>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{/* Thumbnails placeholder */}
|
|
226
|
+
{showThumbnails && hasMultiple && (
|
|
227
|
+
<div className="hidden md:flex gap-2">
|
|
228
|
+
{images.slice(0, 8).map((image, index) => (
|
|
229
|
+
<div
|
|
230
|
+
key={image.id}
|
|
231
|
+
className={cn(
|
|
232
|
+
'w-16 h-16 rounded-lg overflow-hidden bg-muted flex-shrink-0',
|
|
233
|
+
index === 0 && 'ring-2 ring-primary'
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
<GalleryMedia
|
|
237
|
+
media={image}
|
|
238
|
+
className="w-full h-full"
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
))}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
164
248
|
return (
|
|
165
249
|
<div className={cn('space-y-3', className)}>
|
|
166
250
|
<Carousel
|
|
251
|
+
key={carouselKey}
|
|
167
252
|
setApi={setApi}
|
|
168
253
|
opts={{
|
|
169
254
|
loop: true,
|
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
GalleryThumbnailsVirtual,
|
|
11
11
|
GalleryLightbox,
|
|
12
12
|
} from './components'
|
|
13
|
-
export type { GalleryCompactProps, GalleryGridProps, GalleryGridLayout } from './components'
|
|
13
|
+
export type { GalleryCompactProps, GalleryAspectRatio, GalleryGridProps, GalleryGridLayout } from './components'
|
|
14
14
|
|
|
15
15
|
// Hooks
|
|
16
16
|
export {
|