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