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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +32 -0
  4. package/README.md +282 -0
  5. package/dist/components/AddressFields.js +83 -0
  6. package/dist/components/AuthCard.js +94 -0
  7. package/dist/components/AuthSplash.js +88 -0
  8. package/dist/components/BrandHeader.js +81 -0
  9. package/dist/components/BrandLink.js +22 -0
  10. package/dist/components/BrandMark.js +69 -0
  11. package/dist/components/BrandProvider.js +14 -0
  12. package/dist/components/Breadcrumbs.js +62 -0
  13. package/dist/components/CardHead.js +46 -0
  14. package/dist/components/ConfirmProvider.js +77 -0
  15. package/dist/components/DashboardLayout.js +645 -0
  16. package/dist/components/DataTable.js +546 -0
  17. package/dist/components/DetailRows.js +42 -0
  18. package/dist/components/EmptyNote.js +28 -0
  19. package/dist/components/Eyebrow.js +21 -0
  20. package/dist/components/HeadingHelp.js +38 -0
  21. package/dist/components/HeadingLevel.js +18 -0
  22. package/dist/components/IconTile.js +42 -0
  23. package/dist/components/ImagePreviewDialog.js +86 -0
  24. package/dist/components/LabeledField.js +156 -0
  25. package/dist/components/LanguageSwitcher.js +43 -0
  26. package/dist/components/ListRow.js +59 -0
  27. package/dist/components/ModeToggle.js +35 -0
  28. package/dist/components/PageHeader.js +65 -0
  29. package/dist/components/PasswordField.js +43 -0
  30. package/dist/components/PublicLayout.js +27 -0
  31. package/dist/components/PublicNav.js +17 -0
  32. package/dist/components/RouteMessage.js +31 -0
  33. package/dist/components/RowList.js +10 -0
  34. package/dist/components/SafeHtml.js +57 -0
  35. package/dist/components/SaveState.js +13 -0
  36. package/dist/components/SectionPanel.js +48 -0
  37. package/dist/components/SectionTabs.js +139 -0
  38. package/dist/components/SiteNav.js +130 -0
  39. package/dist/components/Skeletons.js +63 -0
  40. package/dist/components/Sortable.js +81 -0
  41. package/dist/components/StatTile.js +66 -0
  42. package/dist/components/StatusChip.js +17 -0
  43. package/dist/components/StatusDot.js +42 -0
  44. package/dist/components/SubCard.js +30 -0
  45. package/dist/components/TabbedCard.js +20 -0
  46. package/dist/components/TableFilters.js +247 -0
  47. package/dist/components/ToastProvider.js +65 -0
  48. package/dist/eslint/chipRules.mjs +65 -0
  49. package/dist/hooks/useDocumentTitle.js +19 -0
  50. package/dist/hooks/useOverflowTabs.js +53 -0
  51. package/dist/hooks/useUnsavedChanges.js +46 -0
  52. package/dist/i18n/loadMessages.js +15 -0
  53. package/dist/i18n/mergeMessages.js +13 -0
  54. package/dist/i18n/messages/en.json +103 -0
  55. package/dist/i18n/messages/es.json +103 -0
  56. package/dist/i18n/messages/zh.json +103 -0
  57. package/dist/lib/authCard.js +4 -0
  58. package/dist/lib/chipTone.js +46 -0
  59. package/dist/lib/countryRegionData.js +29 -0
  60. package/dist/lib/countryRegionData.json +1 -0
  61. package/dist/lib/navLock.js +22 -0
  62. package/dist/lib/navTabSx.js +78 -0
  63. package/dist/lib/orgTime.js +243 -0
  64. package/dist/lib/pageHtml.js +139 -0
  65. package/dist/lib/safeNext.js +13 -0
  66. package/dist/lib/sanitizeHtml.js +36 -0
  67. package/dist/lib/slugify.js +7 -0
  68. package/dist/lib/tableFilter.js +147 -0
  69. package/dist/lib/tableStyle.js +87 -0
  70. package/dist/test/jsdomShims.js +53 -0
  71. package/dist/test/themeContract.js +178 -0
  72. package/dist/theme/OrgThemeRegistry.js +71 -0
  73. package/dist/theme/ThemeRegistry.js +17 -0
  74. package/dist/theme/berryNavRow.js +21 -0
  75. package/dist/theme/createSiteTheme.js +974 -0
  76. package/dist/theme/themes.js +438 -0
  77. 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
+ };