mui-atelier 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/NOTICE +32 -0
- package/README.md +282 -0
- package/dist/components/AddressFields.js +83 -0
- package/dist/components/AuthCard.js +94 -0
- package/dist/components/AuthSplash.js +88 -0
- package/dist/components/BrandHeader.js +81 -0
- package/dist/components/BrandLink.js +22 -0
- package/dist/components/BrandMark.js +69 -0
- package/dist/components/BrandProvider.js +14 -0
- package/dist/components/Breadcrumbs.js +62 -0
- package/dist/components/CardHead.js +46 -0
- package/dist/components/ConfirmProvider.js +77 -0
- package/dist/components/DashboardLayout.js +645 -0
- package/dist/components/DataTable.js +546 -0
- package/dist/components/DetailRows.js +42 -0
- package/dist/components/EmptyNote.js +28 -0
- package/dist/components/Eyebrow.js +21 -0
- package/dist/components/HeadingHelp.js +38 -0
- package/dist/components/HeadingLevel.js +18 -0
- package/dist/components/IconTile.js +42 -0
- package/dist/components/ImagePreviewDialog.js +86 -0
- package/dist/components/LabeledField.js +156 -0
- package/dist/components/LanguageSwitcher.js +43 -0
- package/dist/components/ListRow.js +59 -0
- package/dist/components/ModeToggle.js +35 -0
- package/dist/components/PageHeader.js +65 -0
- package/dist/components/PasswordField.js +43 -0
- package/dist/components/PublicLayout.js +27 -0
- package/dist/components/PublicNav.js +17 -0
- package/dist/components/RouteMessage.js +31 -0
- package/dist/components/RowList.js +10 -0
- package/dist/components/SafeHtml.js +57 -0
- package/dist/components/SaveState.js +13 -0
- package/dist/components/SectionPanel.js +48 -0
- package/dist/components/SectionTabs.js +139 -0
- package/dist/components/SiteNav.js +130 -0
- package/dist/components/Skeletons.js +63 -0
- package/dist/components/Sortable.js +81 -0
- package/dist/components/StatTile.js +66 -0
- package/dist/components/StatusChip.js +17 -0
- package/dist/components/StatusDot.js +42 -0
- package/dist/components/SubCard.js +30 -0
- package/dist/components/TabbedCard.js +20 -0
- package/dist/components/TableFilters.js +247 -0
- package/dist/components/ToastProvider.js +65 -0
- package/dist/eslint/chipRules.mjs +65 -0
- package/dist/hooks/useDocumentTitle.js +19 -0
- package/dist/hooks/useOverflowTabs.js +53 -0
- package/dist/hooks/useUnsavedChanges.js +46 -0
- package/dist/i18n/loadMessages.js +15 -0
- package/dist/i18n/mergeMessages.js +13 -0
- package/dist/i18n/messages/en.json +103 -0
- package/dist/i18n/messages/es.json +103 -0
- package/dist/i18n/messages/zh.json +103 -0
- package/dist/lib/authCard.js +4 -0
- package/dist/lib/chipTone.js +46 -0
- package/dist/lib/countryRegionData.js +29 -0
- package/dist/lib/countryRegionData.json +1 -0
- package/dist/lib/navLock.js +22 -0
- package/dist/lib/navTabSx.js +78 -0
- package/dist/lib/orgTime.js +243 -0
- package/dist/lib/pageHtml.js +139 -0
- package/dist/lib/safeNext.js +13 -0
- package/dist/lib/sanitizeHtml.js +36 -0
- package/dist/lib/slugify.js +7 -0
- package/dist/lib/tableFilter.js +147 -0
- package/dist/lib/tableStyle.js +87 -0
- package/dist/test/jsdomShims.js +53 -0
- package/dist/test/themeContract.js +178 -0
- package/dist/theme/OrgThemeRegistry.js +71 -0
- package/dist/theme/ThemeRegistry.js +17 -0
- package/dist/theme/berryNavRow.js +21 -0
- package/dist/theme/createSiteTheme.js +974 -0
- package/dist/theme/themes.js +438 -0
- package/package.json +118 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import Box from "@mui/material/Box";
|
|
6
|
+
import Checkbox from "@mui/material/Checkbox";
|
|
7
|
+
import IconButton from "@mui/material/IconButton";
|
|
8
|
+
import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
|
|
9
|
+
import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRounded";
|
|
10
|
+
import Card from "@mui/material/Card";
|
|
11
|
+
import Paper from "@mui/material/Paper";
|
|
12
|
+
import Stack from "@mui/material/Stack";
|
|
13
|
+
import Table from "@mui/material/Table";
|
|
14
|
+
import TableBody from "@mui/material/TableBody";
|
|
15
|
+
import TableCell from "@mui/material/TableCell";
|
|
16
|
+
import TableContainer from "@mui/material/TableContainer";
|
|
17
|
+
import TableHead from "@mui/material/TableHead";
|
|
18
|
+
import TablePagination from "@mui/material/TablePagination";
|
|
19
|
+
import TableRow from "@mui/material/TableRow";
|
|
20
|
+
import TableSortLabel from "@mui/material/TableSortLabel";
|
|
21
|
+
import MenuItem from "@mui/material/MenuItem";
|
|
22
|
+
import useMediaQuery from "@mui/material/useMediaQuery";
|
|
23
|
+
import Typography from "@mui/material/Typography";
|
|
24
|
+
import { useTheme } from "@mui/material/styles";
|
|
25
|
+
import CardHead, { CardHeadTitle } from "./CardHead.js";
|
|
26
|
+
import LabeledField, { LabeledSelect } from "./LabeledField.js";
|
|
27
|
+
import {
|
|
28
|
+
getCoreRowModel,
|
|
29
|
+
getPaginationRowModel,
|
|
30
|
+
getSortedRowModel,
|
|
31
|
+
useReactTable
|
|
32
|
+
} from "@tanstack/react-table";
|
|
33
|
+
import TableFilters from "./TableFilters.js";
|
|
34
|
+
import { activeCount, applyFilters, filterableColumns } from "../lib/tableFilter.js";
|
|
35
|
+
function RowCards({ rows, columns, selectable, t, expander = null }) {
|
|
36
|
+
const headline = columns.find((c) => c.primary) ?? columns.find((c) => c.value);
|
|
37
|
+
const actions = columns.find((c) => c.id === "actions");
|
|
38
|
+
const rest = columns.filter((c) => c !== headline && c !== actions);
|
|
39
|
+
return /* @__PURE__ */ jsx(Stack, { spacing: 1, sx: { p: 1.5 }, children: rows.map((row) => /* @__PURE__ */ jsxs(
|
|
40
|
+
Paper,
|
|
41
|
+
{
|
|
42
|
+
variant: "outlined",
|
|
43
|
+
sx: { p: 1.5, borderRadius: (th) => `${th.rt?.radii?.card ?? 10}px` },
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, sx: { alignItems: "flex-start" }, children: [
|
|
46
|
+
selectable ? /* @__PURE__ */ jsx(
|
|
47
|
+
Checkbox,
|
|
48
|
+
{
|
|
49
|
+
size: "small",
|
|
50
|
+
sx: { mt: -0.5, ml: -1 },
|
|
51
|
+
checked: row.getIsSelected(),
|
|
52
|
+
disabled: !row.getCanSelect(),
|
|
53
|
+
onChange: row.getToggleSelectedHandler(),
|
|
54
|
+
slotProps: { input: { "aria-label": t("selectRow", { name: rowLabel(row.original, columns) }) } }
|
|
55
|
+
}
|
|
56
|
+
) : null,
|
|
57
|
+
/* @__PURE__ */ jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [
|
|
58
|
+
headline ? /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", sx: { fontWeight: 600, mb: 0.5 }, children: headline.render ? headline.render(row.original) : headline.value?.(row.original) }) : null,
|
|
59
|
+
/* @__PURE__ */ jsx(Stack, { spacing: 0.5, children: rest.map((c) => {
|
|
60
|
+
const cell = c.render ? c.render(row.original) : c.value?.(row.original);
|
|
61
|
+
if (cell == null || cell === "") return null;
|
|
62
|
+
return /* @__PURE__ */ jsxs(
|
|
63
|
+
Stack,
|
|
64
|
+
{
|
|
65
|
+
direction: "row",
|
|
66
|
+
spacing: 1,
|
|
67
|
+
sx: { alignItems: "baseline", justifyContent: "space-between", gap: 1 },
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary", flexShrink: 0 }, children: c.header }),
|
|
70
|
+
/* @__PURE__ */ jsx(Box, { sx: { minWidth: 0, textAlign: "right" }, children: cell })
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
c.id
|
|
74
|
+
);
|
|
75
|
+
}) })
|
|
76
|
+
] })
|
|
77
|
+
] }),
|
|
78
|
+
actions ? /* @__PURE__ */ jsx(Box, { sx: { mt: 1, pt: 1, borderTop: 1, borderColor: "divider", display: "flex", justifyContent: "flex-end" }, children: actions.render ? actions.render(row.original) : null }) : null,
|
|
79
|
+
expander ? /* @__PURE__ */ jsxs(Box, { sx: { mt: 1, pt: 1, borderTop: 1, borderColor: "divider" }, children: [
|
|
80
|
+
/* @__PURE__ */ jsx(ExpandToggle, { row, columns, expander, t }),
|
|
81
|
+
expander.isOpen(row.id) ? /* @__PURE__ */ jsx(Box, { sx: { mt: 1 }, children: expander.render(row.original) }) : null
|
|
82
|
+
] }) : null
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
row.id
|
|
86
|
+
)) });
|
|
87
|
+
}
|
|
88
|
+
function ExpandToggle({ row, columns, expander, t }) {
|
|
89
|
+
const open = expander.isOpen(row.id);
|
|
90
|
+
const name = rowLabel(row.original, columns);
|
|
91
|
+
return /* @__PURE__ */ jsx(
|
|
92
|
+
IconButton,
|
|
93
|
+
{
|
|
94
|
+
size: "small",
|
|
95
|
+
"aria-expanded": open,
|
|
96
|
+
"aria-label": t(open ? "collapseRow" : "expandRow", { name }),
|
|
97
|
+
onClick: () => expander.toggle(row.id),
|
|
98
|
+
children: open ? /* @__PURE__ */ jsx(KeyboardArrowUpRoundedIcon, { fontSize: "small" }) : /* @__PURE__ */ jsx(KeyboardArrowDownRoundedIcon, { fontSize: "small" })
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
function DataTable({
|
|
103
|
+
rows,
|
|
104
|
+
getRowId,
|
|
105
|
+
columns,
|
|
106
|
+
initialSort,
|
|
107
|
+
selectable = false,
|
|
108
|
+
selection = [],
|
|
109
|
+
onSelectionChange,
|
|
110
|
+
filterable = true,
|
|
111
|
+
persistKey,
|
|
112
|
+
searchAlso,
|
|
113
|
+
searchPlaceholder,
|
|
114
|
+
// Show the free-text search box in the filter bar. Default on; set false for a
|
|
115
|
+
// table whose narrowing is entirely dropdowns, so the bar is just its selects.
|
|
116
|
+
searchable = true,
|
|
117
|
+
title = null,
|
|
118
|
+
action = null,
|
|
119
|
+
toolbar = null,
|
|
120
|
+
// A render prop shown BETWEEN the filter bar and the table header, given the
|
|
121
|
+
// currently-visible (filtered) rows: `banner={(rows) => node}`. Auto-updates as
|
|
122
|
+
// filters change, since it is handed the same `visible` set the table renders.
|
|
123
|
+
// For an overview strip (a chart, a summary) that tracks the filters.
|
|
124
|
+
banner = null,
|
|
125
|
+
empty = null,
|
|
126
|
+
emptyFiltered = null,
|
|
127
|
+
stickyHeader = false,
|
|
128
|
+
maxHeight = "fill",
|
|
129
|
+
paginate = false,
|
|
130
|
+
pageSize = 25,
|
|
131
|
+
rowsPerPageOptions = [10, 25, 50, 100],
|
|
132
|
+
/*
|
|
133
|
+
* SERVER-SIDE mode. Omit it and this table behaves exactly as it always has —
|
|
134
|
+
* it holds every row and filters, sorts and pages them in the browser.
|
|
135
|
+
*
|
|
136
|
+
* Pass it and those three become the SERVER's job: `rows` is one page that has
|
|
137
|
+
* already been narrowed and ordered, and this component only renders the
|
|
138
|
+
* controls and reports what the admin asked for.
|
|
139
|
+
*
|
|
140
|
+
* manual={{
|
|
141
|
+
* rowCount, // total matching rows, all pages
|
|
142
|
+
* state: { page, pageSize, sorting, filters },
|
|
143
|
+
* onChange: (next) => void, // a partial of `state`
|
|
144
|
+
* }}
|
|
145
|
+
*
|
|
146
|
+
* It exists because a large list outgrows the browser: holding every row is
|
|
147
|
+
* what makes client-side filtering possible, and it is exactly what does not
|
|
148
|
+
* scale. Opt-in rather than a rewrite, so tables that fit in memory are
|
|
149
|
+
* untouched.
|
|
150
|
+
*/
|
|
151
|
+
manual = null,
|
|
152
|
+
renderExpanded = null
|
|
153
|
+
}) {
|
|
154
|
+
const t = useTranslations("uikit.table");
|
|
155
|
+
const [expandedIds, setExpandedIds] = useState(() => /* @__PURE__ */ new Set());
|
|
156
|
+
const expander = renderExpanded ? {
|
|
157
|
+
isOpen: (id) => expandedIds.has(id),
|
|
158
|
+
toggle: (id) => setExpandedIds((prev) => {
|
|
159
|
+
const next = new Set(prev);
|
|
160
|
+
if (next.has(id)) next.delete(id);
|
|
161
|
+
else next.add(id);
|
|
162
|
+
return next;
|
|
163
|
+
}),
|
|
164
|
+
render: renderExpanded
|
|
165
|
+
} : null;
|
|
166
|
+
const theme = useTheme();
|
|
167
|
+
const berrySurface = theme.rt?.ui?.surface === "berry";
|
|
168
|
+
const Surface = berrySurface ? Card : Paper;
|
|
169
|
+
const phone = useMediaQuery(theme.breakpoints.down("sm"), { noSsr: true });
|
|
170
|
+
const [ownFilters, setFilterState] = useState(() => restoreFilters(persistKey));
|
|
171
|
+
const [ownSorting, setSorting] = useState(() => restoreSort(persistKey) ?? (initialSort ? [initialSort] : []));
|
|
172
|
+
const [typedQuery, setTypedQuery] = useState(null);
|
|
173
|
+
const searchTimer = useRef(null);
|
|
174
|
+
useEffect(() => () => clearTimeout(searchTimer.current), []);
|
|
175
|
+
const filters = manual ? manual.state.filters ?? {} : ownFilters;
|
|
176
|
+
const sorting = manual ? manual.state.sorting ?? [] : ownSorting;
|
|
177
|
+
const canFilter = filterable && filterableColumns(columns).length > 0;
|
|
178
|
+
const hasFilters = activeCount(filters) > 0;
|
|
179
|
+
const visible = useMemo(
|
|
180
|
+
() => canFilter && !manual ? applyFilters(rows, columns, filters, searchAlso) : rows,
|
|
181
|
+
[canFilter, manual, rows, columns, filters, searchAlso]
|
|
182
|
+
);
|
|
183
|
+
const setFilters = (next) => {
|
|
184
|
+
if (manual) {
|
|
185
|
+
const typing = next.query !== filters.query && JSON.stringify(next.byColumn ?? {}) === JSON.stringify(filters.byColumn ?? {});
|
|
186
|
+
clearTimeout(searchTimer.current);
|
|
187
|
+
if (typing) {
|
|
188
|
+
setTypedQuery(next.query);
|
|
189
|
+
searchTimer.current = setTimeout(() => {
|
|
190
|
+
rememberFilters(persistKey, next);
|
|
191
|
+
if (selection.length > 0) onSelectionChange?.([]);
|
|
192
|
+
manual.onChange({ filters: next, page: 0 });
|
|
193
|
+
}, SEARCH_SETTLE);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
setTypedQuery(null);
|
|
197
|
+
rememberFilters(persistKey, next);
|
|
198
|
+
if (selection.length > 0) onSelectionChange?.([]);
|
|
199
|
+
manual.onChange({ filters: next, page: 0 });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
setFilterState(next);
|
|
203
|
+
rememberFilters(persistKey, next);
|
|
204
|
+
if (!canFilter || selection.length === 0) return;
|
|
205
|
+
const keep = new Set(applyFilters(rows, columns, next, searchAlso).map((r) => getRowId(r)));
|
|
206
|
+
const kept = selection.filter((id) => keep.has(id));
|
|
207
|
+
if (kept.length !== selection.length) onSelectionChange?.(kept);
|
|
208
|
+
};
|
|
209
|
+
const visibleIds = useMemo(() => new Set(visible.map((r) => getRowId(r))), [visible, getRowId]);
|
|
210
|
+
const effectiveSelection = useMemo(
|
|
211
|
+
() => manual ? selection : selection.filter((id) => visibleIds.has(id)),
|
|
212
|
+
[manual, selection, visibleIds]
|
|
213
|
+
);
|
|
214
|
+
const shown = useMemo(() => columns.filter((c) => !c.hidden), [columns]);
|
|
215
|
+
const sortable = useMemo(() => shown.filter((c) => c.sortable !== false && Boolean(c.value)), [shown]);
|
|
216
|
+
const columnDefs = useMemo(() => shown.map((c) => ({
|
|
217
|
+
id: c.id,
|
|
218
|
+
accessorFn: c.value ? (row) => c.value(row) ?? void 0 : () => void 0,
|
|
219
|
+
enableSorting: c.sortable !== false && Boolean(c.value),
|
|
220
|
+
sortUndefined: "last"
|
|
221
|
+
})), [shown]);
|
|
222
|
+
const table = useReactTable({
|
|
223
|
+
data: visible,
|
|
224
|
+
columns: columnDefs,
|
|
225
|
+
/*
|
|
226
|
+
* Keyed on the ROW's own id, never the array index. TanStack stores the
|
|
227
|
+
* selection against this key, so an index would quietly re-point every
|
|
228
|
+
* selection at a different record the moment the sort changed — you tick
|
|
229
|
+
* three rows, sort, and are now holding three others with no visible clue.
|
|
230
|
+
*/
|
|
231
|
+
getRowId,
|
|
232
|
+
state: {
|
|
233
|
+
sorting,
|
|
234
|
+
rowSelection: Object.fromEntries(effectiveSelection.map((id) => [id, true])),
|
|
235
|
+
// Controlled only in manual mode; otherwise TanStack keeps its own and
|
|
236
|
+
// `initialState` below seeds the page size.
|
|
237
|
+
...manual ? { pagination: { pageIndex: manual.state.page ?? 0, pageSize: manual.state.pageSize ?? pageSize } } : {}
|
|
238
|
+
},
|
|
239
|
+
onSortingChange: (updater) => {
|
|
240
|
+
const next = typeof updater === "function" ? updater(sorting) : updater;
|
|
241
|
+
rememberSort(persistKey, next);
|
|
242
|
+
if (manual) return manual.onChange({ sorting: next, page: 0 });
|
|
243
|
+
return setSorting(next);
|
|
244
|
+
},
|
|
245
|
+
onRowSelectionChange: (updater) => {
|
|
246
|
+
const current = Object.fromEntries(effectiveSelection.map((id) => [id, true]));
|
|
247
|
+
const next = typeof updater === "function" ? updater(current) : updater;
|
|
248
|
+
onSelectionChange?.(Object.keys(next).filter((id) => next[id]));
|
|
249
|
+
},
|
|
250
|
+
/*
|
|
251
|
+
* `selectable` may be a predicate, not just a flag — e.g. to make drafts
|
|
252
|
+
* unselectable. That can be load-bearing rather than cosmetic: if a bulk
|
|
253
|
+
* endpoint is all-or-nothing and rejects a selection containing a draft, a
|
|
254
|
+
* selectable draft turns "add a note to these five" into a flat "Not
|
|
255
|
+
* found" with nothing naming the row that caused it.
|
|
256
|
+
*/
|
|
257
|
+
enableRowSelection: typeof selectable === "function" ? (row) => selectable(row.original) : selectable,
|
|
258
|
+
// First click always ascending, and a third click sorts again rather than
|
|
259
|
+
// dropping back to "unsorted" — an admin toggling a column wants the other
|
|
260
|
+
// direction, not the original order back.
|
|
261
|
+
sortDescFirst: false,
|
|
262
|
+
enableSortingRemoval: false,
|
|
263
|
+
getCoreRowModel: getCoreRowModel(),
|
|
264
|
+
getSortedRowModel: getSortedRowModel(),
|
|
265
|
+
/*
|
|
266
|
+
* In manual mode the server has already sorted and paged, so BOTH row
|
|
267
|
+
* models must stand down — re-sorting one page would reorder it against the
|
|
268
|
+
* server's answer, and re-paging twenty-five rows into pages of twenty-five
|
|
269
|
+
* is a no-op that still resets the index. `rowCount` is what the pager
|
|
270
|
+
* counts, since the rows to count are not here.
|
|
271
|
+
*/
|
|
272
|
+
...manual ? {
|
|
273
|
+
manualSorting: true,
|
|
274
|
+
manualPagination: true,
|
|
275
|
+
rowCount: manual.rowCount ?? 0,
|
|
276
|
+
onPaginationChange: (updater) => {
|
|
277
|
+
const current = { pageIndex: manual.state.page ?? 0, pageSize: manual.state.pageSize ?? pageSize };
|
|
278
|
+
const next = typeof updater === "function" ? updater(current) : updater;
|
|
279
|
+
manual.onChange({ page: next.pageIndex, pageSize: next.pageSize });
|
|
280
|
+
}
|
|
281
|
+
} : {},
|
|
282
|
+
// Pagination is opt-in: without the row model TanStack returns every row, so
|
|
283
|
+
// a non-paginated table is unchanged. autoResetPageIndex (on by default)
|
|
284
|
+
// snaps back to page 1 when a filter shrinks the set out from under you.
|
|
285
|
+
...paginate && !manual ? {
|
|
286
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
287
|
+
initialState: { pagination: { pageSize } }
|
|
288
|
+
} : {}
|
|
289
|
+
});
|
|
290
|
+
const displayed = table.getRowModel().rows;
|
|
291
|
+
const choosable = displayed.filter((r) => r.getCanSelect());
|
|
292
|
+
const allSelected = choosable.length > 0 && choosable.every((r) => r.getIsSelected());
|
|
293
|
+
const someSelected = choosable.some((r) => r.getIsSelected());
|
|
294
|
+
const paperRef = useRef(null);
|
|
295
|
+
const wantFill = stickyHeader && maxHeight === "fill";
|
|
296
|
+
const [fillHeight, setFillHeight] = useState(null);
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (!wantFill) return void 0;
|
|
299
|
+
const recompute = () => {
|
|
300
|
+
const el = paperRef.current;
|
|
301
|
+
const vh = window.innerHeight;
|
|
302
|
+
if (!el || !vh) return;
|
|
303
|
+
const top = el.getBoundingClientRect().top + window.scrollY;
|
|
304
|
+
setFillHeight(`${Math.max(240, Math.round(vh - top - 24))}px`);
|
|
305
|
+
};
|
|
306
|
+
recompute();
|
|
307
|
+
window.addEventListener("resize", recompute);
|
|
308
|
+
return () => window.removeEventListener("resize", recompute);
|
|
309
|
+
}, [wantFill]);
|
|
310
|
+
const resolvedMaxHeight = wantFill ? fillHeight ?? "70dvh" : maxHeight;
|
|
311
|
+
return (
|
|
312
|
+
/*
|
|
313
|
+
* The toolbar lives INSIDE the card, above the table and divided from it,
|
|
314
|
+
* rather than floating in the space above. Filters that control a table
|
|
315
|
+
* should read as part of it — detached, they look like two stray form
|
|
316
|
+
* fields someone left on the page.
|
|
317
|
+
*
|
|
318
|
+
* It also renders when there are no rows, which is not incidental: filter
|
|
319
|
+
* everything out and an early return would take the search box with it,
|
|
320
|
+
* leaving no way to undo the filter that emptied the table.
|
|
321
|
+
*/
|
|
322
|
+
/*
|
|
323
|
+
* A berry chrome draws a CARD as plain paper on a grey ground — no border,
|
|
324
|
+
* a drop shadow (see createSiteTheme). The table's card is one of those
|
|
325
|
+
* cards, so on that surface it is rendered as a Card and wears the same
|
|
326
|
+
* skin; everywhere else it stays outlined, as its neighbours are. An
|
|
327
|
+
* outlined table beside borderless cards read as another material.
|
|
328
|
+
*/
|
|
329
|
+
/* @__PURE__ */ jsxs(
|
|
330
|
+
Surface,
|
|
331
|
+
{
|
|
332
|
+
ref: paperRef,
|
|
333
|
+
variant: berrySurface ? "elevation" : "outlined",
|
|
334
|
+
elevation: 0,
|
|
335
|
+
sx: {
|
|
336
|
+
borderRadius: (t2) => `${t2.rt?.radii?.card ?? 10}px`,
|
|
337
|
+
overflow: "hidden",
|
|
338
|
+
// Sticky = the PAPER is the bounded box, a flex column, and only the rows
|
|
339
|
+
// scroll inside it. Capping the TableContainer alone left the filter bar,
|
|
340
|
+
// selection toolbar, cross-page prompt and pager to stack ON TOP of a
|
|
341
|
+
// full-height scroll region, pushing the pager below the fold whenever the
|
|
342
|
+
// selection UI appeared. Bounding the Paper keeps every one of those, the
|
|
343
|
+
// pager included, on screen.
|
|
344
|
+
...stickyHeader ? { display: "flex", flexDirection: "column", maxHeight: resolvedMaxHeight } : {}
|
|
345
|
+
},
|
|
346
|
+
children: [
|
|
347
|
+
!canFilter && (title || action) ? /* @__PURE__ */ jsx(CardHead, { title, action }) : null,
|
|
348
|
+
canFilter ? /* @__PURE__ */ jsx(Box, { sx: { borderBottom: 1, borderColor: "divider" }, children: /* @__PURE__ */ jsx(
|
|
349
|
+
TableFilters,
|
|
350
|
+
{
|
|
351
|
+
columns,
|
|
352
|
+
rows,
|
|
353
|
+
filters: typedQuery === null ? filters : { ...filters, query: typedQuery },
|
|
354
|
+
onChange: setFilters,
|
|
355
|
+
searchPlaceholder,
|
|
356
|
+
searchable,
|
|
357
|
+
resultCount: manual ? manual.rowCount ?? 0 : visible.length,
|
|
358
|
+
totalCount: manual ? manual.unfilteredCount ?? manual.rowCount ?? 0 : rows.length,
|
|
359
|
+
lead: title ? /* @__PURE__ */ jsx(CardHeadTitle, { children: title }) : null,
|
|
360
|
+
trailing: action
|
|
361
|
+
}
|
|
362
|
+
) }) : null,
|
|
363
|
+
toolbar && effectiveSelection.length > 0 ? /* @__PURE__ */ jsx(
|
|
364
|
+
Box,
|
|
365
|
+
{
|
|
366
|
+
sx: {
|
|
367
|
+
px: 2,
|
|
368
|
+
py: 1,
|
|
369
|
+
borderBottom: 1,
|
|
370
|
+
borderColor: "divider",
|
|
371
|
+
bgcolor: (t2) => `rgba(${t2.vars.palette.primary.mainChannel} / 0.08)`
|
|
372
|
+
},
|
|
373
|
+
children: toolbar({
|
|
374
|
+
selection: effectiveSelection,
|
|
375
|
+
clear: () => onSelectionChange?.([])
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
) : null,
|
|
379
|
+
banner && visible.length > 0 ? /* @__PURE__ */ jsx(Box, { sx: { borderBottom: 1, borderColor: "divider" }, children: banner(visible) }) : null,
|
|
380
|
+
visible.length === 0 ? (
|
|
381
|
+
// Two different nothings. An empty table and a filter that matched none
|
|
382
|
+
// of a full one are not the same event, and saying "Nothing here yet"
|
|
383
|
+
// to the second reads as data loss.
|
|
384
|
+
/* @__PURE__ */ jsx(Box, { sx: { p: 3 }, children: (manual ? !hasFilters : rows.length === 0) ? empty : emptyFiltered ?? empty })
|
|
385
|
+
) : (
|
|
386
|
+
/* When sticky, this is the only part that scrolls (both axes): it flexes to
|
|
387
|
+
fill whatever the bounded Paper leaves after the filters/toolbar/prompt/
|
|
388
|
+
pager, so the header pins and wide tables still scroll sideways. A
|
|
389
|
+
page-level sticky header and horizontal scroll cannot share one element:
|
|
390
|
+
overflow-x:auto (needed for wide tables) makes the container a scroll box
|
|
391
|
+
that also traps vertical stickiness — hence the bounded region. */
|
|
392
|
+
/*
|
|
393
|
+
* PHONE: cards. Rendered INSTEAD of the table, not alongside it — two
|
|
394
|
+
* copies of every row would double the DOM and hand a screen reader the
|
|
395
|
+
* whole list twice.
|
|
396
|
+
*/
|
|
397
|
+
phone ? /* @__PURE__ */ jsxs(Box, { sx: { flex: stickyHeader ? 1 : void 0, minHeight: 0, overflowY: "auto" }, children: [
|
|
398
|
+
sortable.length > 0 ? (
|
|
399
|
+
/*
|
|
400
|
+
* The column headers ARE the sort control on a table, and a card
|
|
401
|
+
* list has none — so without this, sorting simply vanished on a
|
|
402
|
+
* phone rather than being unavailable for a reason.
|
|
403
|
+
*/
|
|
404
|
+
/* @__PURE__ */ jsx(Box, { sx: { px: 1.5, pt: 1.5 }, children: /* @__PURE__ */ jsx(LabeledField, { label: t("sortBy"), children: ({ labelId }) => /* @__PURE__ */ jsx(
|
|
405
|
+
LabeledSelect,
|
|
406
|
+
{
|
|
407
|
+
labelId,
|
|
408
|
+
size: "small",
|
|
409
|
+
value: sorting[0]?.id ?? "",
|
|
410
|
+
onChange: (e) => {
|
|
411
|
+
const next = [{ id: e.target.value, desc: false }];
|
|
412
|
+
if (manual) manual.onChange({ sorting: next, page: 0 });
|
|
413
|
+
else setSorting(next);
|
|
414
|
+
},
|
|
415
|
+
children: sortable.map((c) => /* @__PURE__ */ jsx(MenuItem, { value: c.id, children: c.header }, c.id))
|
|
416
|
+
}
|
|
417
|
+
) }) })
|
|
418
|
+
) : null,
|
|
419
|
+
/* @__PURE__ */ jsx(RowCards, { rows: displayed, columns: shown, selectable, t, expander })
|
|
420
|
+
] }) : /* @__PURE__ */ jsx(TableContainer, { sx: stickyHeader ? { flex: 1, minHeight: 0 } : void 0, children: /* @__PURE__ */ jsxs(
|
|
421
|
+
Table,
|
|
422
|
+
{
|
|
423
|
+
stickyHeader,
|
|
424
|
+
sx: stickyHeader ? {
|
|
425
|
+
"& .MuiTableCell-stickyHeader": { backgroundColor: "background.paper" }
|
|
426
|
+
} : void 0,
|
|
427
|
+
children: [
|
|
428
|
+
/* @__PURE__ */ jsx(TableHead, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
429
|
+
expander ? /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }) : null,
|
|
430
|
+
selectable ? /* @__PURE__ */ jsx(TableCell, { padding: "checkbox", children: /* @__PURE__ */ jsx(
|
|
431
|
+
Checkbox,
|
|
432
|
+
{
|
|
433
|
+
size: "small",
|
|
434
|
+
checked: allSelected,
|
|
435
|
+
indeterminate: someSelected && !allSelected,
|
|
436
|
+
onChange: (e) => table.toggleAllPageRowsSelected(e.target.checked),
|
|
437
|
+
slotProps: { input: { "aria-label": t("selectAll") } }
|
|
438
|
+
}
|
|
439
|
+
) }) : null,
|
|
440
|
+
table.getHeaderGroups()[0].headers.map((header) => {
|
|
441
|
+
const col = shown.find((c) => c.id === header.column.id);
|
|
442
|
+
const dir = header.column.getIsSorted();
|
|
443
|
+
return /* @__PURE__ */ jsx(TableCell, { align: col.align, sortDirection: dir || false, children: header.column.getCanSort() ? /* @__PURE__ */ jsx(
|
|
444
|
+
TableSortLabel,
|
|
445
|
+
{
|
|
446
|
+
active: Boolean(dir),
|
|
447
|
+
direction: dir || "asc",
|
|
448
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
449
|
+
children: col.header
|
|
450
|
+
}
|
|
451
|
+
) : col.header }, col.id);
|
|
452
|
+
})
|
|
453
|
+
] }) }),
|
|
454
|
+
/* @__PURE__ */ jsx(TableBody, { children: displayed.map((row) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
455
|
+
/* @__PURE__ */ jsxs(TableRow, { hover: true, selected: row.getIsSelected(), children: [
|
|
456
|
+
expander ? /* @__PURE__ */ jsx(TableCell, { padding: "checkbox", children: /* @__PURE__ */ jsx(ExpandToggle, { row, columns: shown, expander, t }) }) : null,
|
|
457
|
+
selectable ? /* @__PURE__ */ jsx(TableCell, { padding: "checkbox", children: /* @__PURE__ */ jsx(
|
|
458
|
+
Checkbox,
|
|
459
|
+
{
|
|
460
|
+
size: "small",
|
|
461
|
+
checked: row.getIsSelected(),
|
|
462
|
+
disabled: !row.getCanSelect(),
|
|
463
|
+
onChange: row.getToggleSelectedHandler(),
|
|
464
|
+
slotProps: { input: { "aria-label": t("selectRow", { name: rowLabel(row.original, shown) }) } }
|
|
465
|
+
}
|
|
466
|
+
) }) : null,
|
|
467
|
+
shown.map((c) => /* @__PURE__ */ jsx(TableCell, { align: c.align, children: c.render ? c.render(row.original) : c.value?.(row.original) }, c.id))
|
|
468
|
+
] }),
|
|
469
|
+
expander?.isOpen(row.id) ? /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
|
|
470
|
+
TableCell,
|
|
471
|
+
{
|
|
472
|
+
colSpan: shown.length + (selectable ? 1 : 0) + 1,
|
|
473
|
+
sx: { py: 1.5, pl: 7, bgcolor: "action.hover" },
|
|
474
|
+
children: expander.render(row.original)
|
|
475
|
+
}
|
|
476
|
+
) }) : null
|
|
477
|
+
] }, row.id)) })
|
|
478
|
+
]
|
|
479
|
+
}
|
|
480
|
+
) })
|
|
481
|
+
),
|
|
482
|
+
paginate && (visible.length > 0 || manual && (manual.rowCount ?? 0) > 0) ? /* @__PURE__ */ jsx(
|
|
483
|
+
TablePagination,
|
|
484
|
+
{
|
|
485
|
+
component: "div",
|
|
486
|
+
count: manual ? manual.rowCount ?? 0 : visible.length,
|
|
487
|
+
page: table.getState().pagination.pageIndex,
|
|
488
|
+
rowsPerPage: table.getState().pagination.pageSize,
|
|
489
|
+
rowsPerPageOptions,
|
|
490
|
+
onPageChange: (_e, next) => table.setPageIndex(next),
|
|
491
|
+
onRowsPerPageChange: (e) => table.setPageSize(parseInt(e.target.value, 10)),
|
|
492
|
+
labelRowsPerPage: t("rowsPerPage"),
|
|
493
|
+
labelDisplayedRows: ({ from, to, count }) => t("displayedRows", { from, to, count }),
|
|
494
|
+
sx: { borderTop: 1, borderColor: "divider", flexShrink: 0 }
|
|
495
|
+
}
|
|
496
|
+
) : null
|
|
497
|
+
]
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const SEARCH_SETTLE = 250;
|
|
503
|
+
const NO_FILTERS = { query: "", byColumn: {} };
|
|
504
|
+
function restoreFilters(key) {
|
|
505
|
+
if (!key || typeof window === "undefined") return NO_FILTERS;
|
|
506
|
+
try {
|
|
507
|
+
const stored = JSON.parse(window.localStorage.getItem(key) ?? "null");
|
|
508
|
+
return {
|
|
509
|
+
query: typeof stored?.query === "string" ? stored.query : "",
|
|
510
|
+
byColumn: stored?.byColumn && typeof stored.byColumn === "object" ? stored.byColumn : {}
|
|
511
|
+
};
|
|
512
|
+
} catch {
|
|
513
|
+
return NO_FILTERS;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function restoreSort(key) {
|
|
517
|
+
if (!key || typeof window === "undefined") return null;
|
|
518
|
+
try {
|
|
519
|
+
const stored = JSON.parse(window.localStorage.getItem(`${key}:sort`) ?? "null");
|
|
520
|
+
return Array.isArray(stored) && stored.every((s) => typeof s?.id === "string") ? stored : null;
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function rememberSort(key, sorting) {
|
|
526
|
+
if (!key || typeof window === "undefined") return;
|
|
527
|
+
try {
|
|
528
|
+
window.localStorage.setItem(`${key}:sort`, JSON.stringify(sorting));
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function rememberFilters(key, filters) {
|
|
533
|
+
if (!key || typeof window === "undefined") return;
|
|
534
|
+
try {
|
|
535
|
+
if (activeCount(filters) === 0) window.localStorage.removeItem(key);
|
|
536
|
+
else window.localStorage.setItem(key, JSON.stringify(filters));
|
|
537
|
+
} catch {
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
function rowLabel(row, columns) {
|
|
541
|
+
const first = columns.find((c) => c.value);
|
|
542
|
+
return first ? String(first.value(row) ?? "row") : "row";
|
|
543
|
+
}
|
|
544
|
+
export {
|
|
545
|
+
DataTable as default
|
|
546
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Box from "@mui/material/Box";
|
|
4
|
+
import Typography from "@mui/material/Typography";
|
|
5
|
+
function DetailRows({ rows }) {
|
|
6
|
+
return /* @__PURE__ */ jsx(Box, { children: rows.map((row, i) => /* @__PURE__ */ jsxs(
|
|
7
|
+
Box,
|
|
8
|
+
{
|
|
9
|
+
sx: {
|
|
10
|
+
display: "grid",
|
|
11
|
+
gridTemplateColumns: { xs: "1fr", sm: "190px minmax(0, 1fr)" },
|
|
12
|
+
columnGap: 2.5,
|
|
13
|
+
rowGap: 0.5,
|
|
14
|
+
alignItems: "start",
|
|
15
|
+
py: 1.25,
|
|
16
|
+
...i > 0 ? { borderTop: "1px solid", borderColor: "divider" } : null
|
|
17
|
+
},
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ jsx(
|
|
20
|
+
Typography,
|
|
21
|
+
{
|
|
22
|
+
variant: "body1",
|
|
23
|
+
sx: { color: "text.secondary", fontWeight: 500, wordBreak: "break-word", ...row.indent ? { pl: { sm: 2 } } : null },
|
|
24
|
+
children: row.label
|
|
25
|
+
}
|
|
26
|
+
),
|
|
27
|
+
/* @__PURE__ */ jsx(Box, { sx: { minWidth: 0 }, children: typeof row.value === "string" || typeof row.value === "number" ? /* @__PURE__ */ jsx(
|
|
28
|
+
Typography,
|
|
29
|
+
{
|
|
30
|
+
variant: "body1",
|
|
31
|
+
sx: { color: row.value === "" || row.value === null ? "text.secondary" : "text.primary", whiteSpace: "pre-wrap", wordBreak: "break-word" },
|
|
32
|
+
children: row.value === "" || row.value === null ? /* @__PURE__ */ jsx("em", { children: "\u2014" }) : row.value
|
|
33
|
+
}
|
|
34
|
+
) : row.value })
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
row.key ?? i
|
|
38
|
+
)) });
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
DetailRows as default
|
|
42
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Box from "@mui/material/Box";
|
|
3
|
+
import Typography from "@mui/material/Typography";
|
|
4
|
+
import { useTheme } from "@mui/material/styles";
|
|
5
|
+
function EmptyNote({ children, action }) {
|
|
6
|
+
const { rt } = useTheme();
|
|
7
|
+
const flat = rt?.ui?.surface === "flat";
|
|
8
|
+
return /* @__PURE__ */ jsxs(
|
|
9
|
+
Box,
|
|
10
|
+
{
|
|
11
|
+
sx: {
|
|
12
|
+
px: 3,
|
|
13
|
+
py: 3.5,
|
|
14
|
+
textAlign: "center",
|
|
15
|
+
borderRadius: flat ? `${rt?.radii?.card ?? 4}px` : "12px",
|
|
16
|
+
border: flat ? "1px solid" : "1px dashed",
|
|
17
|
+
borderColor: "divider"
|
|
18
|
+
},
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children }),
|
|
21
|
+
action ? /* @__PURE__ */ jsx(Box, { sx: { mt: 2 }, children: action }) : null
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
EmptyNote as default
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import Typography from "@mui/material/Typography";
|
|
4
|
+
import { useTheme } from "@mui/material/styles";
|
|
5
|
+
function Eyebrow({ children, component = "p", sx }) {
|
|
6
|
+
const { rt } = useTheme();
|
|
7
|
+
const glass = rt?.ui?.surface === "glass";
|
|
8
|
+
const fontFamily = glass ? rt?.mono : rt?.headingFont ?? void 0;
|
|
9
|
+
return /* @__PURE__ */ jsx(
|
|
10
|
+
Typography,
|
|
11
|
+
{
|
|
12
|
+
component,
|
|
13
|
+
variant: "overline",
|
|
14
|
+
sx: [{ display: "block", color: "text.secondary", lineHeight: 1.4, fontWeight: 600, fontFamily }, ...Array.isArray(sx) ? sx : [sx]],
|
|
15
|
+
children
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
Eyebrow as default
|
|
21
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import IconButton from "@mui/material/IconButton";
|
|
4
|
+
import Tooltip from "@mui/material/Tooltip";
|
|
5
|
+
import HelpOutlineRoundedIcon from "@mui/icons-material/HelpOutlineRounded";
|
|
6
|
+
function HeadingHelp({ text, label, size = "small", color = "text.secondary" }) {
|
|
7
|
+
if (!text) return null;
|
|
8
|
+
const onBar = color === "inherit";
|
|
9
|
+
return /* @__PURE__ */ jsx(
|
|
10
|
+
Tooltip,
|
|
11
|
+
{
|
|
12
|
+
title: text,
|
|
13
|
+
describeChild: true,
|
|
14
|
+
arrow: true,
|
|
15
|
+
enterTouchDelay: 0,
|
|
16
|
+
leaveTouchDelay: 6e3,
|
|
17
|
+
slotProps: { tooltip: { sx: { fontSize: "0.8125rem", lineHeight: 1.5, py: 1, px: 1.5, maxWidth: 320 } } },
|
|
18
|
+
children: /* @__PURE__ */ jsx(
|
|
19
|
+
IconButton,
|
|
20
|
+
{
|
|
21
|
+
size,
|
|
22
|
+
"aria-label": label ? `Help: ${label}` : "Help",
|
|
23
|
+
sx: {
|
|
24
|
+
color,
|
|
25
|
+
p: 0.25,
|
|
26
|
+
// On a coloured bar there is no accent to move to, so deepen the same
|
|
27
|
+
// colour on hover; in body text move to the org accent as elsewhere.
|
|
28
|
+
...onBar ? { opacity: 0.85, "&:hover": { opacity: 1 } } : { "&:hover": { color: "primary.main" } }
|
|
29
|
+
},
|
|
30
|
+
children: /* @__PURE__ */ jsx(HelpOutlineRoundedIcon, { sx: { fontSize: 18 } })
|
|
31
|
+
}
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
HeadingHelp as default
|
|
38
|
+
};
|