@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.
@@ -9,12 +9,15 @@ declare module "@tanstack/react-table" {
9
9
  /**
10
10
  * Choose which columns appear and in what order.
11
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.
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
- title: string;
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 { Popover, PopoverContent, PopoverTrigger } from "./popover";
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
- * 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.
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
- // Already the order the grid draws, column order and grouping applied.
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 (!columns.length)
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 ids = columns.map((column) => column.id);
34
- const from = ids.indexOf(String(active.id));
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
- table.setColumnOrder(arrayMove(ids, from, to));
58
+ onDraftChange({ ...draft, order: arrayMove(order, from, to) });
39
59
  };
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" }) })] })] }));
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-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);
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
- // `.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 });
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 table = React.useContext(DataTableContext);
24
- if (table === null) {
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, ArrowUpDownIcon, ArrowUpIcon } from "lucide-react";
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
- return (_jsxs(Table, { children: [_jsx(TableHeader, { 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 }) })) })] }));
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
- const Icon = sorted === "asc" ? ArrowUpIcon : sorted === "desc" ? ArrowDownIcon : ArrowUpDownIcon;
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: "text-meta -ml-2 h-auto px-2 py-0 text-muted-foreground hover:bg-transparent hover:text-foreground", onClick: header.column.getToggleSortingHandler(), size: "sm", type: "button", variant: "ghost", children: [_jsx("span", { children: flexRender(header.column.columnDef.header, header.getContext()) }), _jsx(Icon, {})] }));
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
- const pageCount = Math.max(table.getPageCount(), 1);
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: [_jsx("div", { className: "min-w-0", children: selectedRows > 0
12
- ? (labels.rowsSelected?.(selectedRows, totalRows) ??
13
- `${selectedRows} of ${totalRows} row(s) selected.`)
14
- : (labels.rowsShown?.(rowsShown, totalRows) ??
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 })] }))] })] }));
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
- 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, {}) }) })] }) }));
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": "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, {}) })] }));
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("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] }) }));
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("text-meta 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", type: "button", variant: "ghost", children: [_jsx("span", { children: children }), _jsx(Icon, {})] }) }));
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("relative flex w-full 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 }));
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">): React.JSX.Element;
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("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 }));
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 }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciridae/ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "repository": {