create-app-ui 1.0.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.
Files changed (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +117 -0
  3. package/boilerplate/README.md +18 -0
  4. package/boilerplate/react-base/.env.example +1 -0
  5. package/boilerplate/react-base/README.md +3 -0
  6. package/boilerplate/react-base/components.json +19 -0
  7. package/boilerplate/react-base/eslint.config.js +32 -0
  8. package/boilerplate/react-base/index.html +12 -0
  9. package/boilerplate/react-base/package.json +71 -0
  10. package/boilerplate/react-base/postcss.config.js +6 -0
  11. package/boilerplate/react-base/prettier.config.js +6 -0
  12. package/boilerplate/react-base/src/api/axios.ts +20 -0
  13. package/boilerplate/react-base/src/app/store.ts +13 -0
  14. package/boilerplate/react-base/src/components/data-table.tsx +919 -0
  15. package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
  16. package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
  17. package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
  18. package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
  19. package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
  20. package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
  21. package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
  22. package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
  23. package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
  24. package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
  25. package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
  26. package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
  27. package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
  28. package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
  29. package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
  30. package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
  31. package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
  32. package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
  33. package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
  34. package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
  35. package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
  36. package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
  37. package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
  38. package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
  39. package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
  40. package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
  41. package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
  42. package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
  43. package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
  44. package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
  45. package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
  46. package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
  47. package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
  48. package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
  49. package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
  50. package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
  51. package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
  52. package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
  53. package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
  54. package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
  55. package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
  56. package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
  57. package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
  58. package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
  59. package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
  60. package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
  61. package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
  62. package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
  63. package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
  64. package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
  65. package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
  66. package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
  67. package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
  68. package/boilerplate/react-base/src/config/constants.ts +3 -0
  69. package/boilerplate/react-base/src/config/theme.ts +432 -0
  70. package/boilerplate/react-base/src/config/user.ts +52 -0
  71. package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
  72. package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
  73. package/boilerplate/react-base/src/hooks/index.ts +1 -0
  74. package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
  75. package/boilerplate/react-base/src/lib/utils.ts +6 -0
  76. package/boilerplate/react-base/src/routes/index.tsx +7 -0
  77. package/boilerplate/react-base/src/styles/globals.css +15 -0
  78. package/boilerplate/react-base/src/vite-env.d.ts +31 -0
  79. package/boilerplate/react-base/tailwind.config.ts +75 -0
  80. package/boilerplate/react-base/tsconfig.app.json +20 -0
  81. package/boilerplate/react-base/tsconfig.json +7 -0
  82. package/boilerplate/react-base/tsconfig.node.json +16 -0
  83. package/boilerplate/react-base/vite.config.ts +12 -0
  84. package/dist/bin/index.js +8 -0
  85. package/dist/src/cli-args.js +52 -0
  86. package/dist/src/generator.js +85 -0
  87. package/dist/src/installer.js +7 -0
  88. package/dist/src/paths.js +61 -0
  89. package/dist/src/prompts.js +79 -0
  90. package/dist/src/replace-placeholders.js +22 -0
  91. package/dist/src/utils.js +16 -0
  92. package/package.json +63 -0
  93. package/templates/admin-portal/README.md +26 -0
  94. package/templates/admin-portal/src/App.tsx +85 -0
  95. package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
  96. package/templates/admin-portal/src/assets/brand-logo.png +0 -0
  97. package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
  98. package/templates/admin-portal/src/components/app-header.tsx +20 -0
  99. package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
  100. package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
  101. package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
  102. package/templates/admin-portal/src/components/data-table.tsx +919 -0
  103. package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
  104. package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
  105. package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
  106. package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
  107. package/templates/admin-portal/src/components/user-menu.tsx +163 -0
  108. package/templates/admin-portal/src/config/branding.ts +17 -0
  109. package/templates/admin-portal/src/config/chart-data.ts +44 -0
  110. package/templates/admin-portal/src/config/navigation.ts +42 -0
  111. package/templates/admin-portal/src/context/auth-context.tsx +32 -0
  112. package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
  113. package/templates/admin-portal/src/main.tsx +18 -0
  114. package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
  115. package/templates/admin-portal/src/pages/components.tsx +1368 -0
  116. package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
  117. package/templates/admin-portal/src/pages/login.tsx +81 -0
  118. package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
  119. package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
  120. package/templates/admin-portal/src/pages/signup.tsx +81 -0
  121. package/templates/admin-portal/src/pages/users.tsx +12 -0
  122. package/templates/admin-portal/tsconfig.json +10 -0
  123. package/templates/blank/README.md +15 -0
  124. package/templates/blank/src/App.tsx +5 -0
  125. package/templates/blank/src/main.tsx +15 -0
  126. package/templates/blank/src/pages/home.tsx +20 -0
  127. package/templates/blank/tsconfig.json +10 -0
  128. package/templates/tsconfig.overlay.base.json +7 -0
@@ -0,0 +1,919 @@
1
+ import {
2
+ type Column,
3
+ type ColumnDef,
4
+ type ColumnFiltersState,
5
+ type Row,
6
+ type SortingState,
7
+ type Table as TanstackTable,
8
+ type VisibilityState,
9
+ flexRender,
10
+ getCoreRowModel,
11
+ getFacetedRowModel,
12
+ getFacetedUniqueValues,
13
+ getFilteredRowModel,
14
+ getPaginationRowModel,
15
+ getSortedRowModel,
16
+ useReactTable,
17
+ } from "@tanstack/react-table";
18
+ import {
19
+ ArrowDown,
20
+ ArrowUp,
21
+ ArrowUpDown,
22
+ ChevronLeft,
23
+ ChevronRight,
24
+ ChevronsLeft,
25
+ ChevronsRight,
26
+ Download,
27
+ ListFilter,
28
+ RefreshCw,
29
+ Search,
30
+ SlidersHorizontal,
31
+ UserPlus,
32
+ X,
33
+ } from "lucide-react";
34
+ import * as React from "react";
35
+ import { Badge } from "@/components/ui/badge";
36
+ import { Button } from "@/components/ui/button";
37
+ import { Checkbox } from "@/components/ui/checkbox";
38
+ import {
39
+ DropdownMenu,
40
+ DropdownMenuCheckboxItem,
41
+ DropdownMenuContent,
42
+ DropdownMenuItem,
43
+ DropdownMenuLabel,
44
+ DropdownMenuSeparator,
45
+ DropdownMenuTrigger,
46
+ } from "@/components/ui/dropdown-menu";
47
+ import { Input } from "@/components/ui/input";
48
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
49
+ import { ScrollArea } from "@/components/ui/scroll-area";
50
+ import {
51
+ Select,
52
+ SelectContent,
53
+ SelectItem,
54
+ SelectTrigger,
55
+ SelectValue,
56
+ } from "@/components/ui/select";
57
+ import { Separator } from "@/components/ui/separator";
58
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
59
+ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
60
+ import { cn } from "@/lib/utils";
61
+
62
+ declare module "@tanstack/react-table" {
63
+ interface ColumnMeta<TData, TValue> {
64
+ title?: string;
65
+ }
66
+ }
67
+
68
+ export type DataTableFilterOption = {
69
+ label: string;
70
+ value: string;
71
+ };
72
+
73
+ export type DataTableFilterableColumn = {
74
+ id: string;
75
+ title: string;
76
+ options: DataTableFilterOption[];
77
+ };
78
+
79
+ export type DataTableSearchAction = {
80
+ label: string;
81
+ onClick: () => void;
82
+ backgroundColor?: string;
83
+ hoverBackgroundColor?: string;
84
+ textColor?: string;
85
+ icon?: React.ReactNode;
86
+ ariaLabel?: string;
87
+ };
88
+
89
+ type DataTableProps<TData, TValue> = {
90
+ columns: ColumnDef<TData, TValue>[];
91
+ data: TData[];
92
+ pageSize?: number;
93
+ pageSizeOptions?: number[];
94
+ searchPlaceholder?: string;
95
+ filterableColumns?: DataTableFilterableColumn[];
96
+ onRefresh?: () => void | Promise<void>;
97
+ isRefreshing?: boolean;
98
+ onExport?: () => void;
99
+ enableSearch?: boolean;
100
+ enableRowSelection?: boolean;
101
+ enableColumnVisibility?: boolean;
102
+ searchAction?: DataTableSearchAction;
103
+ searchActions?: React.ReactNode;
104
+ toolbarActions?: React.ReactNode;
105
+ bulkActions?: React.ReactNode;
106
+ onSelectionChange?: (rows: TData[]) => void;
107
+ };
108
+
109
+ function getColumnLabel<TData>(column: Column<TData, unknown>) {
110
+ return column.columnDef.meta?.title ?? column.id;
111
+ }
112
+
113
+ const DataTableIconButton = React.forwardRef<
114
+ HTMLButtonElement,
115
+ React.ComponentProps<typeof Button> & { tooltip: string; badge?: number | string }
116
+ >(({ tooltip, badge, children, className, ...props }, ref) => (
117
+ <Tooltip>
118
+ <TooltipTrigger asChild>
119
+ <Button
120
+ ref={ref}
121
+ type="button"
122
+ variant="outline"
123
+ size="sm"
124
+ className={cn("relative h-8 w-8 shrink-0 border-dashed bg-background p-0 shadow-sm", className)}
125
+ aria-label={tooltip}
126
+ {...props}
127
+ >
128
+ {children}
129
+ {badge !== undefined && Number(badge) > 0 ? (
130
+ <Badge
131
+ variant="secondary"
132
+ className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-normal"
133
+ >
134
+ {badge}
135
+ </Badge>
136
+ ) : null}
137
+ </Button>
138
+ </TooltipTrigger>
139
+ <TooltipContent side="bottom">{tooltip}</TooltipContent>
140
+ </Tooltip>
141
+ ));
142
+ DataTableIconButton.displayName = "DataTableIconButton";
143
+
144
+ export type DataTableRowAction = {
145
+ tooltip: string;
146
+ icon: React.ReactNode;
147
+ onClick: () => void;
148
+ variant?: React.ComponentProps<typeof Button>["variant"];
149
+ disabled?: boolean;
150
+ };
151
+
152
+ export function DataTableRowActions({
153
+ actions,
154
+ className,
155
+ align = "center",
156
+ }: {
157
+ actions: DataTableRowAction[];
158
+ className?: string;
159
+ align?: "center" | "end";
160
+ }) {
161
+ return (
162
+ <div
163
+ className={cn(
164
+ "flex w-full items-center gap-1",
165
+ align === "center" ? "justify-center" : "justify-end",
166
+ className,
167
+ )}
168
+ >
169
+ {actions.map((action, index) => (
170
+ <Tooltip key={`${action.tooltip}-${index}`}>
171
+ <TooltipTrigger asChild>
172
+ <Button
173
+ type="button"
174
+ variant={action.variant ?? "outline"}
175
+ size="sm"
176
+ className="h-8 w-8 shrink-0 p-0"
177
+ aria-label={action.tooltip}
178
+ onClick={action.onClick}
179
+ disabled={action.disabled}
180
+ >
181
+ {action.icon}
182
+ </Button>
183
+ </TooltipTrigger>
184
+ <TooltipContent side="bottom">{action.tooltip}</TooltipContent>
185
+ </Tooltip>
186
+ ))}
187
+ </div>
188
+ );
189
+ }
190
+
191
+ /** Standard actions column with icon-only update/delete (extend via getActions). */
192
+ export function dataTableActionsColumn<TData>({
193
+ getActions,
194
+ }: {
195
+ getActions: (row: TData) => DataTableRowAction[];
196
+ }): ColumnDef<TData> {
197
+ return {
198
+ id: "actions",
199
+ meta: { title: "Actions" },
200
+ enableSorting: false,
201
+ enableHiding: false,
202
+ header: () => (
203
+ <div className="flex w-full justify-center">
204
+ <span className="text-sm font-medium">Actions</span>
205
+ </div>
206
+ ),
207
+ cell: ({ row }) => <DataTableRowActions actions={getActions(row.original)} align="center" />,
208
+ };
209
+ }
210
+
211
+ export function dataTableSelectColumn<TData>({
212
+ shape = "square",
213
+ }: {
214
+ shape?: "rounded" | "square";
215
+ } = {}): ColumnDef<TData> {
216
+ return {
217
+ id: "select",
218
+ header: ({ table }) => (
219
+ <Checkbox
220
+ shape={shape}
221
+ checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate")}
222
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
223
+ aria-label="Select all rows on this page"
224
+ />
225
+ ),
226
+ cell: ({ row }) => (
227
+ <Checkbox
228
+ shape={shape}
229
+ checked={row.getIsSelected()}
230
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
231
+ aria-label="Select row"
232
+ />
233
+ ),
234
+ enableSorting: false,
235
+ enableHiding: false,
236
+ };
237
+ }
238
+
239
+ export function DataTableColumnHeader<TData, TValue>({
240
+ column,
241
+ title,
242
+ className,
243
+ }: {
244
+ column: {
245
+ getCanSort: () => boolean;
246
+ getIsSorted: () => false | "asc" | "desc";
247
+ toggleSorting: (desc?: boolean) => void;
248
+ };
249
+ title: string;
250
+ className?: string;
251
+ }) {
252
+ if (!column.getCanSort()) {
253
+ return <span className={cn("text-sm font-medium", className)}>{title}</span>;
254
+ }
255
+
256
+ const sorted = column.getIsSorted();
257
+
258
+ return (
259
+ <button
260
+ type="button"
261
+ className={cn(
262
+ "-ml-2 inline-flex h-8 items-center gap-1.5 rounded-md px-2 text-sm font-medium transition-colors",
263
+ "hover:bg-muted/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
264
+ sorted ? "text-foreground" : "text-muted-foreground",
265
+ className,
266
+ )}
267
+ onClick={() => column.toggleSorting(sorted === "asc")}
268
+ >
269
+ <span>{title}</span>
270
+ {sorted === "desc" ? (
271
+ <ArrowDown className="h-3.5 w-3.5" />
272
+ ) : sorted === "asc" ? (
273
+ <ArrowUp className="h-3.5 w-3.5" />
274
+ ) : (
275
+ <ArrowUpDown className="h-3.5 w-3.5 opacity-50" />
276
+ )}
277
+ </button>
278
+ );
279
+ }
280
+
281
+ function DataTableFacetedFilterGroup<TData>({
282
+ column,
283
+ title,
284
+ options,
285
+ }: {
286
+ column: Column<TData, unknown> | undefined;
287
+ title: string;
288
+ options: DataTableFilterOption[];
289
+ }) {
290
+ const filterValues = (column?.getFilterValue() as string[] | undefined) ?? [];
291
+
292
+ const toggleOption = (optionValue: string, checked: boolean | "indeterminate") => {
293
+ if (!column) {
294
+ return;
295
+ }
296
+
297
+ const next = new Set(filterValues);
298
+ if (checked === true) {
299
+ next.add(optionValue);
300
+ } else {
301
+ next.delete(optionValue);
302
+ }
303
+
304
+ const values = Array.from(next);
305
+ column.setFilterValue(values.length ? values : undefined);
306
+ };
307
+
308
+ return (
309
+ <div className="space-y-2">
310
+ <p className="text-sm font-medium">{title}</p>
311
+ <div className="space-y-1">
312
+ {options.map((option) => {
313
+ const isSelected = filterValues.includes(option.value);
314
+ return (
315
+ <div
316
+ key={option.value}
317
+ className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-muted/60"
318
+ onClick={() => toggleOption(option.value, !isSelected)}
319
+ onKeyDown={(event) => {
320
+ if (event.key === "Enter" || event.key === " ") {
321
+ event.preventDefault();
322
+ toggleOption(option.value, !isSelected);
323
+ }
324
+ }}
325
+ role="button"
326
+ tabIndex={0}
327
+ >
328
+ <Checkbox
329
+ shape="square"
330
+ checked={isSelected}
331
+ onCheckedChange={(checked) => toggleOption(option.value, checked)}
332
+ onClick={(event) => event.stopPropagation()}
333
+ />
334
+ <span>{option.label}</span>
335
+ </div>
336
+ );
337
+ })}
338
+ </div>
339
+ </div>
340
+ );
341
+ }
342
+
343
+ function DataTableSearch<TData>({
344
+ table,
345
+ searchPlaceholder,
346
+ className,
347
+ }: {
348
+ table: TanstackTable<TData>;
349
+ searchPlaceholder: string;
350
+ className?: string;
351
+ }) {
352
+ const query = (table.getState().globalFilter as string) ?? "";
353
+
354
+ return (
355
+ <div className={cn("relative min-w-0 flex-1", className)}>
356
+ <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
357
+ <Input
358
+ placeholder={searchPlaceholder}
359
+ value={query}
360
+ onChange={(event) => table.setGlobalFilter(event.target.value)}
361
+ className="h-9 bg-background pl-8 pr-8 shadow-sm"
362
+ />
363
+ {query ? (
364
+ <button
365
+ type="button"
366
+ aria-label="Clear search"
367
+ className="absolute right-2.5 top-2.5 rounded-sm text-muted-foreground hover:text-foreground"
368
+ onClick={() => table.setGlobalFilter("")}
369
+ >
370
+ <X className="h-4 w-4" />
371
+ </button>
372
+ ) : null}
373
+ </div>
374
+ );
375
+ }
376
+
377
+ function DataTableSearchActionButton({ action }: { action: DataTableSearchAction }) {
378
+ const { label, onClick, backgroundColor, hoverBackgroundColor, textColor, icon, ariaLabel } = action;
379
+ const tooltip = ariaLabel ?? label;
380
+ const hasCustomColors = Boolean(backgroundColor || textColor || hoverBackgroundColor);
381
+
382
+ return (
383
+ <Tooltip>
384
+ <TooltipTrigger asChild>
385
+ <Button
386
+ variant={hasCustomColors ? "default" : "brand"}
387
+ size="sm"
388
+ className={cn(
389
+ "h-8 w-8 shrink-0 border-0 p-0 shadow-sm",
390
+ hasCustomColors && "bg-primary text-primary-foreground hover:bg-primary/90",
391
+ )}
392
+ style={
393
+ hasCustomColors
394
+ ? {
395
+ backgroundColor,
396
+ color: textColor,
397
+ }
398
+ : undefined
399
+ }
400
+ aria-label={tooltip}
401
+ onClick={onClick}
402
+ onMouseEnter={(event) => {
403
+ if (hoverBackgroundColor) {
404
+ event.currentTarget.style.backgroundColor = hoverBackgroundColor;
405
+ }
406
+ }}
407
+ onMouseLeave={(event) => {
408
+ if (backgroundColor) {
409
+ event.currentTarget.style.backgroundColor = backgroundColor;
410
+ }
411
+ }}
412
+ >
413
+ {icon ?? <UserPlus className="h-4 w-4" />}
414
+ </Button>
415
+ </TooltipTrigger>
416
+ <TooltipContent side="bottom">{tooltip}</TooltipContent>
417
+ </Tooltip>
418
+ );
419
+ }
420
+
421
+ function DataTableFiltersPopover<TData>({
422
+ table,
423
+ filterableColumns,
424
+ }: {
425
+ table: TanstackTable<TData>;
426
+ filterableColumns?: DataTableFilterableColumn[];
427
+ }) {
428
+ const activeFilterCount = table.getState().columnFilters.length;
429
+
430
+ if (!filterableColumns?.length) {
431
+ return null;
432
+ }
433
+
434
+ return (
435
+ <Popover modal>
436
+ <PopoverTrigger asChild>
437
+ <DataTableIconButton tooltip="Filters" badge={activeFilterCount || undefined}>
438
+ <ListFilter className="h-4 w-4" />
439
+ </DataTableIconButton>
440
+ </PopoverTrigger>
441
+ <PopoverContent align="start" className="z-[100] w-80 p-0">
442
+ <div className="border-b px-4 py-3">
443
+ <p className="text-sm font-semibold">Filters</p>
444
+ <p className="text-xs text-muted-foreground">Filter rows by column values.</p>
445
+ </div>
446
+ <ScrollArea className="max-h-80">
447
+ <div className="space-y-4 p-4">
448
+ {filterableColumns.map((filterColumn) => (
449
+ <DataTableFacetedFilterGroup
450
+ key={filterColumn.id}
451
+ column={table.getColumn(filterColumn.id)}
452
+ title={filterColumn.title}
453
+ options={filterColumn.options}
454
+ />
455
+ ))}
456
+ </div>
457
+ </ScrollArea>
458
+ {activeFilterCount > 0 && (
459
+ <div className="border-t p-2">
460
+ <Button
461
+ variant="outline"
462
+ size="sm"
463
+ className="h-8 w-full"
464
+ onClick={() => table.resetColumnFilters()}
465
+ >
466
+ Reset filters
467
+ </Button>
468
+ </div>
469
+ )}
470
+ </PopoverContent>
471
+ </Popover>
472
+ );
473
+ }
474
+
475
+ function DataTableSortDropdown<TData>({ table }: { table: TanstackTable<TData> }) {
476
+ const sortableColumns = table.getAllLeafColumns().filter((column) => column.getCanSort());
477
+ const sorting = table.getState().sorting;
478
+
479
+ return (
480
+ <DropdownMenu modal={false}>
481
+ <DropdownMenuTrigger asChild>
482
+ <DataTableIconButton tooltip="Sort" badge={sorting.length || undefined}>
483
+ <ArrowUpDown className="h-4 w-4" />
484
+ </DataTableIconButton>
485
+ </DropdownMenuTrigger>
486
+ <DropdownMenuContent align="start" className="z-[100] w-52">
487
+ <DropdownMenuLabel>Sort by</DropdownMenuLabel>
488
+ <DropdownMenuSeparator />
489
+ {sortableColumns.length === 0 ? (
490
+ <p className="px-2 py-3 text-sm text-muted-foreground">No sortable columns.</p>
491
+ ) : (
492
+ sortableColumns.map((column) => {
493
+ const sorted = column.getIsSorted();
494
+ return (
495
+ <DropdownMenuItem
496
+ key={column.id}
497
+ onSelect={(event) => {
498
+ event.preventDefault();
499
+ column.toggleSorting(sorted === "asc");
500
+ }}
501
+ >
502
+ <span className="flex-1">{getColumnLabel(column)}</span>
503
+ {sorted === "asc" ? (
504
+ <ArrowUp className="h-4 w-4 text-muted-foreground" />
505
+ ) : sorted === "desc" ? (
506
+ <ArrowDown className="h-4 w-4 text-muted-foreground" />
507
+ ) : (
508
+ <ArrowUpDown className="h-4 w-4 opacity-30" />
509
+ )}
510
+ </DropdownMenuItem>
511
+ );
512
+ })
513
+ )}
514
+ {sorting.length > 0 && (
515
+ <>
516
+ <DropdownMenuSeparator />
517
+ <DropdownMenuItem
518
+ onSelect={(event) => {
519
+ event.preventDefault();
520
+ table.resetSorting();
521
+ }}
522
+ className="text-muted-foreground"
523
+ >
524
+ Clear sorting
525
+ </DropdownMenuItem>
526
+ </>
527
+ )}
528
+ </DropdownMenuContent>
529
+ </DropdownMenu>
530
+ );
531
+ }
532
+
533
+ function DataTableViewDropdown<TData>({ table }: { table: TanstackTable<TData> }) {
534
+ const hideableColumns = table.getAllLeafColumns().filter((column) => column.getCanHide());
535
+ const hiddenCount = hideableColumns.filter((column) => !column.getIsVisible()).length;
536
+
537
+ return (
538
+ <DropdownMenu modal={false}>
539
+ <DropdownMenuTrigger asChild>
540
+ <DataTableIconButton tooltip="View columns" badge={hiddenCount || undefined}>
541
+ <SlidersHorizontal className="h-4 w-4" />
542
+ </DataTableIconButton>
543
+ </DropdownMenuTrigger>
544
+ <DropdownMenuContent align="end" className="z-[100] w-52">
545
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
546
+ <DropdownMenuSeparator />
547
+ {hideableColumns.length === 0 ? (
548
+ <p className="px-2 py-3 text-sm text-muted-foreground">No hideable columns.</p>
549
+ ) : (
550
+ hideableColumns.map((column) => (
551
+ <DropdownMenuCheckboxItem
552
+ key={column.id}
553
+ checked={column.getIsVisible()}
554
+ onCheckedChange={(checked) => column.toggleVisibility(checked === true)}
555
+ onSelect={(event) => event.preventDefault()}
556
+ >
557
+ {getColumnLabel(column)}
558
+ </DropdownMenuCheckboxItem>
559
+ ))
560
+ )}
561
+ {hiddenCount > 0 && (
562
+ <>
563
+ <DropdownMenuSeparator />
564
+ <DropdownMenuItem
565
+ onSelect={(event) => {
566
+ event.preventDefault();
567
+ table.resetColumnVisibility();
568
+ }}
569
+ >
570
+ Show all columns
571
+ </DropdownMenuItem>
572
+ </>
573
+ )}
574
+ </DropdownMenuContent>
575
+ </DropdownMenu>
576
+ );
577
+ }
578
+
579
+ type DataTableToolbarProps<TData> = {
580
+ table: TanstackTable<TData>;
581
+ searchPlaceholder: string;
582
+ filterableColumns?: DataTableFilterableColumn[];
583
+ onRefresh?: () => void | Promise<void>;
584
+ isRefreshing?: boolean;
585
+ onExport?: () => void;
586
+ enableSearch?: boolean;
587
+ enableColumnVisibility?: boolean;
588
+ searchAction?: DataTableSearchAction;
589
+ searchActions?: React.ReactNode;
590
+ toolbarActions?: React.ReactNode;
591
+ };
592
+
593
+ function DataTableToolbar<TData>({
594
+ table,
595
+ searchPlaceholder,
596
+ filterableColumns,
597
+ onRefresh,
598
+ isRefreshing,
599
+ onExport,
600
+ enableSearch = true,
601
+ enableColumnVisibility = true,
602
+ searchAction,
603
+ searchActions,
604
+ toolbarActions,
605
+ }: DataTableToolbarProps<TData>) {
606
+ const searchRowActions = (
607
+ <>
608
+ {searchAction ? <DataTableSearchActionButton action={searchAction} /> : null}
609
+ {searchActions}
610
+ </>
611
+ );
612
+ const hasSearchRowActions = Boolean(searchAction || searchActions);
613
+
614
+ return (
615
+ <div className="flex flex-col gap-2 border-b bg-muted/20 px-3 py-2">
616
+ <div className="flex flex-wrap items-center gap-2">
617
+ {enableSearch ? <DataTableSearch table={table} searchPlaceholder={searchPlaceholder} /> : null}
618
+ <div className="ml-auto flex shrink-0 flex-wrap items-center gap-1">
619
+ <DataTableFiltersPopover table={table} filterableColumns={filterableColumns} />
620
+ <DataTableSortDropdown table={table} />
621
+ {onExport ? (
622
+ <DataTableIconButton tooltip="Export" onClick={onExport}>
623
+ <Download className="h-4 w-4" />
624
+ </DataTableIconButton>
625
+ ) : null}
626
+ {enableColumnVisibility ? <DataTableViewDropdown table={table} /> : null}
627
+ {toolbarActions}
628
+ {onRefresh ? (
629
+ <DataTableIconButton
630
+ tooltip="Refresh"
631
+ onClick={() => onRefresh()}
632
+ disabled={isRefreshing}
633
+ >
634
+ <RefreshCw className={cn("h-4 w-4", isRefreshing && "animate-spin")} />
635
+ </DataTableIconButton>
636
+ ) : null}
637
+ {hasSearchRowActions ? searchRowActions : null}
638
+ </div>
639
+ </div>
640
+ </div>
641
+ );
642
+ }
643
+
644
+ function DataTableBulkBar<TData>({
645
+ table,
646
+ bulkActions,
647
+ }: {
648
+ table: TanstackTable<TData>;
649
+ bulkActions?: React.ReactNode;
650
+ }) {
651
+ const selectedCount = table.getFilteredSelectedRowModel().rows.length;
652
+
653
+ if (selectedCount === 0) {
654
+ return null;
655
+ }
656
+
657
+ return (
658
+ <div className="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-dashed bg-muted/30 px-3 py-2">
659
+ <p className="text-sm text-muted-foreground">
660
+ {selectedCount} row{selectedCount === 1 ? "" : "s"} selected
661
+ </p>
662
+ <div className="flex flex-wrap items-center gap-2">
663
+ {bulkActions}
664
+ <Button variant="outline" size="sm" className="h-8" onClick={() => table.resetRowSelection()}>
665
+ Clear selection
666
+ </Button>
667
+ </div>
668
+ </div>
669
+ );
670
+ }
671
+
672
+ function DataTablePagination<TData>({
673
+ table,
674
+ pageSizeOptions,
675
+ enableRowSelection,
676
+ }: {
677
+ table: TanstackTable<TData>;
678
+ pageSizeOptions: number[];
679
+ enableRowSelection: boolean;
680
+ }) {
681
+ const selectedCount = table.getFilteredSelectedRowModel().rows.length;
682
+ const totalCount = table.getFilteredRowModel().rows.length;
683
+
684
+ return (
685
+ <div className="flex flex-col gap-4 px-1 sm:flex-row sm:items-center sm:justify-between">
686
+ <p className="text-sm text-muted-foreground">
687
+ {enableRowSelection ? (
688
+ <>
689
+ {selectedCount} of {totalCount} row(s) selected.
690
+ </>
691
+ ) : (
692
+ <>{totalCount} row(s) total.</>
693
+ )}
694
+ </p>
695
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:gap-6">
696
+ <div className="flex items-center gap-2">
697
+ <span className="whitespace-nowrap text-sm text-muted-foreground">Rows per page</span>
698
+ <Select
699
+ value={`${table.getState().pagination.pageSize}`}
700
+ onValueChange={(value) => table.setPageSize(Number(value))}
701
+ >
702
+ <SelectTrigger className="h-8 w-[4.5rem]">
703
+ <SelectValue />
704
+ </SelectTrigger>
705
+ <SelectContent side="top">
706
+ {pageSizeOptions.map((size) => (
707
+ <SelectItem key={size} value={`${size}`}>
708
+ {size}
709
+ </SelectItem>
710
+ ))}
711
+ </SelectContent>
712
+ </Select>
713
+ </div>
714
+ <div className="flex items-center justify-between gap-4 sm:justify-end">
715
+ <span className="whitespace-nowrap text-sm font-medium">
716
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
717
+ </span>
718
+ <div className="flex items-center gap-1">
719
+ <Button
720
+ variant="outline"
721
+ size="sm"
722
+ className="h-8 w-8 p-0"
723
+ onClick={() => table.setPageIndex(0)}
724
+ disabled={!table.getCanPreviousPage()}
725
+ aria-label="First page"
726
+ >
727
+ <ChevronsLeft className="h-4 w-4" />
728
+ </Button>
729
+ <Button
730
+ variant="outline"
731
+ size="sm"
732
+ className="h-8 w-8 p-0"
733
+ onClick={() => table.previousPage()}
734
+ disabled={!table.getCanPreviousPage()}
735
+ aria-label="Previous page"
736
+ >
737
+ <ChevronLeft className="h-4 w-4" />
738
+ </Button>
739
+ <Button
740
+ variant="outline"
741
+ size="sm"
742
+ className="h-8 w-8 p-0"
743
+ onClick={() => table.nextPage()}
744
+ disabled={!table.getCanNextPage()}
745
+ aria-label="Next page"
746
+ >
747
+ <ChevronRight className="h-4 w-4" />
748
+ </Button>
749
+ <Button
750
+ variant="outline"
751
+ size="sm"
752
+ className="h-8 w-8 p-0"
753
+ onClick={() => table.setPageIndex(table.getPageCount() - 1)}
754
+ disabled={!table.getCanNextPage()}
755
+ aria-label="Last page"
756
+ >
757
+ <ChevronsRight className="h-4 w-4" />
758
+ </Button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+ );
764
+ }
765
+
766
+ function globalUserSearchFilter<TData>(row: Row<TData>, _columnId: string, filterValue: string) {
767
+ const query = filterValue.trim().toLowerCase();
768
+ if (!query) {
769
+ return true;
770
+ }
771
+
772
+ const record = row.original as Record<string, unknown>;
773
+ const name = String(record.name ?? "").toLowerCase();
774
+ const email = String(record.email ?? "").toLowerCase();
775
+ const role = String(record.role ?? "").toLowerCase();
776
+
777
+ return name.includes(query) || email.includes(query) || role.includes(query);
778
+ }
779
+
780
+ export function DataTable<TData, TValue>({
781
+ columns,
782
+ data,
783
+ pageSize = 10,
784
+ pageSizeOptions = [5, 10, 20, 30, 50],
785
+ searchPlaceholder = "Search rows...",
786
+ filterableColumns,
787
+ onRefresh,
788
+ isRefreshing = false,
789
+ onExport,
790
+ enableSearch = true,
791
+ enableRowSelection = false,
792
+ enableColumnVisibility = true,
793
+ searchAction,
794
+ searchActions,
795
+ toolbarActions,
796
+ bulkActions,
797
+ onSelectionChange,
798
+ }: DataTableProps<TData, TValue>) {
799
+ const [sorting, setSorting] = React.useState<SortingState>([]);
800
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
801
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
802
+ const [rowSelection, setRowSelection] = React.useState({});
803
+ const [globalFilter, setGlobalFilter] = React.useState("");
804
+
805
+ const tableColumns = React.useMemo(
806
+ () => (enableRowSelection ? [dataTableSelectColumn<TData>(), ...columns] : columns),
807
+ [columns, enableRowSelection],
808
+ );
809
+
810
+ const table = useReactTable({
811
+ data,
812
+ columns: tableColumns,
813
+ state: {
814
+ sorting,
815
+ columnFilters,
816
+ columnVisibility,
817
+ rowSelection,
818
+ globalFilter,
819
+ },
820
+ enableRowSelection,
821
+ enableSorting: true,
822
+ enableColumnFilters: true,
823
+ enableHiding: true,
824
+ onSortingChange: setSorting,
825
+ onColumnFiltersChange: setColumnFilters,
826
+ onColumnVisibilityChange: setColumnVisibility,
827
+ onRowSelectionChange: setRowSelection,
828
+ onGlobalFilterChange: setGlobalFilter,
829
+ globalFilterFn: globalUserSearchFilter,
830
+ getCoreRowModel: getCoreRowModel(),
831
+ getFilteredRowModel: getFilteredRowModel(),
832
+ getPaginationRowModel: getPaginationRowModel(),
833
+ getSortedRowModel: getSortedRowModel(),
834
+ getFacetedRowModel: getFacetedRowModel(),
835
+ getFacetedUniqueValues: getFacetedUniqueValues(),
836
+ initialState: {
837
+ pagination: { pageSize },
838
+ },
839
+ });
840
+
841
+ React.useEffect(() => {
842
+ if (!onSelectionChange) {
843
+ return;
844
+ }
845
+ const selected = table.getFilteredSelectedRowModel().rows.map((row) => row.original);
846
+ onSelectionChange(selected);
847
+ }, [rowSelection, data, onSelectionChange, table]);
848
+
849
+ return (
850
+ <div className="space-y-4">
851
+ <div className="overflow-hidden rounded-lg border bg-card shadow-sm">
852
+ <DataTableToolbar
853
+ table={table}
854
+ searchPlaceholder={searchPlaceholder}
855
+ filterableColumns={filterableColumns}
856
+ onRefresh={onRefresh}
857
+ isRefreshing={isRefreshing}
858
+ onExport={onExport}
859
+ enableSearch={enableSearch}
860
+ enableColumnVisibility={enableColumnVisibility}
861
+ searchAction={searchAction}
862
+ searchActions={searchActions}
863
+ toolbarActions={toolbarActions}
864
+ />
865
+
866
+ {enableRowSelection ? (
867
+ <div className="border-b px-3 py-2">
868
+ <DataTableBulkBar table={table} bulkActions={bulkActions} />
869
+ </div>
870
+ ) : null}
871
+
872
+ <Table>
873
+ <TableHeader>
874
+ {table.getHeaderGroups().map((headerGroup) => (
875
+ <TableRow key={headerGroup.id} className="border-b bg-muted/30 hover:bg-muted/30">
876
+ {headerGroup.headers.map((header) => (
877
+ <TableHead
878
+ key={header.id}
879
+ className={cn("h-11", header.column.id === "actions" && "text-center")}
880
+ >
881
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
882
+ </TableHead>
883
+ ))}
884
+ </TableRow>
885
+ ))}
886
+ </TableHeader>
887
+ <TableBody>
888
+ {table.getRowModel().rows?.length ? (
889
+ table.getRowModel().rows.map((row) => (
890
+ <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
891
+ {row.getVisibleCells().map((cell) => (
892
+ <TableCell
893
+ key={cell.id}
894
+ className={cn("h-12", cell.column.id === "actions" && "text-center")}
895
+ >
896
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
897
+ </TableCell>
898
+ ))}
899
+ </TableRow>
900
+ ))
901
+ ) : (
902
+ <TableRow>
903
+ <TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
904
+ No results.
905
+ </TableCell>
906
+ </TableRow>
907
+ )}
908
+ </TableBody>
909
+ </Table>
910
+ </div>
911
+
912
+ <DataTablePagination
913
+ table={table}
914
+ pageSizeOptions={pageSizeOptions}
915
+ enableRowSelection={enableRowSelection}
916
+ />
917
+ </div>
918
+ );
919
+ }