veloria-ui 0.1.2
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/CHANGELOG.md +206 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/cli/index.js +511 -0
- package/dist/index.d.mts +1317 -0
- package/dist/index.d.ts +1317 -0
- package/dist/index.js +5373 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +5130 -0
- package/dist/index.mjs.map +1 -0
- package/dist/provider.d.mts +15 -0
- package/dist/provider.d.ts +15 -0
- package/dist/provider.js +1197 -0
- package/dist/provider.js.map +1 -0
- package/dist/provider.mjs +1161 -0
- package/dist/provider.mjs.map +1 -0
- package/dist/tailwind.d.ts +25 -0
- package/dist/tailwind.js +129 -0
- package/package.json +138 -0
- package/src/cli/index.ts +303 -0
- package/src/cli/registry.ts +139 -0
- package/src/components/advanced-forms/index.tsx +975 -0
- package/src/components/basic/Button.tsx +135 -0
- package/src/components/basic/IconButton.tsx +69 -0
- package/src/components/basic/index.tsx +446 -0
- package/src/components/data-display/index.tsx +1158 -0
- package/src/components/feedback/index.tsx +1051 -0
- package/src/components/forms/index.tsx +476 -0
- package/src/components/layout/index.tsx +296 -0
- package/src/components/media/index.tsx +437 -0
- package/src/components/navigation/index.tsx +484 -0
- package/src/components/overlay/index.tsx +473 -0
- package/src/components/utility/index.tsx +566 -0
- package/src/hooks/index.ts +602 -0
- package/src/hooks/use-toast.tsx +74 -0
- package/src/index.ts +396 -0
- package/src/provider.tsx +54 -0
- package/src/styles/atlas.css +252 -0
- package/src/tailwind.ts +124 -0
- package/src/types/index.ts +95 -0
- package/src/utils/cn.ts +66 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../utils/cn";
|
|
3
|
+
|
|
4
|
+
// ─── Image ─────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
7
|
+
fallback?: React.ReactNode;
|
|
8
|
+
aspectRatio?: "square" | "video" | "portrait" | "landscape" | string;
|
|
9
|
+
fit?: "cover" | "contain" | "fill" | "none" | "scale-down";
|
|
10
|
+
rounded?: "none" | "sm" | "md" | "lg" | "xl" | "full";
|
|
11
|
+
loading?: "lazy" | "eager";
|
|
12
|
+
caption?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const roundedMap = {
|
|
16
|
+
none: "rounded-none",
|
|
17
|
+
sm: "rounded-sm",
|
|
18
|
+
md: "rounded-md",
|
|
19
|
+
lg: "rounded-lg",
|
|
20
|
+
xl: "rounded-xl",
|
|
21
|
+
full: "rounded-full",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const aspectMap = {
|
|
25
|
+
square: "aspect-square",
|
|
26
|
+
video: "aspect-video",
|
|
27
|
+
portrait: "aspect-[3/4]",
|
|
28
|
+
landscape: "aspect-[4/3]",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
|
32
|
+
(
|
|
33
|
+
{
|
|
34
|
+
className,
|
|
35
|
+
fallback,
|
|
36
|
+
aspectRatio,
|
|
37
|
+
fit = "cover",
|
|
38
|
+
rounded = "none",
|
|
39
|
+
alt = "",
|
|
40
|
+
loading = "lazy",
|
|
41
|
+
caption,
|
|
42
|
+
onError,
|
|
43
|
+
style,
|
|
44
|
+
...props
|
|
45
|
+
},
|
|
46
|
+
ref
|
|
47
|
+
) => {
|
|
48
|
+
const [errored, setErrored] = React.useState(false);
|
|
49
|
+
|
|
50
|
+
const handleError = (e: React.SyntheticEvent<HTMLImageElement>) => {
|
|
51
|
+
setErrored(true);
|
|
52
|
+
onError?.(e);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const aspectClass =
|
|
56
|
+
aspectRatio && aspectRatio in aspectMap
|
|
57
|
+
? aspectMap[aspectRatio as keyof typeof aspectMap]
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
const img = errored && fallback ? (
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
"atlas-image-fallback flex items-center justify-center bg-muted text-muted-foreground",
|
|
64
|
+
roundedMap[rounded],
|
|
65
|
+
aspectClass,
|
|
66
|
+
className
|
|
67
|
+
)}
|
|
68
|
+
>
|
|
69
|
+
{fallback}
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<img
|
|
73
|
+
ref={ref}
|
|
74
|
+
alt={alt}
|
|
75
|
+
loading={loading}
|
|
76
|
+
onError={handleError}
|
|
77
|
+
className={cn(
|
|
78
|
+
"atlas-image block",
|
|
79
|
+
`object-${fit}`,
|
|
80
|
+
roundedMap[rounded],
|
|
81
|
+
aspectClass && "w-full h-full",
|
|
82
|
+
className
|
|
83
|
+
)}
|
|
84
|
+
style={style}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (caption) {
|
|
90
|
+
return (
|
|
91
|
+
<figure className={cn("inline-block", aspectClass)}>
|
|
92
|
+
{img}
|
|
93
|
+
<figcaption className="mt-2 text-xs text-muted-foreground text-center">
|
|
94
|
+
{caption}
|
|
95
|
+
</figcaption>
|
|
96
|
+
</figure>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return img;
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
Image.displayName = "Image";
|
|
104
|
+
|
|
105
|
+
// ─── VideoPlayer ───────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
export interface VideoPlayerProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
|
|
108
|
+
src: string;
|
|
109
|
+
poster?: string;
|
|
110
|
+
aspectRatio?: "square" | "video" | "portrait" | "landscape";
|
|
111
|
+
rounded?: "none" | "sm" | "md" | "lg" | "xl";
|
|
112
|
+
caption?: string;
|
|
113
|
+
tracks?: {
|
|
114
|
+
src: string;
|
|
115
|
+
kind: "subtitles" | "captions" | "descriptions" | "chapters" | "metadata";
|
|
116
|
+
srcLang: string;
|
|
117
|
+
label: string;
|
|
118
|
+
default?: boolean;
|
|
119
|
+
}[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const VideoPlayer = React.forwardRef<HTMLVideoElement, VideoPlayerProps>(
|
|
123
|
+
({ className, src, poster, aspectRatio = "video", rounded = "lg", caption, tracks = [], controls = true, ...props }, ref) => {
|
|
124
|
+
const aspectClass = aspectMap[aspectRatio] ?? "aspect-video";
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<figure className={cn("atlas-video-player w-full", className)}>
|
|
128
|
+
<div className={cn(aspectClass, "overflow-hidden", roundedMap[rounded], "bg-black")}>
|
|
129
|
+
<video
|
|
130
|
+
ref={ref}
|
|
131
|
+
src={src}
|
|
132
|
+
poster={poster}
|
|
133
|
+
controls={controls}
|
|
134
|
+
className="w-full h-full object-contain"
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
{tracks.map((track, i) => (
|
|
138
|
+
<track
|
|
139
|
+
key={i}
|
|
140
|
+
src={track.src}
|
|
141
|
+
kind={track.kind}
|
|
142
|
+
srcLang={track.srcLang}
|
|
143
|
+
label={track.label}
|
|
144
|
+
default={track.default}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
Your browser does not support the video tag.
|
|
148
|
+
</video>
|
|
149
|
+
</div>
|
|
150
|
+
{caption && (
|
|
151
|
+
<figcaption className="mt-2 text-xs text-muted-foreground text-center">
|
|
152
|
+
{caption}
|
|
153
|
+
</figcaption>
|
|
154
|
+
)}
|
|
155
|
+
</figure>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
);
|
|
159
|
+
VideoPlayer.displayName = "VideoPlayer";
|
|
160
|
+
|
|
161
|
+
// ─── AudioPlayer ───────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
export interface AudioPlayerProps extends Omit<React.AudioHTMLAttributes<HTMLAudioElement>, "title"> {
|
|
164
|
+
src: string;
|
|
165
|
+
title?: string;
|
|
166
|
+
artist?: string;
|
|
167
|
+
coverArt?: string;
|
|
168
|
+
showWaveform?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const AudioPlayer = React.forwardRef<HTMLAudioElement, AudioPlayerProps>(
|
|
172
|
+
({ className, src, title, artist, coverArt, controls = true, ...props }, ref) => {
|
|
173
|
+
const [isPlaying, setIsPlaying] = React.useState(false);
|
|
174
|
+
const [currentTime, setCurrentTime] = React.useState(0);
|
|
175
|
+
const [duration, setDuration] = React.useState(0);
|
|
176
|
+
const audioRef = React.useRef<HTMLAudioElement>(null);
|
|
177
|
+
|
|
178
|
+
const combinedRef = (node: HTMLAudioElement | null) => {
|
|
179
|
+
(audioRef as React.MutableRefObject<HTMLAudioElement | null>).current = node;
|
|
180
|
+
if (typeof ref === "function") ref(node);
|
|
181
|
+
else if (ref) (ref as React.MutableRefObject<HTMLAudioElement | null>).current = node;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const togglePlay = () => {
|
|
185
|
+
const audio = audioRef.current;
|
|
186
|
+
if (!audio) return;
|
|
187
|
+
if (isPlaying) {
|
|
188
|
+
audio.pause();
|
|
189
|
+
} else {
|
|
190
|
+
audio.play();
|
|
191
|
+
}
|
|
192
|
+
setIsPlaying(!isPlaying);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const formatTime = (secs: number) => {
|
|
196
|
+
const m = Math.floor(secs / 60);
|
|
197
|
+
const s = Math.floor(secs % 60);
|
|
198
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className={cn("atlas-audio-player flex items-center gap-4 rounded-xl border border-border bg-card p-4 w-full", className)}>
|
|
203
|
+
<audio
|
|
204
|
+
ref={combinedRef}
|
|
205
|
+
src={src}
|
|
206
|
+
onTimeUpdate={(e) => setCurrentTime(e.currentTarget.currentTime)}
|
|
207
|
+
onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
|
|
208
|
+
onEnded={() => setIsPlaying(false)}
|
|
209
|
+
{...props}
|
|
210
|
+
/>
|
|
211
|
+
{coverArt && (
|
|
212
|
+
<div className="h-12 w-12 shrink-0 rounded-lg overflow-hidden bg-muted">
|
|
213
|
+
<img src={coverArt} alt={title ?? "Album art"} className="h-full w-full object-cover" />
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<button
|
|
217
|
+
type="button"
|
|
218
|
+
onClick={togglePlay}
|
|
219
|
+
aria-label={isPlaying ? "Pause" : "Play"}
|
|
220
|
+
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
221
|
+
>
|
|
222
|
+
{isPlaying ? (
|
|
223
|
+
<svg className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24">
|
|
224
|
+
<path d="M6 4h4v16H6zM14 4h4v16h-4z" />
|
|
225
|
+
</svg>
|
|
226
|
+
) : (
|
|
227
|
+
<svg className="h-4 w-4 ml-0.5" fill="currentColor" viewBox="0 0 24 24">
|
|
228
|
+
<path d="M8 5v14l11-7z" />
|
|
229
|
+
</svg>
|
|
230
|
+
)}
|
|
231
|
+
</button>
|
|
232
|
+
<div className="flex-1 min-w-0">
|
|
233
|
+
{(title || artist) && (
|
|
234
|
+
<div className="mb-1.5">
|
|
235
|
+
{title && <p className="text-sm font-medium truncate">{title}</p>}
|
|
236
|
+
{artist && <p className="text-xs text-muted-foreground truncate">{artist}</p>}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
<div className="flex items-center gap-2">
|
|
240
|
+
<span className="text-xs text-muted-foreground tabular-nums w-8 shrink-0">
|
|
241
|
+
{formatTime(currentTime)}
|
|
242
|
+
</span>
|
|
243
|
+
<input
|
|
244
|
+
type="range"
|
|
245
|
+
min={0}
|
|
246
|
+
max={duration || 100}
|
|
247
|
+
value={currentTime}
|
|
248
|
+
onChange={(e) => {
|
|
249
|
+
const t = Number(e.target.value);
|
|
250
|
+
setCurrentTime(t);
|
|
251
|
+
if (audioRef.current) audioRef.current.currentTime = t;
|
|
252
|
+
}}
|
|
253
|
+
className="flex-1 h-1.5 rounded-full bg-secondary accent-primary cursor-pointer"
|
|
254
|
+
aria-label="Seek"
|
|
255
|
+
/>
|
|
256
|
+
<span className="text-xs text-muted-foreground tabular-nums w-8 shrink-0 text-right">
|
|
257
|
+
{formatTime(duration)}
|
|
258
|
+
</span>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
AudioPlayer.displayName = "AudioPlayer";
|
|
266
|
+
|
|
267
|
+
// ─── Carousel ─────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
270
|
+
items: React.ReactNode[];
|
|
271
|
+
autoPlay?: boolean;
|
|
272
|
+
autoPlayInterval?: number;
|
|
273
|
+
showDots?: boolean;
|
|
274
|
+
showArrows?: boolean;
|
|
275
|
+
loop?: boolean;
|
|
276
|
+
slidesPerView?: number;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const Carousel = React.forwardRef<HTMLDivElement, CarouselProps>(
|
|
280
|
+
({
|
|
281
|
+
className,
|
|
282
|
+
items,
|
|
283
|
+
autoPlay = false,
|
|
284
|
+
autoPlayInterval = 4000,
|
|
285
|
+
showDots = true,
|
|
286
|
+
showArrows = true,
|
|
287
|
+
loop = true,
|
|
288
|
+
slidesPerView = 1,
|
|
289
|
+
...props
|
|
290
|
+
}, ref) => {
|
|
291
|
+
const [current, setCurrent] = React.useState(0);
|
|
292
|
+
const total = items.length;
|
|
293
|
+
|
|
294
|
+
const prev = () => setCurrent((c) => (c === 0 ? (loop ? total - 1 : 0) : c - 1));
|
|
295
|
+
const next = React.useCallback(() => setCurrent((c) => (c === total - 1 ? (loop ? 0 : c) : c + 1)), [total, loop]);
|
|
296
|
+
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
if (!autoPlay) return;
|
|
299
|
+
const id = setInterval(next, autoPlayInterval);
|
|
300
|
+
return () => clearInterval(id);
|
|
301
|
+
}, [autoPlay, autoPlayInterval, next]);
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div ref={ref} className={cn("atlas-carousel relative w-full overflow-hidden", className)} {...props}>
|
|
305
|
+
<div
|
|
306
|
+
className="flex transition-transform duration-400 ease-in-out"
|
|
307
|
+
style={{ transform: `translateX(-${current * (100 / slidesPerView)}%)` }}
|
|
308
|
+
aria-live="polite"
|
|
309
|
+
>
|
|
310
|
+
{items.map((item, i) => (
|
|
311
|
+
<div
|
|
312
|
+
key={i}
|
|
313
|
+
className="w-full shrink-0"
|
|
314
|
+
style={{ width: `${100 / slidesPerView}%` }}
|
|
315
|
+
aria-roledescription="slide"
|
|
316
|
+
aria-label={`Slide ${i + 1} of ${total}`}
|
|
317
|
+
>
|
|
318
|
+
{item}
|
|
319
|
+
</div>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{showArrows && (
|
|
324
|
+
<>
|
|
325
|
+
<button
|
|
326
|
+
type="button"
|
|
327
|
+
onClick={prev}
|
|
328
|
+
disabled={!loop && current === 0}
|
|
329
|
+
aria-label="Previous slide"
|
|
330
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-background/80 border border-border shadow-sm hover:bg-background transition-colors disabled:opacity-40"
|
|
331
|
+
>
|
|
332
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
333
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
334
|
+
</svg>
|
|
335
|
+
</button>
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
onClick={next}
|
|
339
|
+
disabled={!loop && current === total - 1}
|
|
340
|
+
aria-label="Next slide"
|
|
341
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-background/80 border border-border shadow-sm hover:bg-background transition-colors disabled:opacity-40"
|
|
342
|
+
>
|
|
343
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
344
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
345
|
+
</svg>
|
|
346
|
+
</button>
|
|
347
|
+
</>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{showDots && (
|
|
351
|
+
<div role="tablist" aria-label="Slide navigation" className="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-1.5">
|
|
352
|
+
{items.map((_, i) => (
|
|
353
|
+
<button
|
|
354
|
+
key={i}
|
|
355
|
+
type="button"
|
|
356
|
+
role="tab"
|
|
357
|
+
aria-selected={i === current}
|
|
358
|
+
aria-label={`Go to slide ${i + 1}`}
|
|
359
|
+
onClick={() => setCurrent(i)}
|
|
360
|
+
className={cn(
|
|
361
|
+
"h-1.5 rounded-full transition-all duration-200",
|
|
362
|
+
i === current ? "w-4 bg-primary" : "w-1.5 bg-primary/40 hover:bg-primary/70"
|
|
363
|
+
)}
|
|
364
|
+
/>
|
|
365
|
+
))}
|
|
366
|
+
</div>
|
|
367
|
+
)}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
Carousel.displayName = "Carousel";
|
|
373
|
+
|
|
374
|
+
// ─── Gallery ──────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
export interface GalleryImage {
|
|
377
|
+
src: string;
|
|
378
|
+
alt?: string;
|
|
379
|
+
caption?: string;
|
|
380
|
+
width?: number;
|
|
381
|
+
height?: number;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface GalleryProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
385
|
+
images: GalleryImage[];
|
|
386
|
+
columns?: 2 | 3 | 4 | 5;
|
|
387
|
+
gap?: number;
|
|
388
|
+
onImageClick?: (image: GalleryImage, index: number) => void;
|
|
389
|
+
rounded?: "none" | "sm" | "md" | "lg";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const Gallery = React.forwardRef<HTMLDivElement, GalleryProps>(
|
|
393
|
+
({ className, images, columns = 3, gap = 2, onImageClick, rounded = "md", ...props }, ref) => (
|
|
394
|
+
<div
|
|
395
|
+
ref={ref}
|
|
396
|
+
className={cn("atlas-gallery", className)}
|
|
397
|
+
style={{
|
|
398
|
+
display: "grid",
|
|
399
|
+
gridTemplateColumns: `repeat(${columns}, 1fr)`,
|
|
400
|
+
gap: `${gap * 4}px`,
|
|
401
|
+
}}
|
|
402
|
+
role="list"
|
|
403
|
+
aria-label="Image gallery"
|
|
404
|
+
{...props}
|
|
405
|
+
>
|
|
406
|
+
{images.map((image, i) => (
|
|
407
|
+
<div
|
|
408
|
+
key={i}
|
|
409
|
+
role="listitem"
|
|
410
|
+
className={cn(
|
|
411
|
+
"overflow-hidden",
|
|
412
|
+
roundedMap[rounded],
|
|
413
|
+
onImageClick && "cursor-pointer group",
|
|
414
|
+
)}
|
|
415
|
+
onClick={() => onImageClick?.(image, i)}
|
|
416
|
+
onKeyDown={(e) => e.key === "Enter" && onImageClick?.(image, i)}
|
|
417
|
+
tabIndex={onImageClick ? 0 : undefined}
|
|
418
|
+
aria-label={image.alt ?? `Image ${i + 1}`}
|
|
419
|
+
>
|
|
420
|
+
<img
|
|
421
|
+
src={image.src}
|
|
422
|
+
alt={image.alt ?? ""}
|
|
423
|
+
loading="lazy"
|
|
424
|
+
className={cn(
|
|
425
|
+
"w-full h-full object-cover aspect-square",
|
|
426
|
+
"transition-transform duration-300",
|
|
427
|
+
onImageClick && "group-hover:scale-105"
|
|
428
|
+
)}
|
|
429
|
+
/>
|
|
430
|
+
</div>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
)
|
|
434
|
+
);
|
|
435
|
+
Gallery.displayName = "Gallery";
|
|
436
|
+
|
|
437
|
+
export { Image, VideoPlayer, AudioPlayer, Carousel, Gallery };
|