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,1158 @@
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
+
599
+ // ═══════════════════════════════════════════════════════════════
600
+ // New in v0.1.2
601
+ // ═══════════════════════════════════════════════════════════════
602
+
603
+
604
+ // ─── StatsCard ────────────────────────────────────────────────────────────
605
+
606
+ export interface StatsCardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
607
+ title: React.ReactNode;
608
+ value: React.ReactNode;
609
+ icon?: React.ReactNode;
610
+ trend?: { value: number; label?: string };
611
+ description?: React.ReactNode;
612
+ loading?: boolean;
613
+ }
614
+
615
+ const StatsCard = React.forwardRef<HTMLDivElement, StatsCardProps>(
616
+ ({ className, title, value, icon, trend, description, loading, ...props }, ref) => (
617
+ <div
618
+ ref={ref}
619
+ className={cn(
620
+ "atlas-stats-card rounded-xl border border-border bg-card p-6 shadow-sm",
621
+ className
622
+ )}
623
+ {...props}
624
+ >
625
+ <div className="flex items-start justify-between gap-4">
626
+ <div className="flex-1 min-w-0">
627
+ <p className="text-sm font-medium text-muted-foreground truncate">{title}</p>
628
+ <p className="mt-1 text-3xl font-bold tracking-tight">
629
+ {loading ? (
630
+ <span className="inline-block h-8 w-28 animate-pulse rounded bg-muted" />
631
+ ) : value}
632
+ </p>
633
+ {trend && !loading && (
634
+ <p className={cn(
635
+ "mt-1 flex items-center gap-1 text-sm font-medium",
636
+ trend.value > 0 ? "text-success" : trend.value < 0 ? "text-destructive" : "text-muted-foreground"
637
+ )}>
638
+ <svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
639
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
640
+ d={trend.value > 0 ? "M7 17l9-9M7 7h10v10" : trend.value < 0 ? "M17 7l-9 9M7 7v10h10" : "M5 12h14"}
641
+ />
642
+ </svg>
643
+ {Math.abs(trend.value)}%
644
+ {trend.label && <span className="font-normal text-muted-foreground">{trend.label}</span>}
645
+ </p>
646
+ )}
647
+ {description && !loading && (
648
+ <p className="mt-1 text-xs text-muted-foreground">{description}</p>
649
+ )}
650
+ </div>
651
+ {icon && (
652
+ <div className={cn(
653
+ "flex h-12 w-12 shrink-0 items-center justify-center rounded-lg",
654
+ "bg-primary/10 text-primary [&>svg]:h-6 [&>svg]:w-6"
655
+ )}>
656
+ {icon}
657
+ </div>
658
+ )}
659
+ </div>
660
+ </div>
661
+ )
662
+ );
663
+ StatsCard.displayName = "StatsCard";
664
+
665
+ // ─── TreeView ─────────────────────────────────────────────────────────────
666
+
667
+ export interface TreeNode {
668
+ id: string;
669
+ label: React.ReactNode;
670
+ icon?: React.ReactNode;
671
+ children?: TreeNode[];
672
+ disabled?: boolean;
673
+ }
674
+
675
+ export interface TreeViewProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect" | "size"> {
676
+ nodes: TreeNode[];
677
+ selected?: string;
678
+ onSelect?: (id: string) => void;
679
+ defaultExpanded?: string[];
680
+ size?: "sm" | "md";
681
+ }
682
+
683
+ interface TreeItemProps {
684
+ node: TreeNode;
685
+ depth: number;
686
+ selected?: string;
687
+ onSelect?: (id: string) => void;
688
+ expanded: Set<string>;
689
+ onToggle: (id: string) => void;
690
+ size: "sm" | "md";
691
+ }
692
+
693
+ const TreeItem = ({ node, depth, selected, onSelect, expanded, onToggle, size }: TreeItemProps) => {
694
+ const hasChildren = node.children && node.children.length > 0;
695
+ const isExpanded = expanded.has(node.id);
696
+ const isSelected = selected === node.id;
697
+
698
+ return (
699
+ <li role="treeitem" aria-selected={isSelected} aria-expanded={hasChildren ? isExpanded : undefined}>
700
+ <button
701
+ type="button"
702
+ disabled={node.disabled}
703
+ onClick={() => {
704
+ if (hasChildren) onToggle(node.id);
705
+ if (!node.disabled) onSelect?.(node.id);
706
+ }}
707
+ style={{ paddingLeft: `${depth * 16 + 8}px` }}
708
+ className={cn(
709
+ "flex w-full items-center gap-2 rounded-md pr-3 text-left transition-colors",
710
+ size === "sm" ? "py-1 text-xs" : "py-1.5 text-sm",
711
+ isSelected
712
+ ? "bg-accent text-accent-foreground font-medium"
713
+ : "hover:bg-accent/50 text-foreground",
714
+ node.disabled && "opacity-50 cursor-not-allowed pointer-events-none"
715
+ )}
716
+ >
717
+ <span className="flex h-4 w-4 shrink-0 items-center justify-center">
718
+ {hasChildren ? (
719
+ <svg
720
+ className={cn("h-3.5 w-3.5 transition-transform", isExpanded && "rotate-90")}
721
+ fill="none" stroke="currentColor" viewBox="0 0 24 24"
722
+ >
723
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
724
+ </svg>
725
+ ) : (
726
+ <span className="h-1 w-1 rounded-full bg-muted-foreground/40" />
727
+ )}
728
+ </span>
729
+ {node.icon && (
730
+ <span className="shrink-0 [&>svg]:h-4 [&>svg]:w-4 text-muted-foreground" aria-hidden="true">
731
+ {node.icon}
732
+ </span>
733
+ )}
734
+ <span className="flex-1 truncate">{node.label}</span>
735
+ </button>
736
+ {hasChildren && isExpanded && (
737
+ <ul role="group">
738
+ {node.children!.map((child) => (
739
+ <TreeItem
740
+ key={child.id}
741
+ node={child}
742
+ depth={depth + 1}
743
+ selected={selected}
744
+ onSelect={onSelect}
745
+ expanded={expanded}
746
+ onToggle={onToggle}
747
+ size={size}
748
+ />
749
+ ))}
750
+ </ul>
751
+ )}
752
+ </li>
753
+ );
754
+ };
755
+
756
+ const TreeView = React.forwardRef<HTMLDivElement, TreeViewProps>(
757
+ ({ className, nodes, selected, onSelect, defaultExpanded = [], size = "md", ...props }, ref) => {
758
+ const [expanded, setExpanded] = React.useState<Set<string>>(new Set(defaultExpanded));
759
+
760
+ const toggle = (id: string) => {
761
+ setExpanded((prev) => {
762
+ const next = new Set(prev);
763
+ if (next.has(id)) next.delete(id);
764
+ else next.add(id);
765
+ return next;
766
+ });
767
+ };
768
+
769
+ return (
770
+ <div ref={ref} className={cn("atlas-tree-view", className)} {...props}>
771
+ <ul role="tree" className="space-y-0.5">
772
+ {nodes.map((node) => (
773
+ <TreeItem
774
+ key={node.id}
775
+ node={node}
776
+ depth={0}
777
+ selected={selected}
778
+ onSelect={onSelect}
779
+ expanded={expanded}
780
+ onToggle={toggle}
781
+ size={size}
782
+ />
783
+ ))}
784
+ </ul>
785
+ </div>
786
+ );
787
+ }
788
+ );
789
+ TreeView.displayName = "TreeView";
790
+
791
+ // ─── JsonViewer ───────────────────────────────────────────────────────────
792
+
793
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
794
+
795
+ export interface JsonViewerProps extends React.HTMLAttributes<HTMLDivElement> {
796
+ data: JsonValue;
797
+ defaultExpanded?: boolean;
798
+ maxDepth?: number;
799
+ indent?: number;
800
+ }
801
+
802
+ interface JsonNodeProps {
803
+ value: JsonValue;
804
+ keyName?: string;
805
+ depth: number;
806
+ maxDepth: number;
807
+ indent: number;
808
+ defaultExpanded: boolean;
809
+ }
810
+
811
+ const JsonNode = ({ value, keyName, depth, maxDepth, indent, defaultExpanded }: JsonNodeProps) => {
812
+ const [open, setOpen] = React.useState(defaultExpanded || depth < 2);
813
+ const isObject = typeof value === "object" && value !== null && !Array.isArray(value);
814
+ const isArray = Array.isArray(value);
815
+ const isCollapsible = isObject || isArray;
816
+
817
+ const renderValue = () => {
818
+ if (value === null) return <span className="text-muted-foreground">null</span>;
819
+ if (typeof value === "boolean") return <span className="text-blue-500">{String(value)}</span>;
820
+ if (typeof value === "number") return <span className="text-amber-500">{value}</span>;
821
+ if (typeof value === "string") return <span className="text-success">"{value}"</span>;
822
+ return null;
823
+ };
824
+
825
+ const entries = isArray
826
+ ? value.map((v, i) => [String(i), v] as [string, JsonValue])
827
+ : isObject
828
+ ? Object.entries(value as { [key: string]: JsonValue })
829
+ : [];
830
+
831
+ const bracket = isArray ? ["[", "]"] : ["{", "}"];
832
+ const shouldTruncate = depth >= maxDepth;
833
+
834
+ return (
835
+ <span className="block" style={{ paddingLeft: depth > 0 ? `${indent * 12}px` : 0 }}>
836
+ {keyName !== undefined && (
837
+ <span className="text-foreground font-medium">"{keyName}": </span>
838
+ )}
839
+ {!isCollapsible && renderValue()}
840
+ {isCollapsible && (
841
+ <>
842
+ <button
843
+ type="button"
844
+ onClick={() => setOpen(!open)}
845
+ className="inline-flex items-center gap-0.5 font-mono text-foreground hover:text-primary transition-colors"
846
+ aria-expanded={open}
847
+ >
848
+ <svg className={cn("h-3 w-3 transition-transform", open && "rotate-90")} fill="none" stroke="currentColor" viewBox="0 0 24 24">
849
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
850
+ </svg>
851
+ <span>{bracket[0]}</span>
852
+ {!open && (
853
+ <span className="text-muted-foreground text-xs">
854
+ {isArray ? `${value.length} items` : `${entries.length} keys`}
855
+ </span>
856
+ )}
857
+ {!open && <span>{bracket[1]}</span>}
858
+ </button>
859
+ {open && !shouldTruncate && (
860
+ <span className="block">
861
+ {entries.map(([k, v]) => (
862
+ <JsonNode
863
+ key={k}
864
+ keyName={isObject ? k : undefined}
865
+ value={v}
866
+ depth={depth + 1}
867
+ maxDepth={maxDepth}
868
+ indent={indent}
869
+ defaultExpanded={defaultExpanded}
870
+ />
871
+ ))}
872
+ <span style={{ paddingLeft: depth > 0 ? `${(depth) * 12}px` : 0 }} className="block font-mono">
873
+ {bracket[1]}
874
+ </span>
875
+ </span>
876
+ )}
877
+ {open && shouldTruncate && (
878
+ <span className="text-muted-foreground text-xs ml-1">...</span>
879
+ )}
880
+ </>
881
+ )}
882
+ </span>
883
+ );
884
+ };
885
+
886
+ const JsonViewer = React.forwardRef<HTMLDivElement, JsonViewerProps>(
887
+ ({ className, data, defaultExpanded = true, maxDepth = 10, indent = 2, ...props }, ref) => (
888
+ <div
889
+ ref={ref}
890
+ className={cn(
891
+ "atlas-json-viewer rounded-lg border border-border bg-muted/30 p-4 font-mono text-xs overflow-x-auto",
892
+ className
893
+ )}
894
+ {...props}
895
+ >
896
+ <JsonNode
897
+ value={data}
898
+ depth={0}
899
+ maxDepth={maxDepth}
900
+ indent={indent}
901
+ defaultExpanded={defaultExpanded}
902
+ />
903
+ </div>
904
+ )
905
+ );
906
+ JsonViewer.displayName = "JsonViewer";
907
+
908
+ // ─── Heatmap ──────────────────────────────────────────────────────────────
909
+
910
+ export interface HeatmapCell {
911
+ date: string;
912
+ value: number;
913
+ }
914
+
915
+ export interface HeatmapProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
916
+ data: HeatmapCell[];
917
+ title?: React.ReactNode;
918
+ colorScale?: [string, string];
919
+ weeks?: number;
920
+ showMonthLabels?: boolean;
921
+ tooltip?: (cell: HeatmapCell) => string;
922
+ }
923
+
924
+ const HEATMAP_DAYS = ["", "Mon", "", "Wed", "", "Fri", ""];
925
+
926
+ const Heatmap = React.forwardRef<HTMLDivElement, HeatmapProps>(
927
+ ({
928
+ className,
929
+ data,
930
+ title,
931
+ weeks = 52,
932
+ showMonthLabels = true,
933
+ tooltip,
934
+ ...props
935
+ }, ref) => {
936
+ const dataMap = React.useMemo(() => {
937
+ const map = new Map<string, number>();
938
+ data.forEach((d) => map.set(d.date, d.value));
939
+ return map;
940
+ }, [data]);
941
+
942
+ const maxValue = Math.max(...data.map((d) => d.value), 1);
943
+
944
+ const getColor = (value: number) => {
945
+ if (value === 0) return "bg-muted";
946
+ const intensity = value / maxValue;
947
+ if (intensity < 0.25) return "bg-success/25";
948
+ if (intensity < 0.5) return "bg-success/50";
949
+ if (intensity < 0.75) return "bg-success/75";
950
+ return "bg-success";
951
+ };
952
+
953
+ const today = new Date();
954
+ const startDate = new Date(today);
955
+ startDate.setDate(today.getDate() - weeks * 7);
956
+
957
+ const grid: (HeatmapCell | null)[][] = Array.from({ length: 7 }, () => []);
958
+ const current = new Date(startDate);
959
+ current.setDate(current.getDate() - current.getDay());
960
+
961
+ for (let w = 0; w < weeks; w++) {
962
+ for (let d = 0; d < 7; d++) {
963
+ const dateStr = current.toISOString().split("T")[0];
964
+ if (current >= startDate && current <= today) {
965
+ grid[d].push({ date: dateStr, value: dataMap.get(dateStr) ?? 0 });
966
+ } else {
967
+ grid[d].push(null);
968
+ }
969
+ current.setDate(current.getDate() + 1);
970
+ }
971
+ }
972
+
973
+ return (
974
+ <div ref={ref} className={cn("atlas-heatmap", className)} {...props}>
975
+ {title && <p className="mb-3 text-sm font-medium">{title}</p>}
976
+ <div className="flex gap-1">
977
+ <div className="flex flex-col gap-1 mr-1">
978
+ {HEATMAP_DAYS.map((d, i) => (
979
+ <div key={i} className="h-3 w-6 flex items-center">
980
+ <span className="text-[9px] text-muted-foreground">{d}</span>
981
+ </div>
982
+ ))}
983
+ </div>
984
+ <div className="flex gap-1 overflow-x-auto">
985
+ {Array.from({ length: weeks }, (_, w) => (
986
+ <div key={w} className="flex flex-col gap-1">
987
+ {Array.from({ length: 7 }, (_, d) => {
988
+ const cell = grid[d][w];
989
+ if (!cell) return <div key={d} className="h-3 w-3 rounded-sm opacity-0" />;
990
+ return (
991
+ <div
992
+ key={d}
993
+ title={tooltip ? tooltip(cell) : `${cell.date}: ${cell.value}`}
994
+ className={cn("h-3 w-3 rounded-sm transition-opacity hover:opacity-80", getColor(cell.value))}
995
+ role="gridcell"
996
+ aria-label={`${cell.date}: ${cell.value}`}
997
+ />
998
+ );
999
+ })}
1000
+ </div>
1001
+ ))}
1002
+ </div>
1003
+ </div>
1004
+ <div className="mt-2 flex items-center gap-1 justify-end">
1005
+ <span className="text-[10px] text-muted-foreground mr-1">Less</span>
1006
+ {["bg-muted", "bg-success/25", "bg-success/50", "bg-success/75", "bg-success"].map((c, i) => (
1007
+ <div key={i} className={cn("h-3 w-3 rounded-sm", c)} />
1008
+ ))}
1009
+ <span className="text-[10px] text-muted-foreground ml-1">More</span>
1010
+ </div>
1011
+ </div>
1012
+ );
1013
+ }
1014
+ );
1015
+ Heatmap.displayName = "Heatmap";
1016
+
1017
+ // ─── KanbanBoard ──────────────────────────────────────────────────────────
1018
+
1019
+ export interface KanbanCard {
1020
+ id: string;
1021
+ title: string;
1022
+ description?: string;
1023
+ tags?: string[];
1024
+ assignee?: React.ReactNode;
1025
+ }
1026
+
1027
+ export interface KanbanColumn {
1028
+ id: string;
1029
+ title: string;
1030
+ cards: KanbanCard[];
1031
+ color?: string;
1032
+ limit?: number;
1033
+ }
1034
+
1035
+ export interface KanbanBoardProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
1036
+ columns: KanbanColumn[];
1037
+ onChange?: (columns: KanbanColumn[]) => void;
1038
+ renderCard?: (card: KanbanCard, columnId: string) => React.ReactNode;
1039
+ }
1040
+
1041
+ const KanbanBoard = React.forwardRef<HTMLDivElement, KanbanBoardProps>(
1042
+ ({ className, columns, onChange, renderCard, ...props }, ref) => {
1043
+ const [dragging, setDragging] = React.useState<{ cardId: string; fromCol: string } | null>(null);
1044
+ const [dragOver, setDragOver] = React.useState<string | null>(null);
1045
+
1046
+ const handleDragStart = (cardId: string, colId: string) => {
1047
+ setDragging({ cardId, fromCol: colId });
1048
+ };
1049
+
1050
+ const handleDrop = (toColId: string) => {
1051
+ if (!dragging || dragging.fromCol === toColId) {
1052
+ setDragging(null);
1053
+ setDragOver(null);
1054
+ return;
1055
+ }
1056
+ const updated = columns.map((col) => {
1057
+ if (col.id === dragging.fromCol) {
1058
+ return { ...col, cards: col.cards.filter((c) => c.id !== dragging.cardId) };
1059
+ }
1060
+ if (col.id === toColId) {
1061
+ const card = columns
1062
+ .find((c) => c.id === dragging.fromCol)
1063
+ ?.cards.find((c) => c.id === dragging.cardId);
1064
+ if (card) return { ...col, cards: [...col.cards, card] };
1065
+ }
1066
+ return col;
1067
+ });
1068
+ onChange?.(updated);
1069
+ setDragging(null);
1070
+ setDragOver(null);
1071
+ };
1072
+
1073
+ return (
1074
+ <div
1075
+ ref={ref}
1076
+ className={cn("atlas-kanban flex gap-4 overflow-x-auto pb-4", className)}
1077
+ {...props}
1078
+ >
1079
+ {columns.map((col) => (
1080
+ <div
1081
+ key={col.id}
1082
+ onDragOver={(e) => { e.preventDefault(); setDragOver(col.id); }}
1083
+ onDrop={() => handleDrop(col.id)}
1084
+ onDragLeave={() => setDragOver(null)}
1085
+ className={cn(
1086
+ "flex flex-col gap-2 rounded-xl border border-border bg-muted/50 p-3 w-72 shrink-0 min-h-[200px]",
1087
+ "transition-colors",
1088
+ dragOver === col.id && "border-primary bg-primary/5"
1089
+ )}
1090
+ >
1091
+ <div className="flex items-center justify-between px-1 mb-1">
1092
+ <div className="flex items-center gap-2">
1093
+ {col.color && (
1094
+ <span className="h-2 w-2 rounded-full shrink-0" style={{ backgroundColor: col.color }} />
1095
+ )}
1096
+ <h3 className="text-sm font-semibold">{col.title}</h3>
1097
+ <span className="text-xs text-muted-foreground bg-background rounded-full px-1.5 py-0.5 border border-border">
1098
+ {col.cards.length}{col.limit ? `/${col.limit}` : ""}
1099
+ </span>
1100
+ </div>
1101
+ </div>
1102
+ <div className="flex flex-col gap-2">
1103
+ {col.cards.map((card) => (
1104
+ <div
1105
+ key={card.id}
1106
+ draggable
1107
+ onDragStart={() => handleDragStart(card.id, col.id)}
1108
+ className={cn(
1109
+ "rounded-lg border border-border bg-background p-3 shadow-sm cursor-grab active:cursor-grabbing",
1110
+ "transition-opacity hover:shadow-md",
1111
+ dragging?.cardId === card.id && "opacity-40"
1112
+ )}
1113
+ >
1114
+ {renderCard ? renderCard(card, col.id) : (
1115
+ <>
1116
+ <p className="text-sm font-medium leading-snug">{card.title}</p>
1117
+ {card.description && (
1118
+ <p className="mt-1 text-xs text-muted-foreground line-clamp-2">{card.description}</p>
1119
+ )}
1120
+ {(card.tags?.length || card.assignee) && (
1121
+ <div className="mt-2 flex items-center justify-between gap-2">
1122
+ <div className="flex flex-wrap gap-1">
1123
+ {card.tags?.map((tag) => (
1124
+ <span key={tag} className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium">
1125
+ {tag}
1126
+ </span>
1127
+ ))}
1128
+ </div>
1129
+ {card.assignee && <div className="shrink-0">{card.assignee}</div>}
1130
+ </div>
1131
+ )}
1132
+ </>
1133
+ )}
1134
+ </div>
1135
+ ))}
1136
+ </div>
1137
+ </div>
1138
+ ))}
1139
+ </div>
1140
+ );
1141
+ }
1142
+ );
1143
+ KanbanBoard.displayName = "KanbanBoard";
1144
+
1145
+
1146
+ export {
1147
+
1148
+ Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
1149
+ Table, TableHeader, TableBody, TableRow, TableHead, TableCell, TableCaption,
1150
+ DataTable,
1151
+ List, ListItem,
1152
+ Statistic,
1153
+ Timeline,
1154
+ Calendar,
1155
+ CodeBlock,
1156
+ Chart,
1157
+ StatsCard, TreeView, JsonViewer, Heatmap, KanbanBoard
1158
+ };