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.
Files changed (41) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/LICENSE +21 -0
  3. package/README.md +253 -0
  4. package/dist/cli/index.js +511 -0
  5. package/dist/index.d.mts +1317 -0
  6. package/dist/index.d.ts +1317 -0
  7. package/dist/index.js +5373 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/index.mjs +5130 -0
  10. package/dist/index.mjs.map +1 -0
  11. package/dist/provider.d.mts +15 -0
  12. package/dist/provider.d.ts +15 -0
  13. package/dist/provider.js +1197 -0
  14. package/dist/provider.js.map +1 -0
  15. package/dist/provider.mjs +1161 -0
  16. package/dist/provider.mjs.map +1 -0
  17. package/dist/tailwind.d.ts +25 -0
  18. package/dist/tailwind.js +129 -0
  19. package/package.json +138 -0
  20. package/src/cli/index.ts +303 -0
  21. package/src/cli/registry.ts +139 -0
  22. package/src/components/advanced-forms/index.tsx +975 -0
  23. package/src/components/basic/Button.tsx +135 -0
  24. package/src/components/basic/IconButton.tsx +69 -0
  25. package/src/components/basic/index.tsx +446 -0
  26. package/src/components/data-display/index.tsx +1158 -0
  27. package/src/components/feedback/index.tsx +1051 -0
  28. package/src/components/forms/index.tsx +476 -0
  29. package/src/components/layout/index.tsx +296 -0
  30. package/src/components/media/index.tsx +437 -0
  31. package/src/components/navigation/index.tsx +484 -0
  32. package/src/components/overlay/index.tsx +473 -0
  33. package/src/components/utility/index.tsx +566 -0
  34. package/src/hooks/index.ts +602 -0
  35. package/src/hooks/use-toast.tsx +74 -0
  36. package/src/index.ts +396 -0
  37. package/src/provider.tsx +54 -0
  38. package/src/styles/atlas.css +252 -0
  39. package/src/tailwind.ts +124 -0
  40. package/src/types/index.ts +95 -0
  41. 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 };