@tristan2828/ui-foundation 4.54.0 → 4.55.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.
|
@@ -117,6 +117,21 @@ export type DataTableProps<TData extends Record<string, unknown>> = {
|
|
|
117
117
|
* layout's alone: a card layout's cards are as they are.
|
|
118
118
|
*/
|
|
119
119
|
detailPanel?: (row: TData) => ReactNode;
|
|
120
|
+
/**
|
|
121
|
+
* What a click on a row (or Enter on it) does instead of picking it for
|
|
122
|
+
* the `detailPanel`: open its page, say. A link or control inside the row
|
|
123
|
+
* keeps its own click, and the panel is then opened from the app's own
|
|
124
|
+
* control through `detailRow`. Left out, a row click picks the row.
|
|
125
|
+
*/
|
|
126
|
+
onRowClick?: (row: TData) => void;
|
|
127
|
+
/**
|
|
128
|
+
* The row the `detailPanel` shows, controlled: its row id (`getRowId`), or
|
|
129
|
+
* null for none. With `onDetailRowChange` it lets a control of the app's
|
|
130
|
+
* own (a button in a row) open the panel. Left out, the table keeps the
|
|
131
|
+
* pick itself.
|
|
132
|
+
*/
|
|
133
|
+
detailRow?: string | null;
|
|
134
|
+
onDetailRowChange?: (rowId: string | null) => void;
|
|
120
135
|
/** The panel's heading and landmark name: "Details" unless set. */
|
|
121
136
|
detailLabel?: string;
|
|
122
137
|
/**
|
|
@@ -178,6 +193,7 @@ export type DataTableGrouping<TData> = {
|
|
|
178
193
|
options: readonly {
|
|
179
194
|
value: string;
|
|
180
195
|
label: string;
|
|
196
|
+
icon?: ReactNode;
|
|
181
197
|
}[];
|
|
182
198
|
/**
|
|
183
199
|
* The row's value: one option's value, or a list of them for a
|
|
@@ -224,4 +240,4 @@ export type DataTableTotals = {
|
|
|
224
240
|
*/
|
|
225
241
|
cells?: Partial<Record<string, ReactNode>>;
|
|
226
242
|
};
|
|
227
|
-
export declare function DataTable<TData extends Record<string, unknown>>({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width, groupBy, groupSummary, header: showHeader, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, groupings, grouping: groupingProp, onGroupingChange, detailPanel, detailLabel, }: DataTableProps<TData>): import("react").JSX.Element;
|
|
243
|
+
export declare function DataTable<TData extends Record<string, unknown>>({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width, groupBy, groupSummary, header: showHeader, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, groupings, grouping: groupingProp, onGroupingChange, detailPanel, onRowClick: onRowClickProp, detailRow, onDetailRowChange, detailLabel, }: DataTableProps<TData>): import("react").JSX.Element;
|
|
@@ -260,7 +260,7 @@ function useBottomScrollbar(containerRef, isSuccessView) {
|
|
|
260
260
|
}, [containerRef, isSuccessView, isOverflowing]);
|
|
261
261
|
return { barRef, isOverflowing, scrollWidth };
|
|
262
262
|
}
|
|
263
|
-
export function DataTable({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width = 'full', groupBy, groupSummary, header: showHeader = true, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, groupings, grouping: groupingProp, onGroupingChange, detailPanel, detailLabel = 'Details', }) {
|
|
263
|
+
export function DataTable({ columns, data, total, page, pageSize, onPageChange, sorting, onSortingChange, isLoading, error, onRetry, emptyTitle, emptyDescription, emptyAction, toolbar, getRowId, pinFirstColumn, pinLastColumn, enableSortingRemoval, visibleColumns, width = 'full', groupBy, groupSummary, header: showHeader = true, highlightRow, totals, layouts, layout: layoutProp, onLayoutChange, tableLayout, groupings, grouping: groupingProp, onGroupingChange, detailPanel, onRowClick: onRowClickProp, detailRow, onDetailRowChange, detailLabel = 'Details', }) {
|
|
264
264
|
// The switch's own state, for a table that doesn't pass `layout`.
|
|
265
265
|
const [ownLayout, setOwnLayout] = useState('table');
|
|
266
266
|
const requestedLayout = layoutProp ?? ownLayout;
|
|
@@ -275,6 +275,10 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
275
275
|
// doesn't pass `grouping`.
|
|
276
276
|
const [ownGrouping, setOwnGrouping] = useState('');
|
|
277
277
|
const activeGrouping = groupings?.find((candidate) => candidate.id === (groupingProp ?? ownGrouping));
|
|
278
|
+
// Each group's icon (an option's `icon`), by the group's key.
|
|
279
|
+
const groupIcons = activeGrouping
|
|
280
|
+
? new Map(activeGrouping.options.flatMap((option) => (option.icon ? [[option.value, option.icon]] : [])))
|
|
281
|
+
: undefined;
|
|
278
282
|
const setGrouping = (next) => {
|
|
279
283
|
if (groupingProp === undefined)
|
|
280
284
|
setOwnGrouping(next);
|
|
@@ -285,7 +289,20 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
285
289
|
const [folds, setFolds] = useState({ grouping: '', keys: EMPTY_KEYS });
|
|
286
290
|
const folded = activeGrouping && folds.grouping === activeGrouping.id ? folds.keys : EMPTY_KEYS;
|
|
287
291
|
// The picked row (`detailPanel`) and the place it was picked in.
|
|
288
|
-
const [
|
|
292
|
+
const [ownSelection, setOwnSelection] = useState(null);
|
|
293
|
+
// Controlled (`detailRow`): the app's row id, at the place picked in when
|
|
294
|
+
// that's the same row, else the row's first place.
|
|
295
|
+
const selection = detailRow === undefined
|
|
296
|
+
? ownSelection
|
|
297
|
+
: detailRow === null
|
|
298
|
+
? null
|
|
299
|
+
: ownSelection?.rowId === detailRow
|
|
300
|
+
? ownSelection
|
|
301
|
+
: { key: detailRow, rowId: detailRow };
|
|
302
|
+
const setSelection = (next) => {
|
|
303
|
+
setOwnSelection(next);
|
|
304
|
+
onDetailRowChange?.(next?.rowId ?? null);
|
|
305
|
+
};
|
|
289
306
|
const isWide = useIsWide();
|
|
290
307
|
// Editing in place in the rows (cell pattern 18, "In a table's rows"):
|
|
291
308
|
// a cell that renders an <EditableValue> edits where it's shown, one at
|
|
@@ -407,12 +424,18 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
407
424
|
return;
|
|
408
425
|
const control = target.closest(ROW_CONTROL_SELECTOR);
|
|
409
426
|
if (control && event.currentTarget.contains(control)) {
|
|
410
|
-
if (!control.matches('a[href]'))
|
|
427
|
+
if (!control.matches('a[href]') || onRowClickProp)
|
|
411
428
|
return;
|
|
412
429
|
// In the capture phase, so react-router's Link sees it prevented and
|
|
413
430
|
// doesn't navigate.
|
|
414
431
|
event.preventDefault();
|
|
415
432
|
}
|
|
433
|
+
if (onRowClickProp) {
|
|
434
|
+
const row = rows.find((candidate) => candidate.id === place.rowId);
|
|
435
|
+
if (row)
|
|
436
|
+
onRowClickProp(row.original);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
416
439
|
setSelection(place);
|
|
417
440
|
};
|
|
418
441
|
const onListKeyDown = (event) => {
|
|
@@ -437,6 +460,12 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
437
460
|
if (!place)
|
|
438
461
|
return;
|
|
439
462
|
event.preventDefault();
|
|
463
|
+
if (onRowClickProp) {
|
|
464
|
+
const row = rows.find((candidate) => candidate.id === place.rowId);
|
|
465
|
+
if (row)
|
|
466
|
+
onRowClickProp(row.original);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
440
469
|
setSelection(place);
|
|
441
470
|
}
|
|
442
471
|
else if (event.key === 'Escape' && selectedRow) {
|
|
@@ -468,7 +497,7 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
468
497
|
return;
|
|
469
498
|
event.preventDefault();
|
|
470
499
|
closeDetail();
|
|
471
|
-
}, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h2", { className: "type-label text-muted-foreground", children: detailLabel }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Close", onClick: closeDetail, children: _jsx(XIcon, { "aria-hidden": "true" }) })] }), detailPanel(selectedRow.original)] })) : null, children: [cardLayout ? (_jsx(Cards, { layout: cardLayout, runs: runs, rows: rows, folded: folded, onToggleFold: toggleFold, groupSummary: groupSummary })) : (_jsxs(Table, { "data-state": "success", containerRef: containerRef, onKeyDown: detailPanel ? onListKeyDown : undefined,
|
|
500
|
+
}, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("h2", { className: "type-label text-muted-foreground", children: detailLabel }), _jsx(Button, { variant: "ghost", size: "icon-sm", "aria-label": "Close", onClick: closeDetail, children: _jsx(XIcon, { "aria-hidden": "true" }) })] }), detailPanel(selectedRow.original)] })) : null, children: [cardLayout ? (_jsx(Cards, { layout: cardLayout, runs: runs, rows: rows, folded: folded, onToggleFold: toggleFold, groupSummary: groupSummary, groupIcons: groupIcons })) : (_jsxs(Table, { "data-state": "success", containerRef: containerRef, onKeyDown: detailPanel ? onListKeyDown : undefined,
|
|
472
501
|
// With the sticky bar below as the visible scrollbar, hide the
|
|
473
502
|
// container's own. It stays overflow-x-auto and fully
|
|
474
503
|
// scrollable (wheel, trackpad, drag, and the sync effect); it
|
|
@@ -499,7 +528,7 @@ export function DataTable({ columns, data, total, page, pageSize, onPageChange,
|
|
|
499
528
|
: 'none', children: !showHeader ? (_jsx("span", { className: "sr-only", children: content })) : pinFirstColumn && index === 0 ? (_jsx("div", { className: PINNED_FIRST_CONTENT_CLASS, children: content })) : (content) }, header.id));
|
|
500
529
|
}) }, headerGroup.id))) }), runs ? (runs.map((run, runIndex) => {
|
|
501
530
|
const isFolded = folded.has(run.key);
|
|
502
|
-
return (_jsxs(TableBody, { "data-slot": "table-group", "data-folded": isFolded || undefined, children: [_jsx(TableRow, { "data-slot": "table-group-heading", className: "hover:bg-transparent has-aria-expanded:bg-transparent", children: _jsx(TableHead, { scope: "rowgroup", colSpan: visibleColumnCount, className: GROUP_HEADING_CLASS, children: _jsxs("span", { className: "sticky left-(--table-cell-px) inline-flex items-baseline gap-2", children: [_jsx(GroupLabel, { run: run, isFolded: isFolded, onToggle: () => toggleFold(run.key) }), groupSummary && (_jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: groupSummary(run.rows.map((row) => row.original), { key: run.key, label: run.label }) }))] }) }) }), _jsx("tr", { hidden: true, "aria-hidden": "true", "data-slot": "table-group-stripe-offset" }), !isFolded && run.rows.map((row) => renderRow(row, placeKey(run.key, row.id)))] }, `${runIndex}:${run.key}`));
|
|
531
|
+
return (_jsxs(TableBody, { "data-slot": "table-group", "data-folded": isFolded || undefined, children: [_jsx(TableRow, { "data-slot": "table-group-heading", className: "hover:bg-transparent has-aria-expanded:bg-transparent", children: _jsx(TableHead, { scope: "rowgroup", colSpan: visibleColumnCount, className: GROUP_HEADING_CLASS, children: _jsxs("span", { className: "sticky left-(--table-cell-px) inline-flex items-baseline gap-2", children: [_jsx(GroupLabel, { run: run, icon: groupIcons?.get(run.key), isFolded: isFolded, onToggle: () => toggleFold(run.key) }), groupSummary && (_jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: groupSummary(run.rows.map((row) => row.original), { key: run.key, label: run.label }) }))] }) }) }), _jsx("tr", { hidden: true, "aria-hidden": "true", "data-slot": "table-group-stripe-offset" }), !isFolded && run.rows.map((row) => renderRow(row, placeKey(run.key, row.id)))] }, `${runIndex}:${run.key}`));
|
|
503
532
|
})) : (_jsx(TableBody, { children: rows.map((row) => renderRow(row)) })), totals && (_jsx(TableFooter, { className: TOTALS_FOOTER_CLASS, children: _jsx(TableRow, { "data-slot": "table-totals", className: "hover:bg-transparent", children: visibleLeafColumns.map((column, index) => {
|
|
504
533
|
const className = cn(pinFirstColumn && index === 0 && PINNED_TOTALS_LABEL_CLASS, pinLastColumn && index === visibleColumnCount - 1 && PINNED_LAST_COLUMN_CLASS, alignClass(column.columnDef.meta?.align));
|
|
505
534
|
return index === 0 ? (_jsx(TableHead, { scope: "row", className: cn(TOTALS_LABEL_CLASS, className), children: pinFirstColumn ? (_jsx("div", { className: PINNED_FIRST_CONTENT_CLASS, children: totals.label })) : (totals.label) }, column.id)) : (_jsx(TableCell, { className: className, children: totals.cells?.[column.id] }, column.id));
|
|
@@ -518,17 +547,19 @@ function DetailFrame({ enabled, aside, children }) {
|
|
|
518
547
|
}
|
|
519
548
|
// A group's heading text: under a grouping, a button that folds the group,
|
|
520
549
|
// with its row count; under `groupBy`, the label alone.
|
|
521
|
-
function GroupLabel({ run, isFolded, onToggle, }) {
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
550
|
+
function GroupLabel({ run, icon, isFolded, onToggle, }) {
|
|
551
|
+
const label = (_jsxs(_Fragment, { children: [icon ? (_jsx("span", { "aria-hidden": "true", className: "inline-flex shrink-0 items-center self-center", children: icon })) : null, _jsx("span", { className: "type-label text-foreground", children: run.label })] }));
|
|
552
|
+
if (!run.foldable) {
|
|
553
|
+
return icon ? (_jsx("span", { className: "inline-flex items-baseline gap-1.5", children: label })) : (_jsx("span", { className: "type-label text-foreground", children: run.label }));
|
|
554
|
+
}
|
|
555
|
+
return (_jsxs("button", { type: "button", "aria-expanded": !isFolded, onClick: onToggle, className: GROUP_TOGGLE_CLASS, children: [_jsx(ChevronDownIcon, { "aria-hidden": "true", className: cn('size-4 self-center text-muted-foreground transition-transform', isFolded && '-rotate-90') }), label, _jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: run.rows.length })] }));
|
|
525
556
|
}
|
|
526
|
-
function Cards({ layout, runs, rows, folded, onToggleFold, groupSummary, }) {
|
|
557
|
+
function Cards({ layout, runs, rows, folded, onToggleFold, groupSummary, groupIcons, }) {
|
|
527
558
|
const list = (items) => (_jsx("ul", { role: "list", className: cn(layout.arrange === 'list' ? CARD_LIST_CLASS : ['grid', CARD_GRID_CLASS[layout.size ?? 'md'], CARD_GRID_GAP_CLASS]), children: items.map((row) => (_jsx("li", { "data-slot": "data-table-card", className: "min-w-0", children: layout.renderCard(row.original) }, row.id))) }));
|
|
528
559
|
return (_jsx("div", { className: "@container/data-table-cards flex flex-col gap-6", "data-state": "success", "data-slot": "data-table-cards", children: runs
|
|
529
560
|
? runs.map((run, runIndex) => {
|
|
530
561
|
const isFolded = folded.has(run.key);
|
|
531
|
-
return (_jsxs("section", { "data-slot": "table-group", "data-folded": isFolded || undefined, className: "flex flex-col gap-3", children: [_jsxs("h2", { "data-slot": "table-group-heading", className: "flex items-baseline gap-2 border-b border-border pb-1.5", children: [_jsx(GroupLabel, { run: run, isFolded: isFolded, onToggle: () => onToggleFold(run.key) }), groupSummary && (_jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: groupSummary(run.rows.map((row) => row.original), { key: run.key, label: run.label }) }))] }), !isFolded && list(run.rows)] }, `${runIndex}:${run.key}`));
|
|
562
|
+
return (_jsxs("section", { "data-slot": "table-group", "data-folded": isFolded || undefined, className: "flex flex-col gap-3", children: [_jsxs("h2", { "data-slot": "table-group-heading", className: "flex items-baseline gap-2 border-b border-border pb-1.5", children: [_jsx(GroupLabel, { run: run, icon: groupIcons?.get(run.key), isFolded: isFolded, onToggle: () => onToggleFold(run.key) }), groupSummary && (_jsx("span", { className: "type-caption text-muted-foreground tabular-nums", children: groupSummary(run.rows.map((row) => row.original), { key: run.key, label: run.label }) }))] }), !isFolded && list(run.rows)] }, `${runIndex}:${run.key}`));
|
|
532
563
|
})
|
|
533
564
|
: list(rows) }));
|
|
534
565
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.55.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|