@tensor_1/react 2.49.1 → 2.51.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.
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useId, useMemo, useRef, useState } from "react";
3
+ import { memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
4
4
  import { cx } from "../cx.js";
5
+ import { toggleSort } from "../table-engine.js";
5
6
  import { fillLabel } from "../fill-label.js";
6
7
  import { useUixLabels } from "../labels-context.js";
7
8
  import { renderUixLink } from "../link.js";
8
9
  import { isFilterEmpty, summarizeFilter } from "../filter-model.js";
9
10
  import { useResolvedFilterLabels } from "../hooks/useResolvedFilterLabels.js";
11
+ import { useVirtualRows } from "../hooks/useVirtualRows.js";
10
12
  import { BusyRegion } from "./BusyRegion.js";
11
13
  import { Button } from "./Button.js";
12
14
  import { Checkbox } from "./Checkbox.js";
@@ -45,25 +47,60 @@ const DEFAULT_DATA_WORKSPACE_LABELS = {
45
47
  filteredEmpty: "Nothing matches",
46
48
  filteredEmptyDetail: "Change the search or clear a filter.",
47
49
  error: "The list could not be loaded",
48
- retry: "Try again"
50
+ retry: "Try again",
51
+ sortPriority: "Sort priority {n}",
52
+ groupCount: "{count} rows",
53
+ groupCountOne: "{count} row",
54
+ collapseGroup: "Collapse group {name}",
55
+ expandGroup: "Expand group {name}",
56
+ selectGroup: "Select group {name}"
49
57
  };
58
+ const useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect;
50
59
  const ArrowUpRight = () => /* @__PURE__ */ jsx("svg", { className: "uix-id-cell__arrow", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M7 17 17 7M8 7h9v9" }) });
51
60
  const PlusIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) });
52
61
  const MaximizeIcon = ({ restore }) => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: restore ? "M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7" : "M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" }) });
62
+ const ChevronIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.5, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M6 4l4 4-4 4" }) });
63
+ const LADDER_CARDS = 768;
64
+ const LADDER_LOW = 900;
65
+ const LADDER_MID = 600;
66
+ const ladderFor = (mobile, width) => {
67
+ if (!mobile || !(width > 0)) return null;
68
+ if (mobile === "cards" && width < LADDER_CARDS) return "cards";
69
+ if (width < LADDER_MID) return "priority-mid";
70
+ if (width < LADDER_LOW) return "priority";
71
+ return null;
72
+ };
73
+ const hiddenBy = (ladder, priority) => ladder === "priority" && priority === "low" || ladder === "priority-mid" && (priority === "low" || priority === "mid");
74
+ const ROW_HEIGHT = { compact: 44, standard: 56, comfortable: 68 };
75
+ const CONTROLS = 'a, button, input, select, textarea, label, summary, dialog, [role="button"], [role="menuitem"], [contenteditable="true"], [popover], td.uix-table__selcell, td.uix-table__actions';
76
+ const RESERVED_COLS = /* @__PURE__ */ new Set(["select", "id", "actions", "expand"]);
77
+ const textOf = (node) => typeof node === "string" ? node : void 0;
78
+ const columnLabel = (column) => column.label ?? textOf(column.header);
79
+ const ladderAttrs = (on, column, body) => on ? {
80
+ "data-col": RESERVED_COLS.has(column.id) ? void 0 : column.id,
81
+ "data-priority": column.priority && column.priority !== "high" ? column.priority : void 0,
82
+ "data-label": body ? columnLabel(column) : void 0
83
+ } : {};
53
84
  function DataWorkspace({
54
85
  label,
55
86
  rows,
56
87
  getRowId,
88
+ rowLabel,
57
89
  columns,
58
90
  idColumn,
59
91
  sort,
60
92
  onSortChange,
61
93
  selection,
62
94
  onSelectionChange,
95
+ maxSelection,
63
96
  bulkActions,
64
97
  rowActions,
65
98
  getRowProps,
66
99
  density,
100
+ zebra,
101
+ freezeFirstColumn = false,
102
+ fixed,
103
+ mobile,
67
104
  search,
68
105
  filters,
69
106
  savedViews,
@@ -71,13 +108,32 @@ function DataWorkspace({
71
108
  viewChanged = false,
72
109
  onSaveView,
73
110
  onSaveViewAs,
111
+ saveViewAs = "changed",
74
112
  maximized = false,
75
113
  onMaximizedChange,
76
114
  toolbarEnd,
115
+ beforeBody,
77
116
  pagination,
78
117
  status = "ready",
79
118
  refreshing = false,
80
119
  onRetry,
120
+ loadingRows = 6,
121
+ empty,
122
+ emptyAction,
123
+ filteredEmpty,
124
+ filteredEmptyAction,
125
+ error,
126
+ stateOverride,
127
+ groups,
128
+ collapsedGroups,
129
+ onCollapsedGroupsChange,
130
+ virtualize,
131
+ rowNavigation,
132
+ onRowActivate,
133
+ activateOnClick = true,
134
+ activeRowId,
135
+ onActiveRowChange,
136
+ multiSort = false,
81
137
  labels: labelOverrides,
82
138
  className,
83
139
  ...props
@@ -111,68 +167,128 @@ function DataWorkspace({
111
167
  const root = rootRef.current;
112
168
  const target = event.target;
113
169
  const owner = target?.closest?.('[popover], [role="dialog"], [role="menu"], [role="listbox"], [aria-expanded="true"]');
114
- if (owner && root?.contains(owner)) return;
170
+ if (owner && root?.contains(owner) && (owner.getAttribute("aria-expanded") !== "true" || stillExpanded(owner))) return;
115
171
  if (root && openInside(root)) return;
116
172
  onMaximizedChange(false);
117
173
  };
118
174
  window.addEventListener("keydown", onKey);
119
175
  return () => window.removeEventListener("keydown", onKey);
120
176
  }, [maximized, onMaximizedChange]);
177
+ const [ladder, setLadder] = useState(null);
178
+ useIsomorphicLayoutEffect(() => {
179
+ const root = rootRef.current;
180
+ if (!mobile || !root) {
181
+ setLadder(null);
182
+ return;
183
+ }
184
+ const read = () => {
185
+ const style = getComputedStyle(root);
186
+ const padding = (parseFloat(style.paddingLeft) || 0) + (parseFloat(style.paddingRight) || 0);
187
+ setLadder(ladderFor(mobile, root.getBoundingClientRect().width - padding));
188
+ };
189
+ read();
190
+ if (typeof ResizeObserver === "undefined") return;
191
+ const observer = new ResizeObserver(read);
192
+ observer.observe(root);
193
+ return () => observer.disconnect();
194
+ }, [mobile]);
195
+ const cards = ladder === "cards";
121
196
  const filtered = (filters ? filters.fields.some((field) => !isFilterEmpty(filters.values[field.id])) : false) || (search?.value.trim() ?? "") !== "";
122
- const [settled, setSettled] = useState({ rows, filtered });
123
- if (!refreshing && rows !== settled.rows) setSettled({ rows, filtered });
124
- const view = rows.length > 0 ? { rows, filtered } : refreshing || rows === settled.rows ? settled : { rows, filtered };
125
- const shownRows = view.rows;
197
+ const source = groups ?? rows;
198
+ const flat = useMemo(() => groups ? groups.flatMap((group) => group.rows) : rows, [groups, rows]);
199
+ const current = { source, flat, groups, filtered };
200
+ const [settled, setSettled] = useState(current);
201
+ if (!refreshing && source !== settled.source) setSettled(current);
202
+ const view = flat.length > 0 ? current : refreshing || source === settled.source ? settled : current;
203
+ const shownRows = view.flat;
204
+ const shownGroups = view.groups;
205
+ const [ownCollapsed, setOwnCollapsed] = useState(() => /* @__PURE__ */ new Set());
206
+ const collapsed = collapsedGroups ?? ownCollapsed;
207
+ const toggleGroupOpen = (groupId) => {
208
+ const next = new Set(collapsed);
209
+ if (next.has(groupId)) next.delete(groupId);
210
+ else next.add(groupId);
211
+ if (!collapsedGroups) setOwnCollapsed(next);
212
+ onCollapsedGroupsChange?.(next);
213
+ };
126
214
  const ids = useMemo(() => shownRows.map(getRowId), [shownRows, getRowId]);
127
- const selected = selection ?? /* @__PURE__ */ new Set();
215
+ const visibleIds = useMemo(() => {
216
+ if (!shownGroups) return ids;
217
+ const out = [];
218
+ for (const group of shownGroups) if (!collapsed.has(group.id)) for (const row of group.rows) out.push(getRowId(row));
219
+ return out;
220
+ }, [shownGroups, collapsed, ids, getRowId]);
221
+ const selected = selection ?? EMPTY_SET;
128
222
  const selectable = onSelectionChange != null;
223
+ const cap = maxSelection ?? Infinity;
129
224
  const onPage = ids.filter((rowId) => selected.has(rowId)).length;
225
+ const apply = (next, rowIds, on) => {
226
+ for (const rowId of rowIds) {
227
+ if (!on) next.delete(rowId);
228
+ else if (next.has(rowId) || next.size < cap) next.add(rowId);
229
+ }
230
+ return next;
231
+ };
130
232
  const toggleRow = (rowId, on, event) => {
131
233
  if (!onSelectionChange) return;
132
234
  const next = new Set(selected);
133
- const from = anchor.current ? ids.indexOf(anchor.current) : -1;
134
- const to = ids.indexOf(rowId);
235
+ const from = anchor.current ? visibleIds.indexOf(anchor.current) : -1;
236
+ const to = visibleIds.indexOf(rowId);
135
237
  if (event?.shiftKey && from >= 0 && to >= 0) {
136
- for (let i = Math.min(from, to); i <= Math.max(from, to); i += 1) {
137
- if (on) next.add(ids[i]);
138
- else next.delete(ids[i]);
139
- }
238
+ apply(next, visibleIds.slice(Math.min(from, to), Math.max(from, to) + 1), on);
140
239
  } else {
141
- if (on) next.add(rowId);
142
- else next.delete(rowId);
240
+ apply(next, [rowId], on);
143
241
  anchor.current = rowId;
144
242
  }
145
243
  onSelectionChange(next);
146
244
  };
147
- const toggleAll = (on) => {
245
+ const toggleMany = (rowIds, on) => {
148
246
  if (!onSelectionChange) return;
149
- const next = new Set(selected);
150
- for (const rowId of ids) {
151
- if (on) next.add(rowId);
152
- else next.delete(rowId);
153
- }
154
- onSelectionChange(next);
247
+ onSelectionChange(apply(new Set(selected), rowIds, on));
155
248
  };
156
- const actionsByRow = useMemo(() => rowActions ? shownRows.map((row) => rowActions(row)) : [], [shownRows, rowActions]);
249
+ const atCap = selected.size >= cap;
250
+ const actionsByRow = useMemo(() => {
251
+ if (!rowActions) return [];
252
+ const context = { ladder, compact: ladder === "priority-mid" };
253
+ return shownRows.map((row) => rowActions(row, context));
254
+ }, [shownRows, rowActions, ladder]);
157
255
  const hasActions = actionsByRow.some((node) => node != null && node !== false);
158
256
  const activeFilters = filters ? filters.fields.filter((field) => !isFilterEmpty(filters.values[field.id])) : [];
159
257
  const searching = (search?.value.trim() ?? "") !== "";
160
258
  const clearAll = () => {
259
+ if (filters?.onClearAll) {
260
+ filters.onClearAll({ search: searching });
261
+ return;
262
+ }
161
263
  for (const field of activeFilters) filters?.onChange(field.id, void 0);
162
264
  if (searching) search?.onChange("");
163
265
  };
164
- const sortFor = (column) => sort?.columnId === column.id ? sort.direction : "none";
165
- const cycleSort = (column) => {
166
- const current = sortFor(column);
167
- onSortChange?.(current === "none" ? { columnId: column.id, direction: "ascending" } : current === "ascending" ? { columnId: column.id, direction: "descending" } : null);
266
+ const multi = multiSort || Array.isArray(sort);
267
+ const keys = Array.isArray(sort) ? sort : sort ? [sort] : [];
268
+ const sortFor = (column) => keys.find((key) => key.columnId === column.id)?.direction ?? "none";
269
+ const cycleSort = (column, additive) => {
270
+ if (!onSortChange) return;
271
+ const current2 = sortFor(column);
272
+ const turned = current2 === "none" ? "ascending" : current2 === "ascending" ? "descending" : null;
273
+ if (!multi) {
274
+ onSortChange(turned ? { columnId: column.id, direction: turned } : null);
275
+ return;
276
+ }
277
+ const next = toggleSort(keys.map((key) => ({ field: key.columnId, dir: key.direction })), column.id, additive).map((key) => ({ columnId: key.field, direction: key.dir === "descending" ? "descending" : "ascending" }));
278
+ onSortChange(next);
168
279
  };
169
- const toolbar = /* @__PURE__ */ jsxs("div", { className: "uix-toolbar uix-data-workspace__toolbar", role: "toolbar", "aria-label": label, children: [
170
- savedViews && /* @__PURE__ */ jsx(PopoverButton, { id: `${id}-views`, label: savedViews.current, title: typeof savedViews.current === "string" ? fillLabel(labels.savedView, { name: savedViews.current }) : void 0, variant: "secondary", children: (close) => /* @__PURE__ */ jsx(SavedViewMenu, { ...savedViews.menu, onSelect: (viewId) => {
171
- savedViews.menu.onSelect(viewId);
172
- close();
173
- } }) }),
280
+ const showSaveAs = onSaveViewAs != null && (viewChanged || saveViewAs === "always");
281
+ const hasToolbar = savedViews != null || viewChanged && onSaveView != null || showSaveAs || filters != null || search != null || viewMenu != null || onMaximizedChange != null || toolbarEnd != null && toolbarEnd !== false;
282
+ const toolbar = hasToolbar && /* @__PURE__ */ jsxs("div", { className: "uix-toolbar uix-data-workspace__toolbar", role: "toolbar", "aria-label": label, children: [
283
+ savedViews && /* @__PURE__ */ jsx(PopoverButton, { id: `${id}-views`, label: savedViews.current, title: typeof savedViews.current === "string" ? fillLabel(labels.savedView, { name: savedViews.current }) : void 0, variant: "secondary", children: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
284
+ savedViews.note != null && /* @__PURE__ */ jsx("div", { className: "uix-data-workspace__views-note", children: savedViews.note }),
285
+ /* @__PURE__ */ jsx(SavedViewMenu, { ...savedViews.menu, onSelect: (viewId) => {
286
+ savedViews.menu.onSelect(viewId);
287
+ close();
288
+ } })
289
+ ] }) }),
174
290
  viewChanged && onSaveView && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", onClick: onSaveView, children: labels.saveView }),
175
- viewChanged && onSaveViewAs && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: onSaveViewAs, children: labels.saveViewAs }),
291
+ showSaveAs && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", onClick: onSaveViewAs, children: labels.saveViewAs }),
176
292
  filters && /* @__PURE__ */ jsx(FilterChips, { filters, labels, baseId: id }),
177
293
  /* @__PURE__ */ jsx("span", { className: "uix-toolbar__spacer" }),
178
294
  search && /* @__PURE__ */ jsx(
@@ -184,67 +300,78 @@ function DataWorkspace({
184
300
  placeholder: search.placeholder ?? labels.searchPlaceholder,
185
301
  value: search.value,
186
302
  onValueChange: search.onChange,
187
- inputRef: searchRef
303
+ inputRef: searchRef,
304
+ onSubmit: search.onSubmit,
305
+ onClear: search.onClear,
306
+ maxLength: search.maxLength
188
307
  }
189
308
  ),
190
309
  viewMenu && /* @__PURE__ */ jsx(PopoverButton, { id: `${id}-view`, label: labels.view, variant: "ghost", children: () => /* @__PURE__ */ jsx(ViewMenu, { ...viewMenu }) }),
191
310
  onMaximizedChange && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", icon: true, "aria-label": maximized ? labels.restore : labels.maximize, "aria-pressed": maximized, onClick: () => onMaximizedChange(!maximized), children: /* @__PURE__ */ jsx(MaximizeIcon, { restore: maximized }) }),
192
311
  toolbarEnd
193
312
  ] });
313
+ const busy = (node) => /* @__PURE__ */ jsx(BusyRegion, { busy: refreshing, className: "uix-data-workspace__state", children: node });
194
314
  let body;
195
- if (status === "loading") body = /* @__PURE__ */ jsx(LoadingState, { rows: 6, label: labels.loading, className: "uix-data-workspace__loading" });
196
- else if (status === "error") body = /* @__PURE__ */ jsx(ErrorState, { title: labels.error, action: onRetry && /* @__PURE__ */ jsx(Button, { size: "sm", onClick: onRetry, children: labels.retry }) });
315
+ if (stateOverride != null) body = busy(stateOverride);
316
+ else if (status === "loading") body = /* @__PURE__ */ jsx(LoadingState, { rows: loadingRows, label: labels.loading, className: "uix-data-workspace__loading" });
317
+ else if (status === "error") body = error != null ? busy(error) : /* @__PURE__ */ jsx(ErrorState, { title: labels.error, action: onRetry && /* @__PURE__ */ jsx(Button, { size: "sm", onClick: onRetry, children: labels.retry }) });
197
318
  else if (shownRows.length === 0) {
198
- body = /* @__PURE__ */ jsx(BusyRegion, { busy: refreshing, className: "uix-data-workspace__state", children: view.filtered ? /* @__PURE__ */ jsx(FilteredEmptyState, { title: labels.filteredEmpty, action: /* @__PURE__ */ jsx(Button, { size: "sm", onClick: clearAll, children: labels.clearFilters }), children: labels.filteredEmptyDetail }) : /* @__PURE__ */ jsx(EmptyState, { title: labels.empty, children: labels.emptyDetail }) });
319
+ const clear = /* @__PURE__ */ jsx(Button, { size: "sm", onClick: clearAll, children: labels.clearFilters });
320
+ body = busy(view.filtered ? filteredEmpty ?? /* @__PURE__ */ jsx(
321
+ FilteredEmptyState,
322
+ {
323
+ title: labels.filteredEmpty,
324
+ action: filteredEmptyAction != null ? /* @__PURE__ */ jsxs("div", { className: "uix-data-workspace__state-actions", children: [
325
+ clear,
326
+ filteredEmptyAction
327
+ ] }) : clear,
328
+ children: labels.filteredEmptyDetail
329
+ }
330
+ ) : empty ?? /* @__PURE__ */ jsx(EmptyState, { title: labels.empty, action: emptyAction, children: labels.emptyDetail }));
199
331
  } else {
200
- body = /* @__PURE__ */ jsx(TableWrap, { refreshing, className: "uix-data-workspace__table", children: /* @__PURE__ */ jsxs(Table, { density, pinnedCol: idColumn != null, pinnedActions: hasActions, className: cx(selectable && idColumn && "uix-data-workspace__grid--pin-id"), "aria-label": label, children: [
201
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
202
- selectable && /* @__PURE__ */ jsx("th", { className: "uix-table__selcol", scope: "col", children: /* @__PURE__ */ jsx(Checkbox, { "aria-label": labels.selectAll, checked: onPage > 0 && onPage === ids.length, indeterminate: onPage > 0 && onPage < ids.length, onChange: (event) => toggleAll(event.currentTarget.checked) }) }),
203
- idColumn && /* @__PURE__ */ jsx(Th, { scope: "col", children: idColumn.header }),
204
- columns.map((column) => /* @__PURE__ */ jsx(
205
- Th,
206
- {
207
- scope: "col",
208
- className: column.align === "end" ? "uix-col--num" : void 0,
209
- style: column.width ? { width: column.width } : void 0,
210
- sortable: column.sortable && onSortChange != null,
211
- sortDirection: sortFor(column),
212
- onSort: () => cycleSort(column),
213
- children: column.header
214
- },
215
- column.id
216
- )),
217
- hasActions && /* @__PURE__ */ jsx("th", { scope: "col", className: "uix-table__actions", children: /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: labels.rowActions }) })
218
- ] }) }),
219
- /* @__PURE__ */ jsx("tbody", { children: shownRows.map((row, index) => {
220
- const rowId = ids[index];
221
- const text = idColumn?.text(row) ?? rowId;
222
- const isSelected = selected.has(rowId);
223
- return /* @__PURE__ */ jsxs(Tr, { selected: isSelected, ...getRowProps?.(row), children: [
224
- selectable && /* @__PURE__ */ jsx("td", { className: "uix-table__selcell", children: /* @__PURE__ */ jsx(
225
- Checkbox,
226
- {
227
- "aria-label": fillLabel(labels.selectRow, { label: text }),
228
- checked: isSelected,
229
- onClick: (event) => toggleRow(rowId, event.currentTarget.checked, event),
230
- onChange: () => {
231
- }
232
- }
233
- ) }),
234
- idColumn && /* @__PURE__ */ jsx(Td, { className: "uix-id-cell", children: /* @__PURE__ */ jsxs("span", { className: "uix-data-workspace__id", children: [
235
- idColumn.onPeek ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-id-cell__btn", "aria-haspopup": "dialog", onClick: () => idColumn.onPeek?.(row), children: text }) : /* @__PURE__ */ jsx("span", { className: "uix-cell-strong", children: text }),
236
- idColumn.href && renderUixLink(idColumn.renderLink, {
237
- href: idColumn.href(row),
238
- className: "uix-data-workspace__open",
239
- "aria-label": fillLabel(labels.open, { label: text }),
240
- children: /* @__PURE__ */ jsx(ArrowUpRight, {})
241
- })
242
- ] }) }),
243
- columns.map((column) => /* @__PURE__ */ jsx(Td, { className: column.align === "end" ? "uix-col--num" : void 0, children: column.cell(row) }, column.id)),
244
- hasActions && /* @__PURE__ */ jsx(RowActions, { children: actionsByRow[index] })
245
- ] }, rowId);
246
- }) })
247
- ] }) });
332
+ body = /* @__PURE__ */ jsx(
333
+ WorkspaceGrid,
334
+ {
335
+ label,
336
+ rows: shownRows,
337
+ ids,
338
+ groups: shownGroups,
339
+ collapsed,
340
+ onToggleGroup: toggleGroupOpen,
341
+ getRowId,
342
+ rowLabel,
343
+ columns,
344
+ idColumn,
345
+ getRowProps,
346
+ density,
347
+ zebra,
348
+ freezeFirstColumn,
349
+ fixed: fixed ?? columns.some((column) => column.className != null || column.minWidth != null),
350
+ ladderOn: mobile != null,
351
+ ladder,
352
+ refreshing,
353
+ selectable,
354
+ selected,
355
+ onPage,
356
+ atCap,
357
+ toggleRow,
358
+ toggleMany,
359
+ actionsByRow,
360
+ hasActions,
361
+ keys,
362
+ sortable: onSortChange != null,
363
+ multi,
364
+ sortFor,
365
+ cycleSort,
366
+ virtualize: pagination || shownGroups ? void 0 : virtualize,
367
+ rowNavigation: rowNavigation ?? onRowActivate != null,
368
+ onRowActivate,
369
+ activateOnClick,
370
+ activeRowId,
371
+ onActiveRowChange,
372
+ labels
373
+ }
374
+ );
248
375
  }
249
376
  const selectedIds = [...selected];
250
377
  return /* @__PURE__ */ jsxs(
@@ -257,13 +384,627 @@ function DataWorkspace({
257
384
  children: [
258
385
  toolbar,
259
386
  bulkActions && selectedIds.length > 0 && /* @__PURE__ */ jsx(BulkBar, { count: selectedIds.length, label: labels.bulkActions, selectedLabel: (n) => fillLabel(labels.selected, { count: n }), children: bulkActions(selectedIds) }),
387
+ beforeBody != null && /* @__PURE__ */ jsx("div", { className: "uix-data-workspace__before", children: beforeBody }),
260
388
  body,
261
- pagination && status === "ready" && shownRows.length > 0 && /* @__PURE__ */ jsx(Footer, { pagination, labels, rowCount: shownRows.length })
389
+ pagination && status === "ready" && stateOverride == null && shownRows.length > 0 && /* @__PURE__ */ jsx(Footer, { pagination, labels, rowCount: shownRows.length })
262
390
  ]
263
391
  }
264
392
  );
265
393
  }
394
+ const EMPTY_SET = /* @__PURE__ */ new Set();
395
+ function WorkspaceGrid(props) {
396
+ return props.virtualize ? /* @__PURE__ */ jsx(WindowedGrid, { ...props }) : /* @__PURE__ */ jsx(WorkspaceTable, { ...props, windowing: null, virtual: null });
397
+ }
398
+ function WindowedGrid(props) {
399
+ const windowing = props.virtualize === true || !props.virtualize ? {} : props.virtualize;
400
+ const threshold = windowing.threshold ?? 100;
401
+ const virtual = useVirtualRows(props.rows, {
402
+ rowHeight: windowing.estimatedRowHeight ?? ROW_HEIGHT[props.density ?? "standard"],
403
+ overscan: windowing.overscan ?? 8,
404
+ // windowed at `threshold` rows or more (the hook windows above its own threshold)
405
+ threshold: Math.max(0, threshold - 1),
406
+ estimatedViewportHeight: windowing.maxHeight ?? 640,
407
+ measureRows: "tbody > tr[data-uix-row-id]",
408
+ // cards are not windowed: a card's height is its content's
409
+ enabled: props.rows.length >= threshold && props.ladder !== "cards"
410
+ });
411
+ return /* @__PURE__ */ jsx(WorkspaceTable, { ...props, windowing, virtual });
412
+ }
413
+ const columnFloor = (width, minWidth, className) => {
414
+ if (minWidth) return minWidth;
415
+ if (width && !width.trim().endsWith("%")) return width;
416
+ const classes = className?.split(/\s+/) ?? [];
417
+ for (const cls of classes) {
418
+ const tier = /^uix-col--w-(xs|sm|md|lg|xl)$/.exec(cls)?.[1];
419
+ if (tier) return `var(--uix-col-w-${tier})`;
420
+ }
421
+ if (classes.includes("uix-col--flex") || classes.includes("uix-col--primary")) return "var(--uix-col-title-min)";
422
+ return "var(--uix-col-w-sm)";
423
+ };
424
+ const isFlex = (className) => /(^|\s)uix-col--(flex|primary)(\s|$)/.test(className ?? "");
425
+ const colClasses = (className) => className?.split(/\s+/).filter((cls) => /^uix-col--(w-(xs|sm|md|lg|xl)|flex|primary)$/.test(cls)).join(" ") || void 0;
426
+ function WorkspaceTable({
427
+ label,
428
+ rows,
429
+ ids,
430
+ groups,
431
+ collapsed,
432
+ onToggleGroup,
433
+ getRowId,
434
+ rowLabel,
435
+ columns,
436
+ idColumn,
437
+ getRowProps,
438
+ density,
439
+ zebra,
440
+ freezeFirstColumn,
441
+ fixed,
442
+ ladderOn,
443
+ ladder,
444
+ refreshing,
445
+ selectable,
446
+ selected,
447
+ onPage,
448
+ atCap,
449
+ toggleRow,
450
+ toggleMany,
451
+ actionsByRow,
452
+ hasActions,
453
+ keys,
454
+ sortable,
455
+ multi,
456
+ sortFor,
457
+ cycleSort,
458
+ rowNavigation,
459
+ onRowActivate,
460
+ activateOnClick,
461
+ activeRowId,
462
+ onActiveRowChange,
463
+ labels,
464
+ windowing,
465
+ virtual
466
+ }) {
467
+ const cards = ladder === "cards";
468
+ const wrapRef = useRef(null);
469
+ const baseId = useId();
470
+ const maxHeight = windowing?.maxHeight ?? 640;
471
+ const long = windowing != null && !cards && rows.length >= (windowing.threshold ?? 100);
472
+ const containerRef = virtual?.containerRef;
473
+ const setWrap = useCallback((node) => {
474
+ wrapRef.current = node;
475
+ if (containerRef) containerRef.current = node;
476
+ }, [containerRef]);
477
+ const windowed = virtual?.virtualized ?? false;
478
+ const start = virtual && windowed ? virtual.startIndex : 0;
479
+ const mountedRows = virtual && windowed ? virtual.rows : rows;
480
+ const nav = rowNavigation;
481
+ const clickActivates = onRowActivate != null && activateOnClick;
482
+ const [ownActive, setOwnActive] = useState(null);
483
+ const active = activeRowId !== void 0 ? activeRowId : ownActive;
484
+ const navIds = useMemo(() => {
485
+ if (!groups) return ids;
486
+ const out = [];
487
+ for (const group of groups) if (!collapsed.has(group.id)) for (const row of group.rows) out.push(getRowId(row));
488
+ return out;
489
+ }, [groups, collapsed, ids, getRowId]);
490
+ const mountedIds = useMemo(() => new Set(windowed ? ids.slice(start, start + mountedRows.length) : navIds), [windowed, ids, start, mountedRows.length, navIds]);
491
+ const tabStop = active != null && mountedIds.has(active) ? active : navIds.find((rowId) => mountedIds.has(rowId)) ?? null;
492
+ const pendingFocus = useRef(null);
493
+ const focusedRow = useRef(null);
494
+ const rowElement = (rowId) => {
495
+ for (const el of Array.from(wrapRef.current?.querySelectorAll("tbody > tr[data-uix-row-id]") ?? [])) {
496
+ if (el.getAttribute("data-uix-row-id") === rowId) return el;
497
+ }
498
+ return null;
499
+ };
500
+ const reveal = (el) => {
501
+ el.scrollIntoView?.({ block: "nearest", inline: "nearest" });
502
+ const wrap = wrapRef.current;
503
+ const head = wrap?.querySelector("thead");
504
+ if (wrap && head && windowed) {
505
+ const under = head.getBoundingClientRect().bottom - el.getBoundingClientRect().top;
506
+ if (under > 0) wrap.scrollTop -= under;
507
+ }
508
+ };
509
+ const setActive = (rowId) => {
510
+ if (rowId === active) return;
511
+ if (activeRowId === void 0) setOwnActive(rowId);
512
+ onActiveRowChange?.(rowId);
513
+ };
514
+ const focusRow = (rowId, { take = true } = {}) => {
515
+ if (!navIds.includes(rowId)) return;
516
+ setActive(rowId);
517
+ if (!take) return;
518
+ const el = rowElement(rowId);
519
+ if (el) {
520
+ el.focus({ preventScroll: true });
521
+ reveal(el);
522
+ return;
523
+ }
524
+ const index = ids.indexOf(rowId);
525
+ if (index >= 0 && wrapRef.current && virtual && windowed) {
526
+ pendingFocus.current = rowId;
527
+ wrapRef.current.scrollTop = index * virtual.rowHeight;
528
+ }
529
+ };
530
+ useIsomorphicLayoutEffect(() => {
531
+ if (!nav) return;
532
+ const pending = pendingFocus.current;
533
+ if (pending != null) {
534
+ if (!ids.includes(pending)) {
535
+ pendingFocus.current = null;
536
+ return;
537
+ }
538
+ const el2 = rowElement(pending);
539
+ if (el2) {
540
+ pendingFocus.current = null;
541
+ el2.focus({ preventScroll: true });
542
+ reveal(el2);
543
+ }
544
+ return;
545
+ }
546
+ const had = focusedRow.current;
547
+ if (had == null) return;
548
+ const wrap = wrapRef.current;
549
+ const focused = typeof document !== "undefined" ? document.activeElement : null;
550
+ const el = rowElement(had);
551
+ if (el && wrap && focused === wrap) {
552
+ el.focus({ preventScroll: true });
553
+ return;
554
+ }
555
+ if (el || !(focused == null || focused === document.body)) return;
556
+ if (windowed && ids.includes(had) && wrap) {
557
+ wrap.focus({ preventScroll: true });
558
+ } else if (tabStop != null) {
559
+ rowElement(tabStop)?.focus({ preventScroll: true });
560
+ }
561
+ });
562
+ const lastActiveProp = useRef(activeRowId);
563
+ useEffect(() => {
564
+ if (!nav || activeRowId === lastActiveProp.current) return;
565
+ lastActiveProp.current = activeRowId;
566
+ if (activeRowId == null) return;
567
+ const focused = document.activeElement;
568
+ const el = rowElement(activeRowId);
569
+ if (el && el.contains(focused)) return;
570
+ const mayTake = focused == null || focused === document.body || (wrapRef.current?.contains(focused) ?? false);
571
+ focusRow(activeRowId, { take: mayTake });
572
+ });
573
+ const latest = {
574
+ toggle: (rowId, on, event) => toggleRow(rowId, on, event),
575
+ key: (rowId, event) => {
576
+ if (event.target !== event.currentTarget || event.altKey || event.ctrlKey || event.metaKey) return;
577
+ const at = navIds.indexOf(rowId);
578
+ const to = event.key === "ArrowDown" ? at + 1 : event.key === "ArrowUp" ? at - 1 : event.key === "Home" ? 0 : event.key === "End" ? navIds.length - 1 : null;
579
+ if (to != null) {
580
+ event.preventDefault();
581
+ const target = navIds[Math.max(0, Math.min(navIds.length - 1, to))];
582
+ if (target != null) focusRow(target);
583
+ } else if (event.key === "Enter") {
584
+ event.preventDefault();
585
+ const row = rows[ids.indexOf(rowId)];
586
+ if (onRowActivate) {
587
+ if (row !== void 0) onRowActivate(row);
588
+ return;
589
+ }
590
+ const el = event.currentTarget;
591
+ (el.querySelector(".uix-id-cell__btn") ?? el.querySelector("a[href]"))?.click();
592
+ } else if (event.key === " ") {
593
+ event.preventDefault();
594
+ }
595
+ },
596
+ click: (rowId, event) => {
597
+ if (!clickActivates) return;
598
+ if (!event.currentTarget.contains(event.target)) return;
599
+ const control = event.target.closest?.(CONTROLS);
600
+ if (control && control !== event.currentTarget && event.currentTarget.contains(control)) return;
601
+ if (typeof window !== "undefined" && (window.getSelection?.()?.toString() ?? "") !== "") return;
602
+ const row = rows[ids.indexOf(rowId)];
603
+ if (row !== void 0) onRowActivate?.(row);
604
+ },
605
+ focus: (rowId) => {
606
+ focusedRow.current = rowId;
607
+ setActive(rowId);
608
+ },
609
+ blur: (rowId, event) => {
610
+ const next = event.relatedTarget;
611
+ if (next && wrapRef.current?.contains(next)) return;
612
+ if (next) {
613
+ focusedRow.current = null;
614
+ return;
615
+ }
616
+ const el = event.currentTarget;
617
+ setTimeout(() => {
618
+ if (el.isConnected && focusedRow.current === rowId) focusedRow.current = null;
619
+ }, 0);
620
+ }
621
+ };
622
+ const latestRef = useRef(latest);
623
+ latestRef.current = latest;
624
+ const handlers = useMemo(() => ({
625
+ toggle: (...args) => latestRef.current.toggle(...args),
626
+ key: (...args) => latestRef.current.key(...args),
627
+ click: (...args) => latestRef.current.click(...args),
628
+ focus: (...args) => latestRef.current.focus(...args),
629
+ blur: (...args) => latestRef.current.blur(...args)
630
+ }), []);
631
+ const groupAnchor = useRef(null);
632
+ const toggleGroup = (groupId, header) => {
633
+ groupAnchor.current = header ? { id: groupId, top: header.getBoundingClientRect().top } : null;
634
+ onToggleGroup(groupId);
635
+ };
636
+ useIsomorphicLayoutEffect(() => {
637
+ const was = groupAnchor.current;
638
+ if (!was) return;
639
+ groupAnchor.current = null;
640
+ const header = wrapRef.current?.querySelector(`tr[data-group-id="${cssEscape(was.id)}"]`);
641
+ if (!header) return;
642
+ const moved = header.getBoundingClientRect().top - was.top;
643
+ if (Math.abs(moved) < 1) return;
644
+ const wrap = wrapRef.current;
645
+ if (wrap && wrap.scrollHeight > wrap.clientHeight + 1 && getComputedStyle(wrap).overflowY !== "visible") wrap.scrollTop += moved;
646
+ else window.scrollBy?.(0, moved);
647
+ });
648
+ const shiftKey = useRef(false);
649
+ const additive = useRef(false);
650
+ const frozen = !cards && ladder !== "priority-mid" && (idColumn != null || freezeFirstColumn);
651
+ const shown = columns.filter((column) => !hiddenBy(ladder, column.priority));
652
+ const colCount = (selectable ? 1 : 0) + (idColumn ? 1 : 0) + shown.length + (hasActions ? 1 : 0);
653
+ const selectRowLabel = labels.selectRow;
654
+ const openLabel = labels.open;
655
+ const fixedOn = fixed && !cards;
656
+ const [actionsTooWide, setActionsTooWide] = useState(false);
657
+ const [actionsWidth, setActionsWidth] = useState(null);
658
+ const measureActions = !cards && hasActions;
659
+ const measureConfig = useRef({ measureActions, fixedOn, hasActions });
660
+ measureConfig.current = { measureActions, fixedOn, hasActions };
661
+ const measureActionsNow = useRef(() => {
662
+ });
663
+ measureActionsNow.current = () => {
664
+ const wrap = wrapRef.current;
665
+ const { measureActions: on, fixedOn: fixedNow, hasActions: any } = measureConfig.current;
666
+ if (!wrap) return;
667
+ const cells = Array.from(wrap.querySelectorAll("tbody td.uix-table__actions"));
668
+ if (!on) setActionsTooWide(false);
669
+ else {
670
+ let widest = 0;
671
+ for (const td of cells) if (typeof td.getBoundingClientRect === "function") widest = Math.max(widest, td.getBoundingClientRect().width, td.scrollWidth);
672
+ if (wrap.clientWidth > 0 && widest > 0) setActionsTooWide(widest > wrap.clientWidth / 4);
673
+ }
674
+ if (!fixedNow || !any || typeof document.createRange !== "function" || typeof getComputedStyle !== "function") return;
675
+ if (typeof document.createRange().getBoundingClientRect !== "function") return;
676
+ let content = 0;
677
+ for (const td of cells) {
678
+ const range = document.createRange();
679
+ range.selectNodeContents(td);
680
+ const style = getComputedStyle(td);
681
+ content = Math.max(content, range.getBoundingClientRect().width + parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0"));
682
+ }
683
+ const next = content > 0 ? Math.ceil(content) : null;
684
+ setActionsWidth((current) => current === next ? current : next);
685
+ };
686
+ useIsomorphicLayoutEffect(() => {
687
+ const wrap = wrapRef.current;
688
+ if (!wrap) return;
689
+ let frame = 0;
690
+ const schedule = () => {
691
+ if (frame || typeof requestAnimationFrame !== "function") return;
692
+ frame = requestAnimationFrame(() => {
693
+ frame = 0;
694
+ measureActionsNow.current();
695
+ });
696
+ };
697
+ const resize = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(schedule);
698
+ const collect = () => {
699
+ if (!resize) return;
700
+ resize.disconnect();
701
+ resize.observe(wrap);
702
+ const table = wrap.querySelector("table");
703
+ if (table) resize.observe(table);
704
+ for (const td of Array.from(wrap.querySelectorAll("tbody td.uix-table__actions"))) {
705
+ resize.observe(td);
706
+ for (const child of Array.from(td.children)) resize.observe(child);
707
+ }
708
+ };
709
+ collect();
710
+ const mutation = typeof MutationObserver === "undefined" ? null : new MutationObserver(() => {
711
+ collect();
712
+ schedule();
713
+ });
714
+ mutation?.observe(wrap, { childList: true, subtree: true });
715
+ return () => {
716
+ resize?.disconnect();
717
+ mutation?.disconnect();
718
+ if (frame && typeof cancelAnimationFrame === "function") cancelAnimationFrame(frame);
719
+ };
720
+ }, []);
721
+ const rowSet = ids.join("\0");
722
+ useIsomorphicLayoutEffect(() => {
723
+ measureActionsNow.current();
724
+ }, [measureActions, fixedOn, ladder, rowSet]);
725
+ const actionsCol = actionsWidth != null ? `${actionsWidth}px` : "5.25rem";
726
+ const floors = fixedOn ? [
727
+ ...selectable ? ["var(--uix-selcol-w)"] : [],
728
+ ...idColumn ? [columnFloor(idColumn.width, idColumn.minWidth, void 0)] : [],
729
+ ...shown.map((column) => columnFloor(column.width, column.minWidth, column.className)),
730
+ ...hasActions ? [actionsCol] : []
731
+ ] : [];
732
+ const tableStyle = fixedOn && floors.length ? { minWidth: floors.length === 1 ? floors[0] : `calc(${floors.join(" + ")})` } : void 0;
733
+ const sortProps = (column, columnSortable) => {
734
+ const order = keys.findIndex((key) => key.columnId === column.id);
735
+ const canSort = columnSortable && sortable;
736
+ const ordinal = keys.length > 1 && order >= 0 ? order + 1 : void 0;
737
+ return {
738
+ sortable: canSort,
739
+ sortDirection: sortFor(column),
740
+ sortOrder: ordinal,
741
+ sortOrderLabel: ordinal != null ? fillLabel(labels.sortPriority, { n: ordinal }) : void 0,
742
+ onSort: () => {
743
+ cycleSort(column, multi && additive.current);
744
+ additive.current = false;
745
+ },
746
+ ...canSort && multi ? {
747
+ onKeyDownCapture: (event) => {
748
+ if (event.key === "Enter" || event.key === " ") shiftKey.current = event.shiftKey;
749
+ },
750
+ // A click made by Enter or Space has no pointer (detail 0) and may not carry Shift itself.
751
+ onClickCapture: (event) => {
752
+ additive.current = event.shiftKey || event.detail === 0 && shiftKey.current;
753
+ shiftKey.current = false;
754
+ }
755
+ } : {}
756
+ };
757
+ };
758
+ const idSortColumn = idColumn ? { id: idColumn.id ?? "id", header: idColumn.header, cell: () => null } : null;
759
+ const renderRow = (row, index) => {
760
+ const rowId = ids[index];
761
+ const isSelected = selected.has(rowId);
762
+ return /* @__PURE__ */ jsx(
763
+ WorkspaceRow,
764
+ {
765
+ row,
766
+ rowId,
767
+ rowLabel,
768
+ columns,
769
+ idColumn,
770
+ getRowProps,
771
+ selectable,
772
+ selected: isSelected,
773
+ selectDisabled: !isSelected && atCap,
774
+ hasActions,
775
+ actions: actionsByRow[index],
776
+ ladderOn,
777
+ selectRowLabel,
778
+ openLabel,
779
+ marked: nav || windowing != null,
780
+ ariaRowIndex: windowed ? index + 2 : void 0,
781
+ nav,
782
+ tabStop: tabStop === rowId,
783
+ handlers
784
+ },
785
+ rowId
786
+ );
787
+ };
788
+ let tbody;
789
+ if (groups) {
790
+ let offset = 0;
791
+ tbody = groups.map((group, groupIndex) => {
792
+ const first = offset;
793
+ offset += group.rows.length;
794
+ const open = !collapsed.has(group.id);
795
+ const name = group.name ?? textOf(group.label) ?? group.id;
796
+ const groupIds = ids.slice(first, first + group.rows.length);
797
+ const on = groupIds.filter((rowId) => selected.has(rowId)).length;
798
+ const count = group.count ?? group.rows.length;
799
+ const nameId = `${baseId}-g${groupIndex}-name`;
800
+ const countId = `${baseId}-g${groupIndex}-count`;
801
+ const firstAgg = shown.findIndex((column) => group.aggregates?.[column.id] != null);
802
+ const lead = (idColumn ? 1 : 0) + (firstAgg < 0 ? shown.length + (hasActions ? 1 : 0) : firstAgg);
803
+ const leadAgg = lead === 0 && firstAgg === 0 ? group.aggregates?.[shown[0].id] : null;
804
+ return /* @__PURE__ */ jsxs("tbody", { className: "uix-data-workspace__group", "data-collapsed": open ? void 0 : "", children: [
805
+ /* @__PURE__ */ jsxs("tr", { className: "uix-data-workspace__group-head", "data-group-id": group.id, children: [
806
+ selectable && /* @__PURE__ */ jsx("td", { className: "uix-table__selcell", "data-col": ladderOn ? "select" : void 0, children: /* @__PURE__ */ jsx(
807
+ Checkbox,
808
+ {
809
+ "aria-label": fillLabel(labels.selectGroup, { name }),
810
+ disabled: group.rows.length === 0 || atCap && on === 0,
811
+ checked: on > 0 && on === groupIds.length,
812
+ indeterminate: on > 0 && on < groupIds.length,
813
+ onChange: () => toggleMany(groupIds, on < groupIds.length && !atCap)
814
+ }
815
+ ) }),
816
+ /* @__PURE__ */ jsx("th", { scope: "rowgroup", colSpan: Math.max(1, lead), className: "uix-data-workspace__group-label", "aria-labelledby": `${nameId} ${countId}`, children: /* @__PURE__ */ jsxs("span", { className: "uix-data-workspace__group-title", children: [
817
+ /* @__PURE__ */ jsx(
818
+ "button",
819
+ {
820
+ type: "button",
821
+ className: "uix-data-workspace__group-toggle",
822
+ "aria-expanded": open,
823
+ "aria-label": fillLabel(open ? labels.collapseGroup : labels.expandGroup, { name }),
824
+ onClick: (event) => toggleGroup(group.id, event.currentTarget.closest("tr")),
825
+ children: /* @__PURE__ */ jsx(ChevronIcon, {})
826
+ }
827
+ ),
828
+ /* @__PURE__ */ jsx("span", { className: "uix-data-workspace__group-name", id: nameId, children: group.label }),
829
+ /* @__PURE__ */ jsx("span", { className: "uix-data-workspace__group-count", id: countId, children: fillLabel(count === 1 ? labels.groupCountOne : labels.groupCount, { count }) }),
830
+ leadAgg
831
+ ] }) }),
832
+ firstAgg >= 0 && shown.slice(lead === 0 ? 1 : firstAgg).map((column) => /* @__PURE__ */ jsx("td", { className: cx("uix-data-workspace__group-agg", column.align === "end" && "uix-col--num", column.className), children: group.aggregates?.[column.id] }, column.id)),
833
+ firstAgg >= 0 && hasActions && /* @__PURE__ */ jsx("td", { className: "uix-table__actions" })
834
+ ] }),
835
+ open && group.rows.map((row, at) => renderRow(row, first + at))
836
+ ] }, group.id);
837
+ });
838
+ } else {
839
+ const spacer = (height, key) => /* @__PURE__ */ jsx("tr", { "aria-hidden": "true", className: "uix-data-workspace__spacer", children: /* @__PURE__ */ jsx("td", { colSpan: colCount, style: { height } }) }, key);
840
+ tbody = /* @__PURE__ */ jsxs("tbody", { children: [
841
+ virtual && windowed && start > 0 && spacer(virtual.padTop, "pad-top"),
842
+ windowed && start > 0 && start % 2 === 0 && spacer(0, "pad-zebra"),
843
+ mountedRows.map((row, at) => renderRow(row, start + at)),
844
+ virtual && windowed && virtual.padBottom > 0 && spacer(virtual.padBottom, "pad-bottom")
845
+ ] });
846
+ }
847
+ const wrapStyle = long ? { "--uix-table-max-height": `${maxHeight}px` } : void 0;
848
+ return /* @__PURE__ */ jsx(
849
+ TableWrap,
850
+ {
851
+ ref: setWrap,
852
+ refreshing,
853
+ style: wrapStyle,
854
+ tabIndex: windowing != null && nav ? -1 : void 0,
855
+ className: cx("uix-data-workspace__table", long && "uix-table-wrap--scroll", cards && "uix-data-workspace__table--cards"),
856
+ children: /* @__PURE__ */ jsxs(
857
+ Table,
858
+ {
859
+ density,
860
+ zebra,
861
+ pinnedCol: frozen,
862
+ pinnedActions: hasActions && !cards && !actionsTooWide,
863
+ fixed: fixedOn || void 0,
864
+ style: tableStyle,
865
+ className: cx(
866
+ selectable && frozen && "uix-data-workspace__grid--pin-id",
867
+ ladder && `uix-table--${ladder}`,
868
+ groups && zebra !== false && "uix-data-workspace__grid--grouped",
869
+ nav && "uix-data-workspace__grid--rows",
870
+ clickActivates && "uix-data-workspace__grid--click"
871
+ ),
872
+ "aria-label": label,
873
+ "aria-rowcount": windowed ? ids.length + 1 : void 0,
874
+ children: [
875
+ fixedOn && /* @__PURE__ */ jsxs("colgroup", { children: [
876
+ selectable && /* @__PURE__ */ jsx("col", { style: { width: "var(--uix-selcol-w)" } }),
877
+ idColumn && /* @__PURE__ */ jsx("col", { style: idColumn.width ? { width: idColumn.width } : void 0 }),
878
+ shown.map((column) => {
879
+ const width = column.width ?? (isFlex(column.className) ? void 0 : columnFloor(void 0, column.minWidth, column.className));
880
+ return /* @__PURE__ */ jsx("col", { className: colClasses(column.className), style: width ? { width } : void 0 }, column.id);
881
+ }),
882
+ hasActions && /* @__PURE__ */ jsx("col", { style: { width: actionsCol } })
883
+ ] }),
884
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { "aria-rowindex": windowed ? 1 : void 0, children: [
885
+ selectable && /* @__PURE__ */ jsx("th", { className: "uix-table__selcol", scope: "col", "data-col": ladderOn ? "select" : void 0, children: /* @__PURE__ */ jsx(
886
+ Checkbox,
887
+ {
888
+ "aria-label": labels.selectAll,
889
+ checked: onPage > 0 && onPage === ids.length,
890
+ indeterminate: onPage > 0 && onPage < ids.length,
891
+ disabled: atCap && onPage === 0,
892
+ onChange: () => toggleMany(ids, onPage < ids.length && !atCap)
893
+ }
894
+ ) }),
895
+ idColumn && (idColumn.sortable && idSortColumn ? /* @__PURE__ */ jsx(Th, { scope: "col", "data-col": ladderOn ? "id" : void 0, ...sortProps(idSortColumn, true), children: idColumn.header }) : /* @__PURE__ */ jsx(Th, { scope: "col", "data-col": ladderOn ? "id" : void 0, children: idColumn.header })),
896
+ columns.map((column) => {
897
+ const style = !fixedOn && (column.width || column.minWidth) ? { width: column.width, minWidth: column.minWidth } : void 0;
898
+ return /* @__PURE__ */ jsx(
899
+ Th,
900
+ {
901
+ scope: "col",
902
+ className: cx(column.align === "end" && "uix-col--num", column.className) || void 0,
903
+ style,
904
+ ...sortProps(column, column.sortable === true),
905
+ ...ladderAttrs(ladderOn, column, false),
906
+ children: column.header
907
+ },
908
+ column.id
909
+ );
910
+ }),
911
+ hasActions && /* @__PURE__ */ jsx("th", { scope: "col", className: "uix-table__actions", "data-col": ladderOn ? "actions" : void 0, children: /* @__PURE__ */ jsx("span", { className: "uix-visually-hidden", children: labels.rowActions }) })
912
+ ] }) }),
913
+ tbody
914
+ ]
915
+ }
916
+ )
917
+ }
918
+ );
919
+ }
920
+ function WorkspaceRowInner({
921
+ row,
922
+ rowId,
923
+ rowLabel,
924
+ columns,
925
+ idColumn,
926
+ getRowProps,
927
+ selectable,
928
+ selected,
929
+ selectDisabled,
930
+ hasActions,
931
+ actions,
932
+ ladderOn,
933
+ selectRowLabel,
934
+ openLabel,
935
+ marked,
936
+ ariaRowIndex,
937
+ nav,
938
+ tabStop,
939
+ handlers
940
+ }) {
941
+ const text = idColumn?.text(row) ?? rowLabel?.(row) ?? rowId;
942
+ const extra = getRowProps?.(row);
943
+ const navProps = nav ? {
944
+ tabIndex: tabStop ? 0 : -1,
945
+ onKeyDown: (event) => {
946
+ extra?.onKeyDown?.(event);
947
+ if (!event.defaultPrevented) handlers.key(rowId, event);
948
+ },
949
+ onClick: (event) => {
950
+ extra?.onClick?.(event);
951
+ if (!event.defaultPrevented) handlers.click(rowId, event);
952
+ },
953
+ onFocus: (event) => {
954
+ extra?.onFocus?.(event);
955
+ handlers.focus(rowId);
956
+ },
957
+ onBlur: (event) => {
958
+ extra?.onBlur?.(event);
959
+ handlers.blur(rowId, event);
960
+ }
961
+ } : {};
962
+ return /* @__PURE__ */ jsxs(Tr, { selected, "data-uix-row-id": marked ? rowId : void 0, "aria-rowindex": ariaRowIndex, ...extra, ...navProps, children: [
963
+ selectable && /* @__PURE__ */ jsx("td", { className: "uix-table__selcell", "data-col": ladderOn ? "select" : void 0, children: /* @__PURE__ */ jsx(
964
+ Checkbox,
965
+ {
966
+ "aria-label": fillLabel(selectRowLabel, { label: text }),
967
+ checked: selected,
968
+ disabled: selectDisabled,
969
+ onClick: (event) => handlers.toggle(rowId, event.currentTarget.checked, event),
970
+ onChange: () => {
971
+ }
972
+ }
973
+ ) }),
974
+ idColumn && /* @__PURE__ */ jsx(Td, { className: "uix-id-cell", "data-col": ladderOn ? "id" : void 0, children: /* @__PURE__ */ jsx(IdCell, { row, text, idColumn, openLabel }) }),
975
+ columns.map((column) => /* @__PURE__ */ jsx(Td, { className: cx(column.align === "end" && "uix-col--num", column.className) || void 0, ...ladderAttrs(ladderOn, column, true), children: column.cell(row) }, column.id)),
976
+ hasActions && /* @__PURE__ */ jsx(RowActions, { "data-col": ladderOn ? "actions" : void 0, children: actions })
977
+ ] });
978
+ }
979
+ const WorkspaceRow = memo(WorkspaceRowInner);
980
+ function IdCell({ row, text, idColumn, openLabel }) {
981
+ const rich = idColumn.render ? /* @__PURE__ */ jsx("span", { className: "uix-data-workspace__id-text", children: idColumn.render(row) }) : null;
982
+ const label = /* @__PURE__ */ jsx("span", { className: "uix-data-workspace__id-label", children: text });
983
+ const arrow = idColumn.href && renderUixLink(idColumn.renderLink, {
984
+ href: idColumn.href(row),
985
+ className: "uix-data-workspace__open",
986
+ "aria-label": fillLabel(openLabel, { label: text }),
987
+ children: /* @__PURE__ */ jsx(ArrowUpRight, {})
988
+ });
989
+ if (!idColumn.onPeek && idColumn.href && idColumn.linkId) {
990
+ return /* @__PURE__ */ jsx("span", { className: "uix-data-workspace__id", children: renderUixLink(idColumn.renderLink, {
991
+ href: idColumn.href(row),
992
+ className: "uix-id-cell__btn uix-data-workspace__id-link",
993
+ children: /* @__PURE__ */ jsxs(Fragment, { children: [
994
+ rich ?? label,
995
+ /* @__PURE__ */ jsx(ArrowUpRight, {})
996
+ ] })
997
+ }) });
998
+ }
999
+ return /* @__PURE__ */ jsxs("span", { className: "uix-data-workspace__id", children: [
1000
+ idColumn.onPeek ? /* @__PURE__ */ jsx("button", { type: "button", className: "uix-id-cell__btn", "aria-haspopup": "dialog", onClick: () => idColumn.onPeek?.(row), children: rich ?? label }) : rich ?? /* @__PURE__ */ jsx("span", { className: "uix-cell-strong uix-data-workspace__id-label", children: text }),
1001
+ arrow
1002
+ ] });
1003
+ }
1004
+ const cssEscape = (value) => typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
266
1005
  function Footer({ pagination, labels, rowCount }) {
1006
+ const provided = useUixLabels().pagination;
1007
+ const pagerLabels = provided || pagination.labels ? { ...provided, ...pagination.labels } : void 0;
267
1008
  const sizes = pagination.pageSizes;
268
1009
  const size = pagination.mode === "offset" ? pagination.pageSize : pagination.pageSize;
269
1010
  const rangeText = pagination.mode === "offset" ? fillLabel(labels.range, {
@@ -278,7 +1019,7 @@ function Footer({ pagination, labels, rowCount }) {
278
1019
  /* @__PURE__ */ jsx(Select, { size: "sm", value: String(size), onValueChange: (v) => pagination.onPageSizeChange?.(Number(v)), options: sizes.map((s) => ({ value: String(s), label: String(s) })) })
279
1020
  ] }),
280
1021
  /* @__PURE__ */ jsx("span", { className: "uix-toolbar__spacer" }),
281
- pagination.mode === "offset" ? /* @__PURE__ */ jsx(Pagination, { mode: "offset", page: pagination.page, pageCount: Math.max(1, Math.ceil(pagination.total / pagination.pageSize)), onChange: pagination.onPageChange }) : /* @__PURE__ */ jsx(Pagination, { mode: "cursor", hasPrevious: pagination.hasPrevious, hasNext: pagination.hasNext, onFirst: pagination.onFirst, onPrevious: pagination.onPrevious, onNext: pagination.onNext, summary: pagination.summary })
1022
+ pagination.mode === "offset" ? /* @__PURE__ */ jsx(Pagination, { mode: "offset", page: pagination.page, pageCount: Math.max(1, Math.ceil(pagination.total / pagination.pageSize)), onChange: pagination.onPageChange, labels: pagerLabels }) : /* @__PURE__ */ jsx(Pagination, { mode: "cursor", hasPrevious: pagination.hasPrevious, hasNext: pagination.hasNext, onFirst: pagination.onFirst, onPrevious: pagination.onPrevious, onNext: pagination.onNext, summary: pagination.summary, labels: pagerLabels })
282
1023
  ] });
283
1024
  }
284
1025
  function PopoverButton({ id, label, title, variant, children }) {
@@ -303,14 +1044,27 @@ function PopoverButton({ id, label, title, variant, children }) {
303
1044
  }
304
1045
  function ChipSummary({ field, value, options }) {
305
1046
  const resolved = useResolvedFilterLabels(field, value);
306
- return /* @__PURE__ */ jsx(Fragment, { children: summarizeFilter(resolved.field, resolved.value, options) });
1047
+ const provided = useUixLabels().filterSummary;
1048
+ const merged = provided ? { ...options, labels: { ...provided, ...options?.labels, operators: { ...provided.operators, ...options?.labels?.operators } } } : options;
1049
+ return /* @__PURE__ */ jsx(Fragment, { children: summarizeFilter(resolved.field, resolved.value, merged) });
307
1050
  }
308
1051
  function openInside(root) {
309
1052
  try {
310
1053
  if (root.querySelector(":popover-open")) return true;
311
1054
  } catch {
312
1055
  }
313
- return root.querySelector('dialog[open], [aria-expanded="true"]') != null;
1056
+ if (root.querySelector("dialog[open]")) return true;
1057
+ return Array.from(root.querySelectorAll('[aria-expanded="true"]')).some(stillExpanded);
1058
+ }
1059
+ function stillExpanded(el) {
1060
+ const controls = el.getAttribute("aria-controls");
1061
+ const target = controls ? document.getElementById(controls) : null;
1062
+ if (!target?.hasAttribute("popover")) return true;
1063
+ try {
1064
+ return target.matches(":popover-open");
1065
+ } catch {
1066
+ return true;
1067
+ }
314
1068
  }
315
1069
  function FilterChips({ filters, labels, baseId }) {
316
1070
  const [editing, setEditing] = useState(null);
@@ -353,6 +1107,10 @@ function FilterChips({ filters, labels, baseId }) {
353
1107
  setFocusChip(null);
354
1108
  }
355
1109
  });
1110
+ const clearSet = () => {
1111
+ if (filters.onClearAll) filters.onClearAll({ search: false });
1112
+ else for (const f of set) filters.onChange(f.id, void 0);
1113
+ };
356
1114
  return /* @__PURE__ */ jsxs(Fragment, { children: [
357
1115
  set.map((field) => /* @__PURE__ */ jsx("span", { ref: (el) => {
358
1116
  if (el) anchors.current.set(field.id, el);
@@ -368,9 +1126,7 @@ function FilterChips({ filters, labels, baseId }) {
368
1126
  }
369
1127
  ) }, field.id)),
370
1128
  unset.length > 0 && /* @__PURE__ */ jsx("span", { ref: addRef, children: /* @__PURE__ */ jsx(Menu, { trigger: /* @__PURE__ */ jsx(Chip, { variant: "add", icon: /* @__PURE__ */ jsx(PlusIcon, {}), children: labels.addFilter }), children: unset.map((field) => /* @__PURE__ */ jsx(MenuItem, { onSelect: () => edit(field, addRef.current), children: field.label }, field.id)) }) }),
371
- set.length > 0 && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "link", onClick: () => {
372
- for (const f of set) filters.onChange(f.id, void 0);
373
- }, children: labels.clearFilters }),
1129
+ set.length > 0 && /* @__PURE__ */ jsx(Button, { size: "sm", variant: "link", onClick: clearSet, children: labels.clearFilters }),
374
1130
  /* @__PURE__ */ jsx(Popover, { id: popId, anchor: editing?.anchor ?? null, role: "dialog", "aria-label": editing?.field.label, children: editing && /* @__PURE__ */ jsx(FilterEditor, { field: editing.field, value: draft, onValueChange: setDraft, onApply: (v) => finish(v), onClear: () => finish(void 0) }) })
375
1131
  ] });
376
1132
  }