@suphark/ui 0.1.1 → 0.3.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 (30) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +45 -3
  3. package/package.json +14 -2
  4. package/src/components/app-shell/app-brand.tsx +81 -0
  5. package/src/components/app-shell/app-nav.tsx +260 -0
  6. package/src/components/app-shell/app-shell.tsx +270 -0
  7. package/src/components/app-shell/header-breadcrumbs.tsx +78 -0
  8. package/src/components/app-shell/index.ts +14 -0
  9. package/src/components/app-shell/nav-link.tsx +35 -0
  10. package/src/components/app-shell/types.ts +92 -0
  11. package/src/components/app-shell/user-menu.tsx +133 -0
  12. package/src/components/data-table/data-table-column-header.tsx +71 -0
  13. package/src/components/data-table/data-table-date-range-filter.tsx +25 -0
  14. package/src/components/data-table/data-table-faceted-filter.tsx +181 -0
  15. package/src/components/data-table/data-table-pagination.tsx +114 -0
  16. package/src/components/data-table/data-table-row-actions.tsx +166 -0
  17. package/src/components/data-table/data-table-toolbar.tsx +207 -0
  18. package/src/components/data-table/data-table-view-option.tsx +59 -0
  19. package/src/components/data-table/data-table.tsx +399 -0
  20. package/src/components/data-table/index.ts +9 -0
  21. package/src/components/data-table/types.ts +106 -0
  22. package/src/components/data-table/use-data-table.ts +229 -0
  23. package/src/components/design-system/sections/showcase-data-display.tsx +71 -0
  24. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +8 -8
  25. package/src/components/shared/button/delete-many-button.tsx +85 -0
  26. package/src/components/shared/resource-property-filter.tsx +399 -0
  27. package/src/data-table.ts +21 -0
  28. package/src/hooks/use-table-search-params.ts +147 -0
  29. package/src/index.ts +2 -1
  30. package/src/next.ts +8 -0
@@ -0,0 +1,207 @@
1
+ "use client";
2
+
3
+ import { Badge } from "@suphark/ui/components/ui/badge";
4
+ import { Button } from "@suphark/ui/components/ui/button";
5
+ import {
6
+ InputGroup,
7
+ InputGroupAddon,
8
+ InputGroupButton,
9
+ InputGroupInput,
10
+ } from "@suphark/ui/components/ui/input-group";
11
+ import {
12
+ Sheet,
13
+ SheetClose,
14
+ SheetContent,
15
+ SheetDescription,
16
+ SheetFooter,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ SheetTrigger,
20
+ } from "@suphark/ui/components/ui/sheet";
21
+ import type { Table } from "@tanstack/react-table";
22
+ import { Search, SlidersHorizontal, X } from "lucide-react";
23
+ import type React from "react";
24
+ import type { FacetedFilterConfig } from ".";
25
+ import { DataTableFacetedFilter } from "./data-table-faceted-filter";
26
+ import { DataTableViewOptions } from "./data-table-view-option";
27
+
28
+ interface DataTableToolbarProps<TData> {
29
+ table: Table<TData>;
30
+ facetedFilters?: FacetedFilterConfig[];
31
+ actionsComponent?: React.ReactNode;
32
+ globalFilterPlaceholder?: string;
33
+ toolbarFilters?: React.ReactNode;
34
+ preservedFilterIds?: string[];
35
+ }
36
+
37
+ export function DataTableToolbar<TData>({
38
+ table,
39
+ facetedFilters,
40
+ actionsComponent,
41
+ globalFilterPlaceholder,
42
+ toolbarFilters,
43
+ preservedFilterIds = [],
44
+ }: DataTableToolbarProps<TData>) {
45
+ const globalFilter = table.getState().globalFilter;
46
+ const preservedFilterIdSet = new Set(preservedFilterIds);
47
+ const userColumnFilters = table
48
+ .getState()
49
+ .columnFilters.filter((filter) => !preservedFilterIdSet.has(filter.id));
50
+ const activeFilterCount = userColumnFilters.length + (globalFilter ? 1 : 0);
51
+
52
+ const isFiltered = activeFilterCount > 0;
53
+
54
+ const resetUserFilters = () => {
55
+ table.setColumnFilters((filters) =>
56
+ filters.filter((filter) => preservedFilterIdSet.has(filter.id)),
57
+ );
58
+ table.setGlobalFilter("");
59
+ };
60
+
61
+ const renderFilterControls = (fullWidth = false) =>
62
+ facetedFilters?.map((filter: FacetedFilterConfig) => {
63
+ const column = table.getColumn(filter.columnId);
64
+ if (!column) return null;
65
+
66
+ return (
67
+ <DataTableFacetedFilter
68
+ key={filter.columnId}
69
+ column={column}
70
+ title={filter.title}
71
+ options={filter.options}
72
+ compact={filter.compact}
73
+ fullWidth={fullWidth}
74
+ />
75
+ );
76
+ });
77
+
78
+ return (
79
+ <div className="flex flex-wrap items-center gap-2">
80
+ <div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
81
+ {globalFilterPlaceholder && (
82
+ <InputGroup className="h-8 w-full sm:w-[240px] lg:w-[320px]">
83
+ <InputGroupAddon align="inline-start">
84
+ <Search aria-hidden="true" className="size-4" />
85
+ </InputGroupAddon>
86
+ <InputGroupInput
87
+ aria-label="ค้นหาข้อมูลในตาราง"
88
+ name="table-search"
89
+ autoComplete="off"
90
+ placeholder={globalFilterPlaceholder}
91
+ value={(globalFilter as string) ?? ""}
92
+ onChange={(event) => table.setGlobalFilter(String(event.target.value))}
93
+ />
94
+ {!!globalFilter && (
95
+ <InputGroupAddon align="inline-end">
96
+ <InputGroupButton
97
+ variant="ghost"
98
+ aria-label="ล้างคำค้นหา"
99
+ onClick={() => table.setGlobalFilter("")}
100
+ >
101
+ <X aria-hidden="true" className="size-4" />
102
+ </InputGroupButton>
103
+ </InputGroupAddon>
104
+ )}
105
+ </InputGroup>
106
+ )}
107
+
108
+ <div className="hidden xl:contents">
109
+ {renderFilterControls()}
110
+ {toolbarFilters}
111
+ </div>
112
+
113
+ <Sheet>
114
+ <SheetTrigger render={<Button variant="outline" size="sm" className="xl:hidden" />}>
115
+ <SlidersHorizontal />
116
+ ตัวกรอง
117
+ {activeFilterCount > 0 && (
118
+ <Badge variant="secondary" className="ml-1 px-1.5 tabular-nums">
119
+ {activeFilterCount}
120
+ </Badge>
121
+ )}
122
+ </SheetTrigger>
123
+ <SheetContent
124
+ side="right"
125
+ showCloseButton={false}
126
+ className="w-[min(24rem,calc(100vw-1rem))] sm:max-w-md"
127
+ >
128
+ <SheetHeader className="border-b pr-12">
129
+ <SheetTitle>ตัวกรองข้อมูล</SheetTitle>
130
+ <SheetDescription>เลือกได้หลายเงื่อนไข ผลลัพธ์จะอัปเดตทันที</SheetDescription>
131
+ <SheetClose
132
+ render={
133
+ <Button variant="ghost" size="icon-sm" className="absolute top-3 right-3" />
134
+ }
135
+ >
136
+ <X />
137
+ <span className="sr-only">ปิดตัวกรอง</span>
138
+ </SheetClose>
139
+ </SheetHeader>
140
+ <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-4">
141
+ {renderFilterControls(true)}
142
+ {toolbarFilters}
143
+ </div>
144
+ {isFiltered && (
145
+ <SheetFooter className="border-t">
146
+ <Button variant="secondary" onClick={resetUserFilters}>
147
+ <X />
148
+ ล้างตัวกรองทั้งหมด
149
+ </Button>
150
+ </SheetFooter>
151
+ )}
152
+ </SheetContent>
153
+ </Sheet>
154
+
155
+ {isFiltered && (
156
+ <Button variant="secondary" size="sm" onClick={resetUserFilters}>
157
+ ล้างตัวกรอง
158
+ <X />
159
+ </Button>
160
+ )}
161
+ </div>
162
+
163
+ <div className="ml-auto flex flex-wrap items-center justify-end gap-2 self-start">
164
+ {actionsComponent}
165
+ <DataTableViewOptions table={table} />
166
+ </div>
167
+
168
+ {isFiltered && (
169
+ <div className="flex w-full flex-wrap gap-2 pt-1">
170
+ {userColumnFilters.map((filter) => {
171
+ const config = facetedFilters?.find((f) => f.columnId === filter.id);
172
+ if (!config) return null;
173
+
174
+ const values = Array.isArray(filter.value) ? filter.value : [filter.value];
175
+ const labels = values
176
+ .map((val) => {
177
+ const option = config.options.find((opt) => String(opt.value) === String(val));
178
+ return option ? option.label : String(val);
179
+ })
180
+ .join(", ");
181
+ const displayedLabels =
182
+ config.compact && values.length > 1 ? `${values.length} รายการ` : labels;
183
+
184
+ return (
185
+ <Badge
186
+ key={filter.id}
187
+ variant="secondary"
188
+ className="flex items-center gap-1.5 rounded-md px-2 py-1 font-normal"
189
+ >
190
+ <span className="text-muted-foreground">{config.title}:</span>
191
+ <span className="font-medium">{displayedLabels}</span>
192
+ <button
193
+ type="button"
194
+ aria-label={`ล้างตัวกรอง ${config.title}: ${displayedLabels}`}
195
+ onClick={() => table.getColumn(filter.id)?.setFilterValue(undefined)}
196
+ className="ml-1 rounded-full outline-none hover:bg-muted-foreground/20"
197
+ >
198
+ <X className="size-3" />
199
+ </button>
200
+ </Badge>
201
+ );
202
+ })}
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/ui/button";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuCheckboxItem,
7
+ DropdownMenuContent,
8
+ DropdownMenuGroup,
9
+ DropdownMenuLabel,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuTrigger,
12
+ } from "@suphark/ui/components/ui/dropdown-menu";
13
+ import type { Table } from "@tanstack/react-table";
14
+ import { Settings2 } from "lucide-react";
15
+
16
+ interface DataTableViewOptionsProps<TData> {
17
+ table: Table<TData>;
18
+ }
19
+
20
+ export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
21
+ return (
22
+ <DropdownMenu>
23
+ <DropdownMenuTrigger
24
+ render={<Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex" />}
25
+ >
26
+ <Settings2 className="mr-2 h-4 w-4" />
27
+ View
28
+ </DropdownMenuTrigger>
29
+ <DropdownMenuContent align="end" className="w-[150px]">
30
+ <DropdownMenuGroup>
31
+ <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
32
+ </DropdownMenuGroup>
33
+ <DropdownMenuSeparator />
34
+ {table
35
+ .getAllColumns()
36
+ .filter(
37
+ (column) =>
38
+ typeof column.accessorFn !== "undefined" &&
39
+ column.getCanHide() &&
40
+ column.columnDef.enableHiding !== false,
41
+ )
42
+ .map((column) => {
43
+ return (
44
+ <DropdownMenuCheckboxItem
45
+ key={column.id}
46
+ className="capitalize"
47
+ checked={column.getIsVisible()}
48
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
49
+ // Base UI ปิดเมนูหลังคลิกเป็นค่าเริ่มต้น — ติ๊กเลือกคอลัมน์ต้องเปิดค้างไว้
50
+ closeOnClick={false}
51
+ >
52
+ {column.id.replace(/([A-Z])/g, " $1")}
53
+ </DropdownMenuCheckboxItem>
54
+ );
55
+ })}
56
+ </DropdownMenuContent>
57
+ </DropdownMenu>
58
+ );
59
+ }
@@ -0,0 +1,399 @@
1
+ "use client";
2
+
3
+ import {
4
+ closestCenter,
5
+ DndContext,
6
+ type DragEndEvent,
7
+ KeyboardSensor,
8
+ PointerSensor,
9
+ useSensor,
10
+ useSensors,
11
+ } from "@dnd-kit/core";
12
+ import {
13
+ arrayMove,
14
+ SortableContext,
15
+ sortableKeyboardCoordinates,
16
+ useSortable,
17
+ verticalListSortingStrategy,
18
+ } from "@dnd-kit/sortable";
19
+ import { CSS } from "@dnd-kit/utilities";
20
+ import {
21
+ TableBody,
22
+ TableCell,
23
+ TableHead,
24
+ TableHeader,
25
+ TableRow,
26
+ Table as UiTable,
27
+ } from "@suphark/ui/components/ui/table";
28
+ import { cn } from "@suphark/ui/lib/utils";
29
+ import { flexRender, type Row } from "@tanstack/react-table";
30
+ import type { CSSProperties } from "react";
31
+ import * as React from "react";
32
+ import { toast } from "sonner";
33
+ import { DataTablePagination } from "./data-table-pagination";
34
+ import { DataTableToolbar } from "./data-table-toolbar";
35
+ import type { DataTableProps } from "./types";
36
+ import { useDataTable } from "./use-data-table";
37
+
38
+ interface DraggableRowProps<TData> {
39
+ row: Row<TData>;
40
+ enableColumnResizing?: boolean;
41
+ getRowClassName?: (row: Row<TData>) => string;
42
+ stickyEdgeColumns?: boolean;
43
+ }
44
+
45
+ function DraggableRow<TData>({
46
+ row,
47
+ enableColumnResizing,
48
+ getRowClassName,
49
+ stickyEdgeColumns,
50
+ }: DraggableRowProps<TData>) {
51
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
52
+ id: row.id,
53
+ });
54
+
55
+ const style: CSSProperties = {
56
+ transform: CSS.Transform.toString(transform),
57
+ transition,
58
+ opacity: isDragging ? 0.8 : 1,
59
+ zIndex: isDragging ? 1 : 0,
60
+ position: "relative",
61
+ };
62
+
63
+ return (
64
+ <TableRow
65
+ ref={setNodeRef}
66
+ style={style}
67
+ data-state={row.getIsSelected() && "selected"}
68
+ className={cn(stickyEdgeColumns && "group/row", getRowClassName?.(row))}
69
+ >
70
+ {row.getVisibleCells().map((cell) => {
71
+ const isDndColumn = cell.column.id === "dnd-handle";
72
+ return (
73
+ <TableCell
74
+ key={cell.id}
75
+ {...(isDndColumn ? { ...attributes, ...listeners } : {})}
76
+ style={{
77
+ width: enableColumnResizing ? cell.column.getSize() : undefined,
78
+ }}
79
+ className={cn(
80
+ stickyEdgeColumns &&
81
+ cell.column.id === "select" &&
82
+ "sticky left-0 z-10 bg-card group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
83
+ stickyEdgeColumns &&
84
+ cell.column.id === "actions" &&
85
+ "sticky right-0 z-10 bg-card group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
86
+ )}
87
+ >
88
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
89
+ </TableCell>
90
+ );
91
+ })}
92
+ </TableRow>
93
+ );
94
+ }
95
+
96
+ /**
97
+ * DataTable
98
+ * @initialState {{ expanded: true }} -> (ขยายทั้งหมด)
99
+ * @initialState {{ expanded: {} }} -> (หุบทั้งหมด)
100
+ */
101
+ export function DataTable<TData, TValue>({
102
+ columns,
103
+ data,
104
+ className,
105
+ containerClassName,
106
+ tableContainerClassName,
107
+ stickyHeader,
108
+ stickyPagination,
109
+ stickyEdgeColumns,
110
+ emptyStateComponent,
111
+ facetedFilters,
112
+ actionsComponent,
113
+ globalFilterPlaceholder,
114
+ initialVisibility = {},
115
+ globalFilterFn,
116
+ globalFilterKeys,
117
+ getRowClassName,
118
+ getFacetedUniqueValues: getFacetedUniqueValuesProp,
119
+ initialColumnFilters = [],
120
+ columnFilters,
121
+ getSubRows,
122
+ getRowId,
123
+ initialPageSize = 10,
124
+ meta,
125
+ initialState,
126
+ pageCount,
127
+ rowCount,
128
+ pagination,
129
+ sorting,
130
+ globalFilter,
131
+ onGlobalFilterChange,
132
+ manualPagination,
133
+ manualSorting,
134
+ manualFiltering,
135
+ onPaginationChange,
136
+ onPageSizeChange,
137
+ onSortingChange,
138
+ onColumnFiltersChange,
139
+ enableColumnResizing = false,
140
+ enableRowSelection = true,
141
+ isLoading,
142
+ pageSizeOptions,
143
+ toolbarFilters,
144
+ preservedFilterIds,
145
+ onReorder,
146
+ renderCustomView,
147
+ }: DataTableProps<TData, TValue>) {
148
+ const { table } = useDataTable({
149
+ columns,
150
+ data,
151
+ className,
152
+ containerClassName,
153
+ stickyHeader,
154
+ emptyStateComponent,
155
+ initialVisibility,
156
+ globalFilterFn,
157
+ globalFilterKeys,
158
+ getFacetedUniqueValues: getFacetedUniqueValuesProp,
159
+ initialColumnFilters,
160
+ columnFilters,
161
+ getSubRows,
162
+ getRowId,
163
+ initialPageSize,
164
+ meta,
165
+ initialState,
166
+ pageCount,
167
+ rowCount,
168
+ pagination,
169
+ sorting,
170
+ globalFilter,
171
+ onGlobalFilterChange,
172
+ manualPagination,
173
+ manualSorting,
174
+ manualFiltering,
175
+ onPaginationChange,
176
+ onSortingChange,
177
+ onColumnFiltersChange,
178
+ enableColumnResizing,
179
+ enableRowSelection,
180
+ });
181
+
182
+ const sensors = useSensors(
183
+ useSensor(PointerSensor, {
184
+ activationConstraint: {
185
+ distance: 5,
186
+ },
187
+ }),
188
+ useSensor(KeyboardSensor, {
189
+ coordinateGetter: sortableKeyboardCoordinates,
190
+ }),
191
+ );
192
+
193
+ function handleDragEnd(event: DragEndEvent) {
194
+ if (!onReorder) return;
195
+
196
+ const state = table.getState();
197
+ const isFiltered = state.columnFilters.length > 0 || state.globalFilter;
198
+ const isSorted = state.sorting.length > 0;
199
+
200
+ if (isFiltered || isSorted) {
201
+ toast.warning("Cannot reorder while filtering or sorting is active.");
202
+ return;
203
+ }
204
+
205
+ const { active, over } = event;
206
+ if (over && active.id !== over.id) {
207
+ const oldIndex = data.findIndex((item) => {
208
+ const id = getRowId ? getRowId(item) : (item as { id?: string | number }).id;
209
+ return id === active.id;
210
+ });
211
+ const newIndex = data.findIndex((item) => {
212
+ const id = getRowId ? getRowId(item) : (item as { id?: string | number }).id;
213
+ return id === over.id;
214
+ });
215
+
216
+ if (oldIndex !== -1 && newIndex !== -1) {
217
+ const newData = arrayMove(data, oldIndex, newIndex);
218
+ onReorder(newData);
219
+ }
220
+ }
221
+ }
222
+
223
+ const tableRows = table.getRowModel().rows;
224
+ const rowIds = React.useMemo(() => tableRows.map((row) => row.id), [tableRows]);
225
+
226
+ const tableContent = (
227
+ <UiTable
228
+ className={cn(
229
+ "w-full border-collapse",
230
+ enableColumnResizing ? "table-fixed" : "table-auto",
231
+ className,
232
+ )}
233
+ // กล่องชั้นในสุดคือ scroll container ที่ sticky header ยึด จึงต้องกำหนดความสูง
234
+ // ที่กล่องนี้ ไม่ใช่กล่องชั้นนอก
235
+ containerClassName={tableContainerClassName}
236
+ style={enableColumnResizing ? { width: table.getTotalSize() } : {}}
237
+ >
238
+ <TableHeader
239
+ className={stickyHeader ? "sticky top-0 z-10 bg-background shadow-sm" : undefined}
240
+ >
241
+ {table.getHeaderGroups().map((headerGroup) => (
242
+ <TableRow key={headerGroup.id}>
243
+ {headerGroup.headers.map((header) => {
244
+ return (
245
+ <TableHead
246
+ key={header.id}
247
+ colSpan={header.colSpan}
248
+ style={{
249
+ width: enableColumnResizing ? header.getSize() : undefined,
250
+ }}
251
+ className={cn(
252
+ "group relative whitespace-nowrap",
253
+ stickyEdgeColumns &&
254
+ header.column.id === "select" &&
255
+ "sticky left-0 z-20 bg-background",
256
+ stickyEdgeColumns &&
257
+ header.column.id === "actions" &&
258
+ "sticky right-0 z-20 bg-background",
259
+ )}
260
+ >
261
+ {header.isPlaceholder
262
+ ? null
263
+ : flexRender(header.column.columnDef.header, header.getContext())}
264
+ {header.column.getCanResize() && (
265
+ <div
266
+ onMouseDown={header.getResizeHandler()}
267
+ onTouchStart={header.getResizeHandler()}
268
+ className={`absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none select-none bg-border opacity-0 hover:bg-primary group-hover:opacity-100 ${
269
+ header.column.getIsResizing() ? "bg-primary opacity-100" : ""
270
+ }`}
271
+ />
272
+ )}
273
+ </TableHead>
274
+ );
275
+ })}
276
+ </TableRow>
277
+ ))}
278
+ </TableHeader>
279
+ <TableBody>
280
+ {isLoading ? (
281
+ Array.from({ length: initialPageSize }).map((_, i) => (
282
+ // biome-ignore lint/suspicious/noArrayIndexKey: แถว skeleton ไม่มีตัวตน ใช้ตำแหน่งเป็น key ได้
283
+ <TableRow key={i}>
284
+ {columns.map((_col, j) => (
285
+ // biome-ignore lint/suspicious/noArrayIndexKey: เซลล์ skeleton เช่นกัน
286
+ <TableCell key={j}>
287
+ <div className="h-4 w-full animate-pulse rounded bg-muted" />
288
+ </TableCell>
289
+ ))}
290
+ </TableRow>
291
+ ))
292
+ ) : table.getRowModel().rows?.length ? (
293
+ onReorder ? (
294
+ <SortableContext items={rowIds} strategy={verticalListSortingStrategy}>
295
+ {table.getRowModel().rows.map((row) => (
296
+ <DraggableRow
297
+ key={row.id}
298
+ row={row}
299
+ enableColumnResizing={enableColumnResizing}
300
+ getRowClassName={getRowClassName}
301
+ stickyEdgeColumns={stickyEdgeColumns}
302
+ />
303
+ ))}
304
+ </SortableContext>
305
+ ) : (
306
+ table.getRowModel().rows.map((row) => (
307
+ <TableRow
308
+ key={row.id}
309
+ data-state={row.getIsSelected() && "selected"}
310
+ className={cn(stickyEdgeColumns && "group/row", getRowClassName?.(row))}
311
+ >
312
+ {row.getVisibleCells().map((cell) => {
313
+ return (
314
+ <TableCell
315
+ key={cell.id}
316
+ style={{
317
+ width: enableColumnResizing ? cell.column.getSize() : undefined,
318
+ }}
319
+ className={cn(
320
+ stickyEdgeColumns &&
321
+ cell.column.id === "select" &&
322
+ "sticky left-0 z-10 bg-card group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
323
+ stickyEdgeColumns &&
324
+ cell.column.id === "actions" &&
325
+ "sticky right-0 z-10 bg-card group-hover/row:bg-muted group-data-[state=selected]/row:bg-muted",
326
+ )}
327
+ >
328
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
329
+ </TableCell>
330
+ );
331
+ })}
332
+ </TableRow>
333
+ ))
334
+ )
335
+ ) : (
336
+ <TableRow>
337
+ <TableCell colSpan={table.getVisibleLeafColumns().length} className="h-24 text-center">
338
+ {emptyStateComponent || "No results."}
339
+ </TableCell>
340
+ </TableRow>
341
+ )}
342
+ </TableBody>
343
+ </UiTable>
344
+ );
345
+
346
+ const customView = renderCustomView?.(table);
347
+
348
+ return (
349
+ <div className={cn("flex flex-col gap-4", containerClassName)}>
350
+ <DataTableToolbar
351
+ table={table}
352
+ facetedFilters={facetedFilters}
353
+ actionsComponent={actionsComponent?.(table)}
354
+ globalFilterPlaceholder={globalFilterPlaceholder}
355
+ toolbarFilters={toolbarFilters?.(table)}
356
+ preservedFilterIds={preservedFilterIds}
357
+ />
358
+ {customView ? (
359
+ <>
360
+ {customView}
361
+ <DataTablePagination
362
+ table={table}
363
+ pageSizeOptions={pageSizeOptions}
364
+ onPageSizeChange={onPageSizeChange}
365
+ />
366
+ </>
367
+ ) : (
368
+ <>
369
+ <div className="overflow-x-auto rounded-lg border bg-card shadow-sm max-sm:-mx-2 max-sm:rounded-none max-sm:border-x-0">
370
+ {onReorder ? (
371
+ <DndContext
372
+ sensors={sensors}
373
+ collisionDetection={closestCenter}
374
+ onDragEnd={handleDragEnd}
375
+ >
376
+ {tableContent}
377
+ </DndContext>
378
+ ) : (
379
+ tableContent
380
+ )}
381
+ </div>
382
+ <div
383
+ className={
384
+ stickyPagination
385
+ ? "sticky bottom-0 z-20 border-t bg-background py-2 max-sm:-mx-2"
386
+ : undefined
387
+ }
388
+ >
389
+ <DataTablePagination
390
+ table={table}
391
+ pageSizeOptions={pageSizeOptions}
392
+ onPageSizeChange={onPageSizeChange}
393
+ />
394
+ </div>
395
+ </>
396
+ )}
397
+ </div>
398
+ );
399
+ }
@@ -0,0 +1,9 @@
1
+ export * from "./data-table";
2
+ export * from "./data-table-column-header";
3
+ export * from "./data-table-date-range-filter";
4
+ export * from "./data-table-faceted-filter";
5
+ export * from "./data-table-pagination";
6
+ export * from "./data-table-toolbar";
7
+ export * from "./data-table-view-option";
8
+ export * from "./types";
9
+ export * from "./use-data-table";