@nexub/react 5.2.0 → 5.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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "5.2.0",
2
+ "version": "5.4.0",
3
3
  "standard": "WCAG 2.2 Level AA",
4
4
  "method": {
5
5
  "automated": "axe-core rules tagged wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa, run on every Storybook story in light and dark on every pull request; a new violation fails the build.",
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { type ColumnDef, type RowData } from '@tanstack/react-table';
2
+ import { type ColumnDef, type RowSelectionState, type RowData, type VisibilityState, type ColumnSizingState, type ColumnOrderState, type ColumnPinningState } from '@tanstack/react-table';
3
3
  declare module '@tanstack/react-table' {
4
4
  interface ColumnMeta<TData extends RowData, TValue> {
5
5
  /** Horizontal alignment of the header and cells. Use 'end' for numbers. @default 'start' */
@@ -8,6 +8,19 @@ declare module '@tanstack/react-table' {
8
8
  }
9
9
  /** A DataTable column: TanStack's `ColumnDef`, with `meta.align` for numeric columns. */
10
10
  export type DataTableColumn<TData> = ColumnDef<TData, unknown>;
11
+ export type DataTableDensity = 'comfortable' | 'compact';
12
+ /** Which rows to render, and the space to leave above and below them. */
13
+ export interface DataTableRowWindow {
14
+ indexes: number[];
15
+ before: number;
16
+ after: number;
17
+ /** Ref for each rendered row, to measure its real height. */
18
+ measure?: (el: HTMLElement | null) => void;
19
+ /** True when rows are windowed: rows get aria-rowindex, the table aria-rowcount. */
20
+ virtual?: boolean;
21
+ }
22
+ /** A hook choosing the rendered rows. DataTableVirtual passes one; the default renders all. */
23
+ export type DataTableRowWindowHook = (count: number, getScrollElement: () => HTMLElement | null) => DataTableRowWindow;
11
24
  export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivElement> {
12
25
  columns: DataTableColumn<TData>[];
13
26
  /** Names the table for assistive tech (and is shown below it unless `captionHidden`). */
@@ -17,8 +30,44 @@ export interface DataTableProps<TData> extends React.HTMLAttributes<HTMLDivEleme
17
30
  data: TData[];
18
31
  enableSorting?: boolean;
19
32
  enableRowSelection?: boolean;
33
+ /**
34
+ * With `enableRowSelection`: a checkbox column with select-page in its
35
+ * header, and "Select all N" across pages. On by default from 6.0.
36
+ */
37
+ selectionColumn?: boolean;
38
+ /** Controlled selection, keyed by row id (see `getRowId`). */
39
+ rowSelection?: RowSelectionState;
40
+ onRowSelectionChange?: (selection: RowSelectionState) => void;
41
+ /** Stable row ids for selection; defaults to the row index. */
42
+ getRowId?: (row: TData, index: number) => string;
43
+ /** A floating bar while rows are selected: render your actions for the selected rows. */
44
+ bulkActions?: (rows: TData[], clear: () => void) => React.ReactNode;
45
+ density?: DataTableDensity;
46
+ defaultDensity?: DataTableDensity;
47
+ onDensityChange?: (density: DataTableDensity) => void;
48
+ columnVisibility?: VisibilityState;
49
+ onColumnVisibilityChange?: (visibility: VisibilityState) => void;
50
+ /** A "View" menu above the table: density and which columns show. */
51
+ viewOptions?: boolean;
52
+ /** Content at the start of the row above the table (filters, a title). */
53
+ toolbar?: React.ReactNode;
54
+ /** Drag a header's edge, or focus it and press Arrow keys, to resize columns. */
55
+ enableColumnResizing?: boolean;
56
+ columnSizing?: ColumnSizingState;
57
+ onColumnSizingChange?: (sizing: ColumnSizingState) => void;
58
+ /** Drag headers to reorder columns, or press Alt+Arrow on a focused header. */
59
+ enableColumnReordering?: boolean;
60
+ columnOrder?: ColumnOrderState;
61
+ onColumnOrderChange?: (order: ColumnOrderState) => void;
62
+ /** Columns that stay in view while the rest scroll sideways: `{ left: ['name'], right: ['actions'] }`. */
63
+ columnPinning?: ColumnPinningState;
64
+ onColumnPinningChange?: (pinning: ColumnPinningState) => void;
65
+ /** Cap the table's height. Rows scroll and the header stays in view. */
66
+ maxHeight?: number | string;
67
+ /** Rows to render; set by DataTableVirtual. */
68
+ rowWindow?: DataTableRowWindowHook;
20
69
  pagination?: {
21
70
  pageSize?: number;
22
71
  };
23
72
  }
24
- export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
73
+ export declare function DataTable<TData>({ columns, data, enableSorting, enableRowSelection, selectionColumn, rowSelection: rowSelectionProp, onRowSelectionChange, getRowId, bulkActions, density: densityProp, defaultDensity, onDensityChange, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, viewOptions, toolbar, enableColumnResizing, columnSizing: columnSizingProp, onColumnSizingChange, enableColumnReordering, columnOrder: columnOrderProp, onColumnOrderChange, columnPinning: columnPinningProp, onColumnPinningChange, maxHeight, rowWindow, pagination, caption, captionHidden, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: DataTableProps<TData>): React.JSX.Element;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import {
5
5
  useReactTable,
@@ -9,11 +9,17 @@ import {
9
9
  getPaginationRowModel,
10
10
  flexRender
11
11
  } from "@tanstack/react-table";
12
- import { ArrowUp, ArrowDown, ChevronsUpDown } from "@nexub/icons";
12
+ import { ArrowUp, ArrowDown, ChevronsUpDown, SlidersHorizontal, X } from "@nexub/icons";
13
13
  import { cn } from "../../utils/cn.js";
14
14
  import { disabledContainer, pressable } from "../../utils/control.js";
15
- import { useNexubMessages } from "../NexubProvider/NexubProvider.js";
15
+ import { floatingSurface } from "../../utils/surface.js";
16
+ import { useControllableState } from "../../hooks/useControllableState.js";
17
+ import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect.js";
18
+ import { useLocale, useNexubMessages } from "../NexubProvider/NexubProvider.js";
16
19
  import { Table } from "../Table/Table.js";
20
+ import { Checkbox } from "../Checkbox/Checkbox.js";
21
+ import { Button } from "../Button/Button.js";
22
+ import { DropdownMenu } from "../DropdownMenu/DropdownMenu.js";
17
23
  const alignText = {
18
24
  start: "text-start",
19
25
  end: "text-end",
@@ -47,7 +53,7 @@ const TableRow = React.forwardRef(
47
53
  {
48
54
  ref,
49
55
  className: cn(
50
- "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[var(--color-surface-container-low)]",
56
+ "border-b border-[var(--color-outline-variant)] transition-colors hover:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,transparent)] data-[state=selected]:bg-[color-mix(in_srgb,var(--color-primary)_10%,transparent)]",
51
57
  className
52
58
  ),
53
59
  ...props
@@ -79,11 +85,52 @@ const TableCell = React.forwardRef(({ className, ...props }, ref) => /* @__PURE_
79
85
  }
80
86
  ));
81
87
  TableCell.displayName = "DataTable.TableCell";
88
+ function Spacer({ height }) {
89
+ return /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", className: "border-0", children: /* @__PURE__ */ jsx(
90
+ "td",
91
+ {
92
+ ref: (el) => {
93
+ if (el) el.style.height = px(height);
94
+ },
95
+ className: "p-0"
96
+ }
97
+ ) });
98
+ }
99
+ const useAllRows = (count) => ({
100
+ indexes: Array.from({ length: count }, (_, i) => i),
101
+ before: 0,
102
+ after: 0
103
+ });
104
+ const px = (n) => typeof n === "number" ? `${n}px` : n;
105
+ const apply = (u, prev) => typeof u === "function" ? u(prev) : u;
106
+ const columnLabel = (c) => typeof c.columnDef.header === "string" ? c.columnDef.header : c.id;
82
107
  function DataTable({
83
108
  columns,
84
109
  data,
85
110
  enableSorting = false,
86
111
  enableRowSelection = false,
112
+ selectionColumn = false,
113
+ rowSelection: rowSelectionProp,
114
+ onRowSelectionChange,
115
+ getRowId,
116
+ bulkActions,
117
+ density: densityProp,
118
+ defaultDensity = "comfortable",
119
+ onDensityChange,
120
+ columnVisibility: columnVisibilityProp,
121
+ onColumnVisibilityChange,
122
+ viewOptions = false,
123
+ toolbar,
124
+ enableColumnResizing = false,
125
+ columnSizing: columnSizingProp,
126
+ onColumnSizingChange,
127
+ enableColumnReordering = false,
128
+ columnOrder: columnOrderProp,
129
+ onColumnOrderChange,
130
+ columnPinning: columnPinningProp,
131
+ onColumnPinningChange,
132
+ maxHeight,
133
+ rowWindow,
87
134
  pagination,
88
135
  caption,
89
136
  captionHidden = false,
@@ -93,26 +140,115 @@ function DataTable({
93
140
  ...props
94
141
  }) {
95
142
  const messages = useNexubMessages();
143
+ const { locale, dir } = useLocale();
96
144
  const [sorting, setSorting] = React.useState([]);
97
- const [rowSelection, setRowSelection] = React.useState({});
145
+ const [rowSelection = {}, setRowSelection] = useControllableState({
146
+ value: rowSelectionProp,
147
+ defaultValue: {},
148
+ onChange: onRowSelectionChange
149
+ });
150
+ const [density = "comfortable", setDensity] = useControllableState({
151
+ value: densityProp,
152
+ defaultValue: defaultDensity,
153
+ onChange: onDensityChange
154
+ });
155
+ const [columnVisibility = {}, setColumnVisibility] = useControllableState({
156
+ value: columnVisibilityProp,
157
+ defaultValue: {},
158
+ onChange: onColumnVisibilityChange
159
+ });
160
+ const [columnSizing = {}, setColumnSizing] = useControllableState({
161
+ value: columnSizingProp,
162
+ defaultValue: {},
163
+ onChange: onColumnSizingChange
164
+ });
165
+ const [columnOrder = [], setColumnOrder] = useControllableState({
166
+ value: columnOrderProp,
167
+ defaultValue: [],
168
+ onChange: onColumnOrderChange
169
+ });
170
+ const [pinningIn = {}, setColumnPinning] = useControllableState({
171
+ value: columnPinningProp,
172
+ defaultValue: {},
173
+ onChange: onColumnPinningChange
174
+ });
175
+ const [announcement, setAnnouncement] = React.useState("");
176
+ const tableRef = React.useRef(null);
177
+ const dragged = React.useRef(null);
98
178
  const [paginationState, setPaginationState] = React.useState({
99
179
  pageIndex: 0,
100
180
  pageSize: pagination?.pageSize ?? 20
101
181
  });
182
+ const withSelectColumn = enableRowSelection && selectionColumn;
183
+ const columnPinning = React.useMemo(
184
+ () => withSelectColumn && pinningIn.left?.length && !pinningIn.left.includes("__select") ? { ...pinningIn, left: ["__select", ...pinningIn.left] } : pinningIn,
185
+ [withSelectColumn, pinningIn]
186
+ );
187
+ const allColumns = React.useMemo(
188
+ () => withSelectColumn ? [
189
+ {
190
+ id: "__select",
191
+ enableSorting: false,
192
+ enableHiding: false,
193
+ header: ({ table: table2 }) => /* @__PURE__ */ jsx(
194
+ Checkbox,
195
+ {
196
+ "aria-label": messages.selectAllOnPage,
197
+ checked: table2.getIsAllPageRowsSelected() ? true : table2.getIsSomePageRowsSelected() ? "indeterminate" : false,
198
+ onCheckedChange: (v) => table2.toggleAllPageRowsSelected(v === true)
199
+ }
200
+ ),
201
+ cell: ({ row }) => /* @__PURE__ */ jsx(
202
+ Checkbox,
203
+ {
204
+ "aria-label": messages.selectRow,
205
+ checked: row.getIsSelected(),
206
+ disabled: !row.getCanSelect(),
207
+ onCheckedChange: (v) => row.toggleSelected(v === true)
208
+ }
209
+ )
210
+ },
211
+ ...columns
212
+ ] : columns,
213
+ [withSelectColumn, columns, messages.selectAllOnPage, messages.selectRow]
214
+ );
102
215
  const tableState = React.useMemo(
103
216
  () => ({
104
217
  sorting: enableSorting ? sorting : void 0,
105
218
  rowSelection: enableRowSelection ? rowSelection : void 0,
106
- pagination: pagination ? paginationState : void 0
219
+ pagination: pagination ? paginationState : void 0,
220
+ columnVisibility,
221
+ columnSizing,
222
+ columnOrder,
223
+ columnPinning
107
224
  }),
108
- [enableSorting, sorting, enableRowSelection, rowSelection, pagination, paginationState]
225
+ [
226
+ enableSorting,
227
+ sorting,
228
+ enableRowSelection,
229
+ rowSelection,
230
+ pagination,
231
+ paginationState,
232
+ columnVisibility,
233
+ columnSizing,
234
+ columnOrder,
235
+ columnPinning
236
+ ]
109
237
  );
110
238
  const table = useReactTable({
111
239
  data,
112
- columns,
240
+ columns: allColumns,
113
241
  state: tableState,
242
+ getRowId,
114
243
  onSortingChange: enableSorting ? setSorting : void 0,
115
- onRowSelectionChange: enableRowSelection ? setRowSelection : void 0,
244
+ onRowSelectionChange: enableRowSelection ? (u) => setRowSelection(apply(u, rowSelection)) : void 0,
245
+ onColumnVisibilityChange: (u) => setColumnVisibility(apply(u, columnVisibility)),
246
+ onColumnSizingChange: (u) => setColumnSizing(apply(u, columnSizing)),
247
+ onColumnOrderChange: (u) => setColumnOrder(apply(u, columnOrder)),
248
+ onColumnPinningChange: (u) => setColumnPinning(apply(u, columnPinning)),
249
+ enableColumnResizing,
250
+ columnResizeMode: "onChange",
251
+ columnResizeDirection: dir,
116
252
  onPaginationChange: pagination ? setPaginationState : void 0,
117
253
  getCoreRowModel: getCoreRowModel(),
118
254
  getSortedRowModel: enableSorting ? getSortedRowModel() : void 0,
@@ -121,58 +257,238 @@ function DataTable({
121
257
  enableRowSelection,
122
258
  manualPagination: false
123
259
  });
124
- return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), ...props, children: [
125
- /* @__PURE__ */ jsxs(TableRoot, { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, children: [
126
- caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
127
- /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
128
- const align = header.column.columnDef.meta?.align ?? "start";
129
- return /* @__PURE__ */ jsx(
130
- TableHead,
131
- {
132
- className: alignText[align],
133
- "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
134
- children: header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
135
- "button",
260
+ const compact = density === "compact";
261
+ const selectedRows = enableRowSelection ? table.getSelectedRowModel().rows : [];
262
+ const totalRows = table.getCoreRowModel().rows.length;
263
+ const fmt = (n) => new Intl.NumberFormat(locale).format(n);
264
+ const multiSort = sorting.length > 1;
265
+ const offerAll = withSelectColumn && !!pagination && table.getIsAllPageRowsSelected() && totalRows > table.getRowModel().rows.length;
266
+ const clear = () => table.resetRowSelection(true);
267
+ const rows = table.getRowModel().rows;
268
+ const useRowWindow = rowWindow ?? useAllRows;
269
+ const rowWin = useRowWindow(rows.length, () => tableRef.current?.parentElement ?? null);
270
+ const pinned = (columnPinning.left?.length ?? 0) + (columnPinning.right?.length ?? 0) > 0;
271
+ useIsomorphicLayoutEffect(() => {
272
+ const t = tableRef.current;
273
+ if (!t) return;
274
+ t.style.width = enableColumnResizing ? px(table.getTotalSize()) : "";
275
+ const scroller = t.parentElement;
276
+ if (scroller) scroller.style.maxHeight = maxHeight == null ? "" : px(maxHeight);
277
+ });
278
+ const placeCell = (column) => (el) => {
279
+ if (!el) return;
280
+ el.style.width = enableColumnResizing ? px(column.getSize()) : "";
281
+ const side = column.getIsPinned();
282
+ el.style.insetInlineStart = side === "left" ? px(column.getStart("left")) : "";
283
+ el.style.insetInlineEnd = side === "right" ? px(column.getAfter("right")) : "";
284
+ };
285
+ const pinClass = (column) => {
286
+ const side = column.getIsPinned();
287
+ if (!side) return void 0;
288
+ return cn(
289
+ "sticky z-[1] bg-[var(--color-surface)]",
290
+ "[tr:hover>&]:bg-[color-mix(in_srgb,var(--color-on-surface)_8%,var(--color-surface))]",
291
+ "[tr[data-state=selected]>&]:bg-[color-mix(in_srgb,var(--color-primary)_10%,var(--color-surface))]",
292
+ side === "left" && column.getIsLastColumn("left") && "border-e border-[var(--color-outline-variant)]",
293
+ side === "right" && column.getIsFirstColumn("right") && "border-s border-[var(--color-outline-variant)]"
294
+ );
295
+ };
296
+ const moveColumn = (id, to) => {
297
+ const ids = table.getAllLeafColumns().map((c) => c.id);
298
+ const order = (columnOrder.length ? columnOrder : ids).filter((c) => c !== "__select" && ids.includes(c));
299
+ for (const c of ids) if (c !== "__select" && !order.includes(c)) order.push(c);
300
+ const from = order.indexOf(id);
301
+ const target = typeof to === "number" ? from + to : order.indexOf(to);
302
+ if (from < 0 || target < 0 || target >= order.length || target === from) return;
303
+ order.splice(from, 1);
304
+ order.splice(target, 0, id);
305
+ setColumnOrder(withSelectColumn ? ["__select", ...order] : order);
306
+ if (typeof to === "number") {
307
+ const col = table.getColumn(id);
308
+ setAnnouncement(messages.columnMoved(col ? columnLabel(col) : id, target + 1, order.length));
309
+ }
310
+ };
311
+ return /* @__PURE__ */ jsxs("div", { className: cn("space-y-4", className), "data-density": density, ...props, children: [
312
+ (toolbar != null || viewOptions) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
313
+ toolbar,
314
+ viewOptions && /* @__PURE__ */ jsxs(DropdownMenu.Root, { children: [
315
+ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "ms-auto", children: [
316
+ /* @__PURE__ */ jsx(SlidersHorizontal, { size: "sm", "aria-hidden": "true" }),
317
+ messages.view
318
+ ] }) }),
319
+ /* @__PURE__ */ jsxs(DropdownMenu.Content, { align: "end", children: [
320
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.density }),
321
+ /* @__PURE__ */ jsxs(DropdownMenu.RadioGroup, { value: density, onValueChange: (v) => setDensity(v), children: [
322
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "comfortable", children: messages.comfortable }),
323
+ /* @__PURE__ */ jsx(DropdownMenu.RadioItem, { value: "compact", children: messages.compact })
324
+ ] }),
325
+ /* @__PURE__ */ jsx(DropdownMenu.Separator, {}),
326
+ /* @__PURE__ */ jsx(DropdownMenu.Label, { children: messages.columns }),
327
+ table.getAllLeafColumns().filter((c) => c.getCanHide()).map((c) => /* @__PURE__ */ jsx(
328
+ DropdownMenu.CheckboxItem,
329
+ {
330
+ checked: c.getIsVisible(),
331
+ onCheckedChange: (v) => c.toggleVisibility(v === true),
332
+ onSelect: (e) => e.preventDefault(),
333
+ children: columnLabel(c)
334
+ },
335
+ c.id
336
+ ))
337
+ ] })
338
+ ] })
339
+ ] }),
340
+ offerAll && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 rounded-shape-container bg-[var(--color-surface-container-low)] px-4 py-2 text-body-sm", children: table.getIsAllRowsSelected() ? /* @__PURE__ */ jsxs(Fragment, { children: [
341
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(totalRows)) }),
342
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: clear, children: messages.clearSelection })
343
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
344
+ /* @__PURE__ */ jsx("span", { children: messages.selectedCount(fmt(selectedRows.length)) }),
345
+ /* @__PURE__ */ jsx(Button, { variant: "link", size: "sm", onClick: () => table.toggleAllRowsSelected(true), children: messages.selectAllRows(fmt(totalRows)) })
346
+ ] }) }),
347
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "sr-only", children: announcement }),
348
+ /* @__PURE__ */ jsxs(
349
+ TableRoot,
350
+ {
351
+ ref: tableRef,
352
+ "aria-label": ariaLabel,
353
+ "aria-labelledby": ariaLabelledBy,
354
+ "aria-rowcount": rowWin.virtual ? rows.length + 1 : void 0,
355
+ className: cn(enableColumnResizing && "table-fixed"),
356
+ children: [
357
+ caption != null && /* @__PURE__ */ jsx(Table.Caption, { className: cn(captionHidden && "sr-only"), children: caption }),
358
+ /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { className: "hover:bg-transparent", children: headerGroup.headers.map((header) => {
359
+ const align = header.column.columnDef.meta?.align ?? "start";
360
+ const col = header.column;
361
+ const reorderable = enableColumnReordering && col.id !== "__select" && !col.getIsPinned();
362
+ const sortable = enableSorting && col.getCanSort();
363
+ return /* @__PURE__ */ jsxs(
364
+ TableHead,
136
365
  {
137
- type: "button",
138
- onClick: header.column.getToggleSortingHandler(),
366
+ ref: placeCell(col),
139
367
  className: cn(
140
- "flex w-full items-center gap-1 hover:text-[var(--color-on-surface)]",
141
- alignJustify[align]
368
+ alignText[align],
369
+ compact && "h-8",
370
+ col.id === "__select" && "w-10",
371
+ // Anchors the resize handle; sticky (below) wins when set and anchors it too.
372
+ (enableColumnResizing || reorderable) && "relative",
373
+ maxHeight != null && "sticky top-0 z-[2] bg-[var(--color-surface)]",
374
+ pinClass(col),
375
+ maxHeight != null && col.getIsPinned() && "z-[3] [tr:hover>&]:bg-[var(--color-surface)]",
376
+ reorderable && "cursor-grab"
142
377
  ),
378
+ tabIndex: reorderable && !sortable ? 0 : void 0,
379
+ draggable: reorderable || void 0,
380
+ onDragStart: reorderable ? () => dragged.current = col.id : void 0,
381
+ onDragOver: reorderable ? (e) => e.preventDefault() : void 0,
382
+ onDrop: reorderable ? (e) => {
383
+ e.preventDefault();
384
+ if (dragged.current) moveColumn(dragged.current, col.id);
385
+ dragged.current = null;
386
+ } : void 0,
387
+ onKeyDown: reorderable ? (e) => {
388
+ if (!e.altKey || e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
389
+ e.preventDefault();
390
+ const forward = e.key === "ArrowRight" === (dir !== "rtl");
391
+ moveColumn(col.id, forward ? 1 : -1);
392
+ } : void 0,
393
+ "aria-sort": enableSorting && header.column.getCanSort() ? header.column.getIsSorted() === "asc" ? "ascending" : header.column.getIsSorted() === "desc" ? "descending" : "none" : void 0,
143
394
  children: [
144
- flexRender(header.column.columnDef.header, header.getContext()),
145
- header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
395
+ header.isPlaceholder ? null : enableSorting && header.column.getCanSort() ? /* @__PURE__ */ jsxs(
396
+ "button",
397
+ {
398
+ type: "button",
399
+ onClick: header.column.getToggleSortingHandler(),
400
+ className: cn(
401
+ "flex w-full items-center gap-1 hover:text-[var(--color-on-surface)]",
402
+ alignJustify[align]
403
+ ),
404
+ children: [
405
+ flexRender(header.column.columnDef.header, header.getContext()),
406
+ multiSort && header.column.getIsSorted() && /* @__PURE__ */ jsxs("span", { className: "text-[10px] tabular-nums leading-none", children: [
407
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: header.column.getSortIndex() + 1 }),
408
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: messages.sortOrder(header.column.getSortIndex() + 1) })
409
+ ] }),
410
+ header.column.getIsSorted() === "asc" ? /* @__PURE__ */ jsx(ArrowUp, { size: "xs", "aria-hidden": "true" }) : header.column.getIsSorted() === "desc" ? /* @__PURE__ */ jsx(ArrowDown, { size: "xs", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronsUpDown, { size: "xs", className: "opacity-40", "aria-hidden": "true" })
411
+ ]
412
+ }
413
+ ) : flexRender(header.column.columnDef.header, header.getContext()),
414
+ enableColumnResizing && col.getCanResize() && /* @__PURE__ */ jsx(
415
+ "div",
416
+ {
417
+ role: "separator",
418
+ "aria-orientation": "vertical",
419
+ "aria-label": messages.resizeColumn(columnLabel(col)),
420
+ "aria-valuenow": header.getSize(),
421
+ "aria-valuemin": col.columnDef.minSize ?? 20,
422
+ "aria-valuemax": col.columnDef.maxSize ?? 1e3,
423
+ tabIndex: 0,
424
+ draggable: false,
425
+ onMouseDown: header.getResizeHandler(),
426
+ onTouchStart: header.getResizeHandler(),
427
+ onDoubleClick: () => col.resetSize(),
428
+ onKeyDown: (e) => {
429
+ if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
430
+ e.preventDefault();
431
+ e.stopPropagation();
432
+ const grow = e.key === "ArrowRight" === (dir !== "rtl");
433
+ const min = col.columnDef.minSize ?? 20;
434
+ const max = col.columnDef.maxSize ?? 1e3;
435
+ const size = Math.min(max, Math.max(min, header.getSize() + (grow ? 16 : -16)));
436
+ table.setColumnSizing((s) => ({ ...s, [col.id]: size }));
437
+ },
438
+ className: cn(
439
+ "absolute inset-y-0 end-0 z-[1] w-2 cursor-col-resize touch-none select-none outline-none",
440
+ "after:absolute after:inset-y-2 after:end-0 after:w-px after:bg-[var(--color-outline-variant)]",
441
+ "hover:after:w-0.5 hover:after:bg-[var(--color-primary)] focus-visible:after:w-0.5 focus-visible:after:bg-[var(--color-primary)]",
442
+ col.getIsResizing() && "after:w-0.5 after:bg-[var(--color-primary)]"
443
+ )
444
+ }
445
+ )
146
446
  ]
147
- }
148
- ) : flexRender(header.column.columnDef.header, header.getContext())
149
- },
150
- header.id
151
- );
152
- }) }, headerGroup.id)) }),
153
- /* @__PURE__ */ jsx(TableBody, { children: table.getRowModel().rows.length ? table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx(
154
- TableRow,
155
- {
156
- "data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
157
- children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
447
+ },
448
+ header.id
449
+ );
450
+ }) }, headerGroup.id)) }),
451
+ /* @__PURE__ */ jsx(TableBody, { children: rows.length ? /* @__PURE__ */ jsxs(Fragment, { children: [
452
+ rowWin.before > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.before }),
453
+ rowWin.indexes.map((i) => {
454
+ const row = rows[i];
455
+ return /* @__PURE__ */ jsx(
456
+ TableRow,
457
+ {
458
+ ref: rowWin.measure,
459
+ "data-index": i,
460
+ "aria-rowindex": rowWin.virtual ? i + 2 : void 0,
461
+ "data-state": enableRowSelection && row.getIsSelected() ? "selected" : void 0,
462
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(
463
+ TableCell,
464
+ {
465
+ ref: pinned || enableColumnResizing ? placeCell(cell.column) : void 0,
466
+ className: cn(
467
+ alignText[cell.column.columnDef.meta?.align ?? "start"],
468
+ compact && "py-1.5",
469
+ pinClass(cell.column),
470
+ enableColumnResizing && "truncate"
471
+ ),
472
+ children: flexRender(cell.column.columnDef.cell, cell.getContext())
473
+ },
474
+ cell.id
475
+ ))
476
+ },
477
+ row.id
478
+ );
479
+ }),
480
+ rowWin.after > 0 && /* @__PURE__ */ jsx(Spacer, { height: rowWin.after })
481
+ ] }) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
158
482
  TableCell,
159
483
  {
160
- className: alignText[cell.column.columnDef.meta?.align ?? "start"],
161
- children: flexRender(cell.column.columnDef.cell, cell.getContext())
162
- },
163
- cell.id
164
- ))
165
- },
166
- row.id
167
- )) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
168
- TableCell,
169
- {
170
- colSpan: columns.length,
171
- className: "h-24 text-center text-[var(--color-on-surface-muted)]",
172
- children: messages.noResults
173
- }
174
- ) }) })
175
- ] }),
484
+ colSpan: table.getVisibleLeafColumns().length,
485
+ className: "h-24 text-center text-[var(--color-on-surface-muted)]",
486
+ children: messages.noResults
487
+ }
488
+ ) }) })
489
+ ]
490
+ }
491
+ ),
176
492
  pagination && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 text-sm text-[var(--color-on-surface-muted)]", children: [
177
493
  /* @__PURE__ */ jsx("span", { children: messages.pageOf(table.getState().pagination.pageIndex + 1, table.getPageCount()) }),
178
494
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
@@ -197,7 +513,27 @@ function DataTable({
197
513
  }
198
514
  )
199
515
  ] })
200
- ] })
516
+ ] }),
517
+ bulkActions && selectedRows.length > 0 && /* @__PURE__ */ jsxs(
518
+ "div",
519
+ {
520
+ role: "region",
521
+ "aria-label": messages.selectedCount(fmt(selectedRows.length)),
522
+ className: cn(
523
+ floatingSurface,
524
+ "nx-motion sticky bottom-4 z-10 mx-auto flex w-fit max-w-full flex-wrap items-center gap-3 px-4 py-2"
525
+ ),
526
+ "data-state": "open",
527
+ children: [
528
+ /* @__PURE__ */ jsx("span", { "aria-live": "polite", className: "text-label-lg tabular-nums", children: messages.selectedCount(fmt(selectedRows.length)) }),
529
+ bulkActions(
530
+ selectedRows.map((r) => r.original),
531
+ clear
532
+ ),
533
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", onClick: clear, "aria-label": messages.clearSelection, children: /* @__PURE__ */ jsx(X, { size: "sm", "aria-hidden": "true" }) })
534
+ ]
535
+ }
536
+ )
201
537
  ] });
202
538
  }
203
539
  export {
@@ -1,2 +1,2 @@
1
1
  export { DataTable } from './DataTable.js';
2
- export type { DataTableProps, DataTableColumn } from './DataTable.js';
2
+ export type { DataTableProps, DataTableColumn, DataTableDensity, DataTableRowWindow, DataTableRowWindowHook } from './DataTable.js';
@@ -0,0 +1,19 @@
1
+ import * as React from 'react';
2
+ import { type DataTableProps } from '../DataTable/DataTable.js';
3
+ export interface DataTableVirtualProps<TData> extends Omit<DataTableProps<TData>, 'pagination' | 'rowWindow'> {
4
+ /** Height of the scrolling area. Required: rows are windowed within it. */
5
+ maxHeight: number | string;
6
+ /** Row height before measuring, in px. Default 49, or 37 when compact. */
7
+ estimateRowHeight?: number;
8
+ /** Rows rendered beyond the visible ones, each way. Default 8. */
9
+ overscan?: number;
10
+ }
11
+ /**
12
+ * DataTable for thousands of rows: only the rows in view (plus `overscan`)
13
+ * are in the DOM. Rows carry aria-rowindex and the table aria-rowcount, so
14
+ * screen readers still report the full size. Needs `@tanstack/react-virtual`.
15
+ */
16
+ export declare function DataTableVirtual<TData>({ estimateRowHeight, overscan, ...props }: DataTableVirtualProps<TData>): React.JSX.Element;
17
+ export declare namespace DataTableVirtual {
18
+ var displayName: string;
19
+ }