@moontra/moonui 2.3.4 → 2.3.6

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.
@@ -1,623 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import {
5
- ColumnDef,
6
- ColumnFiltersState,
7
- FilterFn,
8
- SortingState,
9
- VisibilityState,
10
- flexRender,
11
- getCoreRowModel,
12
- getFacetedRowModel,
13
- getFacetedUniqueValues,
14
- getFilteredRowModel,
15
- getPaginationRowModel,
16
- getSortedRowModel,
17
- useReactTable,
18
- } from "@tanstack/react-table";
19
- import { cva, type VariantProps } from "class-variance-authority";
20
- import { cn } from "../../lib/utils";
21
- import { Button } from "./button";
22
- import { Input } from "./input";
23
- import {
24
- Table,
25
- TableBody,
26
- TableCell,
27
- TableHead,
28
- TableHeader,
29
- TableRow
30
- } from "./table";
31
- import {
32
- Select,
33
- SelectContent,
34
- SelectItem,
35
- SelectTrigger,
36
- SelectValue,
37
- } from "./select";
38
- import {
39
- DropdownMenu,
40
- DropdownMenuCheckboxItem,
41
- DropdownMenuContent,
42
- DropdownMenuLabel,
43
- DropdownMenuSeparator,
44
- DropdownMenuTrigger,
45
- } from "./dropdown-menu";
46
- import {
47
- ChevronLeft,
48
- ChevronRight,
49
- ChevronsLeft,
50
- ChevronsRight,
51
- ArrowUpDown,
52
- ArrowUp,
53
- ArrowDown,
54
- Search,
55
- Filter,
56
- Columns,
57
- Download,
58
- RefreshCw,
59
- Settings,
60
- X
61
- } from "lucide-react";
62
- import { Badge } from "./badge";
63
- import { Skeleton } from "./skeleton";
64
- import { motion, AnimatePresence } from "framer-motion";
65
-
66
- /**
67
- * Premium DataTable Component
68
- *
69
- * A powerful data table with sorting, filtering, pagination, and more.
70
- * Built on top of @tanstack/react-table v8.
71
- */
72
-
73
- const dataTableVariants = cva(
74
- "w-full",
75
- {
76
- variants: {
77
- variant: {
78
- default: "",
79
- bordered: "border rounded-lg",
80
- striped: "[&_tbody_tr:nth-child(even)]:bg-muted/50",
81
- cards: "gap-4 [&_tbody]:flex [&_tbody]:flex-col [&_tbody_tr]:rounded-lg [&_tbody_tr]:border [&_tbody_tr]:p-4",
82
- },
83
- size: {
84
- default: "",
85
- sm: "text-sm [&_th]:py-2 [&_td]:py-2",
86
- lg: "text-base [&_th]:py-4 [&_td]:py-4",
87
- },
88
- density: {
89
- default: "",
90
- compact: "[&_th]:py-1 [&_td]:py-1",
91
- comfortable: "[&_th]:py-6 [&_td]:py-6",
92
- },
93
- },
94
- defaultVariants: {
95
- variant: "default",
96
- size: "default",
97
- density: "default",
98
- },
99
- }
100
- );
101
-
102
- interface DataTableProps<TData, TValue>
103
- extends VariantProps<typeof dataTableVariants> {
104
- /**
105
- * The table columns definition
106
- */
107
- columns: ColumnDef<TData, TValue>[];
108
- /**
109
- * The data to display
110
- */
111
- data: TData[];
112
- /**
113
- * Enable/disable filtering
114
- */
115
- enableFiltering?: boolean;
116
- /**
117
- * Enable/disable sorting
118
- */
119
- enableSorting?: boolean;
120
- /**
121
- * Enable/disable column visibility toggle
122
- */
123
- enableColumnVisibility?: boolean;
124
- /**
125
- * Enable/disable row selection
126
- */
127
- enableRowSelection?: boolean;
128
- /**
129
- * Enable/disable pagination
130
- */
131
- enablePagination?: boolean;
132
- /**
133
- * Rows per page options
134
- */
135
- pageSizeOptions?: number[];
136
- /**
137
- * Default page size
138
- */
139
- defaultPageSize?: number;
140
- /**
141
- * Filter placeholder text
142
- */
143
- filterPlaceholder?: string;
144
- /**
145
- * Filter column key
146
- */
147
- filterColumn?: string;
148
- /**
149
- * Show loading state
150
- */
151
- isLoading?: boolean;
152
- /**
153
- * Empty state message
154
- */
155
- emptyMessage?: string;
156
- /**
157
- * Table title
158
- */
159
- title?: string;
160
- /**
161
- * Table description
162
- */
163
- description?: string;
164
- /**
165
- * Enable export functionality
166
- */
167
- enableExport?: boolean;
168
- /**
169
- * Export handler
170
- */
171
- onExport?: (data: TData[]) => void;
172
- /**
173
- * Enable refresh functionality
174
- */
175
- enableRefresh?: boolean;
176
- /**
177
- * Refresh handler
178
- */
179
- onRefresh?: () => void;
180
- /**
181
- * Additional toolbar actions
182
- */
183
- toolbarActions?: React.ReactNode;
184
- /**
185
- * Custom filter component
186
- */
187
- customFilter?: React.ReactNode;
188
- /**
189
- * Row click handler
190
- */
191
- onRowClick?: (row: TData) => void;
192
- /**
193
- * Class name
194
- */
195
- className?: string;
196
- }
197
-
198
- function DataTableLoading() {
199
- return (
200
- <div className="space-y-4">
201
- <div className="flex items-center justify-between">
202
- <Skeleton className="h-8 w-[250px]" />
203
- <div className="flex gap-2">
204
- <Skeleton className="h-8 w-8" />
205
- <Skeleton className="h-8 w-8" />
206
- </div>
207
- </div>
208
- <div className="rounded-lg border">
209
- <div className="p-4">
210
- <div className="flex gap-4">
211
- {Array.from({ length: 4 }).map((_, i) => (
212
- <Skeleton key={i} className="h-8 flex-1" />
213
- ))}
214
- </div>
215
- </div>
216
- <div className="p-4 space-y-2">
217
- {Array.from({ length: 5 }).map((_, i) => (
218
- <Skeleton key={i} className="h-12 w-full" />
219
- ))}
220
- </div>
221
- </div>
222
- </div>
223
- );
224
- }
225
-
226
- export function DataTable<TData, TValue>({
227
- columns,
228
- data,
229
- enableFiltering = true,
230
- enableSorting = true,
231
- enableColumnVisibility = true,
232
- enableRowSelection = false,
233
- enablePagination = true,
234
- pageSizeOptions = [10, 20, 30, 50, 100],
235
- defaultPageSize = 10,
236
- filterPlaceholder = "Filter...",
237
- filterColumn,
238
- isLoading = false,
239
- emptyMessage = "No results found.",
240
- title,
241
- description,
242
- enableExport = false,
243
- onExport,
244
- enableRefresh = false,
245
- onRefresh,
246
- toolbarActions,
247
- customFilter,
248
- onRowClick,
249
- variant,
250
- size,
251
- density,
252
- className,
253
- }: DataTableProps<TData, TValue>) {
254
- const [sorting, setSorting] = React.useState<SortingState>([]);
255
- const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
256
- const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
257
- const [rowSelection, setRowSelection] = React.useState({});
258
- const [globalFilter, setGlobalFilter] = React.useState("");
259
-
260
- const table = useReactTable({
261
- data,
262
- columns,
263
- onSortingChange: enableSorting ? setSorting : undefined,
264
- onColumnFiltersChange: enableFiltering ? setColumnFilters : undefined,
265
- onColumnVisibilityChange: enableColumnVisibility ? setColumnVisibility : undefined,
266
- onRowSelectionChange: enableRowSelection ? setRowSelection : undefined,
267
- onGlobalFilterChange: enableFiltering && !filterColumn ? setGlobalFilter : undefined,
268
- getCoreRowModel: getCoreRowModel(),
269
- getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
270
- getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,
271
- getFilteredRowModel: enableFiltering ? getFilteredRowModel() : undefined,
272
- getFacetedRowModel: enableFiltering ? getFacetedRowModel() : undefined,
273
- getFacetedUniqueValues: enableFiltering ? getFacetedUniqueValues() : undefined,
274
- state: {
275
- sorting,
276
- columnFilters,
277
- columnVisibility,
278
- rowSelection,
279
- globalFilter,
280
- },
281
- initialState: {
282
- pagination: {
283
- pageSize: defaultPageSize,
284
- },
285
- },
286
- });
287
-
288
- if (isLoading) {
289
- return <DataTableLoading />;
290
- }
291
-
292
- const selectedRows = table.getFilteredSelectedRowModel().rows;
293
-
294
- return (
295
- <div className={cn("moonui-theme", "space-y-4", className)}>
296
- {/* Header */}
297
- {(title || description) && (
298
- <div className="space-y-1">
299
- {title && <h3 className="text-lg font-semibold">{title}</h3>}
300
- {description && <p className="text-sm text-muted-foreground">{description}</p>}
301
- </div>
302
- )}
303
-
304
- {/* Toolbar */}
305
- <div className="flex items-center justify-between gap-4">
306
- <div className="flex-1 max-w-sm">
307
- {/* Search/Filter */}
308
- {enableFiltering && (
309
- <>
310
- {customFilter || (
311
- <div className="relative">
312
- <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
313
- <Input
314
- placeholder={filterPlaceholder}
315
- value={
316
- filterColumn
317
- ? (table.getColumn(filterColumn)?.getFilterValue() as string) ?? ""
318
- : globalFilter
319
- }
320
- onChange={(event) => {
321
- const value = event.target.value;
322
- if (filterColumn) {
323
- table.getColumn(filterColumn)?.setFilterValue(value);
324
- } else {
325
- setGlobalFilter(value);
326
- }
327
- }}
328
- className="pl-8"
329
- />
330
- {((filterColumn && table.getColumn(filterColumn)?.getFilterValue()) ||
331
- (!filterColumn && globalFilter)) && (
332
- <Button
333
- variant="ghost"
334
- size="sm"
335
- className="absolute right-0 top-0 h-full px-2"
336
- onClick={() => {
337
- if (filterColumn) {
338
- table.getColumn(filterColumn)?.setFilterValue("");
339
- } else {
340
- setGlobalFilter("");
341
- }
342
- }}
343
- >
344
- <X className="h-4 w-4" />
345
- </Button>
346
- )}
347
- </div>
348
- )}
349
- </>
350
- )}
351
- </div>
352
-
353
- {/* Actions */}
354
- <div className="flex items-center gap-2">
355
- {/* Selection info */}
356
- {enableRowSelection && selectedRows.length > 0 && (
357
- <Badge variant="secondary" className="whitespace-nowrap">
358
- {selectedRows.length} selected
359
- </Badge>
360
- )}
361
-
362
- {toolbarActions}
363
-
364
- {/* Export */}
365
- {enableExport && onExport && (
366
- <Button
367
- variant="outline"
368
- size="sm"
369
- onClick={() => onExport(table.getFilteredRowModel().rows.map(row => row.original))}
370
- >
371
- <Download className="mr-2 h-4 w-4" />
372
- Export
373
- </Button>
374
- )}
375
-
376
- {/* Refresh */}
377
- {enableRefresh && onRefresh && (
378
- <Button
379
- variant="outline"
380
- size="icon"
381
- onClick={onRefresh}
382
- >
383
- <RefreshCw className="h-4 w-4" />
384
- </Button>
385
- )}
386
-
387
- {/* Column visibility */}
388
- {enableColumnVisibility && (
389
- <DropdownMenu>
390
- <DropdownMenuTrigger asChild>
391
- <Button variant="outline" size="icon">
392
- <Columns className="h-4 w-4" />
393
- </Button>
394
- </DropdownMenuTrigger>
395
- <DropdownMenuContent align="end" className="w-[180px]">
396
- <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
397
- <DropdownMenuSeparator />
398
- {table
399
- .getAllColumns()
400
- .filter((column) => column.getCanHide())
401
- .map((column) => {
402
- return (
403
- <DropdownMenuCheckboxItem
404
- key={column.id}
405
- className="capitalize"
406
- checked={column.getIsVisible()}
407
- onCheckedChange={(value) => column.toggleVisibility(!!value)}
408
- >
409
- {column.id}
410
- </DropdownMenuCheckboxItem>
411
- );
412
- })}
413
- </DropdownMenuContent>
414
- </DropdownMenu>
415
- )}
416
- </div>
417
- </div>
418
-
419
- {/* Table */}
420
- <div className={cn("rounded-lg", variant === "bordered" && "border")}>
421
- <Table>
422
- <TableHeader>
423
- {table.getHeaderGroups().map((headerGroup) => (
424
- <TableRow key={headerGroup.id}>
425
- {headerGroup.headers.map((header) => {
426
- return (
427
- <TableHead key={header.id}>
428
- {header.isPlaceholder ? null : (
429
- <div
430
- className={cn(
431
- "flex items-center gap-2",
432
- header.column.getCanSort() && "cursor-pointer select-none"
433
- )}
434
- onClick={header.column.getToggleSortingHandler()}
435
- >
436
- {flexRender(
437
- header.column.columnDef.header,
438
- header.getContext()
439
- )}
440
- {enableSorting && header.column.getCanSort() && (
441
- <div className="ml-auto">
442
- {header.column.getIsSorted() === "desc" ? (
443
- <ArrowDown className="h-4 w-4" />
444
- ) : header.column.getIsSorted() === "asc" ? (
445
- <ArrowUp className="h-4 w-4" />
446
- ) : (
447
- <ArrowUpDown className="h-4 w-4 opacity-50" />
448
- )}
449
- </div>
450
- )}
451
- </div>
452
- )}
453
- </TableHead>
454
- );
455
- })}
456
- </TableRow>
457
- ))}
458
- </TableHeader>
459
- <TableBody>
460
- {table.getRowModel().rows?.length ? (
461
- <AnimatePresence>
462
- {table.getRowModel().rows.map((row) => (
463
- <motion.tr
464
- key={row.id}
465
- initial={{ opacity: 0, y: -10 }}
466
- animate={{ opacity: 1, y: 0 }}
467
- exit={{ opacity: 0, y: 10 }}
468
- transition={{ duration: 0.2 }}
469
- data-state={row.getIsSelected() && "selected"}
470
- className={cn(
471
- "border-b transition-colors",
472
- onRowClick && "cursor-pointer hover:bg-muted/50",
473
- row.getIsSelected() && "bg-muted"
474
- )}
475
- onClick={() => onRowClick?.(row.original)}
476
- >
477
- {row.getVisibleCells().map((cell) => (
478
- <TableCell key={cell.id}>
479
- {flexRender(
480
- cell.column.columnDef.cell,
481
- cell.getContext()
482
- )}
483
- </TableCell>
484
- ))}
485
- </motion.tr>
486
- ))}
487
- </AnimatePresence>
488
- ) : (
489
- <TableRow>
490
- <TableCell
491
- colSpan={columns.length}
492
- className="h-24 text-center"
493
- >
494
- {emptyMessage}
495
- </TableCell>
496
- </TableRow>
497
- )}
498
- </TableBody>
499
- </Table>
500
- </div>
501
-
502
- {/* Pagination */}
503
- {enablePagination && (
504
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
505
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
506
- <span>
507
- {table.getFilteredRowModel().rows.length} of{" "}
508
- {table.getCoreRowModel().rows.length} row(s)
509
- </span>
510
- {enableRowSelection && selectedRows.length > 0 && (
511
- <span>• {selectedRows.length} selected</span>
512
- )}
513
- </div>
514
-
515
- <div className="flex items-center gap-2">
516
- <div className="flex items-center gap-2">
517
- <p className="text-sm">Rows per page</p>
518
- <Select
519
- value={`${table.getState().pagination.pageSize}`}
520
- onValueChange={(value) => {
521
- table.setPageSize(Number(value));
522
- }}
523
- >
524
- <SelectTrigger className="h-8 w-[70px]">
525
- <SelectValue placeholder={table.getState().pagination.pageSize} />
526
- </SelectTrigger>
527
- <SelectContent side="top">
528
- {pageSizeOptions.map((pageSize) => (
529
- <SelectItem key={pageSize} value={`${pageSize}`}>
530
- {pageSize}
531
- </SelectItem>
532
- ))}
533
- </SelectContent>
534
- </Select>
535
- </div>
536
-
537
- <div className="flex items-center gap-1">
538
- <Button
539
- variant="outline"
540
- size="icon"
541
- onClick={() => table.setPageIndex(0)}
542
- disabled={!table.getCanPreviousPage()}
543
- >
544
- <ChevronsLeft className="h-4 w-4" />
545
- </Button>
546
- <Button
547
- variant="outline"
548
- size="icon"
549
- onClick={() => table.previousPage()}
550
- disabled={!table.getCanPreviousPage()}
551
- >
552
- <ChevronLeft className="h-4 w-4" />
553
- </Button>
554
- <div className="flex items-center gap-1 text-sm">
555
- <span>Page</span>
556
- <span className="font-medium">
557
- {table.getState().pagination.pageIndex + 1} of{" "}
558
- {table.getPageCount()}
559
- </span>
560
- </div>
561
- <Button
562
- variant="outline"
563
- size="icon"
564
- onClick={() => table.nextPage()}
565
- disabled={!table.getCanNextPage()}
566
- >
567
- <ChevronRight className="h-4 w-4" />
568
- </Button>
569
- <Button
570
- variant="outline"
571
- size="icon"
572
- onClick={() => table.setPageIndex(table.getPageCount() - 1)}
573
- disabled={!table.getCanNextPage()}
574
- >
575
- <ChevronsRight className="h-4 w-4" />
576
- </Button>
577
- </div>
578
- </div>
579
- </div>
580
- )}
581
- </div>
582
- );
583
- }
584
-
585
- /**
586
- * Column Helper Functions
587
- */
588
-
589
- // Create a sortable header
590
- export function createSortableHeader(label: string) {
591
- return ({ column }: any) => (
592
- <Button
593
- variant="ghost"
594
- className="-ml-3 h-8 data-[state=open]:bg-accent"
595
- onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
596
- >
597
- {label}
598
- {column.getIsSorted() === "desc" ? (
599
- <ArrowDown className="ml-2 h-4 w-4" />
600
- ) : column.getIsSorted() === "asc" ? (
601
- <ArrowUp className="ml-2 h-4 w-4" />
602
- ) : (
603
- <ArrowUpDown className="ml-2 h-4 w-4" />
604
- )}
605
- </Button>
606
- );
607
- }
608
-
609
- // Create a filterable column
610
- export function createFilterableColumn<TData, TValue>(
611
- accessorKey: string,
612
- header: string,
613
- filterFn?: FilterFn<TData>
614
- ): ColumnDef<TData, TValue> {
615
- return {
616
- accessorKey,
617
- header: createSortableHeader(header),
618
- filterFn,
619
- };
620
- }
621
-
622
- // Export utilities
623
- export { type ColumnDef } from "@tanstack/react-table";