atlasui-lib 0.1.0
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 +157 -0
- package/LICENSE +21 -0
- package/README.md +253 -0
- package/dist/cli/index.js +364 -0
- package/dist/index.d.mts +1027 -0
- package/dist/index.d.ts +1027 -0
- package/dist/index.js +3954 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3733 -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 +816 -0
- package/dist/provider.js.map +1 -0
- package/dist/provider.mjs +780 -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 +301 -0
- package/src/cli/registry.ts +139 -0
- package/src/components/advanced-forms/index.tsx +567 -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 +608 -0
- package/src/components/feedback/index.tsx +554 -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 +411 -0
- package/src/hooks/index.ts +271 -0
- package/src/hooks/use-toast.tsx +74 -0
- package/src/index.ts +353 -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,608 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { cn } from "../../utils/cn";
|
|
4
|
+
|
|
5
|
+
// ─── Card ──────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const cardVariants = cva(
|
|
8
|
+
"atlas-card rounded-xl border bg-card text-card-foreground",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "border-border shadow-sm",
|
|
13
|
+
outline: "border-border shadow-none",
|
|
14
|
+
elevated: "border-transparent shadow-lg",
|
|
15
|
+
ghost: "border-transparent shadow-none bg-transparent",
|
|
16
|
+
filled: "border-transparent bg-muted",
|
|
17
|
+
},
|
|
18
|
+
interactive: {
|
|
19
|
+
true: "cursor-pointer transition-shadow hover:shadow-md hover:-translate-y-0.5 active:translate-y-0 active:shadow-sm",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: { variant: "default" },
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface CardProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {}
|
|
27
|
+
|
|
28
|
+
const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
|
29
|
+
({ className, variant, interactive, ...props }, ref) => (
|
|
30
|
+
<div ref={ref} className={cn(cardVariants({ variant, interactive, className }))} {...props} />
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
Card.displayName = "Card";
|
|
34
|
+
|
|
35
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
36
|
+
({ className, ...props }, ref) => (
|
|
37
|
+
<div ref={ref} className={cn("flex flex-col gap-1.5 p-6", className)} {...props} />
|
|
38
|
+
)
|
|
39
|
+
);
|
|
40
|
+
CardHeader.displayName = "CardHeader";
|
|
41
|
+
|
|
42
|
+
const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
43
|
+
({ className, ...props }, ref) => (
|
|
44
|
+
<h3 ref={ref} className={cn("text-lg font-semibold leading-tight tracking-tight", className)} {...props} />
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
CardTitle.displayName = "CardTitle";
|
|
48
|
+
|
|
49
|
+
const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
|
|
50
|
+
({ className, ...props }, ref) => (
|
|
51
|
+
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
CardDescription.displayName = "CardDescription";
|
|
55
|
+
|
|
56
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
57
|
+
({ className, ...props }, ref) => (
|
|
58
|
+
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
CardContent.displayName = "CardContent";
|
|
62
|
+
|
|
63
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
64
|
+
({ className, ...props }, ref) => (
|
|
65
|
+
<div ref={ref} className={cn("flex items-center p-6 pt-0 gap-2", className)} {...props} />
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
CardFooter.displayName = "CardFooter";
|
|
69
|
+
|
|
70
|
+
// ─── Table ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
73
|
+
({ className, ...props }, ref) => (
|
|
74
|
+
<div className="atlas-table relative w-full overflow-auto">
|
|
75
|
+
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
);
|
|
79
|
+
Table.displayName = "Table";
|
|
80
|
+
|
|
81
|
+
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
82
|
+
({ className, ...props }, ref) => (
|
|
83
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
TableHeader.displayName = "TableHeader";
|
|
87
|
+
|
|
88
|
+
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
89
|
+
({ className, ...props }, ref) => (
|
|
90
|
+
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
TableBody.displayName = "TableBody";
|
|
94
|
+
|
|
95
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
96
|
+
({ className, ...props }, ref) => (
|
|
97
|
+
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
TableRow.displayName = "TableRow";
|
|
101
|
+
|
|
102
|
+
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
|
103
|
+
({ className, ...props }, ref) => (
|
|
104
|
+
<th ref={ref} className={cn("h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
TableHead.displayName = "TableHead";
|
|
108
|
+
|
|
109
|
+
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
|
110
|
+
({ className, ...props }, ref) => (
|
|
111
|
+
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
|
112
|
+
)
|
|
113
|
+
);
|
|
114
|
+
TableCell.displayName = "TableCell";
|
|
115
|
+
|
|
116
|
+
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
|
117
|
+
({ className, ...props }, ref) => (
|
|
118
|
+
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
TableCaption.displayName = "TableCaption";
|
|
122
|
+
|
|
123
|
+
// ─── DataTable ────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface DataTableColumn<T> {
|
|
126
|
+
key: keyof T | string;
|
|
127
|
+
header: React.ReactNode;
|
|
128
|
+
cell?: (row: T, index: number) => React.ReactNode;
|
|
129
|
+
sortable?: boolean;
|
|
130
|
+
width?: string | number;
|
|
131
|
+
align?: "left" | "center" | "right";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface DataTableProps<T extends Record<string, unknown>> {
|
|
135
|
+
data: T[];
|
|
136
|
+
columns: DataTableColumn<T>[];
|
|
137
|
+
loading?: boolean;
|
|
138
|
+
emptyText?: string;
|
|
139
|
+
onSort?: (key: string, direction: "asc" | "desc") => void;
|
|
140
|
+
striped?: boolean;
|
|
141
|
+
bordered?: boolean;
|
|
142
|
+
className?: string;
|
|
143
|
+
caption?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function DataTable<T extends Record<string, unknown>>({
|
|
147
|
+
data,
|
|
148
|
+
columns,
|
|
149
|
+
loading,
|
|
150
|
+
emptyText = "No data available",
|
|
151
|
+
onSort,
|
|
152
|
+
striped,
|
|
153
|
+
bordered,
|
|
154
|
+
className,
|
|
155
|
+
caption,
|
|
156
|
+
}: DataTableProps<T>) {
|
|
157
|
+
const [sortKey, setSortKey] = React.useState<string | null>(null);
|
|
158
|
+
const [sortDir, setSortDir] = React.useState<"asc" | "desc">("asc");
|
|
159
|
+
|
|
160
|
+
const handleSort = (key: string) => {
|
|
161
|
+
const newDir = sortKey === key && sortDir === "asc" ? "desc" : "asc";
|
|
162
|
+
setSortKey(key);
|
|
163
|
+
setSortDir(newDir);
|
|
164
|
+
onSort?.(key, newDir);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className={cn("atlas-data-table relative w-full overflow-auto rounded-md", bordered && "border border-border", className)}>
|
|
169
|
+
<table className="w-full caption-bottom text-sm">
|
|
170
|
+
{caption && <TableCaption>{caption}</TableCaption>}
|
|
171
|
+
<thead className="border-b bg-muted/50">
|
|
172
|
+
<tr>
|
|
173
|
+
{columns.map((col, i) => (
|
|
174
|
+
<th
|
|
175
|
+
key={i}
|
|
176
|
+
style={{ width: col.width }}
|
|
177
|
+
className={cn(
|
|
178
|
+
"h-10 px-4 font-medium text-muted-foreground",
|
|
179
|
+
col.align === "center" && "text-center",
|
|
180
|
+
col.align === "right" && "text-right",
|
|
181
|
+
col.sortable && "cursor-pointer select-none hover:text-foreground",
|
|
182
|
+
)}
|
|
183
|
+
onClick={() => col.sortable && handleSort(String(col.key))}
|
|
184
|
+
aria-sort={
|
|
185
|
+
sortKey === col.key
|
|
186
|
+
? sortDir === "asc" ? "ascending" : "descending"
|
|
187
|
+
: col.sortable ? "none" : undefined
|
|
188
|
+
}
|
|
189
|
+
>
|
|
190
|
+
<span className="flex items-center gap-1">
|
|
191
|
+
{col.header}
|
|
192
|
+
{col.sortable && sortKey === String(col.key) && (
|
|
193
|
+
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
194
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
195
|
+
d={sortDir === "asc" ? "M5 15l7-7 7 7" : "M19 9l-7 7-7-7"}
|
|
196
|
+
/>
|
|
197
|
+
</svg>
|
|
198
|
+
)}
|
|
199
|
+
</span>
|
|
200
|
+
</th>
|
|
201
|
+
))}
|
|
202
|
+
</tr>
|
|
203
|
+
</thead>
|
|
204
|
+
<tbody>
|
|
205
|
+
{loading ? (
|
|
206
|
+
<tr>
|
|
207
|
+
<td colSpan={columns.length} className="h-24 text-center">
|
|
208
|
+
<svg className="mx-auto h-5 w-5 animate-spin text-muted-foreground" fill="none" viewBox="0 0 24 24">
|
|
209
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
210
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
211
|
+
</svg>
|
|
212
|
+
</td>
|
|
213
|
+
</tr>
|
|
214
|
+
) : data.length === 0 ? (
|
|
215
|
+
<tr>
|
|
216
|
+
<td colSpan={columns.length} className="h-24 text-center text-muted-foreground">{emptyText}</td>
|
|
217
|
+
</tr>
|
|
218
|
+
) : (
|
|
219
|
+
data.map((row, i) => (
|
|
220
|
+
<tr
|
|
221
|
+
key={i}
|
|
222
|
+
className={cn(
|
|
223
|
+
"border-b transition-colors hover:bg-muted/50",
|
|
224
|
+
striped && i % 2 !== 0 && "bg-muted/20"
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{columns.map((col, j) => (
|
|
228
|
+
<td
|
|
229
|
+
key={j}
|
|
230
|
+
className={cn(
|
|
231
|
+
"p-4 align-middle",
|
|
232
|
+
col.align === "center" && "text-center",
|
|
233
|
+
col.align === "right" && "text-right",
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
{col.cell
|
|
237
|
+
? col.cell(row, i)
|
|
238
|
+
: String(row[col.key as keyof T] ?? "")}
|
|
239
|
+
</td>
|
|
240
|
+
))}
|
|
241
|
+
</tr>
|
|
242
|
+
))
|
|
243
|
+
)}
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
DataTable.displayName = "DataTable";
|
|
250
|
+
|
|
251
|
+
// ─── List & ListItem ─────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
export interface ListProps extends Omit<React.HTMLAttributes<HTMLUListElement>, "color"> {
|
|
254
|
+
variant?: "simple" | "bordered" | "divided";
|
|
255
|
+
spacing?: "none" | "sm" | "md" | "lg";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const List = React.forwardRef<HTMLUListElement, ListProps>(
|
|
259
|
+
({ className, variant = "simple", spacing = "none", ...props }, ref) => (
|
|
260
|
+
<ul
|
|
261
|
+
ref={ref}
|
|
262
|
+
className={cn(
|
|
263
|
+
"atlas-list w-full",
|
|
264
|
+
variant === "bordered" && "rounded-md border border-border divide-y divide-border",
|
|
265
|
+
variant === "divided" && "divide-y divide-border",
|
|
266
|
+
spacing === "sm" && "space-y-1",
|
|
267
|
+
spacing === "md" && "space-y-2",
|
|
268
|
+
spacing === "lg" && "space-y-3",
|
|
269
|
+
className
|
|
270
|
+
)}
|
|
271
|
+
{...props}
|
|
272
|
+
/>
|
|
273
|
+
)
|
|
274
|
+
);
|
|
275
|
+
List.displayName = "List";
|
|
276
|
+
|
|
277
|
+
export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
|
|
278
|
+
icon?: React.ReactNode;
|
|
279
|
+
extra?: React.ReactNode;
|
|
280
|
+
active?: boolean;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const ListItem = React.forwardRef<HTMLLIElement, ListItemProps>(
|
|
284
|
+
({ className, icon, extra, active, children, ...props }, ref) => (
|
|
285
|
+
<li
|
|
286
|
+
ref={ref}
|
|
287
|
+
className={cn(
|
|
288
|
+
"atlas-list-item flex items-center gap-3 px-4 py-3",
|
|
289
|
+
active && "bg-accent text-accent-foreground",
|
|
290
|
+
className
|
|
291
|
+
)}
|
|
292
|
+
{...props}
|
|
293
|
+
>
|
|
294
|
+
{icon && <span className="shrink-0 text-muted-foreground [&>svg]:h-4 [&>svg]:w-4" aria-hidden="true">{icon}</span>}
|
|
295
|
+
<span className="flex-1 min-w-0">{children}</span>
|
|
296
|
+
{extra && <span className="shrink-0">{extra}</span>}
|
|
297
|
+
</li>
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
ListItem.displayName = "ListItem";
|
|
301
|
+
|
|
302
|
+
// ─── Statistic ────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
export interface StatisticProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "prefix"> {
|
|
305
|
+
label: React.ReactNode;
|
|
306
|
+
value: React.ReactNode;
|
|
307
|
+
prefix?: React.ReactNode;
|
|
308
|
+
suffix?: React.ReactNode;
|
|
309
|
+
trend?: { value: number; label?: string };
|
|
310
|
+
loading?: boolean;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const Statistic = React.forwardRef<HTMLDivElement, StatisticProps>(
|
|
314
|
+
({ className, label, value, prefix, suffix, trend, loading, ...props }, ref) => (
|
|
315
|
+
<div ref={ref} className={cn("atlas-statistic", className)} {...props}>
|
|
316
|
+
<p className="text-sm font-medium text-muted-foreground">{label}</p>
|
|
317
|
+
<div className="mt-1 flex items-end gap-2">
|
|
318
|
+
<span className="text-3xl font-bold tracking-tight">
|
|
319
|
+
{loading ? (
|
|
320
|
+
<span className="inline-block h-8 w-24 animate-pulse rounded bg-muted" />
|
|
321
|
+
) : (
|
|
322
|
+
<>{prefix}{value}{suffix}</>
|
|
323
|
+
)}
|
|
324
|
+
</span>
|
|
325
|
+
{trend && !loading && (
|
|
326
|
+
<span className={cn(
|
|
327
|
+
"mb-1 flex items-center gap-0.5 text-sm font-medium",
|
|
328
|
+
trend.value > 0 ? "text-success" : trend.value < 0 ? "text-destructive" : "text-muted-foreground"
|
|
329
|
+
)}>
|
|
330
|
+
{trend.value !== 0 && (
|
|
331
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
332
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
333
|
+
d={trend.value > 0 ? "M7 17l9-9M7 7h10v10" : "M7 7l9 9M17 7H7v10"}
|
|
334
|
+
/>
|
|
335
|
+
</svg>
|
|
336
|
+
)}
|
|
337
|
+
{Math.abs(trend.value)}%
|
|
338
|
+
{trend.label && <span className="font-normal text-muted-foreground ml-1">{trend.label}</span>}
|
|
339
|
+
</span>
|
|
340
|
+
)}
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
)
|
|
344
|
+
);
|
|
345
|
+
Statistic.displayName = "Statistic";
|
|
346
|
+
|
|
347
|
+
// ─── Timeline ─────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
export interface TimelineEvent {
|
|
350
|
+
title: React.ReactNode;
|
|
351
|
+
description?: React.ReactNode;
|
|
352
|
+
time?: React.ReactNode;
|
|
353
|
+
icon?: React.ReactNode;
|
|
354
|
+
color?: "default" | "primary" | "success" | "warning" | "danger";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface TimelineProps extends React.HTMLAttributes<HTMLOListElement> {
|
|
358
|
+
events: TimelineEvent[];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const Timeline = React.forwardRef<HTMLOListElement, TimelineProps>(
|
|
362
|
+
({ className, events, ...props }, ref) => (
|
|
363
|
+
<ol ref={ref} className={cn("atlas-timeline relative flex flex-col", className)} {...props}>
|
|
364
|
+
{events.map((event, i) => (
|
|
365
|
+
<li key={i} className="relative flex gap-4 pb-8 last:pb-0">
|
|
366
|
+
<div className="relative flex flex-col items-center">
|
|
367
|
+
<div className={cn(
|
|
368
|
+
"z-10 flex h-8 w-8 items-center justify-center rounded-full border-2 shrink-0",
|
|
369
|
+
"border-background shadow-sm [&>svg]:h-3.5 [&>svg]:w-3.5",
|
|
370
|
+
event.color === "primary" ? "bg-primary text-primary-foreground" :
|
|
371
|
+
event.color === "success" ? "bg-success text-success-foreground" :
|
|
372
|
+
event.color === "warning" ? "bg-warning text-warning-foreground" :
|
|
373
|
+
event.color === "danger" ? "bg-destructive text-destructive-foreground" :
|
|
374
|
+
"bg-muted text-muted-foreground"
|
|
375
|
+
)}>
|
|
376
|
+
{event.icon ?? <span className="h-2 w-2 rounded-full bg-current" />}
|
|
377
|
+
</div>
|
|
378
|
+
{i < events.length - 1 && (
|
|
379
|
+
<div className="mt-1 w-px flex-1 bg-border" aria-hidden="true" />
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
<div className="flex-1 pt-0.5 pb-4 last:pb-0">
|
|
383
|
+
<div className="flex items-start justify-between gap-2">
|
|
384
|
+
<p className="font-medium text-sm">{event.title}</p>
|
|
385
|
+
{event.time && <span className="text-xs text-muted-foreground whitespace-nowrap shrink-0">{event.time}</span>}
|
|
386
|
+
</div>
|
|
387
|
+
{event.description && <p className="mt-1 text-sm text-muted-foreground">{event.description}</p>}
|
|
388
|
+
</div>
|
|
389
|
+
</li>
|
|
390
|
+
))}
|
|
391
|
+
</ol>
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
Timeline.displayName = "Timeline";
|
|
395
|
+
|
|
396
|
+
// ─── Calendar ─────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
export interface CalendarProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
399
|
+
value?: Date;
|
|
400
|
+
onChange?: (date: Date) => void;
|
|
401
|
+
minDate?: Date;
|
|
402
|
+
maxDate?: Date;
|
|
403
|
+
highlightedDates?: Date[];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
407
|
+
const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
408
|
+
|
|
409
|
+
const Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(
|
|
410
|
+
({ className, value, onChange, minDate, maxDate, highlightedDates = [], ...props }, ref) => {
|
|
411
|
+
const today = new Date();
|
|
412
|
+
const [viewDate, setViewDate] = React.useState(value ?? today);
|
|
413
|
+
const year = viewDate.getFullYear();
|
|
414
|
+
const month = viewDate.getMonth();
|
|
415
|
+
|
|
416
|
+
const firstDay = new Date(year, month, 1).getDay();
|
|
417
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
418
|
+
|
|
419
|
+
const cells: (Date | null)[] = [
|
|
420
|
+
...Array.from({ length: firstDay }, (): null => null),
|
|
421
|
+
...Array.from({ length: daysInMonth }, (_, i) => new Date(year, month, i + 1)),
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
const isSameDay = (a: Date, b: Date) =>
|
|
425
|
+
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
426
|
+
|
|
427
|
+
return (
|
|
428
|
+
<div ref={ref} className={cn("atlas-calendar w-fit rounded-lg border border-border bg-background p-3 shadow-sm", className)} {...props}>
|
|
429
|
+
<div className="flex items-center justify-between mb-3">
|
|
430
|
+
<button
|
|
431
|
+
type="button"
|
|
432
|
+
onClick={() => setViewDate(new Date(year, month - 1))}
|
|
433
|
+
className="h-7 w-7 flex items-center justify-center rounded-md hover:bg-accent transition-colors"
|
|
434
|
+
aria-label="Previous month"
|
|
435
|
+
>
|
|
436
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
437
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
438
|
+
</svg>
|
|
439
|
+
</button>
|
|
440
|
+
<span className="text-sm font-semibold">{MONTHS[month]} {year}</span>
|
|
441
|
+
<button
|
|
442
|
+
type="button"
|
|
443
|
+
onClick={() => setViewDate(new Date(year, month + 1))}
|
|
444
|
+
className="h-7 w-7 flex items-center justify-center rounded-md hover:bg-accent transition-colors"
|
|
445
|
+
aria-label="Next month"
|
|
446
|
+
>
|
|
447
|
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
448
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
449
|
+
</svg>
|
|
450
|
+
</button>
|
|
451
|
+
</div>
|
|
452
|
+
<div className="grid grid-cols-7 gap-px">
|
|
453
|
+
{DAYS.map((d) => (
|
|
454
|
+
<div key={d} className="h-8 flex items-center justify-center text-xs font-medium text-muted-foreground">{d}</div>
|
|
455
|
+
))}
|
|
456
|
+
{cells.map((date, i) => {
|
|
457
|
+
if (!date) return <div key={`empty-${i}`} />;
|
|
458
|
+
const isSelected = value ? isSameDay(date, value) : false;
|
|
459
|
+
const isToday = isSameDay(date, today);
|
|
460
|
+
const isHighlighted = highlightedDates.some((d) => isSameDay(d, date));
|
|
461
|
+
const isDisabled =
|
|
462
|
+
(minDate && date < minDate) || (maxDate && date > maxDate);
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<button
|
|
466
|
+
key={date.toISOString()}
|
|
467
|
+
type="button"
|
|
468
|
+
disabled={isDisabled}
|
|
469
|
+
onClick={() => onChange?.(date)}
|
|
470
|
+
aria-label={date.toLocaleDateString()}
|
|
471
|
+
aria-pressed={isSelected}
|
|
472
|
+
className={cn(
|
|
473
|
+
"h-8 w-8 text-xs rounded-md flex items-center justify-center transition-colors font-medium relative",
|
|
474
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
475
|
+
"disabled:pointer-events-none disabled:opacity-30",
|
|
476
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
477
|
+
!isSelected && isToday && "border border-primary text-primary",
|
|
478
|
+
!isSelected && !isToday && "hover:bg-accent",
|
|
479
|
+
)}
|
|
480
|
+
>
|
|
481
|
+
{date.getDate()}
|
|
482
|
+
{isHighlighted && !isSelected && (
|
|
483
|
+
<span className="absolute bottom-1 left-1/2 -translate-x-1/2 h-1 w-1 rounded-full bg-primary" />
|
|
484
|
+
)}
|
|
485
|
+
</button>
|
|
486
|
+
);
|
|
487
|
+
})}
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
);
|
|
493
|
+
Calendar.displayName = "Calendar";
|
|
494
|
+
|
|
495
|
+
// ─── CodeBlock ────────────────────────────────────────────────────────────
|
|
496
|
+
|
|
497
|
+
export interface CodeBlockProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
498
|
+
code: string;
|
|
499
|
+
language?: string;
|
|
500
|
+
showLineNumbers?: boolean;
|
|
501
|
+
caption?: string;
|
|
502
|
+
onCopy?: () => void;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
|
|
506
|
+
({ className, code, language, showLineNumbers, caption, onCopy, ...props }, ref) => {
|
|
507
|
+
const [copied, setCopied] = React.useState(false);
|
|
508
|
+
|
|
509
|
+
const handleCopy = async () => {
|
|
510
|
+
await navigator.clipboard.writeText(code);
|
|
511
|
+
setCopied(true);
|
|
512
|
+
onCopy?.();
|
|
513
|
+
setTimeout(() => setCopied(false), 2000);
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
const lines = code.split("\n");
|
|
517
|
+
|
|
518
|
+
return (
|
|
519
|
+
<div ref={ref} className={cn("atlas-code-block relative rounded-lg border border-border bg-muted/50 overflow-hidden", className)} {...props}>
|
|
520
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/80">
|
|
521
|
+
{language && <span className="text-xs font-medium text-muted-foreground">{language}</span>}
|
|
522
|
+
{caption && <span className="text-xs text-muted-foreground">{caption}</span>}
|
|
523
|
+
<button
|
|
524
|
+
type="button"
|
|
525
|
+
onClick={handleCopy}
|
|
526
|
+
aria-label={copied ? "Copied!" : "Copy code"}
|
|
527
|
+
className="ml-auto flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:text-foreground hover:bg-background transition-colors"
|
|
528
|
+
>
|
|
529
|
+
{copied ? (
|
|
530
|
+
<>
|
|
531
|
+
<svg className="h-3.5 w-3.5 text-success" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
532
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
533
|
+
</svg>
|
|
534
|
+
Copied
|
|
535
|
+
</>
|
|
536
|
+
) : (
|
|
537
|
+
<>
|
|
538
|
+
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
539
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
|
540
|
+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
541
|
+
/>
|
|
542
|
+
</svg>
|
|
543
|
+
Copy
|
|
544
|
+
</>
|
|
545
|
+
)}
|
|
546
|
+
</button>
|
|
547
|
+
</div>
|
|
548
|
+
<pre className="overflow-x-auto p-4 text-sm">
|
|
549
|
+
<code className={`language-${language}`}>
|
|
550
|
+
{showLineNumbers
|
|
551
|
+
? lines.map((line, i) => (
|
|
552
|
+
<span key={i} className="block">
|
|
553
|
+
<span className="mr-4 select-none text-muted-foreground/50 text-xs w-5 inline-block text-right">{i + 1}</span>
|
|
554
|
+
{line}
|
|
555
|
+
</span>
|
|
556
|
+
))
|
|
557
|
+
: code}
|
|
558
|
+
</code>
|
|
559
|
+
</pre>
|
|
560
|
+
</div>
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
CodeBlock.displayName = "CodeBlock";
|
|
565
|
+
|
|
566
|
+
// ─── Chart (placeholder - integrates with recharts/chartjs) ───────────────
|
|
567
|
+
|
|
568
|
+
export interface ChartProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
569
|
+
title?: string;
|
|
570
|
+
description?: string;
|
|
571
|
+
loading?: boolean;
|
|
572
|
+
empty?: boolean;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const Chart = React.forwardRef<HTMLDivElement, ChartProps>(
|
|
576
|
+
({ className, title, description, loading, empty, children, ...props }, ref) => (
|
|
577
|
+
<div ref={ref} className={cn("atlas-chart", className)} {...props}>
|
|
578
|
+
{(title || description) && (
|
|
579
|
+
<div className="mb-4">
|
|
580
|
+
{title && <h3 className="text-base font-semibold">{title}</h3>}
|
|
581
|
+
{description && <p className="text-sm text-muted-foreground">{description}</p>}
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
{loading ? (
|
|
585
|
+
<div className="h-64 w-full animate-pulse rounded-lg bg-muted" />
|
|
586
|
+
) : empty ? (
|
|
587
|
+
<div className="h-64 w-full flex items-center justify-center text-muted-foreground text-sm border border-dashed border-border rounded-lg">
|
|
588
|
+
No chart data available
|
|
589
|
+
</div>
|
|
590
|
+
) : (
|
|
591
|
+
children
|
|
592
|
+
)}
|
|
593
|
+
</div>
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
Chart.displayName = "Chart";
|
|
597
|
+
|
|
598
|
+
export {
|
|
599
|
+
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
|
|
600
|
+
Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption,
|
|
601
|
+
DataTable,
|
|
602
|
+
List, ListItem,
|
|
603
|
+
Statistic,
|
|
604
|
+
Timeline,
|
|
605
|
+
Calendar,
|
|
606
|
+
CodeBlock,
|
|
607
|
+
Chart,
|
|
608
|
+
};
|