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,473 @@
1
+ import * as React from "react";
2
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
3
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
6
+ import { Command } from "cmdk";
7
+ import { cva, type VariantProps } from "class-variance-authority";
8
+ import { cn } from "../../utils/cn";
9
+
10
+ // ─── Shared overlay styles ─────────────────────────────────────────────────
11
+
12
+ const overlayBase = cn(
13
+ "fixed inset-0 z-50 bg-black/50",
14
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
15
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
16
+ );
17
+
18
+ // ─── Dialog ────────────────────────────────────────────────────────────────
19
+
20
+ const Dialog = DialogPrimitive.Root;
21
+ const DialogTrigger = DialogPrimitive.Trigger;
22
+ const DialogPortal = DialogPrimitive.Portal;
23
+ const DialogClose = DialogPrimitive.Close;
24
+
25
+ const DialogOverlay = React.forwardRef<
26
+ React.ElementRef<typeof DialogPrimitive.Overlay>,
27
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
28
+ >(({ className, ...props }, ref) => (
29
+ <DialogPrimitive.Overlay ref={ref} className={cn(overlayBase, className)} {...props} />
30
+ ));
31
+ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
32
+
33
+ const dialogContentVariants = cva(
34
+ [
35
+ "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%]",
36
+ "gap-4 bg-background shadow-xl duration-200",
37
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
38
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
39
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
40
+ "data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]",
41
+ "data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
42
+ ],
43
+ {
44
+ variants: {
45
+ size: {
46
+ sm: "max-w-sm rounded-xl p-6",
47
+ md: "max-w-lg rounded-xl p-6",
48
+ lg: "max-w-2xl rounded-xl p-6",
49
+ xl: "max-w-4xl rounded-xl p-6",
50
+ full: "max-w-[calc(100vw-2rem)] h-[calc(100vh-2rem)] rounded-xl p-6 overflow-y-auto",
51
+ },
52
+ },
53
+ defaultVariants: { size: "md" },
54
+ }
55
+ );
56
+
57
+ const DialogContent = React.forwardRef<
58
+ React.ElementRef<typeof DialogPrimitive.Content>,
59
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> &
60
+ VariantProps<typeof dialogContentVariants> & { showClose?: boolean }
61
+ >(({ className, children, size, showClose = true, ...props }, ref) => (
62
+ <DialogPortal>
63
+ <DialogOverlay />
64
+ <DialogPrimitive.Content
65
+ ref={ref}
66
+ className={cn(dialogContentVariants({ size }), className)}
67
+ {...props}
68
+ >
69
+ {children}
70
+ {showClose && (
71
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring disabled:pointer-events-none data-[state=open]:bg-accent">
72
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
73
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
74
+ </svg>
75
+ <span className="sr-only">Close</span>
76
+ </DialogPrimitive.Close>
77
+ )}
78
+ </DialogPrimitive.Content>
79
+ </DialogPortal>
80
+ ));
81
+ DialogContent.displayName = DialogPrimitive.Content.displayName;
82
+
83
+ const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
84
+ <div className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)} {...props} />
85
+ );
86
+ DialogHeader.displayName = "DialogHeader";
87
+
88
+ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
89
+ <div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end gap-2", className)} {...props} />
90
+ );
91
+ DialogFooter.displayName = "DialogFooter";
92
+
93
+ const DialogTitle = React.forwardRef<
94
+ React.ElementRef<typeof DialogPrimitive.Title>,
95
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
96
+ >(({ className, ...props }, ref) => (
97
+ <DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-tight tracking-tight", className)} {...props} />
98
+ ));
99
+ DialogTitle.displayName = DialogPrimitive.Title.displayName;
100
+
101
+ const DialogDescription = React.forwardRef<
102
+ React.ElementRef<typeof DialogPrimitive.Description>,
103
+ React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
104
+ >(({ className, ...props }, ref) => (
105
+ <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
106
+ ));
107
+ DialogDescription.displayName = DialogPrimitive.Description.displayName;
108
+
109
+ // ─── Modal (alias for Dialog with preset layout) ─────────────────────────
110
+
111
+ export interface ModalProps {
112
+ open?: boolean;
113
+ onOpenChange?: (open: boolean) => void;
114
+ title?: React.ReactNode;
115
+ description?: React.ReactNode;
116
+ footer?: React.ReactNode;
117
+ size?: "sm" | "md" | "lg" | "xl" | "full";
118
+ children?: React.ReactNode;
119
+ showClose?: boolean;
120
+ }
121
+
122
+ const Modal = ({ open, onOpenChange, title, description, footer, size = "md", children, showClose = true }: ModalProps) => (
123
+ <Dialog open={open} onOpenChange={onOpenChange}>
124
+ <DialogContent size={size} showClose={showClose}>
125
+ {(title || description) && (
126
+ <DialogHeader>
127
+ {title && <DialogTitle>{title}</DialogTitle>}
128
+ {description && <DialogDescription>{description}</DialogDescription>}
129
+ </DialogHeader>
130
+ )}
131
+ {children}
132
+ {footer && <DialogFooter>{footer}</DialogFooter>}
133
+ </DialogContent>
134
+ </Dialog>
135
+ );
136
+ Modal.displayName = "Modal";
137
+
138
+ // ─── Drawer ────────────────────────────────────────────────────────────────
139
+
140
+ export interface DrawerProps {
141
+ open?: boolean;
142
+ onOpenChange?: (open: boolean) => void;
143
+ side?: "left" | "right" | "top" | "bottom";
144
+ title?: React.ReactNode;
145
+ description?: React.ReactNode;
146
+ footer?: React.ReactNode;
147
+ children?: React.ReactNode;
148
+ size?: "sm" | "md" | "lg" | "full";
149
+ }
150
+
151
+ const drawerSizes = {
152
+ left: { sm: "w-64", md: "w-80", lg: "w-96", full: "w-screen" },
153
+ right: { sm: "w-64", md: "w-80", lg: "w-96", full: "w-screen" },
154
+ top: { sm: "h-48", md: "h-64", lg: "h-80", full: "h-screen" },
155
+ bottom: { sm: "h-48", md: "h-64", lg: "h-80", full: "h-screen" },
156
+ };
157
+
158
+ const drawerAnimations = {
159
+ left: "data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",
160
+ right: "data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right",
161
+ top: "data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
162
+ bottom: "data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
163
+ };
164
+
165
+ const Drawer = ({ open, onOpenChange, side = "right", title, description, footer, children, size = "md" }: DrawerProps) => (
166
+ <Dialog open={open} onOpenChange={onOpenChange}>
167
+ <DialogPortal>
168
+ <DialogOverlay />
169
+ <DialogPrimitive.Content
170
+ className={cn(
171
+ "atlas-drawer fixed z-50 gap-4 bg-background shadow-xl p-6",
172
+ "transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out duration-300",
173
+ side === "left" && `left-0 top-0 h-full ${drawerSizes.left[size]}`,
174
+ side === "right" && `right-0 top-0 h-full ${drawerSizes.right[size]}`,
175
+ side === "top" && `top-0 left-0 w-full ${drawerSizes.top[size]}`,
176
+ side === "bottom" && `bottom-0 left-0 w-full ${drawerSizes.bottom[size]}`,
177
+ drawerAnimations[side],
178
+ "flex flex-col"
179
+ )}
180
+ >
181
+ <div className="flex items-start justify-between">
182
+ <div>
183
+ {title && <h2 className="text-lg font-semibold">{title}</h2>}
184
+ {description && <p className="text-sm text-muted-foreground">{description}</p>}
185
+ </div>
186
+ <DialogPrimitive.Close className="ml-auto rounded-sm opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring">
187
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
188
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
189
+ </svg>
190
+ <span className="sr-only">Close</span>
191
+ </DialogPrimitive.Close>
192
+ </div>
193
+ <div className="flex-1 overflow-y-auto py-4">{children}</div>
194
+ {footer && <div className="border-t border-border pt-4 mt-auto">{footer}</div>}
195
+ </DialogPrimitive.Content>
196
+ </DialogPortal>
197
+ </Dialog>
198
+ );
199
+ Drawer.displayName = "Drawer";
200
+
201
+ // ─── Sheet (alias for Drawer) ─────────────────────────────────────────────
202
+
203
+ const Sheet = Drawer;
204
+
205
+ // ─── Popover ──────────────────────────────────────────────────────────────
206
+
207
+ const Popover = PopoverPrimitive.Root;
208
+ const PopoverTrigger = PopoverPrimitive.Trigger;
209
+
210
+ const PopoverContent = React.forwardRef<
211
+ React.ElementRef<typeof PopoverPrimitive.Content>,
212
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
213
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
214
+ <PopoverPrimitive.Portal>
215
+ <PopoverPrimitive.Content
216
+ ref={ref}
217
+ align={align}
218
+ sideOffset={sideOffset}
219
+ className={cn(
220
+ "atlas-popover z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md",
221
+ "outline-none",
222
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
223
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
224
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
225
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
226
+ className
227
+ )}
228
+ {...props}
229
+ />
230
+ </PopoverPrimitive.Portal>
231
+ ));
232
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
233
+
234
+ // ─── HoverCard ────────────────────────────────────────────────────────────
235
+
236
+ const HoverCard = HoverCardPrimitive.Root;
237
+ const HoverCardTrigger = HoverCardPrimitive.Trigger;
238
+
239
+ const HoverCardContent = React.forwardRef<
240
+ React.ElementRef<typeof HoverCardPrimitive.Content>,
241
+ React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
242
+ >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
243
+ <HoverCardPrimitive.Content
244
+ ref={ref}
245
+ align={align}
246
+ sideOffset={sideOffset}
247
+ className={cn(
248
+ "atlas-hover-card z-50 w-64 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md",
249
+ "outline-none",
250
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
251
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
252
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
253
+ "data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
254
+ className
255
+ )}
256
+ {...props}
257
+ />
258
+ ));
259
+ HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
260
+
261
+ // ─── ContextMenu ──────────────────────────────────────────────────────────
262
+
263
+ const ContextMenu = ContextMenuPrimitive.Root;
264
+ const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
265
+ const ContextMenuGroup = ContextMenuPrimitive.Group;
266
+ const ContextMenuSub = ContextMenuPrimitive.Sub;
267
+ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
268
+
269
+ const ContextMenuContent = React.forwardRef<
270
+ React.ElementRef<typeof ContextMenuPrimitive.Content>,
271
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
272
+ >(({ className, ...props }, ref) => (
273
+ <ContextMenuPrimitive.Portal>
274
+ <ContextMenuPrimitive.Content
275
+ ref={ref}
276
+ className={cn(
277
+ "atlas-context-menu z-50 min-w-[8rem] overflow-hidden rounded-md border border-border",
278
+ "bg-popover p-1 text-popover-foreground shadow-md",
279
+ "animate-in fade-in-80 data-[state=open]:animate-in data-[state=closed]:animate-out",
280
+ className
281
+ )}
282
+ {...props}
283
+ />
284
+ </ContextMenuPrimitive.Portal>
285
+ ));
286
+ ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
287
+
288
+ const ContextMenuItem = React.forwardRef<
289
+ React.ElementRef<typeof ContextMenuPrimitive.Item>,
290
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & { inset?: boolean; destructive?: boolean }
291
+ >(({ className, inset, destructive, ...props }, ref) => (
292
+ <ContextMenuPrimitive.Item
293
+ ref={ref}
294
+ className={cn(
295
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm",
296
+ "outline-none focus:bg-accent focus:text-accent-foreground gap-2",
297
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
298
+ inset && "pl-8",
299
+ destructive && "text-destructive focus:text-destructive",
300
+ className
301
+ )}
302
+ {...props}
303
+ />
304
+ ));
305
+ ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
306
+
307
+ const ContextMenuSeparator = React.forwardRef<
308
+ React.ElementRef<typeof ContextMenuPrimitive.Separator>,
309
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
310
+ >(({ className, ...props }, ref) => (
311
+ <ContextMenuPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-border", className)} {...props} />
312
+ ));
313
+ ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
314
+
315
+ const ContextMenuLabel = React.forwardRef<
316
+ React.ElementRef<typeof ContextMenuPrimitive.Label>,
317
+ React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & { inset?: boolean }
318
+ >(({ className, inset, ...props }, ref) => (
319
+ <ContextMenuPrimitive.Label
320
+ ref={ref}
321
+ className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", inset && "pl-8", className)}
322
+ {...props}
323
+ />
324
+ ));
325
+ ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
326
+
327
+ // ─── CommandDialog (Command Palette + Dialog combo) ──────────────────────
328
+
329
+ export interface CommandDialogProps {
330
+ open?: boolean;
331
+ onOpenChange?: (open: boolean) => void;
332
+ placeholder?: string;
333
+ children?: React.ReactNode;
334
+ className?: string;
335
+ }
336
+
337
+ const CommandDialog = ({ open, onOpenChange, placeholder = "Search commands...", children, className }: CommandDialogProps) => (
338
+ <Dialog open={open} onOpenChange={onOpenChange}>
339
+ <DialogPortal>
340
+ <DialogOverlay />
341
+ <DialogPrimitive.Content
342
+ className={cn(
343
+ "atlas-command-dialog fixed left-[50%] top-[20%] z-50 w-full max-w-xl translate-x-[-50%]",
344
+ "overflow-hidden rounded-xl border border-border bg-popover shadow-2xl",
345
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
346
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
347
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
348
+ "data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2",
349
+ className
350
+ )}
351
+ >
352
+ <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
353
+ <div className="flex items-center border-b border-border px-3">
354
+ <svg className="mr-2 h-4 w-4 shrink-0 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
355
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
356
+ </svg>
357
+ <Command.Input placeholder={placeholder} className="flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50" />
358
+ </div>
359
+ <Command.List className="max-h-80 overflow-y-auto overflow-x-hidden p-1">
360
+ <Command.Empty className="py-6 text-center text-sm text-muted-foreground">No results found.</Command.Empty>
361
+ {children}
362
+ </Command.List>
363
+ </Command>
364
+ </DialogPrimitive.Content>
365
+ </DialogPortal>
366
+ </Dialog>
367
+ );
368
+ CommandDialog.displayName = "CommandDialog";
369
+
370
+ export const CommandItem = React.forwardRef<
371
+ React.ElementRef<typeof Command.Item>,
372
+ React.ComponentPropsWithoutRef<typeof Command.Item>
373
+ >(({ className, ...props }, ref) => (
374
+ <Command.Item
375
+ ref={ref}
376
+ className={cn(
377
+ "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm gap-2",
378
+ "outline-none aria-selected:bg-accent aria-selected:text-accent-foreground",
379
+ "data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
380
+ className
381
+ )}
382
+ {...props}
383
+ />
384
+ ));
385
+ CommandItem.displayName = Command.Item.displayName;
386
+
387
+ export const CommandGroup = React.forwardRef<
388
+ React.ElementRef<typeof Command.Group>,
389
+ React.ComponentPropsWithoutRef<typeof Command.Group>
390
+ >(({ className, ...props }, ref) => (
391
+ <Command.Group
392
+ ref={ref}
393
+ className={cn(
394
+ "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
395
+ className
396
+ )}
397
+ {...props}
398
+ />
399
+ ));
400
+ CommandGroup.displayName = Command.Group.displayName;
401
+
402
+ export const CommandSeparator = React.forwardRef<
403
+ React.ElementRef<typeof Command.Separator>,
404
+ React.ComponentPropsWithoutRef<typeof Command.Separator>
405
+ >(({ className, ...props }, ref) => (
406
+ <Command.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
407
+ ));
408
+ CommandSeparator.displayName = Command.Separator.displayName;
409
+
410
+ // ─── Lightbox / ImageViewer ───────────────────────────────────────────────
411
+
412
+ export interface LightboxProps {
413
+ open?: boolean;
414
+ onOpenChange?: (open: boolean) => void;
415
+ src: string;
416
+ alt?: string;
417
+ caption?: string;
418
+ }
419
+
420
+ const Lightbox = ({ open, onOpenChange, src, alt = "", caption }: LightboxProps) => (
421
+ <Dialog open={open} onOpenChange={onOpenChange}>
422
+ <DialogPortal>
423
+ <DialogOverlay />
424
+ <DialogPrimitive.Content
425
+ className={cn(
426
+ "atlas-lightbox fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2",
427
+ "max-w-[90vw] max-h-[90vh] overflow-hidden rounded-lg",
428
+ "data-[state=open]:animate-in data-[state=closed]:animate-out",
429
+ "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
430
+ "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
431
+ "focus:outline-none"
432
+ )}
433
+ >
434
+ <div className="relative">
435
+ <img
436
+ src={src}
437
+ alt={alt}
438
+ className="max-w-full max-h-[85vh] object-contain rounded-lg"
439
+ />
440
+ {caption && (
441
+ <div className="absolute bottom-0 left-0 right-0 bg-black/60 px-4 py-2 rounded-b-lg">
442
+ <p className="text-sm text-white text-center">{caption}</p>
443
+ </div>
444
+ )}
445
+ <DialogPrimitive.Close className="absolute top-2 right-2 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/70 transition-colors">
446
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
447
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
448
+ </svg>
449
+ <span className="sr-only">Close</span>
450
+ </DialogPrimitive.Close>
451
+ </div>
452
+ </DialogPrimitive.Content>
453
+ </DialogPortal>
454
+ </Dialog>
455
+ );
456
+ Lightbox.displayName = "Lightbox";
457
+
458
+ const ImageViewer = Lightbox;
459
+
460
+ export {
461
+ Modal,
462
+ Dialog, DialogTrigger, DialogPortal, DialogOverlay, DialogClose,
463
+ DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription,
464
+ Drawer,
465
+ Sheet,
466
+ Popover, PopoverTrigger, PopoverContent,
467
+ HoverCard, HoverCardTrigger, HoverCardContent,
468
+ ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem,
469
+ ContextMenuSeparator, ContextMenuLabel, ContextMenuGroup, ContextMenuSub, ContextMenuRadioGroup,
470
+ CommandDialog,
471
+ Lightbox,
472
+ ImageViewer,
473
+ };