@djangocfg/ui-tools 2.1.119 → 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 +9 -12
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +102 -0
- package/src/tools/Gallery/Gallery.story.tsx +231 -0
- package/src/tools/Gallery/components/compact/GalleryCompact.tsx +72 -7
- package/src/tools/Gallery/components/preview/GalleryCarousel.tsx +83 -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,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
|
+
};
|
|
@@ -56,6 +56,12 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
56
56
|
const [api, setApi] = useState<CarouselApi>()
|
|
57
57
|
const [currentIndex, setCurrentIndex] = useState(0)
|
|
58
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
|
+
}, [])
|
|
59
65
|
|
|
60
66
|
const total = images.length
|
|
61
67
|
const hasMultiple = total > 1
|
|
@@ -90,23 +96,27 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
90
96
|
}
|
|
91
97
|
}, [api])
|
|
92
98
|
|
|
93
|
-
// Navigation handlers
|
|
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
|
+
|
|
94
106
|
const handlePrev = useCallback(
|
|
95
107
|
(e: React.MouseEvent) => {
|
|
96
|
-
e
|
|
97
|
-
e.stopPropagation()
|
|
108
|
+
stopEvent(e)
|
|
98
109
|
api?.scrollPrev()
|
|
99
110
|
},
|
|
100
|
-
[api]
|
|
111
|
+
[api, stopEvent]
|
|
101
112
|
)
|
|
102
113
|
|
|
103
114
|
const handleNext = useCallback(
|
|
104
115
|
(e: React.MouseEvent) => {
|
|
105
|
-
e
|
|
106
|
-
e.stopPropagation()
|
|
116
|
+
stopEvent(e)
|
|
107
117
|
api?.scrollNext()
|
|
108
118
|
},
|
|
109
|
-
[api]
|
|
119
|
+
[api, stopEvent]
|
|
110
120
|
)
|
|
111
121
|
|
|
112
122
|
// Dot click handler
|
|
@@ -165,6 +175,57 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
165
175
|
const visibleDots = images.slice(0, maxDots).map((_, i) => i)
|
|
166
176
|
const remainingCount = total > maxDots ? total - maxDots : 0
|
|
167
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
|
+
|
|
168
229
|
return (
|
|
169
230
|
<div
|
|
170
231
|
className={cn('relative w-full h-full group/gallery', className)}
|
|
@@ -226,6 +287,8 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
226
287
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
227
288
|
)}
|
|
228
289
|
onClick={handlePrev}
|
|
290
|
+
onMouseDown={stopEvent}
|
|
291
|
+
onPointerDown={stopEvent}
|
|
229
292
|
aria-label="Previous image"
|
|
230
293
|
>
|
|
231
294
|
<ChevronLeft className="w-4 h-4" />
|
|
@@ -246,6 +309,8 @@ export const GalleryCompact = memo(function GalleryCompact({
|
|
|
246
309
|
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
|
247
310
|
)}
|
|
248
311
|
onClick={handleNext}
|
|
312
|
+
onMouseDown={stopEvent}
|
|
313
|
+
onPointerDown={stopEvent}
|
|
249
314
|
aria-label="Next image"
|
|
250
315
|
>
|
|
251
316
|
<ChevronRight className="w-4 h-4" />
|
|
@@ -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,12 @@ 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
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
setIsMounted(true)
|
|
73
|
+
}, [])
|
|
68
74
|
|
|
69
75
|
// Notify parent when API is ready
|
|
70
76
|
useEffect(() => {
|
|
@@ -161,6 +167,82 @@ export const GalleryCarousel = memo(function GalleryCarousel({
|
|
|
161
167
|
pointerStartRef.current = null
|
|
162
168
|
}, [enableLightbox, onLightboxOpen])
|
|
163
169
|
|
|
170
|
+
// SSR fallback - render first image only to avoid hydration mismatch
|
|
171
|
+
// The carousel requires DOM access and will cause issues during SSR
|
|
172
|
+
if (!isMounted) {
|
|
173
|
+
return (
|
|
174
|
+
<div className={cn('space-y-3', className)}>
|
|
175
|
+
<div className="relative rounded-xl overflow-hidden">
|
|
176
|
+
<div
|
|
177
|
+
className="relative bg-muted"
|
|
178
|
+
style={{ aspectRatio }}
|
|
179
|
+
>
|
|
180
|
+
<GalleryMedia
|
|
181
|
+
media={images[0]}
|
|
182
|
+
className="w-full h-full"
|
|
183
|
+
priority
|
|
184
|
+
/>
|
|
185
|
+
{/* Dark overlay */}
|
|
186
|
+
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-black/20 pointer-events-none" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
{/* Counter */}
|
|
190
|
+
{showCounter && hasMultiple && (
|
|
191
|
+
<div
|
|
192
|
+
className={cn(
|
|
193
|
+
'absolute bottom-3 right-3 z-10',
|
|
194
|
+
'bg-black/60 backdrop-blur-sm text-white px-3 py-1.5 rounded-full',
|
|
195
|
+
'text-sm font-medium'
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
1 / {total}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Mobile dots */}
|
|
203
|
+
{hasMultiple && (
|
|
204
|
+
<div className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2 md:hidden z-10">
|
|
205
|
+
{mobileDots.map((index) => (
|
|
206
|
+
<div
|
|
207
|
+
key={index}
|
|
208
|
+
className={cn(
|
|
209
|
+
'rounded-full',
|
|
210
|
+
index === 0
|
|
211
|
+
? 'w-2.5 h-2.5 bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]'
|
|
212
|
+
: 'w-2 h-2 bg-white/40'
|
|
213
|
+
)}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
{remainingCount > 0 && (
|
|
217
|
+
<span className="text-white/70 text-xs ml-1">+{remainingCount}</span>
|
|
218
|
+
)}
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Thumbnails placeholder */}
|
|
224
|
+
{showThumbnails && hasMultiple && (
|
|
225
|
+
<div className="hidden md:flex gap-2">
|
|
226
|
+
{images.slice(0, 8).map((image, index) => (
|
|
227
|
+
<div
|
|
228
|
+
key={image.id}
|
|
229
|
+
className={cn(
|
|
230
|
+
'w-16 h-16 rounded-lg overflow-hidden bg-muted flex-shrink-0',
|
|
231
|
+
index === 0 && 'ring-2 ring-primary'
|
|
232
|
+
)}
|
|
233
|
+
>
|
|
234
|
+
<GalleryMedia
|
|
235
|
+
media={image}
|
|
236
|
+
className="w-full h-full"
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
))}
|
|
240
|
+
</div>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
164
246
|
return (
|
|
165
247
|
<div className={cn('space-y-3', className)}>
|
|
166
248
|
<Carousel
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { defineStory, useSelect, useBoolean } from '@djangocfg/playground';
|
|
2
|
+
import { JsonSchemaForm } from './index';
|
|
3
|
+
|
|
4
|
+
export default defineStory({
|
|
5
|
+
title: 'Tools/Json Schema Form',
|
|
6
|
+
component: JsonSchemaForm,
|
|
7
|
+
description: 'Dynamic form generator from JSON Schema using react-jsonschema-form.',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const SCHEMAS = {
|
|
11
|
+
simple: {
|
|
12
|
+
schema: {
|
|
13
|
+
type: 'object' as const,
|
|
14
|
+
required: ['name', 'email'],
|
|
15
|
+
properties: {
|
|
16
|
+
name: { type: 'string' as const, title: 'Name' },
|
|
17
|
+
email: { type: 'string' as const, title: 'Email', format: 'email' },
|
|
18
|
+
age: { type: 'integer' as const, title: 'Age', minimum: 0, maximum: 120 },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
uiSchema: {},
|
|
22
|
+
},
|
|
23
|
+
vehicle: {
|
|
24
|
+
schema: {
|
|
25
|
+
type: 'object' as const,
|
|
26
|
+
required: ['make', 'model', 'year'],
|
|
27
|
+
properties: {
|
|
28
|
+
make: {
|
|
29
|
+
type: 'string' as const,
|
|
30
|
+
title: 'Make',
|
|
31
|
+
enum: ['BMW', 'Mercedes', 'Audi', 'Porsche', 'Tesla'],
|
|
32
|
+
},
|
|
33
|
+
model: { type: 'string' as const, title: 'Model' },
|
|
34
|
+
year: { type: 'integer' as const, title: 'Year', minimum: 1990, maximum: 2025 },
|
|
35
|
+
price: { type: 'number' as const, title: 'Price ($)' },
|
|
36
|
+
features: {
|
|
37
|
+
type: 'array' as const,
|
|
38
|
+
title: 'Features',
|
|
39
|
+
items: {
|
|
40
|
+
type: 'string' as const,
|
|
41
|
+
enum: ['Leather', 'Navigation', 'Sunroof', 'Heated Seats', 'Parking Sensors'],
|
|
42
|
+
},
|
|
43
|
+
uniqueItems: true,
|
|
44
|
+
},
|
|
45
|
+
description: { type: 'string' as const, title: 'Description' },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
uiSchema: {
|
|
49
|
+
description: { 'ui:widget': 'textarea' },
|
|
50
|
+
features: { 'ui:widget': 'checkboxes' },
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
contact: {
|
|
54
|
+
schema: {
|
|
55
|
+
type: 'object' as const,
|
|
56
|
+
required: ['firstName', 'lastName', 'email'],
|
|
57
|
+
properties: {
|
|
58
|
+
firstName: { type: 'string' as const, title: 'First Name' },
|
|
59
|
+
lastName: { type: 'string' as const, title: 'Last Name' },
|
|
60
|
+
email: { type: 'string' as const, title: 'Email', format: 'email' },
|
|
61
|
+
phone: { type: 'string' as const, title: 'Phone' },
|
|
62
|
+
message: { type: 'string' as const, title: 'Message' },
|
|
63
|
+
subscribe: { type: 'boolean' as const, title: 'Subscribe to newsletter' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
uiSchema: {
|
|
67
|
+
message: { 'ui:widget': 'textarea' },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const Interactive = () => {
|
|
73
|
+
const [schemaType] = useSelect('schemaType', {
|
|
74
|
+
options: ['simple', 'vehicle', 'contact'] as const,
|
|
75
|
+
defaultValue: 'vehicle',
|
|
76
|
+
label: 'Schema Type',
|
|
77
|
+
description: 'Select form schema',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const [liveValidate] = useBoolean('liveValidate', {
|
|
81
|
+
defaultValue: false,
|
|
82
|
+
label: 'Live Validate',
|
|
83
|
+
description: 'Validate on every change',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const config = SCHEMAS[schemaType];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="max-w-lg">
|
|
90
|
+
<JsonSchemaForm
|
|
91
|
+
schema={config.schema}
|
|
92
|
+
uiSchema={config.uiSchema}
|
|
93
|
+
liveValidate={liveValidate}
|
|
94
|
+
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const SimpleForm = () => (
|
|
101
|
+
<div className="max-w-md">
|
|
102
|
+
<JsonSchemaForm
|
|
103
|
+
schema={SCHEMAS.simple.schema}
|
|
104
|
+
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export const VehicleForm = () => (
|
|
110
|
+
<div className="max-w-lg">
|
|
111
|
+
<JsonSchemaForm
|
|
112
|
+
schema={SCHEMAS.vehicle.schema}
|
|
113
|
+
uiSchema={SCHEMAS.vehicle.uiSchema}
|
|
114
|
+
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
export const WithDefaultValues = () => (
|
|
120
|
+
<div className="max-w-lg">
|
|
121
|
+
<JsonSchemaForm
|
|
122
|
+
schema={SCHEMAS.vehicle.schema}
|
|
123
|
+
uiSchema={SCHEMAS.vehicle.uiSchema}
|
|
124
|
+
formData={{
|
|
125
|
+
make: 'BMW',
|
|
126
|
+
model: 'X5',
|
|
127
|
+
year: 2023,
|
|
128
|
+
price: 65000,
|
|
129
|
+
features: ['Leather', 'Navigation'],
|
|
130
|
+
}}
|
|
131
|
+
onSubmit={(data) => console.log('Submitted:', data.formData)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
);
|