@burtson-labs/ui 0.12.4 → 0.13.1
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 +1 -1
- package/dist/components/checkbox-card.d.ts +25 -0
- package/dist/components/checkbox-card.js +65 -0
- package/dist/components/checkbox-card.js.map +1 -0
- package/dist/components/data-table.d.ts +53 -3
- package/dist/components/data-table.js +346 -112
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/dialog.d.ts +6 -1
- package/dist/components/dialog.js +21 -10
- package/dist/components/dialog.js.map +1 -1
- package/dist/components/pagination.d.ts +104 -3
- package/dist/components/pagination.js +244 -58
- package/dist/components/pagination.js.map +1 -1
- package/dist/components/select.js +5 -1
- package/dist/components/select.js.map +1 -1
- package/dist/components/sheet.d.ts +6 -1
- package/dist/components/sheet.js +2 -2
- package/dist/components/sheet.js.map +1 -1
- package/dist/components/steps.d.ts +15 -2
- package/dist/components/steps.js +68 -22
- package/dist/components/steps.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,8 @@ import { cn } from "../lib/utils.js";
|
|
|
2
2
|
import { Button } from "./button.js";
|
|
3
3
|
import { Input } from "./input.js";
|
|
4
4
|
import { Checkbox } from "./checkbox.js";
|
|
5
|
-
import {
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
6
|
+
import { Pagination, pageWindow, paginationSummary, usePagination } from "./pagination.js";
|
|
6
7
|
import { Skeleton } from "./skeleton.js";
|
|
7
8
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table.js";
|
|
8
9
|
import * as React from "react";
|
|
@@ -12,6 +13,34 @@ import ChevronsUpDown from "@burtson-labs/icons/react/chevrons-up-down";
|
|
|
12
13
|
import ArrowUp from "@burtson-labs/icons/react/arrow-up";
|
|
13
14
|
import ArrowDown from "@burtson-labs/icons/react/arrow-down";
|
|
14
15
|
//#region src/components/data-table.tsx
|
|
16
|
+
var BREAKPOINTS = {
|
|
17
|
+
sm: 640,
|
|
18
|
+
md: 768,
|
|
19
|
+
lg: 1024,
|
|
20
|
+
xl: 1280
|
|
21
|
+
};
|
|
22
|
+
var hideBelowClass = {
|
|
23
|
+
sm: "hidden sm:table-cell",
|
|
24
|
+
md: "hidden md:table-cell",
|
|
25
|
+
lg: "hidden lg:table-cell",
|
|
26
|
+
xl: "hidden xl:table-cell"
|
|
27
|
+
};
|
|
28
|
+
var subscribeNone = () => () => void 0;
|
|
29
|
+
/**
|
|
30
|
+
* True while the viewport is narrower than `bp`. Without matchMedia (server
|
|
31
|
+
* render, tests) it is false, so the table is the fallback.
|
|
32
|
+
*/
|
|
33
|
+
function useNarrowerThan(bp) {
|
|
34
|
+
const query = bp ? `(max-width: ${BREAKPOINTS[bp] - .02}px)` : null;
|
|
35
|
+
const subscribe = React.useCallback((onChange) => {
|
|
36
|
+
if (!query || typeof matchMedia !== "function") return () => void 0;
|
|
37
|
+
const mq = matchMedia(query);
|
|
38
|
+
mq.addEventListener?.("change", onChange);
|
|
39
|
+
return () => mq.removeEventListener?.("change", onChange);
|
|
40
|
+
}, [query]);
|
|
41
|
+
return React.useSyncExternalStore(query ? subscribe : subscribeNone, () => Boolean(query && typeof matchMedia === "function" && matchMedia(query).matches), () => false);
|
|
42
|
+
}
|
|
43
|
+
var textOf = (node) => typeof node === "string" || typeof node === "number" ? String(node) : void 0;
|
|
15
44
|
var nextSort = (current, id) => {
|
|
16
45
|
if (!current || current.columnId !== id) return {
|
|
17
46
|
columnId: id,
|
|
@@ -26,15 +55,27 @@ var INTERACTIVE = "a,button,input,select,textarea,[role=\"checkbox\"],[role=\"me
|
|
|
26
55
|
/**
|
|
27
56
|
* A table recipe for records: sort, search, select, page, and row actions,
|
|
28
57
|
* with loading, empty and error states. Fully controlled, so the data can
|
|
29
|
-
* come from a server; the table never reorders rows itself.
|
|
58
|
+
* come from a server; the table never reorders rows itself. On a phone the
|
|
59
|
+
* rows become cards (`cardsBelow`), with the same columns.
|
|
30
60
|
*/
|
|
31
|
-
function DataTable({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLabel, sort, onSortChange, filter, onFilterChange, filterPlaceholder = "Search", selected, onSelectedChange, onRowAction, rowActions, page, pageCount, onPageChange, pageSummary, loading = false, error, onRetry, empty = "No results.", toolbar, density = "default", stickyHeader, className, ...props }) {
|
|
61
|
+
function DataTable({ "aria-label": ariaLabel, columns, rows: rowsProp, getRowId, getRowLabel, sort, onSortChange, filter, onFilterChange, filterPlaceholder = "Search", selected, onSelectedChange, onRowAction, rowActions, page, pageCount, onPageChange, pageSummary, pageSize, pageSizeOptions, onPageSizeChange, totalRows, paginate, rowNoun, loading = false, error, onRetry, empty = "No results.", toolbar, density = "default", stickyHeader, cardsBelow = "md", className, ...props }) {
|
|
62
|
+
const paginateOptions = paginate === true ? {} : paginate || void 0;
|
|
63
|
+
const paging = usePagination(rowsProp, {
|
|
64
|
+
defaultPageSize: paginateOptions?.pageSize,
|
|
65
|
+
pageSizeOptions: paginateOptions?.pageSizeOptions,
|
|
66
|
+
storageKey: paginateOptions?.storageKey,
|
|
67
|
+
resetKey: `${filter ?? ""}\u0000${sort?.columnId ?? ""}\u0000${sort?.direction ?? ""}`
|
|
68
|
+
});
|
|
69
|
+
const rows = paginateOptions ? paging.rows : rowsProp;
|
|
70
|
+
const cards = useNarrowerThan(cardsBelow);
|
|
71
|
+
const uid = React.useId();
|
|
32
72
|
const selectable = Boolean(onSelectedChange);
|
|
33
73
|
const selectedSet = React.useMemo(() => new Set(selected ?? []), [selected]);
|
|
34
74
|
const ids = rows.map(getRowId);
|
|
35
75
|
const pageSelected = ids.filter((id) => selectedSet.has(id)).length;
|
|
36
76
|
const allState = ids.length > 0 && pageSelected === ids.length ? true : pageSelected > 0 ? "indeterminate" : false;
|
|
37
|
-
const
|
|
77
|
+
const tableColumns = columns.filter((c) => !c.cardOnly);
|
|
78
|
+
const colCount = tableColumns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0);
|
|
38
79
|
const interactive = Boolean(onRowAction) || selectable;
|
|
39
80
|
const [active, setActive] = React.useState(0);
|
|
40
81
|
const frameRef = React.useRef(null);
|
|
@@ -89,112 +130,119 @@ function DataTable({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLab
|
|
|
89
130
|
}
|
|
90
131
|
}
|
|
91
132
|
};
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
const sortable = onSortChange ? columns.filter((c) => c.sortable) : [];
|
|
134
|
+
const columnLabel = (c) => c.cardLabel ?? textOf(c.header) ?? c.id;
|
|
135
|
+
const cardSort = cards && sortable.length > 0 && onSortChange ? /* @__PURE__ */ jsxs(Select, {
|
|
136
|
+
value: sort ? `${sort.columnId}:${sort.direction}` : "none",
|
|
137
|
+
onValueChange: (v) => {
|
|
138
|
+
if (v === "none") return onSortChange(null);
|
|
139
|
+
const [columnId = "", direction] = v.split(":");
|
|
140
|
+
onSortChange({
|
|
141
|
+
columnId,
|
|
142
|
+
direction: direction === "desc" ? "desc" : "asc"
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
146
|
+
"aria-label": "Sort by",
|
|
147
|
+
className: "pointer-coarse:h-11",
|
|
148
|
+
children: /* @__PURE__ */ jsx(SelectValue, {})
|
|
149
|
+
}), /* @__PURE__ */ jsxs(SelectContent, {
|
|
150
|
+
align: "end",
|
|
151
|
+
children: [/* @__PURE__ */ jsx(SelectItem, {
|
|
152
|
+
value: "none",
|
|
153
|
+
children: "Default order"
|
|
154
|
+
}), sortable.flatMap((c) => [/* @__PURE__ */ jsxs(SelectItem, {
|
|
155
|
+
value: `${c.id}:asc`,
|
|
156
|
+
children: [columnLabel(c), " ascending"]
|
|
157
|
+
}, `${c.id}:asc`), /* @__PURE__ */ jsxs(SelectItem, {
|
|
158
|
+
value: `${c.id}:desc`,
|
|
159
|
+
children: [columnLabel(c), " descending"]
|
|
160
|
+
}, `${c.id}:desc`)])]
|
|
161
|
+
})]
|
|
162
|
+
}) : null;
|
|
163
|
+
const cardSelectAll = cards && selectable ? /* @__PURE__ */ jsxs("label", {
|
|
164
|
+
htmlFor: `${uid}-all`,
|
|
165
|
+
className: "flex min-h-11 cursor-pointer items-center gap-2.5 pr-1 text-sm sm:min-h-9",
|
|
166
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
167
|
+
id: `${uid}-all`,
|
|
168
|
+
checked: allState,
|
|
169
|
+
onCheckedChange: toggleAll,
|
|
170
|
+
disabled: rows.length === 0,
|
|
171
|
+
"aria-label": "Select all rows on this page"
|
|
172
|
+
}), "Select all"]
|
|
173
|
+
}) : null;
|
|
174
|
+
const showToolbar = Boolean(onFilterChange || toolbar || selectable && selectedSet.size || cardSort || cardSelectAll);
|
|
175
|
+
const withNoun = (summary, total) => rowNoun && total ? `${summary} ${rowNoun}` : summary;
|
|
176
|
+
let pager = null;
|
|
177
|
+
if (paginateOptions) {
|
|
178
|
+
if (paging.hasPages) pager = /* @__PURE__ */ jsx(Pagination, {
|
|
179
|
+
...paging.paginationProps,
|
|
180
|
+
summary: withNoun(paging.summary, paging.total)
|
|
181
|
+
});
|
|
182
|
+
} else if (onPageChange && page !== void 0 && pageCount !== void 0 && pageCount > 0) pager = /* @__PURE__ */ jsx(Pagination, {
|
|
183
|
+
page,
|
|
184
|
+
pageCount,
|
|
185
|
+
onPageChange,
|
|
186
|
+
summary: pageSummary ?? (totalRows !== void 0 && pageSize ? withNoun(paginationSummary(pageWindow({
|
|
187
|
+
page,
|
|
188
|
+
pageSize,
|
|
189
|
+
total: totalRows
|
|
190
|
+
})), totalRows) : void 0),
|
|
191
|
+
pageSize,
|
|
192
|
+
pageSizeOptions,
|
|
193
|
+
onPageSizeChange
|
|
194
|
+
});
|
|
195
|
+
const toolbarNode = showToolbar && /* @__PURE__ */ jsxs("div", {
|
|
196
|
+
className: "flex flex-wrap items-center gap-2",
|
|
197
|
+
children: [
|
|
198
|
+
onFilterChange && /* @__PURE__ */ jsxs("div", {
|
|
199
|
+
className: "relative min-w-0 basis-full sm:max-w-xs sm:flex-1 sm:basis-0",
|
|
200
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
201
|
+
className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground",
|
|
202
|
+
"aria-hidden": true
|
|
203
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
204
|
+
type: "search",
|
|
205
|
+
value: filter ?? "",
|
|
206
|
+
onChange: (e) => onFilterChange(e.target.value),
|
|
207
|
+
placeholder: filterPlaceholder,
|
|
208
|
+
"aria-label": `Search ${ariaLabel.toLowerCase()}`,
|
|
209
|
+
className: "pl-8"
|
|
111
210
|
})]
|
|
211
|
+
}),
|
|
212
|
+
cardSelectAll,
|
|
213
|
+
selectable && /* @__PURE__ */ jsx("span", {
|
|
214
|
+
"aria-live": "polite",
|
|
215
|
+
className: "text-xs text-muted-foreground tabular-nums",
|
|
216
|
+
children: selectedSet.size ? `${selectedSet.size} selected` : ""
|
|
217
|
+
}),
|
|
218
|
+
(toolbar || cardSort) && /* @__PURE__ */ jsxs("div", {
|
|
219
|
+
className: "ml-auto flex flex-wrap items-center gap-2",
|
|
220
|
+
children: [cardSort, toolbar]
|
|
112
221
|
})
|
|
113
|
-
|
|
114
|
-
});
|
|
115
|
-
else if (loading && rows.length === 0) body = Array.from({ length: 3 }, (_, r) => /* @__PURE__ */ jsx(TableRow, {
|
|
116
|
-
className: "hover:bg-transparent",
|
|
117
|
-
"aria-hidden": true,
|
|
118
|
-
children: Array.from({ length: colCount }, (_, c) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-full max-w-40" }) }, c))
|
|
119
|
-
}, `loading-${r}`));
|
|
120
|
-
else if (rows.length === 0) body = /* @__PURE__ */ jsx(TableRow, {
|
|
121
|
-
className: "hover:bg-transparent",
|
|
122
|
-
children: /* @__PURE__ */ jsx(TableCell, {
|
|
123
|
-
colSpan: colCount,
|
|
124
|
-
className: "h-auto py-10 text-center whitespace-normal text-muted-foreground",
|
|
125
|
-
children: empty
|
|
126
|
-
})
|
|
127
|
-
});
|
|
128
|
-
else body = rows.map((row, i) => {
|
|
129
|
-
const id = getRowId(row);
|
|
130
|
-
const isSelected = selectedSet.has(id);
|
|
131
|
-
const label = getRowLabel?.(row) ?? id;
|
|
132
|
-
return /* @__PURE__ */ jsxs(TableRow, {
|
|
133
|
-
selected: isSelected,
|
|
134
|
-
tabIndex: interactive ? i === activeRow ? 0 : -1 : void 0,
|
|
135
|
-
onFocus: () => setActive(i),
|
|
136
|
-
onKeyDown: interactive ? (e) => onRowKeyDown(e, i, row) : void 0,
|
|
137
|
-
onClick: onRowAction ? (e) => {
|
|
138
|
-
if (e.target.closest(INTERACTIVE)) return;
|
|
139
|
-
onRowAction(row);
|
|
140
|
-
} : void 0,
|
|
141
|
-
className: cn(interactive && "outline-none focus-visible:bg-muted/60 focus-visible:shadow-[inset_2px_0_0_var(--ring)]", onRowAction && "cursor-pointer"),
|
|
142
|
-
children: [
|
|
143
|
-
selectable && /* @__PURE__ */ jsx(TableCell, {
|
|
144
|
-
className: "w-10",
|
|
145
|
-
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
146
|
-
checked: isSelected,
|
|
147
|
-
onCheckedChange: () => toggle(id),
|
|
148
|
-
"aria-label": `Select ${label}`,
|
|
149
|
-
tabIndex: -1
|
|
150
|
-
})
|
|
151
|
-
}),
|
|
152
|
-
columns.map((col) => /* @__PURE__ */ jsx(TableCell, {
|
|
153
|
-
numeric: col.numeric,
|
|
154
|
-
className: col.className,
|
|
155
|
-
children: col.cell(row)
|
|
156
|
-
}, col.id)),
|
|
157
|
-
rowActions && /* @__PURE__ */ jsx(TableCell, {
|
|
158
|
-
className: "w-12 text-right",
|
|
159
|
-
children: rowActions(row)
|
|
160
|
-
})
|
|
161
|
-
]
|
|
162
|
-
}, id);
|
|
222
|
+
]
|
|
163
223
|
});
|
|
164
224
|
return /* @__PURE__ */ jsxs("div", {
|
|
165
225
|
"data-slot": "data-table",
|
|
166
|
-
|
|
226
|
+
"data-layout": cards ? "cards" : "table",
|
|
227
|
+
className: cn("grid min-w-0 grid-cols-[minmax(0,1fr)] gap-3", className),
|
|
167
228
|
...props,
|
|
168
229
|
children: [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}),
|
|
186
|
-
selectable && /* @__PURE__ */ jsx("span", {
|
|
187
|
-
"aria-live": "polite",
|
|
188
|
-
className: "text-xs text-muted-foreground tabular-nums",
|
|
189
|
-
children: selectedSet.size ? `${selectedSet.size} selected` : ""
|
|
190
|
-
}),
|
|
191
|
-
toolbar && /* @__PURE__ */ jsx("div", {
|
|
192
|
-
className: "ml-auto flex flex-wrap items-center gap-2",
|
|
193
|
-
children: toolbar
|
|
194
|
-
})
|
|
195
|
-
]
|
|
196
|
-
}),
|
|
197
|
-
/* @__PURE__ */ jsx("div", {
|
|
230
|
+
toolbarNode,
|
|
231
|
+
cards ? /* @__PURE__ */ jsx(DataTableCards, {
|
|
232
|
+
"aria-label": ariaLabel,
|
|
233
|
+
columns,
|
|
234
|
+
rows,
|
|
235
|
+
getRowId,
|
|
236
|
+
getRowLabel,
|
|
237
|
+
selectedSet,
|
|
238
|
+
onToggle: selectable ? toggle : void 0,
|
|
239
|
+
onRowAction,
|
|
240
|
+
rowActions,
|
|
241
|
+
loading,
|
|
242
|
+
error,
|
|
243
|
+
onRetry,
|
|
244
|
+
empty
|
|
245
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
198
246
|
ref: frameRef,
|
|
199
247
|
className: "rounded-lg border bg-surface",
|
|
200
248
|
children: /* @__PURE__ */ jsxs(Table, {
|
|
@@ -216,11 +264,11 @@ function DataTable({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLab
|
|
|
216
264
|
"aria-label": "Select all rows on this page"
|
|
217
265
|
})
|
|
218
266
|
}),
|
|
219
|
-
|
|
267
|
+
tableColumns.map((col) => {
|
|
220
268
|
const sorted = sort?.columnId === col.id ? sort.direction : void 0;
|
|
221
269
|
return /* @__PURE__ */ jsx(TableHead, {
|
|
222
270
|
numeric: col.numeric,
|
|
223
|
-
className: col.className,
|
|
271
|
+
className: cn(col.hideBelow && hideBelowClass[col.hideBelow], col.className),
|
|
224
272
|
"aria-sort": col.sortable ? sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : "none" : void 0,
|
|
225
273
|
children: col.sortable && onSortChange ? /* @__PURE__ */ jsxs("button", {
|
|
226
274
|
type: "button",
|
|
@@ -247,18 +295,204 @@ function DataTable({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLab
|
|
|
247
295
|
})
|
|
248
296
|
})
|
|
249
297
|
]
|
|
250
|
-
}) }), /* @__PURE__ */ jsx(TableBody, { children:
|
|
298
|
+
}) }), /* @__PURE__ */ jsx(TableBody, { children: error ? /* @__PURE__ */ jsx(TableRow, {
|
|
299
|
+
className: "hover:bg-transparent",
|
|
300
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
301
|
+
colSpan: colCount,
|
|
302
|
+
className: "h-auto py-8 text-center whitespace-normal",
|
|
303
|
+
children: /* @__PURE__ */ jsx(ErrorMessage, {
|
|
304
|
+
error,
|
|
305
|
+
onRetry
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
}) : loading && rows.length === 0 ? Array.from({ length: 3 }, (_, r) => /* @__PURE__ */ jsx(TableRow, {
|
|
309
|
+
className: "hover:bg-transparent",
|
|
310
|
+
"aria-hidden": true,
|
|
311
|
+
children: Array.from({ length: colCount }, (_, c) => /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-full max-w-40" }) }, c))
|
|
312
|
+
}, `loading-${r}`)) : rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, {
|
|
313
|
+
className: "hover:bg-transparent",
|
|
314
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
315
|
+
colSpan: colCount,
|
|
316
|
+
className: "h-auto py-10 text-center whitespace-normal text-muted-foreground",
|
|
317
|
+
children: empty
|
|
318
|
+
})
|
|
319
|
+
}) : rows.map((row, i) => {
|
|
320
|
+
const id = getRowId(row);
|
|
321
|
+
const isSelected = selectedSet.has(id);
|
|
322
|
+
const label = getRowLabel?.(row) ?? id;
|
|
323
|
+
return /* @__PURE__ */ jsxs(TableRow, {
|
|
324
|
+
selected: isSelected,
|
|
325
|
+
tabIndex: interactive ? i === activeRow ? 0 : -1 : void 0,
|
|
326
|
+
onFocus: () => setActive(i),
|
|
327
|
+
onKeyDown: interactive ? (e) => onRowKeyDown(e, i, row) : void 0,
|
|
328
|
+
onClick: onRowAction ? (e) => {
|
|
329
|
+
if (e.target.closest(INTERACTIVE)) return;
|
|
330
|
+
onRowAction(row);
|
|
331
|
+
} : void 0,
|
|
332
|
+
className: cn(interactive && "outline-none focus-visible:bg-muted/60 focus-visible:shadow-[inset_2px_0_0_var(--ring)]", onRowAction && "cursor-pointer"),
|
|
333
|
+
children: [
|
|
334
|
+
selectable && /* @__PURE__ */ jsx(TableCell, {
|
|
335
|
+
className: "w-10",
|
|
336
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
337
|
+
checked: isSelected,
|
|
338
|
+
onCheckedChange: () => toggle(id),
|
|
339
|
+
"aria-label": `Select ${label}`,
|
|
340
|
+
tabIndex: -1
|
|
341
|
+
})
|
|
342
|
+
}),
|
|
343
|
+
tableColumns.map((col) => /* @__PURE__ */ jsx(TableCell, {
|
|
344
|
+
numeric: col.numeric,
|
|
345
|
+
className: cn(col.hideBelow && hideBelowClass[col.hideBelow], col.className),
|
|
346
|
+
children: col.cell(row)
|
|
347
|
+
}, col.id)),
|
|
348
|
+
rowActions && /* @__PURE__ */ jsx(TableCell, {
|
|
349
|
+
className: "w-12 text-right",
|
|
350
|
+
children: rowActions(row)
|
|
351
|
+
})
|
|
352
|
+
]
|
|
353
|
+
}, id);
|
|
354
|
+
}) })]
|
|
251
355
|
})
|
|
252
356
|
}),
|
|
253
|
-
|
|
254
|
-
page,
|
|
255
|
-
pageCount,
|
|
256
|
-
onPageChange,
|
|
257
|
-
summary: pageSummary
|
|
258
|
-
})
|
|
357
|
+
pager
|
|
259
358
|
]
|
|
260
359
|
});
|
|
261
360
|
}
|
|
361
|
+
function ErrorMessage({ error, onRetry }) {
|
|
362
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
363
|
+
role: "alert",
|
|
364
|
+
className: "grid justify-items-center gap-3 text-sm",
|
|
365
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
366
|
+
className: "text-destructive",
|
|
367
|
+
children: error
|
|
368
|
+
}), onRetry && /* @__PURE__ */ jsx(Button, {
|
|
369
|
+
type: "button",
|
|
370
|
+
size: "sm",
|
|
371
|
+
variant: "outline",
|
|
372
|
+
onClick: onRetry,
|
|
373
|
+
children: "Try again"
|
|
374
|
+
})]
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
var cardLabelClass = "text-[11px] font-semibold tracking-[0.06em] text-muted-foreground uppercase";
|
|
378
|
+
var liftControls = "[&_a]:relative [&_a]:z-10 [&_button]:relative [&_button]:z-10 [&_input]:relative [&_input]:z-10 [&_select]:relative [&_select]:z-10 [&_textarea]:relative [&_textarea]:z-10";
|
|
379
|
+
/**
|
|
380
|
+
* The phone layout: a list of articles, one per row, each named by its title.
|
|
381
|
+
* The title is the row's open button (its hit area covers the card); the
|
|
382
|
+
* checkbox, row actions and footer controls sit above it and keep their own
|
|
383
|
+
* clicks. No roving focus: Tab walks the controls, as in any list of links.
|
|
384
|
+
*/
|
|
385
|
+
function DataTableCards({ "aria-label": ariaLabel, columns, rows, getRowId, getRowLabel, selectedSet, onToggle, onRowAction, rowActions, loading, error, onRetry, empty }) {
|
|
386
|
+
const uid = React.useId();
|
|
387
|
+
if (error) return /* @__PURE__ */ jsx("div", {
|
|
388
|
+
className: "rounded-lg border bg-surface px-4 py-8",
|
|
389
|
+
children: /* @__PURE__ */ jsx(ErrorMessage, {
|
|
390
|
+
error,
|
|
391
|
+
onRetry
|
|
392
|
+
})
|
|
393
|
+
});
|
|
394
|
+
if (rows.length === 0 && !loading) return /* @__PURE__ */ jsx("div", {
|
|
395
|
+
className: "rounded-lg border bg-surface px-4 py-10 text-center text-sm text-muted-foreground",
|
|
396
|
+
children: empty
|
|
397
|
+
});
|
|
398
|
+
const visible = columns.filter((c) => c.card !== "hidden");
|
|
399
|
+
const firstUnplaced = visible.some((c) => c.card === "title") ? void 0 : visible.find((c) => c.card === void 0);
|
|
400
|
+
const slotOf = (c) => c.card ?? (c === firstUnplaced ? "title" : "field");
|
|
401
|
+
const inSlot = (s) => visible.filter((c) => slotOf(c) === s);
|
|
402
|
+
const title = inSlot("title");
|
|
403
|
+
const subtitle = inSlot("subtitle");
|
|
404
|
+
const aside = inSlot("aside");
|
|
405
|
+
const fields = inSlot("field");
|
|
406
|
+
const footer = inSlot("footer");
|
|
407
|
+
const show = (c, row) => (c.cardCell ?? c.cell)(row);
|
|
408
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
409
|
+
role: "list",
|
|
410
|
+
"aria-label": ariaLabel,
|
|
411
|
+
"aria-busy": loading || void 0,
|
|
412
|
+
"data-slot": "data-table-cards",
|
|
413
|
+
className: cn("grid gap-2", loading && rows.length > 0 && "opacity-60 transition-opacity"),
|
|
414
|
+
children: loading && rows.length === 0 ? Array.from({ length: 3 }, (_, r) => /* @__PURE__ */ jsxs("li", {
|
|
415
|
+
"aria-hidden": true,
|
|
416
|
+
className: "grid gap-3 rounded-lg border bg-surface p-3.5",
|
|
417
|
+
children: [/* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-1/2" }), /* @__PURE__ */ jsx(Skeleton, { className: "h-3.5 w-3/4" })]
|
|
418
|
+
}, `loading-${r}`)) : rows.map((row, i) => {
|
|
419
|
+
const id = getRowId(row);
|
|
420
|
+
const label = getRowLabel?.(row) ?? id;
|
|
421
|
+
const isSelected = selectedSet.has(id);
|
|
422
|
+
const titleId = `${uid}-${i}`;
|
|
423
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs("article", {
|
|
424
|
+
"aria-labelledby": title.length ? titleId : void 0,
|
|
425
|
+
"aria-label": title.length ? void 0 : label,
|
|
426
|
+
"data-slot": "data-table-card",
|
|
427
|
+
"data-state": isSelected ? "selected" : void 0,
|
|
428
|
+
className: cn("relative grid gap-3 rounded-lg border bg-surface p-3.5 shadow-xs transition-colors", onRowAction && "hover:bg-muted/40 has-[[data-row-action]:focus-visible]:border-ring has-[[data-row-action]:focus-visible]:inset-ring-1 has-[[data-row-action]:focus-visible]:inset-ring-ring", "data-[state=selected]:border-brand/40 data-[state=selected]:bg-brand-soft/60"),
|
|
429
|
+
children: [
|
|
430
|
+
/* @__PURE__ */ jsxs("div", {
|
|
431
|
+
className: "flex items-start gap-3",
|
|
432
|
+
children: [
|
|
433
|
+
onToggle && /* @__PURE__ */ jsx("label", {
|
|
434
|
+
htmlFor: `${titleId}-select`,
|
|
435
|
+
className: "relative z-10 -mx-3.5 -my-3 grid size-11 shrink-0 cursor-pointer place-items-center",
|
|
436
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
437
|
+
id: `${titleId}-select`,
|
|
438
|
+
checked: isSelected,
|
|
439
|
+
onCheckedChange: () => onToggle(id),
|
|
440
|
+
"aria-label": `Select ${label}`
|
|
441
|
+
})
|
|
442
|
+
}),
|
|
443
|
+
/* @__PURE__ */ jsx("div", {
|
|
444
|
+
className: "grid min-w-0 flex-1 gap-0.5",
|
|
445
|
+
children: title.map((c, j) => /* @__PURE__ */ jsx("div", {
|
|
446
|
+
id: j === 0 ? titleId : void 0,
|
|
447
|
+
className: "min-w-0 text-[15px] leading-5 font-semibold [overflow-wrap:anywhere]",
|
|
448
|
+
children: onRowAction && j === 0 ? /* @__PURE__ */ jsx("button", {
|
|
449
|
+
type: "button",
|
|
450
|
+
"data-row-action": "",
|
|
451
|
+
onClick: () => onRowAction(row),
|
|
452
|
+
className: "text-left outline-hidden! after:absolute after:inset-0 after:rounded-lg",
|
|
453
|
+
children: show(c, row)
|
|
454
|
+
}) : show(c, row)
|
|
455
|
+
}, c.id))
|
|
456
|
+
}),
|
|
457
|
+
aside.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
458
|
+
className: "relative z-10 flex shrink-0 flex-col items-end gap-1",
|
|
459
|
+
children: aside.map((c) => /* @__PURE__ */ jsx("div", { children: show(c, row) }, c.id))
|
|
460
|
+
}),
|
|
461
|
+
rowActions && /* @__PURE__ */ jsx("div", {
|
|
462
|
+
className: "relative z-10 -my-2.5 -mr-1.5 shrink-0",
|
|
463
|
+
children: rowActions(row)
|
|
464
|
+
})
|
|
465
|
+
]
|
|
466
|
+
}),
|
|
467
|
+
subtitle.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
468
|
+
className: cn("-mt-2 grid min-w-0 gap-0.5", liftControls, onToggle && "pl-7"),
|
|
469
|
+
children: subtitle.map((c) => /* @__PURE__ */ jsx("div", {
|
|
470
|
+
className: "min-w-0 text-sm text-muted-foreground [overflow-wrap:anywhere]",
|
|
471
|
+
children: show(c, row)
|
|
472
|
+
}, c.id))
|
|
473
|
+
}),
|
|
474
|
+
fields.length > 0 && /* @__PURE__ */ jsx("dl", {
|
|
475
|
+
className: cn("grid grid-cols-2 gap-x-4 gap-y-2.5 text-sm", liftControls),
|
|
476
|
+
children: fields.map((c) => /* @__PURE__ */ jsxs("div", {
|
|
477
|
+
className: "min-w-0",
|
|
478
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
479
|
+
className: cardLabelClass,
|
|
480
|
+
children: c.cardLabel ?? c.header
|
|
481
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
482
|
+
className: cn("mt-0.5 min-w-0 [overflow-wrap:anywhere]", c.numeric && "tabular-nums"),
|
|
483
|
+
children: show(c, row)
|
|
484
|
+
})]
|
|
485
|
+
}, c.id))
|
|
486
|
+
}),
|
|
487
|
+
footer.map((c) => /* @__PURE__ */ jsx("div", {
|
|
488
|
+
className: "relative z-10 flex flex-wrap gap-2",
|
|
489
|
+
children: show(c, row)
|
|
490
|
+
}, c.id))
|
|
491
|
+
]
|
|
492
|
+
}) }, id);
|
|
493
|
+
})
|
|
494
|
+
});
|
|
495
|
+
}
|
|
262
496
|
//#endregion
|
|
263
497
|
export { DataTable };
|
|
264
498
|
|