@vendure-io/ui 2.0.0-beta.1 → 2.0.0-beta.11
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/README.md +6 -3
- package/package.json +7 -7
- package/src/components/atoms/button.tsx +5 -1
- package/src/components/atoms/card.tsx +39 -4
- package/src/components/atoms/scroll-area.tsx +4 -1
- package/src/components/atoms/sidebar.tsx +26 -11
- package/src/components/atoms/table.tsx +1 -1
- package/src/components/molecules/anonymized-token.tsx +2 -2
- package/src/components/molecules/app-shell.tsx +110 -0
- package/src/components/molecules/code-block.tsx +996 -0
- package/src/components/molecules/copy-feedback-provider.tsx +33 -0
- package/src/components/molecules/copyable-text.tsx +30 -7
- package/src/components/molecules/data-table/data-table-bulk-actions.tsx +10 -9
- package/src/components/molecules/data-table/data-table-types.tsx +29 -2
- package/src/components/molecules/data-table/data-table.tsx +223 -139
- package/src/components/molecules/date-picker.tsx +117 -0
- package/src/components/molecules/date-range-picker.tsx +151 -0
- package/src/components/molecules/date-time-picker.tsx +131 -0
- package/src/components/molecules/file-dropzone.tsx +261 -0
- package/src/components/molecules/id-chip.tsx +1 -1
- package/src/components/molecules/skip-link.tsx +36 -0
- package/src/components/molecules/state-views/loading-state.tsx +7 -3
- package/src/lib/date-value.ts +48 -0
- package/src/lib/highlight.ts +141 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, type ReactNode, useContext, useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
// Function props can't cross a server→client boundary, so a copy surface
|
|
6
|
+
// rendered from RSC (e.g. MDX docs) could never receive an `onCopied` callback
|
|
7
|
+
// for toast wiring. This context is the RSC-safe alternative: mount
|
|
8
|
+
// CopyFeedbackProvider once in a client component (wire your toast there — the
|
|
9
|
+
// DS never toasts), and copy surfaces resolve their feedback in a fixed order —
|
|
10
|
+
// explicit prop → this context → nothing beyond the built-in copied icon.
|
|
11
|
+
interface CopyFeedbackContextValue {
|
|
12
|
+
/** Called after a successful copy. Wire your toast here — the DS never toasts. */
|
|
13
|
+
onCopied?: () => void;
|
|
14
|
+
/** Called when the clipboard write fails. */
|
|
15
|
+
onCopyError?: (error: Error) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CopyFeedbackContext = createContext<CopyFeedbackContextValue>({});
|
|
19
|
+
|
|
20
|
+
function CopyFeedbackProvider({
|
|
21
|
+
children,
|
|
22
|
+
onCopied,
|
|
23
|
+
onCopyError,
|
|
24
|
+
}: CopyFeedbackContextValue & { children: ReactNode }) {
|
|
25
|
+
const value = useMemo(() => ({ onCopied, onCopyError }), [onCopied, onCopyError]);
|
|
26
|
+
return <CopyFeedbackContext.Provider value={value}>{children}</CopyFeedbackContext.Provider>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function useCopyFeedback(): CopyFeedbackContextValue {
|
|
30
|
+
return useContext(CopyFeedbackContext);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { CopyFeedbackProvider, useCopyFeedback, type CopyFeedbackContextValue };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Button } from '@vendure-io/ui/components/atoms/button';
|
|
4
|
+
import { useCopyFeedback } from '@vendure-io/ui/components/molecules/copy-feedback-provider';
|
|
4
5
|
import { useCopy } from '@vendure-io/ui/hooks/use-copy';
|
|
5
6
|
import { cn } from '@vendure-io/ui/lib/utils';
|
|
6
7
|
import { CheckIcon, CopyIcon } from 'lucide-react';
|
|
@@ -11,9 +12,16 @@ interface CopyButtonProps extends Omit<React.ComponentProps<typeof Button>, 'val
|
|
|
11
12
|
value: string;
|
|
12
13
|
/** How long the check-mark feedback stays visible, in ms. @default 2000 */
|
|
13
14
|
timeout?: number;
|
|
14
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Called after a successful copy. Wire your toast here — the DS never toasts.
|
|
17
|
+
* Falls back to `CopyFeedbackProvider` when omitted (the RSC-safe path, since
|
|
18
|
+
* function props can't be passed from server components).
|
|
19
|
+
*/
|
|
15
20
|
onCopied?: () => void;
|
|
16
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Called when the clipboard write fails (e.g. permissions, insecure context).
|
|
23
|
+
* Falls back to `CopyFeedbackProvider` when omitted.
|
|
24
|
+
*/
|
|
17
25
|
onCopyError?: (error: Error) => void;
|
|
18
26
|
/** Accessible label before copying. @default "Copy" */
|
|
19
27
|
copyLabel?: string;
|
|
@@ -40,6 +48,7 @@ function CopyButton({
|
|
|
40
48
|
...props
|
|
41
49
|
}: CopyButtonProps) {
|
|
42
50
|
const { copied, copy } = useCopy({ timeout });
|
|
51
|
+
const copyFeedback = useCopyFeedback();
|
|
43
52
|
|
|
44
53
|
return (
|
|
45
54
|
<Button
|
|
@@ -53,8 +62,9 @@ function CopyButton({
|
|
|
53
62
|
onClick?.(event);
|
|
54
63
|
if (event.defaultPrevented) return;
|
|
55
64
|
const ok = await copy(value);
|
|
56
|
-
if (ok) onCopied?.();
|
|
57
|
-
else
|
|
65
|
+
if (ok) (onCopied ?? copyFeedback.onCopied)?.();
|
|
66
|
+
else
|
|
67
|
+
(onCopyError ?? copyFeedback.onCopyError)?.(new Error('Failed to copy to the clipboard'));
|
|
58
68
|
}}
|
|
59
69
|
{...props}
|
|
60
70
|
>
|
|
@@ -71,10 +81,14 @@ interface CopyableTextProps {
|
|
|
71
81
|
className?: string;
|
|
72
82
|
/** How long the check-mark feedback stays visible, in ms. @default 2000 */
|
|
73
83
|
timeout?: number;
|
|
74
|
-
/** Called after a successful copy. Wire your toast here — the DS never toasts. */
|
|
84
|
+
/** Called after a successful copy. Wire your toast here — the DS never toasts. Falls back to `CopyFeedbackProvider`. */
|
|
75
85
|
onCopied?: () => void;
|
|
76
|
-
/** Called when the clipboard write fails. */
|
|
86
|
+
/** Called when the clipboard write fails. Falls back to `CopyFeedbackProvider`. */
|
|
77
87
|
onCopyError?: (error: Error) => void;
|
|
88
|
+
/** Accessible label before copying, forwarded to the inner `CopyButton`. @default "Copy" */
|
|
89
|
+
copyLabel?: string;
|
|
90
|
+
/** Accessible label shown while the copied state is active, forwarded to the inner `CopyButton`. @default "Copied" */
|
|
91
|
+
copiedLabel?: string;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
/**
|
|
@@ -89,11 +103,20 @@ function CopyableText({
|
|
|
89
103
|
timeout,
|
|
90
104
|
onCopied,
|
|
91
105
|
onCopyError,
|
|
106
|
+
copyLabel,
|
|
107
|
+
copiedLabel,
|
|
92
108
|
}: CopyableTextProps) {
|
|
93
109
|
return (
|
|
94
110
|
<span data-slot="copyable-text" className={cn('inline-flex items-center gap-1.5', className)}>
|
|
95
111
|
{children ?? value}
|
|
96
|
-
<CopyButton
|
|
112
|
+
<CopyButton
|
|
113
|
+
value={value}
|
|
114
|
+
timeout={timeout}
|
|
115
|
+
onCopied={onCopied}
|
|
116
|
+
onCopyError={onCopyError}
|
|
117
|
+
copyLabel={copyLabel}
|
|
118
|
+
copiedLabel={copiedLabel}
|
|
119
|
+
/>
|
|
97
120
|
</span>
|
|
98
121
|
);
|
|
99
122
|
}
|
|
@@ -9,11 +9,12 @@ import type { DataTableBulkActionContext } from '@vendure-io/ui/components/molec
|
|
|
9
9
|
import { cn } from '@vendure-io/ui/lib/utils';
|
|
10
10
|
import * as React from 'react';
|
|
11
11
|
|
|
12
|
-
// The selection
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
12
|
+
// The selection bar that replaces the controls row inside the header band
|
|
13
|
+
// while rows are selected (returns null with an empty selection; the core
|
|
14
|
+
// decides where it renders). The cross-page selection cache lives here (donor
|
|
15
|
+
// parity): `rows` only ever holds the current page, so we accumulate
|
|
16
|
+
// `row.original` by id as pages are seen, letting `bulkActions` receive every
|
|
17
|
+
// selected original — not just the ones still on screen.
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Accumulate `row.original` keyed by row id across page changes. Populated during
|
|
@@ -53,12 +54,12 @@ function DataTableBulkActions<TData>({
|
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
return (
|
|
57
|
+
// A plain band row, not a box: it takes the controls row's place inside
|
|
58
|
+
// the header band, so it carries no chrome of its own. min-h-9 matches
|
|
59
|
+
// the controls row's h-9 inputs so the swap holds the band height steady.
|
|
56
60
|
<div
|
|
57
61
|
data-slot="data-table-bulk-actions"
|
|
58
|
-
className={cn(
|
|
59
|
-
'bg-muted/50 flex flex-wrap items-center gap-2 rounded-md border px-3 py-2',
|
|
60
|
-
className,
|
|
61
|
-
)}
|
|
62
|
+
className={cn('flex min-h-9 flex-wrap items-center gap-2', className)}
|
|
62
63
|
{...props}
|
|
63
64
|
>
|
|
64
65
|
{render(ctx)}
|
|
@@ -182,7 +182,11 @@ export interface DataTableProps<TData> {
|
|
|
182
182
|
header?: React.ReactNode;
|
|
183
183
|
/** Controls row (search, faceted filters, custom buttons). Render-prop gets the live table. */
|
|
184
184
|
toolbar?: React.ReactNode | ((table: Table<TData>) => React.ReactNode);
|
|
185
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* Replaces the controls row inside the header band while selection is
|
|
187
|
+
* non-empty (title and applied-filter chips stay). Only meaningful with
|
|
188
|
+
* `rowSelection` wired.
|
|
189
|
+
*/
|
|
186
190
|
bulkActions?: (ctx: DataTableBulkActionContext<TData>) => React.ReactNode;
|
|
187
191
|
/**
|
|
188
192
|
* Per-row actions → appends an actions column. First arg is `row.original`
|
|
@@ -225,8 +229,31 @@ export interface DataTableProps<TData> {
|
|
|
225
229
|
*/
|
|
226
230
|
setTableOptions?: (options: TableOptions<TData>) => TableOptions<TData>;
|
|
227
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The table's frame. `'card'` (default) renders the band anatomy on a real
|
|
234
|
+
* `Card`: a `CardHeader` (border-b) hosting the header/controls/chips zones,
|
|
235
|
+
* `CardTable` hosting the table flush to the card edges, and a `CardFooter`
|
|
236
|
+
* (border-t) hosting pagination when it is wired — no footer means the last
|
|
237
|
+
* row sits flush against the card's bottom edge. `'plain'` renders the same
|
|
238
|
+
* band structure without any card chrome, for a table embedded in an
|
|
239
|
+
* existing card (e.g. a dashboard widget): it MUST sit inside a `Card`,
|
|
240
|
+
* whose spacing variables (`--card-px`/`--card-gap`) drive the bands and
|
|
241
|
+
* edge-cell alignment — the host card is the frame.
|
|
242
|
+
*/
|
|
243
|
+
frame?: 'card' | 'plain';
|
|
244
|
+
/**
|
|
245
|
+
* Extra `<TableRow>`s appended inside `TableBody` after the data rows — e.g.
|
|
246
|
+
* an order table's subtotal/shipping/total rows. Rendered only alongside
|
|
247
|
+
* real rows, never with the skeleton or empty states, and has no interaction
|
|
248
|
+
* with selection, sorting, or pagination. Use the function form to receive
|
|
249
|
+
* `columnCount` for `colSpan`s — display columns (select, actions) are
|
|
250
|
+
* injected around your columns, so a hard-coded count desyncs. Rows are
|
|
251
|
+
* presentational: give them `hover:bg-transparent`.
|
|
252
|
+
*/
|
|
253
|
+
footerRows?: React.ReactNode | ((ctx: { columnCount: number }) => React.ReactNode);
|
|
254
|
+
|
|
228
255
|
labels?: DataTableLabels;
|
|
229
|
-
className?: string; //
|
|
256
|
+
className?: string; // the frame root (data-slot="data-table")
|
|
230
257
|
}
|
|
231
258
|
|
|
232
259
|
/**
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
useReactTable,
|
|
20
20
|
type VisibilityState,
|
|
21
21
|
} from '@tanstack/react-table';
|
|
22
|
+
import { Card, CardFooter, CardHeader, CardTable } from '@vendure-io/ui/components/atoms/card';
|
|
22
23
|
import { Checkbox } from '@vendure-io/ui/components/atoms/checkbox';
|
|
23
24
|
import {
|
|
24
25
|
ContextMenu,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
} from '@vendure-io/ui/components/molecules/data-table/data-table-filters';
|
|
46
47
|
import {
|
|
47
48
|
buildDisplayColumns,
|
|
49
|
+
getSelectedRowIds,
|
|
48
50
|
resolveSlot,
|
|
49
51
|
} from '@vendure-io/ui/components/molecules/data-table/data-table-helpers';
|
|
50
52
|
import type {
|
|
@@ -59,13 +61,17 @@ import {
|
|
|
59
61
|
} from '@vendure-io/ui/components/molecules/data-table/list-header';
|
|
60
62
|
import { TablePagination } from '@vendure-io/ui/components/molecules/data-table/table-pagination';
|
|
61
63
|
import { cn } from '@vendure-io/ui/lib/utils';
|
|
64
|
+
import { AnimatePresence, motion } from 'motion/react';
|
|
62
65
|
import * as React from 'react';
|
|
63
66
|
|
|
64
67
|
// The composition root. Owns the single `useReactTable` instance and the
|
|
65
|
-
// controlled/uncontrolled bridge, then lays out
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
68
|
+
// controlled/uncontrolled bridge, then lays out the card-framed table anatomy:
|
|
69
|
+
// a CardHeader band (header → controls → chips → bulk overlay), a CardTable
|
|
70
|
+
// hosting the Table flush to the card edges, and a CardFooter band for
|
|
71
|
+
// pagination. `frame="plain"` keeps the band structure without the card
|
|
72
|
+
// chrome. Every capability follows the Phase-1 rule verbatim: a feature
|
|
73
|
+
// renders only if its config is wired — no feature flags, no disabled
|
|
74
|
+
// placeholders. Only this folder imports TanStack; the composed
|
|
69
75
|
// ListHeader/TablePagination/Chip primitives stay TanStack-free.
|
|
70
76
|
|
|
71
77
|
/**
|
|
@@ -98,6 +104,16 @@ function headerLabelText<TData>(column: Column<TData, unknown>): string {
|
|
|
98
104
|
return typeof header === 'string' ? header : column.id;
|
|
99
105
|
}
|
|
100
106
|
|
|
107
|
+
// The controls/bulk replace-on-select swap: the outgoing row drifts slightly
|
|
108
|
+
// up as it fades, the incoming row rises in from below — a subtle vertical
|
|
109
|
+
// hand-off (4px) rather than a plain crossfade.
|
|
110
|
+
const bandRowFade = {
|
|
111
|
+
initial: { opacity: 0, y: 4 },
|
|
112
|
+
animate: { opacity: 1, y: 0 },
|
|
113
|
+
exit: { opacity: 0, y: -4 },
|
|
114
|
+
transition: { duration: 0.1, ease: 'easeOut' },
|
|
115
|
+
} as const;
|
|
116
|
+
|
|
101
117
|
function ariaSort<TData>(
|
|
102
118
|
hasSorting: boolean,
|
|
103
119
|
column: Column<TData, unknown>,
|
|
@@ -126,6 +142,8 @@ function DataTable<TData>({
|
|
|
126
142
|
emptyState,
|
|
127
143
|
renderRow,
|
|
128
144
|
setTableOptions,
|
|
145
|
+
frame = 'card',
|
|
146
|
+
footerRows,
|
|
129
147
|
labels,
|
|
130
148
|
className,
|
|
131
149
|
}: DataTableProps<TData>) {
|
|
@@ -307,7 +325,11 @@ function DataTable<TData>({
|
|
|
307
325
|
? table.getVisibleLeafColumns().find((column) => column.id !== selectColumnId)?.id
|
|
308
326
|
: undefined;
|
|
309
327
|
const columnClass = (id: string): string | undefined => {
|
|
310
|
-
|
|
328
|
+
// `pl-0!`: the select cell is the first child, so CardTable's edge-cell
|
|
329
|
+
// padding would otherwise widen the zero-width cell and push the whole
|
|
330
|
+
// gutter arrangement out; important because CardTable's descendant rule
|
|
331
|
+
// out-specifies a plain utility on the cell.
|
|
332
|
+
if (id === selectColumnId) return 'relative w-0 p-0 pl-0!';
|
|
311
333
|
if (id === leadingColumnId) return 'pl-8';
|
|
312
334
|
// Actions cells drop vertical padding so a row-action button (taller than a
|
|
313
335
|
// text line) sits inside the natural row height rather than inflating every
|
|
@@ -327,153 +349,215 @@ function DataTable<TData>({
|
|
|
327
349
|
filters.showAppliedFilters !== false &&
|
|
328
350
|
table.getState().columnFilters.length > 0;
|
|
329
351
|
const showHeader = header != null || showControls || showChips;
|
|
330
|
-
|
|
352
|
+
// Unlike the other zones, the bulk bar exists only while rows are selected,
|
|
353
|
+
// so it opens the header band on its own only then — a controls-less
|
|
354
|
+
// selectable table must not render an empty band. (Deliberate trade-off:
|
|
355
|
+
// for a table with no header content at all, the band pops in on first
|
|
356
|
+
// selection rather than reserving empty space.)
|
|
357
|
+
const showBulk =
|
|
358
|
+
rowSelection != null &&
|
|
359
|
+
bulkActions != null &&
|
|
360
|
+
getSelectedRowIds(table.getState().rowSelection).length > 0;
|
|
331
361
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
{showHeader && (
|
|
335
|
-
<
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
362
|
+
const bands = (
|
|
363
|
+
<>
|
|
364
|
+
{(showHeader || showBulk) && (
|
|
365
|
+
<CardHeader className="border-b">
|
|
366
|
+
<ListHeader>
|
|
367
|
+
{header}
|
|
368
|
+
{/* Replace-on-select: while rows are selected the bulk bar takes
|
|
369
|
+
the controls row's place (title and applied-filter chips stay).
|
|
370
|
+
The swap fades out then in — one --transition-duration-fast
|
|
371
|
+
(100ms) per phase; mode="wait" keeps the rows sequential so
|
|
372
|
+
they never stack. */}
|
|
373
|
+
{(showControls || (rowSelection != null && bulkActions != null)) && (
|
|
374
|
+
<AnimatePresence initial={false} mode="wait">
|
|
375
|
+
{showBulk && bulkActions ? (
|
|
376
|
+
<motion.div key="bulk-actions" {...bandRowFade}>
|
|
377
|
+
<DataTableBulkActions
|
|
378
|
+
table={table}
|
|
379
|
+
cache={selectionCache}
|
|
380
|
+
render={bulkActions}
|
|
381
|
+
/>
|
|
382
|
+
</motion.div>
|
|
383
|
+
) : showControls ? (
|
|
384
|
+
<motion.div key="controls" {...bandRowFade}>
|
|
385
|
+
<ListHeaderControls>
|
|
386
|
+
{toolbarNode}
|
|
387
|
+
{hasFilterMenu && filters?.columns && (
|
|
388
|
+
<DataTableAddFilter
|
|
389
|
+
table={table}
|
|
390
|
+
columns={filters.columns}
|
|
391
|
+
label={l.addFilter}
|
|
392
|
+
/>
|
|
393
|
+
)}
|
|
394
|
+
{showViewOptions && (
|
|
395
|
+
<DataTableViewOptions
|
|
396
|
+
table={table}
|
|
397
|
+
triggerLabel={l.columnsTrigger}
|
|
398
|
+
heading={l.columnsHeading}
|
|
399
|
+
/>
|
|
400
|
+
)}
|
|
401
|
+
</ListHeaderControls>
|
|
402
|
+
</motion.div>
|
|
403
|
+
) : null}
|
|
404
|
+
</AnimatePresence>
|
|
405
|
+
)}
|
|
406
|
+
{showChips && (
|
|
407
|
+
<ListHeaderChips>
|
|
408
|
+
<DataTableAppliedFilters
|
|
345
409
|
table={table}
|
|
346
|
-
|
|
347
|
-
|
|
410
|
+
columns={filters?.columns}
|
|
411
|
+
inlineChipLimit={filters?.inlineChipLimit}
|
|
412
|
+
removeLabel={l.removeFilter}
|
|
413
|
+
collapsedLabel={l.filtersCollapsed}
|
|
348
414
|
/>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<ListHeaderChips>
|
|
354
|
-
<DataTableAppliedFilters
|
|
355
|
-
table={table}
|
|
356
|
-
columns={filters?.columns}
|
|
357
|
-
inlineChipLimit={filters?.inlineChipLimit}
|
|
358
|
-
removeLabel={l.removeFilter}
|
|
359
|
-
collapsedLabel={l.filtersCollapsed}
|
|
360
|
-
/>
|
|
361
|
-
</ListHeaderChips>
|
|
362
|
-
)}
|
|
363
|
-
</ListHeader>
|
|
415
|
+
</ListHeaderChips>
|
|
416
|
+
)}
|
|
417
|
+
</ListHeader>
|
|
418
|
+
</CardHeader>
|
|
364
419
|
)}
|
|
365
420
|
|
|
366
|
-
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
</TableHead>
|
|
395
|
-
);
|
|
396
|
-
})}
|
|
397
|
-
</TableRow>
|
|
398
|
-
))}
|
|
399
|
-
</TableHeader>
|
|
400
|
-
<TableBody>
|
|
401
|
-
{isLoading && rows.length === 0 ? (
|
|
402
|
-
Array.from({ length: skeletonRowCount }).map((_, rowIndex) => (
|
|
403
|
-
<TableRow key={`skeleton-${rowIndex}`}>
|
|
404
|
-
{Array.from({ length: Math.max(columnCount, 1) }).map((__, cellIndex) => (
|
|
405
|
-
<TableCell key={`skeleton-cell-${cellIndex}`}>
|
|
406
|
-
<Skeleton className="h-4 w-full" />
|
|
407
|
-
</TableCell>
|
|
408
|
-
))}
|
|
421
|
+
<CardTable>
|
|
422
|
+
<Table>
|
|
423
|
+
<TableHeader>
|
|
424
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
425
|
+
<TableRow key={headerGroup.id} className="group/header-row">
|
|
426
|
+
{headerGroup.headers.map((headerCell) => {
|
|
427
|
+
const content = headerCell.isPlaceholder
|
|
428
|
+
? null
|
|
429
|
+
: flexRender(headerCell.column.columnDef.header, headerCell.getContext());
|
|
430
|
+
return (
|
|
431
|
+
<TableHead
|
|
432
|
+
key={headerCell.id}
|
|
433
|
+
aria-sort={ariaSort(sorting != null, headerCell.column)}
|
|
434
|
+
className={columnClass(headerCell.column.id)}
|
|
435
|
+
>
|
|
436
|
+
{sorting != null && !headerCell.isPlaceholder ? (
|
|
437
|
+
<DataTableColumnHeader
|
|
438
|
+
column={headerCell.column}
|
|
439
|
+
sortLabel={l.sortLabel(headerLabelText(headerCell.column))}
|
|
440
|
+
>
|
|
441
|
+
{content}
|
|
442
|
+
</DataTableColumnHeader>
|
|
443
|
+
) : (
|
|
444
|
+
content
|
|
445
|
+
)}
|
|
446
|
+
</TableHead>
|
|
447
|
+
);
|
|
448
|
+
})}
|
|
409
449
|
</TableRow>
|
|
410
|
-
))
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
</TableRow>
|
|
420
|
-
) : (
|
|
421
|
-
bodyRows.map((row) => {
|
|
422
|
-
const rowNode = (
|
|
423
|
-
<TableRow
|
|
424
|
-
className="group/row"
|
|
425
|
-
data-state={row.getIsSelected() ? 'selected' : undefined}
|
|
426
|
-
>
|
|
427
|
-
{row.getVisibleCells().map((cell) => (
|
|
428
|
-
<TableCell key={cell.id} className={columnClass(cell.column.id)}>
|
|
429
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
450
|
+
))}
|
|
451
|
+
</TableHeader>
|
|
452
|
+
<TableBody>
|
|
453
|
+
{isLoading && rows.length === 0 ? (
|
|
454
|
+
Array.from({ length: skeletonRowCount }).map((_, rowIndex) => (
|
|
455
|
+
<TableRow key={`skeleton-${rowIndex}`}>
|
|
456
|
+
{Array.from({ length: Math.max(columnCount, 1) }).map((__, cellIndex) => (
|
|
457
|
+
<TableCell key={`skeleton-cell-${cellIndex}`}>
|
|
458
|
+
<Skeleton className="h-4 w-full" />
|
|
430
459
|
</TableCell>
|
|
431
460
|
))}
|
|
432
461
|
</TableRow>
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
462
|
+
))
|
|
463
|
+
) : bodyRows.length === 0 ? (
|
|
464
|
+
<TableRow className="hover:bg-transparent">
|
|
465
|
+
<TableCell
|
|
466
|
+
colSpan={Math.max(columnCount, 1)}
|
|
467
|
+
className="text-muted-foreground h-24 text-center"
|
|
468
|
+
>
|
|
469
|
+
{emptyState ?? l.empty}
|
|
470
|
+
</TableCell>
|
|
471
|
+
</TableRow>
|
|
472
|
+
) : (
|
|
473
|
+
<>
|
|
474
|
+
{bodyRows.map((row) => {
|
|
475
|
+
const rowNode = (
|
|
476
|
+
<TableRow
|
|
477
|
+
className="group/row"
|
|
478
|
+
data-state={row.getIsSelected() ? 'selected' : undefined}
|
|
479
|
+
>
|
|
480
|
+
{row.getVisibleCells().map((cell) => (
|
|
481
|
+
<TableCell key={cell.id} className={columnClass(cell.column.id)}>
|
|
482
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
483
|
+
</TableCell>
|
|
484
|
+
))}
|
|
485
|
+
</TableRow>
|
|
486
|
+
);
|
|
487
|
+
// Right-click accelerator: the row itself is the context-menu
|
|
488
|
+
// trigger, so the consumer supplies only the items and the core
|
|
489
|
+
// owns the menu chrome.
|
|
490
|
+
const defaultRow = contextActions ? (
|
|
491
|
+
<ContextMenu>
|
|
492
|
+
<ContextMenuTrigger render={rowNode} />
|
|
493
|
+
<ContextMenuContent>
|
|
494
|
+
{contextActions(row.original, { row, table })}
|
|
495
|
+
</ContextMenuContent>
|
|
496
|
+
</ContextMenu>
|
|
497
|
+
) : (
|
|
498
|
+
rowNode
|
|
499
|
+
);
|
|
500
|
+
// Row-render seam: consumers can swap the default row for a
|
|
501
|
+
// full-width utility row or a per-row wrapper the cell grid can't
|
|
502
|
+
// express. Returning `defaultRow` keeps the built-in rendering.
|
|
503
|
+
return (
|
|
504
|
+
<React.Fragment key={row.id}>
|
|
505
|
+
{renderRow ? renderRow(row, { table, columnCount, defaultRow }) : defaultRow}
|
|
506
|
+
</React.Fragment>
|
|
507
|
+
);
|
|
508
|
+
})}
|
|
509
|
+
{typeof footerRows === 'function' ? footerRows({ columnCount }) : footerRows}
|
|
510
|
+
</>
|
|
511
|
+
)}
|
|
512
|
+
</TableBody>
|
|
513
|
+
</Table>
|
|
514
|
+
</CardTable>
|
|
459
515
|
|
|
460
516
|
{pagination && (
|
|
461
|
-
<
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
517
|
+
<CardFooter className="border-t">
|
|
518
|
+
<TablePagination
|
|
519
|
+
className="w-full"
|
|
520
|
+
page={pagination.page}
|
|
521
|
+
pageSize={pagination.pageSize}
|
|
522
|
+
totalItems={pagination.totalItems ?? table.getPrePaginationRowModel().rows.length}
|
|
523
|
+
onPageChange={pagination.onPageChange}
|
|
524
|
+
onPageSizeChange={pagination.onPageSizeChange}
|
|
525
|
+
pageSizeOptions={pagination.pageSizeOptions}
|
|
526
|
+
getPageHref={pagination.getPageHref}
|
|
527
|
+
formatRange={pagination.formatRange}
|
|
528
|
+
navLabel={l.pagination?.navLabel}
|
|
529
|
+
previousLabel={l.pagination?.previousLabel}
|
|
530
|
+
nextLabel={l.pagination?.nextLabel}
|
|
531
|
+
pageSizeLabel={l.pagination?.pageSizeLabel}
|
|
532
|
+
/>
|
|
533
|
+
</CardFooter>
|
|
534
|
+
)}
|
|
535
|
+
</>
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
// `plain` renders a chrome-less stack instead of a Card so the bands and
|
|
539
|
+
// CardTable consume the HOST card's spacing variables (--card-px /
|
|
540
|
+
// --card-gap): edge cells align with the host's padding regardless of its
|
|
541
|
+
// size, and a trailing CardTable's negative margin reaches through the
|
|
542
|
+
// host's bottom padding so the last row sits flush against the host's edge.
|
|
543
|
+
// With no header band the leading CardTable's negative margin would eat the
|
|
544
|
+
// host's flex gap too and press the rows against the preceding content, so
|
|
545
|
+
// the stack restores that one gap as padding.
|
|
546
|
+
return frame === 'plain' ? (
|
|
547
|
+
<div
|
|
548
|
+
data-slot="data-table"
|
|
549
|
+
className={cn(
|
|
550
|
+
'flex flex-col gap-(--card-gap)',
|
|
551
|
+
!(showHeader || showBulk) && 'pt-(--card-gap)',
|
|
552
|
+
className,
|
|
475
553
|
)}
|
|
554
|
+
>
|
|
555
|
+
{bands}
|
|
476
556
|
</div>
|
|
557
|
+
) : (
|
|
558
|
+
<Card data-slot="data-table" className={className}>
|
|
559
|
+
{bands}
|
|
560
|
+
</Card>
|
|
477
561
|
);
|
|
478
562
|
}
|
|
479
563
|
|