@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.
Files changed (78) hide show
  1. package/dist/JsonTree-G2TPWQ4C.mjs +4 -0
  2. package/dist/{JsonTree-6RYAOPSS.mjs.map → JsonTree-G2TPWQ4C.mjs.map} +1 -1
  3. package/dist/JsonTree-TWXUBBIG.cjs +10 -0
  4. package/dist/{JsonTree-7OH6CIHT.cjs.map → JsonTree-TWXUBBIG.cjs.map} +1 -1
  5. package/dist/{Mermaid.client-PNXEC6YL.cjs → Mermaid.client-AF4WOQZR.cjs} +9 -11
  6. package/dist/Mermaid.client-AF4WOQZR.cjs.map +1 -0
  7. package/dist/{Mermaid.client-OKACITCW.mjs → Mermaid.client-W4QXJX7Q.mjs} +9 -11
  8. package/dist/Mermaid.client-W4QXJX7Q.mjs.map +1 -0
  9. package/dist/{PlaygroundLayout-SYMEAG3J.cjs → PlaygroundLayout-RZMJWH3Y.cjs} +25 -25
  10. package/dist/{PlaygroundLayout-SYMEAG3J.cjs.map → PlaygroundLayout-RZMJWH3Y.cjs.map} +1 -1
  11. package/dist/{PlaygroundLayout-UQRBU5RH.mjs → PlaygroundLayout-UQABCZ6K.mjs} +4 -4
  12. package/dist/{PlaygroundLayout-UQRBU5RH.mjs.map → PlaygroundLayout-UQABCZ6K.mjs.map} +1 -1
  13. package/dist/{chunk-UOMPPIED.mjs → chunk-4G4UGMOP.mjs} +3 -3
  14. package/dist/chunk-4G4UGMOP.mjs.map +1 -0
  15. package/dist/{chunk-47T5ECYV.cjs → chunk-CY3CQS26.cjs} +3 -3
  16. package/dist/chunk-CY3CQS26.cjs.map +1 -0
  17. package/dist/{chunk-5QT3QYFZ.cjs → chunk-EGYUND4E.cjs} +2 -2
  18. package/dist/chunk-EGYUND4E.cjs.map +1 -0
  19. package/dist/{chunk-DI3HUXHK.cjs → chunk-OYLQZT62.cjs} +2 -2
  20. package/dist/chunk-OYLQZT62.cjs.map +1 -0
  21. package/dist/{chunk-W6YHQI4F.mjs → chunk-OYYCGIBF.mjs} +2 -2
  22. package/dist/chunk-OYYCGIBF.mjs.map +1 -0
  23. package/dist/{chunk-G6PRZP5I.mjs → chunk-XZZ22EHP.mjs} +2 -2
  24. package/dist/chunk-XZZ22EHP.mjs.map +1 -0
  25. package/dist/{components-EASJYK45.mjs → components-4YSJ5ALL.mjs} +3 -3
  26. package/dist/{components-CJ2IB65O.cjs.map → components-4YSJ5ALL.mjs.map} +1 -1
  27. package/dist/{components-CJ2IB65O.cjs → components-BSTP3VLD.cjs} +7 -7
  28. package/dist/{components-EASJYK45.mjs.map → components-BSTP3VLD.cjs.map} +1 -1
  29. package/dist/index.cjs +27 -27
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.mjs +10 -10
  34. package/dist/index.mjs.map +1 -1
  35. package/package.json +12 -5
  36. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
  37. package/src/tools/Gallery/Gallery.story.tsx +231 -0
  38. package/src/tools/Gallery/components/Gallery.tsx +3 -3
  39. package/src/tools/Gallery/components/compact/GalleryCompact.tsx +363 -0
  40. package/src/tools/Gallery/components/compact/index.ts +1 -0
  41. package/src/tools/Gallery/components/index.ts +17 -12
  42. package/src/tools/Gallery/components/{GalleryLightbox.tsx → lightbox/GalleryLightbox.tsx} +6 -6
  43. package/src/tools/Gallery/components/lightbox/index.ts +1 -0
  44. package/src/tools/Gallery/components/{GalleryImage.tsx → media/GalleryImage.tsx} +1 -1
  45. package/src/tools/Gallery/components/{GalleryMedia.tsx → media/GalleryMedia.tsx} +1 -1
  46. package/src/tools/Gallery/components/{GalleryVideo.tsx → media/GalleryVideo.tsx} +1 -1
  47. package/src/tools/Gallery/components/media/index.ts +3 -0
  48. package/src/tools/Gallery/components/{GalleryCarousel.tsx → preview/GalleryCarousel.tsx} +114 -6
  49. package/src/tools/Gallery/components/{GalleryGrid.tsx → preview/GalleryGrid.tsx} +1 -1
  50. package/src/tools/Gallery/components/preview/index.ts +2 -0
  51. package/src/tools/Gallery/components/{GalleryThumbnails.tsx → thumbnails/GalleryThumbnails.tsx} +1 -1
  52. package/src/tools/Gallery/components/{GalleryThumbnailsVirtual.tsx → thumbnails/GalleryThumbnailsVirtual.tsx} +2 -2
  53. package/src/tools/Gallery/components/thumbnails/index.ts +2 -0
  54. package/src/tools/JsonForm/JsonForm.story.tsx +134 -0
  55. package/src/tools/JsonTree/JsonTree.story.tsx +140 -0
  56. package/src/tools/JsonTree/index.tsx +1 -1
  57. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +95 -0
  58. package/src/tools/Map/Map.story.tsx +300 -0
  59. package/src/tools/Mermaid/Mermaid.story.tsx +131 -0
  60. package/src/tools/Mermaid/hooks/useMermaidCleanup.ts +2 -5
  61. package/src/tools/Mermaid/hooks/useMermaidRenderer.ts +7 -1
  62. package/src/tools/Mermaid/hooks/useMermaidValidation.ts +4 -2
  63. package/src/tools/Mermaid/index.tsx +1 -1
  64. package/src/tools/PrettyCode/PrettyCode.story.tsx +116 -0
  65. package/src/tools/PrettyCode/index.tsx +1 -1
  66. package/src/tools/VideoPlayer/VideoPlayer.story.tsx +87 -0
  67. package/src/tools/VideoPlayer/utils/resolvers.ts +2 -2
  68. package/dist/JsonTree-6RYAOPSS.mjs +0 -4
  69. package/dist/JsonTree-7OH6CIHT.cjs +0 -10
  70. package/dist/Mermaid.client-OKACITCW.mjs.map +0 -1
  71. package/dist/Mermaid.client-PNXEC6YL.cjs.map +0 -1
  72. package/dist/chunk-47T5ECYV.cjs.map +0 -1
  73. package/dist/chunk-5QT3QYFZ.cjs.map +0 -1
  74. package/dist/chunk-DI3HUXHK.cjs.map +0 -1
  75. package/dist/chunk-G6PRZP5I.mjs.map +0 -1
  76. package/dist/chunk-UOMPPIED.mjs.map +0 -1
  77. package/dist/chunk-W6YHQI4F.mjs.map +0 -1
  78. 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 './GalleryGrid'
9
- import { GalleryCarousel } from './GalleryCarousel'
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('./GalleryLightbox').then((mod) => ({ default: mod.GalleryLightbox }))
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
- 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'
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 '../hooks/useSwipe'
9
- import { usePreloadImages } from '../hooks/usePreloadImages'
10
- import { useZoom } from '../hooks/useZoom'
11
- import { GalleryMedia } from './GalleryMedia'
12
- import { GalleryThumbnails } from './GalleryThumbnails'
13
- import type { GalleryLightboxProps } from '../types'
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 '../types'
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 '../types'
6
+ import type { GalleryMediaItem } from '../../types'
7
7
 
8
8
  export interface GalleryMediaProps {
9
9
  /** Media data */