@ciridae/ui 0.3.7 → 0.4.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.
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import type { RowData } from "@tanstack/react-table";
3
+ declare module "@tanstack/react-table" {
4
+ interface ColumnMeta<TData extends RowData, TValue> {
5
+ /** What to call this column where its header cannot be read as text. */
6
+ label?: string;
7
+ }
8
+ }
9
+ /**
10
+ * Choose which columns appear and in what order.
11
+ *
12
+ * Every change applies immediately, like the search box and the facet filters
13
+ * beside it. A draft with its own Apply would be the one control in the toolbar
14
+ * whose state is not already the table's -- and in a table whose view lives in
15
+ * the URL, the draft would have nowhere to live until it was applied.
16
+ */
17
+ declare function DataTableColumnSettings({ title }: {
18
+ title: string;
19
+ }): React.JSX.Element | null;
20
+ export { DataTableColumnSettings };
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core";
3
+ import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
4
+ import { CSS } from "@dnd-kit/utilities";
5
+ import { GripVerticalIcon, SlidersHorizontalIcon } from "lucide-react";
6
+ import { cn } from "../../lib/utils";
7
+ import { useDataTable } from "./data-table-context";
8
+ import { Button } from "./button";
9
+ import { Checkbox } from "./checkbox";
10
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
11
+ import { Separator } from "./separator";
12
+ /**
13
+ * Choose which columns appear and in what order.
14
+ *
15
+ * Every change applies immediately, like the search box and the facet filters
16
+ * beside it. A draft with its own Apply would be the one control in the toolbar
17
+ * whose state is not already the table's -- and in a table whose view lives in
18
+ * the URL, the draft would have nowhere to live until it was applied.
19
+ */
20
+ function DataTableColumnSettings({ title }) {
21
+ const table = useDataTable();
22
+ // Already the order the grid draws, column order and grouping applied.
23
+ const columns = table.getAllLeafColumns();
24
+ const sensors = useSensors(useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }));
25
+ // Reordering stands on its own, so a table where nothing may be hidden still
26
+ // has something to offer here.
27
+ if (!columns.length)
28
+ return null;
29
+ const reorder = (event) => {
30
+ const { active, over } = event;
31
+ if (!over || active.id === over.id)
32
+ return;
33
+ const ids = columns.map((column) => column.id);
34
+ const from = ids.indexOf(String(active.id));
35
+ const to = ids.indexOf(String(over.id));
36
+ if (from === -1 || to === -1)
37
+ return;
38
+ table.setColumnOrder(arrayMove(ids, from, to));
39
+ };
40
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", children: [_jsx(SlidersHorizontalIcon, {}), _jsx("span", { children: title })] }) }), _jsxs(PopoverContent, { align: "end", className: "w-60 p-0", children: [_jsx("div", { className: "px-3 py-2 text-meta text-muted-foreground", children: title }), _jsx(Separator, {}), _jsx(DndContext, { collisionDetection: closestCenter, onDragEnd: reorder, sensors: sensors, children: _jsx(SortableContext, { items: columns.map((column) => column.id), strategy: verticalListSortingStrategy, children: _jsx("ul", { className: "grid gap-0.5 p-1", children: columns.map((column) => (_jsx(ColumnSettingsRow, { column: column }, column.id))) }) }) }), _jsx(Separator, {}), _jsx("div", { className: "p-1", children: _jsx(Button, { className: "w-full justify-center", onClick: () => {
41
+ table.resetColumnOrder();
42
+ table.resetColumnVisibility();
43
+ }, size: "sm", type: "button", variant: "ghost", children: "Reset" }) })] })] }));
44
+ }
45
+ function ColumnSettingsRow({ column }) {
46
+ const label = getColumnLabel(column);
47
+ const { attributes, isDragging, listeners, setNodeRef, transform, transition } = useSortable({
48
+ id: column.id,
49
+ });
50
+ return (_jsxs("li", { className: cn("flex items-center gap-2 rounded-sm px-2 py-1.5 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: column.getIsVisible(), disabled: !column.getCanHide(), id: `column-setting-${column.id}`, onCheckedChange: (checked) => {
51
+ column.toggleVisibility(checked === true);
52
+ } }), _jsx("label", { className: "min-w-0 flex-1 truncate", htmlFor: `column-setting-${column.id}`, children: label })] }));
53
+ }
54
+ /**
55
+ * What to call a column in a list of columns.
56
+ *
57
+ * A header rendered as a component -- a select-all checkbox, an icon -- has no
58
+ * text to borrow, and the column id behind it is a name for code rather than
59
+ * for a reader. `meta.label` is where a caller says what to call it instead.
60
+ */
61
+ function getColumnLabel(column) {
62
+ const label = column.columnDef.meta?.label;
63
+ if (typeof label === "string")
64
+ return label;
65
+ const header = column.columnDef.header;
66
+ return typeof header === "string" ? header : column.id;
67
+ }
68
+ export { DataTableColumnSettings };
@@ -0,0 +1,8 @@
1
+ import * as React from "react";
2
+ import type { Table as TanStackTable } from "@tanstack/react-table";
3
+ declare function DataTableProvider<TData>({ children, table, }: {
4
+ children: React.ReactNode;
5
+ table: TanStackTable<TData>;
6
+ }): React.JSX.Element;
7
+ declare function useDataTable<TData>(): TanStackTable<TData>;
8
+ export { DataTableProvider, useDataTable };
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ /**
4
+ * The table instance `TanStackDataTable` builds, published so the controls
5
+ * placed in its toolbar can read it.
6
+ *
7
+ * The alternative is a function-valued `toolbar` prop, which reads to lint as a
8
+ * component defined during render and would put that error in front of every
9
+ * consumer of a facet filter.
10
+ *
11
+ * Held as `unknown` because a context cannot be generic. Nothing is lost: the
12
+ * only value ever put in is the table `TanStackDataTable` just created, and the
13
+ * only readers are inside that provider, so the row type `useDataTable`
14
+ * restores is the one the caller's own columns were built from.
15
+ */
16
+ const DataTableContext = React.createContext(null);
17
+ function DataTableProvider({ children, table, }) {
18
+ // `.Provider` rather than the React 19 context-as-provider shorthand: this
19
+ // package's react peer is `>=18`, and every other context here does the same.
20
+ return _jsx(DataTableContext.Provider, { value: table, children: children });
21
+ }
22
+ function useDataTable() {
23
+ const table = React.useContext(DataTableContext);
24
+ if (table === null) {
25
+ throw new Error("This control must be rendered inside a <TanStackDataTable />.");
26
+ }
27
+ // 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;
29
+ }
30
+ export { DataTableProvider, useDataTable };
@@ -0,0 +1,23 @@
1
+ import * as React from "react";
2
+ type DataTableFacetOption = {
3
+ icon?: React.ComponentType<{
4
+ className?: string;
5
+ }>;
6
+ label: string;
7
+ value: string;
8
+ };
9
+ /**
10
+ * Narrow a column to a set of its own values.
11
+ *
12
+ * Counts come from the table's faceted row model, which `TanStackDataTable`
13
+ * already builds -- so an option can say how many rows it would leave without
14
+ * the caller counting anything itself. Pair with the `arrIncludesSome` filter
15
+ * function, which is what a multi-select selection means.
16
+ */
17
+ declare function DataTableFacetFilter({ columnId, options, title, }: {
18
+ columnId: string;
19
+ options: DataTableFacetOption[];
20
+ title: string;
21
+ }): React.JSX.Element;
22
+ export { DataTableFacetFilter };
23
+ export type { DataTableFacetOption };
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CheckIcon, PlusCircleIcon } from "lucide-react";
3
+ import { cn } from "../../lib/utils";
4
+ import { useDataTable } from "./data-table-context";
5
+ import { Badge } from "./badge";
6
+ import { Button } from "./button";
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, } from "./command";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
9
+ import { Separator } from "./separator";
10
+ /**
11
+ * Narrow a column to a set of its own values.
12
+ *
13
+ * Counts come from the table's faceted row model, which `TanStackDataTable`
14
+ * already builds -- so an option can say how many rows it would leave without
15
+ * the caller counting anything itself. Pair with the `arrIncludesSome` filter
16
+ * function, which is what a multi-select selection means.
17
+ */
18
+ function DataTableFacetFilter({ columnId, options, title, }) {
19
+ const column = useDataTable().getColumn(columnId);
20
+ const counts = column?.getFacetedUniqueValues();
21
+ const selected = new Set(readSelection(column?.getFilterValue()));
22
+ const select = (value) => {
23
+ const next = new Set(selected);
24
+ if (next.has(value))
25
+ next.delete(value);
26
+ else
27
+ next.add(value);
28
+ // An empty selection is no filter at all, not a filter matching nothing.
29
+ column?.setFilterValue(next.size ? [...next] : undefined);
30
+ };
31
+ return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "outline", children: [_jsx(PlusCircleIcon, {}), title, selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, { className: "mx-0.5 data-[orientation=vertical]:h-4", orientation: "vertical" }), _jsx(FacetSelectionSummary, { labels: options.filter((option) => selected.has(option.value)) })] })) : null] }) }), _jsx(PopoverContent, { align: "start", className: "w-52 p-0", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: title }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matches." }), _jsx(CommandGroup, { children: options.map((option) => (_jsx(FacetOptionItem, { count: counts?.get(option.value), onSelect: () => {
32
+ select(option.value);
33
+ }, option: option, selected: selected.has(option.value) }, option.value))) }), selected.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { className: "justify-center", onSelect: () => {
34
+ column?.setFilterValue(undefined);
35
+ }, children: "Clear" }) })] })) : null] })] }) })] }));
36
+ }
37
+ /** Names the choices while they still fit, then counts them. */
38
+ function FacetSelectionSummary({ labels }) {
39
+ if (labels.length > 2)
40
+ return _jsxs(Badge, { variant: "secondary", children: [labels.length, " selected"] });
41
+ return (_jsx(_Fragment, { children: labels.map((option) => (_jsx(Badge, { variant: "secondary", children: option.label }, option.value))) }));
42
+ }
43
+ function FacetOptionItem({ count, onSelect, option, selected, }) {
44
+ const Icon = option.icon;
45
+ return (_jsxs(CommandItem, { onSelect: onSelect, children: [_jsx("span", { "aria-hidden": true, className: cn("grid size-4 place-content-center rounded-[4px] border border-input", selected ? "border-primary bg-primary text-primary-foreground" : "opacity-50"), children: selected ? _jsx(CheckIcon, { className: "size-3.5" }) : null }), Icon ? _jsx(Icon, { className: "size-4 text-muted-foreground" }) : null, _jsx("span", { className: "truncate", children: option.label }), count === undefined ? null : (_jsx("span", { className: "ml-auto font-mono text-xs tabular-nums text-muted-foreground", children: count }))] }));
46
+ }
47
+ /** A column filter value is `unknown`; only an array of strings is a selection. */
48
+ function readSelection(value) {
49
+ if (!Array.isArray(value))
50
+ return [];
51
+ return value.filter((entry) => typeof entry === "string");
52
+ }
53
+ export { DataTableFacetFilter };
@@ -1,10 +1,11 @@
1
1
  import type { Table as TanStackTable } from "@tanstack/react-table";
2
- import type { ResolvedDataTableLabels } from "./data-table-tanstack";
3
- declare function DataTablePagination<TData>({ labels, pageSizeOptions, selectedRows, table, totalRows, }: {
2
+ import type { DataTablePaginationVariant, ResolvedDataTableLabels } from "./data-table-tanstack";
3
+ declare function DataTablePagination<TData>({ labels, pageSizeOptions, selectedRows, table, totalRows, variant, }: {
4
4
  labels: ResolvedDataTableLabels;
5
5
  pageSizeOptions: number[];
6
6
  selectedRows: number;
7
7
  table: TanStackTable<TData>;
8
8
  totalRows: number;
9
+ variant?: DataTablePaginationVariant;
9
10
  }): import("@types/react").JSX.Element;
10
11
  export { DataTablePagination };
@@ -1,8 +1,9 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, } from "lucide-react";
3
3
  import { Button } from "./button";
4
4
  import { NativeSelect, NativeSelectOption } from "./native-select";
5
- function DataTablePagination({ labels, pageSizeOptions, selectedRows, table, totalRows, }) {
5
+ import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem } from "./pagination";
6
+ function DataTablePagination({ labels, pageSizeOptions, selectedRows, table, totalRows, variant = "compact", }) {
6
7
  const pagination = table.getState().pagination;
7
8
  const pageCount = Math.max(table.getPageCount(), 1);
8
9
  const page = Math.min(pagination.pageIndex + 1, pageCount);
@@ -11,7 +12,53 @@ function DataTablePagination({ labels, pageSizeOptions, selectedRows, table, tot
11
12
  ? (labels.rowsSelected?.(selectedRows, totalRows) ??
12
13
  `${selectedRows} of ${totalRows} row(s) selected.`)
13
14
  : (labels.rowsShown?.(rowsShown, totalRows) ??
14
- `${rowsShown} of ${totalRows} row(s) shown.`) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(NativeSelect, { "aria-label": "Rows per page", onChange: (event) => table.setPageSize(Number(event.target.value)), size: "sm", value: pagination.pageSize, children: pageSizeOptions.map((pageSize) => (_jsxs(NativeSelectOption, { value: pageSize, children: [pageSize, " rows"] }, pageSize))) }), _jsx("div", { className: "min-w-20 text-center tabular-nums", children: labels.page?.(page, pageCount) ?? `Page ${page} of ${pageCount}` }), _jsx(DataTablePaginationButtons, { labels: labels, table: table })] })] }));
15
+ `${rowsShown} of ${totalRows} row(s) shown.`) }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(NativeSelect, { "aria-label": "Rows per page", onChange: (event) => table.setPageSize(Number(event.target.value)), size: "sm", value: pagination.pageSize, children: pageSizeOptions.map((pageSize) => (_jsxs(NativeSelectOption, { value: pageSize, children: [pageSize, " rows"] }, pageSize))) }), variant === "numbered" ? (_jsx(DataTablePager, { page: page, pageCount: pageCount, 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, table: table })] }))] })] }));
16
+ }
17
+ /**
18
+ * Pages as a numbered row rather than a step counter.
19
+ *
20
+ * Worth the extra width when a reader has a page in mind -- page 7 of a member
21
+ * directory is one click, not six. `pageCount` is the clamped count the label
22
+ * uses, so an out-of-range index highlights nothing rather than inventing a
23
+ * page that is not there.
24
+ */
25
+ function DataTablePager({ page, pageCount, table, }) {
26
+ return (_jsx(Pagination, { className: "mx-0 w-auto justify-end", children: _jsxs(PaginationContent, { children: [_jsx(PaginationItem, { children: _jsx(Button, { "aria-label": "Previous page", disabled: !table.getCanPreviousPage(), onClick: () => {
27
+ table.previousPage();
28
+ }, size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronLeftIcon, {}) }) }), 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: () => {
29
+ table.setPageIndex(step - 1);
30
+ }, size: "icon-sm", type: "button", variant: step === page ? "outline" : "ghost", children: step })) }, step === "gap" ? `gap-${String(index)}` : step))), _jsx(PaginationItem, { children: _jsx(Button, { "aria-label": "Next page", disabled: !table.getCanNextPage(), onClick: () => {
31
+ table.nextPage();
32
+ }, size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronRightIcon, {}) }) })] }) }));
33
+ }
34
+ /**
35
+ * The page numbers to draw: always the first and last, always the current and
36
+ * its neighbours, and a gap wherever that skips more than one page.
37
+ *
38
+ * The row is always seven slots wide or fewer, however many pages there are.
39
+ */
40
+ function getPagerSteps(page, pageCount) {
41
+ if (pageCount <= 7)
42
+ return countUp(1, pageCount);
43
+ let from = Math.min(Math.max(page - 1, 2), pageCount - 4);
44
+ let to = Math.max(Math.min(page + 1, pageCount - 1), 5);
45
+ // A gap hiding a single page is wider than the page it hides, so widen the
46
+ // window over it instead. Only one side can be one short of the end at a
47
+ // time above seven pages, so this cannot push the row past seven slots.
48
+ if (from === 3)
49
+ from = 2;
50
+ if (to === pageCount - 2)
51
+ to = pageCount - 1;
52
+ return [
53
+ 1,
54
+ ...(from > 2 ? ["gap"] : []),
55
+ ...countUp(from, to),
56
+ ...(to < pageCount - 1 ? ["gap"] : []),
57
+ pageCount,
58
+ ];
59
+ }
60
+ function countUp(from, to) {
61
+ return Array.from({ length: to - from + 1 }, (_, index) => from + index);
15
62
  }
16
63
  function DataTablePaginationButtons({ labels, table, }) {
17
64
  return (_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { "aria-label": "First page", 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, {}) }), _jsx(Button, { "aria-label": "Last page", disabled: !table.getCanNextPage(), onClick: () => table.lastPage(), size: "icon-sm", type: "button", variant: "outline", children: _jsx(ChevronsRightIcon, {}) })] }));
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The vocabulary a controlled `TanStackDataTable` is driven in.
3
+ *
4
+ * `TanStackDataTableProps` already exposes these types publicly -- a consumer
5
+ * cannot write `onSortingChange` or a `ColumnDef` without naming them. Without
6
+ * this module the only way to name them is to depend on `@tanstack/react-table`
7
+ * directly, which makes every consumer responsible for keeping a version in
8
+ * step with this package's. Re-exporting them keeps the table library an
9
+ * implementation detail on this side of the boundary.
10
+ */
11
+ export type { Cell, Column, ColumnDef, ColumnFiltersState, ColumnOrderState, Header, OnChangeFn, PaginationState, Row, RowSelectionState, SortingState, Table, TableOptions, Updater, VisibilityState, } from "@tanstack/react-table";
12
+ /**
13
+ * Resolve an `Updater` to the value it stands for.
14
+ *
15
+ * Every controlled handler receives either the next state or a function
16
+ * producing it from the current one, so a consumer that stores state elsewhere
17
+ * -- a URL, a store -- has to collapse that fork before it can write anywhere.
18
+ */
19
+ export { functionalUpdate } from "@tanstack/react-table";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Resolve an `Updater` to the value it stands for.
3
+ *
4
+ * Every controlled handler receives either the next state or a function
5
+ * producing it from the current one, so a consumer that stores state elsewhere
6
+ * -- a URL, a store -- has to collapse that fork before it can write anywhere.
7
+ */
8
+ export { functionalUpdate } from "@tanstack/react-table";
@@ -1,7 +1,8 @@
1
1
  import * as React from "react";
2
- import { type ColumnDef, type ColumnFiltersState, type OnChangeFn, type PaginationState, type Row, type RowSelectionState, type SortingState, type TableOptions, type VisibilityState } from "@tanstack/react-table";
2
+ import { type ColumnDef, type ColumnFiltersState, type ColumnOrderState, type OnChangeFn, type PaginationState, type Row, type RowSelectionState, type SortingState, type TableOptions, type VisibilityState } from "@tanstack/react-table";
3
3
  type DataTableState = {
4
4
  columnFilters?: ColumnFiltersState;
5
+ columnOrder?: ColumnOrderState;
5
6
  columnVisibility?: VisibilityState;
6
7
  globalFilter?: string;
7
8
  pagination?: PaginationState;
@@ -19,16 +20,26 @@ type DataTableLabels = {
19
20
  rowsShown?: (shown: number, total: number) => string;
20
21
  };
21
22
  type DataTableFilterMode = "global" | "column" | false;
23
+ type DataTablePaginationVariant = "compact" | "numbered";
22
24
  type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps<"div">, "children"> & {
23
25
  actions?: React.ReactNode;
26
+ clampPageIndex?: boolean;
24
27
  columns: ColumnDef<TData, TValue>[];
25
28
  data: TData[];
29
+ defaultColumnOrder?: ColumnOrderState;
26
30
  defaultColumnVisibility?: VisibilityState;
27
31
  defaultPageSize?: number;
28
32
  empty?: React.ReactNode;
29
33
  enableColumnFilters?: boolean;
30
34
  enableColumnVisibility?: boolean;
31
35
  enableGlobalFilter?: boolean;
36
+ /**
37
+ * Whether columns may be hidden at all, separately from whether the built-in
38
+ * columns menu is offered. They were the same switch while the menu was the
39
+ * only way to hide a column; a caller supplying its own control needs to turn
40
+ * the menu off without also making every column permanent.
41
+ */
42
+ enableHiding?: boolean;
32
43
  enablePagination?: boolean;
33
44
  enableRowSelection?: TableOptions<TData>["enableRowSelection"];
34
45
  enableSorting?: boolean;
@@ -42,6 +53,7 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
42
53
  manualPagination?: boolean;
43
54
  manualSorting?: boolean;
44
55
  onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>;
56
+ onColumnOrderChange?: OnChangeFn<ColumnOrderState>;
45
57
  onColumnVisibilityChange?: OnChangeFn<VisibilityState>;
46
58
  onGlobalFilterChange?: OnChangeFn<string>;
47
59
  onPaginationChange?: OnChangeFn<PaginationState>;
@@ -50,6 +62,7 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
50
62
  onSortingChange?: OnChangeFn<SortingState>;
51
63
  pageCount?: number;
52
64
  pageSizeOptions?: number[];
65
+ paginationVariant?: DataTablePaginationVariant;
53
66
  rowCount?: number;
54
67
  state?: DataTableState;
55
68
  tableOptions?: Partial<TableOptions<TData>>;
@@ -58,4 +71,4 @@ type TanStackDataTableProps<TData, TValue = unknown> = Omit<React.ComponentProps
58
71
  type ResolvedDataTableLabels = Required<Pick<DataTableLabels, "columns" | "filterPlaceholder" | "nextPage" | "previousPage">> & DataTableLabels;
59
72
  declare function TanStackDataTable<TData, TValue = unknown>(props: TanStackDataTableProps<TData, TValue>): React.JSX.Element;
60
73
  export { TanStackDataTable };
61
- export type { DataTableFilterMode, DataTableLabels, ResolvedDataTableLabels, TanStackDataTableProps, };
74
+ export type { DataTableFilterMode, DataTableLabels, DataTablePaginationVariant, DataTableState, ResolvedDataTableLabels, TanStackDataTableProps, };
@@ -4,6 +4,7 @@ import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFiltere
4
4
  import { cn } from "../../lib/utils";
5
5
  import { TableSkeleton } from "./loading-state";
6
6
  import { DataTableTools } from "./data-table-controls";
7
+ import { DataTableProvider } from "./data-table-context";
7
8
  import { DataTableGrid } from "./data-table-grid";
8
9
  import { DataTablePagination } from "./data-table-pagination";
9
10
  const defaultLabels = {
@@ -15,6 +16,7 @@ const defaultLabels = {
15
16
  };
16
17
  function TanStackDataTable(props) {
17
18
  const table = useDataTableInstance(props);
19
+ useClampedPageIndex(table, props.clampPageIndex, props.loading);
18
20
  const labels = { ...defaultLabels, ...props.labels };
19
21
  const rows = table.getRowModel().rows;
20
22
  const pagination = table.getState().pagination;
@@ -23,12 +25,38 @@ function TanStackDataTable(props) {
23
25
  props.actions ||
24
26
  props.filterMode !== false ||
25
27
  props.enableColumnVisibility !== false);
26
- return (_jsxs("div", { "data-slot": "data-table", className: cn("grid gap-(--panel-gap)", props.className), children: [hasToolbar ? _jsx(DataTableTools, { labels: labels, props: props, table: table }) : null, _jsx("div", { "data-slot": "data-table-content", className: "overflow-hidden 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, { labels: labels, pageSizeOptions: props.pageSizeOptions ?? [10, 25, 50, 100], selectedRows: table.getFilteredSelectedRowModel().rows.length, table: table, totalRows: totalRows })) : null] }));
28
+ return (_jsx(DataTableProvider, { table: table, children: _jsxs("div", { "data-slot": "data-table", className: cn("grid gap-(--panel-gap)", props.className), children: [hasToolbar ? _jsx(DataTableTools, { labels: labels, props: props, table: table }) : null, _jsx("div", { "data-slot": "data-table-content", className: "overflow-hidden 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, { labels: labels, pageSizeOptions: props.pageSizeOptions ?? [10, 25, 50, 100], selectedRows: table.getFilteredSelectedRowModel().rows.length, table: table, totalRows: totalRows, variant: props.paginationVariant })) : null] }) }));
29
+ }
30
+ /**
31
+ * Move a page index that points past the last page back onto a page that
32
+ * exists, reporting the correction through `onPaginationChange`.
33
+ *
34
+ * A caller that keeps the page in the URL can be handed any number at all --
35
+ * `?page=99` in a bookmark, or a page that was valid until a filter narrowed
36
+ * the rows under it. Without this the grid renders an empty body while the
37
+ * footer still counts rows the reader cannot reach, and nothing puts the
38
+ * address right. Correcting through the caller's own handler rather than
39
+ * quietly inside the table keeps the URL and the table saying one thing.
40
+ *
41
+ * Off by default, and skipped while `loading`: it reaches out through the
42
+ * caller's handler, which for a caller that navigates on pagination is a
43
+ * navigation -- not something to start doing to an existing table uninvited,
44
+ * or to do at all while an empty result still means "not yet".
45
+ */
46
+ function useClampedPageIndex(table, enabled, loading) {
47
+ const { pageIndex } = table.getState().pagination;
48
+ const lastPageIndex = Math.max(table.getPageCount() - 1, 0);
49
+ const outOfRange = enabled === true && loading !== true && pageIndex > lastPageIndex;
50
+ React.useEffect(() => {
51
+ if (outOfRange)
52
+ table.setPageIndex(lastPageIndex);
53
+ }, [lastPageIndex, outOfRange, table]);
27
54
  }
28
55
  // eslint-disable-next-line complexity -- TanStack table setup is declarative option wiring.
29
56
  function useDataTableInstance(props) {
30
57
  const [sorting, setSorting] = React.useState(props.state?.sorting ?? []);
31
58
  const [columnFilters, setColumnFilters] = React.useState(props.state?.columnFilters ?? []);
59
+ const [columnOrder, setColumnOrder] = React.useState(props.state?.columnOrder ?? props.defaultColumnOrder ?? []);
32
60
  const [columnVisibility, setColumnVisibility] = React.useState(props.state?.columnVisibility ?? props.defaultColumnVisibility ?? {});
33
61
  const [rowSelection, setRowSelection] = React.useState(props.state?.rowSelection ?? {});
34
62
  const [globalFilter, setGlobalFilter] = React.useState(props.state?.globalFilter ?? "");
@@ -37,12 +65,20 @@ function useDataTableInstance(props) {
37
65
  return useReactTable({
38
66
  data: props.data,
39
67
  columns: props.columns,
68
+ // What `table.resetColumnOrder()` and `resetColumnVisibility()` restore.
69
+ // Seeding the hooks above is not enough: reset reads `initialState`, so
70
+ // without this a Reset control would clear a caller's defaults rather
71
+ // than return to them.
72
+ initialState: {
73
+ columnOrder: props.defaultColumnOrder ?? [],
74
+ columnVisibility: props.defaultColumnVisibility ?? {},
75
+ },
40
76
  getRowId: props.getRowId,
41
77
  pageCount: props.pageCount,
42
78
  rowCount: props.rowCount,
43
79
  enableColumnFilters: props.enableColumnFilters ?? true,
44
80
  enableGlobalFilter: props.enableGlobalFilter ?? true,
45
- enableHiding: props.enableColumnVisibility ?? true,
81
+ enableHiding: props.enableHiding ?? props.enableColumnVisibility ?? true,
46
82
  enableRowSelection: props.enableRowSelection ?? false,
47
83
  enableSorting: props.enableSorting ?? true,
48
84
  manualFiltering: props.manualFiltering ?? false,
@@ -50,6 +86,7 @@ function useDataTableInstance(props) {
50
86
  manualSorting: props.manualSorting ?? false,
51
87
  state: {
52
88
  columnFilters: props.state?.columnFilters ?? columnFilters,
89
+ columnOrder: props.state?.columnOrder ?? columnOrder,
53
90
  columnVisibility: props.state?.columnVisibility ?? columnVisibility,
54
91
  globalFilter: props.state?.globalFilter ?? globalFilter,
55
92
  pagination: props.state?.pagination ?? pagination,
@@ -57,6 +94,7 @@ function useDataTableInstance(props) {
57
94
  sorting: props.state?.sorting ?? sorting,
58
95
  },
59
96
  onColumnFiltersChange: props.onColumnFiltersChange ?? setColumnFilters,
97
+ onColumnOrderChange: props.onColumnOrderChange ?? setColumnOrder,
60
98
  onColumnVisibilityChange: props.onColumnVisibilityChange ?? setColumnVisibility,
61
99
  onGlobalFilterChange: props.onGlobalFilterChange ?? setGlobalFilter,
62
100
  onPaginationChange: props.onPaginationChange ?? setPagination,
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-column-settings";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-column-settings";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-context";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-context";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-facet-filter";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-facet-filter";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-state";
@@ -0,0 +1 @@
1
+ export * from "../components/ui/data-table-state";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.3.7",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "repository": {