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,484 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
3
|
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
|
4
|
+
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
5
|
+
import { cva } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../../utils/cn";
|
|
7
|
+
|
|
8
|
+
// ─── Navbar ────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface NavbarProps extends React.HTMLAttributes<HTMLElement> {
|
|
11
|
+
sticky?: boolean;
|
|
12
|
+
bordered?: boolean;
|
|
13
|
+
blurred?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Navbar = React.forwardRef<HTMLElement, NavbarProps>(
|
|
17
|
+
({ className, sticky, bordered, blurred, ...props }, ref) => (
|
|
18
|
+
<header
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
"atlas-navbar z-40 w-full",
|
|
22
|
+
sticky && "sticky top-0",
|
|
23
|
+
bordered && "border-b border-border",
|
|
24
|
+
blurred && "backdrop-blur-md bg-background/80 supports-[backdrop-filter]:bg-background/60",
|
|
25
|
+
!blurred && "bg-background",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
);
|
|
32
|
+
Navbar.displayName = "Navbar";
|
|
33
|
+
|
|
34
|
+
// ─── Sidebar ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export interface SidebarProps extends React.HTMLAttributes<HTMLElement> {
|
|
37
|
+
collapsible?: boolean;
|
|
38
|
+
collapsed?: boolean;
|
|
39
|
+
width?: string;
|
|
40
|
+
side?: "left" | "right";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const Sidebar = React.forwardRef<HTMLElement, SidebarProps>(
|
|
44
|
+
({ className, collapsed, width = "240px", side = "left", style, ...props }, ref) => (
|
|
45
|
+
<aside
|
|
46
|
+
ref={ref}
|
|
47
|
+
aria-label="Sidebar navigation"
|
|
48
|
+
className={cn(
|
|
49
|
+
"atlas-sidebar relative flex flex-col border-r border-border bg-background",
|
|
50
|
+
"transition-[width] duration-300 ease-in-out overflow-hidden shrink-0",
|
|
51
|
+
collapsed && "!w-0 border-transparent",
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
style={{ width: collapsed ? 0 : width, ...style }}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
Sidebar.displayName = "Sidebar";
|
|
60
|
+
|
|
61
|
+
// ─── Menu ──────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
64
|
+
icon?: React.ReactNode;
|
|
65
|
+
active?: boolean;
|
|
66
|
+
disabled?: boolean;
|
|
67
|
+
badge?: React.ReactNode;
|
|
68
|
+
as?: React.ElementType;
|
|
69
|
+
href?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
|
73
|
+
({ className, icon, active, disabled, badge, children, as: Comp = "div", ...props }, ref) => (
|
|
74
|
+
<Comp
|
|
75
|
+
ref={ref}
|
|
76
|
+
role="menuitem"
|
|
77
|
+
aria-current={active ? "page" : undefined}
|
|
78
|
+
aria-disabled={disabled}
|
|
79
|
+
className={cn(
|
|
80
|
+
"atlas-menu-item flex items-center gap-3 px-3 py-2 rounded-md text-sm",
|
|
81
|
+
"transition-colors duration-150 cursor-pointer select-none",
|
|
82
|
+
active
|
|
83
|
+
? "bg-accent text-accent-foreground font-medium"
|
|
84
|
+
: "text-foreground hover:bg-accent hover:text-accent-foreground",
|
|
85
|
+
disabled && "opacity-50 pointer-events-none",
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{icon && <span className="shrink-0 [&>svg]:h-4 [&>svg]:w-4" aria-hidden="true">{icon}</span>}
|
|
91
|
+
<span className="flex-1 truncate">{children}</span>
|
|
92
|
+
{badge && <span className="shrink-0">{badge}</span>}
|
|
93
|
+
</Comp>
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
MenuItem.displayName = "MenuItem";
|
|
97
|
+
|
|
98
|
+
const Menu = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
99
|
+
({ className, ...props }, ref) => (
|
|
100
|
+
<div
|
|
101
|
+
ref={ref}
|
|
102
|
+
role="menu"
|
|
103
|
+
className={cn("atlas-menu flex flex-col gap-0.5 p-1", className)}
|
|
104
|
+
{...props}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
);
|
|
108
|
+
Menu.displayName = "Menu";
|
|
109
|
+
|
|
110
|
+
// ─── DropdownMenu ─────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const DropdownMenu = DropdownMenuPrimitive.Root;
|
|
113
|
+
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
|
114
|
+
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
|
115
|
+
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
|
116
|
+
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
|
117
|
+
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
|
118
|
+
|
|
119
|
+
const DropdownMenuContent = React.forwardRef<
|
|
120
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
|
121
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
|
122
|
+
>(({ className, sideOffset = 4, ...props }, ref) => (
|
|
123
|
+
<DropdownMenuPrimitive.Portal>
|
|
124
|
+
<DropdownMenuPrimitive.Content
|
|
125
|
+
ref={ref}
|
|
126
|
+
sideOffset={sideOffset}
|
|
127
|
+
className={cn(
|
|
128
|
+
"atlas-dropdown-content z-50 min-w-[8rem] overflow-hidden rounded-md border border-border",
|
|
129
|
+
"bg-popover p-1 text-popover-foreground shadow-md",
|
|
130
|
+
"animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out",
|
|
131
|
+
"data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
|
132
|
+
"data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2",
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
/>
|
|
137
|
+
</DropdownMenuPrimitive.Portal>
|
|
138
|
+
));
|
|
139
|
+
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
|
140
|
+
|
|
141
|
+
const DropdownMenuItem = React.forwardRef<
|
|
142
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
|
143
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
|
144
|
+
inset?: boolean;
|
|
145
|
+
destructive?: boolean;
|
|
146
|
+
}
|
|
147
|
+
>(({ className, inset, destructive, ...props }, ref) => (
|
|
148
|
+
<DropdownMenuPrimitive.Item
|
|
149
|
+
ref={ref}
|
|
150
|
+
className={cn(
|
|
151
|
+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm",
|
|
152
|
+
"outline-none transition-colors gap-2",
|
|
153
|
+
"focus:bg-accent focus:text-accent-foreground",
|
|
154
|
+
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
155
|
+
inset && "pl-8",
|
|
156
|
+
destructive && "text-destructive focus:text-destructive",
|
|
157
|
+
className
|
|
158
|
+
)}
|
|
159
|
+
{...props}
|
|
160
|
+
/>
|
|
161
|
+
));
|
|
162
|
+
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
|
163
|
+
|
|
164
|
+
const DropdownMenuSeparator = React.forwardRef<
|
|
165
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
|
166
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
|
167
|
+
>(({ className, ...props }, ref) => (
|
|
168
|
+
<DropdownMenuPrimitive.Separator
|
|
169
|
+
ref={ref}
|
|
170
|
+
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
|
171
|
+
{...props}
|
|
172
|
+
/>
|
|
173
|
+
));
|
|
174
|
+
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
|
175
|
+
|
|
176
|
+
const DropdownMenuLabel = React.forwardRef<
|
|
177
|
+
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
|
178
|
+
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }
|
|
179
|
+
>(({ className, inset, ...props }, ref) => (
|
|
180
|
+
<DropdownMenuPrimitive.Label
|
|
181
|
+
ref={ref}
|
|
182
|
+
className={cn("px-2 py-1.5 text-xs font-semibold text-muted-foreground", inset && "pl-8", className)}
|
|
183
|
+
{...props}
|
|
184
|
+
/>
|
|
185
|
+
));
|
|
186
|
+
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
|
187
|
+
|
|
188
|
+
// ─── Breadcrumb ───────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
export interface BreadcrumbItem {
|
|
191
|
+
label: React.ReactNode;
|
|
192
|
+
href?: string;
|
|
193
|
+
current?: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface BreadcrumbProps extends React.HTMLAttributes<HTMLElement> {
|
|
197
|
+
items: BreadcrumbItem[];
|
|
198
|
+
separator?: React.ReactNode;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const Breadcrumb = React.forwardRef<HTMLElement, BreadcrumbProps>(
|
|
202
|
+
({ className, items, separator, ...props }, ref) => (
|
|
203
|
+
<nav ref={ref} aria-label="Breadcrumb" className={cn("atlas-breadcrumb", className)} {...props}>
|
|
204
|
+
<ol className="flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
|
|
205
|
+
{items.map((item, i) => (
|
|
206
|
+
<li key={i} className="flex items-center gap-1">
|
|
207
|
+
{i > 0 && (
|
|
208
|
+
<span aria-hidden="true" className="text-muted-foreground/50">
|
|
209
|
+
{separator ?? (
|
|
210
|
+
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
211
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
212
|
+
</svg>
|
|
213
|
+
)}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
{item.href && !item.current ? (
|
|
217
|
+
<a href={item.href} className="hover:text-foreground transition-colors">
|
|
218
|
+
{item.label}
|
|
219
|
+
</a>
|
|
220
|
+
) : (
|
|
221
|
+
<span
|
|
222
|
+
className={cn(item.current && "text-foreground font-medium")}
|
|
223
|
+
aria-current={item.current ? "page" : undefined}
|
|
224
|
+
>
|
|
225
|
+
{item.label}
|
|
226
|
+
</span>
|
|
227
|
+
)}
|
|
228
|
+
</li>
|
|
229
|
+
))}
|
|
230
|
+
</ol>
|
|
231
|
+
</nav>
|
|
232
|
+
)
|
|
233
|
+
);
|
|
234
|
+
Breadcrumb.displayName = "Breadcrumb";
|
|
235
|
+
|
|
236
|
+
// ─── Pagination ───────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export interface PaginationProps extends React.HTMLAttributes<HTMLElement> {
|
|
239
|
+
total: number;
|
|
240
|
+
page: number;
|
|
241
|
+
pageSize?: number;
|
|
242
|
+
onPageChange?: (page: number) => void;
|
|
243
|
+
siblingCount?: number;
|
|
244
|
+
showEdges?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const Pagination = React.forwardRef<HTMLElement, PaginationProps>(
|
|
248
|
+
({ className, total, page, pageSize = 10, onPageChange, siblingCount = 1, showEdges = true, ...props }, ref) => {
|
|
249
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
250
|
+
|
|
251
|
+
const getPageNumbers = () => {
|
|
252
|
+
const pages: (number | "...")[] = [];
|
|
253
|
+
const delta = siblingCount;
|
|
254
|
+
|
|
255
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
256
|
+
if (
|
|
257
|
+
i === 1 ||
|
|
258
|
+
i === totalPages ||
|
|
259
|
+
(i >= page - delta && i <= page + delta)
|
|
260
|
+
) {
|
|
261
|
+
pages.push(i);
|
|
262
|
+
} else if (pages[pages.length - 1] !== "...") {
|
|
263
|
+
pages.push("...");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return pages;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (totalPages <= 1) return null;
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<nav ref={ref} aria-label="Pagination" className={cn("atlas-pagination flex items-center gap-1", className)} {...props}>
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => onPageChange?.(page - 1)}
|
|
275
|
+
disabled={page <= 1}
|
|
276
|
+
className="h-8 w-8 flex items-center justify-center rounded-md border border-border text-sm hover:bg-accent disabled:opacity-50 disabled:pointer-events-none"
|
|
277
|
+
aria-label="Previous page"
|
|
278
|
+
>
|
|
279
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
280
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
281
|
+
</svg>
|
|
282
|
+
</button>
|
|
283
|
+
{getPageNumbers().map((p, i) =>
|
|
284
|
+
p === "..." ? (
|
|
285
|
+
<span key={`ellipsis-${i}`} className="px-1 text-sm text-muted-foreground">…</span>
|
|
286
|
+
) : (
|
|
287
|
+
<button
|
|
288
|
+
key={p}
|
|
289
|
+
onClick={() => onPageChange?.(p as number)}
|
|
290
|
+
aria-current={p === page ? "page" : undefined}
|
|
291
|
+
className={cn(
|
|
292
|
+
"h-8 min-w-[2rem] px-2 flex items-center justify-center rounded-md text-sm font-medium transition-colors",
|
|
293
|
+
p === page
|
|
294
|
+
? "bg-primary text-primary-foreground"
|
|
295
|
+
: "border border-border hover:bg-accent"
|
|
296
|
+
)}
|
|
297
|
+
>
|
|
298
|
+
{p}
|
|
299
|
+
</button>
|
|
300
|
+
)
|
|
301
|
+
)}
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => onPageChange?.(page + 1)}
|
|
304
|
+
disabled={page >= totalPages}
|
|
305
|
+
className="h-8 w-8 flex items-center justify-center rounded-md border border-border text-sm hover:bg-accent disabled:opacity-50 disabled:pointer-events-none"
|
|
306
|
+
aria-label="Next page"
|
|
307
|
+
>
|
|
308
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
309
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7 7 7" />
|
|
310
|
+
</svg>
|
|
311
|
+
</button>
|
|
312
|
+
</nav>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
);
|
|
316
|
+
Pagination.displayName = "Pagination";
|
|
317
|
+
|
|
318
|
+
// ─── Tabs ─────────────────────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
const Tabs = TabsPrimitive.Root;
|
|
321
|
+
|
|
322
|
+
const TabsList = React.forwardRef<
|
|
323
|
+
React.ElementRef<typeof TabsPrimitive.List>,
|
|
324
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
|
325
|
+
variant?: "line" | "pills" | "enclosed";
|
|
326
|
+
}
|
|
327
|
+
>(({ className, variant = "line", ...props }, ref) => (
|
|
328
|
+
<TabsPrimitive.List
|
|
329
|
+
ref={ref}
|
|
330
|
+
className={cn(
|
|
331
|
+
"atlas-tabs-list inline-flex items-center",
|
|
332
|
+
variant === "line" && "border-b border-border gap-0 w-full",
|
|
333
|
+
variant === "pills" && "gap-1 bg-muted p-1 rounded-lg",
|
|
334
|
+
variant === "enclosed" && "border border-border rounded-t-lg gap-0",
|
|
335
|
+
className
|
|
336
|
+
)}
|
|
337
|
+
{...props}
|
|
338
|
+
/>
|
|
339
|
+
));
|
|
340
|
+
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
341
|
+
|
|
342
|
+
const TabsTrigger = React.forwardRef<
|
|
343
|
+
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
344
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
|
|
345
|
+
variant?: "line" | "pills" | "enclosed";
|
|
346
|
+
}
|
|
347
|
+
>(({ className, variant = "line", ...props }, ref) => (
|
|
348
|
+
<TabsPrimitive.Trigger
|
|
349
|
+
ref={ref}
|
|
350
|
+
className={cn(
|
|
351
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium",
|
|
352
|
+
"transition-all duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
353
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
354
|
+
variant === "line" && [
|
|
355
|
+
"px-4 py-2.5 border-b-2 border-transparent -mb-px",
|
|
356
|
+
"text-muted-foreground hover:text-foreground",
|
|
357
|
+
"data-[state=active]:border-primary data-[state=active]:text-foreground",
|
|
358
|
+
],
|
|
359
|
+
variant === "pills" && [
|
|
360
|
+
"px-3 py-1.5 rounded-md text-muted-foreground",
|
|
361
|
+
"data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
362
|
+
],
|
|
363
|
+
variant === "enclosed" && [
|
|
364
|
+
"px-4 py-2 border-r last:border-r-0 border-border",
|
|
365
|
+
"text-muted-foreground hover:text-foreground bg-muted",
|
|
366
|
+
"data-[state=active]:bg-background data-[state=active]:text-foreground",
|
|
367
|
+
],
|
|
368
|
+
className
|
|
369
|
+
)}
|
|
370
|
+
{...props}
|
|
371
|
+
/>
|
|
372
|
+
));
|
|
373
|
+
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
374
|
+
|
|
375
|
+
const TabsContent = React.forwardRef<
|
|
376
|
+
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
377
|
+
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
378
|
+
>(({ className, ...props }, ref) => (
|
|
379
|
+
<TabsPrimitive.Content
|
|
380
|
+
ref={ref}
|
|
381
|
+
className={cn(
|
|
382
|
+
"atlas-tabs-content mt-4",
|
|
383
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
384
|
+
className
|
|
385
|
+
)}
|
|
386
|
+
{...props}
|
|
387
|
+
/>
|
|
388
|
+
));
|
|
389
|
+
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
|
390
|
+
|
|
391
|
+
// ─── Stepper ──────────────────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
export interface StepperStep {
|
|
394
|
+
title: string;
|
|
395
|
+
description?: string;
|
|
396
|
+
icon?: React.ReactNode;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export interface StepperProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
400
|
+
steps: StepperStep[];
|
|
401
|
+
current: number;
|
|
402
|
+
orientation?: "horizontal" | "vertical";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const Stepper = React.forwardRef<HTMLDivElement, StepperProps>(
|
|
406
|
+
({ className, steps, current, orientation = "horizontal", ...props }, ref) => (
|
|
407
|
+
<div
|
|
408
|
+
ref={ref}
|
|
409
|
+
className={cn(
|
|
410
|
+
"atlas-stepper flex",
|
|
411
|
+
orientation === "horizontal" ? "flex-row items-center" : "flex-col",
|
|
412
|
+
className
|
|
413
|
+
)}
|
|
414
|
+
role="list"
|
|
415
|
+
aria-label="Progress steps"
|
|
416
|
+
{...props}
|
|
417
|
+
>
|
|
418
|
+
{steps.map((step, i) => {
|
|
419
|
+
const status = i < current ? "complete" : i === current ? "current" : "upcoming";
|
|
420
|
+
|
|
421
|
+
return (
|
|
422
|
+
<React.Fragment key={i}>
|
|
423
|
+
<div
|
|
424
|
+
role="listitem"
|
|
425
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
426
|
+
className={cn(
|
|
427
|
+
"flex items-center gap-3",
|
|
428
|
+
orientation === "vertical" && "flex-col items-start",
|
|
429
|
+
)}
|
|
430
|
+
>
|
|
431
|
+
<div className={cn(
|
|
432
|
+
"flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold",
|
|
433
|
+
"transition-colors border-2",
|
|
434
|
+
status === "complete" && "bg-primary border-primary text-primary-foreground",
|
|
435
|
+
status === "current" && "border-primary text-primary bg-primary/10",
|
|
436
|
+
status === "upcoming" && "border-border text-muted-foreground",
|
|
437
|
+
)}>
|
|
438
|
+
{status === "complete" ? (
|
|
439
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
440
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
441
|
+
</svg>
|
|
442
|
+
) : (step.icon ?? <span>{i + 1}</span>)}
|
|
443
|
+
</div>
|
|
444
|
+
<div className={orientation === "horizontal" ? "hidden sm:block" : ""}>
|
|
445
|
+
<p className={cn("text-sm font-medium", status === "upcoming" && "text-muted-foreground")}>
|
|
446
|
+
{step.title}
|
|
447
|
+
</p>
|
|
448
|
+
{step.description && (
|
|
449
|
+
<p className="text-xs text-muted-foreground">{step.description}</p>
|
|
450
|
+
)}
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
{i < steps.length - 1 && (
|
|
454
|
+
<div className={cn(
|
|
455
|
+
"transition-colors",
|
|
456
|
+
orientation === "horizontal"
|
|
457
|
+
? "flex-1 h-px mx-3"
|
|
458
|
+
: "ml-4 w-px h-8",
|
|
459
|
+
i < current ? "bg-primary" : "bg-border"
|
|
460
|
+
)} aria-hidden="true" />
|
|
461
|
+
)}
|
|
462
|
+
</React.Fragment>
|
|
463
|
+
);
|
|
464
|
+
})}
|
|
465
|
+
</div>
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
Stepper.displayName = "Stepper";
|
|
469
|
+
|
|
470
|
+
// ─── CommandPalette ─────────────────────────────────────────────────────
|
|
471
|
+
// Thin wrapper - full implementation in overlay/CommandDialog
|
|
472
|
+
export { Stepper as CommandPalette } from "./index"; // Placeholder, see CommandDialog
|
|
473
|
+
|
|
474
|
+
export {
|
|
475
|
+
Navbar, Sidebar,
|
|
476
|
+
Menu, MenuItem,
|
|
477
|
+
DropdownMenu, DropdownMenuTrigger, DropdownMenuContent,
|
|
478
|
+
DropdownMenuItem, DropdownMenuSeparator, DropdownMenuLabel,
|
|
479
|
+
DropdownMenuGroup, DropdownMenuPortal, DropdownMenuSub, DropdownMenuRadioGroup,
|
|
480
|
+
Breadcrumb,
|
|
481
|
+
Pagination,
|
|
482
|
+
Tabs, TabsList, TabsTrigger, TabsContent,
|
|
483
|
+
Stepper,
|
|
484
|
+
};
|