@adapttable/mui 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1,1884 +1,1751 @@
1
- import { rowClickProps, resolveDisabledReason, runRowAction, useSavedViews, useTableChrome, useFilterTriggerToggle, useChromeScrollReset, useChromeBodyData, useTableData, isDeclarativeFilters, resolveLabels, ACTIONS_COLUMN_KEY, useColumnDragState, columnMenuRows, GripIcon, columnReorderKeyProps, nextPinSide, pinActionLabel, resolveVirtualRows, tableRenderModel, headerGroupRow, useHorizontalOverflow, PIN_Z, pinnedCellStyle, tableMinWidth, columnResizeHandleProps, pageSizeOptions, useBulkActionRunner, EyeIcon, PinIcon, edgePinStyle, pinnedColumnWidth, RANGE_SUFFIXES, RANGE_OP_LABEL_KEYS, readRangeWidget, filterLabel, RANGE_OPS, useFilterOptions, writeRangeWidget } from '@adapttable/core';
2
- export { createHistoryAdapter, createMemoryAdapter, defaultConfirm, defaultLabels, deriveSortByOptions, getHistoryAdapter, useBackendData, useDataTable, useFrontendData, useSavedViews, useTableUrlState } from '@adapttable/core';
3
- import { TableRow, TableCell, Checkbox, IconButton, Stack, Tooltip, Typography, Box, Button, Popover, Divider, TextField, Paper, LinearProgress, Skeleton, Card, CardContent, Table, TableHead, TableSortLabel, TableBody, TableFooter, Badge, InputAdornment, MenuItem, Chip, Alert, Pagination, Drawer, Popper, ClickAwayListener, FormControl, FormLabel, FormGroup, CircularProgress, FormControlLabel } from '@mui/material';
4
- import { memo, useState, useRef, useMemo, useCallback, useEffect } from 'react';
5
- import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
6
-
7
- // src/components/SavedViewsMenu.tsx
8
- function SavedViewsMenu({
9
- options,
10
- labels
11
- }) {
12
- const { views, save, apply, remove } = useSavedViews(options);
13
- const [anchor, setAnchor] = useState(null);
14
- const [name, setName] = useState("");
15
- const trimmed = name.trim();
16
- return /* @__PURE__ */ jsxs(Fragment, { children: [
17
- /* @__PURE__ */ jsx(
18
- Button,
19
- {
20
- size: "small",
21
- variant: "outlined",
22
- "aria-expanded": anchor !== null,
23
- "aria-haspopup": "true",
24
- onClick: (e) => setAnchor(e.currentTarget),
25
- children: labels.savedViews
26
- }
27
- ),
28
- /* @__PURE__ */ jsx(
29
- Popover,
30
- {
31
- anchorEl: anchor,
32
- open: anchor !== null,
33
- onClose: () => setAnchor(null),
34
- anchorOrigin: { vertical: "bottom", horizontal: "left" },
35
- children: /* @__PURE__ */ jsxs(Box, { sx: { p: 0.75, minWidth: 250 }, children: [
36
- views.map((view) => /* @__PURE__ */ jsxs(
37
- Stack,
38
- {
39
- direction: "row",
40
- alignItems: "center",
41
- spacing: 0.5,
42
- children: [
43
- /* @__PURE__ */ jsx(
44
- Button,
45
- {
46
- size: "small",
47
- fullWidth: true,
48
- sx: { justifyContent: "flex-start" },
49
- onClick: () => {
50
- apply(view.name);
51
- setAnchor(null);
52
- },
53
- children: view.name
54
- }
55
- ),
56
- /* @__PURE__ */ jsx(
57
- IconButton,
58
- {
59
- size: "small",
60
- "aria-label": `${labels.deleteView}: ${view.name}`,
61
- onClick: () => remove(view.name),
62
- children: "\xD7"
63
- }
64
- )
65
- ]
66
- },
67
- view.name
68
- )),
69
- /* @__PURE__ */ jsx(Divider, { sx: { my: 0.5 } }),
70
- /* @__PURE__ */ jsxs(Stack, { direction: "row", alignItems: "center", spacing: 0.5, children: [
71
- /* @__PURE__ */ jsx(
72
- TextField,
73
- {
74
- size: "small",
75
- value: name,
76
- placeholder: labels.viewName,
77
- slotProps: { htmlInput: { "aria-label": labels.viewName } },
78
- onChange: (e) => setName(e.target.value)
79
- }
80
- ),
81
- /* @__PURE__ */ jsx(
82
- Button,
83
- {
84
- size: "small",
85
- variant: "contained",
86
- disabled: trimmed === "",
87
- onClick: () => {
88
- save(trimmed);
89
- setName("");
90
- },
91
- children: labels.saveView
92
- }
93
- )
94
- ] })
95
- ] })
96
- }
97
- )
98
- ] });
1
+ import { ACTIONS_COLUMN_KEY, EyeIcon, GripIcon, PIN_Z, PinIcon, RANGE_OPS, RANGE_OP_LABEL_KEYS, RANGE_SUFFIXES, columnMenuRows, columnReorderKeyProps, columnResizeHandleProps, createHistoryAdapter, createMemoryAdapter, defaultConfirm, defaultLabels, deriveSortByOptions, edgePinStyle, filterLabel, getHistoryAdapter, headerGroupRow, isDeclarativeFilters, nextPinSide, pageSizeOptions, pinActionLabel, pinnedCellStyle, pinnedColumnWidth, readRangeWidget, resolveDisabledReason, resolveLabels, resolveVirtualRows, rowClickProps, runRowAction, tableMinWidth, tableRenderModel, useBackendData, useBulkActionRunner, useChromeBodyData, useChromeScrollReset, useColumnDragState, useDataTable, useFilterOptions, useFilterTriggerToggle, useFrontendData, useHorizontalOverflow, useSavedViews, useSavedViews as useSavedViews$1, useTableChrome, useTableData, useTableUrlState, writeRangeWidget } from "@adapttable/core";
2
+ import { Alert, Badge, Box, Button, Card, CardContent, Checkbox, Chip, CircularProgress, ClickAwayListener, Divider, Drawer, FormControl, FormControlLabel, FormGroup, FormLabel, IconButton, InputAdornment, LinearProgress, MenuItem, Pagination, Paper, Popover, Popper, Skeleton, Stack, Table, TableBody, TableCell, TableFooter, TableHead, TableRow, TableSortLabel, TextField, Tooltip, Typography } from "@mui/material";
3
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ //#region src/components/SavedViewsMenu.tsx
6
+ /**
7
+ * MUI saved-views menu: a toolbar button opening a popover that lists the
8
+ * saved views — click applies one to the table, the trailing × deletes it —
9
+ * above a save row that captures the table's CURRENT URL state (search,
10
+ * sort, page, filters, column layout) under a typed name.
11
+ */
12
+ function SavedViewsMenu({ options, labels }) {
13
+ const { views, save, apply, remove } = useSavedViews$1(options);
14
+ const [anchor, setAnchor] = useState(null);
15
+ const [name, setName] = useState("");
16
+ const trimmed = name.trim();
17
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
18
+ size: "small",
19
+ variant: "outlined",
20
+ "aria-expanded": anchor !== null,
21
+ "aria-haspopup": "true",
22
+ onClick: (e) => setAnchor(e.currentTarget),
23
+ children: labels.savedViews
24
+ }), /* @__PURE__ */ jsx(Popover, {
25
+ anchorEl: anchor,
26
+ open: anchor !== null,
27
+ onClose: () => setAnchor(null),
28
+ anchorOrigin: {
29
+ vertical: "bottom",
30
+ horizontal: "left"
31
+ },
32
+ children: /* @__PURE__ */ jsxs(Box, {
33
+ sx: {
34
+ p: .75,
35
+ minWidth: 250
36
+ },
37
+ children: [
38
+ views.map((view) => /* @__PURE__ */ jsxs(Stack, {
39
+ direction: "row",
40
+ spacing: .5,
41
+ sx: { alignItems: "center" },
42
+ children: [/* @__PURE__ */ jsx(Button, {
43
+ size: "small",
44
+ fullWidth: true,
45
+ sx: { justifyContent: "flex-start" },
46
+ onClick: () => {
47
+ apply(view.name);
48
+ setAnchor(null);
49
+ },
50
+ children: view.name
51
+ }), /* @__PURE__ */ jsx(IconButton, {
52
+ size: "small",
53
+ "aria-label": `${labels.deleteView}: ${view.name}`,
54
+ onClick: () => remove(view.name),
55
+ children: "×"
56
+ })]
57
+ }, view.name)),
58
+ /* @__PURE__ */ jsx(Divider, { sx: { my: .5 } }),
59
+ /* @__PURE__ */ jsxs(Stack, {
60
+ direction: "row",
61
+ spacing: .5,
62
+ sx: { alignItems: "center" },
63
+ children: [/* @__PURE__ */ jsx(TextField, {
64
+ size: "small",
65
+ value: name,
66
+ placeholder: labels.viewName,
67
+ slotProps: { htmlInput: { "aria-label": labels.viewName } },
68
+ onChange: (e) => setName(e.target.value)
69
+ }), /* @__PURE__ */ jsx(Button, {
70
+ size: "small",
71
+ variant: "contained",
72
+ disabled: trimmed === "",
73
+ onClick: () => {
74
+ save(trimmed);
75
+ setName("");
76
+ },
77
+ children: labels.saveView
78
+ })]
79
+ })
80
+ ]
81
+ })
82
+ })] });
99
83
  }
84
+ //#endregion
85
+ //#region src/components/AutoFilterForm.tsx
86
+ /** A scalar filter value as input text (arrays/blanks render empty). */
100
87
  function scalarText(value) {
101
- return typeof value === "string" || typeof value === "number" ? String(value) : "";
88
+ return typeof value === "string" || typeof value === "number" ? String(value) : "";
102
89
  }
90
+ /** A multi-select filter value as the checked-value list. */
103
91
  function selectedList(value) {
104
- return Array.isArray(value) ? value : [];
92
+ return Array.isArray(value) ? value : [];
105
93
  }
106
94
  function TextFilter({ def, source }) {
107
- return /* @__PURE__ */ jsx(
108
- TextField,
109
- {
110
- size: "small",
111
- label: filterLabel(def),
112
- placeholder: def.placeholder,
113
- value: scalarText(source.extra[def.key]),
114
- onChange: (e) => source.setExtra(def.key, e.target.value)
115
- }
116
- );
95
+ return /* @__PURE__ */ jsx(TextField, {
96
+ size: "small",
97
+ label: filterLabel(def),
98
+ placeholder: def.placeholder,
99
+ value: scalarText(source.extra[def.key]),
100
+ onChange: (e) => source.setExtra(def.key, e.target.value)
101
+ });
117
102
  }
118
103
  function SelectFilter({ def, source }) {
119
- const { options, loading } = useFilterOptions(def);
120
- return /* @__PURE__ */ jsxs(
121
- TextField,
122
- {
123
- select: true,
124
- size: "small",
125
- label: filterLabel(def),
126
- value: scalarText(source.extra[def.key]),
127
- onChange: (e) => source.setExtra(def.key, e.target.value),
128
- slotProps: {
129
- select: { native: true },
130
- inputLabel: { shrink: true }
131
- },
132
- children: [
133
- /* @__PURE__ */ jsx("option", { value: "", children: "All" }),
134
- loading && /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "\u2026" }),
135
- options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
136
- ]
137
- }
138
- );
104
+ const { options, loading } = useFilterOptions(def);
105
+ return /* @__PURE__ */ jsxs(TextField, {
106
+ select: true,
107
+ size: "small",
108
+ label: filterLabel(def),
109
+ value: scalarText(source.extra[def.key]),
110
+ onChange: (e) => source.setExtra(def.key, e.target.value),
111
+ slotProps: {
112
+ select: { native: true },
113
+ inputLabel: { shrink: true }
114
+ },
115
+ children: [
116
+ /* @__PURE__ */ jsx("option", {
117
+ value: "",
118
+ children: "All"
119
+ }),
120
+ loading && /* @__PURE__ */ jsx("option", {
121
+ value: "",
122
+ disabled: true,
123
+ children: "…"
124
+ }),
125
+ options.map((option) => /* @__PURE__ */ jsx("option", {
126
+ value: option.value,
127
+ children: option.label
128
+ }, option.value))
129
+ ]
130
+ });
139
131
  }
140
132
  function MultiSelectFilter({ def, source }) {
141
- const { options, loading } = useFilterOptions(def);
142
- const checked = selectedList(source.extra[def.key]);
143
- const toggle = (value, on) => {
144
- const next = on ? [...checked, value] : checked.filter((v) => v !== value);
145
- source.setExtra(def.key, next);
146
- };
147
- return /* @__PURE__ */ jsxs(FormControl, { component: "fieldset", variant: "standard", children: [
148
- /* @__PURE__ */ jsx(FormLabel, { component: "legend", children: filterLabel(def) }),
149
- /* @__PURE__ */ jsx(FormGroup, { row: true, sx: { columnGap: 1.5 }, children: loading ? /* @__PURE__ */ jsx(CircularProgress, { size: 16 }) : options.map((option) => /* @__PURE__ */ jsx(
150
- FormControlLabel,
151
- {
152
- label: option.label,
153
- control: /* @__PURE__ */ jsx(
154
- Checkbox,
155
- {
156
- size: "small",
157
- checked: checked.includes(option.value),
158
- onChange: (_, on) => toggle(option.value, on)
159
- }
160
- )
161
- },
162
- option.value
163
- )) })
164
- ] });
133
+ const { options, loading } = useFilterOptions(def);
134
+ const checked = selectedList(source.extra[def.key]);
135
+ const toggle = (value, on) => {
136
+ const next = on ? [...checked, value] : checked.filter((v) => v !== value);
137
+ source.setExtra(def.key, next);
138
+ };
139
+ return /* @__PURE__ */ jsxs(FormControl, {
140
+ component: "fieldset",
141
+ variant: "standard",
142
+ children: [/* @__PURE__ */ jsx(FormLabel, {
143
+ component: "legend",
144
+ children: filterLabel(def)
145
+ }), /* @__PURE__ */ jsx(FormGroup, {
146
+ row: true,
147
+ sx: { columnGap: 1.5 },
148
+ children: loading ? /* @__PURE__ */ jsx(CircularProgress, { size: 16 }) : options.map((option) => /* @__PURE__ */ jsx(FormControlLabel, {
149
+ label: option.label,
150
+ control: /* @__PURE__ */ jsx(Checkbox, {
151
+ size: "small",
152
+ checked: checked.includes(option.value),
153
+ onChange: (_, on) => toggle(option.value, on)
154
+ })
155
+ }, option.value))
156
+ })]
157
+ });
165
158
  }
166
- function RangeFilter({
167
- def,
168
- type,
169
- source,
170
- labels
171
- }) {
172
- const suffixes = RANGE_SUFFIXES[type];
173
- const lowKey = def.key + suffixes.start;
174
- const highKey = def.key + suffixes.end;
175
- const opLabelKeys = RANGE_OP_LABEL_KEYS[type === "dateRange" ? "date" : "number"];
176
- const state = readRangeWidget(source.extra, lowKey, highKey);
177
- const [op, setOp] = useState(state.op);
178
- const single = op === "lte" ? state.b : state.a;
179
- const commit = (next, a, b) => source.setExtras(writeRangeWidget(next, a, b, lowKey, highKey));
180
- const changeOp = (raw) => {
181
- const next = RANGE_OPS.find((candidate) => candidate === raw);
182
- setOp(next);
183
- commit(next, single, op === "between" ? state.b : "");
184
- };
185
- const input = (label, value, write) => /* @__PURE__ */ jsx(
186
- TextField,
187
- {
188
- size: "small",
189
- sx: { flex: "1 1 7rem", minWidth: "7rem" },
190
- type: type === "dateRange" ? "date" : "number",
191
- label,
192
- value,
193
- onChange: (e) => write(e.target.value),
194
- slotProps: { inputLabel: { shrink: true } }
195
- }
196
- );
197
- let bounds = null;
198
- if (op === "between") {
199
- bounds = /* @__PURE__ */ jsxs(Fragment, { children: [
200
- input(labels.from, state.a, (raw) => commit("between", raw, state.b)),
201
- input(labels.to, state.b, (raw) => commit("between", state.a, raw))
202
- ] });
203
- } else if (op !== void 0) {
204
- bounds = input(labels.value, single, (raw) => commit(op, raw, ""));
205
- }
206
- return /* @__PURE__ */ jsxs(FormControl, { component: "fieldset", variant: "standard", sx: { width: "100%" }, children: [
207
- /* @__PURE__ */ jsx(FormLabel, { component: "legend", sx: { mb: 0.75 }, children: filterLabel(def) }),
208
- /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, useFlexGap: true, flexWrap: "wrap", children: [
209
- /* @__PURE__ */ jsxs(
210
- TextField,
211
- {
212
- select: true,
213
- size: "small",
214
- label: labels.operator,
215
- value: op ?? "",
216
- onChange: (e) => changeOp(e.target.value),
217
- slotProps: {
218
- select: { native: true },
219
- inputLabel: { shrink: true }
220
- },
221
- sx: { flex: "0 0 8.5rem", width: "8.5rem" },
222
- children: [
223
- /* @__PURE__ */ jsx("option", { value: "" }),
224
- RANGE_OPS.map((candidate) => /* @__PURE__ */ jsx("option", { value: candidate, children: labels[opLabelKeys[candidate]] }, candidate))
225
- ]
226
- }
227
- ),
228
- bounds
229
- ] })
230
- ] });
159
+ /**
160
+ * Operator-first range widget: a comparison select, then one value input —
161
+ * or a From/To pair for "between". The UI state is the operator alone; the
162
+ * bounds always live in the source's `Min`/`Max` (`From`/`To`) pair, so
163
+ * URLs, chips, and predicates are untouched by the operator presentation.
164
+ */
165
+ function RangeFilter({ def, type, source, labels }) {
166
+ const suffixes = RANGE_SUFFIXES[type];
167
+ const lowKey = def.key + suffixes.start;
168
+ const highKey = def.key + suffixes.end;
169
+ const opLabelKeys = RANGE_OP_LABEL_KEYS[type === "dateRange" ? "date" : "number"];
170
+ const state = readRangeWidget(source.extra, lowKey, highKey);
171
+ const [op, setOp] = useState(state.op);
172
+ const single = op === "lte" ? state.b : state.a;
173
+ const commit = (next, a, b) => source.setExtras(writeRangeWidget(next, a, b, lowKey, highKey));
174
+ const changeOp = (raw) => {
175
+ const next = RANGE_OPS.find((candidate) => candidate === raw);
176
+ setOp(next);
177
+ commit(next, single, op === "between" ? state.b : "");
178
+ };
179
+ const input = (label, value, write) => /* @__PURE__ */ jsx(TextField, {
180
+ size: "small",
181
+ sx: {
182
+ flex: "1 1 7rem",
183
+ minWidth: "7rem"
184
+ },
185
+ type: type === "dateRange" ? "date" : "number",
186
+ label,
187
+ value,
188
+ onChange: (e) => write(e.target.value),
189
+ slotProps: { inputLabel: { shrink: true } }
190
+ });
191
+ let bounds = null;
192
+ if (op === "between") bounds = /* @__PURE__ */ jsxs(Fragment, { children: [input(labels.from, state.a, (raw) => commit("between", raw, state.b)), input(labels.to, state.b, (raw) => commit("between", state.a, raw))] });
193
+ else if (op !== void 0) bounds = input(labels.value, single, (raw) => commit(op, raw, ""));
194
+ return /* @__PURE__ */ jsxs(FormControl, {
195
+ component: "fieldset",
196
+ variant: "standard",
197
+ sx: { width: "100%" },
198
+ children: [/* @__PURE__ */ jsx(FormLabel, {
199
+ component: "legend",
200
+ sx: { mb: .75 },
201
+ children: filterLabel(def)
202
+ }), /* @__PURE__ */ jsxs(Stack, {
203
+ direction: "row",
204
+ spacing: 1,
205
+ useFlexGap: true,
206
+ sx: { flexWrap: "wrap" },
207
+ children: [/* @__PURE__ */ jsxs(TextField, {
208
+ select: true,
209
+ size: "small",
210
+ label: labels.operator,
211
+ value: op ?? "",
212
+ onChange: (e) => changeOp(e.target.value),
213
+ slotProps: {
214
+ select: { native: true },
215
+ inputLabel: { shrink: true }
216
+ },
217
+ sx: {
218
+ flex: "0 0 8.5rem",
219
+ width: "8.5rem"
220
+ },
221
+ children: [/* @__PURE__ */ jsx("option", { value: "" }), RANGE_OPS.map((candidate) => /* @__PURE__ */ jsx("option", {
222
+ value: candidate,
223
+ children: labels[opLabelKeys[candidate]]
224
+ }, candidate))]
225
+ }), bounds]
226
+ })]
227
+ });
231
228
  }
232
- function FilterField({
233
- def,
234
- source,
235
- labels
236
- }) {
237
- switch (def.type) {
238
- case "text":
239
- return /* @__PURE__ */ jsx(TextFilter, { def, source });
240
- case "select":
241
- return /* @__PURE__ */ jsx(SelectFilter, { def, source });
242
- case "multiSelect":
243
- return /* @__PURE__ */ jsx(MultiSelectFilter, { def, source });
244
- case "dateRange":
245
- case "numberRange":
246
- return /* @__PURE__ */ jsx(
247
- RangeFilter,
248
- {
249
- def,
250
- type: def.type,
251
- source,
252
- labels
253
- }
254
- );
255
- }
229
+ function FilterField({ def, source, labels }) {
230
+ switch (def.type) {
231
+ case "text": return /* @__PURE__ */ jsx(TextFilter, {
232
+ def,
233
+ source
234
+ });
235
+ case "select": return /* @__PURE__ */ jsx(SelectFilter, {
236
+ def,
237
+ source
238
+ });
239
+ case "multiSelect": return /* @__PURE__ */ jsx(MultiSelectFilter, {
240
+ def,
241
+ source
242
+ });
243
+ case "dateRange":
244
+ case "numberRange": return /* @__PURE__ */ jsx(RangeFilter, {
245
+ def,
246
+ type: def.type,
247
+ source,
248
+ labels
249
+ });
250
+ }
256
251
  }
257
- function AutoFilterForm({
258
- defs,
259
- source,
260
- labels
261
- }) {
262
- return /* @__PURE__ */ jsx(Stack, { spacing: 1.5, children: defs.map((def) => /* @__PURE__ */ jsx(FilterField, { def, source, labels }, def.key)) });
252
+ /**
253
+ * The auto-built filter form: one MUI widget per declarative definition,
254
+ * reading and writing the source's extra-filter bag (so chips, URL state
255
+ * and — on frontend data — the row predicate all stay in sync). The selects
256
+ * render natively, so every control works inline, without portal menus.
257
+ *
258
+ * @typeParam TRow - The row type.
259
+ */
260
+ function AutoFilterForm({ defs, source, labels }) {
261
+ return /* @__PURE__ */ jsx(Stack, {
262
+ spacing: 1.5,
263
+ children: defs.map((def) => /* @__PURE__ */ jsx(FilterField, {
264
+ def,
265
+ source,
266
+ labels
267
+ }, def.key))
268
+ });
263
269
  }
264
- function FilterPopover({
265
- open,
266
- onClose,
267
- anchorEl,
268
- filters,
269
- activeFilterCount,
270
- onClearFilters,
271
- labels,
272
- dir = "ltr"
273
- }) {
274
- useEffect(() => {
275
- if (!open) return;
276
- const onKeyDown = (event) => {
277
- if (event.key === "Escape") onClose();
278
- };
279
- document.addEventListener("keydown", onKeyDown);
280
- return () => document.removeEventListener("keydown", onKeyDown);
281
- }, [open, onClose]);
282
- return /* @__PURE__ */ jsx(
283
- Popper,
284
- {
285
- open: open && anchorEl !== null,
286
- anchorEl,
287
- placement: dir === "rtl" ? "bottom-start" : "bottom-end",
288
- modifiers: [{ name: "offset", options: { offset: [0, 4] } }],
289
- style: { zIndex: 1300 },
290
- children: /* @__PURE__ */ jsx(ClickAwayListener, { mouseEvent: "onMouseDown", onClickAway: onClose, children: /* @__PURE__ */ jsx(
291
- Paper,
292
- {
293
- elevation: 8,
294
- sx: {
295
- width: 360,
296
- maxWidth: "calc(100vw - 32px)",
297
- borderRadius: 2
298
- },
299
- children: /* @__PURE__ */ jsxs(Box, { sx: { p: 2, display: "flex", flexDirection: "column", gap: 2 }, children: [
300
- /* @__PURE__ */ jsxs(
301
- Stack,
302
- {
303
- direction: "row",
304
- justifyContent: "space-between",
305
- alignItems: "center",
306
- children: [
307
- /* @__PURE__ */ jsx(Typography, { variant: "subtitle1", fontWeight: 600, children: labels.filters }),
308
- /* @__PURE__ */ jsx(
309
- Button,
310
- {
311
- size: "small",
312
- onClick: onClearFilters,
313
- disabled: activeFilterCount === 0,
314
- children: labels.clearAll
315
- }
316
- )
317
- ]
318
- }
319
- ),
320
- /* @__PURE__ */ jsx(Box, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: filters })
321
- ] })
322
- }
323
- ) })
324
- }
325
- );
270
+ //#endregion
271
+ //#region src/components/FilterPopover.tsx
272
+ /**
273
+ * Anchored filter card (the default filter container). Opens under the Filters
274
+ * button. Built on a non-modal `Popper` (+ `ClickAwayListener`) rather than the
275
+ * Modal-based `Popover`, so it renders NO backdrop/scrim — the background stays
276
+ * visible and interactive. Closes on outside click or Escape. Placement is
277
+ * `bottom-end` (flips to `bottom-start` for RTL). Pair with
278
+ * `filtersMode="drawer"` for the slide-in panel instead.
279
+ */
280
+ function FilterPopover({ open, onClose, anchorEl, filters, activeFilterCount, onClearFilters, labels, dir = "ltr" }) {
281
+ useEffect(() => {
282
+ if (!open) return;
283
+ const onKeyDown = (event) => {
284
+ if (event.key === "Escape") onClose();
285
+ };
286
+ document.addEventListener("keydown", onKeyDown);
287
+ return () => document.removeEventListener("keydown", onKeyDown);
288
+ }, [open, onClose]);
289
+ return /* @__PURE__ */ jsx(Popper, {
290
+ open: open && anchorEl !== null,
291
+ anchorEl,
292
+ placement: dir === "rtl" ? "bottom-start" : "bottom-end",
293
+ modifiers: [{
294
+ name: "offset",
295
+ options: { offset: [0, 4] }
296
+ }],
297
+ style: { zIndex: 1300 },
298
+ children: /* @__PURE__ */ jsx(ClickAwayListener, {
299
+ mouseEvent: "onMouseDown",
300
+ onClickAway: onClose,
301
+ children: /* @__PURE__ */ jsx(Paper, {
302
+ elevation: 8,
303
+ sx: {
304
+ width: 360,
305
+ maxWidth: "calc(100vw - 32px)",
306
+ borderRadius: 2
307
+ },
308
+ children: /* @__PURE__ */ jsxs(Box, {
309
+ sx: {
310
+ p: 2,
311
+ display: "flex",
312
+ flexDirection: "column",
313
+ gap: 2
314
+ },
315
+ children: [/* @__PURE__ */ jsxs(Stack, {
316
+ direction: "row",
317
+ sx: {
318
+ justifyContent: "space-between",
319
+ alignItems: "center"
320
+ },
321
+ children: [/* @__PURE__ */ jsx(Typography, {
322
+ variant: "subtitle1",
323
+ sx: { fontWeight: 600 },
324
+ children: labels.filters
325
+ }), /* @__PURE__ */ jsx(Button, {
326
+ size: "small",
327
+ onClick: onClearFilters,
328
+ disabled: activeFilterCount === 0,
329
+ children: labels.clearAll
330
+ })]
331
+ }), /* @__PURE__ */ jsx(Box, {
332
+ sx: {
333
+ display: "flex",
334
+ flexDirection: "column",
335
+ gap: 2
336
+ },
337
+ children: filters
338
+ })]
339
+ })
340
+ })
341
+ })
342
+ });
326
343
  }
344
+ //#endregion
345
+ //#region src/components/chrome.tsx
346
+ /** Funnel/filter glyph (currentColor, no icon-lib dependency). */
327
347
  function FiltersIcon() {
328
- return /* @__PURE__ */ jsx(
329
- "svg",
330
- {
331
- width: "16",
332
- height: "16",
333
- viewBox: "0 0 24 24",
334
- fill: "none",
335
- stroke: "currentColor",
336
- strokeWidth: "1.8",
337
- strokeLinecap: "round",
338
- strokeLinejoin: "round",
339
- "aria-hidden": "true",
340
- children: /* @__PURE__ */ jsx("path", { d: "M3 5h18l-7 8v5l-4 2v-7L3 5Z" })
341
- }
342
- );
348
+ return /* @__PURE__ */ jsx("svg", {
349
+ width: "16",
350
+ height: "16",
351
+ viewBox: "0 0 24 24",
352
+ fill: "none",
353
+ stroke: "currentColor",
354
+ strokeWidth: "1.8",
355
+ strokeLinecap: "round",
356
+ strokeLinejoin: "round",
357
+ "aria-hidden": "true",
358
+ children: /* @__PURE__ */ jsx("path", { d: "M3 5h18l-7 8v5l-4 2v-7L3 5Z" })
359
+ });
343
360
  }
361
+ /** Magnifying-glass glyph for the search field (currentColor, no icon-lib). */
344
362
  function SearchIcon() {
345
- return /* @__PURE__ */ jsxs(
346
- "svg",
347
- {
348
- width: "18",
349
- height: "18",
350
- viewBox: "0 0 24 24",
351
- fill: "none",
352
- stroke: "currentColor",
353
- strokeWidth: "1.8",
354
- strokeLinecap: "round",
355
- strokeLinejoin: "round",
356
- "aria-hidden": "true",
357
- children: [
358
- /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
359
- /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })
360
- ]
361
- }
362
- );
363
+ return /* @__PURE__ */ jsxs("svg", {
364
+ width: "18",
365
+ height: "18",
366
+ viewBox: "0 0 24 24",
367
+ fill: "none",
368
+ stroke: "currentColor",
369
+ strokeWidth: "1.8",
370
+ strokeLinecap: "round",
371
+ strokeLinejoin: "round",
372
+ "aria-hidden": "true",
373
+ children: [/* @__PURE__ */ jsx("circle", {
374
+ cx: "11",
375
+ cy: "11",
376
+ r: "7"
377
+ }), /* @__PURE__ */ jsx("path", { d: "m21 21-4.3-4.3" })]
378
+ });
363
379
  }
364
- var srOnly = {
365
- position: "absolute",
366
- width: 1,
367
- height: 1,
368
- padding: 0,
369
- margin: -1,
370
- overflow: "hidden",
371
- clip: "rect(0 0 0 0)",
372
- whiteSpace: "nowrap",
373
- border: 0
380
+ /** Inline equivalent of `@mui/utils` visuallyHidden (avoids an extra dep). */
381
+ const srOnly = {
382
+ position: "absolute",
383
+ width: 1,
384
+ height: 1,
385
+ padding: 0,
386
+ margin: -1,
387
+ overflow: "hidden",
388
+ clip: "rect(0 0 0 0)",
389
+ whiteSpace: "nowrap",
390
+ border: 0
374
391
  };
375
- function Toolbar({
376
- table,
377
- hideSearch,
378
- searchPlaceholder,
379
- sortByOptions,
380
- customToolbar,
381
- hasFilters,
382
- activeFilterCount,
383
- showRowsPerPage,
384
- filtersMode,
385
- filters,
386
- filtersOpen,
387
- onToggleFilters,
388
- onFiltersTriggerPointerDown,
389
- onCloseFilters,
390
- onClearFilters,
391
- dir,
392
- columnMenu
393
- }) {
394
- const { labels, source } = table;
395
- const sortOptions = sortByOptions ?? (table.isMobile ? table.sortByOptions : void 0);
396
- const searchProps = table.getSearchInputProps(
397
- searchPlaceholder ? { placeholder: searchPlaceholder } : void 0
398
- );
399
- const filtersAnchorRef = useRef(null);
400
- const filtersButton = hasFilters ? /* @__PURE__ */ jsx(Badge, { color: "primary", badgeContent: activeFilterCount, children: /* @__PURE__ */ jsx(
401
- Button,
402
- {
403
- ref: filtersAnchorRef,
404
- variant: "outlined",
405
- size: "small",
406
- startIcon: /* @__PURE__ */ jsx(FiltersIcon, {}),
407
- "aria-expanded": filtersMode === "popover" ? filtersOpen : void 0,
408
- onPointerDown: onFiltersTriggerPointerDown,
409
- onClick: onToggleFilters,
410
- children: labels.filters
411
- }
412
- ) }) : null;
413
- return /* @__PURE__ */ jsxs(
414
- Stack,
415
- {
416
- direction: "row",
417
- spacing: 1,
418
- flexWrap: "wrap",
419
- useFlexGap: true,
420
- alignItems: "center",
421
- justifyContent: "space-between",
422
- children: [
423
- !hideSearch && /* @__PURE__ */ jsx(
424
- TextField,
425
- {
426
- size: "small",
427
- value: searchProps.value,
428
- placeholder: searchProps.placeholder,
429
- slotProps: {
430
- htmlInput: { "aria-label": labels.search, type: "search" },
431
- input: {
432
- startAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "start", children: /* @__PURE__ */ jsx(SearchIcon, {}) })
433
- }
434
- },
435
- onChange: searchProps.onChange,
436
- sx: { flex: 1, minWidth: 160, maxWidth: 360 }
437
- }
438
- ),
439
- /* @__PURE__ */ jsxs(
440
- Stack,
441
- {
442
- direction: "row",
443
- spacing: 1,
444
- alignItems: "center",
445
- flexWrap: "wrap",
446
- useFlexGap: true,
447
- children: [
448
- sortOptions && sortOptions.length > 0 && /* @__PURE__ */ jsxs(
449
- TextField,
450
- {
451
- select: true,
452
- size: "small",
453
- label: labels.sortBy,
454
- value: source.sortBy ?? "",
455
- onChange: (e) => source.setSort(
456
- e.target.value || void 0,
457
- source.sortDir ?? "asc"
458
- ),
459
- sx: { minWidth: 160 },
460
- children: [
461
- /* @__PURE__ */ jsx(MenuItem, { value: "", children: "\u2014" }),
462
- sortOptions.map((o) => /* @__PURE__ */ jsx(MenuItem, { value: o.value, children: o.label }, o.value))
463
- ]
464
- }
465
- ),
466
- customToolbar,
467
- filtersButton,
468
- hasFilters && filtersMode === "popover" && /* @__PURE__ */ jsx(
469
- FilterPopover,
470
- {
471
- open: filtersOpen,
472
- onClose: onCloseFilters,
473
- anchorEl: filtersAnchorRef.current,
474
- filters,
475
- activeFilterCount,
476
- onClearFilters,
477
- labels,
478
- dir
479
- }
480
- ),
481
- columnMenu,
482
- showRowsPerPage && /* @__PURE__ */ jsx(
483
- TextField,
484
- {
485
- select: true,
486
- size: "small",
487
- label: labels.rowsPerPage,
488
- value: source.limit,
489
- onChange: (e) => source.setLimit(Number(e.target.value)),
490
- sx: { minWidth: 110 },
491
- children: pageSizeOptions(source.limit).map((n) => /* @__PURE__ */ jsx(MenuItem, { value: n, children: n }, n))
492
- }
493
- )
494
- ]
495
- }
496
- )
497
- ]
498
- }
499
- );
392
+ /** Search field + sort select + filters button + rows-per-page. */
393
+ function Toolbar({ table, hideSearch, searchPlaceholder, sortByOptions, customToolbar, hasFilters, activeFilterCount, showRowsPerPage, filtersMode, filters, filtersOpen, onToggleFilters, onFiltersTriggerPointerDown, onCloseFilters, onClearFilters, dir, columnMenu }) {
394
+ const { labels, source } = table;
395
+ const sortOptions = sortByOptions ?? (table.isMobile ? table.sortByOptions : void 0);
396
+ const searchProps = table.getSearchInputProps(searchPlaceholder ? { placeholder: searchPlaceholder } : void 0);
397
+ const filtersAnchorRef = useRef(null);
398
+ const filtersButton = hasFilters ? /* @__PURE__ */ jsx(Badge, {
399
+ color: "primary",
400
+ badgeContent: activeFilterCount,
401
+ children: /* @__PURE__ */ jsx(Button, {
402
+ ref: filtersAnchorRef,
403
+ variant: "outlined",
404
+ size: "small",
405
+ startIcon: /* @__PURE__ */ jsx(FiltersIcon, {}),
406
+ "aria-expanded": filtersMode === "popover" ? filtersOpen : void 0,
407
+ onPointerDown: onFiltersTriggerPointerDown,
408
+ onClick: onToggleFilters,
409
+ children: labels.filters
410
+ })
411
+ }) : null;
412
+ return /* @__PURE__ */ jsxs(Stack, {
413
+ direction: "row",
414
+ spacing: 1,
415
+ useFlexGap: true,
416
+ sx: {
417
+ flexWrap: "wrap",
418
+ alignItems: "center",
419
+ justifyContent: "space-between"
420
+ },
421
+ children: [!hideSearch && /* @__PURE__ */ jsx(TextField, {
422
+ size: "small",
423
+ value: searchProps.value,
424
+ placeholder: searchProps.placeholder,
425
+ slotProps: {
426
+ htmlInput: {
427
+ "aria-label": labels.search,
428
+ type: "search"
429
+ },
430
+ input: { startAdornment: /* @__PURE__ */ jsx(InputAdornment, {
431
+ position: "start",
432
+ children: /* @__PURE__ */ jsx(SearchIcon, {})
433
+ }) }
434
+ },
435
+ onChange: searchProps.onChange,
436
+ sx: {
437
+ flex: 1,
438
+ minWidth: 160,
439
+ maxWidth: 360
440
+ }
441
+ }), /* @__PURE__ */ jsxs(Stack, {
442
+ direction: "row",
443
+ spacing: 1,
444
+ useFlexGap: true,
445
+ sx: {
446
+ alignItems: "center",
447
+ flexWrap: "wrap"
448
+ },
449
+ children: [
450
+ sortOptions && sortOptions.length > 0 && /* @__PURE__ */ jsxs(TextField, {
451
+ select: true,
452
+ size: "small",
453
+ label: labels.sortBy,
454
+ value: source.sortBy ?? "",
455
+ onChange: (e) => source.setSort(e.target.value || void 0, source.sortDir ?? "asc"),
456
+ sx: { minWidth: 160 },
457
+ children: [/* @__PURE__ */ jsx(MenuItem, {
458
+ value: "",
459
+ children: ""
460
+ }), sortOptions.map((o) => /* @__PURE__ */ jsx(MenuItem, {
461
+ value: o.value,
462
+ children: o.label
463
+ }, o.value))]
464
+ }),
465
+ customToolbar,
466
+ filtersButton,
467
+ hasFilters && filtersMode === "popover" && /* @__PURE__ */ jsx(FilterPopover, {
468
+ open: filtersOpen,
469
+ onClose: onCloseFilters,
470
+ anchorEl: filtersAnchorRef.current,
471
+ filters,
472
+ activeFilterCount,
473
+ onClearFilters,
474
+ labels,
475
+ dir
476
+ }),
477
+ columnMenu,
478
+ showRowsPerPage && /* @__PURE__ */ jsx(TextField, {
479
+ select: true,
480
+ size: "small",
481
+ label: labels.rowsPerPage,
482
+ value: source.limit,
483
+ onChange: (e) => source.setLimit(Number(e.target.value)),
484
+ sx: { minWidth: 110 },
485
+ children: pageSizeOptions(source.limit).map((n) => /* @__PURE__ */ jsx(MenuItem, {
486
+ value: n,
487
+ children: n
488
+ }, n))
489
+ })
490
+ ]
491
+ })]
492
+ });
500
493
  }
501
- function Chips({
502
- chips,
503
- onClearAll,
504
- labels
505
- }) {
506
- if (chips.length === 0) return null;
507
- return /* @__PURE__ */ jsxs(
508
- Stack,
509
- {
510
- direction: "row",
511
- spacing: 0.5,
512
- flexWrap: "wrap",
513
- useFlexGap: true,
514
- component: "ul",
515
- sx: { listStyle: "none", p: 0, m: 0 },
516
- "aria-label": labels.filters,
517
- children: [
518
- chips.map((chip) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(
519
- Chip,
520
- {
521
- label: chip.label,
522
- size: "small",
523
- onDelete: chip.onRemove,
524
- deleteIcon: /* @__PURE__ */ jsx("span", { "aria-label": `${labels.clearAll}: ${chip.label}`, children: "\xD7" })
525
- }
526
- ) }, chip.key)),
527
- /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Button, { size: "small", onClick: onClearAll, children: labels.clearAll }) })
528
- ]
529
- }
530
- );
494
+ /** Removable MUI chips. */
495
+ function Chips({ chips, onClearAll, labels }) {
496
+ if (chips.length === 0) return null;
497
+ return /* @__PURE__ */ jsxs(Stack, {
498
+ direction: "row",
499
+ spacing: .5,
500
+ useFlexGap: true,
501
+ component: "ul",
502
+ "aria-label": labels.filters,
503
+ sx: {
504
+ listStyle: "none",
505
+ p: 0,
506
+ m: 0,
507
+ flexWrap: "wrap"
508
+ },
509
+ children: [chips.map((chip) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Chip, {
510
+ label: chip.label,
511
+ size: "small",
512
+ onDelete: chip.onRemove,
513
+ deleteIcon: /* @__PURE__ */ jsx("span", {
514
+ "aria-label": `${labels.clearAll}: ${chip.label}`,
515
+ children: "×"
516
+ })
517
+ }) }, chip.key)), /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Button, {
518
+ size: "small",
519
+ onClick: onClearAll,
520
+ children: labels.clearAll
521
+ }) })]
522
+ });
531
523
  }
532
- function BulkBar({
533
- selection,
534
- total,
535
- bulkActions,
536
- confirm,
537
- labels
538
- }) {
539
- const {
540
- selectedIds,
541
- selectedCount,
542
- headerState,
543
- visibleIds,
544
- allMatching,
545
- selectAllMatching,
546
- clear
547
- } = selection;
548
- const { pending, run } = useBulkActionRunner({
549
- confirm,
550
- cancelLabel: labels.cancel,
551
- onComplete: clear
552
- });
553
- if (selectedCount === 0) return null;
554
- const ids = [...selectedIds];
555
- const showBanner = headerState === "all" && total > visibleIds.length;
556
- return /* @__PURE__ */ jsxs(
557
- Stack,
558
- {
559
- direction: "row",
560
- spacing: 1,
561
- alignItems: "center",
562
- justifyContent: "space-between",
563
- flexWrap: "wrap",
564
- useFlexGap: true,
565
- children: [
566
- /* @__PURE__ */ jsxs(
567
- Stack,
568
- {
569
- direction: "row",
570
- spacing: 1,
571
- alignItems: "center",
572
- flexWrap: "wrap",
573
- useFlexGap: true,
574
- children: [
575
- /* @__PURE__ */ jsx(Typography, { variant: "body2", children: labels.selectedCount(selectedCount) }),
576
- showBanner && (allMatching ? /* @__PURE__ */ jsxs(Fragment, { children: [
577
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: labels.allMatchingSelected(total) }),
578
- /* @__PURE__ */ jsx(
579
- Button,
580
- {
581
- size: "small",
582
- variant: "text",
583
- onClick: clear,
584
- disabled: pending !== null,
585
- children: labels.clearAll
586
- }
587
- )
588
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
589
- /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: labels.pageSelected(visibleIds.length) }),
590
- /* @__PURE__ */ jsx(
591
- Button,
592
- {
593
- size: "small",
594
- variant: "text",
595
- onClick: selectAllMatching,
596
- disabled: pending !== null,
597
- children: labels.selectAllMatching(total)
598
- }
599
- )
600
- ] }))
601
- ]
602
- }
603
- ),
604
- /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
605
- /* @__PURE__ */ jsx(
606
- Button,
607
- {
608
- size: "small",
609
- variant: "text",
610
- onClick: clear,
611
- disabled: pending !== null,
612
- children: labels.clearAll
613
- }
614
- ),
615
- bulkActions.map((action) => {
616
- const reason = resolveDisabledReason(action.disabledReason?.(ids));
617
- return /* @__PURE__ */ jsx(Tooltip, { title: reason ?? "", children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(
618
- Button,
619
- {
620
- size: "small",
621
- variant: "contained",
622
- color: action.color,
623
- startIcon: action.icon,
624
- disabled: reason !== void 0 || pending !== null,
625
- onClick: () => run(
626
- action,
627
- ids,
628
- allMatching ? { allMatching: true, total } : void 0
629
- ),
630
- children: action.label
631
- }
632
- ) }) }, action.key);
633
- })
634
- ] })
635
- ]
636
- }
637
- );
524
+ /** Selection toolbar. */
525
+ function BulkBar({ selection, total, bulkActions, confirm, labels }) {
526
+ const { selectedIds, selectedCount, headerState, visibleIds, allMatching, selectAllMatching, clear } = selection;
527
+ const { pending, run } = useBulkActionRunner({
528
+ confirm,
529
+ cancelLabel: labels.cancel,
530
+ onComplete: clear
531
+ });
532
+ if (selectedCount === 0) return null;
533
+ const ids = [...selectedIds];
534
+ const showBanner = headerState === "all" && total > visibleIds.length;
535
+ return /* @__PURE__ */ jsxs(Stack, {
536
+ direction: "row",
537
+ spacing: 1,
538
+ useFlexGap: true,
539
+ sx: {
540
+ alignItems: "center",
541
+ justifyContent: "space-between",
542
+ flexWrap: "wrap"
543
+ },
544
+ children: [/* @__PURE__ */ jsxs(Stack, {
545
+ direction: "row",
546
+ spacing: 1,
547
+ useFlexGap: true,
548
+ sx: {
549
+ alignItems: "center",
550
+ flexWrap: "wrap"
551
+ },
552
+ children: [/* @__PURE__ */ jsx(Typography, {
553
+ variant: "body2",
554
+ children: labels.selectedCount(selectedCount)
555
+ }), showBanner && (allMatching ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Typography, {
556
+ variant: "body2",
557
+ color: "text.secondary",
558
+ children: labels.allMatchingSelected(total)
559
+ }), /* @__PURE__ */ jsx(Button, {
560
+ size: "small",
561
+ variant: "text",
562
+ onClick: clear,
563
+ disabled: pending !== null,
564
+ children: labels.clearAll
565
+ })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Typography, {
566
+ variant: "body2",
567
+ color: "text.secondary",
568
+ children: labels.pageSelected(visibleIds.length)
569
+ }), /* @__PURE__ */ jsx(Button, {
570
+ size: "small",
571
+ variant: "text",
572
+ onClick: selectAllMatching,
573
+ disabled: pending !== null,
574
+ children: labels.selectAllMatching(total)
575
+ })] }))]
576
+ }), /* @__PURE__ */ jsxs(Stack, {
577
+ direction: "row",
578
+ spacing: 1,
579
+ useFlexGap: true,
580
+ sx: { flexWrap: "wrap" },
581
+ children: [/* @__PURE__ */ jsx(Button, {
582
+ size: "small",
583
+ variant: "text",
584
+ onClick: clear,
585
+ disabled: pending !== null,
586
+ children: labels.clearAll
587
+ }), bulkActions.map((action) => {
588
+ const reason = resolveDisabledReason(action.disabledReason?.(ids));
589
+ return /* @__PURE__ */ jsx(Tooltip, {
590
+ title: reason ?? "",
591
+ children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(Button, {
592
+ size: "small",
593
+ variant: "contained",
594
+ color: action.color,
595
+ startIcon: action.icon,
596
+ disabled: reason !== void 0 || pending !== null,
597
+ onClick: () => run(action, ids, allMatching ? {
598
+ allMatching: true,
599
+ total
600
+ } : void 0),
601
+ children: action.label
602
+ }) })
603
+ }, action.key);
604
+ })]
605
+ })]
606
+ });
638
607
  }
639
- function Footer({
640
- pagination,
641
- total,
642
- limit,
643
- setPage,
644
- setLimit,
645
- labels
646
- }) {
647
- return /* @__PURE__ */ jsxs(
648
- Stack,
649
- {
650
- direction: "row",
651
- spacing: 2,
652
- alignItems: "center",
653
- justifyContent: "space-between",
654
- flexWrap: "wrap",
655
- useFlexGap: true,
656
- children: [
657
- /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1.5, alignItems: "center", useFlexGap: true, children: [
658
- /* @__PURE__ */ jsx(
659
- TextField,
660
- {
661
- select: true,
662
- size: "small",
663
- label: labels.rowsPerPage,
664
- value: String(limit),
665
- onChange: (e) => setLimit(Number(e.target.value)),
666
- sx: { minWidth: 100 },
667
- children: pageSizeOptions(limit).map((n) => /* @__PURE__ */ jsx(MenuItem, { value: String(n), children: n }, n))
668
- }
669
- ),
670
- total > 0 && /* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: labels.showing({
671
- from: pagination.fromIndex,
672
- to: pagination.toIndex,
673
- total
674
- }) })
675
- ] }),
676
- /* @__PURE__ */ jsx(
677
- Pagination,
678
- {
679
- count: pagination.totalPages,
680
- page: pagination.safePage,
681
- onChange: (_, page) => setPage(page),
682
- size: "small",
683
- getItemAriaLabel: (type, page) => {
684
- if (type === "page") return labels.goToPage(page);
685
- return type === "previous" ? labels.previousPage : labels.nextPage;
686
- }
687
- }
688
- )
689
- ]
690
- }
691
- );
608
+ /** Paged footer: rows-per-page + range on the left, pager on the right. */
609
+ function Footer({ pagination, total, limit, setPage, setLimit, labels }) {
610
+ return /* @__PURE__ */ jsxs(Stack, {
611
+ direction: "row",
612
+ spacing: 2,
613
+ useFlexGap: true,
614
+ sx: {
615
+ alignItems: "center",
616
+ justifyContent: "space-between",
617
+ flexWrap: "wrap"
618
+ },
619
+ children: [/* @__PURE__ */ jsxs(Stack, {
620
+ direction: "row",
621
+ spacing: 1.5,
622
+ useFlexGap: true,
623
+ sx: { alignItems: "center" },
624
+ children: [/* @__PURE__ */ jsx(TextField, {
625
+ select: true,
626
+ size: "small",
627
+ label: labels.rowsPerPage,
628
+ value: String(limit),
629
+ onChange: (e) => setLimit(Number(e.target.value)),
630
+ sx: { minWidth: 100 },
631
+ children: pageSizeOptions(limit).map((n) => /* @__PURE__ */ jsx(MenuItem, {
632
+ value: String(n),
633
+ children: n
634
+ }, n))
635
+ }), total > 0 && /* @__PURE__ */ jsx(Typography, {
636
+ variant: "caption",
637
+ color: "text.secondary",
638
+ children: labels.showing({
639
+ from: pagination.fromIndex,
640
+ to: pagination.toIndex,
641
+ total
642
+ })
643
+ })]
644
+ }), /* @__PURE__ */ jsx(Pagination, {
645
+ count: pagination.totalPages,
646
+ page: pagination.safePage,
647
+ onChange: (_, page) => setPage(page),
648
+ size: "small",
649
+ getItemAriaLabel: (type, page) => {
650
+ if (type === "page") return labels.goToPage(page);
651
+ return type === "previous" ? labels.previousPage : labels.nextPage;
652
+ }
653
+ })]
654
+ });
692
655
  }
693
- function ErrorState({
694
- error,
695
- labels,
696
- onRetry
697
- }) {
698
- return /* @__PURE__ */ jsxs(
699
- Alert,
700
- {
701
- severity: "error",
702
- action: onRetry ? /* @__PURE__ */ jsx(Button, { color: "inherit", size: "small", onClick: onRetry, children: labels.retry }) : void 0,
703
- children: [
704
- /* @__PURE__ */ jsx("strong", { children: labels.errorTitle }),
705
- " \u2014 ",
706
- error.message
707
- ]
708
- }
709
- );
656
+ /** Error alert. */
657
+ function ErrorState({ error, labels, onRetry }) {
658
+ return /* @__PURE__ */ jsxs(Alert, {
659
+ severity: "error",
660
+ action: onRetry ? /* @__PURE__ */ jsx(Button, {
661
+ color: "inherit",
662
+ size: "small",
663
+ onClick: onRetry,
664
+ children: labels.retry
665
+ }) : void 0,
666
+ children: [
667
+ /* @__PURE__ */ jsx("strong", { children: labels.errorTitle }),
668
+ " ",
669
+ error.message
670
+ ]
671
+ });
710
672
  }
711
- function LoadingState({
712
- rows,
713
- columns,
714
- loadingLabel
715
- }) {
716
- return /* @__PURE__ */ jsxs(
717
- Box,
718
- {
719
- role: "status",
720
- "aria-busy": "true",
721
- "aria-live": "polite",
722
- "data-testid": "adapttable-loading",
723
- children: [
724
- Array.from({ length: rows }, (_, r) => /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 2, sx: { py: 1 }, children: Array.from({ length: Math.max(columns, 1) }, (_2, c) => /* @__PURE__ */ jsx(Skeleton, { variant: "text", width: c === 0 ? "30%" : "20%" }, c)) }, r)),
725
- loadingLabel ? /* @__PURE__ */ jsx(Box, { component: "span", sx: srOnly, children: loadingLabel }) : null
726
- ]
727
- }
728
- );
673
+ /** Skeleton loading placeholder. */
674
+ function LoadingState({ rows, columns, loadingLabel }) {
675
+ return /* @__PURE__ */ jsxs(Box, {
676
+ role: "status",
677
+ "aria-busy": "true",
678
+ "aria-live": "polite",
679
+ "data-testid": "adapttable-loading",
680
+ children: [Array.from({ length: rows }, (_, r) => /* @__PURE__ */ jsx(Stack, {
681
+ direction: "row",
682
+ spacing: 2,
683
+ sx: { py: 1 },
684
+ children: Array.from({ length: Math.max(columns, 1) }, (_, c) => /* @__PURE__ */ jsx(Skeleton, {
685
+ variant: "text",
686
+ width: c === 0 ? "30%" : "20%"
687
+ }, c))
688
+ }, r)), loadingLabel ? /* @__PURE__ */ jsx(Box, {
689
+ component: "span",
690
+ sx: srOnly,
691
+ children: loadingLabel
692
+ }) : null]
693
+ });
729
694
  }
730
- function FilterDrawer({
731
- open,
732
- onClose,
733
- filters,
734
- activeFilterCount,
735
- onClearFilters,
736
- labels,
737
- dir = "ltr"
738
- }) {
739
- return /* @__PURE__ */ jsx(
740
- Drawer,
741
- {
742
- anchor: dir === "rtl" ? "left" : "right",
743
- open,
744
- onClose,
745
- children: /* @__PURE__ */ jsxs(
746
- Box,
747
- {
748
- sx: {
749
- width: 360,
750
- p: 2,
751
- display: "flex",
752
- flexDirection: "column",
753
- gap: 2,
754
- height: "100%"
755
- },
756
- children: [
757
- /* @__PURE__ */ jsx(Typography, { variant: "h6", children: labels.filters }),
758
- /* @__PURE__ */ jsx(Box, { sx: { flex: 1, display: "flex", flexDirection: "column", gap: 2 }, children: filters }),
759
- /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [
760
- /* @__PURE__ */ jsx(Button, { onClick: onClearFilters, disabled: activeFilterCount === 0, children: labels.clearAll }),
761
- /* @__PURE__ */ jsx(Button, { variant: "contained", onClick: onClose, children: labels.applyFilters })
762
- ] })
763
- ]
764
- }
765
- )
766
- }
767
- );
695
+ /** Filters drawer. */
696
+ function FilterDrawer({ open, onClose, filters, activeFilterCount, onClearFilters, labels, dir = "ltr" }) {
697
+ return /* @__PURE__ */ jsx(Drawer, {
698
+ anchor: dir === "rtl" ? "left" : "right",
699
+ open,
700
+ onClose,
701
+ children: /* @__PURE__ */ jsxs(Box, {
702
+ sx: {
703
+ width: 360,
704
+ p: 2,
705
+ display: "flex",
706
+ flexDirection: "column",
707
+ gap: 2,
708
+ height: "100%"
709
+ },
710
+ children: [
711
+ /* @__PURE__ */ jsx(Typography, {
712
+ variant: "h6",
713
+ children: labels.filters
714
+ }),
715
+ /* @__PURE__ */ jsx(Box, {
716
+ sx: {
717
+ flex: 1,
718
+ display: "flex",
719
+ flexDirection: "column",
720
+ gap: 2
721
+ },
722
+ children: filters
723
+ }),
724
+ /* @__PURE__ */ jsxs(Stack, {
725
+ direction: "row",
726
+ sx: { justifyContent: "space-between" },
727
+ children: [/* @__PURE__ */ jsx(Button, {
728
+ onClick: onClearFilters,
729
+ disabled: activeFilterCount === 0,
730
+ children: labels.clearAll
731
+ }), /* @__PURE__ */ jsx(Button, {
732
+ variant: "contained",
733
+ onClick: onClose,
734
+ children: labels.applyFilters
735
+ })]
736
+ })
737
+ ]
738
+ })
739
+ });
768
740
  }
769
- function VisibilityToggle({
770
- hidden,
771
- name,
772
- labels,
773
- onToggle
774
- }) {
775
- return /* @__PURE__ */ jsx(
776
- IconButton,
777
- {
778
- size: "small",
779
- "aria-label": `${hidden ? labels.showColumn : labels.hideColumn}: ${name}`,
780
- "aria-pressed": !hidden,
781
- color: hidden ? "default" : "primary",
782
- onClick: onToggle,
783
- children: /* @__PURE__ */ jsx(EyeIcon, { off: hidden })
784
- }
785
- );
741
+ //#endregion
742
+ //#region src/components/ColumnMenu.tsx
743
+ /** Eye show/hide toggle — shared by the data rows and the actions row. */
744
+ function VisibilityToggle({ hidden, name, labels, onToggle }) {
745
+ return /* @__PURE__ */ jsx(IconButton, {
746
+ size: "small",
747
+ "aria-label": `${hidden ? labels.showColumn : labels.hideColumn}: ${name}`,
748
+ "aria-pressed": !hidden,
749
+ color: hidden ? "default" : "primary",
750
+ onClick: onToggle,
751
+ children: /* @__PURE__ */ jsx(EyeIcon, { off: hidden })
752
+ });
786
753
  }
787
- function RowName({
788
- hidden,
789
- name
790
- }) {
791
- return /* @__PURE__ */ jsx(
792
- Typography,
793
- {
794
- variant: "body2",
795
- sx: {
796
- flex: 1,
797
- color: hidden ? "text.disabled" : "text.primary",
798
- textDecoration: hidden ? "line-through" : "none"
799
- },
800
- children: name
801
- }
802
- );
754
+ /** Row name — struck through and dimmed while the column is hidden. */
755
+ function RowName({ hidden, name }) {
756
+ return /* @__PURE__ */ jsx(Typography, {
757
+ variant: "body2",
758
+ sx: {
759
+ flex: 1,
760
+ color: hidden ? "text.disabled" : "text.primary",
761
+ textDecoration: hidden ? "line-through" : "none"
762
+ },
763
+ children: name
764
+ });
803
765
  }
804
- function PinToggle({
805
- active,
806
- label,
807
- onClick
808
- }) {
809
- return /* @__PURE__ */ jsx(
810
- IconButton,
811
- {
812
- size: "small",
813
- color: active ? "primary" : "default",
814
- "aria-label": label,
815
- onClick,
816
- children: /* @__PURE__ */ jsx(PinIcon, {})
817
- }
818
- );
766
+ /** Pin button — shared markup; the caller decides the cycle and the label. */
767
+ function PinToggle({ active, label, onClick }) {
768
+ return /* @__PURE__ */ jsx(IconButton, {
769
+ size: "small",
770
+ color: active ? "primary" : "default",
771
+ "aria-label": label,
772
+ onClick,
773
+ children: /* @__PURE__ */ jsx(PinIcon, {})
774
+ });
819
775
  }
820
- function ActionsRow({
821
- layout,
822
- labels
823
- }) {
824
- const hidden = layout.isHidden(ACTIONS_COLUMN_KEY);
825
- const pinned = layout.state.pinned[ACTIONS_COLUMN_KEY] === "right";
826
- return /* @__PURE__ */ jsxs(Fragment, { children: [
827
- /* @__PURE__ */ jsx(Divider, { sx: { my: 0.5 } }),
828
- /* @__PURE__ */ jsxs(
829
- Stack,
830
- {
831
- direction: "row",
832
- alignItems: "center",
833
- spacing: 0.5,
834
- sx: { px: 0.5, py: 0.25 },
835
- children: [
836
- /* @__PURE__ */ jsx(Box, { "aria-hidden": true, sx: { width: 24 } }),
837
- /* @__PURE__ */ jsx(
838
- VisibilityToggle,
839
- {
840
- hidden,
841
- name: labels.actions,
842
- labels,
843
- onToggle: () => layout.toggleVisible(ACTIONS_COLUMN_KEY)
844
- }
845
- ),
846
- /* @__PURE__ */ jsx(RowName, { hidden, name: labels.actions }),
847
- /* @__PURE__ */ jsx(
848
- PinToggle,
849
- {
850
- active: pinned,
851
- label: `${pinned ? labels.unpin : labels.pinRight}: ${labels.actions}`,
852
- onClick: () => layout.setPinned(ACTIONS_COLUMN_KEY, pinned ? void 0 : "right")
853
- }
854
- )
855
- ]
856
- }
857
- )
858
- ] });
776
+ /**
777
+ * Trailing menu row for the injected row-actions column. It is not a data
778
+ * column — no reorder grip, no left pin — but the layout state treats the
779
+ * reserved `"actions"` key like any other, so the eye hides it and the pin is
780
+ * a ONE-CLICK right↔unpinned toggle (the column always trails, so a left pin
781
+ * would be meaningless).
782
+ */
783
+ function ActionsRow({ layout, labels }) {
784
+ const hidden = layout.isHidden(ACTIONS_COLUMN_KEY);
785
+ const pinned = layout.state.pinned[ACTIONS_COLUMN_KEY] === "right";
786
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Divider, { sx: { my: .5 } }), /* @__PURE__ */ jsxs(Stack, {
787
+ direction: "row",
788
+ spacing: .5,
789
+ sx: {
790
+ px: .5,
791
+ py: .25,
792
+ alignItems: "center"
793
+ },
794
+ children: [
795
+ /* @__PURE__ */ jsx(Box, {
796
+ "aria-hidden": true,
797
+ sx: { width: 24 }
798
+ }),
799
+ /* @__PURE__ */ jsx(VisibilityToggle, {
800
+ hidden,
801
+ name: labels.actions,
802
+ labels,
803
+ onToggle: () => layout.toggleVisible(ACTIONS_COLUMN_KEY)
804
+ }),
805
+ /* @__PURE__ */ jsx(RowName, {
806
+ hidden,
807
+ name: labels.actions
808
+ }),
809
+ /* @__PURE__ */ jsx(PinToggle, {
810
+ active: pinned,
811
+ label: `${pinned ? labels.unpin : labels.pinRight}: ${labels.actions}`,
812
+ onClick: () => layout.setPinned(ACTIONS_COLUMN_KEY, pinned ? void 0 : "right")
813
+ })
814
+ ]
815
+ })] });
859
816
  }
860
- function ColumnMenu({
861
- allColumns,
862
- layout,
863
- labels,
864
- hasRowActions
865
- }) {
866
- const drag = useColumnDragState();
867
- const [anchor, setAnchor] = useState(null);
868
- return /* @__PURE__ */ jsxs(Fragment, { children: [
869
- /* @__PURE__ */ jsx(
870
- Button,
871
- {
872
- size: "small",
873
- variant: "outlined",
874
- "aria-expanded": anchor !== null,
875
- "aria-haspopup": "true",
876
- onClick: (e) => setAnchor(e.currentTarget),
877
- children: labels.columns
878
- }
879
- ),
880
- /* @__PURE__ */ jsx(
881
- Popover,
882
- {
883
- anchorEl: anchor,
884
- open: anchor !== null,
885
- onClose: () => setAnchor(null),
886
- anchorOrigin: { vertical: "bottom", horizontal: "left" },
887
- children: /* @__PURE__ */ jsxs(Box, { sx: { p: 0.75, minWidth: 250 }, children: [
888
- /* @__PURE__ */ jsx(
889
- Typography,
890
- {
891
- variant: "caption",
892
- sx: {
893
- display: "block",
894
- px: 1,
895
- pb: 0.5,
896
- fontWeight: 600,
897
- textTransform: "uppercase",
898
- letterSpacing: "0.06em",
899
- color: "text.secondary"
900
- },
901
- children: labels.columns
902
- }
903
- ),
904
- columnMenuRows(allColumns, layout).map((r) => {
905
- const indicator = drag.rowAttrs(r.key, r.index);
906
- const edge = indicator["data-drop"];
907
- const edgeOffset = edge === "before" ? "2px" : "-2px";
908
- return /* @__PURE__ */ jsxs(
909
- Stack,
910
- {
911
- direction: "row",
912
- alignItems: "center",
913
- spacing: 0.5,
914
- sx: {
915
- px: 0.5,
916
- py: 0.25,
917
- cursor: "grab",
918
- opacity: "data-dragging" in indicator ? 0.4 : void 0,
919
- boxShadow: edge ? (theme) => `inset 0 ${edgeOffset} 0 0 ${theme.palette.primary.main}` : void 0
920
- },
921
- ...drag.rowDragProps(r.key, r.index),
922
- ...drag.dropProps(r.index, layout.move),
923
- ...indicator,
924
- children: [
925
- /* @__PURE__ */ jsx(
926
- IconButton,
927
- {
928
- size: "small",
929
- sx: { cursor: "grab", color: "text.disabled" },
930
- ...columnReorderKeyProps(
931
- r.key,
932
- r.index,
933
- layout.move,
934
- `${labels.moveLeft} / ${labels.moveRight}: ${r.name}`
935
- ),
936
- children: /* @__PURE__ */ jsx(GripIcon, {})
937
- }
938
- ),
939
- /* @__PURE__ */ jsx(
940
- VisibilityToggle,
941
- {
942
- hidden: r.hidden,
943
- name: r.name,
944
- labels,
945
- onToggle: () => layout.toggleVisible(r.key)
946
- }
947
- ),
948
- /* @__PURE__ */ jsx(RowName, { hidden: r.hidden, name: r.name }),
949
- /* @__PURE__ */ jsx(
950
- PinToggle,
951
- {
952
- active: r.pinned !== void 0,
953
- label: `${pinActionLabel(r.pinned, labels)}: ${r.name}`,
954
- onClick: () => layout.setPinned(r.key, nextPinSide(r.pinned))
955
- }
956
- )
957
- ]
958
- },
959
- r.key
960
- );
961
- }),
962
- hasRowActions && /* @__PURE__ */ jsx(ActionsRow, { layout, labels }),
963
- /* @__PURE__ */ jsx(Divider, { sx: { my: 0.5 } }),
964
- /* @__PURE__ */ jsx(
965
- Button,
966
- {
967
- size: "small",
968
- fullWidth: true,
969
- sx: { justifyContent: "flex-start" },
970
- onClick: () => layout.reset(),
971
- children: labels.resetColumns
972
- }
973
- )
974
- ] })
975
- }
976
- )
977
- ] });
817
+ /**
818
+ * MUI column-management popover: per-column drag grip (reorder), eye
819
+ * (show/hide), and pin toggle. A `Popover` (not a `Menu`) so list keyboard
820
+ * navigation never fights the grip's arrow-key reorder. With row actions, a
821
+ * separated trailing row manages the injected actions column too.
822
+ */
823
+ function ColumnMenu({ allColumns, layout, labels, hasRowActions }) {
824
+ const drag = useColumnDragState();
825
+ const [anchor, setAnchor] = useState(null);
826
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
827
+ size: "small",
828
+ variant: "outlined",
829
+ "aria-expanded": anchor !== null,
830
+ "aria-haspopup": "true",
831
+ onClick: (e) => setAnchor(e.currentTarget),
832
+ children: labels.columns
833
+ }), /* @__PURE__ */ jsx(Popover, {
834
+ anchorEl: anchor,
835
+ open: anchor !== null,
836
+ onClose: () => setAnchor(null),
837
+ anchorOrigin: {
838
+ vertical: "bottom",
839
+ horizontal: "left"
840
+ },
841
+ children: /* @__PURE__ */ jsxs(Box, {
842
+ sx: {
843
+ p: .75,
844
+ minWidth: 250
845
+ },
846
+ children: [
847
+ /* @__PURE__ */ jsx(Typography, {
848
+ variant: "caption",
849
+ sx: {
850
+ display: "block",
851
+ px: 1,
852
+ pb: .5,
853
+ fontWeight: 600,
854
+ textTransform: "uppercase",
855
+ letterSpacing: "0.06em",
856
+ color: "text.secondary"
857
+ },
858
+ children: labels.columns
859
+ }),
860
+ columnMenuRows(allColumns, layout).map((r) => {
861
+ const indicator = drag.rowAttrs(r.key, r.index);
862
+ const edge = indicator["data-drop"];
863
+ const edgeOffset = edge === "before" ? "2px" : "-2px";
864
+ return /* @__PURE__ */ jsxs(Stack, {
865
+ direction: "row",
866
+ spacing: .5,
867
+ sx: {
868
+ alignItems: "center",
869
+ px: .5,
870
+ py: .25,
871
+ cursor: "grab",
872
+ opacity: "data-dragging" in indicator ? .4 : void 0,
873
+ boxShadow: edge ? (theme) => `inset 0 ${edgeOffset} 0 0 ${theme.palette.primary.main}` : void 0
874
+ },
875
+ ...drag.rowDragProps(r.key, r.index),
876
+ ...drag.dropProps(r.index, layout.move),
877
+ ...indicator,
878
+ children: [
879
+ /* @__PURE__ */ jsx(IconButton, {
880
+ size: "small",
881
+ sx: {
882
+ cursor: "grab",
883
+ color: "text.disabled"
884
+ },
885
+ ...columnReorderKeyProps(r.key, r.index, layout.move, `${labels.moveLeft} / ${labels.moveRight}: ${r.name}`),
886
+ children: /* @__PURE__ */ jsx(GripIcon, {})
887
+ }),
888
+ /* @__PURE__ */ jsx(VisibilityToggle, {
889
+ hidden: r.hidden,
890
+ name: r.name,
891
+ labels,
892
+ onToggle: () => layout.toggleVisible(r.key)
893
+ }),
894
+ /* @__PURE__ */ jsx(RowName, {
895
+ hidden: r.hidden,
896
+ name: r.name
897
+ }),
898
+ /* @__PURE__ */ jsx(PinToggle, {
899
+ active: r.pinned !== void 0,
900
+ label: `${pinActionLabel(r.pinned, labels)}: ${r.name}`,
901
+ onClick: () => layout.setPinned(r.key, nextPinSide(r.pinned))
902
+ })
903
+ ]
904
+ }, r.key);
905
+ }),
906
+ hasRowActions && /* @__PURE__ */ jsx(ActionsRow, {
907
+ layout,
908
+ labels
909
+ }),
910
+ /* @__PURE__ */ jsx(Divider, { sx: { my: .5 } }),
911
+ /* @__PURE__ */ jsx(Button, {
912
+ size: "small",
913
+ fullWidth: true,
914
+ sx: { justifyContent: "flex-start" },
915
+ onClick: () => layout.reset(),
916
+ children: labels.resetColumns
917
+ })
918
+ ]
919
+ })
920
+ })] });
978
921
  }
979
- var RESIZE_HANDLE_SX = {
980
- position: "absolute",
981
- insetInlineEnd: 0,
982
- top: 0,
983
- height: "100%",
984
- width: 8,
985
- cursor: "col-resize",
986
- touchAction: "none",
987
- userSelect: "none"
922
+ //#endregion
923
+ //#region src/components/tables.tsx
924
+ /** Sx for an absolutely-positioned column-resize handle. */
925
+ const RESIZE_HANDLE_SX = {
926
+ position: "absolute",
927
+ insetInlineEnd: 0,
928
+ top: 0,
929
+ height: "100%",
930
+ width: 8,
931
+ cursor: "col-resize",
932
+ touchAction: "none",
933
+ userSelect: "none"
988
934
  };
935
+ /** Map a destructive colour token to MUI's `"error"` palette, else default. */
989
936
  function muiColor(color) {
990
- return color === "danger" || color === "red" || color === "error" ? "error" : "default";
937
+ return color === "danger" || color === "red" || color === "error" ? "error" : "default";
991
938
  }
992
- var EXPAND_WIDTH = 48;
939
+ /** Width (px) of the leading expand-chevron column (MUI's checkbox cell). */
940
+ const EXPAND_WIDTH = 48;
941
+ /**
942
+ * Identity-stable dispatcher for `selection.toggle` / `expansion.toggle`.
943
+ * `selection.toggle` is recreated whenever the selection changes, so handing
944
+ * it straight to a memoized row would either defeat the memo (if compared)
945
+ * or go stale (if not — in the controlled mode it computes from the captured
946
+ * set). This wrapper never changes identity and always dispatches to the
947
+ * CURRENT target, so skipped rows still toggle against fresh state.
948
+ */
993
949
  function useStableToggle(target) {
994
- const ref = useRef(target);
995
- ref.current = target;
996
- return useCallback((id) => ref.current?.toggle(id), []);
950
+ const ref = useRef(target);
951
+ ref.current = target;
952
+ return useCallback((id) => ref.current?.toggle(id), []);
997
953
  }
998
- function ExpandChevron({
999
- expanded,
1000
- dir
1001
- }) {
1002
- let transform;
1003
- if (expanded) transform = "rotate(90deg)";
1004
- else if (dir === "rtl") transform = "rotate(180deg)";
1005
- return /* @__PURE__ */ jsx(
1006
- Box,
1007
- {
1008
- component: "span",
1009
- "aria-hidden": true,
1010
- sx: { display: "inline-flex", transition: "transform 150ms", transform },
1011
- children: /* @__PURE__ */ jsx(
1012
- "svg",
1013
- {
1014
- width: "1em",
1015
- height: "1em",
1016
- viewBox: "0 0 24 24",
1017
- fill: "none",
1018
- stroke: "currentColor",
1019
- strokeWidth: "2",
1020
- strokeLinecap: "round",
1021
- strokeLinejoin: "round",
1022
- children: /* @__PURE__ */ jsx("path", { d: "M9 6l6 6-6 6" })
1023
- }
1024
- )
1025
- }
1026
- );
954
+ /** Inline chevron pointing at the reading end; rotates down when open. */
955
+ function ExpandChevron({ expanded, dir }) {
956
+ let transform;
957
+ if (expanded) transform = "rotate(90deg)";
958
+ else if (dir === "rtl") transform = "rotate(180deg)";
959
+ return /* @__PURE__ */ jsx(Box, {
960
+ component: "span",
961
+ "aria-hidden": true,
962
+ sx: {
963
+ display: "inline-flex",
964
+ transition: "transform 150ms",
965
+ transform
966
+ },
967
+ children: /* @__PURE__ */ jsx("svg", {
968
+ width: "1em",
969
+ height: "1em",
970
+ viewBox: "0 0 24 24",
971
+ fill: "none",
972
+ stroke: "currentColor",
973
+ strokeWidth: "2",
974
+ strokeLinecap: "round",
975
+ strokeLinejoin: "round",
976
+ children: /* @__PURE__ */ jsx("path", { d: "M9 6l6 6-6 6" })
977
+ })
978
+ });
1027
979
  }
1028
- function ExpandToggle({
1029
- id,
1030
- expanded,
1031
- onToggle,
1032
- dir,
1033
- expandLabel,
1034
- collapseLabel
1035
- }) {
1036
- return /* @__PURE__ */ jsx(
1037
- IconButton,
1038
- {
1039
- size: "small",
1040
- "aria-expanded": expanded,
1041
- "aria-label": expanded ? collapseLabel : expandLabel,
1042
- onClick: () => onToggle(id),
1043
- children: /* @__PURE__ */ jsx(ExpandChevron, { expanded, dir })
1044
- }
1045
- );
980
+ /** The per-row expand/collapse chevron button (desktop cell + mobile card). */
981
+ function ExpandToggle({ id, expanded, onToggle, dir, expandLabel, collapseLabel }) {
982
+ return /* @__PURE__ */ jsx(IconButton, {
983
+ size: "small",
984
+ "aria-expanded": expanded,
985
+ "aria-label": expanded ? collapseLabel : expandLabel,
986
+ onClick: () => onToggle(id),
987
+ children: /* @__PURE__ */ jsx(ExpandChevron, {
988
+ expanded,
989
+ dir
990
+ })
991
+ });
1046
992
  }
993
+ /**
994
+ * Logical (RTL-aware) `text-align` for a column. Applied via `sx` rather
995
+ * than MUI's physical `align` prop so `"end"` follows the writing direction
996
+ * (right in LTR, left in RTL).
997
+ */
1047
998
  function muiAlign(align) {
1048
- if (align === "center") return "center";
1049
- if (align === "end") return "end";
1050
- return "start";
999
+ if (align === "center") return "center";
1000
+ if (align === "end") return "end";
1001
+ return "start";
1051
1002
  }
1052
- function RowActionButtons({
1053
- row,
1054
- actions,
1055
- confirm,
1056
- cancelLabel
1057
- }) {
1058
- return /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 0.5, justifyContent: "flex-end", children: actions.map((action) => {
1059
- if (action.isHidden?.(row)) return null;
1060
- const reason = resolveDisabledReason(action.disabledReason?.(row));
1061
- const disabled = reason !== void 0 || (action.isDisabled?.(row) ?? false);
1062
- return /* @__PURE__ */ jsx(Tooltip, { title: reason ?? action.label, children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(
1063
- IconButton,
1064
- {
1065
- size: "small",
1066
- color: muiColor(action.color),
1067
- disabled,
1068
- "aria-label": action.label,
1069
- onClick: (
1070
- // The disabled attribute already blocks activation, so
1071
- // attach the handler only when the action can run.
1072
- disabled ? void 0 : (e) => {
1073
- e.stopPropagation();
1074
- runRowAction(action, row, confirm, cancelLabel);
1075
- }
1076
- ),
1077
- children: action.icon ?? /* @__PURE__ */ jsx(Typography, { variant: "caption", children: action.label })
1078
- }
1079
- ) }) }, action.key);
1080
- }) });
1003
+ function RowActionButtons({ row, actions, confirm, cancelLabel }) {
1004
+ return /* @__PURE__ */ jsx(Stack, {
1005
+ direction: "row",
1006
+ spacing: .5,
1007
+ sx: { justifyContent: "flex-end" },
1008
+ children: actions.map((action) => {
1009
+ if (action.isHidden?.(row)) return null;
1010
+ const reason = resolveDisabledReason(action.disabledReason?.(row));
1011
+ const disabled = reason !== void 0 || (action.isDisabled?.(row) ?? false);
1012
+ return /* @__PURE__ */ jsx(Tooltip, {
1013
+ title: reason ?? action.label,
1014
+ children: /* @__PURE__ */ jsx("span", { children: /* @__PURE__ */ jsx(IconButton, {
1015
+ size: "small",
1016
+ color: muiColor(action.color),
1017
+ disabled,
1018
+ "aria-label": action.label,
1019
+ onClick: disabled ? void 0 : (e) => {
1020
+ e.stopPropagation();
1021
+ runRowAction(action, row, confirm, cancelLabel);
1022
+ },
1023
+ children: action.icon ?? /* @__PURE__ */ jsx(Typography, {
1024
+ variant: "caption",
1025
+ children: action.label
1026
+ })
1027
+ }) })
1028
+ }, action.key);
1029
+ })
1030
+ });
1081
1031
  }
1082
- var DESKTOP_ROW_COMPARED = [
1083
- "row",
1084
- "index",
1085
- "selected",
1086
- "expanded",
1087
- "columns",
1088
- "sx",
1089
- "columnSpan",
1090
- "size",
1091
- "dir",
1092
- "className",
1093
- "hasSelection",
1094
- "hasExpansion",
1095
- "showActions",
1096
- "selectRowLabel",
1097
- "cancelLabel",
1098
- "expandLabel",
1099
- "collapseLabel"
1032
+ /**
1033
+ * The visual inputs a desktop row re-renders for. Deliberately NOT the
1034
+ * per-render `table` object or the handler props: handlers are stable (or
1035
+ * latest-dispatching wrappers), and everything that changes what the row
1036
+ * LOOKS like is listed here — row identity, selection/expansion flags,
1037
+ * density, column set, the precomputed sx map (pins + widths), the
1038
+ * rowClassName output, and direction.
1039
+ */
1040
+ const DESKTOP_ROW_COMPARED = [
1041
+ "row",
1042
+ "index",
1043
+ "selected",
1044
+ "expanded",
1045
+ "columns",
1046
+ "sx",
1047
+ "columnSpan",
1048
+ "size",
1049
+ "dir",
1050
+ "className",
1051
+ "hasSelection",
1052
+ "hasExpansion",
1053
+ "showActions",
1054
+ "selectRowLabel",
1055
+ "cancelLabel",
1056
+ "expandLabel",
1057
+ "collapseLabel"
1100
1058
  ];
1101
1059
  function desktopRowPropsAreEqual(prev, next) {
1102
- return DESKTOP_ROW_COMPARED.every((key) => prev[key] === next[key]);
1060
+ return DESKTOP_ROW_COMPARED.every((key) => prev[key] === next[key]);
1103
1061
  }
1104
- function DesktopRowImpl({
1105
- row,
1106
- index,
1107
- selected,
1108
- expanded,
1109
- columns,
1110
- sx,
1111
- columnSpan,
1112
- dir,
1113
- className,
1114
- hasSelection,
1115
- hasExpansion,
1116
- showActions,
1117
- selectRowLabel,
1118
- cancelLabel,
1119
- expandLabel,
1120
- collapseLabel,
1121
- id,
1122
- rowActions,
1123
- confirm,
1124
- renderRowDetail,
1125
- onToggleSelect,
1126
- onToggleExpand,
1127
- onRowClick,
1128
- prefetch,
1129
- measureElement
1130
- }) {
1131
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1132
- /* @__PURE__ */ jsxs(
1133
- TableRow,
1134
- {
1135
- ...rowClickProps(row, onRowClick),
1136
- className,
1137
- ref: measureElement,
1138
- "data-index": index,
1139
- hover: true,
1140
- selected,
1141
- onMouseEnter: prefetch ? () => prefetch(row) : void 0,
1142
- children: [
1143
- hasExpansion && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox", sx: sx.expand, children: /* @__PURE__ */ jsx(
1144
- ExpandToggle,
1145
- {
1146
- id,
1147
- expanded,
1148
- onToggle: onToggleExpand,
1149
- dir,
1150
- expandLabel,
1151
- collapseLabel
1152
- }
1153
- ) }),
1154
- hasSelection && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox", sx: sx.selection, children: /* @__PURE__ */ jsx(
1155
- Checkbox,
1156
- {
1157
- slotProps: { input: { "aria-label": selectRowLabel } },
1158
- checked: selected,
1159
- onChange: () => onToggleSelect(id)
1160
- }
1161
- ) }),
1162
- columns.map((column) => /* @__PURE__ */ jsx(TableCell, { sx: sx.cells[column.key], children: column.Cell ? /* @__PURE__ */ jsx(column.Cell, { row, rowIndex: index }) : column.accessor?.(row) }, column.key)),
1163
- showActions && /* @__PURE__ */ jsx(TableCell, { sx: sx.actions, children: /* @__PURE__ */ jsx(
1164
- RowActionButtons,
1165
- {
1166
- row,
1167
- actions: rowActions,
1168
- confirm,
1169
- cancelLabel
1170
- }
1171
- ) })
1172
- ]
1173
- }
1174
- ),
1175
- expanded && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, { colSpan: columnSpan, children: renderRowDetail(row) }) })
1176
- ] });
1062
+ function DesktopRowImpl({ row, index, selected, expanded, columns, sx, columnSpan, dir, className, hasSelection, hasExpansion, showActions, selectRowLabel, cancelLabel, expandLabel, collapseLabel, id, rowActions, confirm, renderRowDetail, onToggleSelect, onToggleExpand, onRowClick, prefetch, measureElement }) {
1063
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(TableRow, {
1064
+ ...rowClickProps(row, onRowClick),
1065
+ className,
1066
+ ref: measureElement,
1067
+ "data-index": index,
1068
+ hover: true,
1069
+ selected,
1070
+ onMouseEnter: prefetch ? () => prefetch(row) : void 0,
1071
+ children: [
1072
+ hasExpansion && /* @__PURE__ */ jsx(TableCell, {
1073
+ padding: "checkbox",
1074
+ sx: sx.expand,
1075
+ children: /* @__PURE__ */ jsx(ExpandToggle, {
1076
+ id,
1077
+ expanded,
1078
+ onToggle: onToggleExpand,
1079
+ dir,
1080
+ expandLabel,
1081
+ collapseLabel
1082
+ })
1083
+ }),
1084
+ hasSelection && /* @__PURE__ */ jsx(TableCell, {
1085
+ padding: "checkbox",
1086
+ sx: sx.selection,
1087
+ children: /* @__PURE__ */ jsx(Checkbox, {
1088
+ slotProps: { input: { "aria-label": selectRowLabel } },
1089
+ checked: selected,
1090
+ onChange: () => onToggleSelect(id)
1091
+ })
1092
+ }),
1093
+ columns.map((column) => /* @__PURE__ */ jsx(TableCell, {
1094
+ sx: sx.cells[column.key],
1095
+ children: column.Cell ? /* @__PURE__ */ jsx(column.Cell, {
1096
+ row,
1097
+ rowIndex: index
1098
+ }) : column.accessor?.(row)
1099
+ }, column.key)),
1100
+ showActions && /* @__PURE__ */ jsx(TableCell, {
1101
+ sx: sx.actions,
1102
+ children: /* @__PURE__ */ jsx(RowActionButtons, {
1103
+ row,
1104
+ actions: rowActions,
1105
+ confirm,
1106
+ cancelLabel
1107
+ })
1108
+ })
1109
+ ]
1110
+ }), expanded && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
1111
+ colSpan: columnSpan,
1112
+ children: renderRowDetail(row)
1113
+ }) })] });
1177
1114
  }
1178
- var DesktopRow = memo(
1179
- DesktopRowImpl,
1180
- desktopRowPropsAreEqual
1181
- );
1182
- function DesktopTable({
1183
- table,
1184
- rows,
1185
- rowActions,
1186
- confirm,
1187
- getRowId,
1188
- size,
1189
- dir,
1190
- prefetch,
1191
- onRowClick,
1192
- rowClassName,
1193
- renderRowDetail,
1194
- summaryRow,
1195
- expansion,
1196
- rowEntries,
1197
- paddingTop = 0,
1198
- paddingBottom = 0,
1199
- measureElement,
1200
- stickyHeader = false,
1201
- stickyTop = 0,
1202
- pinOffset,
1203
- maxHeight,
1204
- virtualScrollRef,
1205
- setWidth,
1206
- columnWidths,
1207
- resizeLabel = "Resize column",
1208
- actionsPinned = false
1209
- }) {
1210
- const { columns, selection, labels, showActions, entries, columnSpan } = tableRenderModel({
1211
- table,
1212
- rows,
1213
- rowActions,
1214
- getRowId,
1215
- rowEntries,
1216
- renderRowDetail,
1217
- expansion
1218
- });
1219
- const groupRow = headerGroupRow(columns);
1220
- const summaryCells = summaryRow?.(rows);
1221
- const isExpanded = expansion && renderRowDetail ? expansion.isExpanded : void 0;
1222
- const expandActive = isExpanded !== void 0;
1223
- const onToggleSelect = useStableToggle(selection);
1224
- const onToggleExpand = useStableToggle(expansion);
1225
- const hasPinned = actionsPinned || table.columns.some((c) => pinOffset?.(c.key) != null);
1226
- const overflow = useHorizontalOverflow();
1227
- const inScrollBox = maxHeight != null || hasPinned || overflow.overflowing;
1228
- const headSx = stickyHeader ? {
1229
- position: "sticky",
1230
- top: inScrollBox ? 0 : stickyTop,
1231
- zIndex: PIN_Z.header,
1232
- bgcolor: "background.paper"
1233
- } : void 0;
1234
- const selectionWidth = 48;
1235
- const actionsWidth = 120;
1236
- const leadLeft = (expandActive ? EXPAND_WIDTH : 0) + (selection ? selectionWidth : 0);
1237
- const leadRight = showActions ? actionsWidth : 0;
1238
- const leads = { left: leadLeft, right: leadRight };
1239
- const selectionLead = expandActive ? EXPAND_WIDTH : 0;
1240
- const hasLeftPin = table.columns.some(
1241
- (c) => pinOffset?.(c.key)?.side === "left"
1242
- );
1243
- const hasRightPin = table.columns.some(
1244
- (c) => pinOffset?.(c.key)?.side === "right"
1245
- );
1246
- const stickActions = hasRightPin || actionsPinned;
1247
- const headCellSx = (column) => {
1248
- const pin = pinnedCellStyle(
1249
- pinOffset?.(column.key),
1250
- PIN_Z.headerPinned,
1251
- leads
1252
- );
1253
- const width = pin ? pinnedColumnWidth(column, columnWidths) : columnWidths?.[column.key] ?? column.width;
1254
- const needsRelative = Boolean(setWidth) && !headSx && !pin;
1255
- return {
1256
- ...headSx,
1257
- ...pin && { ...pin, bgcolor: "background.paper" },
1258
- ...needsRelative && { position: "relative" },
1259
- textAlign: muiAlign(column.align),
1260
- ...width != null && { width }
1261
- };
1262
- };
1263
- const edgeHeadSx = (side, active, lead = 0) => {
1264
- const pin = edgePinStyle(side, active, PIN_Z.headerPinned);
1265
- return {
1266
- ...headSx,
1267
- ...pin && { ...pin, bgcolor: "background.paper" },
1268
- ...pin && lead > 0 && { insetInlineStart: lead }
1269
- };
1270
- };
1271
- const rowSx = useMemo(() => {
1272
- const edge = (side, active, lead = 0) => {
1273
- const pin = edgePinStyle(side, active, PIN_Z.body);
1274
- if (!pin) return void 0;
1275
- return {
1276
- ...pin,
1277
- ...lead > 0 && { insetInlineStart: lead },
1278
- bgcolor: "background.paper"
1279
- };
1280
- };
1281
- const cells = {};
1282
- for (const column of columns) {
1283
- const pin = pinnedCellStyle(pinOffset?.(column.key), PIN_Z.body, {
1284
- left: leadLeft,
1285
- right: leadRight
1286
- });
1287
- cells[column.key] = {
1288
- ...pin && { ...pin, bgcolor: "background.paper" },
1289
- textAlign: muiAlign(column.align)
1290
- };
1291
- }
1292
- return {
1293
- cells,
1294
- expand: edge("left", hasLeftPin),
1295
- selection: edge("left", hasLeftPin, selectionLead),
1296
- actions: { ...edge("right", stickActions), textAlign: "end" }
1297
- };
1298
- }, [
1299
- columns,
1300
- pinOffset,
1301
- leadLeft,
1302
- leadRight,
1303
- hasLeftPin,
1304
- stickActions,
1305
- selectionLead
1306
- ]);
1307
- let boxSx;
1308
- if (maxHeight != null) {
1309
- boxSx = { maxHeight, overflow: "auto" };
1310
- } else if (hasPinned || overflow.overflowing) {
1311
- boxSx = { overflowX: "auto" };
1312
- }
1313
- const minWidth = tableMinWidth(columns, {
1314
- widths: columnWidths,
1315
- extra: leadLeft + leadRight
1316
- });
1317
- return /* @__PURE__ */ jsx(
1318
- Box,
1319
- {
1320
- ref: (node) => {
1321
- overflow.ref(node);
1322
- virtualScrollRef?.(node);
1323
- },
1324
- sx: boxSx,
1325
- children: /* @__PURE__ */ jsxs(
1326
- Table,
1327
- {
1328
- size,
1329
- "aria-label": table.getTableProps()["aria-label"],
1330
- sx: minWidth > 0 ? { minWidth } : void 0,
1331
- children: [
1332
- /* @__PURE__ */ jsxs(TableHead, { children: [
1333
- groupRow && // Decorative group row. It deliberately skips the sticky `headSx`
1334
- // treatment: sticking both header rows at the same `top` would
1335
- // overlap them, so only the sortable header row pins.
1336
- /* @__PURE__ */ jsxs(TableRow, { children: [
1337
- expandActive && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1338
- selection && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1339
- groupRow.map((cell) => /* @__PURE__ */ jsx(
1340
- TableCell,
1341
- {
1342
- colSpan: cell.span,
1343
- sx: { textAlign: "center", fontWeight: 600 },
1344
- children: cell.label
1345
- },
1346
- cell.key
1347
- )),
1348
- showActions && /* @__PURE__ */ jsx(TableCell, {})
1349
- ] }),
1350
- /* @__PURE__ */ jsxs(TableRow, { children: [
1351
- expandActive && /* @__PURE__ */ jsx(
1352
- TableCell,
1353
- {
1354
- padding: "checkbox",
1355
- sx: edgeHeadSx("left", hasLeftPin)
1356
- }
1357
- ),
1358
- selection && /* @__PURE__ */ jsx(
1359
- TableCell,
1360
- {
1361
- padding: "checkbox",
1362
- sx: edgeHeadSx("left", hasLeftPin, selectionLead),
1363
- children: /* @__PURE__ */ jsx(
1364
- Checkbox,
1365
- {
1366
- slotProps: { input: { "aria-label": labels.selectAll } },
1367
- checked: selection.headerState === "all",
1368
- indeterminate: selection.headerState === "some",
1369
- onChange: selection.toggleAll
1370
- }
1371
- )
1372
- }
1373
- ),
1374
- columns.map((column) => {
1375
- const headerCellProps = table.getHeaderCellProps(column);
1376
- const ariaSort = headerCellProps["aria-sort"];
1377
- const active = ariaSort === "ascending" || ariaSort === "descending";
1378
- const sortIndex = headerCellProps["data-sort-index"];
1379
- return /* @__PURE__ */ jsxs(
1380
- TableCell,
1381
- {
1382
- "aria-sort": ariaSort,
1383
- "data-sort-index": sortIndex,
1384
- sx: headCellSx(column),
1385
- children: [
1386
- column.sortable ? /* @__PURE__ */ jsxs(
1387
- TableSortLabel,
1388
- {
1389
- active,
1390
- direction: ariaSort === "descending" ? "desc" : "asc",
1391
- onClick: table.getSortButtonProps(column).onClick,
1392
- children: [
1393
- column.header,
1394
- sortIndex !== void 0 && /* @__PURE__ */ jsx(Box, { component: "span", sx: { fontSize: 10, ml: 0.5 }, children: sortIndex })
1395
- ]
1396
- }
1397
- ) : column.header,
1398
- setWidth && /* @__PURE__ */ jsx(
1399
- Box,
1400
- {
1401
- component: "span",
1402
- sx: RESIZE_HANDLE_SX,
1403
- ...columnResizeHandleProps(
1404
- column.key,
1405
- setWidth,
1406
- `${resizeLabel}: ${typeof column.header === "string" ? column.header : column.key}`
1407
- )
1408
- }
1409
- )
1410
- ]
1411
- },
1412
- column.key
1413
- );
1414
- }),
1415
- showActions && /* @__PURE__ */ jsx(
1416
- TableCell,
1417
- {
1418
- sx: { ...edgeHeadSx("right", stickActions), textAlign: "end" },
1419
- children: labels.actions
1420
- }
1421
- )
1422
- ] })
1423
- ] }),
1424
- /* @__PURE__ */ jsxs(TableBody, { children: [
1425
- paddingTop > 0 && /* @__PURE__ */ jsx(TableRow, { "aria-hidden": true, children: /* @__PURE__ */ jsx(
1426
- TableCell,
1427
- {
1428
- colSpan: columnSpan,
1429
- sx: { height: paddingTop, p: 0 }
1430
- }
1431
- ) }),
1432
- entries.map(({ row, index, key }) => {
1433
- const id = getRowId(row);
1434
- return /* @__PURE__ */ jsx(
1435
- DesktopRow,
1436
- {
1437
- row,
1438
- index,
1439
- selected: selection?.isSelected(id) ?? false,
1440
- expanded: isExpanded ? isExpanded(id) : false,
1441
- columns,
1442
- sx: rowSx,
1443
- columnSpan,
1444
- size,
1445
- dir,
1446
- className: rowClassName?.(row, index),
1447
- hasSelection: Boolean(selection),
1448
- hasExpansion: expandActive,
1449
- showActions,
1450
- selectRowLabel: labels.selectRow,
1451
- cancelLabel: labels.cancel,
1452
- expandLabel: labels.expandRow,
1453
- collapseLabel: labels.collapseRow,
1454
- id,
1455
- rowActions,
1456
- confirm,
1457
- renderRowDetail,
1458
- onToggleSelect,
1459
- onToggleExpand,
1460
- onRowClick,
1461
- prefetch,
1462
- measureElement
1463
- },
1464
- key
1465
- );
1466
- }),
1467
- paddingBottom > 0 && /* @__PURE__ */ jsx(TableRow, { "aria-hidden": true, children: /* @__PURE__ */ jsx(
1468
- TableCell,
1469
- {
1470
- colSpan: columnSpan,
1471
- sx: { height: paddingBottom, p: 0 }
1472
- }
1473
- ) })
1474
- ] }),
1475
- summaryCells && /* @__PURE__ */ jsx(TableFooter, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
1476
- expandActive && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1477
- selection && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1478
- columns.map((column) => (
1479
- // One cell per column keeps the summary aligned under its
1480
- // column; keys absent from the result render empty cells.
1481
- /* @__PURE__ */ jsx(
1482
- TableCell,
1483
- {
1484
- sx: { textAlign: muiAlign(column.align) },
1485
- children: summaryCells[column.key]
1486
- },
1487
- column.key
1488
- )
1489
- )),
1490
- showActions && /* @__PURE__ */ jsx(TableCell, {})
1491
- ] }) })
1492
- ]
1493
- }
1494
- )
1495
- }
1496
- );
1115
+ /**
1116
+ * Memoized desktop row: a search keystroke (or any chrome re-render) must
1117
+ * not re-run every cell accessor, and toggling one row's checkbox must
1118
+ * re-render only that row. The cast restores the generic signature `memo`
1119
+ * erases.
1120
+ */
1121
+ const DesktopRow = memo(DesktopRowImpl, desktopRowPropsAreEqual);
1122
+ /** Desktop MUI table. */
1123
+ function DesktopTable({ table, rows, rowActions, confirm, getRowId, size, dir, prefetch, onRowClick, rowClassName, renderRowDetail, summaryRow, expansion, rowEntries, paddingTop = 0, paddingBottom = 0, measureElement, stickyHeader = false, stickyTop = 0, pinOffset, maxHeight, virtualScrollRef, setWidth, columnWidths, resizeLabel = "Resize column", actionsPinned = false }) {
1124
+ const { columns, selection, labels, showActions, entries, columnSpan } = tableRenderModel({
1125
+ table,
1126
+ rows,
1127
+ rowActions,
1128
+ getRowId,
1129
+ rowEntries,
1130
+ renderRowDetail,
1131
+ expansion
1132
+ });
1133
+ const groupRow = headerGroupRow(columns);
1134
+ const summaryCells = summaryRow?.(rows);
1135
+ const isExpanded = expansion && renderRowDetail ? expansion.isExpanded : void 0;
1136
+ const expandActive = isExpanded !== void 0;
1137
+ const onToggleSelect = useStableToggle(selection);
1138
+ const onToggleExpand = useStableToggle(expansion);
1139
+ const hasPinned = actionsPinned || table.columns.some((c) => pinOffset?.(c.key) != null);
1140
+ const overflow = useHorizontalOverflow();
1141
+ const inScrollBox = maxHeight != null || hasPinned || overflow.overflowing;
1142
+ const headSx = stickyHeader ? {
1143
+ position: "sticky",
1144
+ top: inScrollBox ? 0 : stickyTop,
1145
+ zIndex: PIN_Z.header,
1146
+ bgcolor: "background.paper"
1147
+ } : void 0;
1148
+ const selectionWidth = 48;
1149
+ const actionsWidth = 120;
1150
+ const leadLeft = (expandActive ? EXPAND_WIDTH : 0) + (selection ? selectionWidth : 0);
1151
+ const leadRight = showActions ? actionsWidth : 0;
1152
+ const leads = {
1153
+ left: leadLeft,
1154
+ right: leadRight
1155
+ };
1156
+ const selectionLead = expandActive ? EXPAND_WIDTH : 0;
1157
+ const hasLeftPin = table.columns.some((c) => pinOffset?.(c.key)?.side === "left");
1158
+ const stickActions = table.columns.some((c) => pinOffset?.(c.key)?.side === "right") || actionsPinned;
1159
+ const headCellSx = (column) => {
1160
+ const pin = pinnedCellStyle(pinOffset?.(column.key), PIN_Z.headerPinned, leads);
1161
+ const width = pin ? pinnedColumnWidth(column, columnWidths) : columnWidths?.[column.key] ?? column.width;
1162
+ const needsRelative = Boolean(setWidth) && !headSx && !pin;
1163
+ return {
1164
+ ...headSx,
1165
+ ...pin && {
1166
+ ...pin,
1167
+ bgcolor: "background.paper"
1168
+ },
1169
+ ...needsRelative && { position: "relative" },
1170
+ textAlign: muiAlign(column.align),
1171
+ ...width != null && { width }
1172
+ };
1173
+ };
1174
+ const edgeHeadSx = (side, active, lead = 0) => {
1175
+ const pin = edgePinStyle(side, active, PIN_Z.headerPinned);
1176
+ return {
1177
+ ...headSx,
1178
+ ...pin && {
1179
+ ...pin,
1180
+ bgcolor: "background.paper"
1181
+ },
1182
+ ...pin && lead > 0 && { insetInlineStart: lead }
1183
+ };
1184
+ };
1185
+ const rowSx = useMemo(() => {
1186
+ const edge = (side, active, lead = 0) => {
1187
+ const pin = edgePinStyle(side, active, PIN_Z.body);
1188
+ if (!pin) return void 0;
1189
+ return {
1190
+ ...pin,
1191
+ ...lead > 0 && { insetInlineStart: lead },
1192
+ bgcolor: "background.paper"
1193
+ };
1194
+ };
1195
+ const cells = {};
1196
+ for (const column of columns) {
1197
+ const pin = pinnedCellStyle(pinOffset?.(column.key), PIN_Z.body, {
1198
+ left: leadLeft,
1199
+ right: leadRight
1200
+ });
1201
+ cells[column.key] = {
1202
+ ...pin && {
1203
+ ...pin,
1204
+ bgcolor: "background.paper"
1205
+ },
1206
+ textAlign: muiAlign(column.align)
1207
+ };
1208
+ }
1209
+ return {
1210
+ cells,
1211
+ expand: edge("left", hasLeftPin),
1212
+ selection: edge("left", hasLeftPin, selectionLead),
1213
+ actions: {
1214
+ ...edge("right", stickActions),
1215
+ textAlign: "end"
1216
+ }
1217
+ };
1218
+ }, [
1219
+ columns,
1220
+ pinOffset,
1221
+ leadLeft,
1222
+ leadRight,
1223
+ hasLeftPin,
1224
+ stickActions,
1225
+ selectionLead
1226
+ ]);
1227
+ let boxSx;
1228
+ if (maxHeight != null) boxSx = {
1229
+ maxHeight,
1230
+ overflow: "auto"
1231
+ };
1232
+ else if (hasPinned || overflow.overflowing) boxSx = { overflowX: "auto" };
1233
+ const minWidth = tableMinWidth(columns, {
1234
+ widths: columnWidths,
1235
+ extra: leadLeft + leadRight
1236
+ });
1237
+ return /* @__PURE__ */ jsx(Box, {
1238
+ ref: (node) => {
1239
+ overflow.ref(node);
1240
+ virtualScrollRef?.(node);
1241
+ },
1242
+ sx: boxSx,
1243
+ children: /* @__PURE__ */ jsxs(Table, {
1244
+ size,
1245
+ "aria-label": table.getTableProps()["aria-label"],
1246
+ sx: minWidth > 0 ? { minWidth } : void 0,
1247
+ children: [
1248
+ /* @__PURE__ */ jsxs(TableHead, { children: [groupRow && /* @__PURE__ */ jsxs(TableRow, { children: [
1249
+ expandActive && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1250
+ selection && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1251
+ groupRow.map((cell) => /* @__PURE__ */ jsx(TableCell, {
1252
+ colSpan: cell.span,
1253
+ sx: {
1254
+ textAlign: "center",
1255
+ fontWeight: 600
1256
+ },
1257
+ children: cell.label
1258
+ }, cell.key)),
1259
+ showActions && /* @__PURE__ */ jsx(TableCell, {})
1260
+ ] }), /* @__PURE__ */ jsxs(TableRow, { children: [
1261
+ expandActive && /* @__PURE__ */ jsx(TableCell, {
1262
+ padding: "checkbox",
1263
+ sx: edgeHeadSx("left", hasLeftPin)
1264
+ }),
1265
+ selection && /* @__PURE__ */ jsx(TableCell, {
1266
+ padding: "checkbox",
1267
+ sx: edgeHeadSx("left", hasLeftPin, selectionLead),
1268
+ children: /* @__PURE__ */ jsx(Checkbox, {
1269
+ slotProps: { input: { "aria-label": labels.selectAll } },
1270
+ checked: selection.headerState === "all",
1271
+ indeterminate: selection.headerState === "some",
1272
+ onChange: selection.toggleAll
1273
+ })
1274
+ }),
1275
+ columns.map((column) => {
1276
+ const headerCellProps = table.getHeaderCellProps(column);
1277
+ const ariaSort = headerCellProps["aria-sort"];
1278
+ const active = ariaSort === "ascending" || ariaSort === "descending";
1279
+ const sortIndex = headerCellProps["data-sort-index"];
1280
+ return /* @__PURE__ */ jsxs(TableCell, {
1281
+ "aria-sort": ariaSort,
1282
+ "data-sort-index": sortIndex,
1283
+ sx: headCellSx(column),
1284
+ children: [column.sortable ? /* @__PURE__ */ jsxs(TableSortLabel, {
1285
+ active,
1286
+ direction: ariaSort === "descending" ? "desc" : "asc",
1287
+ onClick: table.getSortButtonProps(column).onClick,
1288
+ children: [column.header, sortIndex !== void 0 && /* @__PURE__ */ jsx(Box, {
1289
+ component: "span",
1290
+ sx: {
1291
+ fontSize: 10,
1292
+ ml: .5
1293
+ },
1294
+ children: sortIndex
1295
+ })]
1296
+ }) : column.header, setWidth && /* @__PURE__ */ jsx(Box, {
1297
+ component: "span",
1298
+ sx: RESIZE_HANDLE_SX,
1299
+ ...columnResizeHandleProps(column.key, setWidth, `${resizeLabel}: ${typeof column.header === "string" ? column.header : column.key}`)
1300
+ })]
1301
+ }, column.key);
1302
+ }),
1303
+ showActions && /* @__PURE__ */ jsx(TableCell, {
1304
+ sx: {
1305
+ ...edgeHeadSx("right", stickActions),
1306
+ textAlign: "end"
1307
+ },
1308
+ children: labels.actions
1309
+ })
1310
+ ] })] }),
1311
+ /* @__PURE__ */ jsxs(TableBody, { children: [
1312
+ paddingTop > 0 && /* @__PURE__ */ jsx(TableRow, {
1313
+ "aria-hidden": true,
1314
+ children: /* @__PURE__ */ jsx(TableCell, {
1315
+ colSpan: columnSpan,
1316
+ sx: {
1317
+ height: paddingTop,
1318
+ p: 0
1319
+ }
1320
+ })
1321
+ }),
1322
+ entries.map(({ row, index, key }) => {
1323
+ const id = getRowId(row);
1324
+ return /* @__PURE__ */ jsx(DesktopRow, {
1325
+ row,
1326
+ index,
1327
+ selected: selection?.isSelected(id) ?? false,
1328
+ expanded: isExpanded ? isExpanded(id) : false,
1329
+ columns,
1330
+ sx: rowSx,
1331
+ columnSpan,
1332
+ size,
1333
+ dir,
1334
+ className: rowClassName?.(row, index),
1335
+ hasSelection: Boolean(selection),
1336
+ hasExpansion: expandActive,
1337
+ showActions,
1338
+ selectRowLabel: labels.selectRow,
1339
+ cancelLabel: labels.cancel,
1340
+ expandLabel: labels.expandRow,
1341
+ collapseLabel: labels.collapseRow,
1342
+ id,
1343
+ rowActions,
1344
+ confirm,
1345
+ renderRowDetail,
1346
+ onToggleSelect,
1347
+ onToggleExpand,
1348
+ onRowClick,
1349
+ prefetch,
1350
+ measureElement
1351
+ }, key);
1352
+ }),
1353
+ paddingBottom > 0 && /* @__PURE__ */ jsx(TableRow, {
1354
+ "aria-hidden": true,
1355
+ children: /* @__PURE__ */ jsx(TableCell, {
1356
+ colSpan: columnSpan,
1357
+ sx: {
1358
+ height: paddingBottom,
1359
+ p: 0
1360
+ }
1361
+ })
1362
+ })
1363
+ ] }),
1364
+ summaryCells && /* @__PURE__ */ jsx(TableFooter, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
1365
+ expandActive && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1366
+ selection && /* @__PURE__ */ jsx(TableCell, { padding: "checkbox" }),
1367
+ columns.map((column) => /* @__PURE__ */ jsx(TableCell, {
1368
+ sx: { textAlign: muiAlign(column.align) },
1369
+ children: summaryCells[column.key]
1370
+ }, column.key)),
1371
+ showActions && /* @__PURE__ */ jsx(TableCell, {})
1372
+ ] }) })
1373
+ ]
1374
+ })
1375
+ });
1497
1376
  }
1498
1377
  function mobileLabel(column) {
1499
- return column.mobileLabel ?? (typeof column.header === "string" ? column.header : column.key);
1378
+ return column.mobileLabel ?? (typeof column.header === "string" ? column.header : column.key);
1500
1379
  }
1501
- function MobileCards({
1502
- table,
1503
- rows,
1504
- rowActions,
1505
- confirm,
1506
- getRowId,
1507
- size,
1508
- dir,
1509
- onRowClick,
1510
- rowClassName,
1511
- renderRowDetail,
1512
- summaryRow,
1513
- expansion,
1514
- rowEntries,
1515
- paddingTop = 0,
1516
- paddingBottom = 0,
1517
- measureElement
1518
- }) {
1519
- const { columns, selection, labels } = table;
1520
- const entries = resolveVirtualRows(rows, getRowId, rowEntries);
1521
- const compact = size === "small";
1522
- const expand = expansion && renderRowDetail ? expansion : void 0;
1523
- const summaryCells = summaryRow?.(rows);
1524
- return /* @__PURE__ */ jsxs(
1525
- Stack,
1526
- {
1527
- spacing: compact ? 1 : 1.5,
1528
- role: "list",
1529
- "aria-label": table.getTableProps()["aria-label"],
1530
- children: [
1531
- paddingTop > 0 && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, sx: { height: paddingTop } }),
1532
- entries.map(({ row, index, key }) => {
1533
- const id = getRowId(row);
1534
- return /* @__PURE__ */ jsx(
1535
- Card,
1536
- {
1537
- ref: measureElement,
1538
- "data-index": index,
1539
- variant: "outlined",
1540
- role: "listitem",
1541
- className: rowClassName?.(row, index),
1542
- ...rowClickProps(row, onRowClick),
1543
- children: /* @__PURE__ */ jsxs(
1544
- CardContent,
1545
- {
1546
- sx: compact ? { p: 1.25, "&:last-child": { pb: 1.25 } } : void 0,
1547
- children: [
1548
- selection && /* @__PURE__ */ jsx(
1549
- Checkbox,
1550
- {
1551
- slotProps: { input: { "aria-label": labels.selectRow } },
1552
- checked: selection.isSelected(id),
1553
- onChange: () => selection.toggle(id)
1554
- }
1555
- ),
1556
- expand && /* @__PURE__ */ jsx(
1557
- ExpandToggle,
1558
- {
1559
- id,
1560
- expanded: expand.isExpanded(id),
1561
- onToggle: expand.toggle,
1562
- dir,
1563
- expandLabel: labels.expandRow,
1564
- collapseLabel: labels.collapseRow
1565
- }
1566
- ),
1567
- columns.map((column) => /* @__PURE__ */ jsxs(Box, { sx: { mb: compact ? 0.5 : 1 }, children: [
1568
- /* @__PURE__ */ jsx(
1569
- Typography,
1570
- {
1571
- variant: "caption",
1572
- color: "text.secondary",
1573
- display: "block",
1574
- children: mobileLabel(column)
1575
- }
1576
- ),
1577
- /* @__PURE__ */ jsx(Typography, { component: "div", variant: "body2", children: column.Cell ? /* @__PURE__ */ jsx(column.Cell, { row, rowIndex: index }) : column.accessor?.(row) })
1578
- ] }, column.key)),
1579
- rowActions && rowActions.length > 0 && /* @__PURE__ */ jsx(
1580
- RowActionButtons,
1581
- {
1582
- row,
1583
- actions: rowActions,
1584
- confirm,
1585
- cancelLabel: labels.cancel
1586
- }
1587
- ),
1588
- expand?.isExpanded(id) && // Inside the card — and therefore inside the measured
1589
- // element — so virtualization keeps accurate card heights.
1590
- /* @__PURE__ */ jsx(Box, { sx: { mt: 1 }, children: renderRowDetail(row) })
1591
- ]
1592
- }
1593
- )
1594
- },
1595
- key
1596
- );
1597
- }),
1598
- summaryCells && /* @__PURE__ */ jsx(Card, { variant: "outlined", role: "listitem", children: /* @__PURE__ */ jsx(
1599
- CardContent,
1600
- {
1601
- sx: compact ? { p: 1.25, "&:last-child": { pb: 1.25 } } : void 0,
1602
- children: columns.map((column) => {
1603
- const value = summaryCells[column.key];
1604
- if (value === void 0) return null;
1605
- return /* @__PURE__ */ jsxs(Box, { sx: { mb: compact ? 0.5 : 1 }, children: [
1606
- /* @__PURE__ */ jsx(
1607
- Typography,
1608
- {
1609
- variant: "caption",
1610
- color: "text.secondary",
1611
- display: "block",
1612
- children: mobileLabel(column)
1613
- }
1614
- ),
1615
- /* @__PURE__ */ jsx(Typography, { component: "div", variant: "body2", children: value })
1616
- ] }, column.key);
1617
- })
1618
- }
1619
- ) }),
1620
- paddingBottom > 0 && /* @__PURE__ */ jsx(Box, { "aria-hidden": true, sx: { height: paddingBottom } })
1621
- ]
1622
- }
1623
- );
1380
+ /** Mobile MUI card list. */
1381
+ function MobileCards({ table, rows, rowActions, confirm, getRowId, size, dir, onRowClick, rowClassName, renderRowDetail, summaryRow, expansion, rowEntries, paddingTop = 0, paddingBottom = 0, measureElement }) {
1382
+ const { columns, selection, labels } = table;
1383
+ const entries = resolveVirtualRows(rows, getRowId, rowEntries);
1384
+ const compact = size === "small";
1385
+ const expand = expansion && renderRowDetail ? expansion : void 0;
1386
+ const summaryCells = summaryRow?.(rows);
1387
+ return /* @__PURE__ */ jsxs(Stack, {
1388
+ spacing: compact ? 1 : 1.5,
1389
+ role: "list",
1390
+ "aria-label": table.getTableProps()["aria-label"],
1391
+ children: [
1392
+ paddingTop > 0 && /* @__PURE__ */ jsx(Box, {
1393
+ "aria-hidden": true,
1394
+ sx: { height: paddingTop }
1395
+ }),
1396
+ entries.map(({ row, index, key }) => {
1397
+ const id = getRowId(row);
1398
+ return /* @__PURE__ */ jsx(Card, {
1399
+ ref: measureElement,
1400
+ "data-index": index,
1401
+ variant: "outlined",
1402
+ role: "listitem",
1403
+ className: rowClassName?.(row, index),
1404
+ ...rowClickProps(row, onRowClick),
1405
+ children: /* @__PURE__ */ jsxs(CardContent, {
1406
+ sx: compact ? {
1407
+ p: 1.25,
1408
+ "&:last-child": { pb: 1.25 }
1409
+ } : void 0,
1410
+ children: [
1411
+ selection && /* @__PURE__ */ jsx(Checkbox, {
1412
+ slotProps: { input: { "aria-label": labels.selectRow } },
1413
+ checked: selection.isSelected(id),
1414
+ onChange: () => selection.toggle(id)
1415
+ }),
1416
+ expand && /* @__PURE__ */ jsx(ExpandToggle, {
1417
+ id,
1418
+ expanded: expand.isExpanded(id),
1419
+ onToggle: expand.toggle,
1420
+ dir,
1421
+ expandLabel: labels.expandRow,
1422
+ collapseLabel: labels.collapseRow
1423
+ }),
1424
+ columns.map((column) => /* @__PURE__ */ jsxs(Box, {
1425
+ sx: { mb: compact ? .5 : 1 },
1426
+ children: [/* @__PURE__ */ jsx(Typography, {
1427
+ variant: "caption",
1428
+ color: "text.secondary",
1429
+ sx: { display: "block" },
1430
+ children: mobileLabel(column)
1431
+ }), /* @__PURE__ */ jsx(Typography, {
1432
+ component: "div",
1433
+ variant: "body2",
1434
+ children: column.Cell ? /* @__PURE__ */ jsx(column.Cell, {
1435
+ row,
1436
+ rowIndex: index
1437
+ }) : column.accessor?.(row)
1438
+ })]
1439
+ }, column.key)),
1440
+ rowActions && rowActions.length > 0 && /* @__PURE__ */ jsx(RowActionButtons, {
1441
+ row,
1442
+ actions: rowActions,
1443
+ confirm,
1444
+ cancelLabel: labels.cancel
1445
+ }),
1446
+ expand?.isExpanded(id) && /* @__PURE__ */ jsx(Box, {
1447
+ sx: { mt: 1 },
1448
+ children: renderRowDetail(row)
1449
+ })
1450
+ ]
1451
+ })
1452
+ }, key);
1453
+ }),
1454
+ summaryCells && /* @__PURE__ */ jsx(Card, {
1455
+ variant: "outlined",
1456
+ role: "listitem",
1457
+ children: /* @__PURE__ */ jsx(CardContent, {
1458
+ sx: compact ? {
1459
+ p: 1.25,
1460
+ "&:last-child": { pb: 1.25 }
1461
+ } : void 0,
1462
+ children: columns.map((column) => {
1463
+ const value = summaryCells[column.key];
1464
+ if (value === void 0) return null;
1465
+ return /* @__PURE__ */ jsxs(Box, {
1466
+ sx: { mb: compact ? .5 : 1 },
1467
+ children: [/* @__PURE__ */ jsx(Typography, {
1468
+ variant: "caption",
1469
+ color: "text.secondary",
1470
+ sx: { display: "block" },
1471
+ children: mobileLabel(column)
1472
+ }), /* @__PURE__ */ jsx(Typography, {
1473
+ component: "div",
1474
+ variant: "body2",
1475
+ children: value
1476
+ })]
1477
+ }, column.key);
1478
+ })
1479
+ })
1480
+ }),
1481
+ paddingBottom > 0 && /* @__PURE__ */ jsx(Box, {
1482
+ "aria-hidden": true,
1483
+ sx: { height: paddingBottom }
1484
+ })
1485
+ ]
1486
+ });
1624
1487
  }
1488
+ //#endregion
1489
+ //#region src/DataTable.tsx
1490
+ /**
1491
+ * Map row density to MUI's table `size`, independent of column pinning. An
1492
+ * explicit `size` prop still wins for backward compatibility.
1493
+ */
1625
1494
  function tableSize(size, density) {
1626
- if (size) return size;
1627
- return density === "compact" ? "small" : "medium";
1495
+ if (size) return size;
1496
+ return density === "compact" ? "small" : "medium";
1628
1497
  }
1498
+ /** The width setter only when column resize is enabled (opt-in). */
1629
1499
  function resizeSetter(enabled, setWidth) {
1630
- return enabled ? setWidth : void 0;
1500
+ return enabled ? setWidth : void 0;
1631
1501
  }
1502
+ /**
1503
+ * The injected actions column is first-class in column management: the
1504
+ * layout state treats keys opaquely, so the reserved "actions" key hides and
1505
+ * end-pins like any data column. Hiding strips `rowActions` BEFORE the
1506
+ * renderers, so the column, its pin lead, and the colSpans all disappear
1507
+ * consistently (desktop and mobile alike). `actionsPinned` reports the
1508
+ * Columns-menu end pin — only meaningful while the column renders.
1509
+ */
1632
1510
  function resolveActionsColumn(declared, layout) {
1633
- const hasRowActions = (declared?.length ?? 0) > 0;
1634
- const rowActions = hasRowActions && !layout.isHidden(ACTIONS_COLUMN_KEY) ? declared : void 0;
1635
- const actionsPinned = rowActions !== void 0 && layout.state.pinned[ACTIONS_COLUMN_KEY] === "right";
1636
- return { hasRowActions, rowActions, actionsPinned };
1511
+ const hasRowActions = (declared?.length ?? 0) > 0;
1512
+ const rowActions = hasRowActions && !layout.isHidden(ACTIONS_COLUMN_KEY) ? declared : void 0;
1513
+ return {
1514
+ hasRowActions,
1515
+ rowActions,
1516
+ actionsPinned: rowActions !== void 0 && layout.state.pinned[ACTIONS_COLUMN_KEY] === "right"
1517
+ };
1637
1518
  }
1519
+ /**
1520
+ * Resolve the data tier (`source` > `data` + `onQueryChange` > `data`) and
1521
+ * the filter content, then overlay them on the caller's props: caller JSX
1522
+ * filters pass through; the declarative array becomes the auto-built
1523
+ * {@link AutoFilterForm} (or nothing, when no definitions resolved); the
1524
+ * runtime's chip-label resolvers merge under any caller overrides.
1525
+ */
1638
1526
  function useChromeProps(props) {
1639
- const { source, runtime } = useTableData({
1640
- locale: props.locale,
1641
- source: props.source,
1642
- data: props.data,
1643
- total: props.total,
1644
- loading: props.loading,
1645
- onQueryChange: props.onQueryChange,
1646
- columns: props.columns,
1647
- filters: props.filters,
1648
- adapter: props.urlSync === false ? void 0 : props.urlAdapter,
1649
- enabled: props.urlSync,
1650
- urlKey: props.urlKey
1651
- });
1652
- let filtersNode;
1653
- if (isDeclarativeFilters(props.filters) || props.filters === void 0) {
1654
- filtersNode = runtime.defs.length > 0 ? /* @__PURE__ */ jsx(
1655
- AutoFilterForm,
1656
- {
1657
- defs: runtime.defs,
1658
- source,
1659
- labels: resolveLabels(props.labels)
1660
- }
1661
- ) : void 0;
1662
- } else {
1663
- filtersNode = props.filters;
1664
- }
1665
- return {
1666
- ...props,
1667
- source,
1668
- filters: filtersNode,
1669
- filterLabels: { ...runtime.filterLabels, ...props.filterLabels }
1670
- };
1527
+ const { source, runtime } = useTableData({
1528
+ locale: props.locale,
1529
+ source: props.source,
1530
+ data: props.data,
1531
+ total: props.total,
1532
+ loading: props.loading,
1533
+ onQueryChange: props.onQueryChange,
1534
+ columns: props.columns,
1535
+ filters: props.filters,
1536
+ adapter: props.urlSync === false ? void 0 : props.urlAdapter,
1537
+ enabled: props.urlSync,
1538
+ urlKey: props.urlKey
1539
+ });
1540
+ let filtersNode;
1541
+ if (isDeclarativeFilters(props.filters) || props.filters === void 0) filtersNode = runtime.defs.length > 0 ? /* @__PURE__ */ jsx(AutoFilterForm, {
1542
+ defs: runtime.defs,
1543
+ source,
1544
+ labels: resolveLabels(props.labels)
1545
+ }) : void 0;
1546
+ else filtersNode = props.filters;
1547
+ return {
1548
+ ...props,
1549
+ source,
1550
+ filters: filtersNode,
1551
+ filterLabels: {
1552
+ ...runtime.filterLabels,
1553
+ ...props.filterLabels
1554
+ }
1555
+ };
1671
1556
  }
1557
+ /**
1558
+ * Batteries-included Material UI data table. Drop in `columns`, `data` (or
1559
+ * `data` + `onQueryChange` for server fetching, or a full `source`), and a
1560
+ * `rowKey` for a fully styled, sortable, filterable, paginated MUI table
1561
+ * with selection, bulk actions, RTL, and dark mode — a free DataGrid-style
1562
+ * experience on the headless `@adapttable/core` engine. Declarative
1563
+ * `filters` (and column `filter` shorthands) render an auto-built form.
1564
+ *
1565
+ * @typeParam TRow - The row type.
1566
+ */
1672
1567
  function DataTable(props) {
1673
- const { slots, className } = props;
1674
- const size = tableSize(props.size, props.density);
1675
- const { filtersMode = "popover" } = props;
1676
- const chromeProps = useChromeProps(props);
1677
- const { source, filters: filtersNode } = chromeProps;
1678
- const c = useTableChrome(chromeProps);
1679
- const { table, confirm, getRowId } = c;
1680
- const { labels } = table;
1681
- const [filtersOpen, setFiltersOpen] = useState(false);
1682
- const filtersTrigger = useFilterTriggerToggle(filtersOpen, setFiltersOpen);
1683
- const rootRef = useRef(null);
1684
- useChromeScrollReset(rootRef, c, chromeProps);
1685
- const { virtualization, loadMoreRef, canLoadMore, virtualScrollRef } = useChromeBodyData(c, chromeProps);
1686
- const { hasRowActions, rowActions, actionsPinned } = resolveActionsColumn(
1687
- props.rowActions,
1688
- c.columnLayout
1689
- );
1690
- const columnMenu = props.enableColumnMenu && !c.isMobile && /* @__PURE__ */ jsx(
1691
- ColumnMenu,
1692
- {
1693
- allColumns: c.allColumns,
1694
- layout: c.columnLayout,
1695
- labels,
1696
- hasRowActions
1697
- }
1698
- );
1699
- const savedViewsMenu = props.savedViews && /* @__PURE__ */ jsx(
1700
- SavedViewsMenu,
1701
- {
1702
- options: {
1703
- adapter: props.urlAdapter,
1704
- urlKey: props.urlKey,
1705
- ...props.savedViews
1706
- },
1707
- labels
1708
- }
1709
- );
1710
- let body;
1711
- if (c.body === "skeleton") {
1712
- body = slots?.skeleton ?? /* @__PURE__ */ jsx(
1713
- LoadingState,
1714
- {
1715
- rows: props.skeletonRows ?? source.limit,
1716
- columns: table.columns.length,
1717
- loadingLabel: labels.loading
1718
- }
1719
- );
1720
- } else if (c.body === "empty") {
1721
- body = slots?.empty ?? /* @__PURE__ */ jsxs(Stack, { role: "status", spacing: 1.5, alignItems: "center", sx: { py: 6 }, children: [
1722
- /* @__PURE__ */ jsx(Typography, { color: "text.secondary", align: "center", children: c.emptyVariant === "noResults" ? labels.noResults : labels.noData }),
1723
- c.emptyVariant === "noResults" && /* @__PURE__ */ jsx(Button, { variant: "outlined", size: "small", onClick: c.clearFilters, children: labels.clearAll })
1724
- ] });
1725
- } else if (c.body === "mobile") {
1726
- body = /* @__PURE__ */ jsx(
1727
- MobileCards,
1728
- {
1729
- table,
1730
- rows: source.rows,
1731
- rowActions,
1732
- confirm,
1733
- getRowId,
1734
- size,
1735
- dir: props.dir,
1736
- onRowClick: props.onRowClick,
1737
- rowClassName: props.rowClassName,
1738
- renderRowDetail: props.renderRowDetail,
1739
- summaryRow: props.summaryRow,
1740
- expansion: c.detail?.expansion,
1741
- rowEntries: virtualization.enabled ? virtualization.rows : void 0,
1742
- paddingTop: virtualization.paddingTop,
1743
- paddingBottom: virtualization.paddingBottom,
1744
- measureElement: virtualization.measureElement
1745
- }
1746
- );
1747
- } else {
1748
- body = /* @__PURE__ */ jsx(
1749
- DesktopTable,
1750
- {
1751
- table,
1752
- rows: source.rows,
1753
- rowActions,
1754
- actionsPinned,
1755
- confirm,
1756
- getRowId,
1757
- size,
1758
- dir: props.dir,
1759
- prefetch: props.prefetch,
1760
- onRowClick: props.onRowClick,
1761
- rowClassName: props.rowClassName,
1762
- renderRowDetail: props.renderRowDetail,
1763
- summaryRow: props.summaryRow,
1764
- expansion: c.detail?.expansion,
1765
- rowEntries: virtualization.enabled ? virtualization.rows : void 0,
1766
- paddingTop: virtualization.paddingTop,
1767
- paddingBottom: virtualization.paddingBottom,
1768
- measureElement: virtualization.measureElement,
1769
- stickyHeader: props.stickyHeader,
1770
- stickyTop: props.stickyTop,
1771
- pinOffset: c.columnLayout.pinOffset,
1772
- maxHeight: props.maxHeight,
1773
- virtualScrollRef,
1774
- setWidth: resizeSetter(props.resizableColumns, c.columnLayout.setWidth),
1775
- columnWidths: c.columnLayout.state.widths,
1776
- resizeLabel: labels.resizeColumn
1777
- }
1778
- );
1779
- }
1780
- return /* @__PURE__ */ jsxs(
1781
- Paper,
1782
- {
1783
- ref: rootRef,
1784
- variant: "outlined",
1785
- dir: props.dir,
1786
- className,
1787
- "aria-busy": c.isRefreshing || void 0,
1788
- sx: { p: 1.5 },
1789
- children: [
1790
- /* @__PURE__ */ jsxs(Stack, { spacing: 1.5, children: [
1791
- /* @__PURE__ */ jsx(
1792
- Toolbar,
1793
- {
1794
- table,
1795
- hideSearch: props.hideSearch,
1796
- searchPlaceholder: props.searchPlaceholder,
1797
- sortByOptions: props.sortByOptions,
1798
- customToolbar: /* @__PURE__ */ jsxs(Fragment, { children: [
1799
- savedViewsMenu,
1800
- props.toolbar
1801
- ] }),
1802
- hasFilters: Boolean(filtersNode),
1803
- activeFilterCount: c.activeFilterCount,
1804
- showRowsPerPage: canLoadMore,
1805
- filtersMode,
1806
- filters: filtersNode,
1807
- filtersOpen,
1808
- onToggleFilters: filtersTrigger.onClick,
1809
- onFiltersTriggerPointerDown: filtersTrigger.onPointerDown,
1810
- onCloseFilters: () => setFiltersOpen(false),
1811
- onClearFilters: c.clearFilters,
1812
- dir: props.dir,
1813
- columnMenu
1814
- }
1815
- ),
1816
- c.isRefreshing && /* @__PURE__ */ jsx(LinearProgress, { "aria-label": labels.loading }),
1817
- /* @__PURE__ */ jsx(
1818
- Chips,
1819
- {
1820
- chips: c.mergedChips,
1821
- onClearAll: c.clearFilters,
1822
- labels
1823
- }
1824
- ),
1825
- table.selection && props.bulkActions && /* @__PURE__ */ jsx(
1826
- BulkBar,
1827
- {
1828
- selection: table.selection,
1829
- total: source.total,
1830
- bulkActions: props.bulkActions,
1831
- confirm,
1832
- labels
1833
- }
1834
- ),
1835
- source.error ? /* @__PURE__ */ jsx(
1836
- ErrorState,
1837
- {
1838
- error: source.error,
1839
- labels,
1840
- onRetry: source.refetch ? () => void source.refetch?.() : void 0
1841
- }
1842
- ) : body,
1843
- canLoadMore && source.hasNextPage && /* @__PURE__ */ jsx(Box, { ref: loadMoreRef, display: "flex", justifyContent: "center", py: 1, children: /* @__PURE__ */ jsx(
1844
- Button,
1845
- {
1846
- variant: "outlined",
1847
- size: "small",
1848
- disabled: source.isFetchingNextPage,
1849
- onClick: () => source.fetchNextPage(),
1850
- children: labels.loadMore
1851
- }
1852
- ) }),
1853
- c.showFooter && /* @__PURE__ */ jsx(
1854
- Footer,
1855
- {
1856
- pagination: table.pagination,
1857
- total: source.total,
1858
- limit: source.limit,
1859
- setPage: source.setPage,
1860
- setLimit: source.setLimit,
1861
- labels
1862
- }
1863
- )
1864
- ] }),
1865
- filtersNode && filtersMode === "drawer" && /* @__PURE__ */ jsx(
1866
- FilterDrawer,
1867
- {
1868
- open: filtersOpen,
1869
- onClose: () => setFiltersOpen(false),
1870
- filters: filtersNode,
1871
- activeFilterCount: c.activeFilterCount,
1872
- onClearFilters: c.clearFilters,
1873
- labels,
1874
- dir: props.dir
1875
- }
1876
- )
1877
- ]
1878
- }
1879
- );
1568
+ const { slots, className } = props;
1569
+ const size = tableSize(props.size, props.density);
1570
+ const { filtersMode = "popover" } = props;
1571
+ const chromeProps = useChromeProps(props);
1572
+ const { source, filters: filtersNode } = chromeProps;
1573
+ const c = useTableChrome(chromeProps);
1574
+ const { table, confirm, getRowId } = c;
1575
+ const { labels } = table;
1576
+ const [filtersOpen, setFiltersOpen] = useState(false);
1577
+ const filtersTrigger = useFilterTriggerToggle(filtersOpen, setFiltersOpen);
1578
+ const rootRef = useRef(null);
1579
+ useChromeScrollReset(rootRef, c, chromeProps);
1580
+ const { virtualization, loadMoreRef, canLoadMore, virtualScrollRef } = useChromeBodyData(c, chromeProps);
1581
+ const { hasRowActions, rowActions, actionsPinned } = resolveActionsColumn(props.rowActions, c.columnLayout);
1582
+ const columnMenu = props.enableColumnMenu && !c.isMobile && /* @__PURE__ */ jsx(ColumnMenu, {
1583
+ allColumns: c.allColumns,
1584
+ layout: c.columnLayout,
1585
+ labels,
1586
+ hasRowActions
1587
+ });
1588
+ const savedViewsMenu = props.savedViews && /* @__PURE__ */ jsx(SavedViewsMenu, {
1589
+ options: {
1590
+ adapter: props.urlAdapter,
1591
+ urlKey: props.urlKey,
1592
+ ...props.savedViews
1593
+ },
1594
+ labels
1595
+ });
1596
+ let body;
1597
+ if (c.body === "skeleton") body = slots?.skeleton ?? /* @__PURE__ */ jsx(LoadingState, {
1598
+ rows: props.skeletonRows ?? source.limit,
1599
+ columns: table.columns.length,
1600
+ loadingLabel: labels.loading
1601
+ });
1602
+ else if (c.body === "empty") body = slots?.empty ?? /* @__PURE__ */ jsxs(Stack, {
1603
+ role: "status",
1604
+ spacing: 1.5,
1605
+ sx: {
1606
+ py: 6,
1607
+ alignItems: "center"
1608
+ },
1609
+ children: [/* @__PURE__ */ jsx(Typography, {
1610
+ color: "text.secondary",
1611
+ align: "center",
1612
+ children: c.emptyVariant === "noResults" ? labels.noResults : labels.noData
1613
+ }), c.emptyVariant === "noResults" && /* @__PURE__ */ jsx(Button, {
1614
+ variant: "outlined",
1615
+ size: "small",
1616
+ onClick: c.clearFilters,
1617
+ children: labels.clearAll
1618
+ })]
1619
+ });
1620
+ else if (c.body === "mobile") body = /* @__PURE__ */ jsx(MobileCards, {
1621
+ table,
1622
+ rows: source.rows,
1623
+ rowActions,
1624
+ confirm,
1625
+ getRowId,
1626
+ size,
1627
+ dir: props.dir,
1628
+ onRowClick: props.onRowClick,
1629
+ rowClassName: props.rowClassName,
1630
+ renderRowDetail: props.renderRowDetail,
1631
+ summaryRow: props.summaryRow,
1632
+ expansion: c.detail?.expansion,
1633
+ rowEntries: virtualization.enabled ? virtualization.rows : void 0,
1634
+ paddingTop: virtualization.paddingTop,
1635
+ paddingBottom: virtualization.paddingBottom,
1636
+ measureElement: virtualization.measureElement
1637
+ });
1638
+ else body = /* @__PURE__ */ jsx(DesktopTable, {
1639
+ table,
1640
+ rows: source.rows,
1641
+ rowActions,
1642
+ actionsPinned,
1643
+ confirm,
1644
+ getRowId,
1645
+ size,
1646
+ dir: props.dir,
1647
+ prefetch: props.prefetch,
1648
+ onRowClick: props.onRowClick,
1649
+ rowClassName: props.rowClassName,
1650
+ renderRowDetail: props.renderRowDetail,
1651
+ summaryRow: props.summaryRow,
1652
+ expansion: c.detail?.expansion,
1653
+ rowEntries: virtualization.enabled ? virtualization.rows : void 0,
1654
+ paddingTop: virtualization.paddingTop,
1655
+ paddingBottom: virtualization.paddingBottom,
1656
+ measureElement: virtualization.measureElement,
1657
+ stickyHeader: props.stickyHeader,
1658
+ stickyTop: props.stickyTop,
1659
+ pinOffset: c.columnLayout.pinOffset,
1660
+ maxHeight: props.maxHeight,
1661
+ virtualScrollRef,
1662
+ setWidth: resizeSetter(props.resizableColumns, c.columnLayout.setWidth),
1663
+ columnWidths: c.columnLayout.state.widths,
1664
+ resizeLabel: labels.resizeColumn
1665
+ });
1666
+ return /* @__PURE__ */ jsxs(Paper, {
1667
+ ref: rootRef,
1668
+ variant: "outlined",
1669
+ dir: props.dir,
1670
+ className,
1671
+ "aria-busy": c.isRefreshing || void 0,
1672
+ sx: { p: 1.5 },
1673
+ children: [/* @__PURE__ */ jsxs(Stack, {
1674
+ spacing: 1.5,
1675
+ children: [
1676
+ /* @__PURE__ */ jsx(Toolbar, {
1677
+ table,
1678
+ hideSearch: props.hideSearch,
1679
+ searchPlaceholder: props.searchPlaceholder,
1680
+ sortByOptions: props.sortByOptions,
1681
+ customToolbar: /* @__PURE__ */ jsxs(Fragment, { children: [savedViewsMenu, props.toolbar] }),
1682
+ hasFilters: Boolean(filtersNode),
1683
+ activeFilterCount: c.activeFilterCount,
1684
+ showRowsPerPage: canLoadMore,
1685
+ filtersMode,
1686
+ filters: filtersNode,
1687
+ filtersOpen,
1688
+ onToggleFilters: filtersTrigger.onClick,
1689
+ onFiltersTriggerPointerDown: filtersTrigger.onPointerDown,
1690
+ onCloseFilters: () => setFiltersOpen(false),
1691
+ onClearFilters: c.clearFilters,
1692
+ dir: props.dir,
1693
+ columnMenu
1694
+ }),
1695
+ c.isRefreshing && /* @__PURE__ */ jsx(LinearProgress, { "aria-label": labels.loading }),
1696
+ /* @__PURE__ */ jsx(Chips, {
1697
+ chips: c.mergedChips,
1698
+ onClearAll: c.clearFilters,
1699
+ labels
1700
+ }),
1701
+ table.selection && props.bulkActions && /* @__PURE__ */ jsx(BulkBar, {
1702
+ selection: table.selection,
1703
+ total: source.total,
1704
+ bulkActions: props.bulkActions,
1705
+ confirm,
1706
+ labels
1707
+ }),
1708
+ source.error ? /* @__PURE__ */ jsx(ErrorState, {
1709
+ error: source.error,
1710
+ labels,
1711
+ onRetry: source.refetch ? () => void source.refetch?.() : void 0
1712
+ }) : body,
1713
+ canLoadMore && source.hasNextPage && /* @__PURE__ */ jsx(Box, {
1714
+ ref: loadMoreRef,
1715
+ sx: {
1716
+ display: "flex",
1717
+ justifyContent: "center",
1718
+ py: 1
1719
+ },
1720
+ children: /* @__PURE__ */ jsx(Button, {
1721
+ variant: "outlined",
1722
+ size: "small",
1723
+ disabled: source.isFetchingNextPage,
1724
+ onClick: () => source.fetchNextPage(),
1725
+ children: labels.loadMore
1726
+ })
1727
+ }),
1728
+ c.showFooter && /* @__PURE__ */ jsx(Footer, {
1729
+ pagination: table.pagination,
1730
+ total: source.total,
1731
+ limit: source.limit,
1732
+ setPage: source.setPage,
1733
+ setLimit: source.setLimit,
1734
+ labels
1735
+ })
1736
+ ]
1737
+ }), filtersNode && filtersMode === "drawer" && /* @__PURE__ */ jsx(FilterDrawer, {
1738
+ open: filtersOpen,
1739
+ onClose: () => setFiltersOpen(false),
1740
+ filters: filtersNode,
1741
+ activeFilterCount: c.activeFilterCount,
1742
+ onClearFilters: c.clearFilters,
1743
+ labels,
1744
+ dir: props.dir
1745
+ })]
1746
+ });
1880
1747
  }
1748
+ //#endregion
1749
+ export { DataTable, SavedViewsMenu, createHistoryAdapter, createMemoryAdapter, defaultConfirm, defaultLabels, deriveSortByOptions, getHistoryAdapter, useBackendData, useDataTable, useFrontendData, useSavedViews, useTableUrlState };
1881
1750
 
1882
- export { DataTable, SavedViewsMenu };
1883
- //# sourceMappingURL=index.js.map
1884
1751
  //# sourceMappingURL=index.js.map