cortena-ui 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +68 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/data-table/data-table.js +32 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/system-columns.js +22 -3
- package/dist/components/data-table/system-columns.js.map +1 -1
- package/dist/components/data-table/types.d.ts +42 -1
- package/dist/components/data-table/use-data-table.js +51 -11
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/toast.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/data-table/data-table.tsx +62 -6
- package/src/components/data-table/system-columns.tsx +24 -1
- package/src/components/data-table/types.ts +42 -1
- package/src/components/data-table/use-data-table.ts +88 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
4
|
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
5
|
|
|
6
|
+
## 1.8.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **A `DataTable` column can take the width the others leave** (DESIGN-77).
|
|
11
|
+
The grid is `table-fixed`, so a column that declared no `size` sat at
|
|
12
|
+
TanStack's 150 px default: on a list of one wide column and a few narrow
|
|
13
|
+
ones, the column people came to read was clipped while empty space sat to
|
|
14
|
+
its right. See "Column widths" in `README.md`.
|
|
15
|
+
- `meta: { fill: true }` marks that column. It is the one column drawn with
|
|
16
|
+
no fixed width, so the remainder lands on it instead of on the filler
|
|
17
|
+
cell; every other column keeps its `size`. One per table — the first
|
|
18
|
+
visible one wins, and a pinned column is not eligible: a pinned column is
|
|
19
|
+
placed at an offset measured off its neighbours' widths, so one with no
|
|
20
|
+
width of its own would misplace every pinned column beside it. Its own
|
|
21
|
+
`size` stays as a floor, since the table's `min-width` still counts it,
|
|
22
|
+
and it is not itself resizable: a column with no width of its own has no
|
|
23
|
+
edge to drag.
|
|
24
|
+
- A table with a fill column defaults `enableColumnResizing` to `true`. The
|
|
25
|
+
drag now has somewhere to go — the dragged column takes the width and the
|
|
26
|
+
fill column gives it up. An explicit `false` still wins. The default
|
|
27
|
+
commits the new width on release (`columnResizeMode: "onEnd"`), because a
|
|
28
|
+
table that did not ask to be resizable should not re-render every row on
|
|
29
|
+
every mouse move; setting `enableColumnResizing` yourself keeps the live
|
|
30
|
+
`"onChange"` drag.
|
|
31
|
+
- Cells carry their full text in the native `title`, so text the row
|
|
32
|
+
truncates is still reachable — on every row, independent of anything else
|
|
33
|
+
on it. A column that renders its own value gets it from that value; a
|
|
34
|
+
column with its own `cell` renderer draws a node the table will not guess
|
|
35
|
+
at, and says what the hover text is with
|
|
36
|
+
`meta: { hoverText: (row) => string | undefined }`. Headers get a title
|
|
37
|
+
only from `meta: { label }`; repeating a one-word header in a tooltip on
|
|
38
|
+
every `<th>` is noise, and the select and expand columns have no sentence
|
|
39
|
+
to repeat.
|
|
40
|
+
- In dev, a table with an unsized column and no fill column warns once,
|
|
41
|
+
naming the column.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- **The expander and the selection checkbox no longer fire the row's own
|
|
46
|
+
click** (DESIGN-79). On a table with both `onRowClick` and
|
|
47
|
+
`renderSubComponent`, opening a row's detail panel also navigated away from
|
|
48
|
+
it, and ticking the checkbox selected the row and then left the page. Both
|
|
49
|
+
controls now stop the event, by pointer and by keyboard, so a consumer no
|
|
50
|
+
longer has to recognise the click target itself.
|
|
51
|
+
|
|
6
52
|
## 1.7.0
|
|
7
53
|
|
|
8
54
|
### Added
|
package/README.md
CHANGED
|
@@ -354,6 +354,74 @@ names the session (the key is still written, so one window stays on one
|
|
|
354
354
|
session), `listSessions: false` for a host with its own session list, and
|
|
355
355
|
`mintSessionKey` for a host that binds sessions some other way.
|
|
356
356
|
|
|
357
|
+
## Column widths
|
|
358
|
+
|
|
359
|
+
`DataTable` lays the grid out `table-fixed`: every column is drawn at its
|
|
360
|
+
`size`, and a column that never declared one sits at TanStack's 150 px
|
|
361
|
+
default. A list of one wide column and a few narrow ones — a title beside an
|
|
362
|
+
id, a status, an assignee — then clips the column people came to read while
|
|
363
|
+
empty space sits to its right. Three rules keep a list readable.
|
|
364
|
+
|
|
365
|
+
**One fill column: the column people scan the list by.** Mark it
|
|
366
|
+
`meta: { fill: true }` and give every other column a `size`. The fill column
|
|
367
|
+
is the one drawn with no fixed width, so the remainder lands on it. Its own
|
|
368
|
+
`size` stays as a floor — the table's `min-width` still counts it, so a narrow
|
|
369
|
+
window scrolls rather than squeezing the column to nothing — and it is not
|
|
370
|
+
itself resizable, because a column with no width has no edge to drag. **A fill
|
|
371
|
+
column cannot be pinned**: a pinned column is placed at an offset measured off
|
|
372
|
+
the widths of the columns beside it, so one with no width of its own would put
|
|
373
|
+
every pinned neighbour in the wrong place. Pin it and the table skips it and
|
|
374
|
+
falls back to the filler cell.
|
|
375
|
+
|
|
376
|
+
A table with a fill column turns `enableColumnResizing` on by default:
|
|
377
|
+
dragging any other column's edge now has somewhere to go, and the fill column
|
|
378
|
+
absorbs the difference. That default resizes on release (`onEnd`), since a
|
|
379
|
+
table that never asked to be resizable should not re-render every row on every
|
|
380
|
+
mouse move; set `enableColumnResizing` yourself and the drag is live
|
|
381
|
+
(`onChange`). Pass `enableColumnResizing={false}` to turn it off.
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
const columns = helper.columns([
|
|
385
|
+
helper.accessor("id", { header: "ID", size: 110 }),
|
|
386
|
+
helper.accessor("title", { header: "Title", size: 240, meta: { fill: true } }),
|
|
387
|
+
helper.accessor("status", { header: "Status", size: 130, meta: { filter: "faceted" } }),
|
|
388
|
+
helper.accessor("assignee", { header: "Assignee", size: 200 }),
|
|
389
|
+
]);
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
In dev, a table with an unsized column and no fill column warns once, naming
|
|
393
|
+
the column. It is the one layout mistake that produces no error and no
|
|
394
|
+
warning from anything else — just clipped text.
|
|
395
|
+
|
|
396
|
+
**Cells truncate, never wrap, and carry their full text on hover.** Rows keep
|
|
397
|
+
one height: a list whose rows grow to fit their longest cell is no longer
|
|
398
|
+
scannable, and there is no wrapping option. Truncated text has to stay
|
|
399
|
+
reachable, so a column that renders its own value puts that value in the
|
|
400
|
+
cell's `title` automatically — every row of it, whatever else is or is not on
|
|
401
|
+
that row. A column with its own `cell` renderer draws a node the table cannot
|
|
402
|
+
read; say what the hover text is with
|
|
403
|
+
`meta: { hoverText: (row) => row.title }`, or leave it with none. It is the
|
|
404
|
+
native `title`, not a tooltip component — a tooltip on every cell of a
|
|
405
|
+
hundred-row grid is a hundred listeners and a lot of noise.
|
|
406
|
+
|
|
407
|
+
Headers are not cells and get no automatic title: a header is one short phrase
|
|
408
|
+
the consumer chose, and repeating it in a tooltip on every `<th>` is noise. A
|
|
409
|
+
column that wants one sets `meta: { label: "…" }`, which is the same label the
|
|
410
|
+
view menu and the exports use.
|
|
411
|
+
|
|
412
|
+
**Long text belongs in an expandable row.** A description, an error body, a
|
|
413
|
+
payload: none of them fit on a row, and widening the column does not make them
|
|
414
|
+
fit. Pass `renderSubComponent` and the table adds an expander column; the
|
|
415
|
+
detail panel opens beneath the row, full width.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<DataTable
|
|
419
|
+
columns={columns}
|
|
420
|
+
dataSource={{ kind: "client", rows }}
|
|
421
|
+
renderSubComponent={(row) => <Markdown>{row.original.description}</Markdown>}
|
|
422
|
+
/>
|
|
423
|
+
```
|
|
424
|
+
|
|
357
425
|
## Charts in tests
|
|
358
426
|
|
|
359
427
|
Both chart engines size themselves from the parent box, and under jsdom every
|
|
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
//#region src/components/badge.d.ts
|
|
5
5
|
declare const badgeVariants: (props?: ({
|
|
6
|
-
variant?: "default" | "success" | "
|
|
6
|
+
variant?: "default" | "success" | "warn" | "destructive" | "danger" | "info" | "warning" | "neutral" | "secondary" | "outline" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
8
|
export interface BadgeProps extends React.ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
|
|
9
9
|
declare function Badge({ className, variant, ...props }: BadgeProps): React.JSX.Element;
|
|
@@ -109,6 +109,23 @@ const alignClass = {
|
|
|
109
109
|
center: "text-center",
|
|
110
110
|
end: "text-right"
|
|
111
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* Full text for a cell's `title`, or undefined for no title.
|
|
114
|
+
*
|
|
115
|
+
* Cells truncate and never wrap, so the only way back to the full text is the
|
|
116
|
+
* pointer. A column that renders its own value can be read straight off the
|
|
117
|
+
* value; a column with a `cell` renderer draws a node the table cannot turn
|
|
118
|
+
* into a sentence, and guessing produces titles like "[object Object]" on a
|
|
119
|
+
* row of buttons. Such a column says what its hover text is with
|
|
120
|
+
* `meta.hoverText`, or has none.
|
|
121
|
+
*/
|
|
122
|
+
function hoverTextFor(cell) {
|
|
123
|
+
const meta = cell.column.columnDef.meta;
|
|
124
|
+
if (meta?.hoverText) return meta.hoverText(cell.row.original) || void 0;
|
|
125
|
+
if (!meta?.plainCell) return void 0;
|
|
126
|
+
const value = cell.getValue();
|
|
127
|
+
return typeof value === "string" || typeof value === "number" ? String(value) || void 0 : void 0;
|
|
128
|
+
}
|
|
112
129
|
function EditableCell({ cell, instance, onDone }) {
|
|
113
130
|
const editable = cell.column.columnDef.meta?.editable;
|
|
114
131
|
const config = typeof editable === "object" ? editable : {};
|
|
@@ -155,6 +172,9 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
155
172
|
const { table, paginationMode } = instance;
|
|
156
173
|
const rows = table.getRowModel().rows;
|
|
157
174
|
const leafColumns = table.getVisibleLeafColumns();
|
|
175
|
+
const fillColumnId = leafColumns.find((column) => column.columnDef.meta?.fill && !column.getIsPinned())?.id;
|
|
176
|
+
const widthFor = (column, size) => column.id === fillColumnId ? void 0 : size;
|
|
177
|
+
const fillerStyle = fillColumnId ? { width: 0 } : void 0;
|
|
158
178
|
const colCount = leafColumns.length;
|
|
159
179
|
const headerGroups = table.getHeaderGroups();
|
|
160
180
|
const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;
|
|
@@ -372,8 +392,9 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
372
392
|
"data-editing": isEditing ? "" : void 0,
|
|
373
393
|
"aria-colindex": c + 1,
|
|
374
394
|
tabIndex: tabIndexFor(index, c),
|
|
395
|
+
title: isEditing ? void 0 : hoverTextFor(cell),
|
|
375
396
|
style: {
|
|
376
|
-
width: column.getSize(),
|
|
397
|
+
width: widthFor(column, column.getSize()),
|
|
377
398
|
...pin.style
|
|
378
399
|
},
|
|
379
400
|
onDoubleClick: canEdit && !isEditing ? () => instance.editing.start({
|
|
@@ -410,6 +431,7 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
410
431
|
/* @__PURE__ */ jsx("td", {
|
|
411
432
|
"aria-hidden": true,
|
|
412
433
|
"data-slot": "data-table-filler",
|
|
434
|
+
style: fillerStyle,
|
|
413
435
|
className: "p-0"
|
|
414
436
|
}),
|
|
415
437
|
cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))
|
|
@@ -432,9 +454,12 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
432
454
|
className: "border-b border-[var(--ds-border-subtle)]",
|
|
433
455
|
children: [leafColumns.map((column) => /* @__PURE__ */ jsx("td", {
|
|
434
456
|
className: cellPadding,
|
|
435
|
-
style: { width: column.getSize() },
|
|
457
|
+
style: { width: widthFor(column, column.getSize()) },
|
|
436
458
|
children: /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-3/4" })
|
|
437
|
-
}, column.id)), /* @__PURE__ */ jsx("td", {
|
|
459
|
+
}, column.id)), /* @__PURE__ */ jsx("td", {
|
|
460
|
+
style: fillerStyle,
|
|
461
|
+
className: "p-0"
|
|
462
|
+
})]
|
|
438
463
|
}, `skeleton-${i}`));
|
|
439
464
|
return /* @__PURE__ */ jsxs("div", {
|
|
440
465
|
"data-slot": "data-table",
|
|
@@ -496,8 +521,9 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
496
521
|
"aria-sort": sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : column.getCanSort() ? "none" : void 0,
|
|
497
522
|
colSpan: header.colSpan,
|
|
498
523
|
tabIndex: tabIndexFor(-1, c),
|
|
524
|
+
title: meta?.label,
|
|
499
525
|
style: {
|
|
500
|
-
width: header.getSize(),
|
|
526
|
+
width: widthFor(column, header.getSize()),
|
|
501
527
|
...pin.style
|
|
502
528
|
},
|
|
503
529
|
className: cn("group/head relative h-10 align-middle font-medium outline-none", headPadding, "bg-[var(--ds-muted)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]", "border-b border-[var(--ds-border)]", alignClass[meta?.align ?? "start"], "focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]", pin.pinned && "z-[3]", pin.pinned === "start" && pin.edge && "shadow-[inset_-1px_0_0_var(--ds-border)]", pin.pinned === "end" && pin.edge && "shadow-[inset_1px_0_0_var(--ds-border)]", meta?.headerClassName),
|
|
@@ -521,6 +547,8 @@ function DataTableView({ instance, className, maxHeight, virtualize, rowHeight =
|
|
|
521
547
|
group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c)),
|
|
522
548
|
/* @__PURE__ */ jsx("th", {
|
|
523
549
|
"aria-hidden": true,
|
|
550
|
+
"data-slot": "data-table-filler-head",
|
|
551
|
+
style: fillerStyle,
|
|
524
552
|
className: "border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0"
|
|
525
553
|
}),
|
|
526
554
|
group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-table.js","names":[],"sources":["../../../src/components/data-table/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { Inbox } from \"lucide-react\";\nimport * as React from \"react\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { Input } from \"@/components/input\";\nimport { Skeleton } from \"@/components/skeleton\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\nimport { DataTableColumnHeader, DataTablePagination, DataTableToolbar } from \"./parts\";\nimport type {\n DataTableCell,\n DataTableColumn,\n DataTableInstance,\n DataTableRow,\n DataTableTable,\n UseDataTableOptions,\n} from \"./types\";\nimport { useDataTable } from \"./use-data-table\";\n\n/**\n * DataTable — the composite from cortenaUI&Design/04-data-tables.md.\n *\n * `<DataTable columns={columns} dataSource={{ kind: \"client\", rows }} />`\n * renders a complete table: toolbar with search, faceted filters, view and\n * export menus; sortable, pinnable, hideable, resizable columns; sticky\n * header; pager; and, past 100 rows, virtualised rows. A server source\n * changes nothing in the markup and everything in the data flow.\n *\n * Markup is a real `<table>` with `role=\"grid\"` so screen readers get rows\n * and columns, and keyboard users get one tab stop with arrow-key movement\n * between cells (roving tabindex), Enter to edit an editable cell, Escape to\n * cancel, Space to toggle selection.\n *\n * Virtualised bodies keep the table element: rows outside the viewport are\n * replaced by two spacer rows sized from the virtualizer, which keeps column\n * widths, sticky headers and pinned columns working without absolute\n * positioning.\n */\n\nconst VIRTUALIZE_ABOVE = 100;\nconst DEFAULT_VIRTUAL_HEIGHT = 600;\nconst INFINITE_THRESHOLD_PX = 240;\n\n/** What a `ref` on `DataTable` hands back. */\nexport interface DataTableHandle<Row extends RowData> {\n /**\n * Refetch the page on screen through the same query the auto-refresh\n * interval drives — never a second fetch path. A table subscribed to a\n * notification stream calls this from the stream and leaves the interval as\n * the fallback for when the stream is unavailable.\n */\n refresh: () => void;\n /** Everything else the table knows, for a caller that needs more than a refresh. */\n instance: DataTableInstance<Row>;\n}\n\nexport interface DataTableViewProps<Row extends RowData> {\n className?: string;\n /** Imperative handle: `ref.current.refresh()`. */\n ref?: React.Ref<DataTableHandle<Row>>;\n /** Height of the scrolling body. Required for virtualisation to mean anything; defaults to 600px when virtualised. */\n maxHeight?: number | string;\n /** Force virtualisation on or off; undefined virtualises above 100 rows. */\n virtualize?: boolean;\n /** Estimated row height in px for the virtualizer. @default 40 */\n rowHeight?: number;\n /** @default true */\n stickyHeader?: boolean;\n /** @default \"default\" */\n density?: \"default\" | \"compact\";\n /** Detail panel under an expanded row; enables the expander column. */\n renderSubComponent?: (row: DataTableRow<Row>) => React.ReactNode;\n /** `false` hides the toolbar; a function replaces it. */\n toolbar?: boolean | ((ctx: { instance: DataTableInstance<Row> }) => React.ReactNode);\n /** Rendered in the toolbar while rows are selected. */\n bulkActions?: (ctx: { rows: Row[]; table: DataTableTable<Row> }) => React.ReactNode;\n /** Extra toolbar controls (right side). */\n toolbarActions?: React.ReactNode;\n searchPlaceholder?: string;\n emptyState?: React.ReactNode;\n /** @default \"No results\" */\n emptyMessage?: string;\n pageSizeOptions?: number[];\n /** @default true */\n enableExport?: boolean;\n /** @default \"export\" */\n exportFileName?: string;\n /** Show the loading skeleton regardless of the source's own state. */\n loading?: boolean;\n /** Error to show above the table, in addition to the server source's. */\n error?: string | Error | null;\n onRowClick?: (row: Row, event: React.MouseEvent<HTMLTableRowElement>) => void;\n /**\n * The row a detail pane is currently showing, matched against the row id\n * (`getRowId`, the row index when that is not set). It gets `data-active`\n * and the `--ds-primary-soft` tint, independent of checkbox selection — a\n * master/detail list highlights one row without selecting it, and a table\n * with both can show a selected set and one open row at once.\n */\n activeRowId?: string | null;\n /** Extra classes per row, e.g. to dim rows the app considers stale. */\n rowClassName?: (row: Row) => string | undefined;\n}\n\nexport type DataTableProps<Row extends RowData> = DataTableViewProps<Row> &\n ({ instance: DataTableInstance<Row> } | UseDataTableOptions<Row>);\n\nfunction DataTable<Row extends RowData>(props: DataTableProps<Row>) {\n if (\"instance\" in props) {\n const { instance, ...view } = props;\n return <DataTableView instance={instance} {...view} />;\n }\n return <DataTableOwned {...props} />;\n}\n\nfunction DataTableOwned<Row extends RowData>(props: DataTableViewProps<Row> & UseDataTableOptions<Row>) {\n const {\n className,\n maxHeight,\n virtualize,\n rowHeight,\n stickyHeader,\n density,\n renderSubComponent,\n toolbar,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage,\n pageSizeOptions,\n enableExport,\n exportFileName,\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n ...options\n } = props;\n const instance = useDataTable<Row>({\n ...options,\n enableExpanding: options.enableExpanding ?? Boolean(renderSubComponent),\n });\n return (\n <DataTableView\n instance={instance}\n className={className}\n maxHeight={maxHeight}\n virtualize={virtualize}\n rowHeight={rowHeight}\n stickyHeader={stickyHeader}\n density={density}\n renderSubComponent={renderSubComponent}\n toolbar={toolbar}\n bulkActions={bulkActions}\n toolbarActions={toolbarActions}\n searchPlaceholder={searchPlaceholder}\n emptyState={emptyState}\n emptyMessage={emptyMessage}\n pageSizeOptions={pageSizeOptions}\n enableExport={enableExport}\n exportFileName={exportFileName}\n loading={loading}\n error={error}\n onRowClick={onRowClick}\n activeRowId={activeRowId}\n rowClassName={rowClassName}\n ref={ref}\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Pinning */\n/* ------------------------------------------------------------------------ */\n\nfunction pinAttrs<Row extends RowData>(\n column: DataTableColumn<Row, any>,\n table: DataTableTable<Row>,\n): { style: React.CSSProperties; pinned: \"start\" | \"end\" | undefined; edge: boolean } {\n const pinned = column.getIsPinned();\n if (!pinned) return { style: {}, pinned: undefined, edge: false };\n if (pinned === \"start\") {\n const last = table.getStartVisibleLeafColumns().at(-1);\n return {\n style: { position: \"sticky\", left: column.getStart(\"start\") },\n pinned,\n edge: last?.id === column.id,\n };\n }\n const first = table.getEndVisibleLeafColumns()[0];\n return {\n style: { position: \"sticky\", right: column.getAfter(\"end\") },\n pinned,\n edge: first?.id === column.id,\n };\n}\n\nconst alignClass = { start: \"text-left\", center: \"text-center\", end: \"text-right\" } as const;\n\n/* ------------------------------------------------------------------------ */\n/* Editable cell */\n/* ------------------------------------------------------------------------ */\n\nfunction EditableCell<Row extends RowData>({\n cell,\n instance,\n onDone,\n}: {\n cell: DataTableCell<Row, any>;\n instance: DataTableInstance<Row>;\n onDone: () => void;\n}) {\n const editable = cell.column.columnDef.meta?.editable;\n const config = typeof editable === \"object\" ? editable : {};\n const initial = cell.getValue();\n const [draft, setDraft] = React.useState(initial == null ? \"\" : String(initial));\n const done = React.useRef(false);\n\n const finish = () => {\n done.current = true;\n instance.editing.stop();\n onDone();\n };\n const commit = () => {\n if (done.current) return;\n const value = config.parse ? config.parse(draft) : config.type === \"number\" ? Number(draft) : draft;\n void instance.editing.commit(cell.row, cell.column.id, value);\n finish();\n };\n const cancel = () => {\n if (done.current) return;\n finish();\n };\n\n return (\n <Input\n data-slot=\"data-table-editor\"\n autoFocus\n type={config.type === \"number\" ? \"number\" : \"text\"}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onBlur={commit}\n onKeyDown={(e) => {\n // The editor owns its keys; the grid must not move the focus cell.\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n cancel();\n }\n }}\n className=\"h-7 px-2 py-0 text-[length:var(--ds-text-body-sm)]\"\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* View */\n/* ------------------------------------------------------------------------ */\n\ntype CellCoord = { r: number; c: number };\n\nfunction DataTableView<Row extends RowData>({\n instance,\n className,\n maxHeight,\n virtualize,\n rowHeight = 40,\n stickyHeader = true,\n density = \"default\",\n renderSubComponent,\n toolbar = true,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage = \"No results\",\n pageSizeOptions,\n enableExport = true,\n exportFileName = \"export\",\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n}: DataTableViewProps<Row> & { instance: DataTableInstance<Row> }) {\n const { table, paginationMode } = instance;\n const rows = table.getRowModel().rows;\n const leafColumns = table.getVisibleLeafColumns();\n const colCount = leafColumns.length;\n const headerGroups = table.getHeaderGroups();\n const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;\n const isLoading = loading || instance.isLoading;\n const showSkeleton = isLoading && rows.length === 0;\n const showEmpty = !isLoading && rows.length === 0;\n const resolvedHeight = maxHeight ?? (isVirtual ? DEFAULT_VIRTUAL_HEIGHT : undefined);\n const errorMessage = error instanceof Error ? error.message : (error ?? instance.error?.message ?? \"\");\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const tableRef = React.useRef<HTMLTableElement>(null);\n\n const refresh = instance.refetch;\n React.useImperativeHandle(ref, () => ({ refresh, instance }), [refresh, instance]);\n\n // Mandatory on every server-backed table (P-39): a Refresh button beside the\n // search box and a stamp saying which moment these rows are. The switch only\n // appears where the table asked for `autoRefresh`.\n const refreshProps =\n instance.dataSource.kind === \"server\"\n ? {\n onRefresh: refresh,\n isFetching: instance.isFetching,\n lastUpdatedAt: instance.lastUpdatedAt,\n lastErrorAt: instance.lastErrorAt,\n autoRefresh: instance.autoRefresh,\n }\n : undefined;\n\n /* Virtualisation ------------------------------------------------------ */\n\n const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n count: rows.length,\n enabled: isVirtual,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 8,\n getItemKey: (index) => rows[index]?.id ?? index,\n // An expanded row and its detail row are one virtual item.\n measureElement: (element) => {\n let height = element.getBoundingClientRect().height;\n const next = element.nextElementSibling as HTMLElement | null;\n if (next?.dataset.slot === \"data-table-subrow\") height += next.getBoundingClientRect().height;\n return height;\n },\n });\n const virtualItems = isVirtual ? virtualizer.getVirtualItems() : null;\n const paddingTop = virtualItems?.length ? virtualItems[0]!.start : 0;\n const paddingBottom = virtualItems?.length\n ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1]!.end\n : 0;\n\n /* Infinite scroll ----------------------------------------------------- */\n\n const onScroll = React.useCallback(\n (event: React.UIEvent<HTMLDivElement>) => {\n if (paginationMode !== \"infinite\" || !instance.hasNextPage || instance.isFetching) return;\n const el = event.currentTarget;\n if (el.scrollHeight - el.scrollTop - el.clientHeight < INFINITE_THRESHOLD_PX) instance.fetchNextPage();\n },\n [paginationMode, instance],\n );\n\n /* Keyboard navigation ------------------------------------------------- */\n\n const [focused, setFocused] = React.useState<CellCoord>({ r: -1, c: 0 });\n const pendingFocus = React.useRef<CellCoord | null>(null);\n\n const cellElement = (coord: CellCoord) =>\n tableRef.current?.querySelector<HTMLElement>(\n `[data-row-index=\"${coord.r}\"][data-col-index=\"${coord.c}\"]`,\n ) ?? null;\n\n const focusCell = React.useCallback(\n (coord: CellCoord) => {\n const el = cellElement(coord);\n if (el) {\n setFocused(coord);\n el.focus();\n return;\n }\n if (isVirtual && coord.r >= 0) {\n pendingFocus.current = coord;\n setFocused(coord);\n virtualizer.scrollToIndex(coord.r, { align: \"auto\" });\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [isVirtual, virtualizer],\n );\n\n React.useEffect(() => {\n const coord = pendingFocus.current;\n if (!coord) return;\n const el = cellElement(coord);\n if (el) {\n pendingFocus.current = null;\n el.focus();\n }\n });\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n // Portaled menus bubble through React but live outside the table element.\n if (!tableRef.current?.contains(target)) return;\n if (instance.editing.cell) return;\n const cell = target.closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n const lastRow = rows.length - 1;\n const lastCol = colCount - 1;\n const onCellItself = target === cell;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 1), c });\n return;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 1), c });\n return;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell({ r, c: Math.min(lastCol, c + 1) });\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell({ r, c: Math.max(0, c - 1) });\n return;\n case \"Home\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: -1, c: 0 } : { r, c: 0 });\n return;\n case \"End\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: lastRow, c: lastCol } : { r, c: lastCol });\n return;\n case \"PageDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 10), c });\n return;\n case \"PageUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 10), c });\n return;\n case \"Enter\": {\n if (!onCellItself) return;\n if (r === -1) {\n const column = leafColumns[c];\n if (column?.getCanSort()) {\n event.preventDefault();\n column.toggleSorting(undefined, event.shiftKey);\n }\n return;\n }\n const row = rows[r];\n const column = leafColumns[c];\n if (row && column && column.columnDef.meta?.editable) {\n event.preventDefault();\n instance.editing.start({ rowId: row.id, columnId: column.id });\n }\n return;\n }\n case \" \": {\n if (!onCellItself || r === -1) return;\n const row = rows[r];\n if (row?.getCanSelect()) {\n event.preventDefault();\n row.toggleSelected();\n }\n return;\n }\n default:\n return;\n }\n };\n\n const onFocus = (event: React.FocusEvent<HTMLTableElement>) => {\n const cell = (event.target as HTMLElement).closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell || !tableRef.current?.contains(cell)) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n setFocused((f) => (f.r === r && f.c === c ? f : { r, c }));\n };\n\n const tabIndexFor = (r: number, c: number) => (focused.r === r && focused.c === c ? 0 : -1);\n\n /* Rendering ----------------------------------------------------------- */\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2.5\";\n const headPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2\";\n const editingCell = instance.editing.cell;\n\n const renderRow = (row: DataTableRow<Row>, index: number, virtualIndex?: number) => {\n const cells = row.getVisibleCells();\n const endStart = cells.findIndex((cell) => cell.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? cells.length : endStart;\n const selected = row.getIsSelected();\n const active = activeRowId != null && row.id === activeRowId;\n const expanded = renderSubComponent && row.getIsExpanded();\n\n const renderCell = (cell: DataTableCell<Row, any>, c: number) => {\n const column = cell.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const isEditing = editingCell?.rowId === row.id && editingCell.columnId === column.id;\n const canEdit = Boolean(meta?.editable);\n return (\n <td\n key={cell.id}\n role=\"gridcell\"\n data-slot=\"data-table-cell\"\n data-row-index={index}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n data-editable={canEdit ? \"\" : undefined}\n data-editing={isEditing ? \"\" : undefined}\n aria-colindex={c + 1}\n tabIndex={tabIndexFor(index, c)}\n style={{ width: column.getSize(), ...pin.style }}\n onDoubleClick={\n canEdit && !isEditing\n ? () => instance.editing.start({ rowId: row.id, columnId: column.id })\n : undefined\n }\n className={cn(\n \"relative align-middle outline-none\",\n isEditing ? \"px-1.5 py-1\" : cellPadding,\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n // Pinned cells cover what scrolls under them, so their background is\n // opaque card with the translucent hover / selected tint layered on.\n pin.pinned &&\n \"z-[1] bg-[var(--ds-card)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n !isEditing && \"truncate\",\n meta?.cellClassName,\n )}\n >\n {isEditing ? (\n <EditableCell\n cell={cell}\n instance={instance}\n onDone={() => {\n pendingFocus.current = { r: index, c };\n }}\n />\n ) : (\n <table.FlexRender cell={cell} />\n )}\n </td>\n );\n };\n\n return (\n <React.Fragment key={row.id}>\n <tr\n role=\"row\"\n data-slot=\"data-table-row\"\n data-index={virtualIndex}\n data-selected={selected ? \"\" : undefined}\n data-active={active ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n aria-rowindex={index + 2}\n aria-selected={table.options.enableRowSelection ? selected : undefined}\n ref={isVirtual ? virtualizer.measureElement : undefined}\n onClick={onRowClick ? (e) => onRowClick(row.original, e) : undefined}\n className={cn(\n \"group/row border-b border-[var(--ds-border-subtle)] transition-colors duration-[var(--ds-duration-fast)]\",\n \"hover:bg-[var(--ds-hover)] data-[selected]:bg-[var(--ds-primary-soft)]\",\n // Independent of selection: a detail pane highlights one row\n // without ticking its checkbox.\n \"data-[active]:bg-[var(--ds-primary-soft)]\",\n onRowClick && \"cursor-pointer\",\n rowClassName?.(row.original),\n )}\n >\n {cells.slice(0, splitAt).map((cell, c) => renderCell(cell, c))}\n <td aria-hidden data-slot=\"data-table-filler\" className=\"p-0\" />\n {cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))}\n </tr>\n {expanded ? (\n <tr role=\"row\" data-slot=\"data-table-subrow\" className=\"border-b border-[var(--ds-border-subtle)]\">\n <td\n role=\"gridcell\"\n colSpan={colCount + 1}\n className=\"bg-[var(--ds-muted)] px-4 py-3 text-[length:var(--ds-text-body-sm)]\"\n >\n {renderSubComponent(row)}\n </td>\n </tr>\n ) : null}\n </React.Fragment>\n );\n };\n\n const skeletonRows = Array.from({ length: Math.min(8, table.state.pagination.pageSize) }, (_, i) => (\n <tr key={`skeleton-${i}`} aria-hidden data-slot=\"data-table-skeleton-row\" className=\"border-b border-[var(--ds-border-subtle)]\">\n {leafColumns.map((column) => (\n <td key={column.id} className={cellPadding} style={{ width: column.getSize() }}>\n <Skeleton className=\"h-3.5 w-3/4\" />\n </td>\n ))}\n <td className=\"p-0\" />\n </tr>\n ));\n\n return (\n <div data-slot=\"data-table\" className={cn(\"flex min-w-0 flex-col gap-3\", className)}>\n {errorMessage ? (\n <ErrorBanner message={errorMessage} onRetry={instance.error ? instance.refetch : undefined} />\n ) : null}\n {toolbar === false ? null : typeof toolbar === \"function\" ? (\n toolbar({ instance })\n ) : (\n <DataTableToolbar\n table={table}\n bulkActions={bulkActions}\n searchPlaceholder={searchPlaceholder}\n enableExport={enableExport}\n exportFileName={exportFileName}\n refresh={refreshProps}\n >\n {toolbarActions}\n </DataTableToolbar>\n )}\n <div\n ref={scrollRef}\n data-slot=\"data-table-container\"\n onScroll={onScroll}\n style={{ maxHeight: resolvedHeight }}\n className={cn(\n \"relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-card)]\",\n )}\n >\n <table\n ref={tableRef}\n role=\"grid\"\n data-slot=\"data-table-grid\"\n data-virtualized={isVirtual ? \"\" : undefined}\n aria-rowcount={rows.length + 1}\n aria-colcount={colCount}\n aria-busy={instance.isFetching || undefined}\n aria-multiselectable={table.options.enableRowSelection ? true : undefined}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n style={{ minWidth: table.getTotalSize() }}\n className={cn(\n \"w-full table-fixed border-collapse caption-bottom\",\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n <thead data-slot=\"data-table-header\" className={cn(stickyHeader && \"sticky top-0 z-[2]\")}>\n {headerGroups.map((group) => {\n const endStart = group.headers.findIndex((h) => h.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? group.headers.length : endStart;\n const renderHead = (header: (typeof group.headers)[number], c: number) => {\n const column = header.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const sorted = column.getIsSorted();\n const def = column.columnDef;\n return (\n <th\n key={header.id}\n role=\"columnheader\"\n scope=\"col\"\n data-slot=\"data-table-head\"\n data-row-index={-1}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n aria-colindex={c + 1}\n aria-sort={\n sorted === \"asc\"\n ? \"ascending\"\n : sorted === \"desc\"\n ? \"descending\"\n : column.getCanSort()\n ? \"none\"\n : undefined\n }\n colSpan={header.colSpan}\n tabIndex={tabIndexFor(-1, c)}\n style={{ width: header.getSize(), ...pin.style }}\n className={cn(\n \"group/head relative h-10 align-middle font-medium outline-none\",\n headPadding,\n \"bg-[var(--ds-muted)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\",\n \"border-b border-[var(--ds-border)]\",\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n pin.pinned && \"z-[3]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n meta?.headerClassName,\n )}\n >\n {header.isPlaceholder ? null : typeof def.header === \"string\" ? (\n <DataTableColumnHeader column={column} title={def.header} />\n ) : (\n <table.FlexRender header={header} />\n )}\n {column.getCanResize() ? (\n <div\n data-slot=\"data-table-resizer\"\n data-resizing={column.getIsResizing() ? \"\" : undefined}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n onDoubleClick={() => column.resetSize()}\n className={cn(\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] hover:opacity-100 data-[resizing]:opacity-100\",\n \"bg-[var(--ds-ring)]\",\n )}\n />\n ) : null}\n </th>\n );\n };\n return (\n <tr key={group.id} role=\"row\" aria-rowindex={1}>\n {group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c))}\n <th aria-hidden className=\"border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0\" />\n {group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))}\n </tr>\n );\n })}\n </thead>\n <tbody data-slot=\"data-table-body\">\n {showSkeleton ? skeletonRows : null}\n {showEmpty ? (\n <tr role=\"row\" data-slot=\"data-table-empty\">\n <td role=\"gridcell\" colSpan={colCount + 1} className=\"p-0\">\n {emptyState ?? (\n <div className=\"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-[color:var(--ds-muted-foreground)]\">\n <Inbox aria-hidden className=\"size-6 text-[color:var(--ds-text-tertiary)]\" />\n <p className=\"text-[length:var(--ds-text-body-sm)]\">{emptyMessage}</p>\n </div>\n )}\n </td>\n </tr>\n ) : null}\n {virtualItems ? (\n <>\n {paddingTop > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingTop }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n {virtualItems.map((item) => {\n const row = rows[item.index];\n return row ? renderRow(row, item.index, item.index) : null;\n })}\n {paddingBottom > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingBottom }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n </>\n ) : (\n rows.map((row, index) => renderRow(row, index))\n )}\n </tbody>\n </table>\n {paginationMode === \"infinite\" && instance.isFetching && rows.length > 0 ? (\n <div\n data-slot=\"data-table-loading-more\"\n className=\"flex items-center justify-center gap-2 border-t border-[var(--ds-border-subtle)] py-2 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n <Spinner size={14} /> Loading more\n </div>\n ) : null}\n </div>\n {paginationMode === \"pages\" ? (\n <DataTablePagination table={table} pageSizeOptions={pageSizeOptions} />\n ) : paginationMode === \"infinite\" ? (\n <div className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\">\n {rows.length} of {instance.total}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable, DataTableView };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"data-table.js","names":[],"sources":["../../../src/components/data-table/data-table.tsx"],"sourcesContent":["\"use client\";\n\nimport type { RowData } from \"@tanstack/react-table\";\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { Inbox } from \"lucide-react\";\nimport * as React from \"react\";\nimport { ErrorBanner } from \"@/components/error-banner\";\nimport { Input } from \"@/components/input\";\nimport { Skeleton } from \"@/components/skeleton\";\nimport { Spinner } from \"@/components/spinner\";\nimport { cn } from \"@/lib/cn\";\nimport { DataTableColumnHeader, DataTablePagination, DataTableToolbar } from \"./parts\";\nimport type {\n DataTableCell,\n DataTableColumn,\n DataTableInstance,\n DataTableRow,\n DataTableTable,\n UseDataTableOptions,\n} from \"./types\";\nimport { useDataTable } from \"./use-data-table\";\n\n/**\n * DataTable — the composite from cortenaUI&Design/04-data-tables.md.\n *\n * `<DataTable columns={columns} dataSource={{ kind: \"client\", rows }} />`\n * renders a complete table: toolbar with search, faceted filters, view and\n * export menus; sortable, pinnable, hideable, resizable columns; sticky\n * header; pager; and, past 100 rows, virtualised rows. A server source\n * changes nothing in the markup and everything in the data flow.\n *\n * Markup is a real `<table>` with `role=\"grid\"` so screen readers get rows\n * and columns, and keyboard users get one tab stop with arrow-key movement\n * between cells (roving tabindex), Enter to edit an editable cell, Escape to\n * cancel, Space to toggle selection.\n *\n * Virtualised bodies keep the table element: rows outside the viewport are\n * replaced by two spacer rows sized from the virtualizer, which keeps column\n * widths, sticky headers and pinned columns working without absolute\n * positioning.\n */\n\nconst VIRTUALIZE_ABOVE = 100;\nconst DEFAULT_VIRTUAL_HEIGHT = 600;\nconst INFINITE_THRESHOLD_PX = 240;\n\n/** What a `ref` on `DataTable` hands back. */\nexport interface DataTableHandle<Row extends RowData> {\n /**\n * Refetch the page on screen through the same query the auto-refresh\n * interval drives — never a second fetch path. A table subscribed to a\n * notification stream calls this from the stream and leaves the interval as\n * the fallback for when the stream is unavailable.\n */\n refresh: () => void;\n /** Everything else the table knows, for a caller that needs more than a refresh. */\n instance: DataTableInstance<Row>;\n}\n\nexport interface DataTableViewProps<Row extends RowData> {\n className?: string;\n /** Imperative handle: `ref.current.refresh()`. */\n ref?: React.Ref<DataTableHandle<Row>>;\n /** Height of the scrolling body. Required for virtualisation to mean anything; defaults to 600px when virtualised. */\n maxHeight?: number | string;\n /** Force virtualisation on or off; undefined virtualises above 100 rows. */\n virtualize?: boolean;\n /** Estimated row height in px for the virtualizer. @default 40 */\n rowHeight?: number;\n /** @default true */\n stickyHeader?: boolean;\n /** @default \"default\" */\n density?: \"default\" | \"compact\";\n /** Detail panel under an expanded row; enables the expander column. */\n renderSubComponent?: (row: DataTableRow<Row>) => React.ReactNode;\n /** `false` hides the toolbar; a function replaces it. */\n toolbar?: boolean | ((ctx: { instance: DataTableInstance<Row> }) => React.ReactNode);\n /** Rendered in the toolbar while rows are selected. */\n bulkActions?: (ctx: { rows: Row[]; table: DataTableTable<Row> }) => React.ReactNode;\n /** Extra toolbar controls (right side). */\n toolbarActions?: React.ReactNode;\n searchPlaceholder?: string;\n emptyState?: React.ReactNode;\n /** @default \"No results\" */\n emptyMessage?: string;\n pageSizeOptions?: number[];\n /** @default true */\n enableExport?: boolean;\n /** @default \"export\" */\n exportFileName?: string;\n /** Show the loading skeleton regardless of the source's own state. */\n loading?: boolean;\n /** Error to show above the table, in addition to the server source's. */\n error?: string | Error | null;\n onRowClick?: (row: Row, event: React.MouseEvent<HTMLTableRowElement>) => void;\n /**\n * The row a detail pane is currently showing, matched against the row id\n * (`getRowId`, the row index when that is not set). It gets `data-active`\n * and the `--ds-primary-soft` tint, independent of checkbox selection — a\n * master/detail list highlights one row without selecting it, and a table\n * with both can show a selected set and one open row at once.\n */\n activeRowId?: string | null;\n /** Extra classes per row, e.g. to dim rows the app considers stale. */\n rowClassName?: (row: Row) => string | undefined;\n}\n\nexport type DataTableProps<Row extends RowData> = DataTableViewProps<Row> &\n ({ instance: DataTableInstance<Row> } | UseDataTableOptions<Row>);\n\nfunction DataTable<Row extends RowData>(props: DataTableProps<Row>) {\n if (\"instance\" in props) {\n const { instance, ...view } = props;\n return <DataTableView instance={instance} {...view} />;\n }\n return <DataTableOwned {...props} />;\n}\n\nfunction DataTableOwned<Row extends RowData>(props: DataTableViewProps<Row> & UseDataTableOptions<Row>) {\n const {\n className,\n maxHeight,\n virtualize,\n rowHeight,\n stickyHeader,\n density,\n renderSubComponent,\n toolbar,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage,\n pageSizeOptions,\n enableExport,\n exportFileName,\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n ...options\n } = props;\n const instance = useDataTable<Row>({\n ...options,\n enableExpanding: options.enableExpanding ?? Boolean(renderSubComponent),\n });\n return (\n <DataTableView\n instance={instance}\n className={className}\n maxHeight={maxHeight}\n virtualize={virtualize}\n rowHeight={rowHeight}\n stickyHeader={stickyHeader}\n density={density}\n renderSubComponent={renderSubComponent}\n toolbar={toolbar}\n bulkActions={bulkActions}\n toolbarActions={toolbarActions}\n searchPlaceholder={searchPlaceholder}\n emptyState={emptyState}\n emptyMessage={emptyMessage}\n pageSizeOptions={pageSizeOptions}\n enableExport={enableExport}\n exportFileName={exportFileName}\n loading={loading}\n error={error}\n onRowClick={onRowClick}\n activeRowId={activeRowId}\n rowClassName={rowClassName}\n ref={ref}\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* Pinning */\n/* ------------------------------------------------------------------------ */\n\nfunction pinAttrs<Row extends RowData>(\n column: DataTableColumn<Row, any>,\n table: DataTableTable<Row>,\n): { style: React.CSSProperties; pinned: \"start\" | \"end\" | undefined; edge: boolean } {\n const pinned = column.getIsPinned();\n if (!pinned) return { style: {}, pinned: undefined, edge: false };\n if (pinned === \"start\") {\n const last = table.getStartVisibleLeafColumns().at(-1);\n return {\n style: { position: \"sticky\", left: column.getStart(\"start\") },\n pinned,\n edge: last?.id === column.id,\n };\n }\n const first = table.getEndVisibleLeafColumns()[0];\n return {\n style: { position: \"sticky\", right: column.getAfter(\"end\") },\n pinned,\n edge: first?.id === column.id,\n };\n}\n\nconst alignClass = { start: \"text-left\", center: \"text-center\", end: \"text-right\" } as const;\n\n/* ------------------------------------------------------------------------ */\n/* Column widths and hover text (DESIGN-77) */\n/* ------------------------------------------------------------------------ */\n\n/**\n * Full text for a cell's `title`, or undefined for no title.\n *\n * Cells truncate and never wrap, so the only way back to the full text is the\n * pointer. A column that renders its own value can be read straight off the\n * value; a column with a `cell` renderer draws a node the table cannot turn\n * into a sentence, and guessing produces titles like \"[object Object]\" on a\n * row of buttons. Such a column says what its hover text is with\n * `meta.hoverText`, or has none.\n */\nfunction hoverTextFor<Row extends RowData>(cell: DataTableCell<Row, any>): string | undefined {\n const def = cell.column.columnDef;\n const meta = def.meta;\n if (meta?.hoverText) return meta.hoverText(cell.row.original) || undefined;\n if (!meta?.plainCell) return undefined;\n const value = cell.getValue();\n // `|| undefined` on the way out too: an empty string is a `title=\"\"`, which\n // is a pointer that stops on an empty tooltip rather than no tooltip.\n return typeof value === \"string\" || typeof value === \"number\"\n ? String(value) || undefined\n : undefined;\n}\n\n/* ------------------------------------------------------------------------ */\n/* Editable cell */\n/* ------------------------------------------------------------------------ */\n\nfunction EditableCell<Row extends RowData>({\n cell,\n instance,\n onDone,\n}: {\n cell: DataTableCell<Row, any>;\n instance: DataTableInstance<Row>;\n onDone: () => void;\n}) {\n const editable = cell.column.columnDef.meta?.editable;\n const config = typeof editable === \"object\" ? editable : {};\n const initial = cell.getValue();\n const [draft, setDraft] = React.useState(initial == null ? \"\" : String(initial));\n const done = React.useRef(false);\n\n const finish = () => {\n done.current = true;\n instance.editing.stop();\n onDone();\n };\n const commit = () => {\n if (done.current) return;\n const value = config.parse ? config.parse(draft) : config.type === \"number\" ? Number(draft) : draft;\n void instance.editing.commit(cell.row, cell.column.id, value);\n finish();\n };\n const cancel = () => {\n if (done.current) return;\n finish();\n };\n\n return (\n <Input\n data-slot=\"data-table-editor\"\n autoFocus\n type={config.type === \"number\" ? \"number\" : \"text\"}\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onFocus={(e) => e.target.select()}\n onBlur={commit}\n onKeyDown={(e) => {\n // The editor owns its keys; the grid must not move the focus cell.\n e.stopPropagation();\n if (e.key === \"Enter\") {\n e.preventDefault();\n commit();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n cancel();\n }\n }}\n className=\"h-7 px-2 py-0 text-[length:var(--ds-text-body-sm)]\"\n />\n );\n}\n\n/* ------------------------------------------------------------------------ */\n/* View */\n/* ------------------------------------------------------------------------ */\n\ntype CellCoord = { r: number; c: number };\n\nfunction DataTableView<Row extends RowData>({\n instance,\n className,\n maxHeight,\n virtualize,\n rowHeight = 40,\n stickyHeader = true,\n density = \"default\",\n renderSubComponent,\n toolbar = true,\n bulkActions,\n toolbarActions,\n searchPlaceholder,\n emptyState,\n emptyMessage = \"No results\",\n pageSizeOptions,\n enableExport = true,\n exportFileName = \"export\",\n loading,\n error,\n onRowClick,\n activeRowId,\n rowClassName,\n ref,\n}: DataTableViewProps<Row> & { instance: DataTableInstance<Row> }) {\n const { table, paginationMode } = instance;\n const rows = table.getRowModel().rows;\n const leafColumns = table.getVisibleLeafColumns();\n // The one column drawn with no fixed width, so the space the sized columns\n // leave lands on it instead of on the filler cell (DESIGN-77). First visible\n // one wins; hide it and the table falls back to the old filler behaviour.\n // A pinned column is skipped: pinning positions it with a `left`/`right`\n // offset read off the widths of the columns beside it, and a column with no\n // width of its own would leave every pinned neighbour at the wrong offset.\n const fillColumnId = leafColumns.find(\n (column) => column.columnDef.meta?.fill && !column.getIsPinned(),\n )?.id;\n const widthFor = (column: DataTableColumn<Row, any>, size: number) =>\n column.id === fillColumnId ? undefined : size;\n // A width the fill column would otherwise have to share the remainder with.\n const fillerStyle = fillColumnId ? ({ width: 0 } as const) : undefined;\n const colCount = leafColumns.length;\n const headerGroups = table.getHeaderGroups();\n const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;\n const isLoading = loading || instance.isLoading;\n const showSkeleton = isLoading && rows.length === 0;\n const showEmpty = !isLoading && rows.length === 0;\n const resolvedHeight = maxHeight ?? (isVirtual ? DEFAULT_VIRTUAL_HEIGHT : undefined);\n const errorMessage = error instanceof Error ? error.message : (error ?? instance.error?.message ?? \"\");\n\n const scrollRef = React.useRef<HTMLDivElement>(null);\n const tableRef = React.useRef<HTMLTableElement>(null);\n\n const refresh = instance.refetch;\n React.useImperativeHandle(ref, () => ({ refresh, instance }), [refresh, instance]);\n\n // Mandatory on every server-backed table (P-39): a Refresh button beside the\n // search box and a stamp saying which moment these rows are. The switch only\n // appears where the table asked for `autoRefresh`.\n const refreshProps =\n instance.dataSource.kind === \"server\"\n ? {\n onRefresh: refresh,\n isFetching: instance.isFetching,\n lastUpdatedAt: instance.lastUpdatedAt,\n lastErrorAt: instance.lastErrorAt,\n autoRefresh: instance.autoRefresh,\n }\n : undefined;\n\n /* Virtualisation ------------------------------------------------------ */\n\n const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({\n count: rows.length,\n enabled: isVirtual,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 8,\n getItemKey: (index) => rows[index]?.id ?? index,\n // An expanded row and its detail row are one virtual item.\n measureElement: (element) => {\n let height = element.getBoundingClientRect().height;\n const next = element.nextElementSibling as HTMLElement | null;\n if (next?.dataset.slot === \"data-table-subrow\") height += next.getBoundingClientRect().height;\n return height;\n },\n });\n const virtualItems = isVirtual ? virtualizer.getVirtualItems() : null;\n const paddingTop = virtualItems?.length ? virtualItems[0]!.start : 0;\n const paddingBottom = virtualItems?.length\n ? virtualizer.getTotalSize() - virtualItems[virtualItems.length - 1]!.end\n : 0;\n\n /* Infinite scroll ----------------------------------------------------- */\n\n const onScroll = React.useCallback(\n (event: React.UIEvent<HTMLDivElement>) => {\n if (paginationMode !== \"infinite\" || !instance.hasNextPage || instance.isFetching) return;\n const el = event.currentTarget;\n if (el.scrollHeight - el.scrollTop - el.clientHeight < INFINITE_THRESHOLD_PX) instance.fetchNextPage();\n },\n [paginationMode, instance],\n );\n\n /* Keyboard navigation ------------------------------------------------- */\n\n const [focused, setFocused] = React.useState<CellCoord>({ r: -1, c: 0 });\n const pendingFocus = React.useRef<CellCoord | null>(null);\n\n const cellElement = (coord: CellCoord) =>\n tableRef.current?.querySelector<HTMLElement>(\n `[data-row-index=\"${coord.r}\"][data-col-index=\"${coord.c}\"]`,\n ) ?? null;\n\n const focusCell = React.useCallback(\n (coord: CellCoord) => {\n const el = cellElement(coord);\n if (el) {\n setFocused(coord);\n el.focus();\n return;\n }\n if (isVirtual && coord.r >= 0) {\n pendingFocus.current = coord;\n setFocused(coord);\n virtualizer.scrollToIndex(coord.r, { align: \"auto\" });\n }\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [isVirtual, virtualizer],\n );\n\n React.useEffect(() => {\n const coord = pendingFocus.current;\n if (!coord) return;\n const el = cellElement(coord);\n if (el) {\n pendingFocus.current = null;\n el.focus();\n }\n });\n\n const onKeyDown = (event: React.KeyboardEvent<HTMLTableElement>) => {\n const target = event.target as HTMLElement;\n // Portaled menus bubble through React but live outside the table element.\n if (!tableRef.current?.contains(target)) return;\n if (instance.editing.cell) return;\n const cell = target.closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n const lastRow = rows.length - 1;\n const lastCol = colCount - 1;\n const onCellItself = target === cell;\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 1), c });\n return;\n case \"ArrowUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 1), c });\n return;\n case \"ArrowRight\":\n event.preventDefault();\n focusCell({ r, c: Math.min(lastCol, c + 1) });\n return;\n case \"ArrowLeft\":\n event.preventDefault();\n focusCell({ r, c: Math.max(0, c - 1) });\n return;\n case \"Home\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: -1, c: 0 } : { r, c: 0 });\n return;\n case \"End\":\n event.preventDefault();\n focusCell(event.ctrlKey ? { r: lastRow, c: lastCol } : { r, c: lastCol });\n return;\n case \"PageDown\":\n event.preventDefault();\n focusCell({ r: Math.min(lastRow, r + 10), c });\n return;\n case \"PageUp\":\n event.preventDefault();\n focusCell({ r: Math.max(-1, r - 10), c });\n return;\n case \"Enter\": {\n if (!onCellItself) return;\n if (r === -1) {\n const column = leafColumns[c];\n if (column?.getCanSort()) {\n event.preventDefault();\n column.toggleSorting(undefined, event.shiftKey);\n }\n return;\n }\n const row = rows[r];\n const column = leafColumns[c];\n if (row && column && column.columnDef.meta?.editable) {\n event.preventDefault();\n instance.editing.start({ rowId: row.id, columnId: column.id });\n }\n return;\n }\n case \" \": {\n if (!onCellItself || r === -1) return;\n const row = rows[r];\n if (row?.getCanSelect()) {\n event.preventDefault();\n row.toggleSelected();\n }\n return;\n }\n default:\n return;\n }\n };\n\n const onFocus = (event: React.FocusEvent<HTMLTableElement>) => {\n const cell = (event.target as HTMLElement).closest<HTMLElement>(\"[data-row-index][data-col-index]\");\n if (!cell || !tableRef.current?.contains(cell)) return;\n const r = Number(cell.dataset.rowIndex);\n const c = Number(cell.dataset.colIndex);\n setFocused((f) => (f.r === r && f.c === c ? f : { r, c }));\n };\n\n const tabIndexFor = (r: number, c: number) => (focused.r === r && focused.c === c ? 0 : -1);\n\n /* Rendering ----------------------------------------------------------- */\n\n const cellPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2.5\";\n const headPadding = density === \"compact\" ? \"px-3 py-1.5\" : \"px-3 py-2\";\n const editingCell = instance.editing.cell;\n\n const renderRow = (row: DataTableRow<Row>, index: number, virtualIndex?: number) => {\n const cells = row.getVisibleCells();\n const endStart = cells.findIndex((cell) => cell.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? cells.length : endStart;\n const selected = row.getIsSelected();\n const active = activeRowId != null && row.id === activeRowId;\n const expanded = renderSubComponent && row.getIsExpanded();\n\n const renderCell = (cell: DataTableCell<Row, any>, c: number) => {\n const column = cell.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const isEditing = editingCell?.rowId === row.id && editingCell.columnId === column.id;\n const canEdit = Boolean(meta?.editable);\n return (\n <td\n key={cell.id}\n role=\"gridcell\"\n data-slot=\"data-table-cell\"\n data-row-index={index}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n data-editable={canEdit ? \"\" : undefined}\n data-editing={isEditing ? \"\" : undefined}\n aria-colindex={c + 1}\n tabIndex={tabIndexFor(index, c)}\n title={isEditing ? undefined : hoverTextFor(cell)}\n style={{ width: widthFor(column, column.getSize()), ...pin.style }}\n onDoubleClick={\n canEdit && !isEditing\n ? () => instance.editing.start({ rowId: row.id, columnId: column.id })\n : undefined\n }\n className={cn(\n \"relative align-middle outline-none\",\n isEditing ? \"px-1.5 py-1\" : cellPadding,\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n // Pinned cells cover what scrolls under them, so their background is\n // opaque card with the translucent hover / selected tint layered on.\n pin.pinned &&\n \"z-[1] bg-[var(--ds-card)] group-hover/row:bg-[linear-gradient(var(--ds-hover),var(--ds-hover))] group-data-[selected]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))] group-data-[active]/row:bg-[linear-gradient(var(--ds-primary-soft),var(--ds-primary-soft))]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n !isEditing && \"truncate\",\n meta?.cellClassName,\n )}\n >\n {isEditing ? (\n <EditableCell\n cell={cell}\n instance={instance}\n onDone={() => {\n pendingFocus.current = { r: index, c };\n }}\n />\n ) : (\n <table.FlexRender cell={cell} />\n )}\n </td>\n );\n };\n\n return (\n <React.Fragment key={row.id}>\n <tr\n role=\"row\"\n data-slot=\"data-table-row\"\n data-index={virtualIndex}\n data-selected={selected ? \"\" : undefined}\n data-active={active ? \"\" : undefined}\n data-expanded={expanded ? \"\" : undefined}\n aria-rowindex={index + 2}\n aria-selected={table.options.enableRowSelection ? selected : undefined}\n ref={isVirtual ? virtualizer.measureElement : undefined}\n onClick={onRowClick ? (e) => onRowClick(row.original, e) : undefined}\n className={cn(\n \"group/row border-b border-[var(--ds-border-subtle)] transition-colors duration-[var(--ds-duration-fast)]\",\n \"hover:bg-[var(--ds-hover)] data-[selected]:bg-[var(--ds-primary-soft)]\",\n // Independent of selection: a detail pane highlights one row\n // without ticking its checkbox.\n \"data-[active]:bg-[var(--ds-primary-soft)]\",\n onRowClick && \"cursor-pointer\",\n rowClassName?.(row.original),\n )}\n >\n {cells.slice(0, splitAt).map((cell, c) => renderCell(cell, c))}\n <td aria-hidden data-slot=\"data-table-filler\" style={fillerStyle} className=\"p-0\" />\n {cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))}\n </tr>\n {expanded ? (\n <tr role=\"row\" data-slot=\"data-table-subrow\" className=\"border-b border-[var(--ds-border-subtle)]\">\n <td\n role=\"gridcell\"\n colSpan={colCount + 1}\n className=\"bg-[var(--ds-muted)] px-4 py-3 text-[length:var(--ds-text-body-sm)]\"\n >\n {renderSubComponent(row)}\n </td>\n </tr>\n ) : null}\n </React.Fragment>\n );\n };\n\n const skeletonRows = Array.from({ length: Math.min(8, table.state.pagination.pageSize) }, (_, i) => (\n <tr key={`skeleton-${i}`} aria-hidden data-slot=\"data-table-skeleton-row\" className=\"border-b border-[var(--ds-border-subtle)]\">\n {leafColumns.map((column) => (\n <td key={column.id} className={cellPadding} style={{ width: widthFor(column, column.getSize()) }}>\n <Skeleton className=\"h-3.5 w-3/4\" />\n </td>\n ))}\n <td style={fillerStyle} className=\"p-0\" />\n </tr>\n ));\n\n return (\n <div data-slot=\"data-table\" className={cn(\"flex min-w-0 flex-col gap-3\", className)}>\n {errorMessage ? (\n <ErrorBanner message={errorMessage} onRetry={instance.error ? instance.refetch : undefined} />\n ) : null}\n {toolbar === false ? null : typeof toolbar === \"function\" ? (\n toolbar({ instance })\n ) : (\n <DataTableToolbar\n table={table}\n bulkActions={bulkActions}\n searchPlaceholder={searchPlaceholder}\n enableExport={enableExport}\n exportFileName={exportFileName}\n refresh={refreshProps}\n >\n {toolbarActions}\n </DataTableToolbar>\n )}\n <div\n ref={scrollRef}\n data-slot=\"data-table-container\"\n onScroll={onScroll}\n style={{ maxHeight: resolvedHeight }}\n className={cn(\n \"relative w-full overflow-auto rounded-[var(--ds-radius-md)] border border-[var(--ds-border)] bg-[var(--ds-card)]\",\n )}\n >\n <table\n ref={tableRef}\n role=\"grid\"\n data-slot=\"data-table-grid\"\n data-virtualized={isVirtual ? \"\" : undefined}\n aria-rowcount={rows.length + 1}\n aria-colcount={colCount}\n aria-busy={instance.isFetching || undefined}\n aria-multiselectable={table.options.enableRowSelection ? true : undefined}\n onKeyDown={onKeyDown}\n onFocus={onFocus}\n style={{ minWidth: table.getTotalSize() }}\n className={cn(\n \"w-full table-fixed border-collapse caption-bottom\",\n \"text-[length:var(--ds-text-body-sm)] leading-[var(--ds-text-body-sm--line-height)] text-[color:var(--ds-foreground)]\",\n )}\n >\n <thead data-slot=\"data-table-header\" className={cn(stickyHeader && \"sticky top-0 z-[2]\")}>\n {headerGroups.map((group) => {\n const endStart = group.headers.findIndex((h) => h.column.getIsPinned() === \"end\");\n const splitAt = endStart === -1 ? group.headers.length : endStart;\n const renderHead = (header: (typeof group.headers)[number], c: number) => {\n const column = header.column;\n const meta = column.columnDef.meta;\n const pin = pinAttrs(column, table);\n const sorted = column.getIsSorted();\n const def = column.columnDef;\n return (\n <th\n key={header.id}\n role=\"columnheader\"\n scope=\"col\"\n data-slot=\"data-table-head\"\n data-row-index={-1}\n data-col-index={c}\n data-pinned={pin.pinned}\n data-pinned-edge={pin.edge ? \"\" : undefined}\n aria-colindex={c + 1}\n aria-sort={\n sorted === \"asc\"\n ? \"ascending\"\n : sorted === \"desc\"\n ? \"descending\"\n : column.getCanSort()\n ? \"none\"\n : undefined\n }\n colSpan={header.colSpan}\n tabIndex={tabIndexFor(-1, c)}\n // Only where the column says so. A header is one short\n // phrase the consumer chose, so repeating it in a tooltip\n // on every `<th>` is noise, and it was wrong outright on\n // the select and expand columns, whose header is a node\n // with no sentence in it. `meta.label` is the column\n // saying what it is called; when it is set, `title` on the\n // `<th>` reaches the button inside it too, since a pointer\n // over a node with no title of its own takes its\n // ancestor's. Body cells keep their own hover text.\n title={meta?.label}\n style={{ width: widthFor(column, header.getSize()), ...pin.style }}\n className={cn(\n \"group/head relative h-10 align-middle font-medium outline-none\",\n headPadding,\n \"bg-[var(--ds-muted)] text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\",\n \"border-b border-[var(--ds-border)]\",\n alignClass[meta?.align ?? \"start\"],\n \"focus-visible:ring-[2px] focus-visible:ring-inset focus-visible:ring-[var(--ds-ring)]\",\n pin.pinned && \"z-[3]\",\n pin.pinned === \"start\" && pin.edge && \"shadow-[inset_-1px_0_0_var(--ds-border)]\",\n pin.pinned === \"end\" && pin.edge && \"shadow-[inset_1px_0_0_var(--ds-border)]\",\n meta?.headerClassName,\n )}\n >\n {header.isPlaceholder ? null : typeof def.header === \"string\" ? (\n <DataTableColumnHeader column={column} title={def.header} />\n ) : (\n <table.FlexRender header={header} />\n )}\n {column.getCanResize() ? (\n <div\n data-slot=\"data-table-resizer\"\n data-resizing={column.getIsResizing() ? \"\" : undefined}\n onMouseDown={header.getResizeHandler()}\n onTouchStart={header.getResizeHandler()}\n onDoubleClick={() => column.resetSize()}\n className={cn(\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none\",\n \"opacity-0 transition-opacity duration-[var(--ds-duration-fast)] hover:opacity-100 data-[resizing]:opacity-100\",\n \"bg-[var(--ds-ring)]\",\n )}\n />\n ) : null}\n </th>\n );\n };\n return (\n <tr key={group.id} role=\"row\" aria-rowindex={1}>\n {group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c))}\n <th\n aria-hidden\n data-slot=\"data-table-filler-head\"\n style={fillerStyle}\n className=\"border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0\"\n />\n {group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))}\n </tr>\n );\n })}\n </thead>\n <tbody data-slot=\"data-table-body\">\n {showSkeleton ? skeletonRows : null}\n {showEmpty ? (\n <tr role=\"row\" data-slot=\"data-table-empty\">\n <td role=\"gridcell\" colSpan={colCount + 1} className=\"p-0\">\n {emptyState ?? (\n <div className=\"flex flex-col items-center justify-center gap-2 px-4 py-12 text-center text-[color:var(--ds-muted-foreground)]\">\n <Inbox aria-hidden className=\"size-6 text-[color:var(--ds-text-tertiary)]\" />\n <p className=\"text-[length:var(--ds-text-body-sm)]\">{emptyMessage}</p>\n </div>\n )}\n </td>\n </tr>\n ) : null}\n {virtualItems ? (\n <>\n {paddingTop > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingTop }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n {virtualItems.map((item) => {\n const row = rows[item.index];\n return row ? renderRow(row, item.index, item.index) : null;\n })}\n {paddingBottom > 0 ? (\n <tr aria-hidden data-slot=\"data-table-spacer\" style={{ height: paddingBottom }}>\n <td colSpan={colCount + 1} className=\"p-0\" />\n </tr>\n ) : null}\n </>\n ) : (\n rows.map((row, index) => renderRow(row, index))\n )}\n </tbody>\n </table>\n {paginationMode === \"infinite\" && instance.isFetching && rows.length > 0 ? (\n <div\n data-slot=\"data-table-loading-more\"\n className=\"flex items-center justify-center gap-2 border-t border-[var(--ds-border-subtle)] py-2 text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\"\n >\n <Spinner size={14} /> Loading more\n </div>\n ) : null}\n </div>\n {paginationMode === \"pages\" ? (\n <DataTablePagination table={table} pageSizeOptions={pageSizeOptions} />\n ) : paginationMode === \"infinite\" ? (\n <div className=\"text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]\">\n {rows.length} of {instance.total}\n </div>\n ) : null}\n </div>\n );\n}\n\nexport { DataTable, DataTableView };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,MAAM,mBAAmB;AACzB,MAAM,yBAAyB;AAC/B,MAAM,wBAAwB;AAkE9B,SAAS,UAA+B,OAA4B;CAClE,IAAI,cAAc,OAAO;EACvB,MAAM,EAAE,UAAU,GAAG,SAAS;EAC9B,OAAO,oBAAC,eAAD;GAAyB;GAAU,GAAI;EAAO,CAAA;CACvD;CACA,OAAO,oBAAC,gBAAD,EAAgB,GAAI,MAAQ,CAAA;AACrC;AAEA,SAAS,eAAoC,OAA2D;CACtG,MAAM,EACJ,WACA,WACA,YACA,WACA,cACA,SACA,oBACA,SACA,aACA,gBACA,mBACA,YACA,cACA,iBACA,cACA,gBACA,SACA,OACA,YACA,aACA,cACA,KACA,GAAG,YACD;CACJ,MAAM,WAAW,aAAkB;EACjC,GAAG;EACH,iBAAiB,QAAQ,mBAAmB,QAAQ,kBAAkB;CACxE,CAAC;CACD,OACE,oBAAC,eAAD;EACY;EACC;EACA;EACC;EACD;EACG;EACL;EACW;EACX;EACI;EACG;EACG;EACP;EACE;EACG;EACH;EACE;EACP;EACF;EACK;EACC;EACC;EACT;CACN,CAAA;AAEL;AAMA,SAAS,SACP,QACA,OACoF;CACpF,MAAM,SAAS,OAAO,YAAY;CAClC,IAAI,CAAC,QAAQ,OAAO;EAAE,OAAO,CAAC;EAAG,QAAQ,KAAA;EAAW,MAAM;CAAM;CAChE,IAAI,WAAW,SAAS;EACtB,MAAM,OAAO,MAAM,2BAA2B,CAAC,CAAC,GAAG,EAAE;EACrD,OAAO;GACL,OAAO;IAAE,UAAU;IAAU,MAAM,OAAO,SAAS,OAAO;GAAE;GAC5D;GACA,MAAM,MAAM,OAAO,OAAO;EAC5B;CACF;CACA,MAAM,QAAQ,MAAM,yBAAyB,CAAC,CAAC;CAC/C,OAAO;EACL,OAAO;GAAE,UAAU;GAAU,OAAO,OAAO,SAAS,KAAK;EAAE;EAC3D;EACA,MAAM,OAAO,OAAO,OAAO;CAC7B;AACF;AAEA,MAAM,aAAa;CAAE,OAAO;CAAa,QAAQ;CAAe,KAAK;AAAa;;;;;;;;;;;AAgBlF,SAAS,aAAkC,MAAmD;CAE5F,MAAM,OADM,KAAK,OAAO,UACP;CACjB,IAAI,MAAM,WAAW,OAAO,KAAK,UAAU,KAAK,IAAI,QAAQ,KAAK,KAAA;CACjE,IAAI,CAAC,MAAM,WAAW,OAAO,KAAA;CAC7B,MAAM,QAAQ,KAAK,SAAS;CAG5B,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,WACjD,OAAO,KAAK,KAAK,KAAA,IACjB,KAAA;AACN;AAMA,SAAS,aAAkC,EACzC,MACA,UACA,UAKC;CACD,MAAM,WAAW,KAAK,OAAO,UAAU,MAAM;CAC7C,MAAM,SAAS,OAAO,aAAa,WAAW,WAAW,CAAC;CAC1D,MAAM,UAAU,KAAK,SAAS;CAC9B,MAAM,CAAC,OAAO,YAAY,MAAM,SAAS,WAAW,OAAO,KAAK,OAAO,OAAO,CAAC;CAC/E,MAAM,OAAO,MAAM,OAAO,KAAK;CAE/B,MAAM,eAAe;EACnB,KAAK,UAAU;EACf,SAAS,QAAQ,KAAK;EACtB,OAAO;CACT;CACA,MAAM,eAAe;EACnB,IAAI,KAAK,SAAS;EAClB,MAAM,QAAQ,OAAO,QAAQ,OAAO,MAAM,KAAK,IAAI,OAAO,SAAS,WAAW,OAAO,KAAK,IAAI;EAC9F,SAAc,QAAQ,OAAO,KAAK,KAAK,KAAK,OAAO,IAAI,KAAK;EAC5D,OAAO;CACT;CACA,MAAM,eAAe;EACnB,IAAI,KAAK,SAAS;EAClB,OAAO;CACT;CAEA,OACE,oBAAC,OAAD;EACE,aAAU;EACV,WAAA;EACA,MAAM,OAAO,SAAS,WAAW,WAAW;EAC5C,OAAO;EACP,WAAW,MAAM,SAAS,EAAE,OAAO,KAAK;EACxC,UAAU,MAAM,EAAE,OAAO,OAAO;EAChC,QAAQ;EACR,YAAY,MAAM;GAEhB,EAAE,gBAAgB;GAClB,IAAI,EAAE,QAAQ,SAAS;IACrB,EAAE,eAAe;IACjB,OAAO;GACT,OAAO,IAAI,EAAE,QAAQ,UAAU;IAC7B,EAAE,eAAe;IACjB,OAAO;GACT;EACF;EACA,WAAU;CACX,CAAA;AAEL;AAQA,SAAS,cAAmC,EAC1C,UACA,WACA,WACA,YACA,YAAY,IACZ,eAAe,MACf,UAAU,WACV,oBACA,UAAU,MACV,aACA,gBACA,mBACA,YACA,eAAe,cACf,iBACA,eAAe,MACf,iBAAiB,UACjB,SACA,OACA,YACA,aACA,cACA,OACiE;CACjE,MAAM,EAAE,OAAO,mBAAmB;CAClC,MAAM,OAAO,MAAM,YAAY,CAAC,CAAC;CACjC,MAAM,cAAc,MAAM,sBAAsB;CAOhD,MAAM,eAAe,YAAY,MAC9B,WAAW,OAAO,UAAU,MAAM,QAAQ,CAAC,OAAO,YAAY,CACjE,CAAC,EAAE;CACH,MAAM,YAAY,QAAmC,SACnD,OAAO,OAAO,eAAe,KAAA,IAAY;CAE3C,MAAM,cAAc,eAAgB,EAAE,OAAO,EAAE,IAAc,KAAA;CAC7D,MAAM,WAAW,YAAY;CAC7B,MAAM,eAAe,MAAM,gBAAgB;CAC3C,MAAM,YAAY,cAAc,KAAK,SAAS;CAC9C,MAAM,YAAY,WAAW,SAAS;CACtC,MAAM,eAAe,aAAa,KAAK,WAAW;CAClD,MAAM,YAAY,CAAC,aAAa,KAAK,WAAW;CAChD,MAAM,iBAAiB,cAAc,YAAY,yBAAyB,KAAA;CAC1E,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAW,SAAS,SAAS,OAAO,WAAW;CAEnG,MAAM,YAAY,MAAM,OAAuB,IAAI;CACnD,MAAM,WAAW,MAAM,OAAyB,IAAI;CAEpD,MAAM,UAAU,SAAS;CACzB,MAAM,oBAAoB,YAAY;EAAE;EAAS;CAAS,IAAI,CAAC,SAAS,QAAQ,CAAC;CAKjF,MAAM,eACJ,SAAS,WAAW,SAAS,WACzB;EACE,WAAW;EACX,YAAY,SAAS;EACrB,eAAe,SAAS;EACxB,aAAa,SAAS;EACtB,aAAa,SAAS;CACxB,IACA,KAAA;CAIN,MAAM,cAAc,eAAoD;EACtE,OAAO,KAAK;EACZ,SAAS;EACT,wBAAwB,UAAU;EAClC,oBAAoB;EACpB,UAAU;EACV,aAAa,UAAU,KAAK,MAAM,EAAE,MAAM;EAE1C,iBAAiB,YAAY;GAC3B,IAAI,SAAS,QAAQ,sBAAsB,CAAC,CAAC;GAC7C,MAAM,OAAO,QAAQ;GACrB,IAAI,MAAM,QAAQ,SAAS,qBAAqB,UAAU,KAAK,sBAAsB,CAAC,CAAC;GACvF,OAAO;EACT;CACF,CAAC;CACD,MAAM,eAAe,YAAY,YAAY,gBAAgB,IAAI;CACjE,MAAM,aAAa,cAAc,SAAS,aAAa,EAAE,CAAE,QAAQ;CACnE,MAAM,gBAAgB,cAAc,SAChC,YAAY,aAAa,IAAI,aAAa,aAAa,SAAS,EAAE,CAAE,MACpE;CAIJ,MAAM,WAAW,MAAM,aACpB,UAAyC;EACxC,IAAI,mBAAmB,cAAc,CAAC,SAAS,eAAe,SAAS,YAAY;EACnF,MAAM,KAAK,MAAM;EACjB,IAAI,GAAG,eAAe,GAAG,YAAY,GAAG,eAAe,uBAAuB,SAAS,cAAc;CACvG,GACA,CAAC,gBAAgB,QAAQ,CAC3B;CAIA,MAAM,CAAC,SAAS,cAAc,MAAM,SAAoB;EAAE,GAAG;EAAI,GAAG;CAAE,CAAC;CACvE,MAAM,eAAe,MAAM,OAAyB,IAAI;CAExD,MAAM,eAAe,UACnB,SAAS,SAAS,cAChB,oBAAoB,MAAM,EAAE,qBAAqB,MAAM,EAAE,GAC3D,KAAK;CAEP,MAAM,YAAY,MAAM,aACrB,UAAqB;EACpB,MAAM,KAAK,YAAY,KAAK;EAC5B,IAAI,IAAI;GACN,WAAW,KAAK;GAChB,GAAG,MAAM;GACT;EACF;EACA,IAAI,aAAa,MAAM,KAAK,GAAG;GAC7B,aAAa,UAAU;GACvB,WAAW,KAAK;GAChB,YAAY,cAAc,MAAM,GAAG,EAAE,OAAO,OAAO,CAAC;EACtD;CACF,GAEA,CAAC,WAAW,WAAW,CACzB;CAEA,MAAM,gBAAgB;EACpB,MAAM,QAAQ,aAAa;EAC3B,IAAI,CAAC,OAAO;EACZ,MAAM,KAAK,YAAY,KAAK;EAC5B,IAAI,IAAI;GACN,aAAa,UAAU;GACvB,GAAG,MAAM;EACX;CACF,CAAC;CAED,MAAM,aAAa,UAAiD;EAClE,MAAM,SAAS,MAAM;EAErB,IAAI,CAAC,SAAS,SAAS,SAAS,MAAM,GAAG;EACzC,IAAI,SAAS,QAAQ,MAAM;EAC3B,MAAM,OAAO,OAAO,QAAqB,kCAAkC;EAC3E,IAAI,CAAC,MAAM;EACX,MAAM,IAAI,OAAO,KAAK,QAAQ,QAAQ;EACtC,MAAM,IAAI,OAAO,KAAK,QAAQ,QAAQ;EACtC,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,UAAU,WAAW;EAC3B,MAAM,eAAe,WAAW;EAEhC,QAAQ,MAAM,KAAd;GACE,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE,GAAG,KAAK,IAAI,SAAS,IAAI,CAAC;KAAG;IAAE,CAAC;IAC5C;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE,GAAG,KAAK,IAAI,IAAI,IAAI,CAAC;KAAG;IAAE,CAAC;IACvC;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE;KAAG,GAAG,KAAK,IAAI,SAAS,IAAI,CAAC;IAAE,CAAC;IAC5C;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE;KAAG,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;IAAE,CAAC;IACtC;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,MAAM,UAAU;KAAE,GAAG;KAAI,GAAG;IAAE,IAAI;KAAE;KAAG,GAAG;IAAE,CAAC;IACvD;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU,MAAM,UAAU;KAAE,GAAG;KAAS,GAAG;IAAQ,IAAI;KAAE;KAAG,GAAG;IAAQ,CAAC;IACxE;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE,GAAG,KAAK,IAAI,SAAS,IAAI,EAAE;KAAG;IAAE,CAAC;IAC7C;GACF,KAAK;IACH,MAAM,eAAe;IACrB,UAAU;KAAE,GAAG,KAAK,IAAI,IAAI,IAAI,EAAE;KAAG;IAAE,CAAC;IACxC;GACF,KAAK,SAAS;IACZ,IAAI,CAAC,cAAc;IACnB,IAAI,MAAM,IAAI;KACZ,MAAM,SAAS,YAAY;KAC3B,IAAI,QAAQ,WAAW,GAAG;MACxB,MAAM,eAAe;MACrB,OAAO,cAAc,KAAA,GAAW,MAAM,QAAQ;KAChD;KACA;IACF;IACA,MAAM,MAAM,KAAK;IACjB,MAAM,SAAS,YAAY;IAC3B,IAAI,OAAO,UAAU,OAAO,UAAU,MAAM,UAAU;KACpD,MAAM,eAAe;KACrB,SAAS,QAAQ,MAAM;MAAE,OAAO,IAAI;MAAI,UAAU,OAAO;KAAG,CAAC;IAC/D;IACA;GACF;GACA,KAAK,KAAK;IACR,IAAI,CAAC,gBAAgB,MAAM,IAAI;IAC/B,MAAM,MAAM,KAAK;IACjB,IAAI,KAAK,aAAa,GAAG;KACvB,MAAM,eAAe;KACrB,IAAI,eAAe;IACrB;IACA;GACF;GACA,SACE;EACJ;CACF;CAEA,MAAM,WAAW,UAA8C;EAC7D,MAAM,OAAQ,MAAM,OAAuB,QAAqB,kCAAkC;EAClG,IAAI,CAAC,QAAQ,CAAC,SAAS,SAAS,SAAS,IAAI,GAAG;EAChD,MAAM,IAAI,OAAO,KAAK,QAAQ,QAAQ;EACtC,MAAM,IAAI,OAAO,KAAK,QAAQ,QAAQ;EACtC,YAAY,MAAO,EAAE,MAAM,KAAK,EAAE,MAAM,IAAI,IAAI;GAAE;GAAG;EAAE,CAAE;CAC3D;CAEA,MAAM,eAAe,GAAW,MAAe,QAAQ,MAAM,KAAK,QAAQ,MAAM,IAAI,IAAI;CAIxF,MAAM,cAAc,YAAY,YAAY,gBAAgB;CAC5D,MAAM,cAAc,YAAY,YAAY,gBAAgB;CAC5D,MAAM,cAAc,SAAS,QAAQ;CAErC,MAAM,aAAa,KAAwB,OAAe,iBAA0B;EAClF,MAAM,QAAQ,IAAI,gBAAgB;EAClC,MAAM,WAAW,MAAM,WAAW,SAAS,KAAK,OAAO,YAAY,MAAM,KAAK;EAC9E,MAAM,UAAU,aAAa,KAAK,MAAM,SAAS;EACjD,MAAM,WAAW,IAAI,cAAc;EACnC,MAAM,SAAS,eAAe,QAAQ,IAAI,OAAO;EACjD,MAAM,WAAW,sBAAsB,IAAI,cAAc;EAEzD,MAAM,cAAc,MAA+B,MAAc;GAC/D,MAAM,SAAS,KAAK;GACpB,MAAM,OAAO,OAAO,UAAU;GAC9B,MAAM,MAAM,SAAS,QAAQ,KAAK;GAClC,MAAM,YAAY,aAAa,UAAU,IAAI,MAAM,YAAY,aAAa,OAAO;GACnF,MAAM,UAAU,QAAQ,MAAM,QAAQ;GACtC,OACE,oBAAC,MAAD;IAEE,MAAK;IACL,aAAU;IACV,kBAAgB;IAChB,kBAAgB;IAChB,eAAa,IAAI;IACjB,oBAAkB,IAAI,OAAO,KAAK,KAAA;IAClC,iBAAe,UAAU,KAAK,KAAA;IAC9B,gBAAc,YAAY,KAAK,KAAA;IAC/B,iBAAe,IAAI;IACnB,UAAU,YAAY,OAAO,CAAC;IAC9B,OAAO,YAAY,KAAA,IAAY,aAAa,IAAI;IAChD,OAAO;KAAE,OAAO,SAAS,QAAQ,OAAO,QAAQ,CAAC;KAAG,GAAG,IAAI;IAAM;IACjE,eACE,WAAW,CAAC,kBACF,SAAS,QAAQ,MAAM;KAAE,OAAO,IAAI;KAAI,UAAU,OAAO;IAAG,CAAC,IACnE,KAAA;IAEN,WAAW,GACT,sCACA,YAAY,gBAAgB,aAC5B,WAAW,MAAM,SAAS,UAC1B,yFAGA,IAAI,UACF,6RACF,IAAI,WAAW,WAAW,IAAI,QAAQ,4CACtC,IAAI,WAAW,SAAS,IAAI,QAAQ,2CACpC,CAAC,aAAa,YACd,MAAM,aACR;IAEC,UAAA,YACC,oBAAC,cAAD;KACQ;KACI;KACV,cAAc;MACZ,aAAa,UAAU;OAAE,GAAG;OAAO;MAAE;KACvC;IACD,CAAA,IAED,oBAAC,MAAM,YAAP,EAAwB,KAAO,CAAA;GAE/B,GA5CG,KAAK,EA4CR;EAER;EAEA,OACE,qBAAC,MAAM,UAAP,EAAA,UAAA,CACE,qBAAC,MAAD;GACE,MAAK;GACL,aAAU;GACV,cAAY;GACZ,iBAAe,WAAW,KAAK,KAAA;GAC/B,eAAa,SAAS,KAAK,KAAA;GAC3B,iBAAe,WAAW,KAAK,KAAA;GAC/B,iBAAe,QAAQ;GACvB,iBAAe,MAAM,QAAQ,qBAAqB,WAAW,KAAA;GAC7D,KAAK,YAAY,YAAY,iBAAiB,KAAA;GAC9C,SAAS,cAAc,MAAM,WAAW,IAAI,UAAU,CAAC,IAAI,KAAA;GAC3D,WAAW,GACT,4GACA,0EAGA,6CACA,cAAc,kBACd,eAAe,IAAI,QAAQ,CAC7B;GAnBF,UAAA;IAqBG,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,MAAM,MAAM,WAAW,MAAM,CAAC,CAAC;IAC7D,oBAAC,MAAD;KAAI,eAAA;KAAY,aAAU;KAAoB,OAAO;KAAa,WAAU;IAAO,CAAA;IAClF,MAAM,MAAM,OAAO,CAAC,CAAC,KAAK,MAAM,MAAM,WAAW,MAAM,UAAU,CAAC,CAAC;GAClE;EACH,CAAA,GAAA,WACC,oBAAC,MAAD;GAAI,MAAK;GAAM,aAAU;GAAoB,WAAU;GACrD,UAAA,oBAAC,MAAD;IACE,MAAK;IACL,SAAS,WAAW;IACpB,WAAU;IAET,UAAA,mBAAmB,GAAG;GACrB,CAAA;EACF,CAAA,IACF,IACU,EAAA,GArCK,IAAI,EAqCT;CAEpB;CAEA,MAAM,eAAe,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,MAAM,WAAW,QAAQ,EAAE,IAAI,GAAG,MAC5F,qBAAC,MAAD;EAA0B,eAAA;EAAY,aAAU;EAA0B,WAAU;EAApF,UAAA,CACG,YAAY,KAAK,WAChB,oBAAC,MAAD;GAAoB,WAAW;GAAa,OAAO,EAAE,OAAO,SAAS,QAAQ,OAAO,QAAQ,CAAC,EAAE;GAC7F,UAAA,oBAAC,UAAD,EAAU,WAAU,cAAe,CAAA;EACjC,GAFK,OAAO,EAEZ,CACL,GACD,oBAAC,MAAD;GAAI,OAAO;GAAa,WAAU;EAAO,CAAA,CACvC;CAPK,GAAA,YAAY,GAOjB,CACL;CAED,OACE,qBAAC,OAAD;EAAK,aAAU;EAAa,WAAW,GAAG,+BAA+B,SAAS;EAAlF,UAAA;GACG,eACC,oBAAC,aAAD;IAAa,SAAS;IAAc,SAAS,SAAS,QAAQ,SAAS,UAAU,KAAA;GAAY,CAAA,IAC3F;GACH,YAAY,QAAQ,OAAO,OAAO,YAAY,aAC7C,QAAQ,EAAE,SAAS,CAAC,IAEpB,oBAAC,kBAAD;IACS;IACM;IACM;IACL;IACE;IAChB,SAAS;IAER,UAAA;GACe,CAAA;GAEpB,qBAAC,OAAD;IACE,KAAK;IACL,aAAU;IACA;IACV,OAAO,EAAE,WAAW,eAAe;IACnC,WAAW,GACT,kHACF;IAPF,UAAA,CASE,qBAAC,SAAD;KACE,KAAK;KACL,MAAK;KACL,aAAU;KACV,oBAAkB,YAAY,KAAK,KAAA;KACnC,iBAAe,KAAK,SAAS;KAC7B,iBAAe;KACf,aAAW,SAAS,cAAc,KAAA;KAClC,wBAAsB,MAAM,QAAQ,qBAAqB,OAAO,KAAA;KACrD;KACF;KACT,OAAO,EAAE,UAAU,MAAM,aAAa,EAAE;KACxC,WAAW,GACT,qDACA,sHACF;KAfF,UAAA,CAiBE,oBAAC,SAAD;MAAO,aAAU;MAAoB,WAAW,GAAG,gBAAgB,oBAAoB;MACpF,UAAA,aAAa,KAAK,UAAU;OAC3B,MAAM,WAAW,MAAM,QAAQ,WAAW,MAAM,EAAE,OAAO,YAAY,MAAM,KAAK;OAChF,MAAM,UAAU,aAAa,KAAK,MAAM,QAAQ,SAAS;OACzD,MAAM,cAAc,QAAwC,MAAc;QACxE,MAAM,SAAS,OAAO;QACtB,MAAM,OAAO,OAAO,UAAU;QAC9B,MAAM,MAAM,SAAS,QAAQ,KAAK;QAClC,MAAM,SAAS,OAAO,YAAY;QAClC,MAAM,MAAM,OAAO;QACnB,OACE,qBAAC,MAAD;SAEE,MAAK;SACL,OAAM;SACN,aAAU;SACV,kBAAgB;SAChB,kBAAgB;SAChB,eAAa,IAAI;SACjB,oBAAkB,IAAI,OAAO,KAAK,KAAA;SAClC,iBAAe,IAAI;SACnB,aACE,WAAW,QACP,cACA,WAAW,SACT,eACA,OAAO,WAAW,IAChB,SACA,KAAA;SAEV,SAAS,OAAO;SAChB,UAAU,YAAY,IAAI,CAAC;SAU3B,OAAO,MAAM;SACb,OAAO;UAAE,OAAO,SAAS,QAAQ,OAAO,QAAQ,CAAC;UAAG,GAAG,IAAI;SAAM;SACjE,WAAW,GACT,kEACA,aACA,wGACA,sCACA,WAAW,MAAM,SAAS,UAC1B,yFACA,IAAI,UAAU,SACd,IAAI,WAAW,WAAW,IAAI,QAAQ,4CACtC,IAAI,WAAW,SAAS,IAAI,QAAQ,2CACpC,MAAM,eACR;SA3CF,UAAA,CA6CG,OAAO,gBAAgB,OAAO,OAAO,IAAI,WAAW,WACnD,oBAAC,uBAAD;UAA+B;UAAQ,OAAO,IAAI;SAAS,CAAA,IAE3D,oBAAC,MAAM,YAAP,EAA0B,OAAS,CAAA,GAEpC,OAAO,aAAa,IACnB,oBAAC,OAAD;UACE,aAAU;UACV,iBAAe,OAAO,cAAc,IAAI,KAAK,KAAA;UAC7C,aAAa,OAAO,iBAAiB;UACrC,cAAc,OAAO,iBAAiB;UACtC,qBAAqB,OAAO,UAAU;UACtC,WAAW,GACT,8EACA,iHACA,qBACF;SACD,CAAA,IACC,IACF;QA/DG,GAAA,OAAO,EA+DV;OAER;OACA,OACE,qBAAC,MAAD;QAAmB,MAAK;QAAM,iBAAe;QAA7C,UAAA;SACG,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,CAAC,KAAK,QAAQ,MAAM,WAAW,QAAQ,CAAC,CAAC;SACzE,oBAAC,MAAD;UACE,eAAA;UACA,aAAU;UACV,OAAO;UACP,WAAU;SACX,CAAA;SACA,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC,KAAK,QAAQ,MAAM,WAAW,QAAQ,UAAU,CAAC,CAAC;QAC9E;OATK,GAAA,MAAM,EASX;MAER,CAAC;KACI,CAAA,GACP,qBAAC,SAAD;MAAO,aAAU;MAAjB,UAAA;OACG,eAAe,eAAe;OAC9B,YACC,oBAAC,MAAD;QAAI,MAAK;QAAM,aAAU;QACvB,UAAA,oBAAC,MAAD;SAAI,MAAK;SAAW,SAAS,WAAW;SAAG,WAAU;SAClD,UAAA,cACC,qBAAC,OAAD;UAAK,WAAU;UAAf,UAAA,CACE,oBAAC,OAAD;WAAO,eAAA;WAAY,WAAU;UAA+C,CAAA,GAC5E,oBAAC,KAAD;WAAG,WAAU;WAAwC,UAAA;UAAgB,CAAA,CAClE;;QAEL,CAAA;OACF,CAAA,IACF;OACH,eACC,qBAAA,UAAA,EAAA,UAAA;QACG,aAAa,IACZ,oBAAC,MAAD;SAAI,eAAA;SAAY,aAAU;SAAoB,OAAO,EAAE,QAAQ,WAAW;SACxE,UAAA,oBAAC,MAAD;UAAI,SAAS,WAAW;UAAG,WAAU;SAAO,CAAA;QAC1C,CAAA,IACF;QACH,aAAa,KAAK,SAAS;SAC1B,MAAM,MAAM,KAAK,KAAK;SACtB,OAAO,MAAM,UAAU,KAAK,KAAK,OAAO,KAAK,KAAK,IAAI;QACxD,CAAC;QACA,gBAAgB,IACf,oBAAC,MAAD;SAAI,eAAA;SAAY,aAAU;SAAoB,OAAO,EAAE,QAAQ,cAAc;SAC3E,UAAA,oBAAC,MAAD;UAAI,SAAS,WAAW;UAAG,WAAU;SAAO,CAAA;QAC1C,CAAA,IACF;OACJ,EAAA,CAAA,IAEF,KAAK,KAAK,KAAK,UAAU,UAAU,KAAK,KAAK,CAAC;MAE3C;KACF,CAAA,CAAA;IACN,CAAA,GAAA,mBAAmB,cAAc,SAAS,cAAc,KAAK,SAAS,IACrE,qBAAC,OAAD;KACE,aAAU;KACV,WAAU;KAFZ,UAAA,CAIE,oBAAC,SAAD,EAAS,MAAM,GAAK,CAAA,GAAC,eAClB;IACH,CAAA,IAAA,IACD;;GACJ,mBAAmB,UAClB,oBAAC,qBAAD;IAA4B;IAAwB;GAAkB,CAAA,IACpE,mBAAmB,aACrB,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA;KACG,KAAK;KAAO;KAAK,SAAS;IACxB;GACH,CAAA,IAAA;EACD;;AAET"}
|
|
@@ -10,7 +10,20 @@ import { jsx } from "react/jsx-runtime";
|
|
|
10
10
|
* when `enableRowSelection` is set, the expander when a `renderSubComponent`
|
|
11
11
|
* is given. Both are display columns, neither sorts, hides, resizes or
|
|
12
12
|
* exports, and a consumer column with the same id replaces them.
|
|
13
|
+
*
|
|
14
|
+
* Both controls stop the event from reaching the row (DESIGN-79). A table
|
|
15
|
+
* with `onRowClick` puts a navigation on the whole row, and without this the
|
|
16
|
+
* chevron opens the detail panel and navigates away from it in the same
|
|
17
|
+
* gesture — and the checkbox selects a row and then leaves the page. It has
|
|
18
|
+
* to be here: every consumer of these columns would otherwise need the same
|
|
19
|
+
* guard, and the one that forgets has a control that cannot be used.
|
|
13
20
|
*/
|
|
21
|
+
const stopRowActivation = {
|
|
22
|
+
onClick: (event) => event.stopPropagation(),
|
|
23
|
+
onKeyDown: (event) => {
|
|
24
|
+
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
14
27
|
function selectColumn(helper) {
|
|
15
28
|
return helper.display({
|
|
16
29
|
id: "select",
|
|
@@ -21,14 +34,16 @@ function selectColumn(helper) {
|
|
|
21
34
|
"aria-label": "Select all rows on this page",
|
|
22
35
|
checked: all,
|
|
23
36
|
indeterminate: some,
|
|
24
|
-
onCheckedChange: (checked) => table.toggleAllPageRowsSelected(Boolean(checked))
|
|
37
|
+
onCheckedChange: (checked) => table.toggleAllPageRowsSelected(Boolean(checked)),
|
|
38
|
+
...stopRowActivation
|
|
25
39
|
});
|
|
26
40
|
},
|
|
27
41
|
cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
28
42
|
"aria-label": "Select row",
|
|
29
43
|
checked: row.getIsSelected(),
|
|
30
44
|
disabled: !row.getCanSelect(),
|
|
31
|
-
onCheckedChange: (checked) => row.toggleSelected(Boolean(checked))
|
|
45
|
+
onCheckedChange: (checked) => row.toggleSelected(Boolean(checked)),
|
|
46
|
+
...stopRowActivation
|
|
32
47
|
}),
|
|
33
48
|
size: 40,
|
|
34
49
|
minSize: 40,
|
|
@@ -58,7 +73,11 @@ function expandColumn(helper) {
|
|
|
58
73
|
"data-slot": "data-table-expander",
|
|
59
74
|
"aria-label": row.getIsExpanded() ? "Collapse row" : "Expand row",
|
|
60
75
|
"aria-expanded": row.getIsExpanded(),
|
|
61
|
-
onClick:
|
|
76
|
+
onClick: (event) => {
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
row.getToggleExpandedHandler()();
|
|
79
|
+
},
|
|
80
|
+
onKeyDown: stopRowActivation.onKeyDown,
|
|
62
81
|
className: cn("inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-sm)]", "text-[color:var(--ds-muted-foreground)] hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]", "outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40"),
|
|
63
82
|
children: /* @__PURE__ */ jsx(ChevronRight, { className: cn("size-4 transition-transform duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]", row.getIsExpanded() && "rotate-90") })
|
|
64
83
|
}) : null,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-columns.js","names":[],"sources":["../../../src/components/data-table/system-columns.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ColumnHelper, RowData } from \"@tanstack/react-table\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Checkbox } from \"@/components/checkbox\";\nimport { cn } from \"@/lib/cn\";\nimport type { DataTableFeatures } from \"./features\";\nimport type { DataTableColumnDef } from \"./types\";\n\n/**\n * Columns the table adds on the consumer's behalf: the selection checkbox\n * when `enableRowSelection` is set, the expander when a `renderSubComponent`\n * is given. Both are display columns, neither sorts, hides, resizes or\n * exports, and a consumer column with the same id replaces them.\n */\nexport function selectColumn<Row extends RowData>(\n helper: ColumnHelper<DataTableFeatures<Row>, Row>,\n): DataTableColumnDef<Row, unknown> {\n return helper.display({\n id: \"select\",\n header: ({ table }) => {\n const all = table.getIsAllPageRowsSelected();\n const some = table.getIsSomePageRowsSelected() && !all;\n return (\n <Checkbox\n aria-label=\"Select all rows on this page\"\n checked={all}\n indeterminate={some}\n onCheckedChange={(checked) => table.toggleAllPageRowsSelected(Boolean(checked))}\n />\n );\n },\n cell: ({ row }) => (\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(checked) => row.toggleSelected(Boolean(checked))}\n />\n ),\n size: 40,\n minSize: 40,\n maxSize: 40,\n enableSorting: false,\n enableHiding: false,\n enableResizing: false,\n enableColumnFilter: false,\n enableGlobalFilter: false,\n meta: { exportable: false, align: \"center\", cellClassName: \"px-2\", headerClassName: \"px-2\" },\n });\n}\n\nexport function expandColumn<Row extends RowData>(\n helper: ColumnHelper<DataTableFeatures<Row>, Row>,\n): DataTableColumnDef<Row, unknown> {\n return helper.display({\n id: \"expand\",\n header: () => <span className=\"sr-only\">Expand</span>,\n cell: ({ row }) =>\n row.getCanExpand() ? (\n <button\n type=\"button\"\n data-slot=\"data-table-expander\"\n aria-label={row.getIsExpanded() ? \"Collapse row\" : \"Expand row\"}\n aria-expanded={row.getIsExpanded()}\n onClick={row.getToggleExpandedHandler()}\n className={cn(\n \"inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-sm)]\",\n \"text-[color:var(--ds-muted-foreground)] hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40\",\n )}\n >\n <ChevronRight\n className={cn(\n \"size-4 transition-transform duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n row.getIsExpanded() && \"rotate-90\",\n )}\n />\n </button>\n ) : null,\n size: 40,\n minSize: 40,\n maxSize: 40,\n enableSorting: false,\n enableHiding: false,\n enableResizing: false,\n enableColumnFilter: false,\n enableGlobalFilter: false,\n meta: { exportable: false, align: \"center\", cellClassName: \"px-2\", headerClassName: \"px-2\" },\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"system-columns.js","names":[],"sources":["../../../src/components/data-table/system-columns.tsx"],"sourcesContent":["\"use client\";\n\nimport type { ColumnHelper, RowData } from \"@tanstack/react-table\";\nimport type * as React from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { Checkbox } from \"@/components/checkbox\";\nimport { cn } from \"@/lib/cn\";\nimport type { DataTableFeatures } from \"./features\";\nimport type { DataTableColumnDef } from \"./types\";\n\n/**\n * Columns the table adds on the consumer's behalf: the selection checkbox\n * when `enableRowSelection` is set, the expander when a `renderSubComponent`\n * is given. Both are display columns, neither sorts, hides, resizes or\n * exports, and a consumer column with the same id replaces them.\n *\n * Both controls stop the event from reaching the row (DESIGN-79). A table\n * with `onRowClick` puts a navigation on the whole row, and without this the\n * chevron opens the detail panel and navigates away from it in the same\n * gesture — and the checkbox selects a row and then leaves the page. It has\n * to be here: every consumer of these columns would otherwise need the same\n * guard, and the one that forgets has a control that cannot be used.\n */\nconst stopRowActivation = {\n onClick: (event: React.MouseEvent) => event.stopPropagation(),\n // A button fires `click` for Enter and Space, so `onClick` covers the\n // keyboard too; this stops the grid's own key handling from seeing the key\n // before the control does.\n onKeyDown: (event: React.KeyboardEvent) => {\n if (event.key === \"Enter\" || event.key === \" \") event.stopPropagation();\n },\n} as const;\nexport function selectColumn<Row extends RowData>(\n helper: ColumnHelper<DataTableFeatures<Row>, Row>,\n): DataTableColumnDef<Row, unknown> {\n return helper.display({\n id: \"select\",\n header: ({ table }) => {\n const all = table.getIsAllPageRowsSelected();\n const some = table.getIsSomePageRowsSelected() && !all;\n return (\n <Checkbox\n aria-label=\"Select all rows on this page\"\n checked={all}\n indeterminate={some}\n onCheckedChange={(checked) => table.toggleAllPageRowsSelected(Boolean(checked))}\n {...stopRowActivation}\n />\n );\n },\n cell: ({ row }) => (\n <Checkbox\n aria-label=\"Select row\"\n checked={row.getIsSelected()}\n disabled={!row.getCanSelect()}\n onCheckedChange={(checked) => row.toggleSelected(Boolean(checked))}\n {...stopRowActivation}\n />\n ),\n size: 40,\n minSize: 40,\n maxSize: 40,\n enableSorting: false,\n enableHiding: false,\n enableResizing: false,\n enableColumnFilter: false,\n enableGlobalFilter: false,\n meta: { exportable: false, align: \"center\", cellClassName: \"px-2\", headerClassName: \"px-2\" },\n });\n}\n\nexport function expandColumn<Row extends RowData>(\n helper: ColumnHelper<DataTableFeatures<Row>, Row>,\n): DataTableColumnDef<Row, unknown> {\n return helper.display({\n id: \"expand\",\n header: () => <span className=\"sr-only\">Expand</span>,\n cell: ({ row }) =>\n row.getCanExpand() ? (\n <button\n type=\"button\"\n data-slot=\"data-table-expander\"\n aria-label={row.getIsExpanded() ? \"Collapse row\" : \"Expand row\"}\n aria-expanded={row.getIsExpanded()}\n onClick={(event) => {\n event.stopPropagation();\n row.getToggleExpandedHandler()();\n }}\n onKeyDown={stopRowActivation.onKeyDown}\n className={cn(\n \"inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-sm)]\",\n \"text-[color:var(--ds-muted-foreground)] hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]\",\n \"outline-none focus-visible:ring-[3px] focus-visible:ring-[var(--ds-ring)]/40\",\n )}\n >\n <ChevronRight\n className={cn(\n \"size-4 transition-transform duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n row.getIsExpanded() && \"rotate-90\",\n )}\n />\n </button>\n ) : null,\n size: 40,\n minSize: 40,\n maxSize: 40,\n enableSorting: false,\n enableHiding: false,\n enableResizing: false,\n enableColumnFilter: false,\n enableGlobalFilter: false,\n meta: { exportable: false, align: \"center\", cellClassName: \"px-2\", headerClassName: \"px-2\" },\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,oBAAoB;CACxB,UAAU,UAA4B,MAAM,gBAAgB;CAI5D,YAAY,UAA+B;EACzC,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK,MAAM,gBAAgB;CACxE;AACF;AACA,SAAgB,aACd,QACkC;CAClC,OAAO,OAAO,QAAQ;EACpB,IAAI;EACJ,SAAS,EAAE,YAAY;GACrB,MAAM,MAAM,MAAM,yBAAyB;GAC3C,MAAM,OAAO,MAAM,0BAA0B,KAAK,CAAC;GACnD,OACE,oBAAC,UAAD;IACE,cAAW;IACX,SAAS;IACT,eAAe;IACf,kBAAkB,YAAY,MAAM,0BAA0B,QAAQ,OAAO,CAAC;IAC9E,GAAI;GACL,CAAA;EAEL;EACA,OAAO,EAAE,UACP,oBAAC,UAAD;GACE,cAAW;GACX,SAAS,IAAI,cAAc;GAC3B,UAAU,CAAC,IAAI,aAAa;GAC5B,kBAAkB,YAAY,IAAI,eAAe,QAAQ,OAAO,CAAC;GACjE,GAAI;EACL,CAAA;EAEH,MAAM;EACN,SAAS;EACT,SAAS;EACT,eAAe;EACf,cAAc;EACd,gBAAgB;EAChB,oBAAoB;EACpB,oBAAoB;EACpB,MAAM;GAAE,YAAY;GAAO,OAAO;GAAU,eAAe;GAAQ,iBAAiB;EAAO;CAC7F,CAAC;AACH;AAEA,SAAgB,aACd,QACkC;CAClC,OAAO,OAAO,QAAQ;EACpB,IAAI;EACJ,cAAc,oBAAC,QAAD;GAAM,WAAU;GAAU,UAAA;EAAY,CAAA;EACpD,OAAO,EAAE,UACP,IAAI,aAAa,IACf,oBAAC,UAAD;GACE,MAAK;GACL,aAAU;GACV,cAAY,IAAI,cAAc,IAAI,iBAAiB;GACnD,iBAAe,IAAI,cAAc;GACjC,UAAU,UAAU;IAClB,MAAM,gBAAgB;IACtB,IAAI,yBAAyB,CAAC,CAAC;GACjC;GACA,WAAW,kBAAkB;GAC7B,WAAW,GACT,gFACA,8GACA,8EACF;GAEA,UAAA,oBAAC,cAAD,EACE,WAAW,GACT,4FACA,IAAI,cAAc,KAAK,WACzB,EACD,CAAA;EACK,CAAA,IACN;EACN,MAAM;EACN,SAAS;EACT,SAAS;EACT,eAAe;EACf,cAAc;EACd,gBAAgB;EAChB,oBAAoB;EACpB,oBAAoB;EACpB,MAAM;GAAE,YAAY;GAAO,OAAO;GAAU,eAAe;GAAQ,iBAAiB;EAAO;CAC7F,CAAC;AACH"}
|
|
@@ -76,6 +76,34 @@ export interface DataTableColumnMeta<Row = unknown> {
|
|
|
76
76
|
}>;
|
|
77
77
|
/** Text alignment of the header and cells. Numbers usually want `end`. */
|
|
78
78
|
align?: "start" | "center" | "end";
|
|
79
|
+
/**
|
|
80
|
+
* This column takes whatever width the sized columns leave (DESIGN-77).
|
|
81
|
+
*
|
|
82
|
+
* The grid is `table-fixed`: every column is drawn at its `size`, and a
|
|
83
|
+
* column that never declared one sits at TanStack's 150 px default. On a
|
|
84
|
+
* list that is one wide column and a few narrow ones — a title beside an
|
|
85
|
+
* id, a status and an assignee — that means the column people actually read
|
|
86
|
+
* is clipped while the table shows empty space to its right. Mark that
|
|
87
|
+
* column `fill` and it is the one column drawn with no fixed width, so the
|
|
88
|
+
* remainder lands on it.
|
|
89
|
+
*
|
|
90
|
+
* One per table: the first visible one wins. It keeps its `size` as a floor
|
|
91
|
+
* — the table's `min-width` still counts it, so the row scrolls rather than
|
|
92
|
+
* squeezing the column to nothing — and it is not itself resizable, because
|
|
93
|
+
* a column with no width of its own has no edge to drag.
|
|
94
|
+
*/
|
|
95
|
+
fill?: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Full text for the `title` on every cell in this column, when the cell
|
|
98
|
+
* draws something other than its own value.
|
|
99
|
+
*
|
|
100
|
+
* Cells truncate and never wrap, so the full text has to be reachable on
|
|
101
|
+
* hover. A column that renders its value directly gets that for free. A
|
|
102
|
+
* column with its own `cell` renderer draws an arbitrary node, and the
|
|
103
|
+
* table will not guess a string for it — say what the hover text is here.
|
|
104
|
+
* Return `undefined` for no title.
|
|
105
|
+
*/
|
|
106
|
+
hoverText?: (row: Row) => string | undefined;
|
|
79
107
|
/** Enter or double-click opens an editor; Enter commits, Escape cancels. */
|
|
80
108
|
editable?: boolean | DataTableEditable;
|
|
81
109
|
/** Receives the committed value. Falls back to the table's `onCellCommit`. */
|
|
@@ -90,6 +118,14 @@ export interface DataTableColumnMeta<Row = unknown> {
|
|
|
90
118
|
cellClassName?: string;
|
|
91
119
|
/** Extra classes for the header cell. */
|
|
92
120
|
headerClassName?: string;
|
|
121
|
+
/**
|
|
122
|
+
* @internal Set by `useDataTable`, never by a consumer. Marks a column whose
|
|
123
|
+
* cell renders its own value, so the view can put the full text in a `title`
|
|
124
|
+
* without guessing a string out of a React node (DESIGN-77). It has to be
|
|
125
|
+
* recorded before TanStack merges its fallback cell renderer in, after which
|
|
126
|
+
* a column that declared no `cell` looks exactly like one that did.
|
|
127
|
+
*/
|
|
128
|
+
plainCell?: boolean;
|
|
93
129
|
}
|
|
94
130
|
export type DataTableColumnDef<Row extends RowData, Value = unknown> = ColumnDef<DataTableFeatures<Row>, Row, Value>;
|
|
95
131
|
export type DataTableColumn<Row extends RowData, Value = unknown> = Column<DataTableFeatures<Row>, Row, Value>;
|
|
@@ -166,7 +202,12 @@ export interface UseDataTableOptions<Row extends RowData> {
|
|
|
166
202
|
enableHiding?: boolean;
|
|
167
203
|
/** @default true */
|
|
168
204
|
enableColumnPinning?: boolean;
|
|
169
|
-
/**
|
|
205
|
+
/**
|
|
206
|
+
* Drag handles on the column edges. Defaults to `false`, or to `true` when
|
|
207
|
+
* a column is marked `meta.fill`: there the drag has somewhere to go, since
|
|
208
|
+
* the fill column absorbs whatever the dragged column takes or gives back.
|
|
209
|
+
* An explicit `false` still wins.
|
|
210
|
+
*/
|
|
170
211
|
enableColumnResizing?: boolean;
|
|
171
212
|
/** Starting sort, filters, visibility, pinning, sizing. */
|
|
172
213
|
initialState?: Partial<DataTableState<Row>>;
|
|
@@ -15,6 +15,31 @@ const AUTO_REFRESH_DEFAULT_MS = 6e4;
|
|
|
15
15
|
const AUTO_REFRESH_STORAGE_PREFIX = "cortena.data-table.auto-refresh.";
|
|
16
16
|
/** One warning per process, not one per render. */
|
|
17
17
|
const warned = { missingStateKey: false };
|
|
18
|
+
/** Column sets already complained about, so a re-render does not repeat it. */
|
|
19
|
+
const warnedUnsized = /* @__PURE__ */ new Set();
|
|
20
|
+
/**
|
|
21
|
+
* True where a developer is the one who would read a `console.warn`.
|
|
22
|
+
*
|
|
23
|
+
* Neither signal is universal, and the unguarded form of either one throws.
|
|
24
|
+
* `process` is a Node global: a bundler that replaces `process.env.NODE_ENV`
|
|
25
|
+
* leaves nothing behind, but a plain ESM consumer or a browser import map has
|
|
26
|
+
* no `process` at all, and the bare identifier is a ReferenceError that takes
|
|
27
|
+
* the table down with it. `import.meta.env` is Vite's, and is simply absent
|
|
28
|
+
* under webpack or in the published ESM as Node loads it. So: ask Vite, then
|
|
29
|
+
* ask Node, and when neither answers say nothing. A development warning is
|
|
30
|
+
* worth having only where someone can act on it, and a library that throws to
|
|
31
|
+
* deliver one is worse than a library that stays quiet.
|
|
32
|
+
*/
|
|
33
|
+
function isDevBuild() {
|
|
34
|
+
const viteEnv = import.meta.env;
|
|
35
|
+
if (viteEnv) return viteEnv.DEV === true;
|
|
36
|
+
if (typeof process === "undefined") return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** The id TanStack will give this column, for a message written before the table exists. */
|
|
40
|
+
function columnDefId(column) {
|
|
41
|
+
return column.id ?? (typeof column.accessorKey === "string" ? column.accessorKey : void 0) ?? (typeof column.header === "string" ? column.header : "(unnamed)");
|
|
42
|
+
}
|
|
18
43
|
function readStoredFlag(key) {
|
|
19
44
|
try {
|
|
20
45
|
return window.localStorage.getItem(key) === "on";
|
|
@@ -76,7 +101,7 @@ function useDebounced(value, ms) {
|
|
|
76
101
|
* whole instance to `<DataTable instance={...} />`.
|
|
77
102
|
*/
|
|
78
103
|
function useDataTable(options) {
|
|
79
|
-
const { columns, dataSource, getRowId, getSubRows, pagination: paginationMode = "pages", pageSize = 25, enableRowSelection = false, enableExpanding = false, getRowCanExpand, enableSorting = true, enableMultiSort = false, enableGlobalFilter = true, enableColumnFilters = true, enableHiding = true, enableColumnPinning = true, enableColumnResizing
|
|
104
|
+
const { columns, dataSource, getRowId, getSubRows, pagination: paginationMode = "pages", pageSize = 25, enableRowSelection = false, enableExpanding = false, getRowCanExpand, enableSorting = true, enableMultiSort = false, enableGlobalFilter = true, enableColumnFilters = true, enableHiding = true, enableColumnPinning = true, enableColumnResizing, initialState, onCellCommit, onRowSelectionChange, searchDebounceMs = 250, autoRefresh, stateKey } = options;
|
|
80
105
|
const isServer = dataSource.kind === "server";
|
|
81
106
|
const [globalFilter, setGlobalFilter] = React.useState(initialState?.globalFilter ?? "");
|
|
82
107
|
const [columnFilters, setColumnFilters] = React.useState(initialState?.columnFilters ?? []);
|
|
@@ -131,6 +156,16 @@ function useDataTable(options) {
|
|
|
131
156
|
active: autoActive
|
|
132
157
|
} : void 0);
|
|
133
158
|
const data = isServer ? server.rows : dataSource.rows;
|
|
159
|
+
const hasFillColumn = columns.some((c) => c.meta?.fill);
|
|
160
|
+
const resizing = enableColumnResizing ?? hasFillColumn;
|
|
161
|
+
const resizeMode = enableColumnResizing === void 0 ? "onEnd" : "onChange";
|
|
162
|
+
const unsizedIds = columns.filter((c) => c.size === void 0).map(columnDefId).join(", ");
|
|
163
|
+
React.useEffect(() => {
|
|
164
|
+
if (!isDevBuild()) return;
|
|
165
|
+
if (hasFillColumn || unsizedIds === "" || warnedUnsized.has(unsizedIds)) return;
|
|
166
|
+
warnedUnsized.add(unsizedIds);
|
|
167
|
+
console.warn(`[cortena-ui] DataTable: no column is marked \`meta: { fill: true }\`, and ${unsizedIds} declared no \`size\`, so ${unsizedIds.includes(",") ? "they sit" : "it sits"} at the 150px default and the table leaves empty space to the right. Mark the column people scan the list by with \`meta: { fill: true }\` and give the rest a \`size\`.`);
|
|
168
|
+
}, [hasFillColumn, unsizedIds]);
|
|
134
169
|
const resolvedColumns = React.useMemo(() => {
|
|
135
170
|
const helper = createColumnHelper$1();
|
|
136
171
|
const out = [];
|
|
@@ -138,15 +173,20 @@ function useDataTable(options) {
|
|
|
138
173
|
if (enableExpanding && !columns.some((c) => c.id === "expand")) out.push(expandColumn(helper));
|
|
139
174
|
for (const column of columns) {
|
|
140
175
|
const filter = column.meta?.filter;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
const defaults = {};
|
|
177
|
+
if (filter === "faceted" && column.filterFn === void 0) defaults.filterFn = "oneOf";
|
|
178
|
+
else if (filter === "text" && column.filterFn === void 0) defaults.filterFn = "includesString";
|
|
179
|
+
if (column.meta?.fill && column.enableResizing === void 0) defaults.enableResizing = false;
|
|
180
|
+
const plain = !("cell" in column) || column.cell == null;
|
|
181
|
+
const resolved = {
|
|
146
182
|
...column,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
...defaults,
|
|
184
|
+
...plain ? { meta: {
|
|
185
|
+
...column.meta,
|
|
186
|
+
plainCell: true
|
|
187
|
+
} } : null
|
|
188
|
+
};
|
|
189
|
+
out.push(resolved);
|
|
150
190
|
}
|
|
151
191
|
return out;
|
|
152
192
|
}, [
|
|
@@ -186,8 +226,8 @@ function useDataTable(options) {
|
|
|
186
226
|
enableColumnFilters,
|
|
187
227
|
enableHiding,
|
|
188
228
|
enableColumnPinning,
|
|
189
|
-
enableColumnResizing,
|
|
190
|
-
columnResizeMode:
|
|
229
|
+
enableColumnResizing: resizing,
|
|
230
|
+
columnResizeMode: resizeMode,
|
|
191
231
|
globalFilterFn: "search"
|
|
192
232
|
});
|
|
193
233
|
const onSelectionRef = React.useRef(onRowSelectionChange);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-data-table.js","names":["createColumnHelper"],"sources":["../../../src/components/data-table/use-data-table.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useTable,\n type ColumnFiltersState,\n type PaginationState,\n type RowData,\n type RowSelectionState,\n type SortingState,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { createColumnHelper, dataTableFeatures } from \"./features\";\nimport { expandColumn, selectColumn } from \"./system-columns\";\nimport type {\n DataSource,\n DataTableAutoRefreshState,\n DataTableColumnDef,\n DataTableEditingCell,\n DataTableInstance,\n DataTableRefreshPause,\n DataTableRow,\n DataTableTable,\n TableQuery,\n UseDataTableOptions,\n} from \"./types\";\nimport { useServerSource } from \"./use-server-source\";\n\nconst EMPTY_ROWS: never[] = [];\n\n/** The floor on `autoRefresh.intervalMs`; anything shorter is load, not freshness. */\nexport const AUTO_REFRESH_MIN_MS = 30_000;\n/** Used when `autoRefresh` names no interval. */\nexport const AUTO_REFRESH_DEFAULT_MS = 60_000;\n/** `cortena.data-table.auto-refresh.<stateKey>` holds `\"on\"` or `\"off\"`. */\nexport const AUTO_REFRESH_STORAGE_PREFIX = \"cortena.data-table.auto-refresh.\";\n\n/** One warning per process, not one per render. */\nconst warned = { missingStateKey: false };\n\nfunction readStoredFlag(key: string): boolean {\n try {\n return window.localStorage.getItem(key) === \"on\";\n } catch {\n return false;\n }\n}\n\n/**\n * The user's auto-refresh choice, remembered per table.\n *\n * It starts off and is read from storage in an effect rather than in the\n * initial state, so a server-rendered table and its first client render agree\n * — a switch that hydrates in the opposite position is a React mismatch and,\n * worse, a table that starts polling before the page is interactive.\n */\nfunction usePersistedFlag(storageKey: string | null): [boolean, (on: boolean) => void] {\n const [on, setOn] = React.useState(false);\n React.useEffect(() => {\n setOn(storageKey ? readStoredFlag(storageKey) : false);\n }, [storageKey]);\n const set = React.useCallback(\n (next: boolean) => {\n setOn(next);\n if (!storageKey) return;\n try {\n window.localStorage.setItem(storageKey, next ? \"on\" : \"off\");\n } catch {\n /* Private mode, or storage full: the switch still works for this visit. */\n }\n },\n [storageKey],\n );\n return [on, set];\n}\n\nconst subscribeVisibility = (onChange: () => void) => {\n document.addEventListener(\"visibilitychange\", onChange);\n return () => document.removeEventListener(\"visibilitychange\", onChange);\n};\n\n/** True while the tab is in the background. Polling a tab nobody is looking at is a battery bill. */\nfunction useDocumentHidden(): boolean {\n return React.useSyncExternalStore(\n subscribeVisibility,\n () => document.visibilityState === \"hidden\",\n () => false,\n );\n}\n\nfunction useDebounced<T>(value: T, ms: number): T {\n const [debounced, setDebounced] = React.useState(value);\n React.useEffect(() => {\n if (ms <= 0) {\n setDebounced(value);\n return;\n }\n const id = setTimeout(() => setDebounced(value), ms);\n return () => clearTimeout(id);\n }, [value, ms]);\n return ms <= 0 ? value : debounced;\n}\n\n/**\n * Builds the table for a `dataSource`. This is where the design decision's\n * \"the component sets the manual flags itself\" lives: a server source flips\n * `manualFiltering`, `manualSorting` and `manualPagination` and feeds the\n * table's own filter / sort / page state back out as a `TableQuery`; a client\n * source leaves every row model in memory. Everything else (selection,\n * expansion, visibility, pinning, sizing) is owned by the table.\n *\n * Use it directly when the default layout is not enough: pass the returned\n * `table` to `DataTableToolbar`, `DataTablePagination` and friends, or the\n * whole instance to `<DataTable instance={...} />`.\n */\nexport function useDataTable<Row extends RowData>(\n options: UseDataTableOptions<Row>,\n): DataTableInstance<Row> {\n const {\n columns,\n dataSource,\n getRowId,\n getSubRows,\n pagination: paginationMode = \"pages\",\n pageSize = 25,\n enableRowSelection = false,\n enableExpanding = false,\n getRowCanExpand,\n enableSorting = true,\n enableMultiSort = false,\n enableGlobalFilter = true,\n enableColumnFilters = true,\n enableHiding = true,\n enableColumnPinning = true,\n enableColumnResizing = false,\n initialState,\n onCellCommit,\n onRowSelectionChange,\n searchDebounceMs = 250,\n autoRefresh,\n stateKey,\n } = options;\n\n const isServer = dataSource.kind === \"server\";\n\n // Slices the server query is built from are React state; the table mirrors\n // them through `state` + `on*Change`, which v9 still supports alongside atoms.\n const [globalFilter, setGlobalFilter] = React.useState<string>(\n (initialState?.globalFilter as string | undefined) ?? \"\",\n );\n const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(\n initialState?.columnFilters ?? [],\n );\n const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);\n const [pagination, setPagination] = React.useState<PaginationState>(\n initialState?.pagination ?? { pageIndex: 0, pageSize },\n );\n // Selection and the open editor are lifted for the same reason the query\n // slices are: auto-refresh has to know about them before the query for this\n // render is declared, and `table.state` only exists after `useTable`.\n const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(\n initialState?.rowSelection ?? {},\n );\n const [editingCell, setEditingCell] = React.useState<DataTableEditingCell | null>(null);\n\n const searchQuery = useDebounced(globalFilter, isServer ? searchDebounceMs : 0);\n\n // A new search, filter or sort starts from page one again. Client sources\n // get this from `autoResetPageIndex`; manual pagination has to do it here.\n const scopeKey = JSON.stringify([searchQuery, columnFilters, sorting]);\n const lastScope = React.useRef(scopeKey);\n React.useEffect(() => {\n if (lastScope.current === scopeKey) return;\n lastScope.current = scopeKey;\n setPagination((p) => (p.pageIndex === 0 ? p : { ...p, pageIndex: 0 }));\n }, [scopeKey]);\n\n const query = React.useMemo<TableQuery>(\n () => ({\n // Trimmed for the same reason the client filter trims (features.ts): a\n // backend asked for `q: \" \"` returns nothing, and the user sees an empty\n // table with no explanation. Whitespace-only means no search term.\n q: searchQuery.trim() || undefined,\n filters: columnFilters.length ? columnFilters : undefined,\n sort: sorting.length ? sorting : undefined,\n page: pagination.pageIndex,\n pageSize: pagination.pageSize,\n }),\n [searchQuery, columnFilters, sorting, pagination.pageIndex, pagination.pageSize],\n );\n\n /* Freshness (DESIGN-D19 / P-39) -------------------------------------- */\n\n const wantsAuto = isServer && autoRefresh !== undefined;\n React.useEffect(() => {\n if (!wantsAuto || stateKey || warned.missingStateKey) return;\n warned.missingStateKey = true;\n console.warn(\n \"[cortena-ui] DataTable: `autoRefresh` needs a `stateKey`. Without one the \" +\n \"user's on/off choice cannot be remembered between visits, and two tables \" +\n \"on one screen would share the same setting.\",\n );\n }, [wantsAuto, stateKey]);\n const storageKey = wantsAuto && stateKey ? AUTO_REFRESH_STORAGE_PREFIX + stateKey : null;\n const [autoEnabled, setAutoEnabled] = usePersistedFlag(storageKey);\n const hidden = useDocumentHidden();\n const baseIntervalMs = Math.max(\n autoRefresh?.intervalMs ?? AUTO_REFRESH_DEFAULT_MS,\n AUTO_REFRESH_MIN_MS,\n );\n // The polite rules, in the order a user would explain them. Each one is a\n // reason the interval is not running, and the toolbar says which.\n const pausedBy: DataTableRefreshPause | null = hidden\n ? \"hidden\"\n : editingCell\n ? \"editing\"\n : Object.keys(rowSelection).length > 0\n ? \"selection\"\n : autoRefresh?.paused\n ? \"app\"\n : null;\n const autoActive = wantsAuto && autoEnabled && pausedBy === null;\n\n const server = useServerSource<Row>(\n isServer ? dataSource : undefined,\n query,\n paginationMode,\n wantsAuto ? { intervalMs: baseIntervalMs, active: autoActive } : undefined,\n );\n\n const data: ReadonlyArray<Row> = isServer ? server.rows : dataSource.rows;\n\n // System columns first; then each consumer column with its meta defaults\n // applied (a faceted column filters with `oneOf` unless told otherwise).\n const resolvedColumns = React.useMemo(() => {\n const helper = createColumnHelper<Row>();\n const out: DataTableColumnDef<Row, any>[] = [];\n if (enableRowSelection && !columns.some((c) => c.id === \"select\")) out.push(selectColumn(helper));\n if (enableExpanding && !columns.some((c) => c.id === \"expand\")) out.push(expandColumn(helper));\n for (const column of columns) {\n const filter = column.meta?.filter;\n if (filter === \"faceted\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"oneOf\" });\n } else if (filter === \"text\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"includesString\" });\n } else {\n out.push(column);\n }\n }\n return out;\n }, [columns, enableRowSelection, enableExpanding]);\n\n const table: DataTableTable<Row> = useTable({\n features: dataTableFeatures<Row>(),\n columns: resolvedColumns,\n data: data.length ? data : EMPTY_ROWS,\n getRowId,\n getSubRows,\n initialState,\n state: { globalFilter, columnFilters, sorting, pagination, rowSelection },\n onGlobalFilterChange: setGlobalFilter,\n onColumnFiltersChange: setColumnFilters,\n onSortingChange: setSorting,\n onPaginationChange: setPagination,\n onRowSelectionChange: setRowSelection,\n manualFiltering: isServer,\n manualSorting: isServer,\n manualPagination: isServer || paginationMode !== \"pages\",\n rowCount: isServer ? server.total : undefined,\n enableRowSelection,\n enableExpanding,\n getRowCanExpand: getRowCanExpand ?? (enableExpanding ? () => true : undefined),\n enableSorting,\n enableMultiSort,\n enableGlobalFilter,\n enableColumnFilters,\n enableHiding,\n enableColumnPinning,\n enableColumnResizing,\n columnResizeMode: \"onChange\",\n // `search`, not `includesString`: it trims the query, so a box holding only\n // whitespace filters nothing rather than emptying the grid. See features.ts.\n globalFilterFn: \"search\",\n });\n\n // Selection callback with original rows, for consumers that keep selection outside.\n const onSelectionRef = React.useRef(onRowSelectionChange);\n onSelectionRef.current = onRowSelectionChange;\n const firstSelection = React.useRef(true);\n React.useEffect(() => {\n if (firstSelection.current) {\n firstSelection.current = false;\n return;\n }\n onSelectionRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rowSelection]);\n\n const commit = React.useCallback(\n async (row: DataTableRow<Row>, columnId: string, value: unknown) => {\n const meta = table.getColumn(columnId)?.columnDef.meta;\n const handler = meta?.onCommit ?? onCellCommit;\n await handler?.(row.original, columnId, value);\n },\n [table, onCellCommit],\n );\n const editing = React.useMemo(\n () => ({\n cell: editingCell,\n start: (cell: DataTableEditingCell) => setEditingCell(cell),\n stop: () => setEditingCell(null),\n commit,\n }),\n [editingCell, commit],\n );\n\n // The stamp keeps the last real response time: with keepPreviousData a page\n // change puts a placeholder on screen whose `dataUpdatedAt` is zero, and the\n // stamp must not blink out while the next page loads.\n const lastUpdatedRef = React.useRef<number | null>(null);\n if (server.lastUpdatedAt && server.lastUpdatedAt > (lastUpdatedRef.current ?? 0)) {\n lastUpdatedRef.current = server.lastUpdatedAt;\n }\n const lastUpdatedAt = lastUpdatedRef.current;\n\n const autoState = React.useMemo<DataTableAutoRefreshState | null>(\n () =>\n wantsAuto\n ? {\n enabled: autoEnabled,\n setEnabled: setAutoEnabled,\n active: autoActive && !server.mutating,\n // A mutation in flight is the one pause only the query layer can\n // see; it is folded in here so the toolbar can still say why.\n pausedBy: autoEnabled ? (pausedBy ?? (server.mutating ? \"mutation\" : null)) : null,\n intervalMs: server.intervalMs === false ? baseIntervalMs : server.intervalMs,\n baseIntervalMs,\n backingOff: server.failures > 0,\n }\n : null,\n [\n wantsAuto,\n autoEnabled,\n setAutoEnabled,\n autoActive,\n pausedBy,\n server.mutating,\n server.intervalMs,\n server.failures,\n baseIntervalMs,\n ],\n );\n\n const total = isServer\n ? (server.total ?? 0)\n : paginationMode === \"pages\"\n ? table.getPrePaginatedRowModel().rows.length\n : table.getRowModel().rows.length;\n\n return {\n table,\n dataSource: dataSource as DataSource<Row>,\n paginationMode,\n rows: data as Row[],\n total,\n isLoading: server.isLoading,\n isFetching: server.isFetching,\n error: server.error,\n refetch: server.refetch,\n fetchNextPage: server.fetchNextPage,\n hasNextPage: server.hasNextPage,\n searchQuery,\n lastUpdatedAt,\n lastErrorAt: server.lastErrorAt,\n autoRefresh: autoState,\n editing,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-data-table.js","names":["createColumnHelper"],"sources":["../../../src/components/data-table/use-data-table.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useTable,\n type ColumnFiltersState,\n type PaginationState,\n type RowData,\n type RowSelectionState,\n type SortingState,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { createColumnHelper, dataTableFeatures } from \"./features\";\nimport { expandColumn, selectColumn } from \"./system-columns\";\nimport type {\n DataSource,\n DataTableAutoRefreshState,\n DataTableColumnDef,\n DataTableEditingCell,\n DataTableInstance,\n DataTableRefreshPause,\n DataTableRow,\n DataTableTable,\n TableQuery,\n UseDataTableOptions,\n} from \"./types\";\nimport { useServerSource } from \"./use-server-source\";\n\nconst EMPTY_ROWS: never[] = [];\n\n/** The floor on `autoRefresh.intervalMs`; anything shorter is load, not freshness. */\nexport const AUTO_REFRESH_MIN_MS = 30_000;\n/** Used when `autoRefresh` names no interval. */\nexport const AUTO_REFRESH_DEFAULT_MS = 60_000;\n/** `cortena.data-table.auto-refresh.<stateKey>` holds `\"on\"` or `\"off\"`. */\nexport const AUTO_REFRESH_STORAGE_PREFIX = \"cortena.data-table.auto-refresh.\";\n\n/** One warning per process, not one per render. */\nconst warned = { missingStateKey: false };\n/** Column sets already complained about, so a re-render does not repeat it. */\nconst warnedUnsized = new Set<string>();\n\n/**\n * True where a developer is the one who would read a `console.warn`.\n *\n * Neither signal is universal, and the unguarded form of either one throws.\n * `process` is a Node global: a bundler that replaces `process.env.NODE_ENV`\n * leaves nothing behind, but a plain ESM consumer or a browser import map has\n * no `process` at all, and the bare identifier is a ReferenceError that takes\n * the table down with it. `import.meta.env` is Vite's, and is simply absent\n * under webpack or in the published ESM as Node loads it. So: ask Vite, then\n * ask Node, and when neither answers say nothing. A development warning is\n * worth having only where someone can act on it, and a library that throws to\n * deliver one is worse than a library that stays quiet.\n */\nfunction isDevBuild(): boolean {\n const viteEnv = (import.meta as unknown as { env?: { DEV?: boolean } }).env;\n if (viteEnv) return viteEnv.DEV === true;\n if (typeof process === \"undefined\") return false;\n return process.env?.NODE_ENV !== \"production\";\n}\n\n/**\n * Forgets which column sets have already warned.\n *\n * @internal Test hook. The warning is once per process, which makes every test\n * after the first one depend on using column ids no earlier test used. Call\n * this in a `beforeEach` instead.\n */\nexport function resetUnsizedWarnings(): void {\n warnedUnsized.clear();\n}\n\n/** The id TanStack will give this column, for a message written before the table exists. */\nfunction columnDefId(column: { id?: string; accessorKey?: unknown; header?: unknown }): string {\n return column.id ?? (typeof column.accessorKey === \"string\" ? column.accessorKey : undefined) ??\n (typeof column.header === \"string\" ? column.header : \"(unnamed)\");\n}\n\nfunction readStoredFlag(key: string): boolean {\n try {\n return window.localStorage.getItem(key) === \"on\";\n } catch {\n return false;\n }\n}\n\n/**\n * The user's auto-refresh choice, remembered per table.\n *\n * It starts off and is read from storage in an effect rather than in the\n * initial state, so a server-rendered table and its first client render agree\n * — a switch that hydrates in the opposite position is a React mismatch and,\n * worse, a table that starts polling before the page is interactive.\n */\nfunction usePersistedFlag(storageKey: string | null): [boolean, (on: boolean) => void] {\n const [on, setOn] = React.useState(false);\n React.useEffect(() => {\n setOn(storageKey ? readStoredFlag(storageKey) : false);\n }, [storageKey]);\n const set = React.useCallback(\n (next: boolean) => {\n setOn(next);\n if (!storageKey) return;\n try {\n window.localStorage.setItem(storageKey, next ? \"on\" : \"off\");\n } catch {\n /* Private mode, or storage full: the switch still works for this visit. */\n }\n },\n [storageKey],\n );\n return [on, set];\n}\n\nconst subscribeVisibility = (onChange: () => void) => {\n document.addEventListener(\"visibilitychange\", onChange);\n return () => document.removeEventListener(\"visibilitychange\", onChange);\n};\n\n/** True while the tab is in the background. Polling a tab nobody is looking at is a battery bill. */\nfunction useDocumentHidden(): boolean {\n return React.useSyncExternalStore(\n subscribeVisibility,\n () => document.visibilityState === \"hidden\",\n () => false,\n );\n}\n\nfunction useDebounced<T>(value: T, ms: number): T {\n const [debounced, setDebounced] = React.useState(value);\n React.useEffect(() => {\n if (ms <= 0) {\n setDebounced(value);\n return;\n }\n const id = setTimeout(() => setDebounced(value), ms);\n return () => clearTimeout(id);\n }, [value, ms]);\n return ms <= 0 ? value : debounced;\n}\n\n/**\n * Builds the table for a `dataSource`. This is where the design decision's\n * \"the component sets the manual flags itself\" lives: a server source flips\n * `manualFiltering`, `manualSorting` and `manualPagination` and feeds the\n * table's own filter / sort / page state back out as a `TableQuery`; a client\n * source leaves every row model in memory. Everything else (selection,\n * expansion, visibility, pinning, sizing) is owned by the table.\n *\n * Use it directly when the default layout is not enough: pass the returned\n * `table` to `DataTableToolbar`, `DataTablePagination` and friends, or the\n * whole instance to `<DataTable instance={...} />`.\n */\nexport function useDataTable<Row extends RowData>(\n options: UseDataTableOptions<Row>,\n): DataTableInstance<Row> {\n const {\n columns,\n dataSource,\n getRowId,\n getSubRows,\n pagination: paginationMode = \"pages\",\n pageSize = 25,\n enableRowSelection = false,\n enableExpanding = false,\n getRowCanExpand,\n enableSorting = true,\n enableMultiSort = false,\n enableGlobalFilter = true,\n enableColumnFilters = true,\n enableHiding = true,\n enableColumnPinning = true,\n enableColumnResizing,\n initialState,\n onCellCommit,\n onRowSelectionChange,\n searchDebounceMs = 250,\n autoRefresh,\n stateKey,\n } = options;\n\n const isServer = dataSource.kind === \"server\";\n\n // Slices the server query is built from are React state; the table mirrors\n // them through `state` + `on*Change`, which v9 still supports alongside atoms.\n const [globalFilter, setGlobalFilter] = React.useState<string>(\n (initialState?.globalFilter as string | undefined) ?? \"\",\n );\n const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(\n initialState?.columnFilters ?? [],\n );\n const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);\n const [pagination, setPagination] = React.useState<PaginationState>(\n initialState?.pagination ?? { pageIndex: 0, pageSize },\n );\n // Selection and the open editor are lifted for the same reason the query\n // slices are: auto-refresh has to know about them before the query for this\n // render is declared, and `table.state` only exists after `useTable`.\n const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(\n initialState?.rowSelection ?? {},\n );\n const [editingCell, setEditingCell] = React.useState<DataTableEditingCell | null>(null);\n\n const searchQuery = useDebounced(globalFilter, isServer ? searchDebounceMs : 0);\n\n // A new search, filter or sort starts from page one again. Client sources\n // get this from `autoResetPageIndex`; manual pagination has to do it here.\n const scopeKey = JSON.stringify([searchQuery, columnFilters, sorting]);\n const lastScope = React.useRef(scopeKey);\n React.useEffect(() => {\n if (lastScope.current === scopeKey) return;\n lastScope.current = scopeKey;\n setPagination((p) => (p.pageIndex === 0 ? p : { ...p, pageIndex: 0 }));\n }, [scopeKey]);\n\n const query = React.useMemo<TableQuery>(\n () => ({\n // Trimmed for the same reason the client filter trims (features.ts): a\n // backend asked for `q: \" \"` returns nothing, and the user sees an empty\n // table with no explanation. Whitespace-only means no search term.\n q: searchQuery.trim() || undefined,\n filters: columnFilters.length ? columnFilters : undefined,\n sort: sorting.length ? sorting : undefined,\n page: pagination.pageIndex,\n pageSize: pagination.pageSize,\n }),\n [searchQuery, columnFilters, sorting, pagination.pageIndex, pagination.pageSize],\n );\n\n /* Freshness (DESIGN-D19 / P-39) -------------------------------------- */\n\n const wantsAuto = isServer && autoRefresh !== undefined;\n React.useEffect(() => {\n if (!wantsAuto || stateKey || warned.missingStateKey) return;\n warned.missingStateKey = true;\n console.warn(\n \"[cortena-ui] DataTable: `autoRefresh` needs a `stateKey`. Without one the \" +\n \"user's on/off choice cannot be remembered between visits, and two tables \" +\n \"on one screen would share the same setting.\",\n );\n }, [wantsAuto, stateKey]);\n const storageKey = wantsAuto && stateKey ? AUTO_REFRESH_STORAGE_PREFIX + stateKey : null;\n const [autoEnabled, setAutoEnabled] = usePersistedFlag(storageKey);\n const hidden = useDocumentHidden();\n const baseIntervalMs = Math.max(\n autoRefresh?.intervalMs ?? AUTO_REFRESH_DEFAULT_MS,\n AUTO_REFRESH_MIN_MS,\n );\n // The polite rules, in the order a user would explain them. Each one is a\n // reason the interval is not running, and the toolbar says which.\n const pausedBy: DataTableRefreshPause | null = hidden\n ? \"hidden\"\n : editingCell\n ? \"editing\"\n : Object.keys(rowSelection).length > 0\n ? \"selection\"\n : autoRefresh?.paused\n ? \"app\"\n : null;\n const autoActive = wantsAuto && autoEnabled && pausedBy === null;\n\n const server = useServerSource<Row>(\n isServer ? dataSource : undefined,\n query,\n paginationMode,\n wantsAuto ? { intervalMs: baseIntervalMs, active: autoActive } : undefined,\n );\n\n const data: ReadonlyArray<Row> = isServer ? server.rows : dataSource.rows;\n\n /* Column widths (DESIGN-77) ------------------------------------------ */\n\n // A fill column is the one column drawn with no fixed width, so a grid that\n // is `table-fixed` end to end still has somewhere to put the space left\n // over. It also turns resizing on, because the drag now has somewhere to go.\n const hasFillColumn = columns.some((c) => c.meta?.fill);\n const resizing = enableColumnResizing ?? hasFillColumn;\n // A consumer who asked for resizing gets the live drag. A table that only\n // has it because of the fill column commits on release: every move in\n // `onChange` re-renders every row, and a table nobody asked to be resizable\n // should not pay that for a drag it did not advertise.\n const resizeMode = enableColumnResizing === undefined ? \"onEnd\" : \"onChange\";\n\n // A column with no `size` sits at TanStack's 150 px default. That is fine\n // for a status chip and wrong for the title people scan the list by, and\n // nothing on screen says which happened — the text is simply clipped.\n const unsizedIds = columns.filter((c) => c.size === undefined).map(columnDefId).join(\", \");\n React.useEffect(() => {\n if (!isDevBuild()) return;\n if (hasFillColumn || unsizedIds === \"\" || warnedUnsized.has(unsizedIds)) return;\n warnedUnsized.add(unsizedIds);\n console.warn(\n `[cortena-ui] DataTable: no column is marked \\`meta: { fill: true }\\`, and ` +\n `${unsizedIds} declared no \\`size\\`, so ${unsizedIds.includes(\",\") ? \"they sit\" : \"it sits\"} ` +\n \"at the 150px default and the table leaves empty space to the right. Mark the \" +\n \"column people scan the list by with `meta: { fill: true }` and give the rest a `size`.\",\n );\n }, [hasFillColumn, unsizedIds]);\n\n // System columns first; then each consumer column with its meta defaults\n // applied (a faceted column filters with `oneOf` unless told otherwise).\n const resolvedColumns = React.useMemo(() => {\n const helper = createColumnHelper<Row>();\n const out: DataTableColumnDef<Row, any>[] = [];\n if (enableRowSelection && !columns.some((c) => c.id === \"select\")) out.push(selectColumn(helper));\n if (enableExpanding && !columns.some((c) => c.id === \"expand\")) out.push(expandColumn(helper));\n for (const column of columns) {\n const filter = column.meta?.filter;\n const defaults: Record<string, unknown> = {};\n if (filter === \"faceted\" && column.filterFn === undefined) defaults.filterFn = \"oneOf\";\n else if (filter === \"text\" && column.filterFn === undefined) defaults.filterFn = \"includesString\";\n // The fill column has no width of its own, so there is no edge to drag.\n if (column.meta?.fill && column.enableResizing === undefined) defaults.enableResizing = false;\n // Recorded before TanStack merges its fallback renderer in: by the time\n // the view is handed a `columnDef`, the default cell renderer is there\n // and a column that declared no `cell` is indistinguishable from one\n // that did. It goes in `meta`, which TanStack carries verbatim, rather\n // than on a symbol key the table is under no obligation to copy.\n const plain = !(\"cell\" in column) || column.cell == null;\n const resolved = {\n ...column,\n ...defaults,\n ...(plain ? { meta: { ...column.meta, plainCell: true } } : null),\n };\n out.push(resolved as DataTableColumnDef<Row, any>);\n }\n return out;\n }, [columns, enableRowSelection, enableExpanding]);\n\n const table: DataTableTable<Row> = useTable({\n features: dataTableFeatures<Row>(),\n columns: resolvedColumns,\n data: data.length ? data : EMPTY_ROWS,\n getRowId,\n getSubRows,\n initialState,\n state: { globalFilter, columnFilters, sorting, pagination, rowSelection },\n onGlobalFilterChange: setGlobalFilter,\n onColumnFiltersChange: setColumnFilters,\n onSortingChange: setSorting,\n onPaginationChange: setPagination,\n onRowSelectionChange: setRowSelection,\n manualFiltering: isServer,\n manualSorting: isServer,\n manualPagination: isServer || paginationMode !== \"pages\",\n rowCount: isServer ? server.total : undefined,\n enableRowSelection,\n enableExpanding,\n getRowCanExpand: getRowCanExpand ?? (enableExpanding ? () => true : undefined),\n enableSorting,\n enableMultiSort,\n enableGlobalFilter,\n enableColumnFilters,\n enableHiding,\n enableColumnPinning,\n enableColumnResizing: resizing,\n columnResizeMode: resizeMode,\n // `search`, not `includesString`: it trims the query, so a box holding only\n // whitespace filters nothing rather than emptying the grid. See features.ts.\n globalFilterFn: \"search\",\n });\n\n // Selection callback with original rows, for consumers that keep selection outside.\n const onSelectionRef = React.useRef(onRowSelectionChange);\n onSelectionRef.current = onRowSelectionChange;\n const firstSelection = React.useRef(true);\n React.useEffect(() => {\n if (firstSelection.current) {\n firstSelection.current = false;\n return;\n }\n onSelectionRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rowSelection]);\n\n const commit = React.useCallback(\n async (row: DataTableRow<Row>, columnId: string, value: unknown) => {\n const meta = table.getColumn(columnId)?.columnDef.meta;\n const handler = meta?.onCommit ?? onCellCommit;\n await handler?.(row.original, columnId, value);\n },\n [table, onCellCommit],\n );\n const editing = React.useMemo(\n () => ({\n cell: editingCell,\n start: (cell: DataTableEditingCell) => setEditingCell(cell),\n stop: () => setEditingCell(null),\n commit,\n }),\n [editingCell, commit],\n );\n\n // The stamp keeps the last real response time: with keepPreviousData a page\n // change puts a placeholder on screen whose `dataUpdatedAt` is zero, and the\n // stamp must not blink out while the next page loads.\n const lastUpdatedRef = React.useRef<number | null>(null);\n if (server.lastUpdatedAt && server.lastUpdatedAt > (lastUpdatedRef.current ?? 0)) {\n lastUpdatedRef.current = server.lastUpdatedAt;\n }\n const lastUpdatedAt = lastUpdatedRef.current;\n\n const autoState = React.useMemo<DataTableAutoRefreshState | null>(\n () =>\n wantsAuto\n ? {\n enabled: autoEnabled,\n setEnabled: setAutoEnabled,\n active: autoActive && !server.mutating,\n // A mutation in flight is the one pause only the query layer can\n // see; it is folded in here so the toolbar can still say why.\n pausedBy: autoEnabled ? (pausedBy ?? (server.mutating ? \"mutation\" : null)) : null,\n intervalMs: server.intervalMs === false ? baseIntervalMs : server.intervalMs,\n baseIntervalMs,\n backingOff: server.failures > 0,\n }\n : null,\n [\n wantsAuto,\n autoEnabled,\n setAutoEnabled,\n autoActive,\n pausedBy,\n server.mutating,\n server.intervalMs,\n server.failures,\n baseIntervalMs,\n ],\n );\n\n const total = isServer\n ? (server.total ?? 0)\n : paginationMode === \"pages\"\n ? table.getPrePaginatedRowModel().rows.length\n : table.getRowModel().rows.length;\n\n return {\n table,\n dataSource: dataSource as DataSource<Row>,\n paginationMode,\n rows: data as Row[],\n total,\n isLoading: server.isLoading,\n isFetching: server.isFetching,\n error: server.error,\n refetch: server.refetch,\n fetchNextPage: server.fetchNextPage,\n hasNextPage: server.hasNextPage,\n searchQuery,\n lastUpdatedAt,\n lastErrorAt: server.lastErrorAt,\n autoRefresh: autoState,\n editing,\n };\n}\n"],"mappings":"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"}
|
|
@@ -21,7 +21,7 @@ import { Toast } from "@base-ui/react/toast";
|
|
|
21
21
|
* `toastManager` to `Toaster` when you need to fire toasts outside React.
|
|
22
22
|
*/
|
|
23
23
|
declare const toastVariants: (props?: ({
|
|
24
|
-
variant?: "default" | "success" | "
|
|
24
|
+
variant?: "default" | "success" | "destructive" | "warning" | null | undefined;
|
|
25
25
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
26
26
|
/** The Base UI toast manager: `{ toasts, add, close, update, promise }`. */
|
|
27
27
|
declare const useToast: typeof Toast.useToastManager;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortena-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Cortena UI \u2014 the shared React component library for cortenaweb and every Cortena extension. shadcn over Base UI, styled entirely from cortena-design tokens.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"repository": {
|
|
@@ -203,6 +203,33 @@ function pinAttrs<Row extends RowData>(
|
|
|
203
203
|
|
|
204
204
|
const alignClass = { start: "text-left", center: "text-center", end: "text-right" } as const;
|
|
205
205
|
|
|
206
|
+
/* ------------------------------------------------------------------------ */
|
|
207
|
+
/* Column widths and hover text (DESIGN-77) */
|
|
208
|
+
/* ------------------------------------------------------------------------ */
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Full text for a cell's `title`, or undefined for no title.
|
|
212
|
+
*
|
|
213
|
+
* Cells truncate and never wrap, so the only way back to the full text is the
|
|
214
|
+
* pointer. A column that renders its own value can be read straight off the
|
|
215
|
+
* value; a column with a `cell` renderer draws a node the table cannot turn
|
|
216
|
+
* into a sentence, and guessing produces titles like "[object Object]" on a
|
|
217
|
+
* row of buttons. Such a column says what its hover text is with
|
|
218
|
+
* `meta.hoverText`, or has none.
|
|
219
|
+
*/
|
|
220
|
+
function hoverTextFor<Row extends RowData>(cell: DataTableCell<Row, any>): string | undefined {
|
|
221
|
+
const def = cell.column.columnDef;
|
|
222
|
+
const meta = def.meta;
|
|
223
|
+
if (meta?.hoverText) return meta.hoverText(cell.row.original) || undefined;
|
|
224
|
+
if (!meta?.plainCell) return undefined;
|
|
225
|
+
const value = cell.getValue();
|
|
226
|
+
// `|| undefined` on the way out too: an empty string is a `title=""`, which
|
|
227
|
+
// is a pointer that stops on an empty tooltip rather than no tooltip.
|
|
228
|
+
return typeof value === "string" || typeof value === "number"
|
|
229
|
+
? String(value) || undefined
|
|
230
|
+
: undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
206
233
|
/* ------------------------------------------------------------------------ */
|
|
207
234
|
/* Editable cell */
|
|
208
235
|
/* ------------------------------------------------------------------------ */
|
|
@@ -297,6 +324,19 @@ function DataTableView<Row extends RowData>({
|
|
|
297
324
|
const { table, paginationMode } = instance;
|
|
298
325
|
const rows = table.getRowModel().rows;
|
|
299
326
|
const leafColumns = table.getVisibleLeafColumns();
|
|
327
|
+
// The one column drawn with no fixed width, so the space the sized columns
|
|
328
|
+
// leave lands on it instead of on the filler cell (DESIGN-77). First visible
|
|
329
|
+
// one wins; hide it and the table falls back to the old filler behaviour.
|
|
330
|
+
// A pinned column is skipped: pinning positions it with a `left`/`right`
|
|
331
|
+
// offset read off the widths of the columns beside it, and a column with no
|
|
332
|
+
// width of its own would leave every pinned neighbour at the wrong offset.
|
|
333
|
+
const fillColumnId = leafColumns.find(
|
|
334
|
+
(column) => column.columnDef.meta?.fill && !column.getIsPinned(),
|
|
335
|
+
)?.id;
|
|
336
|
+
const widthFor = (column: DataTableColumn<Row, any>, size: number) =>
|
|
337
|
+
column.id === fillColumnId ? undefined : size;
|
|
338
|
+
// A width the fill column would otherwise have to share the remainder with.
|
|
339
|
+
const fillerStyle = fillColumnId ? ({ width: 0 } as const) : undefined;
|
|
300
340
|
const colCount = leafColumns.length;
|
|
301
341
|
const headerGroups = table.getHeaderGroups();
|
|
302
342
|
const isVirtual = virtualize ?? rows.length > VIRTUALIZE_ABOVE;
|
|
@@ -519,7 +559,8 @@ function DataTableView<Row extends RowData>({
|
|
|
519
559
|
data-editing={isEditing ? "" : undefined}
|
|
520
560
|
aria-colindex={c + 1}
|
|
521
561
|
tabIndex={tabIndexFor(index, c)}
|
|
522
|
-
|
|
562
|
+
title={isEditing ? undefined : hoverTextFor(cell)}
|
|
563
|
+
style={{ width: widthFor(column, column.getSize()), ...pin.style }}
|
|
523
564
|
onDoubleClick={
|
|
524
565
|
canEdit && !isEditing
|
|
525
566
|
? () => instance.editing.start({ rowId: row.id, columnId: column.id })
|
|
@@ -579,7 +620,7 @@ function DataTableView<Row extends RowData>({
|
|
|
579
620
|
)}
|
|
580
621
|
>
|
|
581
622
|
{cells.slice(0, splitAt).map((cell, c) => renderCell(cell, c))}
|
|
582
|
-
<td aria-hidden data-slot="data-table-filler" className="p-0" />
|
|
623
|
+
<td aria-hidden data-slot="data-table-filler" style={fillerStyle} className="p-0" />
|
|
583
624
|
{cells.slice(splitAt).map((cell, i) => renderCell(cell, splitAt + i))}
|
|
584
625
|
</tr>
|
|
585
626
|
{expanded ? (
|
|
@@ -600,11 +641,11 @@ function DataTableView<Row extends RowData>({
|
|
|
600
641
|
const skeletonRows = Array.from({ length: Math.min(8, table.state.pagination.pageSize) }, (_, i) => (
|
|
601
642
|
<tr key={`skeleton-${i}`} aria-hidden data-slot="data-table-skeleton-row" className="border-b border-[var(--ds-border-subtle)]">
|
|
602
643
|
{leafColumns.map((column) => (
|
|
603
|
-
<td key={column.id} className={cellPadding} style={{ width: column.getSize() }}>
|
|
644
|
+
<td key={column.id} className={cellPadding} style={{ width: widthFor(column, column.getSize()) }}>
|
|
604
645
|
<Skeleton className="h-3.5 w-3/4" />
|
|
605
646
|
</td>
|
|
606
647
|
))}
|
|
607
|
-
<td className="p-0" />
|
|
648
|
+
<td style={fillerStyle} className="p-0" />
|
|
608
649
|
</tr>
|
|
609
650
|
));
|
|
610
651
|
|
|
@@ -685,7 +726,17 @@ function DataTableView<Row extends RowData>({
|
|
|
685
726
|
}
|
|
686
727
|
colSpan={header.colSpan}
|
|
687
728
|
tabIndex={tabIndexFor(-1, c)}
|
|
688
|
-
|
|
729
|
+
// Only where the column says so. A header is one short
|
|
730
|
+
// phrase the consumer chose, so repeating it in a tooltip
|
|
731
|
+
// on every `<th>` is noise, and it was wrong outright on
|
|
732
|
+
// the select and expand columns, whose header is a node
|
|
733
|
+
// with no sentence in it. `meta.label` is the column
|
|
734
|
+
// saying what it is called; when it is set, `title` on the
|
|
735
|
+
// `<th>` reaches the button inside it too, since a pointer
|
|
736
|
+
// over a node with no title of its own takes its
|
|
737
|
+
// ancestor's. Body cells keep their own hover text.
|
|
738
|
+
title={meta?.label}
|
|
739
|
+
style={{ width: widthFor(column, header.getSize()), ...pin.style }}
|
|
689
740
|
className={cn(
|
|
690
741
|
"group/head relative h-10 align-middle font-medium outline-none",
|
|
691
742
|
headPadding,
|
|
@@ -724,7 +775,12 @@ function DataTableView<Row extends RowData>({
|
|
|
724
775
|
return (
|
|
725
776
|
<tr key={group.id} role="row" aria-rowindex={1}>
|
|
726
777
|
{group.headers.slice(0, splitAt).map((header, c) => renderHead(header, c))}
|
|
727
|
-
<th
|
|
778
|
+
<th
|
|
779
|
+
aria-hidden
|
|
780
|
+
data-slot="data-table-filler-head"
|
|
781
|
+
style={fillerStyle}
|
|
782
|
+
className="border-b border-[var(--ds-border)] bg-[var(--ds-muted)] p-0"
|
|
783
|
+
/>
|
|
728
784
|
{group.headers.slice(splitAt).map((header, i) => renderHead(header, splitAt + i))}
|
|
729
785
|
</tr>
|
|
730
786
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { ColumnHelper, RowData } from "@tanstack/react-table";
|
|
4
|
+
import type * as React from "react";
|
|
4
5
|
import { ChevronRight } from "lucide-react";
|
|
5
6
|
import { Checkbox } from "@/components/checkbox";
|
|
6
7
|
import { cn } from "@/lib/cn";
|
|
@@ -12,7 +13,23 @@ import type { DataTableColumnDef } from "./types";
|
|
|
12
13
|
* when `enableRowSelection` is set, the expander when a `renderSubComponent`
|
|
13
14
|
* is given. Both are display columns, neither sorts, hides, resizes or
|
|
14
15
|
* exports, and a consumer column with the same id replaces them.
|
|
16
|
+
*
|
|
17
|
+
* Both controls stop the event from reaching the row (DESIGN-79). A table
|
|
18
|
+
* with `onRowClick` puts a navigation on the whole row, and without this the
|
|
19
|
+
* chevron opens the detail panel and navigates away from it in the same
|
|
20
|
+
* gesture — and the checkbox selects a row and then leaves the page. It has
|
|
21
|
+
* to be here: every consumer of these columns would otherwise need the same
|
|
22
|
+
* guard, and the one that forgets has a control that cannot be used.
|
|
15
23
|
*/
|
|
24
|
+
const stopRowActivation = {
|
|
25
|
+
onClick: (event: React.MouseEvent) => event.stopPropagation(),
|
|
26
|
+
// A button fires `click` for Enter and Space, so `onClick` covers the
|
|
27
|
+
// keyboard too; this stops the grid's own key handling from seeing the key
|
|
28
|
+
// before the control does.
|
|
29
|
+
onKeyDown: (event: React.KeyboardEvent) => {
|
|
30
|
+
if (event.key === "Enter" || event.key === " ") event.stopPropagation();
|
|
31
|
+
},
|
|
32
|
+
} as const;
|
|
16
33
|
export function selectColumn<Row extends RowData>(
|
|
17
34
|
helper: ColumnHelper<DataTableFeatures<Row>, Row>,
|
|
18
35
|
): DataTableColumnDef<Row, unknown> {
|
|
@@ -27,6 +44,7 @@ export function selectColumn<Row extends RowData>(
|
|
|
27
44
|
checked={all}
|
|
28
45
|
indeterminate={some}
|
|
29
46
|
onCheckedChange={(checked) => table.toggleAllPageRowsSelected(Boolean(checked))}
|
|
47
|
+
{...stopRowActivation}
|
|
30
48
|
/>
|
|
31
49
|
);
|
|
32
50
|
},
|
|
@@ -36,6 +54,7 @@ export function selectColumn<Row extends RowData>(
|
|
|
36
54
|
checked={row.getIsSelected()}
|
|
37
55
|
disabled={!row.getCanSelect()}
|
|
38
56
|
onCheckedChange={(checked) => row.toggleSelected(Boolean(checked))}
|
|
57
|
+
{...stopRowActivation}
|
|
39
58
|
/>
|
|
40
59
|
),
|
|
41
60
|
size: 40,
|
|
@@ -63,7 +82,11 @@ export function expandColumn<Row extends RowData>(
|
|
|
63
82
|
data-slot="data-table-expander"
|
|
64
83
|
aria-label={row.getIsExpanded() ? "Collapse row" : "Expand row"}
|
|
65
84
|
aria-expanded={row.getIsExpanded()}
|
|
66
|
-
onClick={
|
|
85
|
+
onClick={(event) => {
|
|
86
|
+
event.stopPropagation();
|
|
87
|
+
row.getToggleExpandedHandler()();
|
|
88
|
+
}}
|
|
89
|
+
onKeyDown={stopRowActivation.onKeyDown}
|
|
67
90
|
className={cn(
|
|
68
91
|
"inline-flex size-6 items-center justify-center rounded-[var(--ds-radius-sm)]",
|
|
69
92
|
"text-[color:var(--ds-muted-foreground)] hover:bg-[var(--ds-hover)] hover:text-[color:var(--ds-foreground)]",
|
|
@@ -71,6 +71,34 @@ export interface DataTableColumnMeta<Row = unknown> {
|
|
|
71
71
|
filterOptions?: Array<{ value: string | number | boolean; label?: string; icon?: React.ComponentType<{ className?: string }> }>;
|
|
72
72
|
/** Text alignment of the header and cells. Numbers usually want `end`. */
|
|
73
73
|
align?: "start" | "center" | "end";
|
|
74
|
+
/**
|
|
75
|
+
* This column takes whatever width the sized columns leave (DESIGN-77).
|
|
76
|
+
*
|
|
77
|
+
* The grid is `table-fixed`: every column is drawn at its `size`, and a
|
|
78
|
+
* column that never declared one sits at TanStack's 150 px default. On a
|
|
79
|
+
* list that is one wide column and a few narrow ones — a title beside an
|
|
80
|
+
* id, a status and an assignee — that means the column people actually read
|
|
81
|
+
* is clipped while the table shows empty space to its right. Mark that
|
|
82
|
+
* column `fill` and it is the one column drawn with no fixed width, so the
|
|
83
|
+
* remainder lands on it.
|
|
84
|
+
*
|
|
85
|
+
* One per table: the first visible one wins. It keeps its `size` as a floor
|
|
86
|
+
* — the table's `min-width` still counts it, so the row scrolls rather than
|
|
87
|
+
* squeezing the column to nothing — and it is not itself resizable, because
|
|
88
|
+
* a column with no width of its own has no edge to drag.
|
|
89
|
+
*/
|
|
90
|
+
fill?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Full text for the `title` on every cell in this column, when the cell
|
|
93
|
+
* draws something other than its own value.
|
|
94
|
+
*
|
|
95
|
+
* Cells truncate and never wrap, so the full text has to be reachable on
|
|
96
|
+
* hover. A column that renders its value directly gets that for free. A
|
|
97
|
+
* column with its own `cell` renderer draws an arbitrary node, and the
|
|
98
|
+
* table will not guess a string for it — say what the hover text is here.
|
|
99
|
+
* Return `undefined` for no title.
|
|
100
|
+
*/
|
|
101
|
+
hoverText?: (row: Row) => string | undefined;
|
|
74
102
|
/** Enter or double-click opens an editor; Enter commits, Escape cancels. */
|
|
75
103
|
editable?: boolean | DataTableEditable;
|
|
76
104
|
/** Receives the committed value. Falls back to the table's `onCellCommit`. */
|
|
@@ -85,6 +113,14 @@ export interface DataTableColumnMeta<Row = unknown> {
|
|
|
85
113
|
cellClassName?: string;
|
|
86
114
|
/** Extra classes for the header cell. */
|
|
87
115
|
headerClassName?: string;
|
|
116
|
+
/**
|
|
117
|
+
* @internal Set by `useDataTable`, never by a consumer. Marks a column whose
|
|
118
|
+
* cell renders its own value, so the view can put the full text in a `title`
|
|
119
|
+
* without guessing a string out of a React node (DESIGN-77). It has to be
|
|
120
|
+
* recorded before TanStack merges its fallback cell renderer in, after which
|
|
121
|
+
* a column that declared no `cell` looks exactly like one that did.
|
|
122
|
+
*/
|
|
123
|
+
plainCell?: boolean;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
126
|
export type DataTableColumnDef<Row extends RowData, Value = unknown> = ColumnDef<
|
|
@@ -183,7 +219,12 @@ export interface UseDataTableOptions<Row extends RowData> {
|
|
|
183
219
|
enableHiding?: boolean;
|
|
184
220
|
/** @default true */
|
|
185
221
|
enableColumnPinning?: boolean;
|
|
186
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* Drag handles on the column edges. Defaults to `false`, or to `true` when
|
|
224
|
+
* a column is marked `meta.fill`: there the drag has somewhere to go, since
|
|
225
|
+
* the fill column absorbs whatever the dragged column takes or gives back.
|
|
226
|
+
* An explicit `false` still wins.
|
|
227
|
+
*/
|
|
187
228
|
enableColumnResizing?: boolean;
|
|
188
229
|
/** Starting sort, filters, visibility, pinning, sizing. */
|
|
189
230
|
initialState?: Partial<DataTableState<Row>>;
|
|
@@ -36,6 +36,45 @@ export const AUTO_REFRESH_STORAGE_PREFIX = "cortena.data-table.auto-refresh.";
|
|
|
36
36
|
|
|
37
37
|
/** One warning per process, not one per render. */
|
|
38
38
|
const warned = { missingStateKey: false };
|
|
39
|
+
/** Column sets already complained about, so a re-render does not repeat it. */
|
|
40
|
+
const warnedUnsized = new Set<string>();
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* True where a developer is the one who would read a `console.warn`.
|
|
44
|
+
*
|
|
45
|
+
* Neither signal is universal, and the unguarded form of either one throws.
|
|
46
|
+
* `process` is a Node global: a bundler that replaces `process.env.NODE_ENV`
|
|
47
|
+
* leaves nothing behind, but a plain ESM consumer or a browser import map has
|
|
48
|
+
* no `process` at all, and the bare identifier is a ReferenceError that takes
|
|
49
|
+
* the table down with it. `import.meta.env` is Vite's, and is simply absent
|
|
50
|
+
* under webpack or in the published ESM as Node loads it. So: ask Vite, then
|
|
51
|
+
* ask Node, and when neither answers say nothing. A development warning is
|
|
52
|
+
* worth having only where someone can act on it, and a library that throws to
|
|
53
|
+
* deliver one is worse than a library that stays quiet.
|
|
54
|
+
*/
|
|
55
|
+
function isDevBuild(): boolean {
|
|
56
|
+
const viteEnv = (import.meta as unknown as { env?: { DEV?: boolean } }).env;
|
|
57
|
+
if (viteEnv) return viteEnv.DEV === true;
|
|
58
|
+
if (typeof process === "undefined") return false;
|
|
59
|
+
return process.env?.NODE_ENV !== "production";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Forgets which column sets have already warned.
|
|
64
|
+
*
|
|
65
|
+
* @internal Test hook. The warning is once per process, which makes every test
|
|
66
|
+
* after the first one depend on using column ids no earlier test used. Call
|
|
67
|
+
* this in a `beforeEach` instead.
|
|
68
|
+
*/
|
|
69
|
+
export function resetUnsizedWarnings(): void {
|
|
70
|
+
warnedUnsized.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The id TanStack will give this column, for a message written before the table exists. */
|
|
74
|
+
function columnDefId(column: { id?: string; accessorKey?: unknown; header?: unknown }): string {
|
|
75
|
+
return column.id ?? (typeof column.accessorKey === "string" ? column.accessorKey : undefined) ??
|
|
76
|
+
(typeof column.header === "string" ? column.header : "(unnamed)");
|
|
77
|
+
}
|
|
39
78
|
|
|
40
79
|
function readStoredFlag(key: string): boolean {
|
|
41
80
|
try {
|
|
@@ -131,7 +170,7 @@ export function useDataTable<Row extends RowData>(
|
|
|
131
170
|
enableColumnFilters = true,
|
|
132
171
|
enableHiding = true,
|
|
133
172
|
enableColumnPinning = true,
|
|
134
|
-
enableColumnResizing
|
|
173
|
+
enableColumnResizing,
|
|
135
174
|
initialState,
|
|
136
175
|
onCellCommit,
|
|
137
176
|
onRowSelectionChange,
|
|
@@ -229,6 +268,35 @@ export function useDataTable<Row extends RowData>(
|
|
|
229
268
|
|
|
230
269
|
const data: ReadonlyArray<Row> = isServer ? server.rows : dataSource.rows;
|
|
231
270
|
|
|
271
|
+
/* Column widths (DESIGN-77) ------------------------------------------ */
|
|
272
|
+
|
|
273
|
+
// A fill column is the one column drawn with no fixed width, so a grid that
|
|
274
|
+
// is `table-fixed` end to end still has somewhere to put the space left
|
|
275
|
+
// over. It also turns resizing on, because the drag now has somewhere to go.
|
|
276
|
+
const hasFillColumn = columns.some((c) => c.meta?.fill);
|
|
277
|
+
const resizing = enableColumnResizing ?? hasFillColumn;
|
|
278
|
+
// A consumer who asked for resizing gets the live drag. A table that only
|
|
279
|
+
// has it because of the fill column commits on release: every move in
|
|
280
|
+
// `onChange` re-renders every row, and a table nobody asked to be resizable
|
|
281
|
+
// should not pay that for a drag it did not advertise.
|
|
282
|
+
const resizeMode = enableColumnResizing === undefined ? "onEnd" : "onChange";
|
|
283
|
+
|
|
284
|
+
// A column with no `size` sits at TanStack's 150 px default. That is fine
|
|
285
|
+
// for a status chip and wrong for the title people scan the list by, and
|
|
286
|
+
// nothing on screen says which happened — the text is simply clipped.
|
|
287
|
+
const unsizedIds = columns.filter((c) => c.size === undefined).map(columnDefId).join(", ");
|
|
288
|
+
React.useEffect(() => {
|
|
289
|
+
if (!isDevBuild()) return;
|
|
290
|
+
if (hasFillColumn || unsizedIds === "" || warnedUnsized.has(unsizedIds)) return;
|
|
291
|
+
warnedUnsized.add(unsizedIds);
|
|
292
|
+
console.warn(
|
|
293
|
+
`[cortena-ui] DataTable: no column is marked \`meta: { fill: true }\`, and ` +
|
|
294
|
+
`${unsizedIds} declared no \`size\`, so ${unsizedIds.includes(",") ? "they sit" : "it sits"} ` +
|
|
295
|
+
"at the 150px default and the table leaves empty space to the right. Mark the " +
|
|
296
|
+
"column people scan the list by with `meta: { fill: true }` and give the rest a `size`.",
|
|
297
|
+
);
|
|
298
|
+
}, [hasFillColumn, unsizedIds]);
|
|
299
|
+
|
|
232
300
|
// System columns first; then each consumer column with its meta defaults
|
|
233
301
|
// applied (a faceted column filters with `oneOf` unless told otherwise).
|
|
234
302
|
const resolvedColumns = React.useMemo(() => {
|
|
@@ -238,13 +306,23 @@ export function useDataTable<Row extends RowData>(
|
|
|
238
306
|
if (enableExpanding && !columns.some((c) => c.id === "expand")) out.push(expandColumn(helper));
|
|
239
307
|
for (const column of columns) {
|
|
240
308
|
const filter = column.meta?.filter;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
309
|
+
const defaults: Record<string, unknown> = {};
|
|
310
|
+
if (filter === "faceted" && column.filterFn === undefined) defaults.filterFn = "oneOf";
|
|
311
|
+
else if (filter === "text" && column.filterFn === undefined) defaults.filterFn = "includesString";
|
|
312
|
+
// The fill column has no width of its own, so there is no edge to drag.
|
|
313
|
+
if (column.meta?.fill && column.enableResizing === undefined) defaults.enableResizing = false;
|
|
314
|
+
// Recorded before TanStack merges its fallback renderer in: by the time
|
|
315
|
+
// the view is handed a `columnDef`, the default cell renderer is there
|
|
316
|
+
// and a column that declared no `cell` is indistinguishable from one
|
|
317
|
+
// that did. It goes in `meta`, which TanStack carries verbatim, rather
|
|
318
|
+
// than on a symbol key the table is under no obligation to copy.
|
|
319
|
+
const plain = !("cell" in column) || column.cell == null;
|
|
320
|
+
const resolved = {
|
|
321
|
+
...column,
|
|
322
|
+
...defaults,
|
|
323
|
+
...(plain ? { meta: { ...column.meta, plainCell: true } } : null),
|
|
324
|
+
};
|
|
325
|
+
out.push(resolved as DataTableColumnDef<Row, any>);
|
|
248
326
|
}
|
|
249
327
|
return out;
|
|
250
328
|
}, [columns, enableRowSelection, enableExpanding]);
|
|
@@ -275,8 +353,8 @@ export function useDataTable<Row extends RowData>(
|
|
|
275
353
|
enableColumnFilters,
|
|
276
354
|
enableHiding,
|
|
277
355
|
enableColumnPinning,
|
|
278
|
-
enableColumnResizing,
|
|
279
|
-
columnResizeMode:
|
|
356
|
+
enableColumnResizing: resizing,
|
|
357
|
+
columnResizeMode: resizeMode,
|
|
280
358
|
// `search`, not `includesString`: it trims the query, so a box holding only
|
|
281
359
|
// whitespace filters nothing rather than emptying the grid. See features.ts.
|
|
282
360
|
globalFilterFn: "search",
|