cursedbelt 5.25.0 → 5.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/react/components/Badge.d.ts +6 -6
- package/dist/react/components/Badge.d.ts.map +1 -1
- package/dist/react/components/Badge.js +13 -5
- package/dist/react/components/Badge.js.map +1 -1
- package/dist/react/components/DataTable/DataTable.d.ts +125 -5
- package/dist/react/components/DataTable/DataTable.d.ts.map +1 -1
- package/dist/react/components/DataTable/DataTable.js +335 -121
- package/dist/react/components/DataTable/DataTable.js.map +1 -1
- package/dist/react/components/DataTable/RowActions.d.ts +3 -1
- package/dist/react/components/DataTable/RowActions.d.ts.map +1 -1
- package/dist/react/components/DataTable/RowActions.js +9 -2
- package/dist/react/components/DataTable/RowActions.js.map +1 -1
- package/dist/react/components/DataTable/SortHeader.d.ts +4 -1
- package/dist/react/components/DataTable/SortHeader.d.ts.map +1 -1
- package/dist/react/components/DataTable/SortHeader.js +7 -1
- package/dist/react/components/DataTable/SortHeader.js.map +1 -1
- package/dist/react/components/DataTable/cells/CellRenderer.d.ts.map +1 -1
- package/dist/react/components/DataTable/cells/CellRenderer.js +4 -1
- package/dist/react/components/DataTable/cells/CellRenderer.js.map +1 -1
- package/dist/react/components/DataTable/index.d.ts +2 -2
- package/dist/react/components/DataTable/index.d.ts.map +1 -1
- package/dist/react/components/DataTable/index.js +1 -1
- package/dist/react/components/DataTable/index.js.map +1 -1
- package/dist/react/components/DataTable/lazyPart.d.ts +27 -0
- package/dist/react/components/DataTable/lazyPart.d.ts.map +1 -0
- package/dist/react/components/DataTable/lazyPart.js +46 -0
- package/dist/react/components/DataTable/lazyPart.js.map +1 -0
- package/dist/react/components/DataTable/types.d.ts +106 -0
- package/dist/react/components/DataTable/types.d.ts.map +1 -1
- package/dist/react/components/DataTable/types.js.map +1 -1
- package/dist/react/components/auth/SignInCard.d.ts +49 -0
- package/dist/react/components/auth/SignInCard.d.ts.map +1 -0
- package/dist/react/components/auth/SignInCard.js +21 -0
- package/dist/react/components/auth/SignInCard.js.map +1 -0
- package/dist/react/components/auth/index.d.ts +1 -0
- package/dist/react/components/auth/index.d.ts.map +1 -1
- package/dist/react/components/auth/index.js +1 -0
- package/dist/react/components/auth/index.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/styles-areas/analytics.css +9 -6
- package/dist/styles-areas/core.css +8 -7
- package/dist/styles-areas/data-table.css +8 -5
- package/dist/styles-utilities.css +75 -0
- package/package.json +2 -2
- package/src/react/components/Badge.capsule.spec.ts +178 -0
- package/src/react/components/Badge.tsx +13 -5
- package/src/react/components/DataTable/DataTable.spec.tsx +6 -2
- package/src/react/components/DataTable/DataTable.tsx +518 -104
- package/src/react/components/DataTable/RowActions.tsx +18 -10
- package/src/react/components/DataTable/SortHeader.tsx +19 -0
- package/src/react/components/DataTable/cells/CellRenderer.tsx +2 -0
- package/src/react/components/DataTable/index.ts +10 -1
- package/src/react/components/DataTable/lazyPart.spec.tsx +99 -0
- package/src/react/components/DataTable/lazyPart.tsx +58 -0
- package/src/react/components/DataTable/rowHooks.spec.tsx +289 -0
- package/src/react/components/DataTable/selection.spec.tsx +6 -2
- package/src/react/components/DataTable/tableSystem.spec.tsx +6 -2
- package/src/react/components/DataTable/types.ts +110 -0
- package/src/react/components/DataTable/virtualize.spec.tsx +38 -2
- package/src/react/components/auth/SignInCard.spec.tsx +48 -0
- package/src/react/components/auth/SignInCard.tsx +90 -0
- package/src/react/components/auth/index.ts +1 -0
- package/src/react/index.ts +5 -0
- package/src/styles-areas/analytics.css +9 -6
- package/src/styles-areas/core.css +8 -7
- package/src/styles-areas/data-table.css +8 -5
- package/src/styles-utilities.css +75 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from '@tanstack/react-table';
|
|
3
3
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
|
-
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { Fragment, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
5
5
|
import { useScrollEdgeFade } from '../../hooks/useScrollEdgeFade.js';
|
|
6
6
|
import { cn } from '../../lib/cn.js';
|
|
7
|
+
import { TAP_TARGET_HEIGHT } from '../../lib/tapTarget.js';
|
|
7
8
|
import { CardTapTarget } from '../CardTapTarget.js';
|
|
8
9
|
import { Checkbox } from '../Checkbox.js';
|
|
9
10
|
import { EmptyState } from '../EmptyState.js';
|
|
@@ -13,12 +14,39 @@ import { Tooltip } from '../Tooltip.js';
|
|
|
13
14
|
import { CardView } from './CardView.js';
|
|
14
15
|
import { CellRenderer } from './cells/CellRenderer.js';
|
|
15
16
|
import { EMPTY_FILTER_QUERY, evaluateFilterQuery, filterableText, } from './filterQuery.js';
|
|
16
|
-
import {
|
|
17
|
-
import { RowActions } from './RowActions.js';
|
|
17
|
+
import { preloadRowMenu, RowActions } from './RowActions.js';
|
|
18
18
|
import { writeRowDrag } from './rowDrag.js';
|
|
19
19
|
import { SortHeader } from './SortHeader.js';
|
|
20
|
-
import { TableToolbar } from './Toolbar.js';
|
|
21
20
|
import { resolveColumnAlign } from './types.js';
|
|
21
|
+
import { lazyPart } from './lazyPart.js';
|
|
22
|
+
// 🔴 The toolbar and the pager load only when a table DRAWS them (forge task 2309).
|
|
23
|
+
// Between them they reach Radix's Select, Menu and Popover — ~20 kB gzip that a
|
|
24
|
+
// read-only list (`searchable={false}`, `serverWindow`, no row menu) never opens.
|
|
25
|
+
// Measured on apps/music's track list: 67 kB gzip over its entry ceiling, most of
|
|
26
|
+
// it these two parts' dependencies. A static import made every consumer pay for
|
|
27
|
+
// them. The row kebab's menu is lazy the same way, inside `RowActions`; all three
|
|
28
|
+
// are loaded ahead by `preloadDataTableParts`. `lazyPart` erases the component's
|
|
29
|
+
// type parameter, so each is given back its own signature.
|
|
30
|
+
const ToolbarPart = lazyPart(() => import('./Toolbar.js').then((m) => m.TableToolbar));
|
|
31
|
+
const PaginationPart = lazyPart(() => import('./Pagination.js').then((m) => m.TablePagination));
|
|
32
|
+
/**
|
|
33
|
+
* Load the table's lazy parts — the toolbar, the pager, the row kebab's menu — now,
|
|
34
|
+
* so every table mounted after this resolves draws them on its first render. For a
|
|
35
|
+
* spec setup that queries them synchronously after `render(…)`, and for an app about
|
|
36
|
+
* to show a table with a toolbar. Never required: without it they arrive a tick late,
|
|
37
|
+
* into a placeholder of their own height.
|
|
38
|
+
*/
|
|
39
|
+
export async function preloadDataTableParts() {
|
|
40
|
+
await Promise.all([ToolbarPart.preload(), PaginationPart.preload(), preloadRowMenu()]);
|
|
41
|
+
}
|
|
42
|
+
/** Where the toolbar will be, the height it will be — so its arrival shifts nothing. */
|
|
43
|
+
function ToolbarPlaceholder() {
|
|
44
|
+
return (_jsx("div", { "data-cb-table-toolbar-pending": '', "aria-hidden": true, className: 'border-b border-border px-2 py-2', children: _jsx("div", { className: cn('h-9', TAP_TARGET_HEIGHT) }) }));
|
|
45
|
+
}
|
|
46
|
+
/** Where the pager will be, the height it will be. */
|
|
47
|
+
function PaginationPlaceholder() {
|
|
48
|
+
return (_jsx("div", { "data-cb-table-pagination-pending": '', "aria-hidden": true, className: 'border-t border-border px-4 py-2.5', children: _jsx("div", { className: cn('h-8', TAP_TARGET_HEIGHT) }) }));
|
|
49
|
+
}
|
|
22
50
|
/** Build an OnChangeFn that updates local state AND notifies an optional listener. */
|
|
23
51
|
function chainUpdater(set, notify) {
|
|
24
52
|
return (updater) => set((prev) => {
|
|
@@ -119,15 +147,16 @@ export function declaredTrackFloor(columns, hasActions = false) {
|
|
|
119
147
|
* been required to be stable since ES2019 and TanStack's sorted row model uses
|
|
120
148
|
* it, so rows that tie on every key in the stack keep the order they arrived in.
|
|
121
149
|
*/
|
|
122
|
-
export function nextSortStack(current, id, multi) {
|
|
150
|
+
export function nextSortStack(current, id, multi, options = {}) {
|
|
151
|
+
const first = options.descFirst ?? false;
|
|
123
152
|
const existing = current.find((sort) => sort.id === id);
|
|
124
153
|
if (!existing) {
|
|
125
|
-
const
|
|
126
|
-
return multi ? [
|
|
154
|
+
const started = { id, desc: first };
|
|
155
|
+
return multi ? [started, ...current] : [started];
|
|
127
156
|
}
|
|
128
|
-
if (
|
|
129
|
-
const flipped =
|
|
130
|
-
return multi ?
|
|
157
|
+
if (existing.desc === first || options.removable === false) {
|
|
158
|
+
const flipped = { id, desc: !existing.desc };
|
|
159
|
+
return multi ? current.map((sort) => (sort.id === id ? flipped : sort)) : [flipped];
|
|
131
160
|
}
|
|
132
161
|
return multi ? current.filter((sort) => sort.id !== id) : [];
|
|
133
162
|
}
|
|
@@ -146,6 +175,48 @@ function readPath(row, key) {
|
|
|
146
175
|
return value[part];
|
|
147
176
|
}, row);
|
|
148
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* `ColDef.hideBelow` as the container-query classes that do it. Literal strings, so the style
|
|
180
|
+
* generators (`scripts/generate*Styles.ts`) find them in the source.
|
|
181
|
+
*
|
|
182
|
+
* 🔴 The column COLLAPSES — zero width, no padding, its content removed — rather than
|
|
183
|
+
* `display: none`. A hidden cell is not a column, but the rows that SPAN the table (the
|
|
184
|
+
* virtualizer's spacers, a server window's placeholder rows, an expanded panel) still say
|
|
185
|
+
* `colSpan={every column}`, and a span wider than the table's real columns makes the missing
|
|
186
|
+
* ones up: they took the width the hidden columns gave back, so the table's rows stopped short
|
|
187
|
+
* of its right edge and the title — the one width-less column — was crushed to "S…". Measured
|
|
188
|
+
* on apps/music at 390px and 1100px, forge task 2302. Collapsed, the column count never moves,
|
|
189
|
+
* and every span stays true. (`[&>*]:hidden` takes the content out of the accessibility tree
|
|
190
|
+
* too, so a collapsed cell reads as nothing, not as a value nobody can see.)
|
|
191
|
+
*/
|
|
192
|
+
const HIDE_BELOW_CLASS = {
|
|
193
|
+
sm: '@max-2xl:w-0! @max-2xl:min-w-0! @max-2xl:p-0! @max-2xl:border-0 @max-2xl:[&>*]:hidden',
|
|
194
|
+
md: '@max-4xl:w-0! @max-4xl:min-w-0! @max-4xl:p-0! @max-4xl:border-0 @max-4xl:[&>*]:hidden',
|
|
195
|
+
lg: '@max-6xl:w-0! @max-6xl:min-w-0! @max-6xl:p-0! @max-6xl:border-0 @max-6xl:[&>*]:hidden',
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* The table's own props for a row, with the caller's `getRowProps` merged on top: classes join,
|
|
199
|
+
* styles spread, and a handler both set runs the table's first, then the caller's.
|
|
200
|
+
*/
|
|
201
|
+
export function mergeRowProps(own, extra) {
|
|
202
|
+
if (!extra)
|
|
203
|
+
return own;
|
|
204
|
+
const merged = { ...own, ...extra };
|
|
205
|
+
if (own.className || extra.className)
|
|
206
|
+
merged.className = cn(own.className, extra.className);
|
|
207
|
+
if (own.style || extra.style)
|
|
208
|
+
merged.style = { ...own.style, ...extra.style };
|
|
209
|
+
for (const [key, handler] of Object.entries(extra)) {
|
|
210
|
+
const mine = own[key];
|
|
211
|
+
if (!key.startsWith('on') || typeof handler !== 'function' || typeof mine !== 'function')
|
|
212
|
+
continue;
|
|
213
|
+
merged[key] = (event) => {
|
|
214
|
+
mine(event);
|
|
215
|
+
handler(event);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return merged;
|
|
219
|
+
}
|
|
149
220
|
/** True when a cell value carries nothing worth a column: null/undefined/''/[]. */
|
|
150
221
|
function isEmptyCellValue(v) {
|
|
151
222
|
if (v == null)
|
|
@@ -169,11 +240,15 @@ function isEmptyCellValue(v) {
|
|
|
169
240
|
* full-width accordion detail row that never breaks column alignment, and an automatic
|
|
170
241
|
* stacked-card layout under `sm` so phones are never handed a clipped grid.
|
|
171
242
|
*/
|
|
172
|
-
export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRowDelete, rowMenu, loading, emptyState, toolbar, pageSize = 50, pageSizeOptions, hidePagination, caption, className, stickyHeader, stickyFirstColumn, renderExpanded, expandedRowId, rowClassName, hideEmptyColumns, fixedLayout, minTableWidth, scrollBody, virtualize, mobileCards = true, serverSide, pageCount, rowCount, onSortingChange, onColumnFiltersChange, onGlobalFilterChange, onPaginationChange, defaultSort, density = 'comfortable', viewMode, onViewModeChange, cardMinWidth, searchable = true, searchPlaceholder, multiSort = true, filterQuery: filterQueryProp, onFilterQueryChange, getRowDrag, dragSourceId, selectedIds, onSelectionChange, isRowSelectable, unselectableReason, selectionLabel, selectionSummary, }) {
|
|
243
|
+
export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRowDelete, rowMenu, loading, emptyState, toolbar, pageSize = 50, pageSizeOptions, hidePagination, caption, className, stickyHeader, stickyFirstColumn, renderExpanded, expandedRowId, rowClassName, hideEmptyColumns, fixedLayout, minTableWidth, scrollBody, virtualize, serverWindow, handleRef, scrollClassName, mobileCards = true, serverSide, pageCount, rowCount, onSortingChange, onColumnFiltersChange, onGlobalFilterChange, onPaginationChange, defaultSort, sorting: sortingProp, enableSortingRemoval = true, sortDisabledReason, density = 'comfortable', viewMode, onViewModeChange, cardMinWidth, searchable = true, searchPlaceholder, multiSort = true, filterQuery: filterQueryProp, onFilterQueryChange, getRowDrag, dragSourceId, selectedIds, onSelectionChange, isRowSelectable, unselectableReason, selectionLabel, selectionSummary, selectAll, getRowProps, }) {
|
|
173
244
|
// Vertical cell padding for the wide grid tier — the one lever the density knob
|
|
174
245
|
// pulls (headers + body cells stay aligned by sharing it).
|
|
175
246
|
const cellPadY = density === 'compact' ? 'py-1.5' : 'py-2.5';
|
|
176
|
-
const [
|
|
247
|
+
const [ownSorting, setOwnSorting] = useState(defaultSort ?? []);
|
|
248
|
+
const sorting = sortingProp ?? ownSorting;
|
|
249
|
+
/** The sort as of THIS render, for a write that has to start from it (see `applySorting`). */
|
|
250
|
+
const sortingRef = useRef(sorting);
|
|
251
|
+
sortingRef.current = sorting;
|
|
177
252
|
const [columnFilters, setColumnFilters] = useState([]);
|
|
178
253
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
179
254
|
const [columnVisibility, setColumnVisibility] = useState(() => Object.fromEntries(columns.filter((c) => c.hidden).map((c) => [c.key, false])));
|
|
@@ -227,14 +302,17 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
227
302
|
* server-side table ends up sorted differently from what its chips say.
|
|
228
303
|
*/
|
|
229
304
|
const applySorting = useCallback((next) => {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
305
|
+
// Computed OUTSIDE a state updater: under a controlled `sorting` there is no state to
|
|
306
|
+
// update, and a notify inside an updater fires twice in StrictMode.
|
|
307
|
+
const value = next(sortingRef.current);
|
|
308
|
+
sortingRef.current = value;
|
|
309
|
+
if (sortingProp === undefined)
|
|
310
|
+
setOwnSorting(value);
|
|
311
|
+
setPagination((p) => (p.pageIndex === 0 ? p : { ...p, pageIndex: 0 }));
|
|
312
|
+
onSortingChange?.(value);
|
|
313
|
+
}, [onSortingChange, sortingProp]);
|
|
314
|
+
const sortDescFirstOf = useCallback((id) => columns.find((col) => col.key === id)?.sortDescFirst ?? false, [columns]);
|
|
315
|
+
const toggleSortKey = useCallback((id) => applySorting((prev) => nextSortStack(prev, id, multiSort, { descFirst: sortDescFirstOf(id), removable: enableSortingRemoval })), [applySorting, enableSortingRemoval, multiSort, sortDescFirstOf]);
|
|
238
316
|
const tsColumns = useMemo(() => liveColumns.map((col) => ({
|
|
239
317
|
id: col.key,
|
|
240
318
|
accessorKey: col.key,
|
|
@@ -245,6 +323,8 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
245
323
|
align: resolveColumnAlign(col),
|
|
246
324
|
width: col.width,
|
|
247
325
|
minWidth: col.minWidth,
|
|
326
|
+
hideBelow: col.hideBelow,
|
|
327
|
+
leading: col.leading,
|
|
248
328
|
},
|
|
249
329
|
size: typeof col.width === 'number' ? col.width : undefined,
|
|
250
330
|
enableSorting: col.sortable ?? false,
|
|
@@ -266,10 +346,10 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
266
346
|
// TanStack's column instances mid-interaction.
|
|
267
347
|
header: ({ column, table: t }) => {
|
|
268
348
|
const stack = t.getState().sorting;
|
|
269
|
-
return (_jsx(SortHeader, { column: column, label: col.label, sortable: col.sortable, sortIndex: stack.length > 1 ? stack.findIndex((s) => s.id === col.key) : -1, onSort: () => toggleSortKey(col.key) }));
|
|
349
|
+
return (_jsx(SortHeader, { column: column, label: col.label, sortable: col.sortable, sortIndex: stack.length > 1 ? stack.findIndex((s) => s.id === col.key) : -1, onSort: () => toggleSortKey(col.key), disabledReason: sortDisabledReason }));
|
|
270
350
|
},
|
|
271
351
|
cell: ({ getValue, row }) => (_jsx(CellRenderer, { col: col, value: getValue(), row: row.original, onEdit: onRowEdit ? () => onRowEdit(row.original) : undefined })),
|
|
272
|
-
})), [liveColumns, onRowEdit, toggleSortKey]);
|
|
352
|
+
})), [liveColumns, onRowEdit, sortDisabledReason, toggleSortKey]);
|
|
273
353
|
const labelOf = useCallback((id) => liveColumns.find((col) => col.key === id)?.label ?? id, [liveColumns]);
|
|
274
354
|
const controls = useMemo(() => ({
|
|
275
355
|
searchText: globalFilter,
|
|
@@ -321,7 +401,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
321
401
|
* failure the pager's absence hides rather than reveals, which is why it is tested by
|
|
322
402
|
* measuring `scrollHeight` in a real browser.
|
|
323
403
|
*/
|
|
324
|
-
const virtualOptions = virtualize === true ? {} : virtualize || null;
|
|
404
|
+
const virtualOptions = virtualize === true || (serverWindow && !virtualize) ? {} : virtualize || null;
|
|
325
405
|
const virtualEnabled = virtualOptions !== null;
|
|
326
406
|
const bodyScrolls = scrollBody || virtualEnabled;
|
|
327
407
|
const paginationHidden = hidePagination || virtualEnabled;
|
|
@@ -340,7 +420,9 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
340
420
|
columns: tsColumns,
|
|
341
421
|
getRowId: resolvedGetRowId,
|
|
342
422
|
state: { sorting, columnFilters, globalFilter: search, columnVisibility, pagination },
|
|
343
|
-
|
|
423
|
+
// Through `applySorting`, like every other write — so a controlled `sorting` is honoured
|
|
424
|
+
// whether TanStack or a header asked for the change.
|
|
425
|
+
onSortingChange: (updater) => applySorting((prev) => (typeof updater === 'function' ? updater(prev) : updater)),
|
|
344
426
|
onColumnFiltersChange: chainUpdaterResettingPage(setColumnFilters, setPagination, onColumnFiltersChange),
|
|
345
427
|
// TanStack hands the updater whatever `state.globalFilter` is, which is now
|
|
346
428
|
// a TableSearchState; the toolbar sets TEXT, so normalise both shapes.
|
|
@@ -368,14 +450,16 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
368
450
|
return false;
|
|
369
451
|
return filterableText(row.getValue(columnId)).toLowerCase().includes(text.toLowerCase());
|
|
370
452
|
},
|
|
371
|
-
|
|
372
|
-
|
|
453
|
+
// A server window is server-ordered and server-filtered by construction: a WINDOW
|
|
454
|
+
// re-sorted locally would put row 4,000's neighbours next to row 12's.
|
|
455
|
+
manualSorting: serverSide || !!serverWindow,
|
|
456
|
+
manualFiltering: serverSide || !!serverWindow,
|
|
373
457
|
manualPagination: serverSide,
|
|
374
458
|
pageCount: serverSide ? pageCount : undefined,
|
|
375
459
|
rowCount: serverSide ? rowCount : undefined,
|
|
376
460
|
getCoreRowModel: getCoreRowModel(),
|
|
377
|
-
getSortedRowModel: serverSide ? undefined : getSortedRowModel(),
|
|
378
|
-
getFilteredRowModel: serverSide ? undefined : getFilteredRowModel(),
|
|
461
|
+
getSortedRowModel: serverSide || serverWindow ? undefined : getSortedRowModel(),
|
|
462
|
+
getFilteredRowModel: serverSide || serverWindow ? undefined : getFilteredRowModel(),
|
|
379
463
|
// 🔴 `virtualEnabled` too, not only `serverSide`. See the note where the flag is
|
|
380
464
|
// computed: a paginated virtualizer sizes its spacer from one PAGE.
|
|
381
465
|
getPaginationRowModel: serverSide || virtualEnabled ? undefined : getPaginationRowModel(),
|
|
@@ -413,17 +497,94 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
413
497
|
};
|
|
414
498
|
};
|
|
415
499
|
const rows = table.getRowModel().rows;
|
|
416
|
-
|
|
500
|
+
/**
|
|
501
|
+
* 🔴 A {@link serverWindow} row is FIXED-height, never measured. A measured list places
|
|
502
|
+
* every unloaded row at the estimate and every loaded one at its real height, so the page
|
|
503
|
+
* that arrives above the viewport re-sizes a hundred rows and moves everything under the
|
|
504
|
+
* reader — measured in the gate: a smooth reveal of row 4,000 that paged in the rows above
|
|
505
|
+
* it on arrival came to rest thirteen rows short. apps/music's own list held every row at
|
|
506
|
+
* one height for the same reason ("Fixed row height is what makes the arithmetic jitter-free").
|
|
507
|
+
*/
|
|
508
|
+
const fixedRowHeight = serverWindow
|
|
509
|
+
? (serverWindow.rowHeight ?? virtualOptions?.estimateRowHeight ?? (density === 'compact' ? 36 : 44))
|
|
510
|
+
: null;
|
|
511
|
+
const estimateRowHeight = fixedRowHeight ?? virtualOptions?.estimateRowHeight ?? (density === 'compact' ? 36 : 44);
|
|
417
512
|
const scrollRef = useRef(null);
|
|
513
|
+
/** Rows before the loaded window, and the size of the whole result set — both 0/`rows.length`
|
|
514
|
+
* without a {@link serverWindow}, which makes every index below the same number as before. */
|
|
515
|
+
const windowStart = serverWindow ? Math.max(0, serverWindow.start) : 0;
|
|
516
|
+
const windowTotal = serverWindow ? Math.max(serverWindow.total, windowStart + rows.length) : rows.length;
|
|
517
|
+
/** The caller's window callbacks, read at FIRE time: by the time a settle timer fires the
|
|
518
|
+
* window may have moved, and the fresh handler is the one that knows. */
|
|
519
|
+
const windowRef = useRef(serverWindow);
|
|
520
|
+
windowRef.current = serverWindow;
|
|
521
|
+
const loadedRef = useRef({ start: windowStart, end: windowStart + rows.length, total: windowTotal });
|
|
522
|
+
loadedRef.current = { start: windowStart, end: windowStart + rows.length, total: windowTotal };
|
|
523
|
+
const settleRef = useRef(null);
|
|
524
|
+
/** The `scrollTop` the last `onChange` saw. The virtualizer also calls `onChange` when its
|
|
525
|
+
* is-scrolling flag resets and on every re-render; only a MOVE may arm the settle timer, or
|
|
526
|
+
* the flag's own reset re-asks for the window that was just requested (measured: twice). */
|
|
527
|
+
const lastTopRef = useRef(null);
|
|
528
|
+
useEffect(() => () => {
|
|
529
|
+
if (settleRef.current)
|
|
530
|
+
clearTimeout(settleRef.current);
|
|
531
|
+
}, []);
|
|
418
532
|
const rowVirtualizer = useVirtualizer({
|
|
419
|
-
count: virtualEnabled ?
|
|
533
|
+
count: virtualEnabled ? windowTotal : 0,
|
|
420
534
|
getScrollElement: () => scrollRef.current,
|
|
421
535
|
estimateSize: () => estimateRowHeight,
|
|
422
536
|
overscan: virtualOptions?.overscan ?? 8,
|
|
537
|
+
onChange: serverWindow
|
|
538
|
+
? (instance) => {
|
|
539
|
+
const win = windowRef.current;
|
|
540
|
+
const element = instance.scrollElement;
|
|
541
|
+
if (!win || !element)
|
|
542
|
+
return;
|
|
543
|
+
const { start, end, total } = loadedRef.current;
|
|
544
|
+
// Rows over the whole result set: the first one on screen, and the one after the last.
|
|
545
|
+
const items = instance.getVirtualItems();
|
|
546
|
+
if (items.length === 0)
|
|
547
|
+
return;
|
|
548
|
+
const top = element.scrollTop;
|
|
549
|
+
const bottom = top + element.clientHeight;
|
|
550
|
+
const firstShown = items.find((item) => item.end > top)?.index ?? items[0].index;
|
|
551
|
+
const lastShown = (items.findLast((item) => item.start < bottom)?.index ?? items.at(-1).index) + 1;
|
|
552
|
+
const moved = lastTopRef.current !== top;
|
|
553
|
+
lastTopRef.current = top;
|
|
554
|
+
// Nothing on screen is loaded. Hand the destination over once the scroll RESTS,
|
|
555
|
+
// and do not also ask for a page — it would load rows nowhere near the reader.
|
|
556
|
+
if (firstShown >= end || lastShown <= start) {
|
|
557
|
+
if (!moved)
|
|
558
|
+
return;
|
|
559
|
+
if (settleRef.current)
|
|
560
|
+
clearTimeout(settleRef.current);
|
|
561
|
+
settleRef.current = setTimeout(() => {
|
|
562
|
+
settleRef.current = null;
|
|
563
|
+
// Read at FIRE time: the window may have arrived while the scroll rested.
|
|
564
|
+
const loaded = loadedRef.current;
|
|
565
|
+
if (firstShown < loaded.end && lastShown > loaded.start)
|
|
566
|
+
return;
|
|
567
|
+
windowRef.current?.onNeedWindow?.(firstShown);
|
|
568
|
+
}, win.settleMs ?? 120);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (settleRef.current) {
|
|
572
|
+
clearTimeout(settleRef.current);
|
|
573
|
+
settleRef.current = null;
|
|
574
|
+
}
|
|
575
|
+
// Against the end of the LOADED window, never the spacer's — see the prop doc.
|
|
576
|
+
if (win.onReachEnd && end < total && end - lastShown < (win.reachRows ?? 20))
|
|
577
|
+
win.onReachEnd();
|
|
578
|
+
// Tighter, and only off row 0: a reveal lands a few rows into a fresh window, and a
|
|
579
|
+
// generous trigger here would fire on arrival and cascade back to the top.
|
|
580
|
+
if (win.onReachStart && start > 0 && firstShown - start < (win.reachRowsUp ?? 8))
|
|
581
|
+
win.onReachStart();
|
|
582
|
+
}
|
|
583
|
+
: undefined,
|
|
423
584
|
// 🔴 MEASURED, not assumed. A wrapping cell, an expanded detail panel and the
|
|
424
585
|
// `compact` density all change a row's height, and a caller cannot be asked to
|
|
425
586
|
// compute any of them. `estimateSize` only seeds the spacer before first paint.
|
|
426
|
-
measureElement: (el) => el.getBoundingClientRect().height,
|
|
587
|
+
measureElement: (el) => fixedRowHeight ?? el.getBoundingClientRect().height,
|
|
427
588
|
});
|
|
428
589
|
/**
|
|
429
590
|
* 🔴 Invalidate the measurement cache when the row height changes.
|
|
@@ -447,8 +608,27 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
447
608
|
}, [rowVirtualizer, estimateRowHeight]);
|
|
448
609
|
const virtualRows = virtualEnabled ? rowVirtualizer.getVirtualItems() : [];
|
|
449
610
|
/** The rows to render, and the two spacers that keep the scrollbar honest about the
|
|
450
|
-
* ones that are not. Off, this is every row and no spacers — byte-identical output.
|
|
451
|
-
|
|
611
|
+
* ones that are not. Off, this is every row and no spacers — byte-identical output.
|
|
612
|
+
* `null` is a row a {@link serverWindow} has not loaded: a placeholder of the estimated
|
|
613
|
+
* height, so the window can land anywhere in the spacer before its rows arrive. */
|
|
614
|
+
const bodyRows = virtualEnabled
|
|
615
|
+
? virtualRows.map((v) => rows[v.index - windowStart] ?? null)
|
|
616
|
+
: rows;
|
|
617
|
+
useImperativeHandle(handleRef, () => ({
|
|
618
|
+
get scrollElement() {
|
|
619
|
+
return scrollRef.current;
|
|
620
|
+
},
|
|
621
|
+
scrollToIndex(index, options = {}) {
|
|
622
|
+
const element = scrollRef.current;
|
|
623
|
+
if (!element || !virtualEnabled)
|
|
624
|
+
return null;
|
|
625
|
+
const clamped = Math.max(0, Math.min(index, loadedRef.current.total - 1));
|
|
626
|
+
const [offset] = rowVirtualizer.getOffsetForIndex(clamped, options.align ?? 'center') ?? [clamped * estimateRowHeight];
|
|
627
|
+
const top = Math.max(0, Math.round(offset));
|
|
628
|
+
element.scrollTo({ top, behavior: options.behavior ?? 'auto' });
|
|
629
|
+
return top;
|
|
630
|
+
},
|
|
631
|
+
}), [estimateRowHeight, rowVirtualizer, virtualEnabled]);
|
|
452
632
|
const padTop = virtualEnabled ? (virtualRows[0]?.start ?? 0) : 0;
|
|
453
633
|
const padBottom = virtualEnabled
|
|
454
634
|
? rowVirtualizer.getTotalSize() - (virtualRows.at(-1)?.end ?? 0)
|
|
@@ -534,7 +714,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
534
714
|
// Enter-to-open handler.
|
|
535
715
|
onKeyDown: (event) => event.stopPropagation(),
|
|
536
716
|
});
|
|
537
|
-
const headerCheckbox = (label) => (_jsx(Checkbox, { checked: headerChecked, disabled: selectableFilteredIds.length === 0, onCheckedChange: toggleAllSelected, "aria-label": selectAllLabel, label: label, labelClassName: label == null ? undefined : 'text-xs text-muted-foreground' }));
|
|
717
|
+
const headerCheckbox = (label) => selectAll ? (_jsx(Tooltip, { content: selectAll.label, children: _jsx("span", { className: 'inline-flex', children: _jsx(Checkbox, { checked: selectAll.checked, disabled: selectAll.disabled, onCheckedChange: (checked) => selectAll.onChange(checked === true), "aria-label": selectAll.label, label: label, labelClassName: label == null ? undefined : 'text-xs text-muted-foreground' }) }) })) : (_jsx(Checkbox, { checked: headerChecked, disabled: selectableFilteredIds.length === 0, onCheckedChange: toggleAllSelected, "aria-label": selectAllLabel, label: label, labelClassName: label == null ? undefined : 'text-xs text-muted-foreground' }));
|
|
538
718
|
const summaryNode = selectedIds && selectionSummary
|
|
539
719
|
? selectionSummary(resolvedGetRowId
|
|
540
720
|
? data.filter((row) => selectedIds.has(resolvedGetRowId(row)))
|
|
@@ -550,7 +730,7 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
550
730
|
// passes its own gets exactly that, and `TableToolbar` reads the same
|
|
551
731
|
// controls off `table.options.meta`, so composing a toolbar is never
|
|
552
732
|
// the thing that costs you the search box.
|
|
553
|
-
(searchable ? _jsx(
|
|
733
|
+
(searchable ? (_jsx(ToolbarPart, { table: table, searchPlaceholder: searchPlaceholder, fallback: _jsx(ToolbarPlaceholder, {}) })) : null));
|
|
554
734
|
const visibleColumnCount = table.getVisibleLeafColumns().length + (hasActions ? 1 : 0) + (selectionEnabled ? 1 : 0);
|
|
555
735
|
// Cards track the SAME live visibility state as the grid (ViewOptionsMenu toggles
|
|
556
736
|
// apply to both) — not just the ColDefs' initial `hidden` flags.
|
|
@@ -566,12 +746,14 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
566
746
|
const tableMinWidth = fixedLayout
|
|
567
747
|
? Math.max(minTableWidth ?? 0, declaredTrackFloor(cardColumns, hasActions) + (selectionEnabled ? SELECT_TRACK_PX : 0))
|
|
568
748
|
: minTableWidth;
|
|
569
|
-
return (_jsxs("div", { className: cn('@container flex min-h-0 flex-col', className), children: [toolbarNode, summaryNode != null ? (_jsx("div", { "data-cb-table-selection-summary": '', "aria-live": 'polite', className: 'flex min-h-11 flex-wrap items-center gap-3 border-b border-border px-4 py-2 text-sm', children: summaryNode })) : null, loading ? (_jsx("div", { className: 'flex flex-col gap-2 p-4', children: Array.from({ length: 5 }).map((_, i) => (_jsx(Skeleton, { className: 'h-10 w-full' }, `dt-skeleton-${i}`))) })) : rows.length === 0 ? (_jsx("div", { className: 'p-2', children: emptyState ?? _jsx(EmptyState, { title: 'No results' }) })) : (_jsxs("div", { ref: scrollRef,
|
|
749
|
+
return (_jsxs("div", { className: cn('@container flex min-h-0 flex-col', className), children: [toolbarNode, summaryNode != null ? (_jsx("div", { "data-cb-table-selection-summary": '', "aria-live": 'polite', className: 'flex min-h-11 flex-wrap items-center gap-3 border-b border-border px-4 py-2 text-sm', children: summaryNode })) : null, loading ? (_jsx("div", { className: 'flex flex-col gap-2 p-4', children: Array.from({ length: 5 }).map((_, i) => (_jsx(Skeleton, { className: 'h-10 w-full' }, `dt-skeleton-${i}`))) })) : rows.length === 0 && windowTotal === 0 ? (_jsx("div", { className: 'p-2', children: emptyState ?? _jsx(EmptyState, { title: 'No results' }) })) : (_jsxs("div", { ref: scrollRef, "data-cb-table-scroll": bodyScrolls ? '' : undefined,
|
|
750
|
+
// guardrails-ignore no-raw-overflow: dual-axis table scroll container — sticky header pins to it; ScrollArea cannot own horizontal table scroll.
|
|
751
|
+
className: cn(bodyScrolls && 'min-h-0 flex-1 overflow-auto', scrollClassName), children: [selectionEnabled && (viewMode === 'card' || mobileCards) ? (_jsx("div", { "data-cb-table-select-all": '', className: cn('flex items-center px-2 py-1', viewMode !== 'card' && '@xl:hidden'), children: headerCheckbox('Select all') })) : null, viewMode === 'card' ? (_jsx(CardView, { columns: cardColumns, data: rows.map((row) => row.original), getRowId: getRowId, onRowClick: onRowClick, dragPropsFor: getRowDrag ? dragPropsFor : undefined, selectFor: selectionEnabled
|
|
570
752
|
? (_row, index) => {
|
|
571
753
|
const tsRow = rows[index];
|
|
572
754
|
return tsRow ? rowCheckbox(tsRow) : null;
|
|
573
755
|
}
|
|
574
|
-
: undefined, cardMinWidth: cardMinWidth, className: 'p-2' })) : (_jsxs(_Fragment, { children: [mobileCards && (_jsx("div", { "data-cb-table-cards": '', className: 'flex flex-col gap-3 @xl:hidden', children: rows.map((row) => (_jsx(MobileRowCard, { row: row, columns: cardColumns, className: rowClassName?.(row.original), onRowClick: onRowClick, dragProps: dragPropsFor(row.original), select: selectionEnabled ? rowCheckbox(row) : undefined, actions: hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) })) : null, expanded: renderExpanded && expandedRowId != null && row.id === expandedRowId
|
|
756
|
+
: undefined, cardMinWidth: cardMinWidth, className: 'p-2' })) : (_jsxs(_Fragment, { children: [mobileCards && (_jsx("div", { "data-cb-table-cards": '', className: 'flex flex-col gap-3 @xl:hidden', children: rows.map((row) => (_jsx(MobileRowCard, { row: row, columns: cardColumns, className: rowClassName?.(row.original), onRowClick: onRowClick, dragProps: mergeRowProps(dragPropsFor(row.original), getRowProps?.(row.original, windowStart + row.index)), select: selectionEnabled ? rowCheckbox(row) : undefined, actions: hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) })) : null, expanded: renderExpanded && expandedRowId != null && row.id === expandedRowId
|
|
575
757
|
? renderExpanded(row.original)
|
|
576
758
|
: null }, row.id))) })), mobileCards && (_jsx("div", { "data-cb-table-list": '', className: 'hidden @xl:block @3xl:hidden', children: (() => {
|
|
577
759
|
const [title, ...rest] = cardColumns;
|
|
@@ -601,44 +783,59 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
601
783
|
const header = (_jsxs("div", { "data-cb-table-list-header": '', className: 'grid items-center gap-3 border-b border-border px-4 py-2 text-caption font-bold uppercase tracking-wider text-muted-foreground', style: { gridTemplateColumns: template }, children: [selectionEnabled ? _jsx("span", { className: 'flex justify-center', children: headerCheckbox() }) : null, _jsx("span", { children: title?.label ?? '' }), rest.map((col) => (_jsx("span", { className: cn(resolveColumnAlign(col) === 'right' && 'text-right'), children: col.label }, col.key))), hasActions ? _jsx("span", { "aria-hidden": true }) : null] }));
|
|
602
784
|
const body = rows.map((row) => {
|
|
603
785
|
const isExpanded = renderExpanded != null && expandedRowId != null && row.id === expandedRowId;
|
|
604
|
-
return (_jsxs(Fragment, { children: [_jsxs("div", { ...
|
|
786
|
+
return (_jsxs(Fragment, { children: [_jsxs("div", { ...mergeRowProps({
|
|
787
|
+
...dragPropsFor(row.original),
|
|
788
|
+
className: cn('grid items-center gap-3 px-4 py-3 transition-colors hover:bg-muted/40', getRowDrag && 'cursor-grab active:cursor-grabbing', rowClassName?.(row.original)),
|
|
789
|
+
style: { gridTemplateColumns: template },
|
|
790
|
+
}, getRowProps?.(row.original, windowStart + row.index)), children: [selectionEnabled ? (_jsx("div", { ...selectCellProps(row), className: '-my-3 flex self-stretch items-center justify-center', children: rowCheckbox(row) })) : null, _jsx("div", { className: 'min-w-0', children: title ? (_jsx(CellRenderer, { col: title, value: row.original[title.key], row: row.original })) : null }), rest.map((col) => (_jsx("div", { className: cn(resolveColumnAlign(col) === 'right' && 'justify-self-end text-right'), children: _jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }) }, col.key))), hasActions ? (_jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) })) : null] }), isExpanded ? (_jsx("div", { className: 'bg-muted/20 px-4 pt-1 pb-3', children: _jsx("div", { className: 'rounded-lg border border-border bg-card p-3 shadow-raised', children: renderExpanded(row.original) }) })) : null] }, row.id));
|
|
605
791
|
});
|
|
606
792
|
return (_jsxs(_Fragment, { children: [header, _jsx("div", { className: 'divide-y divide-border', children: body })] }));
|
|
607
793
|
})() })), _jsx("div", { ref: bodyScrolls ? undefined : gridFade.ref, onScroll: bodyScrolls ? undefined : gridFade.onScroll, style: bodyScrolls ? undefined : gridFade.maskStyle,
|
|
608
794
|
// guardrails-ignore no-raw-overflow: content-sized horizontal table scroll, not a page-level vertical scroll region — see reasoning above. Under scrollBody the OUTER wrapper owns both axes (a nested overflow ancestor would un-stick the sticky header).
|
|
609
|
-
className: cn(!bodyScrolls && 'overflow-x-auto', mobileCards && 'hidden @3xl:block'), children: _jsxs("table", { className: cn('w-full border-collapse text-sm', fixedLayout && 'table-fixed'), style: tableMinWidth != null ? { minWidth: tableMinWidth } : undefined, children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { children: table.getHeaderGroups().map((hg) => (_jsxs("tr", { children: [
|
|
610
|
-
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
? '
|
|
619
|
-
:
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
795
|
+
className: cn(!bodyScrolls && 'overflow-x-auto', mobileCards && 'hidden @3xl:block'), children: _jsxs("table", { className: cn('w-full border-collapse text-sm', fixedLayout && 'table-fixed'), style: tableMinWidth != null ? { minWidth: tableMinWidth } : undefined, children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { children: table.getHeaderGroups().map((hg) => (_jsxs("tr", { children: [(() => {
|
|
796
|
+
// `ColDef.leading` columns sit BEFORE the checkbox column; see its doc.
|
|
797
|
+
const renderHeader = (h, headerIndex) => {
|
|
798
|
+
const sorted = h.column.getIsSorted();
|
|
799
|
+
const meta = h.column.columnDef.meta;
|
|
800
|
+
const width = meta?.width ?? h.column.columnDef.size;
|
|
801
|
+
const align = meta?.align;
|
|
802
|
+
const frozen = stickyFirstColumn === true && headerIndex === 0;
|
|
803
|
+
return (_jsx("th", { scope: 'col', "aria-sort": sorted === 'asc'
|
|
804
|
+
? 'ascending'
|
|
805
|
+
: sorted === 'desc'
|
|
806
|
+
? 'descending'
|
|
807
|
+
: undefined, style: width != null || meta?.minWidth != null
|
|
808
|
+
? { width, minWidth: meta?.minWidth }
|
|
809
|
+
: undefined, className: cn(
|
|
810
|
+
// Metadata treatment: headers are chrome, not data — small,
|
|
811
|
+
// bold, uppercase, tracked, muted (STANDARDS §5c) — via the
|
|
812
|
+
// explicit secondary token, never an opacity fade (the
|
|
813
|
+
// design manual bans opacity-blended text). The border
|
|
814
|
+
// rides the <th> (not the <tr>) so it pins with stickyHeader.
|
|
815
|
+
'border-b border-border px-4 text-caption font-bold uppercase tracking-wider text-muted-foreground', cellPadY, align === 'right' ? 'text-right' : 'text-left',
|
|
816
|
+
// Sticky pins per-<th> (a <tr> can't stick); needs its own
|
|
817
|
+
// near-opaque bg so rows scrolling beneath never bleed through.
|
|
818
|
+
stickyHeader &&
|
|
819
|
+
'sticky top-0 z-10 bg-card/95 backdrop-blur-sm',
|
|
820
|
+
// The corner cell must outrank BOTH pinned axes, or the
|
|
821
|
+
// next header slides over the frozen one. It is opaque
|
|
822
|
+
// for the same reason the body cells are.
|
|
823
|
+
frozen &&
|
|
824
|
+
(selectionEnabled
|
|
825
|
+
? 'sticky left-11 bg-card shadow-[1px_0_0_0_var(--color-border)]'
|
|
826
|
+
: 'sticky left-0 bg-card shadow-[1px_0_0_0_var(--color-border)]'), frozen && (stickyHeader ? 'z-30' : 'z-20'), meta?.hideBelow && HIDE_BELOW_CLASS[meta.hideBelow]), children: h.isPlaceholder
|
|
827
|
+
? null
|
|
828
|
+
: flexRender(h.column.columnDef.header, h.getContext()) }, h.id));
|
|
829
|
+
};
|
|
830
|
+
const isLeading = (h) => h.column.columnDef.meta?.leading === true;
|
|
831
|
+
return (_jsxs(_Fragment, { children: [hg.headers.filter(isLeading).map((h) => renderHeader(h, -1)), selectionEnabled ? (_jsx("th", { scope: 'col', className: cn('w-11 border-b border-border px-0 text-center', stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm', stickyFirstColumn && 'sticky left-0 bg-card', stickyFirstColumn && (stickyHeader ? 'z-30' : 'z-20')), children: headerCheckbox() })) : null, hg.headers.filter((h) => !isLeading(h)).map(renderHeader)] }));
|
|
832
|
+
})(), hasActions ? (_jsx("th", { className: cn('w-20 border-b border-border px-2', stickyHeader && 'sticky top-0 z-10 bg-card/95 backdrop-blur-sm'), "aria-label": 'Row actions' })) : null] }, hg.id))) }), _jsxs("tbody", { children: [padTop > 0 ? (_jsx("tr", { "aria-hidden": true, style: { height: padTop }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null, bodyRows.map((row, sliceIndex) => {
|
|
833
|
+
if (row === null) {
|
|
834
|
+
// A row the server window has not loaded yet: the estimated height, so
|
|
835
|
+
// the spacer arithmetic holds, and a skeleton line rather than a gap.
|
|
836
|
+
const index = virtualRows[sliceIndex].index;
|
|
837
|
+
return (_jsx("tr", { "data-index": index, "data-cb-placeholder": '', "aria-hidden": true, className: cn('border-b border-border', index % 2 === 1 && 'bg-muted/50'), style: { height: estimateRowHeight }, children: _jsx("td", { colSpan: visibleColumnCount, className: 'px-3', children: _jsx(Skeleton, { className: 'h-3', style: { width: '40%' } }) }) }, `cb-placeholder-${index}`));
|
|
838
|
+
}
|
|
642
839
|
/* 🔴 The ZEBRA index is the row's ABSOLUTE position, not its
|
|
643
840
|
position in the rendered slice. Off by that, the stripes
|
|
644
841
|
would repaint themselves every time the window moved — the
|
|
@@ -646,61 +843,78 @@ export function DataTable({ columns, data, getRowId, onRowClick, onRowEdit, onRo
|
|
|
646
843
|
fault rather than as a guide. */
|
|
647
844
|
const rowIndex = virtualEnabled ? virtualRows[sliceIndex].index : sliceIndex;
|
|
648
845
|
const isExpanded = renderExpanded != null && expandedRowId != null && row.id === expandedRowId;
|
|
649
|
-
return (_jsxs(Fragment, { children: [_jsxs("tr", {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
//
|
|
653
|
-
//
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
'cursor-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
846
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", {
|
|
847
|
+
// 🔴 The virtualizer reads its real height back off this node, and
|
|
848
|
+
// `data-index` is how it knows WHICH row it measured — without it
|
|
849
|
+
// every measurement lands on index 0 and the spacer never grows.
|
|
850
|
+
// (The ref stays OUT of the merge: `getRowProps` cannot take it.)
|
|
851
|
+
ref: virtualEnabled && fixedRowHeight === null ? rowVirtualizer.measureElement : undefined, ...mergeRowProps({
|
|
852
|
+
...(virtualEnabled ? { 'data-index': rowIndex } : {}),
|
|
853
|
+
style: fixedRowHeight === null ? undefined : { height: fixedRowHeight },
|
|
854
|
+
'data-expanded': isExpanded || undefined,
|
|
855
|
+
...dragPropsFor(row.original),
|
|
856
|
+
className: cn(getRowDrag && 'cursor-grab active:cursor-grabbing',
|
|
857
|
+
// Always-on hover wash: wide rows need a trace line for the
|
|
858
|
+
// eye even when the row itself isn't clickable. Zebra fill on
|
|
859
|
+
// every second row gives the eye a guide path across wide
|
|
860
|
+
// columns (design manual §4 zebra-vs-outline rule) — indexed
|
|
861
|
+
// off the DATA row, not DOM position, so an expansion row
|
|
862
|
+
// never flips the rhythm.
|
|
863
|
+
'border-b border-border transition-colors last:border-0 hover:bg-muted', rowIndex % 2 === 1 && 'bg-muted/50', rowClassName?.(row.original), isExpanded && 'border-0 bg-muted/30', onRowClick &&
|
|
864
|
+
'cursor-pointer hover:bg-muted focus-visible:bg-muted focus-visible:outline-none'),
|
|
865
|
+
tabIndex: onRowClick ? 0 : undefined,
|
|
866
|
+
onClick: onRowClick ? (event) => onRowClick(row.original, event) : undefined,
|
|
867
|
+
onKeyDown: onRowClick
|
|
868
|
+
? (e) => {
|
|
869
|
+
if (e.key === 'Enter')
|
|
870
|
+
onRowClick(row.original);
|
|
871
|
+
}
|
|
872
|
+
: undefined,
|
|
873
|
+
}, getRowProps?.(row.original, rowIndex)), children: [(() => {
|
|
874
|
+
const cells = row.getVisibleCells();
|
|
875
|
+
const isLeading = (cell) => cell.column.columnDef.meta?.leading === true;
|
|
876
|
+
const renderCell = (cell, cellIndex) => {
|
|
877
|
+
const frozen = stickyFirstColumn === true && cellIndex === 0;
|
|
878
|
+
return (_jsx("td", { style: frozen
|
|
879
|
+
? {
|
|
880
|
+
// The zebra step, made OPAQUE — see the prop doc.
|
|
881
|
+
// A custom property rather than `background:`
|
|
882
|
+
// directly, so the value survives any CSS parser
|
|
883
|
+
// that has not learned `color-mix` (happy-dom) and
|
|
884
|
+
// the gate can read it back.
|
|
885
|
+
'--cb-frozen-bg': rowIndex % 2 === 1
|
|
886
|
+
? 'color-mix(in srgb, var(--color-muted) 50%, var(--color-card))'
|
|
887
|
+
: 'var(--color-card)',
|
|
888
|
+
}
|
|
889
|
+
: undefined, className: cn('px-4 align-middle', cellPadY, frozen &&
|
|
890
|
+
(selectionEnabled
|
|
891
|
+
? 'sticky left-11 z-10 bg-[var(--cb-frozen-bg)] shadow-[1px_0_0_0_var(--color-border)]'
|
|
892
|
+
: 'sticky left-0 z-10 bg-[var(--cb-frozen-bg)] shadow-[1px_0_0_0_var(--color-border)]'),
|
|
893
|
+
// A rigid track cannot widen for its content, so an
|
|
894
|
+
// unbreakable token (a task id, a path, a hash) would
|
|
895
|
+
// otherwise run straight across the next column's value.
|
|
896
|
+
// Break inside the word instead — the row grows taller,
|
|
897
|
+
// which is recoverable; overlapping text is not. Not
|
|
898
|
+
// `overflow: hidden`: a cell may host a menu/tooltip that
|
|
899
|
+
// has to escape its bounds.
|
|
900
|
+
fixedLayout && '[overflow-wrap:anywhere]', cell.column.columnDef.meta?.align === 'right' && 'text-right', cell.column.columnDef.meta?.hideBelow &&
|
|
901
|
+
HIDE_BELOW_CLASS[cell.column.columnDef.meta.hideBelow]), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
902
|
+
};
|
|
903
|
+
return (_jsxs(_Fragment, { children: [cells.filter(isLeading).map((cell) => renderCell(cell, -1)), selectionEnabled ? (_jsx("td", { ...selectCellProps(row), style: stickyFirstColumn
|
|
904
|
+
? {
|
|
905
|
+
'--cb-frozen-bg': rowIndex % 2 === 1
|
|
906
|
+
? 'color-mix(in srgb, var(--color-muted) 50%, var(--color-card))'
|
|
907
|
+
: 'var(--color-card)',
|
|
908
|
+
}
|
|
909
|
+
: undefined, className: cn('w-11 cursor-pointer px-0 text-center align-middle', stickyFirstColumn && 'sticky left-0 z-10 bg-[var(--cb-frozen-bg)]'), children: rowCheckbox(row) })) : null, cells.filter((cell) => !isLeading(cell)).map(renderCell)] }));
|
|
910
|
+
})(), hasActions ? (_jsx("td", { className: 'px-2 py-2', children: _jsx(RowActions, { row: row.original, onEdit: onRowEdit, onDelete: onRowDelete, menu: rowMenu?.(row.original) }) })) : null] }), isExpanded ? (_jsx("tr", { className: 'border-b border-border bg-muted/20', ref: (el) => {
|
|
697
911
|
if (el && scrolledExpansionRef.current !== row.id) {
|
|
698
912
|
scrolledExpansionRef.current = row.id;
|
|
699
913
|
// A row expanded near the fold must reveal its panel.
|
|
700
914
|
el.scrollIntoView({ block: 'nearest' });
|
|
701
915
|
}
|
|
702
916
|
}, children: _jsx("td", { colSpan: visibleColumnCount, className: 'p-0', children: _jsx("div", { className: 'animate-in fade-in slide-in-from-top-1 px-4 pt-1 pb-3 duration-200', children: _jsx("div", { className: 'rounded-lg border border-border bg-card p-3 shadow-raised', children: renderExpanded(row.original) }) }) }) })) : null] }, row.id));
|
|
703
|
-
}), padBottom > 0 ? (_jsx("tr", { "aria-hidden": true, style: { height: padBottom }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null] })] }) })] })), bodyScrolls && (_jsx("div", { "aria-hidden": true, className: 'pointer-events-none sticky bottom-0 -mt-6 h-6 w-full bg-gradient-to-t from-card to-transparent' }))] })), !loading && rows.length > 0 && !paginationHidden ? (_jsx(
|
|
917
|
+
}), padBottom > 0 ? (_jsx("tr", { "aria-hidden": true, style: { height: padBottom }, children: _jsx("td", { colSpan: visibleColumnCount }) })) : null] })] }) })] })), bodyScrolls && (_jsx("div", { "aria-hidden": true, className: 'pointer-events-none sticky bottom-0 -mt-6 h-6 w-full bg-gradient-to-t from-card to-transparent' }))] })), !loading && rows.length > 0 && !paginationHidden ? (_jsx(PaginationPart, { table: table, pageSizeOptions: pageSizeOptions, fallback: _jsx(PaginationPlaceholder, {}) })) : null] }));
|
|
704
918
|
}
|
|
705
919
|
/**
|
|
706
920
|
* One row rendered as a phone card: first visible column = the title zone (unlabeled,
|
|
@@ -723,6 +937,6 @@ function MobileRowCard({ row, columns, className, onRowClick, dragProps, select,
|
|
|
723
937
|
const [titleSideCol, ...restInline] = inline;
|
|
724
938
|
const body = (_jsxs("div", { className: cn('flex flex-col gap-2', actions && 'pr-10'), children: [title ? (_jsxs("div", { className: 'flex items-start justify-between gap-2 text-sm', children: [_jsx("div", { className: 'min-w-0 flex-1', children: _jsx(CellRenderer, { col: title, value: row.original[title.key], row: row.original }) }), titleSideCol ? (_jsx("span", { className: 'shrink-0', children: _jsx(CellRenderer, { col: titleSideCol, value: row.original[titleSideCol.key], row: row.original }) })) : null] })) : null, restInline.length > 0 && (_jsx("div", { className: 'flex flex-wrap items-center gap-2', children: restInline.map((col) => (_jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }, col.key))) })), detail.length > 0 && (_jsx("dl", { className: 'flex flex-col gap-1.5', children: detail.map((col) => (_jsxs("div", { className: 'flex items-baseline justify-between gap-3', children: [_jsx("dt", { className: 'cb-meta-label shrink-0', children: col.label }), _jsx("dd", { className: 'min-w-0 text-right text-sm', children: _jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }) })] }, col.key))) }))] }));
|
|
725
939
|
const main = onRowClick ? (_jsx(CardTapTarget, { onClick: () => onRowClick(row.original), className: '-m-1 rounded-md p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', children: body })) : (body);
|
|
726
|
-
return (_jsxs(Surface, { level: 'raised', pad: 'sm', radius: 'lg', ...dragProps, className: cn('relative isolate', dragProps?.draggable && 'cursor-grab', className), children: [actions ? _jsx("div", { className: 'absolute top-3 right-3 z-10', children: actions }) : null, select == null ? main : (_jsxs("div", { className: 'flex items-start gap-1', children: [_jsx("div", { className: '-mt-2 -ml-2 shrink-0', children: select }), _jsx("div", { className: 'min-w-0 flex-1', children: main })] })), footer.length > 0 && (_jsx("div", { className: 'mt-3 flex items-center justify-end gap-2 border-t border-border/60 pt-3', children: footer.map((col) => (_jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }, col.key))) })), expanded ? _jsx("div", { className: 'mt-4', children: expanded }) : null] }));
|
|
940
|
+
return (_jsxs(Surface, { level: 'raised', pad: 'sm', radius: 'lg', ...dragProps, className: cn('relative isolate', dragProps?.draggable && 'cursor-grab', className, dragProps?.className), children: [actions ? _jsx("div", { className: 'absolute top-3 right-3 z-10', children: actions }) : null, select == null ? main : (_jsxs("div", { className: 'flex items-start gap-1', children: [_jsx("div", { className: '-mt-2 -ml-2 shrink-0', children: select }), _jsx("div", { className: 'min-w-0 flex-1', children: main })] })), footer.length > 0 && (_jsx("div", { className: 'mt-3 flex items-center justify-end gap-2 border-t border-border/60 pt-3', children: footer.map((col) => (_jsx(CellRenderer, { col: col, value: row.original[col.key], row: row.original }, col.key))) })), expanded ? _jsx("div", { className: 'mt-4', children: expanded }) : null] }));
|
|
727
941
|
}
|
|
728
942
|
//# sourceMappingURL=DataTable.js.map
|