@ciridae/ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/data-table-column-settings.d.ts +9 -6
- package/dist/components/ui/data-table-column-settings.js +123 -21
- package/dist/components/ui/data-table-context.js +30 -6
- package/dist/components/ui/data-table-grid.js +18 -4
- package/dist/components/ui/data-table-pagination.d.ts +7 -1
- package/dist/components/ui/data-table-pagination.js +40 -17
- package/dist/components/ui/data-table-tanstack.d.ts +26 -1
- package/dist/components/ui/data-table-tanstack.js +22 -4
- package/dist/components/ui/data-table.js +9 -1
- package/dist/components/ui/sidebar/layout.js +6 -1
- package/dist/components/ui/table.d.ts +12 -1
- package/dist/components/ui/table.js +9 -3
- package/package.json +1 -1
|
@@ -9,12 +9,15 @@ declare module "@tanstack/react-table" {
|
|
|
9
9
|
/**
|
|
10
10
|
* Choose which columns appear and in what order.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* A draft the reader commits rather than a live edit. Rearranging a table is
|
|
13
|
+
* several changes that only mean something together, and applying each one as
|
|
14
|
+
* it is made redraws the table under the hand still working on it. Cancel has
|
|
15
|
+
* to be able to mean "none of that", so nothing here touches the table until
|
|
16
|
+
* Apply.
|
|
16
17
|
*/
|
|
17
|
-
declare function DataTableColumnSettings({ title }: {
|
|
18
|
-
|
|
18
|
+
declare function DataTableColumnSettings({ description, title, triggerLabel, }: {
|
|
19
|
+
description?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
triggerLabel?: string;
|
|
19
22
|
}): React.JSX.Element | null;
|
|
20
23
|
export { DataTableColumnSettings };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
|
|
3
4
|
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
4
5
|
import { CSS } from "@dnd-kit/utilities";
|
|
@@ -7,50 +8,151 @@ import { cn } from "../../lib/utils";
|
|
|
7
8
|
import { useDataTable } from "./data-table-context";
|
|
8
9
|
import { Button } from "./button";
|
|
9
10
|
import { Checkbox } from "./checkbox";
|
|
10
|
-
import {
|
|
11
|
-
import { Separator } from "./separator";
|
|
11
|
+
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "./sheet";
|
|
12
12
|
/**
|
|
13
13
|
* Choose which columns appear and in what order.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* A draft the reader commits rather than a live edit. Rearranging a table is
|
|
16
|
+
* several changes that only mean something together, and applying each one as
|
|
17
|
+
* it is made redraws the table under the hand still working on it. Cancel has
|
|
18
|
+
* to be able to mean "none of that", so nothing here touches the table until
|
|
19
|
+
* Apply.
|
|
19
20
|
*/
|
|
20
|
-
function DataTableColumnSettings({ title }) {
|
|
21
|
+
function DataTableColumnSettings({ description = "Choose which columns appear and drag them into the order you prefer.", title = "Column settings", triggerLabel = "Settings", }) {
|
|
21
22
|
const table = useDataTable();
|
|
22
|
-
|
|
23
|
+
const [open, setOpen] = React.useState(false);
|
|
24
|
+
// `null` means "whatever the table is showing"; anything else is an edit in
|
|
25
|
+
// progress. Cleared on every open and close, so a cancelled edit cannot come
|
|
26
|
+
// back the next time the sheet is opened.
|
|
27
|
+
const [draft, setDraft] = React.useState(null);
|
|
23
28
|
const columns = table.getAllLeafColumns();
|
|
24
|
-
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
25
29
|
// Reordering stands on its own, so a table where nothing may be hidden still
|
|
26
30
|
// has something to offer here.
|
|
27
|
-
if (
|
|
31
|
+
if (columns.length === 0)
|
|
28
32
|
return null;
|
|
33
|
+
const current = draft ?? readColumns(table);
|
|
34
|
+
return (_jsxs(Sheet, { onOpenChange: (next) => {
|
|
35
|
+
setDraft(null);
|
|
36
|
+
setOpen(next);
|
|
37
|
+
}, open: open, children: [_jsx(SheetTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "ghost", children: [_jsx(SlidersHorizontalIcon, {}), _jsx("span", { children: triggerLabel })] }) }), _jsxs(SheetContent, { size: "md", children: [_jsxs(SheetHeader, { children: [_jsx(SheetTitle, { children: title }), _jsx(SheetDescription, { children: description })] }), _jsx(SheetBody, { className: "pb-(--panel-padding)", children: _jsx(ColumnDraftList, { columns: columns, draft: current, onDraftChange: setDraft }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { className: "mr-auto", onClick: () => {
|
|
38
|
+
setDraft(readDefaultColumns(table));
|
|
39
|
+
}, type: "button", variant: "ghost", children: "Reset" }), _jsx(Button, { onClick: () => {
|
|
40
|
+
setOpen(false);
|
|
41
|
+
}, type: "button", variant: "outline", children: "Cancel" }), _jsx(Button, { onClick: () => {
|
|
42
|
+
applyColumns(table, current);
|
|
43
|
+
setOpen(false);
|
|
44
|
+
}, type: "button", children: "Apply" })] })] })] }));
|
|
45
|
+
}
|
|
46
|
+
function ColumnDraftList({ columns, draft, onDraftChange, }) {
|
|
47
|
+
const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
|
|
48
|
+
const order = draft.order;
|
|
49
|
+
const byId = new Map(columns.map((column) => [column.id, column]));
|
|
29
50
|
const reorder = (event) => {
|
|
30
51
|
const { active, over } = event;
|
|
31
52
|
if (!over || active.id === over.id)
|
|
32
53
|
return;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const to = ids.indexOf(String(over.id));
|
|
54
|
+
const from = order.indexOf(String(active.id));
|
|
55
|
+
const to = order.indexOf(String(over.id));
|
|
36
56
|
if (from === -1 || to === -1)
|
|
37
57
|
return;
|
|
38
|
-
|
|
58
|
+
onDraftChange({ ...draft, order: arrayMove(order, from, to) });
|
|
39
59
|
};
|
|
40
|
-
return (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
return (_jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: reorder, sensors: sensors, children: _jsx(SortableContext, { items: order, strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "grid gap-0.5", children: order.map((id) => {
|
|
61
|
+
const column = byId.get(id);
|
|
62
|
+
if (column === undefined)
|
|
63
|
+
return null;
|
|
64
|
+
return (_jsx(ColumnSettingsRow, { column: column, hidden: draft.hidden.includes(id), onToggle: (visible) => {
|
|
65
|
+
onDraftChange({
|
|
66
|
+
hidden: visible
|
|
67
|
+
? draft.hidden.filter((entry) => entry !== id)
|
|
68
|
+
: [...draft.hidden, id],
|
|
69
|
+
order,
|
|
70
|
+
});
|
|
71
|
+
} }, id));
|
|
72
|
+
}) }) }) }));
|
|
44
73
|
}
|
|
45
|
-
function ColumnSettingsRow({ column }) {
|
|
74
|
+
function ColumnSettingsRow({ column, hidden, onToggle, }) {
|
|
46
75
|
const label = getColumnLabel(column);
|
|
47
76
|
const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
|
|
48
77
|
id: column.id,
|
|
49
78
|
});
|
|
50
|
-
return (_jsxs("li", { className: cn("flex items-center gap-
|
|
51
|
-
|
|
79
|
+
return (_jsxs("li", { className: cn("flex items-center gap-3 rounded-sm px-1 py-2 text-sm", isDragging && "relative z-10 bg-accent"), ref: setNodeRef, style: { transform: CSS.Translate.toString(transform), transition }, children: [_jsx("button", { "aria-label": `Reorder ${label}`, className: "cursor-grab text-muted-foreground outline-none focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50", type: "button", ...attributes, ...listeners, children: _jsx(GripVerticalIcon, { className: "size-4" }) }), _jsx(Checkbox, { checked: !hidden, disabled: !column.getCanHide(), id: `column-setting-${column.id}`, onCheckedChange: (checked) => {
|
|
80
|
+
onToggle(checked === true);
|
|
52
81
|
} }), _jsx("label", { className: "min-w-0 flex-1 truncate", htmlFor: `column-setting-${column.id}`, children: label })] }));
|
|
53
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* What the table is showing now, in the shape the draft edits.
|
|
85
|
+
*
|
|
86
|
+
* Both fields are spelled out rather than left empty to mean "unchanged": the
|
|
87
|
+
* list has to draw an order either way, and an empty one would have to be
|
|
88
|
+
* filled in from somewhere -- from the current order, which would make Reset
|
|
89
|
+
* write back the very arrangement it was asked to undo.
|
|
90
|
+
*/
|
|
91
|
+
function readColumns(table) {
|
|
92
|
+
const columns = table.getAllLeafColumns();
|
|
93
|
+
return {
|
|
94
|
+
hidden: columns.filter((column) => !column.getIsVisible()).map((column) => column.id),
|
|
95
|
+
order: columns.map((column) => column.id),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What Reset goes back to: the caller's own defaults, which `TanStackDataTable`
|
|
100
|
+
* keeps in `initialState` for exactly this.
|
|
101
|
+
*
|
|
102
|
+
* Not "every column, as declared" -- a table given a `defaultColumnVisibility`
|
|
103
|
+
* starts with something hidden on purpose, and a Reset that revealed it would
|
|
104
|
+
* be showing a view the caller never offered.
|
|
105
|
+
*
|
|
106
|
+
* A `columnOrder` is a partial list in TanStack: it names the columns it cares
|
|
107
|
+
* about and everything else keeps its declared position after them. The draft
|
|
108
|
+
* cannot be partial, because the list draws exactly what the draft names -- so
|
|
109
|
+
* a default naming two of five columns would make the other three vanish from
|
|
110
|
+
* the sheet, unreorderable and unhideable, and Apply would then write that
|
|
111
|
+
* two-column order back. Reconciling against the live leaf ids keeps the
|
|
112
|
+
* caller's prefix and restores the rest behind it.
|
|
113
|
+
*/
|
|
114
|
+
function readDefaultColumns(table) {
|
|
115
|
+
const { columnOrder, columnVisibility } = table.initialState;
|
|
116
|
+
// Declared order, not drawn order: `getAllLeafColumns` has already had the
|
|
117
|
+
// current order applied to it, so resetting through it would return the
|
|
118
|
+
// arrangement Reset was asked to undo.
|
|
119
|
+
const declared = table
|
|
120
|
+
.getAllFlatColumns()
|
|
121
|
+
.filter((column) => column.columns.length === 0)
|
|
122
|
+
.map((column) => column.id);
|
|
123
|
+
const named = (columnOrder ?? []).filter((id) => declared.includes(id));
|
|
124
|
+
return {
|
|
125
|
+
hidden: Object.entries(columnVisibility ?? {})
|
|
126
|
+
.filter(([, visible]) => !visible)
|
|
127
|
+
.map(([id]) => id),
|
|
128
|
+
order: [...named, ...declared.filter((id) => !named.includes(id))],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Commit a draft to the table.
|
|
133
|
+
*
|
|
134
|
+
* Back to the defaults goes through the table's own reset rather than through
|
|
135
|
+
* an equivalent-looking explicit order. Writing the full list would leave a
|
|
136
|
+
* caller mirroring this into a URL with an order parameter saying "arranged"
|
|
137
|
+
* for a view that is not, and would pin today's columns so a later release
|
|
138
|
+
* appended its new one at the end rather than where it was declared.
|
|
139
|
+
*/
|
|
140
|
+
function applyColumns(table, draft) {
|
|
141
|
+
if (isDefaultColumns(draft, readDefaultColumns(table))) {
|
|
142
|
+
table.resetColumnOrder();
|
|
143
|
+
table.resetColumnVisibility();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
table.setColumnOrder(draft.order);
|
|
147
|
+
table.setColumnVisibility(Object.fromEntries(draft.hidden.map((id) => [id, false])));
|
|
148
|
+
}
|
|
149
|
+
/** Whether a draft is the default arrangement, in whatever order it names it. */
|
|
150
|
+
function isDefaultColumns(draft, defaults) {
|
|
151
|
+
return (draft.order.length === defaults.order.length &&
|
|
152
|
+
draft.order.every((id, index) => defaults.order[index] === id) &&
|
|
153
|
+
draft.hidden.length === defaults.hidden.length &&
|
|
154
|
+
defaults.hidden.every((id) => draft.hidden.includes(id)));
|
|
155
|
+
}
|
|
54
156
|
/**
|
|
55
157
|
* What to call a column in a list of columns.
|
|
56
158
|
*
|
|
@@ -15,16 +15,40 @@ import * as React from "react";
|
|
|
15
15
|
*/
|
|
16
16
|
const DataTableContext = React.createContext(null);
|
|
17
17
|
function DataTableProvider({ children, table, }) {
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
18
|
+
// A fresh object every render, deliberately, and the one thing here that must
|
|
19
|
+
// not be memoised.
|
|
20
|
+
//
|
|
21
|
+
// A control like `DataTableFacetFilter` takes only static props -- a column
|
|
22
|
+
// id, a title, a list of options -- and reads everything that changes from
|
|
23
|
+
// this context. In an app compiled with React Compiler that makes its element
|
|
24
|
+
// cacheable: no reactive props, so the element is built once and React skips
|
|
25
|
+
// re-rendering it. Publishing the table object itself would close the other
|
|
26
|
+
// door too, because its identity never changes however much its state does,
|
|
27
|
+
// so context propagation would never fire either and the control would render
|
|
28
|
+
// once and then show the table as it was at mount forever.
|
|
29
|
+
//
|
|
30
|
+
// Wrapping restores propagation: the provider re-renders whenever the table's
|
|
31
|
+
// state changes, the value is new each time, and React re-renders every
|
|
32
|
+
// consumer regardless of what the compiler did to their elements.
|
|
33
|
+
//
|
|
34
|
+
// `state` is carried so a reader can see what the value is meant to track,
|
|
35
|
+
// and because TanStack rebuilds its state object each render where the table
|
|
36
|
+
// object is the same one forever.
|
|
37
|
+
//
|
|
38
|
+
// It is NOT a guard against this package one day being built with React
|
|
39
|
+
// Compiler. The compiler takes its dependencies from the sources of an
|
|
40
|
+
// expression, and the only source of `table.getState()` is `table` -- so it
|
|
41
|
+
// would cache the call on `[table]`, cache this literal on the result, and
|
|
42
|
+
// freeze the whole thing exactly as before. If that day comes, this
|
|
43
|
+
// component needs a real bailout, not another field.
|
|
44
|
+
return (_jsx(DataTableContext.Provider, { value: { state: table.getState(), table }, children: children }));
|
|
21
45
|
}
|
|
22
46
|
function useDataTable() {
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
47
|
+
const context = React.useContext(DataTableContext);
|
|
48
|
+
if (context === null) {
|
|
25
49
|
throw new Error("This control must be rendered inside a <TanStackDataTable />.");
|
|
26
50
|
}
|
|
27
51
|
// eslint-disable-next-line no-restricted-syntax -- restores the row type erased by the context above; the provider only ever stores a TanStackTable.
|
|
28
|
-
return table;
|
|
52
|
+
return context.table;
|
|
29
53
|
}
|
|
30
54
|
export { DataTableProvider, useDataTable };
|
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { ArrowDownIcon,
|
|
2
|
+
import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon } from "lucide-react";
|
|
3
3
|
import { flexRender, } from "@tanstack/react-table";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
5
|
import { Button } from "./button";
|
|
6
6
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
|
|
7
|
+
/** Font taken from the enclosing `TableHead`; see `DataTableHeaderCell`. */
|
|
8
|
+
const inheritType = { fontSize: "inherit", fontWeight: "inherit", lineHeight: "inherit" };
|
|
7
9
|
function DataTableGrid({ empty, getRowClassName, onRowClick, rows, table, }) {
|
|
8
|
-
|
|
10
|
+
// One scroller, owned by `data-table-content` outside this: nesting an
|
|
11
|
+
// `overflow-x-auto` inside it would give the sticky header a scrollport that
|
|
12
|
+
// only ever scrolls sideways, so the header would ride away with the rows.
|
|
13
|
+
return (_jsxs(Table, { scrollable: false, children: [_jsx(TableHeader, { className: "sticky top-0 z-10 bg-background", children: table.getHeaderGroups().map((group) => (_jsx(TableRow, { children: group.headers.map((header) => (_jsx(TableHead, { style: { width: header.getSize() }, children: header.isPlaceholder ? null : _jsx(DataTableHeaderCell, { header: header }) }, header.id))) }, group.id))) }), _jsx(TableBody, { children: rows.length ? (rows.map((row) => (_jsx(DataTableDataRow, { getRowClassName: getRowClassName, onRowClick: onRowClick, row: row }, row.id)))) : (_jsx(TableRow, { children: _jsx(TableCell, { className: "h-32 text-center text-sm text-muted-foreground", colSpan: table.getVisibleLeafColumns().length, children: empty }) })) })] }));
|
|
9
14
|
}
|
|
10
15
|
function DataTableDataRow({ getRowClassName, onRowClick, row, }) {
|
|
11
16
|
return (_jsx(TableRow, { className: cn(onRowClick && "cursor-pointer", getRowClassName?.(row)), "data-state": row.getIsSelected() ? "selected" : undefined, onClick: onRowClick ? () => onRowClick(row) : undefined, children: row.getVisibleCells().map((cell) => (_jsx(TableCell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))) }));
|
|
12
17
|
}
|
|
13
18
|
function DataTableHeaderCell({ header }) {
|
|
14
19
|
const sorted = header.column.getIsSorted();
|
|
15
|
-
|
|
20
|
+
// Paired chevrons while unsorted, a single arrow once sorted: the pair reads
|
|
21
|
+
// as "this can go either way", the arrow as "it went this way".
|
|
22
|
+
const Icon = sorted === "asc" ? ArrowUpIcon : sorted === "desc" ? ArrowDownIcon : ChevronsUpDownIcon;
|
|
16
23
|
if (!header.column.getCanSort())
|
|
17
24
|
return flexRender(header.column.columnDef.header, header.getContext());
|
|
18
|
-
return (_jsxs(Button, { "aria-pressed": Boolean(sorted), className: "
|
|
25
|
+
return (_jsxs(Button, { "aria-pressed": Boolean(sorted), className: "-ml-2 h-auto px-2 py-0 text-muted-foreground hover:bg-transparent hover:text-foreground",
|
|
26
|
+
// Type taken from the `<th>` this sits in, whatever that turns out to be,
|
|
27
|
+
// so a caller restyling its headers moves the sortable ones with them.
|
|
28
|
+
// Inline because Tailwind has no font-size or font-weight `inherit`
|
|
29
|
+
// utility, and an arbitrary-value one would be a second utility in the
|
|
30
|
+
// same group as the Button recipe's own -- decided by stylesheet order
|
|
31
|
+
// rather than by this file.
|
|
32
|
+
style: inheritType, onClick: header.column.getToggleSortingHandler(), size: "sm", type: "button", variant: "ghost", children: [_jsx("span", { children: flexRender(header.column.columnDef.header, header.getContext()) }), _jsx(Icon, {})] }));
|
|
19
33
|
}
|
|
20
34
|
export { DataTableGrid };
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Table as TanStackTable } from "@tanstack/react-table";
|
|
2
2
|
import type { DataTablePaginationVariant, ResolvedDataTableLabels } from "./data-table-tanstack";
|
|
3
|
-
declare function DataTablePagination<TData>({ labels, pageSizeOptions, selectedRows, table, totalRows, variant, }: {
|
|
3
|
+
declare function DataTablePagination<TData>({ footerControls, labels, pageSizeOptions, selectedRows, table, totalRows, variant, }: {
|
|
4
|
+
/**
|
|
5
|
+
* Controls that belong beside the row count rather than above the table --
|
|
6
|
+
* column settings, an export. Whatever governs the whole result set reads
|
|
7
|
+
* better next to the number saying how big it is.
|
|
8
|
+
*/
|
|
9
|
+
footerControls?: React.ReactNode;
|
|
4
10
|
labels: ResolvedDataTableLabels;
|
|
5
11
|
pageSizeOptions: number[];
|
|
6
12
|
selectedRows: number;
|
|
@@ -3,16 +3,20 @@ import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon,
|
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { NativeSelect, NativeSelectOption } from "./native-select";
|
|
5
5
|
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem } from "./pagination";
|
|
6
|
-
function DataTablePagination({ labels, pageSizeOptions, selectedRows, table, totalRows, variant = "compact", }) {
|
|
6
|
+
function DataTablePagination({ footerControls, labels, pageSizeOptions, selectedRows, table, totalRows, variant = "compact", }) {
|
|
7
7
|
const pagination = table.getState().pagination;
|
|
8
|
-
|
|
8
|
+
// `-1` is TanStack's "unknown", which a server-driven table reports until it
|
|
9
|
+
// is told a total. Clamping is right for the labels -- a reader is on page 1
|
|
10
|
+
// of at least 1 -- but a control that jumps to the end needs the difference.
|
|
11
|
+
const totalPages = table.getPageCount();
|
|
12
|
+
const pageCount = Math.max(totalPages, 1);
|
|
9
13
|
const page = Math.min(pagination.pageIndex + 1, pageCount);
|
|
10
14
|
const rowsShown = table.getRowModel().rows.length;
|
|
11
|
-
return (_jsxs("div", { "data-slot": "data-table-pagination", className: "flex flex-col gap-2 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between", children: [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
return (_jsxs("div", { "data-slot": "data-table-pagination", className: "flex flex-col gap-2 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "min-w-0 truncate", children: selectedRows > 0
|
|
16
|
+
? (labels.rowsSelected?.(selectedRows, totalRows) ??
|
|
17
|
+
`${selectedRows} of ${totalRows} row(s) selected.`)
|
|
18
|
+
: (labels.rowsShown?.(rowsShown, totalRows) ??
|
|
19
|
+
`${rowsShown} of ${totalRows} row(s) shown.`) }), footerControls] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [_jsx("span", { className: "shrink-0", children: labels.rowsPerPage }), _jsx(NativeSelect, { "aria-label": labels.rowsPerPageName, className: "min-w-20 shrink-0", onChange: (event) => table.setPageSize(Number(event.target.value)), size: "sm", value: pagination.pageSize, children: pageSizeOptions.map((pageSize) => (_jsx(NativeSelectOption, { value: pageSize, children: pageSize }, pageSize))) }), variant === "numbered" ? (_jsx(DataTablePager, { labels: labels, page: page, pageCount: pageCount, pageCountKnown: totalPages >= 0, table: table })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "min-w-20 text-center tabular-nums", children: labels.page?.(page, pageCount) ?? `Page ${page} of ${pageCount}` }), _jsx(DataTablePaginationButtons, { labels: labels, pageCountKnown: totalPages >= 0, table: table })] }))] })] }));
|
|
16
20
|
}
|
|
17
21
|
/**
|
|
18
22
|
* Pages as a numbered row rather than a step counter.
|
|
@@ -22,14 +26,33 @@ function DataTablePagination({ labels, pageSizeOptions, selectedRows, table, tot
|
|
|
22
26
|
* uses, so an out-of-range index highlights nothing rather than inventing a
|
|
23
27
|
* page that is not there.
|
|
24
28
|
*/
|
|
25
|
-
function DataTablePager({ page, pageCount, table, }) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
function DataTablePager({ labels, page, pageCount, pageCountKnown, table, }) {
|
|
30
|
+
const back = !table.getCanPreviousPage();
|
|
31
|
+
const forward = !table.getCanNextPage();
|
|
32
|
+
return (_jsx(Pagination, { className: "mx-0 w-auto justify-end", children: _jsxs(PaginationContent, { children: [_jsx(PagerStepButton, { disabled: back, icon: ChevronsLeftIcon, label: labels.firstPage, onClick: () => {
|
|
33
|
+
table.firstPage();
|
|
34
|
+
} }), _jsx(PagerStepButton, { disabled: back, icon: ChevronLeftIcon, label: labels.previousPage, onClick: () => {
|
|
35
|
+
table.previousPage();
|
|
36
|
+
} }), _jsx(PagerNumbers, { page: page, pageCount: pageCount, table: table }), _jsx(PagerStepButton, { disabled: forward, icon: ChevronRightIcon, label: labels.nextPage, onClick: () => {
|
|
37
|
+
table.nextPage();
|
|
38
|
+
} }), pageCountKnown ? (_jsx(PagerStepButton, { disabled: forward, icon: ChevronsRightIcon, label: labels.lastPage, onClick: () => {
|
|
39
|
+
table.lastPage();
|
|
40
|
+
} })) : null] }) }));
|
|
41
|
+
}
|
|
42
|
+
/** The numbers themselves, with an ellipsis wherever the row elides a run. */
|
|
43
|
+
function PagerNumbers({ page, pageCount, table, }) {
|
|
44
|
+
return (_jsx(_Fragment, { children: getPagerSteps(page, pageCount).map((step, index) => (_jsx(PaginationItem, { children: step === "gap" ? (_jsx(PaginationEllipsis, { className: "size-8" })) : (_jsx(Button, { "aria-current": step === page ? "page" : undefined, "aria-label": `Page ${String(step)}`, className: "tabular-nums", onClick: () => {
|
|
45
|
+
table.setPageIndex(step - 1);
|
|
46
|
+
}, size: "icon-sm", type: "button", variant: step === page ? "outline" : "ghost", children: step })) }, step === "gap" ? `gap-${String(index)}` : step))) }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One of the four steps either side of the numbers, which differ only in these.
|
|
50
|
+
*
|
|
51
|
+
* Ghost rather than outlined: a row of six bordered boxes reads as six equal
|
|
52
|
+
* things, and the only one worth pointing at is the page you are on.
|
|
53
|
+
*/
|
|
54
|
+
function PagerStepButton({ disabled, icon: Icon, label, onClick, }) {
|
|
55
|
+
return (_jsx(PaginationItem, { children: _jsx(Button, { "aria-label": label, disabled: disabled, onClick: onClick, size: "icon-sm", type: "button", variant: "ghost", children: _jsx(Icon, {}) }) }));
|
|
33
56
|
}
|
|
34
57
|
/**
|
|
35
58
|
* The page numbers to draw: always the first and last, always the current and
|
|
@@ -60,7 +83,7 @@ function getPagerSteps(page, pageCount) {
|
|
|
60
83
|
function countUp(from, to) {
|
|
61
84
|
return Array.from({ length: to - from + 1 }, (_, index) => from + index);
|
|
62
85
|
}
|
|
63
|
-
function DataTablePaginationButtons({ labels, table, }) {
|
|
64
|
-
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { "aria-label":
|
|
86
|
+
function DataTablePaginationButtons({ labels, pageCountKnown, table, }) {
|
|
87
|
+
return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { "aria-label": labels.firstPage, disabled: !table.getCanPreviousPage(), onClick: () => table.firstPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronsLeftIcon, {}) }), _jsx(Button, { "aria-label": labels.previousPage, disabled: !table.getCanPreviousPage(), onClick: () => table.previousPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronLeftIcon, {}) }), _jsx(Button, { "aria-label": labels.nextPage, disabled: !table.getCanNextPage(), onClick: () => table.nextPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronRightIcon, {}) }), pageCountKnown ? (_jsx(Button, { "aria-label": labels.lastPage, disabled: !table.getCanNextPage(), onClick: () => table.lastPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronsRightIcon, {}) })) : null] }));
|
|
65
88
|
}
|
|
66
89
|
export { DataTablePagination };
|
|
@@ -13,9 +13,19 @@ type DataTableLabels = {
|
|
|
13
13
|
columns?: string;
|
|
14
14
|
empty?: React.ReactNode;
|
|
15
15
|
filterPlaceholder?: string;
|
|
16
|
+
firstPage?: string;
|
|
17
|
+
lastPage?: string;
|
|
16
18
|
nextPage?: string;
|
|
17
19
|
page?: (page: number, pageCount: number) => string;
|
|
18
20
|
previousPage?: string;
|
|
21
|
+
/** Sits before the page-size control, which reads "Show 25" rather than "25 rows". */
|
|
22
|
+
rowsPerPage?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The page-size control's accessible name. Separate from `rowsPerPage`
|
|
25
|
+
* because that one is terse by design: "Show" beside a number says enough
|
|
26
|
+
* with the footer in view and nothing at all read out on its own.
|
|
27
|
+
*/
|
|
28
|
+
rowsPerPageName?: string;
|
|
19
29
|
rowsSelected?: (selected: number, total: number) => string;
|
|
20
30
|
rowsShown?: (shown: number, total: number) => string;
|
|
21
31
|
};
|
|
@@ -45,6 +55,15 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
|
|
|
45
55
|
enableSorting?: boolean;
|
|
46
56
|
filterMode?: DataTableFilterMode;
|
|
47
57
|
filterPlaceholder?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Controls for the footer, beside the row count. Whatever governs the whole
|
|
60
|
+
* result set -- column settings, an export -- reads better next to the number
|
|
61
|
+
* saying how big it is than stacked above the table with the filters.
|
|
62
|
+
*
|
|
63
|
+
* The footer is the pagination row, so `enablePagination={false}` removes
|
|
64
|
+
* these along with it; put them in `actions` for a table that does not page.
|
|
65
|
+
*/
|
|
66
|
+
footerControls?: React.ReactNode;
|
|
48
67
|
getRowClassName?: (row: Row<TData>) => string | undefined;
|
|
49
68
|
getRowId?: TableOptions<TData>["getRowId"];
|
|
50
69
|
labels?: DataTableLabels;
|
|
@@ -56,6 +75,12 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
|
|
|
56
75
|
onColumnOrderChange?: OnChangeFn<ColumnOrderState>;
|
|
57
76
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
|
|
58
77
|
onGlobalFilterChange?: OnChangeFn<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Called instead of `onPaginationChange` when `clampPageIndex` moves a page
|
|
80
|
+
* index that pointed past the end, so a caller storing the page can replace
|
|
81
|
+
* the address that was wrong rather than pushing the correction on top of it.
|
|
82
|
+
*/
|
|
83
|
+
onPageIndexCorrected?: (pageIndex: number) => void;
|
|
59
84
|
onPaginationChange?: OnChangeFn<PaginationState>;
|
|
60
85
|
onRowClick?: (row: Row<TData>) => void;
|
|
61
86
|
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
|
|
@@ -68,7 +93,7 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
|
|
|
68
93
|
tableOptions?: Partial<TableOptions<TData>>;
|
|
69
94
|
toolbar?: React.ReactNode;
|
|
70
95
|
};
|
|
71
|
-
type ResolvedDataTableLabels = Required<Pick<DataTableLabels, "columns" | "filterPlaceholder" | "nextPage" | "previousPage">> & DataTableLabels;
|
|
96
|
+
type ResolvedDataTableLabels = Required<Pick<DataTableLabels, "columns" | "filterPlaceholder" | "firstPage" | "lastPage" | "nextPage" | "previousPage" | "rowsPerPage" | "rowsPerPageName">> & DataTableLabels;
|
|
72
97
|
declare function TanStackDataTable<TData, TValue = unknown>(props: TanStackDataTableProps<TData, TValue>): React.JSX.Element;
|
|
73
98
|
export { TanStackDataTable };
|
|
74
99
|
export type { DataTableFilterMode, DataTableLabels, DataTablePaginationVariant, DataTableState, ResolvedDataTableLabels, TanStackDataTableProps, };
|
|
@@ -11,12 +11,16 @@ const defaultLabels = {
|
|
|
11
11
|
columns: "Columns",
|
|
12
12
|
empty: "No results.",
|
|
13
13
|
filterPlaceholder: "Filter rows...",
|
|
14
|
+
firstPage: "First page",
|
|
15
|
+
lastPage: "Last page",
|
|
14
16
|
nextPage: "Next page",
|
|
15
17
|
previousPage: "Previous page",
|
|
18
|
+
rowsPerPage: "Show",
|
|
19
|
+
rowsPerPageName: "Rows per page",
|
|
16
20
|
};
|
|
17
21
|
function TanStackDataTable(props) {
|
|
18
22
|
const table = useDataTableInstance(props);
|
|
19
|
-
useClampedPageIndex(table, props.clampPageIndex, props.loading);
|
|
23
|
+
useClampedPageIndex(table, props.clampPageIndex, props.loading, props.onPageIndexCorrected);
|
|
20
24
|
const labels = { ...defaultLabels, ...props.labels };
|
|
21
25
|
const rows = table.getRowModel().rows;
|
|
22
26
|
const pagination = table.getState().pagination;
|
|
@@ -25,7 +29,7 @@ function TanStackDataTable(props) {
|
|
|
25
29
|
props.actions ||
|
|
26
30
|
props.filterMode !== false ||
|
|
27
31
|
props.enableColumnVisibility !== false);
|
|
28
|
-
return (_jsx(DataTableProvider, { table: table, children: _jsxs("div", { "data-slot": "data-table", className: cn("
|
|
32
|
+
return (_jsx(DataTableProvider, { table: table, children: _jsxs("div", { "data-slot": "data-table", className: cn("flex flex-col gap-(--panel-gap)", props.className), children: [hasToolbar ? _jsx(DataTableTools, { labels: labels, props: props, table: table }) : null, _jsx("div", { "data-slot": "data-table-content", className: "min-h-0 flex-1 overflow-auto border-t border-border", children: props.loading ? (_jsx(TableSkeleton, { className: "rounded-none border-x-0 border-b-0", columns: table.getVisibleLeafColumns().length, rows: pagination.pageSize })) : (_jsx(DataTableGrid, { empty: props.empty ?? labels.empty, getRowClassName: props.getRowClassName, onRowClick: props.onRowClick, rows: rows, table: table })) }), props.enablePagination !== false ? (_jsx(DataTablePagination, { footerControls: props.footerControls, labels: labels, pageSizeOptions: props.pageSizeOptions ?? [10, 25, 50, 100], selectedRows: table.getFilteredSelectedRowModel().rows.length, table: table, totalRows: totalRows, variant: props.paginationVariant })) : null] }) }));
|
|
29
33
|
}
|
|
30
34
|
/**
|
|
31
35
|
* Move a page index that points past the last page back onto a page that
|
|
@@ -42,13 +46,27 @@ function TanStackDataTable(props) {
|
|
|
42
46
|
* caller's handler, which for a caller that navigates on pagination is a
|
|
43
47
|
* navigation -- not something to start doing to an existing table uninvited,
|
|
44
48
|
* or to do at all while an empty result still means "not yet".
|
|
49
|
+
*
|
|
50
|
+
* `onPageIndexCorrected` exists so that handler can tell a correction from a
|
|
51
|
+
* page turn. A caller writing the page into the URL wants a turn to be a place
|
|
52
|
+
* to come back to and a correction to replace the address that was wrong --
|
|
53
|
+
* and without this they arrive as the same call.
|
|
45
54
|
*/
|
|
46
|
-
function useClampedPageIndex(table, enabled, loading) {
|
|
55
|
+
function useClampedPageIndex(table, enabled, loading, onCorrected) {
|
|
47
56
|
const { pageIndex } = table.getState().pagination;
|
|
48
57
|
const lastPageIndex = Math.max(table.getPageCount() - 1, 0);
|
|
49
58
|
const outOfRange = enabled === true && loading !== true && pageIndex > lastPageIndex;
|
|
59
|
+
const correctedRef = React.useRef(onCorrected);
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
correctedRef.current = onCorrected;
|
|
62
|
+
}, [onCorrected]);
|
|
50
63
|
React.useEffect(() => {
|
|
51
|
-
if (outOfRange)
|
|
64
|
+
if (!outOfRange)
|
|
65
|
+
return;
|
|
66
|
+
const corrected = correctedRef.current;
|
|
67
|
+
if (corrected)
|
|
68
|
+
corrected(lastPageIndex);
|
|
69
|
+
else
|
|
52
70
|
table.setPageIndex(lastPageIndex);
|
|
53
71
|
}, [lastPageIndex, outOfRange, table]);
|
|
54
72
|
}
|
|
@@ -20,8 +20,16 @@ function DataTableEmpty({ className, ...props }) {
|
|
|
20
20
|
function DataTableSkeleton({ className, ...props }) {
|
|
21
21
|
return (_jsx(DataTableContent, { className: "border-t-0", children: _jsx(TableSkeleton, { className: cn("rounded-none border-x-0 border-b-0", className), ...props }) }));
|
|
22
22
|
}
|
|
23
|
+
/** Font taken from the enclosing `TableHead`; see `SortableTableHead`. */
|
|
24
|
+
const inheritType = { fontSize: "inherit", fontWeight: "inherit", lineHeight: "inherit" };
|
|
23
25
|
function SortableTableHead({ active = false, children, className, direction = "asc", onSort, align = "left", ...props }) {
|
|
24
26
|
const Icon = active ? (direction === "asc" ? ArrowUpIcon : ArrowDownIcon) : ArrowUpDownIcon;
|
|
25
|
-
return (_jsx(TableHead, { "aria-sort": active ? (direction === "asc" ? "ascending" : "descending") : "none", className: cn(align === "right" && "text-right", className), ...props, children: _jsxs(Button, { "aria-pressed": active, className: cn("
|
|
27
|
+
return (_jsx(TableHead, { "aria-sort": active ? (direction === "asc" ? "ascending" : "descending") : "none", className: cn(align === "right" && "text-right", className), ...props, children: _jsxs(Button, { "aria-pressed": active, className: cn("h-auto px-0 py-0 text-muted-foreground hover:bg-transparent hover:text-foreground", align === "right" ? "ml-auto justify-end" : "justify-start"), onClick: onSort, size: "sm",
|
|
28
|
+
// Type taken from the `<th>`, including a `className` the caller put
|
|
29
|
+
// there, so a restyled header row stays one typeface. Inline rather
|
|
30
|
+
// than a utility: Tailwind has no font-size or font-weight `inherit`,
|
|
31
|
+
// and an arbitrary-value one would compete with the Button recipe's
|
|
32
|
+
// own on stylesheet order.
|
|
33
|
+
style: inheritType, type: "button", variant: "ghost", children: [_jsx("span", { children: children }), _jsx(Icon, {})] }) }));
|
|
26
34
|
}
|
|
27
35
|
export { DataTable, DataTableToolbar, DataTableContent, DataTableEmpty, DataTableSkeleton, TanStackDataTable, SortableTableHead, };
|
|
@@ -53,7 +53,12 @@ function SidebarGap({ variant }) {
|
|
|
53
53
|
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)") }));
|
|
54
54
|
}
|
|
55
55
|
function SidebarInset({ className, ...props }) {
|
|
56
|
-
return (_jsx("main", { "data-slot": "sidebar-inset", className: cn(
|
|
56
|
+
return (_jsx("main", { "data-slot": "sidebar-inset", className: cn(
|
|
57
|
+
// `min-w-0` because this is a flex item beside the sidebar, and a flex
|
|
58
|
+
// item will not shrink below its content: without it a page holding
|
|
59
|
+
// anything wide -- a table with sized columns -- pushes the whole app
|
|
60
|
+
// sideways instead of scrolling inside its own box.
|
|
61
|
+
"relative flex w-full min-w-0 flex-1 flex-col bg-background", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-lg md:peer-data-[variant=inset]:shadow-xs md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", className), ...props }));
|
|
57
62
|
}
|
|
58
63
|
function SidebarInput({ className, ...props }) {
|
|
59
64
|
return (_jsx(Input, { "data-slot": "sidebar-input", "data-sidebar": "input", className: cn("h-8 w-full bg-background shadow-none", className), ...props }));
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
declare function Table({ className, ...props }: React.ComponentProps<"table">
|
|
2
|
+
declare function Table({ className, scrollable, ...props }: React.ComponentProps<"table"> & {
|
|
3
|
+
/**
|
|
4
|
+
* Whether the wrapper scrolls a too-wide table sideways. A caller that
|
|
5
|
+
* already scrolls the table from outside passes `false`: an `overflow` of
|
|
6
|
+
* anything but `visible` makes this wrapper a scrollport on *both* axes, so
|
|
7
|
+
* leaving it on would anchor a sticky header here rather than to the box
|
|
8
|
+
* that actually scrolls. Two overflow utilities cannot settle this between
|
|
9
|
+
* them -- `cn` does not dedupe them, and the axis-specific one wins -- so it
|
|
10
|
+
* is a switch rather than a class.
|
|
11
|
+
*/
|
|
12
|
+
scrollable?: boolean;
|
|
13
|
+
}): React.JSX.Element;
|
|
3
14
|
declare function TableHeader({ className, ...props }: React.ComponentProps<"thead">): React.JSX.Element;
|
|
4
15
|
declare function TableBody({ className, ...props }: React.ComponentProps<"tbody">): React.JSX.Element;
|
|
5
16
|
declare function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">): React.JSX.Element;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "../../lib/utils";
|
|
4
|
-
function Table({ className, ...props }) {
|
|
5
|
-
return (_jsx("div", { "data-slot": "table-container", className: "relative w-full overflow-x-auto", children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }) }));
|
|
4
|
+
function Table({ className, scrollable = true, ...props }) {
|
|
5
|
+
return (_jsx("div", { "data-slot": "table-container", className: cn("relative w-full", scrollable && "overflow-x-auto"), children: _jsx("table", { "data-slot": "table", className: cn("w-full caption-bottom text-sm", className), ...props }) }));
|
|
6
6
|
}
|
|
7
7
|
function TableHeader({ className, ...props }) {
|
|
8
8
|
return _jsx("thead", { "data-slot": "table-header", className: cn("[&_tr]:border-b", className), ...props });
|
|
@@ -17,7 +17,13 @@ function TableRow({ className, ...props }) {
|
|
|
17
17
|
return (_jsx("tr", { "data-slot": "table-row", className: cn("h-(--table-row-height) border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted", className), ...props }));
|
|
18
18
|
}
|
|
19
19
|
function TableHead({ className, ...props }) {
|
|
20
|
-
return (_jsx("th", { "data-slot": "table-head", className: cn(
|
|
20
|
+
return (_jsx("th", { "data-slot": "table-head", className: cn(
|
|
21
|
+
// A step down from the rows and a step up in weight, so the header
|
|
22
|
+
// reads as the label of the column rather than as its first entry.
|
|
23
|
+
// It lives here rather than on the sort button so that a table mixing
|
|
24
|
+
// sortable and plain columns draws one header row, not two typefaces
|
|
25
|
+
// side by side -- the sort buttons restate this pair to match.
|
|
26
|
+
"h-(--table-row-height) px-(--table-cell-padding-x) py-(--table-cell-padding-y) text-left align-middle text-xs font-medium tracking-normal whitespace-nowrap text-table-header-foreground normal-case [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props }));
|
|
21
27
|
}
|
|
22
28
|
function TableCell({ className, ...props }) {
|
|
23
29
|
return (_jsx("td", { "data-slot": "table-cell", className: cn("px-(--table-cell-padding-x) py-(--table-cell-padding-y) align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className), ...props }));
|