@tensor_1/react 2.50.0 → 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.
@@ -2,6 +2,7 @@ import * as react from 'react';
2
2
  import { HTMLAttributes, ReactNode } from 'react';
3
3
  import { UixRenderLink } from '../link.js';
4
4
  import { FilterField, FilterValue, SummarizeFilterOptions } from '../filter-model.js';
5
+ import { PaginationLabels } from './Pagination.js';
5
6
  import { TableDensity } from './Table.js';
6
7
  import { SavedViewMenuProps, ViewMenuProps } from './TableControls.js';
7
8
 
@@ -110,6 +111,17 @@ interface DataWorkspaceIdColumn<T> {
110
111
  width?: string;
111
112
  /** The column's floor in a fixed layout. Default: the `md` column tier. */
112
113
  minWidth?: string;
114
+ /**
115
+ * The sort key the id column answers to in `sort` / `onSortChange` (HAR-2631), e.g. `title`
116
+ * when the id shows the title. Default `'id'`. Keep it distinct from the data columns' ids.
117
+ */
118
+ id?: string;
119
+ /**
120
+ * The id header sorts like a data column (HAR-2631): a click cycles it, Shift + click (or
121
+ * Shift + Enter / Space) adds it as a further key in a multi-sort, with its ordinal and
122
+ * `aria-sort`. Off: the header is plain, as in 2.50.0.
123
+ */
124
+ sortable?: boolean;
113
125
  renderLink?: UixRenderLink;
114
126
  }
115
127
  /** A group of rows under one header row (HAR-2625). The product computes the groups. */
@@ -125,6 +137,13 @@ interface DataWorkspaceGroup<T> {
125
137
  /** Aggregated cells in the header row, by column id (a sum, a range), each under its column. */
126
138
  aggregates?: Readonly<Record<string, ReactNode>>;
127
139
  }
140
+ /** Where a row's actions are shown (HAR-2634), the second argument of `rowActions`. */
141
+ interface DataWorkspaceRowActionsContext {
142
+ /** The ladder's step: `null` for the full table (or without `mobile`). */
143
+ ladder: 'cards' | 'priority' | 'priority-mid' | null;
144
+ /** The narrowest table step (`priority-mid`): render only the always-on control. Cards keep the full set, on a line of their own. */
145
+ compact: boolean;
146
+ }
128
147
  /** Windowing for a long unpaged list (HAR-2626), over `useVirtualRows`. */
129
148
  interface DataWorkspaceVirtualize {
130
149
  /** Window only above this many rows. Default 100. */
@@ -136,7 +155,11 @@ interface DataWorkspaceVirtualize {
136
155
  /** Rows rendered beyond each edge. Default 8. */
137
156
  overscan?: number;
138
157
  }
139
- type DataWorkspacePagination = {
158
+ /**
159
+ * The footer's pager. `labels` words the pager itself; without them it takes `UixLabelsProvider`
160
+ * `pagination`, then English (HAR-2631).
161
+ */
162
+ type DataWorkspacePagination = ({
140
163
  mode: 'offset';
141
164
  page: number;
142
165
  pageSize: number;
@@ -155,12 +178,20 @@ type DataWorkspacePagination = {
155
178
  pageSize?: number;
156
179
  pageSizes?: readonly number[];
157
180
  onPageSizeChange?: (size: number) => void;
181
+ }) & {
182
+ labels?: Partial<PaginationLabels>;
158
183
  };
159
184
  interface DataWorkspaceProps<T, S extends DataWorkspaceSortState = DataWorkspaceSort | null> extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
160
185
  /** Names the workspace region and the table. */
161
186
  label: string;
162
187
  rows: readonly T[];
163
188
  getRowId: (row: T) => string;
189
+ /**
190
+ * A row's name in words, where the kit names a row (its checkbox, HAR-2632) and there is no
191
+ * `idColumn.text`: a title or a person, never a key. Without it the row id is used, which is
192
+ * a UUID on many tables. Keep it stable (`useCallback`), as rows are memoised.
193
+ */
194
+ rowLabel?: (row: T) => string;
164
195
  columns: readonly DataWorkspaceColumn<T>[];
165
196
  idColumn?: DataWorkspaceIdColumn<T>;
166
197
  /**
@@ -186,8 +217,13 @@ interface DataWorkspaceProps<T, S extends DataWorkspaceSortState = DataWorkspace
186
217
  /**
187
218
  * A row's actions (a `Menu`, quick buttons), in a column frozen at the right edge and sized to
188
219
  * them. Return `null` for a row without actions; with none on the page there is no column.
220
+ * The second argument says where the row is shown (HAR-2634): at the narrowest step of the
221
+ * ladder (`compact`, a workspace under 600 px with `mobile`), render only the always-on control,
222
+ * the peek or a kebab; the peek or the record carries the verbs. A frozen actions cell is for
223
+ * compact controls: word verbs (wider than a quarter of the table) unfreeze and scroll with
224
+ * their row, at any width.
189
225
  */
190
- rowActions?: (row: T) => ReactNode;
226
+ rowActions?: (row: T, context: DataWorkspaceRowActionsContext) => ReactNode;
191
227
  /** More props for a row, e.g. a data attribute that marks it changed (see ChangeHighlight, PR #164). */
192
228
  getRowProps?: (row: T) => HTMLAttributes<HTMLTableRowElement> & Record<`data-${string}`, string | undefined>;
193
229
  density?: TableDensity;
@@ -333,6 +369,6 @@ interface DataWorkspaceProps<T, S extends DataWorkspaceSortState = DataWorkspace
333
369
  * rich id cell, multi-column sort, the product's state bodies and `beforeBody`, row groups,
334
370
  * windowing (`virtualize`) and a keyboard row model (`onRowActivate`).
335
371
  */
336
- declare function DataWorkspace<T, S extends DataWorkspaceSortState = DataWorkspaceSort | null>({ label, rows, getRowId, columns, idColumn, sort, onSortChange, selection, onSelectionChange, maxSelection, bulkActions, rowActions, getRowProps, density, zebra, freezeFirstColumn, fixed, mobile, search, filters, savedViews, viewMenu, viewChanged, onSaveView, onSaveViewAs, saveViewAs, maximized, onMaximizedChange, toolbarEnd, beforeBody, pagination, status, refreshing, onRetry, loadingRows, empty, emptyAction, filteredEmpty, filteredEmptyAction, error, stateOverride, groups, collapsedGroups, onCollapsedGroupsChange, virtualize, rowNavigation, onRowActivate, activateOnClick, activeRowId, onActiveRowChange, multiSort, labels: labelOverrides, className, ...props }: DataWorkspaceProps<T, S>): react.JSX.Element;
372
+ declare function DataWorkspace<T, S extends DataWorkspaceSortState = DataWorkspaceSort | null>({ label, rows, getRowId, rowLabel, columns, idColumn, sort, onSortChange, selection, onSelectionChange, maxSelection, bulkActions, rowActions, getRowProps, density, zebra, freezeFirstColumn, fixed, mobile, search, filters, savedViews, viewMenu, viewChanged, onSaveView, onSaveViewAs, saveViewAs, maximized, onMaximizedChange, toolbarEnd, beforeBody, pagination, status, refreshing, onRetry, loadingRows, empty, emptyAction, filteredEmpty, filteredEmptyAction, error, stateOverride, groups, collapsedGroups, onCollapsedGroupsChange, virtualize, rowNavigation, onRowActivate, activateOnClick, activeRowId, onActiveRowChange, multiSort, labels: labelOverrides, className, ...props }: DataWorkspaceProps<T, S>): react.JSX.Element;
337
373
 
338
- export { DEFAULT_DATA_WORKSPACE_LABELS, DataWorkspace, type DataWorkspaceColumn, type DataWorkspaceGroup, type DataWorkspaceIdColumn, type DataWorkspaceLabels, type DataWorkspacePagination, type DataWorkspaceProps, type DataWorkspaceSort, type DataWorkspaceSortState, type DataWorkspaceSortValue, type DataWorkspaceVirtualize };
374
+ export { DEFAULT_DATA_WORKSPACE_LABELS, DataWorkspace, type DataWorkspaceColumn, type DataWorkspaceGroup, type DataWorkspaceIdColumn, type DataWorkspaceLabels, type DataWorkspacePagination, type DataWorkspaceProps, type DataWorkspaceRowActionsContext, type DataWorkspaceSort, type DataWorkspaceSortState, type DataWorkspaceSortValue, type DataWorkspaceVirtualize };
@@ -85,6 +85,7 @@ function DataWorkspace({
85
85
  label,
86
86
  rows,
87
87
  getRowId,
88
+ rowLabel,
88
89
  columns,
89
90
  idColumn,
90
91
  sort,
@@ -246,7 +247,11 @@ function DataWorkspace({
246
247
  onSelectionChange(apply(new Set(selected), rowIds, on));
247
248
  };
248
249
  const atCap = selected.size >= cap;
249
- const actionsByRow = useMemo(() => rowActions ? shownRows.map((row) => rowActions(row)) : [], [shownRows, rowActions]);
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]);
250
255
  const hasActions = actionsByRow.some((node) => node != null && node !== false);
251
256
  const activeFilters = filters ? filters.fields.filter((field) => !isFilterEmpty(filters.values[field.id])) : [];
252
257
  const searching = (search?.value.trim() ?? "") !== "";
@@ -273,7 +278,8 @@ function DataWorkspace({
273
278
  onSortChange(next);
274
279
  };
275
280
  const showSaveAs = onSaveViewAs != null && (viewChanged || saveViewAs === "always");
276
- const toolbar = /* @__PURE__ */ jsxs("div", { className: "uix-toolbar uix-data-workspace__toolbar", role: "toolbar", "aria-label": label, children: [
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: [
277
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: [
278
284
  savedViews.note != null && /* @__PURE__ */ jsx("div", { className: "uix-data-workspace__views-note", children: savedViews.note }),
279
285
  /* @__PURE__ */ jsx(SavedViewMenu, { ...savedViews.menu, onSelect: (viewId) => {
@@ -333,6 +339,7 @@ function DataWorkspace({
333
339
  collapsed,
334
340
  onToggleGroup: toggleGroupOpen,
335
341
  getRowId,
342
+ rowLabel,
336
343
  columns,
337
344
  idColumn,
338
345
  getRowProps,
@@ -424,6 +431,7 @@ function WorkspaceTable({
424
431
  collapsed,
425
432
  onToggleGroup,
426
433
  getRowId,
434
+ rowLabel,
427
435
  columns,
428
436
  idColumn,
429
437
  getRowProps,
@@ -645,20 +653,75 @@ function WorkspaceTable({
645
653
  const selectRowLabel = labels.selectRow;
646
654
  const openLabel = labels.open;
647
655
  const fixedOn = fixed && !cards;
656
+ const [actionsTooWide, setActionsTooWide] = useState(false);
648
657
  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") ?? [])) {
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) {
653
678
  const range = document.createRange();
654
679
  range.selectNodeContents(td);
655
- const content = range.getBoundingClientRect().width;
656
680
  const style = getComputedStyle(td);
657
- widest = Math.max(widest, content + parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0"));
681
+ content = Math.max(content, range.getBoundingClientRect().width + parseFloat(style.paddingLeft || "0") + parseFloat(style.paddingRight || "0"));
658
682
  }
659
- const next = widest > 0 ? Math.ceil(widest) : null;
660
- if (next !== actionsWidth) setActionsWidth(next);
661
- });
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]);
662
725
  const actionsCol = actionsWidth != null ? `${actionsWidth}px` : "5.25rem";
663
726
  const floors = fixedOn ? [
664
727
  ...selectable ? ["var(--uix-selcol-w)"] : [],
@@ -667,6 +730,32 @@ function WorkspaceTable({
667
730
  ...hasActions ? [actionsCol] : []
668
731
  ] : [];
669
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;
670
759
  const renderRow = (row, index) => {
671
760
  const rowId = ids[index];
672
761
  const isSelected = selected.has(rowId);
@@ -675,6 +764,7 @@ function WorkspaceTable({
675
764
  {
676
765
  row,
677
766
  rowId,
767
+ rowLabel,
678
768
  columns,
679
769
  idColumn,
680
770
  getRowProps,
@@ -769,7 +859,7 @@ function WorkspaceTable({
769
859
  density,
770
860
  zebra,
771
861
  pinnedCol: frozen,
772
- pinnedActions: hasActions && !cards,
862
+ pinnedActions: hasActions && !cards && !actionsTooWide,
773
863
  fixed: fixedOn || void 0,
774
864
  style: tableStyle,
775
865
  className: cx(
@@ -802,36 +892,16 @@ function WorkspaceTable({
802
892
  onChange: () => toggleMany(ids, onPage < ids.length && !atCap)
803
893
  }
804
894
  ) }),
805
- idColumn && /* @__PURE__ */ jsx(Th, { scope: "col", "data-col": ladderOn ? "id" : void 0, children: idColumn.header }),
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 })),
806
896
  columns.map((column) => {
807
- const order = keys.findIndex((key) => key.columnId === column.id);
808
- const canSort = column.sortable && sortable;
809
897
  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
898
  return /* @__PURE__ */ jsx(
812
899
  Th,
813
900
  {
814
901
  scope: "col",
815
902
  className: cx(column.align === "end" && "uix-col--num", column.className) || void 0,
816
903
  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
- } : {},
904
+ ...sortProps(column, column.sortable === true),
835
905
  ...ladderAttrs(ladderOn, column, false),
836
906
  children: column.header
837
907
  },
@@ -850,6 +920,7 @@ function WorkspaceTable({
850
920
  function WorkspaceRowInner({
851
921
  row,
852
922
  rowId,
923
+ rowLabel,
853
924
  columns,
854
925
  idColumn,
855
926
  getRowProps,
@@ -867,7 +938,7 @@ function WorkspaceRowInner({
867
938
  tabStop,
868
939
  handlers
869
940
  }) {
870
- const text = idColumn?.text(row) ?? rowId;
941
+ const text = idColumn?.text(row) ?? rowLabel?.(row) ?? rowId;
871
942
  const extra = getRowProps?.(row);
872
943
  const navProps = nav ? {
873
944
  tabIndex: tabStop ? 0 : -1,
@@ -908,6 +979,7 @@ function WorkspaceRowInner({
908
979
  const WorkspaceRow = memo(WorkspaceRowInner);
909
980
  function IdCell({ row, text, idColumn, openLabel }) {
910
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 });
911
983
  const arrow = idColumn.href && renderUixLink(idColumn.renderLink, {
912
984
  href: idColumn.href(row),
913
985
  className: "uix-data-workspace__open",
@@ -919,18 +991,20 @@ function IdCell({ row, text, idColumn, openLabel }) {
919
991
  href: idColumn.href(row),
920
992
  className: "uix-id-cell__btn uix-data-workspace__id-link",
921
993
  children: /* @__PURE__ */ jsxs(Fragment, { children: [
922
- rich ?? text,
994
+ rich ?? label,
923
995
  /* @__PURE__ */ jsx(ArrowUpRight, {})
924
996
  ] })
925
997
  }) });
926
998
  }
927
999
  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 }),
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 }),
929
1001
  arrow
930
1002
  ] });
931
1003
  }
932
1004
  const cssEscape = (value) => typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value.replace(/["\\]/g, "\\$&");
933
1005
  function Footer({ pagination, labels, rowCount }) {
1006
+ const provided = useUixLabels().pagination;
1007
+ const pagerLabels = provided || pagination.labels ? { ...provided, ...pagination.labels } : void 0;
934
1008
  const sizes = pagination.pageSizes;
935
1009
  const size = pagination.mode === "offset" ? pagination.pageSize : pagination.pageSize;
936
1010
  const rangeText = pagination.mode === "offset" ? fillLabel(labels.range, {
@@ -945,7 +1019,7 @@ function Footer({ pagination, labels, rowCount }) {
945
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) })) })
946
1020
  ] }),
947
1021
  /* @__PURE__ */ jsx("span", { className: "uix-toolbar__spacer" }),
948
- 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 })
949
1023
  ] });
950
1024
  }
951
1025
  function PopoverButton({ id, label, title, variant, children }) {
@@ -970,7 +1044,9 @@ function PopoverButton({ id, label, title, variant, children }) {
970
1044
  }
971
1045
  function ChipSummary({ field, value, options }) {
972
1046
  const resolved = useResolvedFilterLabels(field, value);
973
- 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) });
974
1050
  }
975
1051
  function openInside(root) {
976
1052
  try {
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { TextFilterOperator, NumberFilterOperator, DateFilterOperator, FilterField, FilterValue } from '../filter-model.js';
2
+ import { TextFilterOperator, NumberFilterOperator, DateFilterOperator, EnumFilterOperator, FilterField, FilterValue } from '../filter-model.js';
3
3
 
4
4
  interface FilterEditorLabels {
5
5
  apply: string;
@@ -35,6 +35,8 @@ interface FilterEditorLabels {
35
35
  operators: Record<TextFilterOperator | NumberFilterOperator, string>;
36
36
  /** `date` conditions (HAR-1832). Optional, like `presets`. */
37
37
  dateOperators?: Partial<Record<DateFilterOperator, string>>;
38
+ /** `enum` conditions (HAR-2631), when the field offers both. Optional, like `presets`. */
39
+ enumOperators?: Partial<Record<EnumFilterOperator, string>>;
38
40
  }
39
41
  declare const DEFAULT_FILTER_EDITOR_LABELS: FilterEditorLabels;
40
42
  interface FilterEditorProps {
@@ -12,7 +12,7 @@ import { cx } from "../cx.js";
12
12
  import { useUixLabels } from "../labels-context.js";
13
13
  import { useDelayedPending } from "../hooks/useDelayedPending.js";
14
14
  import { foldForSearch } from "../search-suggest-model.js";
15
- import { DATE_FILTER_OPERATORS, NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, isUnresolvedChoice, matchFilterPreset, orderFilterRange } from "../filter-model.js";
15
+ import { DATE_FILTER_OPERATORS, ENUM_FILTER_OPERATORS, NUMBER_FILTER_OPERATORS, TEXT_FILTER_OPERATORS, emptyFilterValue, isUnresolvedChoice, matchFilterPreset, orderFilterRange } from "../filter-model.js";
16
16
  const DEFAULT_FILTER_EDITOR_LABELS = {
17
17
  apply: "Apply",
18
18
  clear: "Clear",
@@ -38,6 +38,7 @@ const DEFAULT_FILTER_EDITOR_LABELS = {
38
38
  addTagHint: "Enter or a comma adds it.",
39
39
  date: "Date",
40
40
  dateOperators: { on: "on", before: "before", after: "after", between: "between" },
41
+ enumOperators: { "is-any-of": "is any of", "is-none-of": "is none of" },
41
42
  operators: {
42
43
  contains: "contains",
43
44
  "not-contains": "does not contain",
@@ -60,7 +61,8 @@ function FilterEditor({ field, value: valueProp, onValueChange, onApply, onClear
60
61
  ...provided,
61
62
  ...labelOverrides,
62
63
  operators: { ...DEFAULT_FILTER_EDITOR_LABELS.operators, ...provided?.operators, ...labelOverrides?.operators },
63
- dateOperators: { ...DEFAULT_FILTER_EDITOR_LABELS.dateOperators, ...provided?.dateOperators, ...labelOverrides?.dateOperators }
64
+ dateOperators: { ...DEFAULT_FILTER_EDITOR_LABELS.dateOperators, ...provided?.dateOperators, ...labelOverrides?.dateOperators },
65
+ enumOperators: { ...DEFAULT_FILTER_EDITOR_LABELS.enumOperators, ...provided?.enumOperators, ...labelOverrides?.enumOperators }
64
66
  };
65
67
  const id = useId();
66
68
  let value = valueProp?.kind === field.kind ? valueProp : emptyFilterValue(field.kind);
@@ -163,16 +165,30 @@ function EnumBody({ field, value, onChange, labels, threshold }) {
163
165
  return q ? options.filter((o) => foldForSearch(o.label).text.includes(q)) : options;
164
166
  }, [options, query]);
165
167
  const selected = new Set(value.values);
166
- const toggle = (v, on) => onChange({ kind: "enum", values: on ? [...value.values, v] : value.values.filter((x) => x !== v) });
168
+ const ops = (field.enumOperators ?? []).filter((op) => ENUM_FILTER_OPERATORS.includes(op));
169
+ const condition = value.operator ?? (ops.length === 1 ? ops[0] : void 0);
170
+ const withValues = (values) => condition && condition !== "is-any-of" ? { kind: "enum", values, operator: condition } : { kind: "enum", values };
171
+ const toggle = (v, on) => onChange(withValues(on ? [...value.values, v] : value.values.filter((x) => x !== v)));
167
172
  const presets = field.presets ?? [];
168
173
  const active = matchFilterPreset(field, value);
174
+ const words = labels.enumOperators ?? {};
169
175
  return /* @__PURE__ */ jsxs(Fragment, { children: [
170
- presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group uix-filter-editor__presets", role: "group", "aria-label": labels.presets ?? DEFAULT_FILTER_EDITOR_LABELS.presets, children: presets.map((p) => /* @__PURE__ */ jsx(Chip, { pressed: p === active, onPressedChange: () => onChange({ kind: "enum", values: [...p.values] }), children: p.label }, p.id)) }),
176
+ ops.length > 1 && /* @__PURE__ */ jsx("div", { className: "uix-filter-editor__row", children: /* @__PURE__ */ jsx(
177
+ Select,
178
+ {
179
+ size: "sm",
180
+ "aria-label": labels.operator,
181
+ value: value.operator ?? "is-any-of",
182
+ options: ops.map((op) => ({ value: op, label: words[op] ?? DEFAULT_FILTER_EDITOR_LABELS.enumOperators[op] })),
183
+ onValueChange: (op) => onChange(op === "is-none-of" ? { kind: "enum", values: value.values, operator: "is-none-of" } : { kind: "enum", values: value.values })
184
+ }
185
+ ) }),
186
+ presets.length > 0 && /* @__PURE__ */ jsx("div", { className: "uix-chip-group uix-filter-editor__presets", role: "group", "aria-label": labels.presets ?? DEFAULT_FILTER_EDITOR_LABELS.presets, children: presets.map((p) => /* @__PURE__ */ jsx(Chip, { pressed: p === active, onPressedChange: () => onChange(withValues([...p.values])), children: p.label }, p.id)) }),
171
187
  options.length > threshold && /* @__PURE__ */ jsx(Input, { type: "search", size: "sm", "aria-label": labels.search, placeholder: labels.search, value: query, onChange: (e) => setQuery(e.currentTarget.value), autoFocus: true }),
172
188
  /* @__PURE__ */ jsxs("div", { className: "uix-filter-editor__bulk", children: [
173
189
  /* @__PURE__ */ jsx("span", { className: "uix-filter-editor__count", "aria-live": "polite", children: fillLabel(labels.selected, { count: value.values.length }) }),
174
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange({ kind: "enum", values: [.../* @__PURE__ */ new Set([...value.values, ...shown.map((o) => o.value)])] }), children: labels.selectAll }),
175
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange({ kind: "enum", values: [] }), children: labels.selectNone })
190
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange(withValues([.../* @__PURE__ */ new Set([...value.values, ...shown.map((o) => o.value)])])), children: labels.selectAll }),
191
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", size: "sm", onClick: () => onChange(withValues([])), children: labels.selectNone })
176
192
  ] }),
177
193
  /* @__PURE__ */ jsx("div", { className: "uix-filter-editor__list", children: shown.length === 0 ? /* @__PURE__ */ jsx("p", { className: "uix-filter-editor__note", children: labels.noMatches }) : shown.map((o) => /* @__PURE__ */ jsx(Checkbox, { label: o.label, checked: selected.has(o.value), onChange: (e) => toggle(o.value, e.currentTarget.checked) }, o.value)) })
178
194
  ] });
@@ -20,6 +20,10 @@ declare const TableWrap: react.ForwardRefExoticComponent<TableWrapProps & react.
20
20
  type TableDensity = 'compact' | 'standard' | 'comfortable';
21
21
  type SortDirection = 'ascending' | 'descending' | 'none';
22
22
  interface TableProps extends TableHTMLAttributes<HTMLTableElement> {
23
+ /**
24
+ * The row height. Named, the table holds it, `standard` included, whatever the page sets
25
+ * `--uix-row-h` to (a density preference); left out, the table follows `--uix-row-h`.
26
+ */
23
27
  density?: TableDensity;
24
28
  zebra?: boolean;
25
29
  pinnedCol?: boolean;
@@ -13,7 +13,7 @@ const TableWrap = forwardRef(function TableWrap2({ announcement, refreshing, chi
13
13
  ] });
14
14
  });
15
15
  function Table({
16
- density = "standard",
16
+ density,
17
17
  zebra = true,
18
18
  pinnedCol,
19
19
  pinnedActions,
@@ -35,7 +35,7 @@ function Table({
35
35
  fixed && "uix-table--fixed",
36
36
  className
37
37
  ),
38
- "data-density": density !== "standard" ? density : void 0,
38
+ "data-density": density,
39
39
  "data-uix-rowpeek": rowPeek || void 0,
40
40
  "data-uix-table-v2": v2 || void 0,
41
41
  ...props,
@@ -15,9 +15,15 @@ type NumberFilterOperator = 'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'betwee
15
15
  * `dateFilterBounds` turns each into the half-open range a query needs.
16
16
  */
17
17
  type DateFilterOperator = 'on' | 'before' | 'after' | 'between';
18
+ /**
19
+ * An `enum` condition (HAR-2631): the row's value is one of the chosen values, or none of them
20
+ * ("status is none of Withdrawn, Lost"). Absent means `is-any-of`.
21
+ */
22
+ type EnumFilterOperator = 'is-any-of' | 'is-none-of';
18
23
  type FilterValue = {
19
24
  kind: 'enum';
20
25
  values: string[];
26
+ operator?: EnumFilterOperator;
21
27
  } | {
22
28
  kind: 'text';
23
29
  operator: TextFilterOperator;
@@ -79,6 +85,11 @@ interface FilterField {
79
85
  operators?: readonly (TextFilterOperator | NumberFilterOperator)[];
80
86
  /** `date`: the conditions offered, in order. Default: on, before, after, between. */
81
87
  dateOperators?: readonly DateFilterOperator[];
88
+ /**
89
+ * `enum`: the conditions offered, in order (HAR-2631). Default: `is-any-of` alone, so the editor
90
+ * shows no condition control; with both, it shows one above the options.
91
+ */
92
+ enumOperators?: readonly EnumFilterOperator[];
82
93
  /** `number`: a unit shown after the value, e.g. "days". */
83
94
  unit?: string;
84
95
  /** `boolean`: words for true and false, e.g. Escalated / Not escalated. Default from labels. */
@@ -105,6 +116,8 @@ interface FilterSummaryLabels {
105
116
  more: string;
106
117
  /** An `enum` value equal to a preset: `{label}` (the preset), `{field}`. Default `{label}`. */
107
118
  preset?: string;
119
+ /** An `enum` value that excludes (`is-none-of`, HAR-2631): `{values}`. Default "not {values}" ("Status: not Open, Closed"). */
120
+ noneOf?: string;
108
121
  /** `date` conditions (HAR-1832), `{date}`. Default "{date}", "before {date}", "after {date}". */
109
122
  on?: string;
110
123
  before?: string;
@@ -114,6 +127,7 @@ declare const DEFAULT_FILTER_SUMMARY_LABELS: FilterSummaryLabels;
114
127
  declare const TEXT_FILTER_OPERATORS: readonly TextFilterOperator[];
115
128
  declare const NUMBER_FILTER_OPERATORS: readonly NumberFilterOperator[];
116
129
  declare const DATE_FILTER_OPERATORS: readonly DateFilterOperator[];
130
+ declare const ENUM_FILTER_OPERATORS: readonly EnumFilterOperator[];
117
131
  /** The empty value for a field kind (what Clear resets to). */
118
132
  declare function emptyFilterValue(kind: FilterValueKind): FilterValue;
119
133
  /** True when the value filters nothing (so the chip goes away). */
@@ -165,4 +179,4 @@ interface SummarizeFilterOptions {
165
179
  */
166
180
  declare function summarizeFilter(field: FilterField, input: FilterValue | undefined, options?: SummarizeFilterOptions): string;
167
181
 
168
- export { DATE_FILTER_OPERATORS, DEFAULT_FILTER_SUMMARY_LABELS, type DateFilterOperator, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, dateFilterBounds, emptyFilterValue, isFilterEmpty, isUnresolvedChoice, matchFilterPreset, orderFilterRange, resolveFilterLabels, summarizeFilter };
182
+ export { DATE_FILTER_OPERATORS, DEFAULT_FILTER_SUMMARY_LABELS, type DateFilterOperator, ENUM_FILTER_OPERATORS, type EnumFilterOperator, type FilterChoice, type FilterField, type FilterPreset, type FilterSummaryLabels, type FilterValue, type FilterValueKind, NUMBER_FILTER_OPERATORS, type NumberFilterOperator, type SummarizeFilterOptions, TEXT_FILTER_OPERATORS, type TextFilterOperator, dateFilterBounds, emptyFilterValue, isFilterEmpty, isUnresolvedChoice, matchFilterPreset, orderFilterRange, resolveFilterLabels, summarizeFilter };
@@ -20,6 +20,7 @@ const DEFAULT_FILTER_SUMMARY_LABELS = {
20
20
  until: "until {to}",
21
21
  more: "{first} +{count}",
22
22
  preset: "{label}",
23
+ noneOf: "not {values}",
23
24
  on: "{date}",
24
25
  before: "before {date}",
25
26
  after: "after {date}"
@@ -27,6 +28,7 @@ const DEFAULT_FILTER_SUMMARY_LABELS = {
27
28
  const TEXT_FILTER_OPERATORS = ["contains", "not-contains", "is", "is-not", "starts-with"];
28
29
  const NUMBER_FILTER_OPERATORS = ["eq", "neq", "lt", "lte", "gt", "gte", "between"];
29
30
  const DATE_FILTER_OPERATORS = ["on", "before", "after", "between"];
31
+ const ENUM_FILTER_OPERATORS = ["is-any-of", "is-none-of"];
30
32
  function emptyFilterValue(kind) {
31
33
  switch (kind) {
32
34
  case "enum":
@@ -108,7 +110,7 @@ async function resolveFilterLabels(field, value) {
108
110
  return { kind: "reference", values: value.values.map((v) => resolved.has(v.value) ? { value: v.value, label: resolved.get(v.value) } : v) };
109
111
  }
110
112
  function matchFilterPreset(field, value) {
111
- if (value?.kind !== "enum" || !field.presets?.length) return void 0;
113
+ if (value?.kind !== "enum" || value.operator === "is-none-of" || !field.presets?.length) return void 0;
112
114
  const chosen = new Set(value.values);
113
115
  return field.presets.find((preset) => {
114
116
  const wanted = new Set(preset.values);
@@ -131,7 +133,10 @@ function summarizeFilter(field, input, options = {}) {
131
133
  const preset = matchFilterPreset(field, value);
132
134
  if (preset) return fill(labels.preset ?? DEFAULT_FILTER_SUMMARY_LABELS.preset, { label: preset.label, field: field.label });
133
135
  const byValue = new Map((field.options ?? []).map((o) => [o.value, o.label]));
134
- return `${field.label}: ${list(value.values.map((v) => byValue.get(v) ?? v))}`;
136
+ const named = list(value.values.map((v) => byValue.get(v) ?? v));
137
+ const condition = value.operator ?? (field.enumOperators?.length === 1 ? field.enumOperators[0] : "is-any-of");
138
+ if (condition === "is-none-of") return `${field.label}: ${fill(labels.noneOf ?? DEFAULT_FILTER_SUMMARY_LABELS.noneOf, { values: named, field: field.label })}`;
139
+ return `${field.label}: ${named}`;
135
140
  }
136
141
  case "reference":
137
142
  return `${field.label}: ${list(value.values.map((v) => v.label))}`;
@@ -155,6 +160,7 @@ function summarizeFilter(field, input, options = {}) {
155
160
  export {
156
161
  DATE_FILTER_OPERATORS,
157
162
  DEFAULT_FILTER_SUMMARY_LABELS,
163
+ ENUM_FILTER_OPERATORS,
158
164
  NUMBER_FILTER_OPERATORS,
159
165
  TEXT_FILTER_OPERATORS,
160
166
  dateFilterBounds,