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,1051 @@
1
+ import * as React from "react";
2
+ import * as ToastPrimitive from "@radix-ui/react-toast";
3
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ // ─── Alert ─────────────────────────────────────────────────────────────────
8
+
9
+ const alertVariants = cva(
10
+ "atlas-alert relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:pl-7",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "bg-background text-foreground",
15
+ info: "border-info/30 bg-info/10 text-info-foreground [&>svg]:text-info",
16
+ success: "border-success/30 bg-success/10 text-success-foreground [&>svg]:text-success",
17
+ warning: "border-warning/30 bg-warning/10 text-warning-foreground [&>svg]:text-warning",
18
+ danger: "border-destructive/30 bg-destructive/10 text-destructive [&>svg]:text-destructive",
19
+ },
20
+ },
21
+ defaultVariants: { variant: "default" },
22
+ }
23
+ );
24
+
25
+ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof alertVariants> {
26
+ icon?: React.ReactNode;
27
+ closable?: boolean;
28
+ onClose?: () => void;
29
+ }
30
+
31
+ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
32
+ ({ className, variant, icon, closable, onClose, children, ...props }, ref) => (
33
+ <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props}>
34
+ {icon}
35
+ <div className="flex items-start justify-between gap-2">
36
+ <div className="flex-1">{children}</div>
37
+ {closable && (
38
+ <button
39
+ type="button"
40
+ onClick={onClose}
41
+ className="shrink-0 rounded-md p-0.5 text-current/50 hover:text-current transition-colors"
42
+ aria-label="Dismiss alert"
43
+ >
44
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
46
+ </svg>
47
+ </button>
48
+ )}
49
+ </div>
50
+ </div>
51
+ )
52
+ );
53
+ Alert.displayName = "Alert";
54
+
55
+ const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
56
+ ({ className, ...props }, ref) => (
57
+ <h5 ref={ref} className={cn("mb-1 font-semibold leading-tight tracking-tight", className)} {...props} />
58
+ )
59
+ );
60
+ AlertTitle.displayName = "AlertTitle";
61
+
62
+ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
63
+ ({ className, ...props }, ref) => (
64
+ <p ref={ref} className={cn("text-sm leading-relaxed", className)} {...props} />
65
+ )
66
+ );
67
+ AlertDescription.displayName = "AlertDescription";
68
+
69
+ // ─── Toast ─────────────────────────────────────────────────────────────────
70
+
71
+ const ToastProvider = ToastPrimitive.Provider;
72
+
73
+ const ToastViewport = React.forwardRef<
74
+ React.ElementRef<typeof ToastPrimitive.Viewport>,
75
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Viewport>
76
+ >(({ className, ...props }, ref) => (
77
+ <ToastPrimitive.Viewport
78
+ ref={ref}
79
+ className={cn(
80
+ "atlas-toast-viewport fixed bottom-4 right-4 z-[100] flex flex-col gap-2 w-full max-w-sm p-4",
81
+ className
82
+ )}
83
+ {...props}
84
+ />
85
+ ));
86
+ ToastViewport.displayName = ToastPrimitive.Viewport.displayName;
87
+
88
+ const toastVariants = cva(
89
+ [
90
+ "group pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden",
91
+ "rounded-lg border p-4 shadow-lg transition-all",
92
+ "data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]",
93
+ "data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none",
94
+ "data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom-full data-[state=open]:fade-in",
95
+ "data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-right-full",
96
+ ],
97
+ {
98
+ variants: {
99
+ variant: {
100
+ default: "bg-background border-border",
101
+ success: "bg-success/10 border-success/20 text-success",
102
+ warning: "bg-warning/10 border-warning/20 text-warning",
103
+ danger: "bg-destructive/10 border-destructive/20 text-destructive",
104
+ info: "bg-info/10 border-info/20 text-info",
105
+ },
106
+ },
107
+ defaultVariants: { variant: "default" },
108
+ }
109
+ );
110
+
111
+ const Toast = React.forwardRef<
112
+ React.ElementRef<typeof ToastPrimitive.Root>,
113
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Root> & VariantProps<typeof toastVariants>
114
+ >(({ className, variant, ...props }, ref) => (
115
+ <ToastPrimitive.Root ref={ref} className={cn(toastVariants({ variant }), className)} {...props} />
116
+ ));
117
+ Toast.displayName = ToastPrimitive.Root.displayName;
118
+
119
+ const ToastTitle = React.forwardRef<
120
+ React.ElementRef<typeof ToastPrimitive.Title>,
121
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Title>
122
+ >(({ className, ...props }, ref) => (
123
+ <ToastPrimitive.Title ref={ref} className={cn("text-sm font-semibold", className)} {...props} />
124
+ ));
125
+ ToastTitle.displayName = ToastPrimitive.Title.displayName;
126
+
127
+ const ToastDescription = React.forwardRef<
128
+ React.ElementRef<typeof ToastPrimitive.Description>,
129
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Description>
130
+ >(({ className, ...props }, ref) => (
131
+ <ToastPrimitive.Description ref={ref} className={cn("text-sm opacity-80", className)} {...props} />
132
+ ));
133
+ ToastDescription.displayName = ToastPrimitive.Description.displayName;
134
+
135
+ const ToastClose = React.forwardRef<
136
+ React.ElementRef<typeof ToastPrimitive.Close>,
137
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Close>
138
+ >(({ className, ...props }, ref) => (
139
+ <ToastPrimitive.Close
140
+ ref={ref}
141
+ toast-close=""
142
+ className={cn(
143
+ "ml-auto shrink-0 rounded-md p-0.5 opacity-50 hover:opacity-100 transition-opacity",
144
+ className
145
+ )}
146
+ aria-label="Close"
147
+ {...props}
148
+ >
149
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
150
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
151
+ </svg>
152
+ </ToastPrimitive.Close>
153
+ ));
154
+ ToastClose.displayName = ToastPrimitive.Close.displayName;
155
+
156
+ const ToastAction = React.forwardRef<
157
+ React.ElementRef<typeof ToastPrimitive.Action>,
158
+ React.ComponentPropsWithoutRef<typeof ToastPrimitive.Action>
159
+ >(({ className, ...props }, ref) => (
160
+ <ToastPrimitive.Action
161
+ ref={ref}
162
+ className={cn(
163
+ "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium",
164
+ "transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring",
165
+ "disabled:pointer-events-none disabled:opacity-50",
166
+ className
167
+ )}
168
+ {...props}
169
+ />
170
+ ));
171
+ ToastAction.displayName = ToastPrimitive.Action.displayName;
172
+
173
+ // ─── Snackbar ─────────────────────────────────────────────────────────────
174
+
175
+ export interface SnackbarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
176
+ open?: boolean;
177
+ message: React.ReactNode;
178
+ action?: React.ReactNode;
179
+ variant?: "default" | "success" | "warning" | "danger";
180
+ position?: "bottom-center" | "bottom-left" | "bottom-right" | "top-center";
181
+ }
182
+
183
+ const Snackbar = React.forwardRef<HTMLDivElement, SnackbarProps>(
184
+ ({ className, open, message, action, variant = "default", position = "bottom-center", ...props }, ref) => {
185
+ if (!open) return null;
186
+
187
+ return (
188
+ <div
189
+ ref={ref}
190
+ role="status"
191
+ aria-live="polite"
192
+ className={cn(
193
+ "atlas-snackbar fixed z-50 flex items-center gap-4 rounded-lg px-4 py-3 shadow-lg",
194
+ "min-w-[280px] max-w-[480px]",
195
+ position === "bottom-center" && "bottom-4 left-1/2 -translate-x-1/2",
196
+ position === "bottom-left" && "bottom-4 left-4",
197
+ position === "bottom-right" && "bottom-4 right-4",
198
+ position === "top-center" && "top-4 left-1/2 -translate-x-1/2",
199
+ variant === "default" && "bg-foreground text-background",
200
+ variant === "success" && "bg-success text-success-foreground",
201
+ variant === "warning" && "bg-warning text-warning-foreground",
202
+ variant === "danger" && "bg-destructive text-destructive-foreground",
203
+ className
204
+ )}
205
+ {...props}
206
+ >
207
+ <p className="flex-1 text-sm font-medium">{message}</p>
208
+ {action && <div className="shrink-0">{action}</div>}
209
+ </div>
210
+ );
211
+ }
212
+ );
213
+ Snackbar.displayName = "Snackbar";
214
+
215
+ // ─── Progress ─────────────────────────────────────────────────────────────
216
+
217
+ export interface ProgressProps extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
218
+ label?: string;
219
+ showValue?: boolean;
220
+ size?: "sm" | "md" | "lg";
221
+ color?: "default" | "success" | "warning" | "danger";
222
+ }
223
+
224
+ const Progress = React.forwardRef<React.ElementRef<typeof ProgressPrimitive.Root>, ProgressProps>(
225
+ ({ className, value, label, showValue, size = "md", color = "default", ...props }, ref) => (
226
+ <div className="atlas-progress w-full">
227
+ {(label || showValue) && (
228
+ <div className="flex justify-between items-center mb-1.5">
229
+ {label && <span className="text-sm font-medium">{label}</span>}
230
+ {showValue && <span className="text-sm text-muted-foreground">{value ?? 0}%</span>}
231
+ </div>
232
+ )}
233
+ <ProgressPrimitive.Root
234
+ ref={ref}
235
+ className={cn(
236
+ "relative overflow-hidden rounded-full bg-secondary",
237
+ size === "sm" && "h-1.5",
238
+ size === "md" && "h-2.5",
239
+ size === "lg" && "h-4",
240
+ className
241
+ )}
242
+ {...props}
243
+ >
244
+ <ProgressPrimitive.Indicator
245
+ className={cn(
246
+ "h-full w-full flex-1 transition-all duration-500 ease-in-out",
247
+ color === "default" && "bg-primary",
248
+ color === "success" && "bg-success",
249
+ color === "warning" && "bg-warning",
250
+ color === "danger" && "bg-destructive",
251
+ )}
252
+ style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
253
+ />
254
+ </ProgressPrimitive.Root>
255
+ </div>
256
+ )
257
+ );
258
+ Progress.displayName = "Progress";
259
+
260
+ // ─── CircularProgress ─────────────────────────────────────────────────────
261
+
262
+ export interface CircularProgressProps extends React.SVGAttributes<SVGElement> {
263
+ value?: number;
264
+ size?: number;
265
+ thickness?: number;
266
+ showValue?: boolean;
267
+ label?: string;
268
+ color?: "default" | "success" | "warning" | "danger";
269
+ indeterminate?: boolean;
270
+ }
271
+
272
+ const CircularProgress = ({
273
+ value = 0,
274
+ size = 48,
275
+ thickness = 4,
276
+ showValue,
277
+ label,
278
+ color = "default",
279
+ indeterminate,
280
+ className,
281
+ ...props
282
+ }: CircularProgressProps) => {
283
+ const radius = (size - thickness) / 2;
284
+ const circumference = 2 * Math.PI * radius;
285
+ const offset = circumference - (value / 100) * circumference;
286
+
287
+ const colorMap = {
288
+ default: "stroke-primary",
289
+ success: "stroke-success",
290
+ warning: "stroke-warning",
291
+ danger: "stroke-destructive",
292
+ };
293
+
294
+ return (
295
+ <div className={cn("atlas-circular-progress relative inline-flex items-center justify-center", className)}>
296
+ <svg
297
+ width={size}
298
+ height={size}
299
+ viewBox={`0 0 ${size} ${size}`}
300
+ fill="none"
301
+ className={indeterminate ? "animate-spin" : ""}
302
+ role="progressbar"
303
+ aria-valuenow={indeterminate ? undefined : value}
304
+ aria-valuemin={0}
305
+ aria-valuemax={100}
306
+ aria-label={label}
307
+ {...props}
308
+ >
309
+ <circle
310
+ cx={size / 2}
311
+ cy={size / 2}
312
+ r={radius}
313
+ strokeWidth={thickness}
314
+ className="stroke-secondary"
315
+ />
316
+ <circle
317
+ cx={size / 2}
318
+ cy={size / 2}
319
+ r={radius}
320
+ strokeWidth={thickness}
321
+ strokeDasharray={circumference}
322
+ strokeDashoffset={indeterminate ? circumference * 0.75 : offset}
323
+ strokeLinecap="round"
324
+ className={cn("transition-all duration-500", colorMap[color])}
325
+ transform={`rotate(-90 ${size / 2} ${size / 2})`}
326
+ />
327
+ </svg>
328
+ {showValue && !indeterminate && (
329
+ <span className="absolute text-xs font-semibold">{value}%</span>
330
+ )}
331
+ </div>
332
+ );
333
+ };
334
+ CircularProgress.displayName = "CircularProgress";
335
+
336
+ // ─── Skeleton ─────────────────────────────────────────────────────────────
337
+
338
+ export interface SkeletonProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
339
+ variant?: "text" | "rect" | "circle";
340
+ width?: string | number;
341
+ height?: string | number;
342
+ lines?: number;
343
+ }
344
+
345
+ const Skeleton = React.forwardRef<HTMLDivElement, SkeletonProps>(
346
+ ({ className, variant = "rect", width, height, lines = 1, style, ...props }, ref) => {
347
+ if (variant === "text" && lines > 1) {
348
+ return (
349
+ <div className={cn("atlas-skeleton space-y-2", className)} ref={ref} {...props}>
350
+ {Array.from({ length: lines }).map((_, i) => (
351
+ <div
352
+ key={i}
353
+ className="h-4 animate-pulse rounded bg-muted"
354
+ style={{ width: i === lines - 1 ? "75%" : "100%" }}
355
+ />
356
+ ))}
357
+ </div>
358
+ );
359
+ }
360
+
361
+ return (
362
+ <div
363
+ ref={ref}
364
+ className={cn(
365
+ "atlas-skeleton animate-pulse bg-muted",
366
+ variant === "circle" ? "rounded-full" : "rounded-md",
367
+ variant === "text" && "h-4",
368
+ className
369
+ )}
370
+ style={{ width, height, ...style }}
371
+ aria-hidden="true"
372
+ {...props}
373
+ />
374
+ );
375
+ }
376
+ );
377
+ Skeleton.displayName = "Skeleton";
378
+
379
+ // ─── LoadingSpinner ───────────────────────────────────────────────────────
380
+
381
+ export interface LoadingSpinnerProps extends React.SVGAttributes<SVGElement> {
382
+ size?: "xs" | "sm" | "md" | "lg" | "xl";
383
+ label?: string;
384
+ }
385
+
386
+ const spinnerSizes = { xs: 12, sm: 16, md: 24, lg: 32, xl: 48 };
387
+
388
+ const LoadingSpinner = ({ size = "md", label = "Loading", className, ...props }: LoadingSpinnerProps) => (
389
+ <svg
390
+ width={spinnerSizes[size]}
391
+ height={spinnerSizes[size]}
392
+ viewBox="0 0 24 24"
393
+ fill="none"
394
+ className={cn("atlas-loading-spinner animate-spin text-primary", className)}
395
+ role="status"
396
+ aria-label={label}
397
+ {...props}
398
+ >
399
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
400
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
401
+ </svg>
402
+ );
403
+ LoadingSpinner.displayName = "LoadingSpinner";
404
+
405
+ // ─── EmptyState ───────────────────────────────────────────────────────────
406
+
407
+ export interface EmptyStateProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
408
+ icon?: React.ReactNode;
409
+ title: string;
410
+ description?: string;
411
+ action?: React.ReactNode;
412
+ }
413
+
414
+ const EmptyState = React.forwardRef<HTMLDivElement, EmptyStateProps>(
415
+ ({ className, icon, title, description, action, ...props }, ref) => (
416
+ <div
417
+ ref={ref}
418
+ className={cn(
419
+ "atlas-empty-state flex flex-col items-center justify-center gap-3 text-center py-16 px-6",
420
+ className
421
+ )}
422
+ {...props}
423
+ >
424
+ {icon && (
425
+ <div className="rounded-full bg-muted p-4 text-muted-foreground [&>svg]:h-8 [&>svg]:w-8">
426
+ {icon}
427
+ </div>
428
+ )}
429
+ <div className="max-w-xs">
430
+ <h3 className="text-base font-semibold">{title}</h3>
431
+ {description && <p className="mt-1 text-sm text-muted-foreground">{description}</p>}
432
+ </div>
433
+ {action && <div className="mt-2">{action}</div>}
434
+ </div>
435
+ )
436
+ );
437
+ EmptyState.displayName = "EmptyState";
438
+
439
+ // ─── StatusIndicator ──────────────────────────────────────────────────────
440
+
441
+ export interface StatusIndicatorProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color" | "size"> {
442
+ status: "online" | "offline" | "busy" | "away" | "idle";
443
+ label?: string;
444
+ pulse?: boolean;
445
+ size?: "sm" | "md" | "lg";
446
+ }
447
+
448
+ const statusColors = {
449
+ online: "bg-success",
450
+ offline: "bg-muted-foreground",
451
+ busy: "bg-destructive",
452
+ away: "bg-warning",
453
+ idle: "bg-warning/60",
454
+ };
455
+
456
+ const StatusIndicator = React.forwardRef<HTMLSpanElement, StatusIndicatorProps>(
457
+ ({ className, status, label, pulse, size = "md", ...props }, ref) => (
458
+ <span
459
+ ref={ref}
460
+ className={cn("atlas-status-indicator inline-flex items-center gap-1.5", className)}
461
+ {...props}
462
+ >
463
+ <span className="relative inline-flex">
464
+ <span className={cn(
465
+ "rounded-full",
466
+ statusColors[status],
467
+ size === "sm" && "h-1.5 w-1.5",
468
+ size === "md" && "h-2.5 w-2.5",
469
+ size === "lg" && "h-3.5 w-3.5",
470
+ )} />
471
+ {pulse && status === "online" && (
472
+ <span className={cn(
473
+ "absolute inline-flex rounded-full animate-ping opacity-75",
474
+ statusColors[status],
475
+ size === "sm" && "h-1.5 w-1.5",
476
+ size === "md" && "h-2.5 w-2.5",
477
+ size === "lg" && "h-3.5 w-3.5",
478
+ )} />
479
+ )}
480
+ </span>
481
+ {label && <span className="text-sm capitalize">{label ?? status}</span>}
482
+ </span>
483
+ )
484
+ );
485
+ StatusIndicator.displayName = "StatusIndicator";
486
+
487
+ // ─── Notification ─────────────────────────────────────────────────────────
488
+
489
+ export interface NotificationProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
490
+ title: React.ReactNode;
491
+ description?: React.ReactNode;
492
+ avatar?: React.ReactNode;
493
+ icon?: React.ReactNode;
494
+ time?: React.ReactNode;
495
+ unread?: boolean;
496
+ onClose?: () => void;
497
+ }
498
+
499
+ const Notification = React.forwardRef<HTMLDivElement, NotificationProps>(
500
+ ({ className, title, description, avatar, icon, time, unread, onClose, ...props }, ref) => (
501
+ <div
502
+ ref={ref}
503
+ role="listitem"
504
+ className={cn(
505
+ "atlas-notification flex gap-3 p-4 transition-colors",
506
+ unread && "bg-primary/5",
507
+ className
508
+ )}
509
+ {...props}
510
+ >
511
+ <div className="shrink-0">
512
+ {avatar ?? (
513
+ <div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:h-4 [&>svg]:w-4">
514
+ {icon}
515
+ </div>
516
+ )}
517
+ </div>
518
+ <div className="flex-1 min-w-0">
519
+ <div className="flex items-start justify-between gap-2">
520
+ <p className={cn("text-sm font-medium leading-snug", unread && "font-semibold")}>{title}</p>
521
+ {unread && <span className="mt-1 h-2 w-2 shrink-0 rounded-full bg-primary" aria-label="Unread" />}
522
+ </div>
523
+ {description && <p className="mt-0.5 text-sm text-muted-foreground line-clamp-2">{description}</p>}
524
+ {time && <p className="mt-1 text-xs text-muted-foreground">{time}</p>}
525
+ </div>
526
+ {onClose && (
527
+ <button
528
+ type="button"
529
+ onClick={onClose}
530
+ className="shrink-0 self-start rounded p-0.5 text-muted-foreground hover:text-foreground transition-colors"
531
+ aria-label="Dismiss notification"
532
+ >
533
+ <svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
534
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
535
+ </svg>
536
+ </button>
537
+ )}
538
+ </div>
539
+ )
540
+ );
541
+ Notification.displayName = "Notification";
542
+
543
+
544
+ // ═══════════════════════════════════════════════════════════════
545
+ // New in v0.1.2
546
+ // ═══════════════════════════════════════════════════════════════
547
+
548
+
549
+ // ─── BannerAlert ──────────────────────────────────────────────────────────
550
+
551
+ export interface BannerAlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
552
+ title?: React.ReactNode;
553
+ description?: React.ReactNode;
554
+ variant?: "info" | "success" | "warning" | "danger";
555
+ dismissible?: boolean;
556
+ onDismiss?: () => void;
557
+ action?: React.ReactNode;
558
+ icon?: React.ReactNode;
559
+ }
560
+
561
+ const bannerVariants = {
562
+ info: "bg-info/10 border-info/30 text-info-foreground [&_.atlas-banner-icon]:text-info",
563
+ success: "bg-success/10 border-success/30 text-success-foreground [&_.atlas-banner-icon]:text-success",
564
+ warning: "bg-warning/10 border-warning/30 text-warning-foreground [&_.atlas-banner-icon]:text-warning",
565
+ danger: "bg-destructive/10 border-destructive/30 text-destructive [&_.atlas-banner-icon]:text-destructive",
566
+ };
567
+
568
+ const defaultBannerIcons: Record<string, React.ReactNode> = {
569
+ info: (
570
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
571
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
572
+ </svg>
573
+ ),
574
+ success: (
575
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
576
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
577
+ </svg>
578
+ ),
579
+ warning: (
580
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
581
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
582
+ </svg>
583
+ ),
584
+ danger: (
585
+ <svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
586
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
587
+ </svg>
588
+ ),
589
+ };
590
+
591
+ const BannerAlert = React.forwardRef<HTMLDivElement, BannerAlertProps>(
592
+ ({ className, title, description, variant = "info", dismissible, onDismiss, action, icon, ...props }, ref) => (
593
+ <div
594
+ ref={ref}
595
+ role="alert"
596
+ className={cn(
597
+ "atlas-banner-alert w-full border-y px-4 py-3",
598
+ bannerVariants[variant],
599
+ className
600
+ )}
601
+ {...props}
602
+ >
603
+ <div className="flex items-start gap-3 max-w-screen-xl mx-auto">
604
+ <span className="atlas-banner-icon shrink-0 mt-0.5">
605
+ {icon ?? defaultBannerIcons[variant]}
606
+ </span>
607
+ <div className="flex-1 min-w-0">
608
+ {title && <p className="font-semibold text-sm">{title}</p>}
609
+ {description && <p className="text-sm mt-0.5 opacity-90">{description}</p>}
610
+ </div>
611
+ <div className="flex items-center gap-2 shrink-0">
612
+ {action}
613
+ {dismissible && (
614
+ <button
615
+ type="button"
616
+ onClick={onDismiss}
617
+ aria-label="Dismiss"
618
+ className="rounded-md p-1 opacity-60 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2 focus:ring-current"
619
+ >
620
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
621
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
622
+ </svg>
623
+ </button>
624
+ )}
625
+ </div>
626
+ </div>
627
+ </div>
628
+ )
629
+ );
630
+ BannerAlert.displayName = "BannerAlert";
631
+
632
+ // ─── ConfirmDialog ────────────────────────────────────────────────────────
633
+
634
+ export interface ConfirmDialogProps {
635
+ open?: boolean;
636
+ onOpenChange?: (open: boolean) => void;
637
+ title?: React.ReactNode;
638
+ description?: React.ReactNode;
639
+ confirmLabel?: string;
640
+ cancelLabel?: string;
641
+ variant?: "default" | "danger";
642
+ onConfirm?: () => void | Promise<void>;
643
+ onCancel?: () => void;
644
+ loading?: boolean;
645
+ }
646
+
647
+ const ConfirmDialog = ({
648
+ open,
649
+ onOpenChange,
650
+ title = "Are you sure?",
651
+ description = "This action cannot be undone.",
652
+ confirmLabel = "Confirm",
653
+ cancelLabel = "Cancel",
654
+ variant = "default",
655
+ onConfirm,
656
+ onCancel,
657
+ loading,
658
+ }: ConfirmDialogProps) => {
659
+ const [pending, setPending] = React.useState(false);
660
+
661
+ if (!open) return null;
662
+
663
+ const handleConfirm = async () => {
664
+ setPending(true);
665
+ try {
666
+ await onConfirm?.();
667
+ onOpenChange?.(false);
668
+ } finally {
669
+ setPending(false);
670
+ }
671
+ };
672
+
673
+ const handleCancel = () => {
674
+ onCancel?.();
675
+ onOpenChange?.(false);
676
+ };
677
+
678
+ const isBusy = pending || loading;
679
+
680
+ return (
681
+ <div className="atlas-confirm-dialog fixed inset-0 z-50 flex items-center justify-center p-4">
682
+ <div
683
+ className="fixed inset-0 bg-black/50 animate-in fade-in-0"
684
+ onClick={handleCancel}
685
+ aria-hidden="true"
686
+ />
687
+ <div
688
+ role="alertdialog"
689
+ aria-modal="true"
690
+ aria-labelledby="confirm-title"
691
+ aria-describedby="confirm-description"
692
+ className={cn(
693
+ "relative z-10 w-full max-w-sm rounded-xl border border-border bg-background p-6 shadow-xl",
694
+ "animate-in fade-in-0 zoom-in-95"
695
+ )}
696
+ >
697
+ <div className="flex items-start gap-4">
698
+ <div className={cn(
699
+ "flex h-10 w-10 shrink-0 items-center justify-center rounded-full",
700
+ variant === "danger" ? "bg-destructive/10" : "bg-primary/10"
701
+ )}>
702
+ {variant === "danger" ? (
703
+ <svg className="h-5 w-5 text-destructive" fill="none" stroke="currentColor" viewBox="0 0 24 24">
704
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
705
+ </svg>
706
+ ) : (
707
+ <svg className="h-5 w-5 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
708
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
709
+ </svg>
710
+ )}
711
+ </div>
712
+ <div>
713
+ <h2 id="confirm-title" className="text-base font-semibold">{title}</h2>
714
+ <p id="confirm-description" className="mt-1 text-sm text-muted-foreground">{description}</p>
715
+ </div>
716
+ </div>
717
+ <div className="mt-6 flex justify-end gap-2">
718
+ <button
719
+ type="button"
720
+ onClick={handleCancel}
721
+ disabled={isBusy}
722
+ className="inline-flex h-9 items-center justify-center rounded-md border border-border bg-background px-4 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50"
723
+ >
724
+ {cancelLabel}
725
+ </button>
726
+ <button
727
+ type="button"
728
+ onClick={handleConfirm}
729
+ disabled={isBusy}
730
+ className={cn(
731
+ "inline-flex h-9 items-center justify-center gap-2 rounded-md px-4 text-sm font-medium transition-colors disabled:opacity-50",
732
+ variant === "danger"
733
+ ? "bg-destructive text-destructive-foreground hover:bg-destructive/90"
734
+ : "bg-primary text-primary-foreground hover:bg-primary/90"
735
+ )}
736
+ >
737
+ {isBusy && (
738
+ <svg className="h-3.5 w-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
739
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
740
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
741
+ </svg>
742
+ )}
743
+ {confirmLabel}
744
+ </button>
745
+ </div>
746
+ </div>
747
+ </div>
748
+ );
749
+ };
750
+ ConfirmDialog.displayName = "ConfirmDialog";
751
+
752
+ // ─── FloatingActionButton ─────────────────────────────────────────────────
753
+
754
+ export interface FABAction {
755
+ icon: React.ReactNode;
756
+ label: string;
757
+ onClick: () => void;
758
+ disabled?: boolean;
759
+ }
760
+
761
+ export interface FloatingActionButtonProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onClick" | "size"> {
762
+ icon?: React.ReactNode;
763
+ label?: string;
764
+ actions?: FABAction[];
765
+ position?: "bottom-right" | "bottom-left" | "bottom-center";
766
+ size?: "sm" | "md" | "lg";
767
+ onClick?: () => void;
768
+ }
769
+
770
+ const positionMap = {
771
+ "bottom-right": "fixed bottom-6 right-6 z-40",
772
+ "bottom-left": "fixed bottom-6 left-6 z-40",
773
+ "bottom-center": "fixed bottom-6 left-1/2 -translate-x-1/2 z-40",
774
+ };
775
+
776
+ const fabSizes = {
777
+ sm: "h-12 w-12 [&>svg]:h-5 [&>svg]:w-5",
778
+ md: "h-14 w-14 [&>svg]:h-6 [&>svg]:w-6",
779
+ lg: "h-16 w-16 [&>svg]:h-7 [&>svg]:w-7",
780
+ };
781
+
782
+ const FloatingActionButton = React.forwardRef<HTMLDivElement, FloatingActionButtonProps>(
783
+ ({ className, icon, label = "Open actions", actions = [], position = "bottom-right", size = "md", onClick, ...props }, ref) => {
784
+ const [open, setOpen] = React.useState(false);
785
+ const hasActions = actions.length > 0;
786
+
787
+ return (
788
+ <div
789
+ ref={ref}
790
+ className={cn(positionMap[position], "flex flex-col-reverse items-center gap-3", className)}
791
+ {...props}
792
+ >
793
+ {hasActions && open && (
794
+ <div className="flex flex-col-reverse gap-2">
795
+ {actions.map((action, i) => (
796
+ <div key={i} className="flex items-center gap-2">
797
+ <span className="rounded-md bg-foreground/90 px-2 py-1 text-xs text-background font-medium shadow whitespace-nowrap">
798
+ {action.label}
799
+ </span>
800
+ <button
801
+ type="button"
802
+ disabled={action.disabled}
803
+ onClick={() => { action.onClick(); setOpen(false); }}
804
+ aria-label={action.label}
805
+ className={cn(
806
+ "flex h-10 w-10 items-center justify-center rounded-full bg-background border border-border",
807
+ "shadow-md text-foreground hover:bg-accent transition-all",
808
+ "[&>svg]:h-4 [&>svg]:w-4",
809
+ "disabled:opacity-50 disabled:pointer-events-none"
810
+ )}
811
+ >
812
+ {action.icon}
813
+ </button>
814
+ </div>
815
+ ))}
816
+ </div>
817
+ )}
818
+ <button
819
+ type="button"
820
+ aria-label={label}
821
+ aria-expanded={hasActions ? open : undefined}
822
+ onClick={hasActions ? () => setOpen(!open) : onClick}
823
+ className={cn(
824
+ "flex items-center justify-center rounded-full",
825
+ "bg-primary text-primary-foreground shadow-lg",
826
+ "hover:bg-primary/90 active:scale-95 transition-all",
827
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
828
+ fabSizes[size]
829
+ )}
830
+ >
831
+ {icon ?? (
832
+ <svg
833
+ className={cn("transition-transform duration-200", hasActions && open && "rotate-45")}
834
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
835
+ style={{ width: "1.5rem", height: "1.5rem" }}
836
+ >
837
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
838
+ </svg>
839
+ )}
840
+ </button>
841
+ </div>
842
+ );
843
+ }
844
+ );
845
+ FloatingActionButton.displayName = "FloatingActionButton";
846
+
847
+ // ─── RichTooltip ──────────────────────────────────────────────────────────
848
+
849
+ export interface RichTooltipProps {
850
+ children: React.ReactNode;
851
+ title?: React.ReactNode;
852
+ description?: React.ReactNode;
853
+ action?: React.ReactNode;
854
+ side?: "top" | "right" | "bottom" | "left";
855
+ open?: boolean;
856
+ defaultOpen?: boolean;
857
+ delayDuration?: number;
858
+ }
859
+
860
+ const RichTooltip = ({
861
+ children,
862
+ title,
863
+ description,
864
+ action,
865
+ side = "top",
866
+ open: controlledOpen,
867
+ defaultOpen = false,
868
+ delayDuration = 400,
869
+ }: RichTooltipProps) => {
870
+ const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
871
+ const isOpen = controlledOpen ?? internalOpen;
872
+ const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
873
+
874
+ const show = () => {
875
+ timeoutRef.current = setTimeout(() => setInternalOpen(true), delayDuration);
876
+ };
877
+ const hide = () => {
878
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
879
+ setInternalOpen(false);
880
+ };
881
+
882
+ const positionClasses: Record<string, string> = {
883
+ top: "bottom-full left-1/2 -translate-x-1/2 mb-2",
884
+ bottom: "top-full left-1/2 -translate-x-1/2 mt-2",
885
+ left: "right-full top-1/2 -translate-y-1/2 mr-2",
886
+ right: "left-full top-1/2 -translate-y-1/2 ml-2",
887
+ };
888
+
889
+ return (
890
+ <div
891
+ className="atlas-rich-tooltip relative inline-flex"
892
+ onMouseEnter={show}
893
+ onMouseLeave={hide}
894
+ onFocus={show}
895
+ onBlur={hide}
896
+ >
897
+ {children}
898
+ {isOpen && (
899
+ <div
900
+ role="tooltip"
901
+ className={cn(
902
+ "absolute z-50 w-64 rounded-lg border border-border bg-popover p-3 shadow-lg",
903
+ "animate-in fade-in-0 zoom-in-95",
904
+ positionClasses[side]
905
+ )}
906
+ >
907
+ {title && <p className="text-sm font-semibold mb-1">{title}</p>}
908
+ {description && <p className="text-xs text-muted-foreground leading-relaxed">{description}</p>}
909
+ {action && <div className="mt-2 pt-2 border-t border-border">{action}</div>}
910
+ </div>
911
+ )}
912
+ </div>
913
+ );
914
+ };
915
+ RichTooltip.displayName = "RichTooltip";
916
+
917
+ // ─── Tour ─────────────────────────────────────────────────────────────────
918
+
919
+ export interface TourStep {
920
+ target?: string;
921
+ title: string;
922
+ description: React.ReactNode;
923
+ placement?: "top" | "bottom" | "left" | "right";
924
+ }
925
+
926
+ export interface TourProps {
927
+ steps: TourStep[];
928
+ open?: boolean;
929
+ onOpenChange?: (open: boolean) => void;
930
+ onComplete?: () => void;
931
+ currentStep?: number;
932
+ onStepChange?: (step: number) => void;
933
+ }
934
+
935
+ const Tour = ({
936
+ steps,
937
+ open,
938
+ onOpenChange,
939
+ onComplete,
940
+ currentStep: controlledStep,
941
+ onStepChange,
942
+ }: TourProps) => {
943
+ const [internalStep, setInternalStep] = React.useState(0);
944
+ const step = controlledStep ?? internalStep;
945
+ const current = steps[step];
946
+
947
+ if (!open || !current) return null;
948
+
949
+ const goNext = () => {
950
+ if (step < steps.length - 1) {
951
+ const next = step + 1;
952
+ onStepChange?.(next);
953
+ setInternalStep(next);
954
+ } else {
955
+ onComplete?.();
956
+ onOpenChange?.(false);
957
+ }
958
+ };
959
+
960
+ const goPrev = () => {
961
+ if (step > 0) {
962
+ const prev = step - 1;
963
+ onStepChange?.(prev);
964
+ setInternalStep(prev);
965
+ }
966
+ };
967
+
968
+ return (
969
+ <div className="atlas-tour fixed inset-0 z-50 flex items-center justify-center p-4">
970
+ <div
971
+ className="fixed inset-0 bg-black/40 animate-in fade-in-0"
972
+ onClick={() => onOpenChange?.(false)}
973
+ aria-hidden="true"
974
+ />
975
+ <div
976
+ role="dialog"
977
+ aria-modal="true"
978
+ aria-label={`Tour step ${step + 1} of ${steps.length}: ${current.title}`}
979
+ className="relative z-10 w-full max-w-sm rounded-xl border border-border bg-background p-5 shadow-xl animate-in fade-in-0 zoom-in-95"
980
+ >
981
+ <div className="flex items-center justify-between mb-3">
982
+ <span className="text-xs font-medium text-muted-foreground">
983
+ Step {step + 1} of {steps.length}
984
+ </span>
985
+ <button
986
+ type="button"
987
+ onClick={() => onOpenChange?.(false)}
988
+ aria-label="Close tour"
989
+ className="rounded p-0.5 text-muted-foreground hover:text-foreground transition-colors"
990
+ >
991
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
992
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
993
+ </svg>
994
+ </button>
995
+ </div>
996
+
997
+ <h3 className="text-base font-semibold mb-1">{current.title}</h3>
998
+ <div className="text-sm text-muted-foreground">{current.description}</div>
999
+
1000
+ <div className="mt-5 flex items-center justify-between gap-3">
1001
+ <div className="flex gap-1">
1002
+ {steps.map((_, i) => (
1003
+ <span
1004
+ key={i}
1005
+ className={cn(
1006
+ "h-1.5 rounded-full transition-all",
1007
+ i === step ? "w-4 bg-primary" : "w-1.5 bg-muted"
1008
+ )}
1009
+ />
1010
+ ))}
1011
+ </div>
1012
+ <div className="flex gap-2">
1013
+ {step > 0 && (
1014
+ <button
1015
+ type="button"
1016
+ onClick={goPrev}
1017
+ className="h-8 px-3 rounded-md border border-border text-sm hover:bg-accent transition-colors"
1018
+ >
1019
+ Back
1020
+ </button>
1021
+ )}
1022
+ <button
1023
+ type="button"
1024
+ onClick={goNext}
1025
+ className="h-8 px-4 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:bg-primary/90 transition-colors"
1026
+ >
1027
+ {step === steps.length - 1 ? "Done" : "Next"}
1028
+ </button>
1029
+ </div>
1030
+ </div>
1031
+ </div>
1032
+ </div>
1033
+ );
1034
+ };
1035
+ Tour.displayName = "Tour";
1036
+
1037
+
1038
+ export {
1039
+
1040
+ Alert, AlertTitle, AlertDescription,
1041
+ ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction,
1042
+ Snackbar,
1043
+ Progress,
1044
+ CircularProgress,
1045
+ Skeleton,
1046
+ LoadingSpinner,
1047
+ EmptyState,
1048
+ StatusIndicator,
1049
+ Notification,
1050
+ BannerAlert, ConfirmDialog, FloatingActionButton, RichTooltip, Tour
1051
+ };