@ai-matrx/design-system 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/data-table/copy-helpers.d.cts +2 -1
  3. package/dist/data-table/copy-helpers.d.ts +2 -1
  4. package/dist/data-table/data-row-inspector.cjs.map +1 -1
  5. package/dist/data-table/data-row-inspector.js.map +1 -1
  6. package/dist/data-table/data-row-window.cjs.map +1 -1
  7. package/dist/data-table/data-row-window.js.map +1 -1
  8. package/dist/data-table/facets.cjs +118 -0
  9. package/dist/data-table/facets.cjs.map +1 -0
  10. package/dist/data-table/facets.d.cts +100 -0
  11. package/dist/data-table/facets.d.ts +100 -0
  12. package/dist/data-table/facets.js +98 -0
  13. package/dist/data-table/facets.js.map +1 -0
  14. package/dist/data-table/filter-engine.d.cts +2 -1
  15. package/dist/data-table/filter-engine.d.ts +2 -1
  16. package/dist/data-table/host.cjs.map +1 -1
  17. package/dist/data-table/host.d.cts +13 -3
  18. package/dist/data-table/host.d.ts +13 -3
  19. package/dist/data-table/host.js.map +1 -1
  20. package/dist/data-table/index.cjs +680 -145
  21. package/dist/data-table/index.cjs.map +1 -1
  22. package/dist/data-table/index.d.cts +4 -2
  23. package/dist/data-table/index.d.ts +4 -2
  24. package/dist/data-table/index.js +718 -182
  25. package/dist/data-table/index.js.map +1 -1
  26. package/dist/data-table/infer-filter.d.cts +2 -1
  27. package/dist/data-table/infer-filter.d.ts +2 -1
  28. package/dist/data-table/layered-filters.d.cts +2 -1
  29. package/dist/data-table/layered-filters.d.ts +2 -1
  30. package/dist/data-table/menu-targets.cjs +77 -0
  31. package/dist/data-table/menu-targets.cjs.map +1 -0
  32. package/dist/data-table/menu-targets.d.cts +108 -0
  33. package/dist/data-table/menu-targets.d.ts +108 -0
  34. package/dist/data-table/menu-targets.js +57 -0
  35. package/dist/data-table/menu-targets.js.map +1 -0
  36. package/dist/data-table/pagination.cjs.map +1 -1
  37. package/dist/data-table/pagination.js.map +1 -1
  38. package/dist/data-table/query-control.d.cts +2 -1
  39. package/dist/data-table/query-control.d.ts +2 -1
  40. package/dist/data-table/spreadsheet.cjs +61 -13
  41. package/dist/data-table/spreadsheet.cjs.map +1 -1
  42. package/dist/data-table/spreadsheet.d.cts +36 -1
  43. package/dist/data-table/spreadsheet.d.ts +36 -1
  44. package/dist/data-table/spreadsheet.js +61 -13
  45. package/dist/data-table/spreadsheet.js.map +1 -1
  46. package/dist/data-table/types.cjs.map +1 -1
  47. package/dist/data-table/types.d.cts +2 -1
  48. package/dist/data-table/types.d.ts +2 -1
  49. package/dist/data-table/url-state.d.cts +2 -1
  50. package/dist/data-table/url-state.d.ts +2 -1
  51. package/dist/data-table/uuid-cell.cjs.map +1 -1
  52. package/dist/data-table/uuid-cell.js.map +1 -1
  53. package/dist/data-table/xlsx.d.cts +2 -1
  54. package/dist/data-table/xlsx.d.ts +2 -1
  55. package/dist/{types-nlLCMiqd.d.ts → types-Cu5_eUG1.d.ts} +115 -3
  56. package/dist/{types-CmQrOz-T.d.cts → types-DrJXAAoD.d.cts} +115 -3
  57. package/package.json +21 -1
@@ -10,7 +10,7 @@ import {
10
10
  useId as useId2,
11
11
  useLayoutEffect as useLayoutEffect2,
12
12
  useMemo as useMemo8,
13
- useRef as useRef14,
13
+ useRef as useRef15,
14
14
  useState as useState22
15
15
  } from "react";
16
16
  import {
@@ -5139,10 +5139,10 @@ function passesColumnFilter(value, filter) {
5139
5139
  if (raw === "") return false;
5140
5140
  const normalized = raw.replace(/[^0-9.-]/g, "");
5141
5141
  if (normalized === "") return false;
5142
- const n = typeof value === "number" ? value : Number(normalized);
5143
- if (!Number.isFinite(n)) return false;
5144
- if (filter.min !== void 0 && n < filter.min) return false;
5145
- if (filter.max !== void 0 && n > filter.max) return false;
5142
+ const n2 = typeof value === "number" ? value : Number(normalized);
5143
+ if (!Number.isFinite(n2)) return false;
5144
+ if (filter.min !== void 0 && n2 < filter.min) return false;
5145
+ if (filter.max !== void 0 && n2 > filter.max) return false;
5146
5146
  return true;
5147
5147
  }
5148
5148
  }
@@ -5205,26 +5205,26 @@ function filterAndSortRows(rows, columns, columnFilters, sort, globalSearch, any
5205
5205
  return result;
5206
5206
  }
5207
5207
  function countActiveColumnFilters(filters) {
5208
- let n = 0;
5208
+ let n2 = 0;
5209
5209
  for (const f of Object.values(filters)) {
5210
5210
  if (!f) continue;
5211
5211
  switch (f.kind) {
5212
5212
  case "text":
5213
- if (f.value.trim() || f.mode && f.mode !== "contains") n += 1;
5213
+ if (f.value.trim() || f.mode && f.mode !== "contains") n2 += 1;
5214
5214
  break;
5215
5215
  case "select":
5216
5216
  if (f.values !== void 0 || f.value && f.value !== "__all__")
5217
- n += 1;
5217
+ n2 += 1;
5218
5218
  break;
5219
5219
  case "boolean":
5220
- n += 1;
5220
+ n2 += 1;
5221
5221
  break;
5222
5222
  case "number":
5223
- if (f.min !== void 0 || f.max !== void 0) n += 1;
5223
+ if (f.min !== void 0 || f.max !== void 0) n2 += 1;
5224
5224
  break;
5225
5225
  }
5226
5226
  }
5227
- return n;
5227
+ return n2;
5228
5228
  }
5229
5229
  function isColumnFilterActive(filter) {
5230
5230
  if (!filter) return false;
@@ -5506,8 +5506,8 @@ function parseLayeredFilters(raw) {
5506
5506
  }
5507
5507
  }
5508
5508
  function positiveInt(raw, fallback) {
5509
- const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
5510
- return Number.isFinite(n) && n > 0 ? n : fallback;
5509
+ const n2 = raw ? Number.parseInt(raw, 10) : Number.NaN;
5510
+ return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
5511
5511
  }
5512
5512
  function tableUrlParamPrefix(tableId) {
5513
5513
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(tableId)) {
@@ -5708,6 +5708,7 @@ import {
5708
5708
  forwardRef as forwardRef12,
5709
5709
  useEffect as useEffect9,
5710
5710
  useMemo as useMemo5,
5711
+ useRef as useRef10,
5711
5712
  useState as useState14
5712
5713
  } from "react";
5713
5714
  import { useDraggable } from "@dnd-kit/core";
@@ -5855,6 +5856,95 @@ var CommandShortcut = ({
5855
5856
  };
5856
5857
  CommandShortcut.displayName = "CommandShortcut";
5857
5858
 
5859
+ // src/data-table/facets.ts
5860
+ var MAX_LISTABLE_FACET_LENGTH = 300;
5861
+ var DEFAULT_FACET_LIMIT = 200;
5862
+ var MAX_FACET_LIMIT = 500;
5863
+ function facetText(raw) {
5864
+ if (raw === null || raw === void 0) return "";
5865
+ if (typeof raw === "object") {
5866
+ try {
5867
+ return JSON.stringify(raw);
5868
+ } catch {
5869
+ return String(raw);
5870
+ }
5871
+ }
5872
+ return String(raw);
5873
+ }
5874
+ function computeColumnFacets(args) {
5875
+ const { columnId: columnId2, rows, readValue: readValue2 } = args;
5876
+ const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);
5877
+ const counts = /* @__PURE__ */ new Map();
5878
+ const longValues = /* @__PURE__ */ new Set();
5879
+ let blank = 0;
5880
+ let maxLength = 0;
5881
+ for (const row of rows) {
5882
+ let raw;
5883
+ try {
5884
+ raw = readValue2(row, columnId2);
5885
+ } catch {
5886
+ raw = void 0;
5887
+ }
5888
+ const trimmed = facetText(raw).trim();
5889
+ if (trimmed === "") {
5890
+ blank += 1;
5891
+ continue;
5892
+ }
5893
+ if (trimmed.length > maxLength) maxLength = trimmed.length;
5894
+ if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {
5895
+ longValues.add(trimmed);
5896
+ continue;
5897
+ }
5898
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
5899
+ }
5900
+ const values = [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, limit);
5901
+ return {
5902
+ columnId: columnId2,
5903
+ totalRows: rows.length,
5904
+ filled: rows.length - blank,
5905
+ blank,
5906
+ distinctCount: counts.size + longValues.size,
5907
+ maxLength,
5908
+ unlistable: longValues.size,
5909
+ limit,
5910
+ truncated: counts.size > limit,
5911
+ values,
5912
+ answeredBy: "local",
5913
+ complete: args.complete ?? true
5914
+ };
5915
+ }
5916
+ function localFacetsAreComplete(loadedRowCount, total) {
5917
+ if (total === void 0 || !Number.isFinite(total) || total < 0) return false;
5918
+ return loadedRowCount >= total;
5919
+ }
5920
+ var NUMBERS = new Intl.NumberFormat("en-US");
5921
+ function n(value) {
5922
+ try {
5923
+ return NUMBERS.format(value);
5924
+ } catch {
5925
+ return String(value);
5926
+ }
5927
+ }
5928
+ function describeFacetSource(facets, noun = "row") {
5929
+ if (!facets) return "";
5930
+ const plural2 = `${noun}s`;
5931
+ if (!facets.complete) {
5932
+ return `These counts cover the ${n(facets.totalRows)} ${plural2} loaded here, not the whole table.`;
5933
+ }
5934
+ if (facets.truncated) {
5935
+ return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;
5936
+ }
5937
+ if (facets.unlistable > 0) {
5938
+ return `${n(facets.unlistable)} ${facets.unlistable === 1 ? "value is" : "values are"} too long to list. Match text instead to reach ${facets.unlistable === 1 ? "it" : "them"}.`;
5939
+ }
5940
+ return "";
5941
+ }
5942
+ var FACETS_LOADING_NOTICE = "Counting values\u2026";
5943
+ function describeFacetFailure(error) {
5944
+ const detail = error instanceof Error ? error.message : String(error ?? "");
5945
+ return detail ? `The values in this column could not be counted: ${detail} Match text instead.` : "The values in this column could not be counted. Match text instead.";
5946
+ }
5947
+
5858
5948
  // src/data-table/column-marker.ts
5859
5949
  var MARKER_ACCESSORS = {
5860
5950
  favorite: "favorite",
@@ -5922,6 +6012,10 @@ function ColumnHeaderCell({
5922
6012
  filterFooterHint,
5923
6013
  allowNegation = false,
5924
6014
  allowNullModes = false,
6015
+ facets,
6016
+ facetsLoading = false,
6017
+ facetsError,
6018
+ onFilterOpen,
5925
6019
  reorderColumn
5926
6020
  }) {
5927
6021
  const reorderEnabled = reorderColumn !== void 0;
@@ -5956,6 +6050,11 @@ function ColumnHeaderCell({
5956
6050
  setPendingNegation(filterValue?.negated ?? false);
5957
6051
  }
5958
6052
  }, [filterOpen, filterValue]);
6053
+ const notifyOpen = useRef10(onFilterOpen);
6054
+ notifyOpen.current = onFilterOpen;
6055
+ useEffect9(() => {
6056
+ if (filterOpen) notifyOpen.current?.();
6057
+ }, [filterOpen]);
5959
6058
  const applyFilter = () => {
5960
6059
  onFilterChange(draftFilter);
5961
6060
  setFilterOpen(false);
@@ -6050,6 +6149,9 @@ function ColumnHeaderCell({
6050
6149
  selectOptions,
6051
6150
  selectSingle,
6052
6151
  selectAllowCustom,
6152
+ facets: facets ?? null,
6153
+ facetsLoading,
6154
+ ...facetsError === void 0 ? {} : { facetsError },
6053
6155
  ...markerLabels ? {
6054
6156
  booleanLabels: {
6055
6157
  all: "All",
@@ -6274,8 +6376,43 @@ function FilterBody({
6274
6376
  selectSingle = false,
6275
6377
  selectAllowCustom = false,
6276
6378
  allowNullModes = false,
6277
- booleanLabels
6379
+ booleanLabels,
6380
+ facets = null,
6381
+ facetsLoading = false,
6382
+ facetsError
6278
6383
  }) {
6384
+ const checklist = facetsLoading || facetsError || facets && (facets.values.length > 0 || facets.blank > 0) ? /* @__PURE__ */ jsx31(
6385
+ FacetChecklist,
6386
+ {
6387
+ facets,
6388
+ loading: facetsLoading,
6389
+ ...facetsError === void 0 ? {} : { error: facetsError },
6390
+ value,
6391
+ onChange
6392
+ }
6393
+ ) : null;
6394
+ if (checklist && kind === "select" && facets && facets.values.length > 0) {
6395
+ return /* @__PURE__ */ jsx31("div", { className: "space-y-2", children: checklist });
6396
+ }
6397
+ if (checklist) {
6398
+ return /* @__PURE__ */ jsxs22("div", { className: "space-y-2", children: [
6399
+ checklist,
6400
+ /* @__PURE__ */ jsx31("div", { className: "h-px bg-border" }),
6401
+ /* @__PURE__ */ jsx31(
6402
+ FilterBody,
6403
+ {
6404
+ kind,
6405
+ value,
6406
+ onChange,
6407
+ selectOptions,
6408
+ selectSingle,
6409
+ selectAllowCustom,
6410
+ allowNullModes,
6411
+ ...booleanLabels === void 0 ? {} : { booleanLabels }
6412
+ }
6413
+ )
6414
+ ] });
6415
+ }
6279
6416
  if (kind === "text") {
6280
6417
  const text = value?.kind === "text" ? value.value : "";
6281
6418
  const mode = (value?.kind === "text" ? value.mode : void 0) ?? "contains";
@@ -6386,6 +6523,112 @@ function FilterBody({
6386
6523
  }
6387
6524
  return /* @__PURE__ */ jsx31(NumberFilterBody, { value, onChange });
6388
6525
  }
6526
+ function FacetChecklist({
6527
+ facets,
6528
+ loading,
6529
+ error,
6530
+ value,
6531
+ onChange
6532
+ }) {
6533
+ const [query, setQuery] = useState14("");
6534
+ const selected = value?.kind === "select" ? value.values ?? (value.value && value.value !== "__all__" ? [value.value] : []) : [];
6535
+ const selectedSet = new Set(selected);
6536
+ const rows = useMemo5(() => {
6537
+ if (!facets) return [];
6538
+ const listed = facets.values.map((entry) => ({
6539
+ value: entry.value,
6540
+ label: entry.value,
6541
+ count: entry.count
6542
+ }));
6543
+ const withBlank = facets.blank > 0 ? [{ value: "", label: "(empty)", count: facets.blank }, ...listed] : listed;
6544
+ const q = query.trim().toLowerCase();
6545
+ if (!q) return withBlank;
6546
+ return withBlank.filter((row) => row.label.toLowerCase().includes(q));
6547
+ }, [facets, query]);
6548
+ const toggle = (candidate) => {
6549
+ const next = new Set(selectedSet);
6550
+ if (next.has(candidate)) next.delete(candidate);
6551
+ else next.add(candidate);
6552
+ const values = [...next];
6553
+ onChange(
6554
+ values.length === 0 ? void 0 : { kind: "select", value: values[0] ?? "", values }
6555
+ );
6556
+ };
6557
+ const notice = error ?? (loading ? FACETS_LOADING_NOTICE : describeFacetSource(facets));
6558
+ return /* @__PURE__ */ jsxs22("div", { className: "space-y-1.5", children: [
6559
+ /* @__PURE__ */ jsx31("p", { className: "px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: "Values" }),
6560
+ facets && facets.values.length + (facets.blank > 0 ? 1 : 0) > 8 ? /* @__PURE__ */ jsx31(
6561
+ Input,
6562
+ {
6563
+ "data-matrx-filter-autofocus": true,
6564
+ value: query,
6565
+ onChange: (event) => setQuery(event.target.value),
6566
+ placeholder: "Find a value\u2026",
6567
+ className: "h-7 text-sm",
6568
+ style: { fontSize: "16px" }
6569
+ }
6570
+ ) : null,
6571
+ rows.length > 0 ? /* @__PURE__ */ jsx31("ul", { className: "max-h-56 space-y-0.5 overflow-y-auto", "data-matrx-facet-list": "true", children: rows.map((row) => {
6572
+ const checked = selectedSet.has(row.value);
6573
+ return /* @__PURE__ */ jsx31("li", { children: /* @__PURE__ */ jsxs22(
6574
+ "button",
6575
+ {
6576
+ type: "button",
6577
+ role: "checkbox",
6578
+ "aria-checked": checked,
6579
+ "data-matrx-facet-value": row.value === "" ? "__blank__" : row.value,
6580
+ onClick: () => toggle(row.value),
6581
+ className: cn(
6582
+ "flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-xs hover:bg-muted",
6583
+ checked && "bg-primary/10"
6584
+ ),
6585
+ children: [
6586
+ /* @__PURE__ */ jsx31(
6587
+ "span",
6588
+ {
6589
+ "aria-hidden": true,
6590
+ className: cn(
6591
+ "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-[3px] border",
6592
+ checked ? "border-primary bg-primary text-primary-foreground" : "border-border"
6593
+ ),
6594
+ children: checked ? /* @__PURE__ */ jsx31(Check, { className: "h-2.5 w-2.5" }) : null
6595
+ }
6596
+ ),
6597
+ /* @__PURE__ */ jsx31("span", { className: cn("min-w-0 flex-1 truncate", row.value === "" && "italic text-muted-foreground"), children: row.label }),
6598
+ /* @__PURE__ */ jsx31(
6599
+ "span",
6600
+ {
6601
+ "data-matrx-facet-count": String(row.count),
6602
+ className: "shrink-0 tabular-nums text-[11px] text-muted-foreground",
6603
+ children: row.count.toLocaleString("en-US")
6604
+ }
6605
+ )
6606
+ ]
6607
+ }
6608
+ ) }, row.value === "" ? "__blank__" : row.value);
6609
+ }) }) : loading ? null : /* @__PURE__ */ jsx31("p", { className: "px-1 text-[11px] text-muted-foreground", children: query.trim() ? "No value matches that." : "This column has no values yet." }),
6610
+ notice ? /* @__PURE__ */ jsx31(
6611
+ "p",
6612
+ {
6613
+ "data-matrx-facet-notice": "true",
6614
+ className: cn(
6615
+ "px-1 text-[11px] leading-snug",
6616
+ error ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground"
6617
+ ),
6618
+ children: notice
6619
+ }
6620
+ ) : null,
6621
+ selected.length > 0 ? /* @__PURE__ */ jsx31(
6622
+ "button",
6623
+ {
6624
+ type: "button",
6625
+ onClick: () => onChange(void 0),
6626
+ className: "px-1 text-[11px] text-muted-foreground underline-offset-2 hover:underline",
6627
+ children: "Clear selection"
6628
+ }
6629
+ ) : null
6630
+ ] });
6631
+ }
6389
6632
  function SearchableSelectFilter({
6390
6633
  selected,
6391
6634
  options,
@@ -6572,15 +6815,15 @@ function NumberFilterBody({
6572
6815
  }
6573
6816
  function parseOptionalNumber(raw) {
6574
6817
  if (raw.trim() === "") return void 0;
6575
- const n = Number(raw.replace(/[^0-9.-]/g, ""));
6576
- return Number.isFinite(n) ? n : void 0;
6818
+ const n2 = Number(raw.replace(/[^0-9.-]/g, ""));
6819
+ return Number.isFinite(n2) ? n2 : void 0;
6577
6820
  }
6578
6821
 
6579
6822
  // src/data-table/ColumnResizeHandle.tsx
6580
6823
  import {
6581
6824
  useCallback as useCallback6,
6582
6825
  useEffect as useEffect10,
6583
- useRef as useRef10,
6826
+ useRef as useRef11,
6584
6827
  useState as useState15
6585
6828
  } from "react";
6586
6829
 
@@ -6624,11 +6867,11 @@ function ColumnResizeHandle({
6624
6867
  onResizeCancel,
6625
6868
  onResizeCommit
6626
6869
  }) {
6627
- const handleRef = useRef10(null);
6628
- const activeResizeRef = useRef10(null);
6629
- const previewRafRef = useRef10(null);
6630
- const queuedWidthRef = useRef10(null);
6631
- const keyboardWidthRef = useRef10(null);
6870
+ const handleRef = useRef11(null);
6871
+ const activeResizeRef = useRef11(null);
6872
+ const previewRafRef = useRef11(null);
6873
+ const queuedWidthRef = useRef11(null);
6874
+ const keyboardWidthRef = useRef11(null);
6632
6875
  const [announcedWidth, setAnnouncedWidth] = useState15(null);
6633
6876
  const bounds = resolveColumnResizeBounds(minWidth, maxWidth);
6634
6877
  const cssVar = columnResizeCssVar(columnId2);
@@ -6890,16 +7133,16 @@ function computeAggregate(rows, column) {
6890
7133
  value = total;
6891
7134
  break;
6892
7135
  case "sum":
6893
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0);
7136
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0);
6894
7137
  break;
6895
7138
  case "average":
6896
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0) / numbers.length;
7139
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0) / numbers.length;
6897
7140
  break;
6898
7141
  case "min":
6899
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n < acc ? n : acc);
7142
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 < acc ? n2 : acc);
6900
7143
  break;
6901
7144
  case "max":
6902
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n > acc ? n : acc);
7145
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 > acc ? n2 : acc);
6903
7146
  break;
6904
7147
  case "empty":
6905
7148
  value = total - filled.length;
@@ -7217,7 +7460,7 @@ function scrollOffsetForIndex(input) {
7217
7460
  }
7218
7461
 
7219
7462
  // src/data-table/useSpreadsheetGrid.ts
7220
- import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo6, useRef as useRef11, useState as useState16 } from "react";
7463
+ import { useCallback as useCallback7, useEffect as useEffect11, useMemo as useMemo6, useRef as useRef12, useState as useState16 } from "react";
7221
7464
 
7222
7465
  // src/data-table/grid-selection.ts
7223
7466
  function rangeBounds(range, rowIds, fieldNames) {
@@ -7323,7 +7566,7 @@ function moveSelection(current, move, rowIds, fieldNames) {
7323
7566
  if (r === -1 || c === -1) return first;
7324
7567
  const lastRow = rowIds.length - 1;
7325
7568
  const lastCol = fieldNames.length - 1;
7326
- const clamp = (n, max) => Math.min(Math.max(n, 0), max);
7569
+ const clamp = (n2, max) => Math.min(Math.max(n2, 0), max);
7327
7570
  switch (move) {
7328
7571
  case "up":
7329
7572
  r = clamp(r - 1, lastRow);
@@ -7455,14 +7698,14 @@ function parseClipboardGrid(text) {
7455
7698
  let row = [];
7456
7699
  let cell = "";
7457
7700
  let i = 0;
7458
- const n = text.length;
7459
- while (i < n) {
7701
+ const n2 = text.length;
7702
+ while (i < n2) {
7460
7703
  const ch = text[i];
7461
7704
  if (cell === "" && ch === '"') {
7462
7705
  let j = i + 1;
7463
7706
  let out = "";
7464
7707
  let closed = false;
7465
- while (j < n) {
7708
+ while (j < n2) {
7466
7709
  const c = text[j];
7467
7710
  if (c === '"') {
7468
7711
  if (text[j + 1] === '"') {
@@ -7477,10 +7720,10 @@ function parseClipboardGrid(text) {
7477
7720
  out += c;
7478
7721
  j += 1;
7479
7722
  }
7480
- if (closed && (j >= n || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
7723
+ if (closed && (j >= n2 || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
7481
7724
  cell = out;
7482
7725
  i = j;
7483
- if (i >= n) break;
7726
+ if (i >= n2) break;
7484
7727
  const d = text[i];
7485
7728
  if (d === " ") {
7486
7729
  row.push(cell);
@@ -7516,7 +7759,7 @@ function parseClipboardGrid(text) {
7516
7759
  cell += ch;
7517
7760
  i += 1;
7518
7761
  }
7519
- const endedOnBreak = n > 0 && (text[n - 1] === "\n" || text[n - 1] === "\r");
7762
+ const endedOnBreak = n2 > 0 && (text[n2 - 1] === "\n" || text[n2 - 1] === "\r");
7520
7763
  if (!endedOnBreak) {
7521
7764
  row.push(cell);
7522
7765
  rows.push(row);
@@ -7595,11 +7838,11 @@ function directionOf(source, target) {
7595
7838
  return null;
7596
7839
  }
7597
7840
  function tileIndex(i, a, b) {
7598
- const n = b - a + 1;
7599
- if (n <= 0) return a;
7841
+ const n2 = b - a + 1;
7842
+ if (n2 <= 0) return a;
7600
7843
  if (i >= a && i <= b) return i;
7601
- if (i > b) return a + (i - a) % n;
7602
- return b - (a - 1 - i) % n;
7844
+ if (i > b) return a + (i - a) % n2;
7845
+ return b - (a - 1 - i) % n2;
7603
7846
  }
7604
7847
  function isFiniteNumber(v) {
7605
7848
  return typeof v === "number" && Number.isFinite(v);
@@ -7915,8 +8158,8 @@ function useSpreadsheetGrid(options) {
7915
8158
  const [range, setRange] = useState16(null);
7916
8159
  const [fillTarget, setFillTarget] = useState16(null);
7917
8160
  const [stack, setStack] = useState16(() => createUndoStack(options?.undoDepth ?? 100));
7918
- const draggingRef = useRef11(null);
7919
- const optionsRef = useRef11(options);
8161
+ const draggingRef = useRef12(null);
8162
+ const optionsRef = useRef12(options);
7920
8163
  optionsRef.current = options;
7921
8164
  const rowIds = options?.rowIds ?? [];
7922
8165
  const columnIds = useMemo6(
@@ -7944,6 +8187,26 @@ function useSpreadsheetGrid(options) {
7944
8187
  for (let i = 0; i < columnIds.length; i += 1) map.set(columnIds[i], i);
7945
8188
  return map;
7946
8189
  }, [columnIds]);
8190
+ const screenPatches = useCallback7(
8191
+ (patches) => {
8192
+ const validate = optionsRef.current?.validateCell;
8193
+ if (!validate) return { accepted: patches, refusals: [] };
8194
+ const accepted = [];
8195
+ const refusals = [];
8196
+ for (const patch of patches) {
8197
+ let reason;
8198
+ try {
8199
+ reason = validate({ rowId: patch.rowId, columnId: patch.columnId, value: patch.value });
8200
+ } catch {
8201
+ reason = void 0;
8202
+ }
8203
+ if (reason) refusals.push({ columnId: patch.columnId, reason });
8204
+ else accepted.push(patch);
8205
+ }
8206
+ return { accepted, refusals };
8207
+ },
8208
+ []
8209
+ );
7947
8210
  const applyPatches = useCallback7(
7948
8211
  async (patches, intent, label) => {
7949
8212
  const current = optionsRef.current;
@@ -8000,9 +8263,10 @@ function useSpreadsheetGrid(options) {
8000
8263
  ...base,
8001
8264
  range: { top: bounds.r0, left: bounds.c0, bottom: bounds.r1, right: bounds.c1 }
8002
8265
  });
8003
- await applyPatches(result.patches, "fill-down", "Filled down");
8004
- announceSkips(result.skipped, result.summary);
8005
- }, [fillRequestBase, bounds, applyPatches, announceSkips]);
8266
+ const screened = screenPatches(result.patches);
8267
+ await applyPatches(screened.accepted, "fill-down", "Filled down");
8268
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8269
+ }, [fillRequestBase, bounds, applyPatches, announceSkips, screenPatches]);
8006
8270
  const doClear = useCallback7(async () => {
8007
8271
  const current = optionsRef.current;
8008
8272
  if (!current || !bounds) return;
@@ -8023,9 +8287,13 @@ function useSpreadsheetGrid(options) {
8023
8287
  patches.push({ rowId, columnId: columnId2, value: null });
8024
8288
  }
8025
8289
  }
8026
- await applyPatches(patches, "clear", "Cleared");
8027
- announceSkips(skipped, `Cleared ${patches.length} cells.`);
8028
- }, [bounds, applyPatches, announceSkips]);
8290
+ const screened = screenPatches(patches);
8291
+ await applyPatches(screened.accepted, "clear", "Cleared");
8292
+ announceSkips(
8293
+ [...skipped, ...screened.refusals],
8294
+ `Cleared ${screened.accepted.length} cells.`
8295
+ );
8296
+ }, [bounds, applyPatches, announceSkips, screenPatches]);
8029
8297
  const doUndo = useCallback7(async () => {
8030
8298
  const taken = popUndo(stack);
8031
8299
  if (!taken.entry) return;
@@ -8110,6 +8378,14 @@ function useSpreadsheetGrid(options) {
8110
8378
  case "escape":
8111
8379
  setRange(null);
8112
8380
  return;
8381
+ case "edit":
8382
+ case "editSeeded": {
8383
+ const focused = range?.focus;
8384
+ if (!focused || !current.onEditRequest) return;
8385
+ const seed = action.kind === "editSeeded" ? action.seed : null;
8386
+ if (current.onEditRequest(focused, seed)) event.preventDefault();
8387
+ return;
8388
+ }
8113
8389
  default:
8114
8390
  return;
8115
8391
  }
@@ -8124,6 +8400,13 @@ function useSpreadsheetGrid(options) {
8124
8400
  );
8125
8401
  return gridToTsv(grid);
8126
8402
  }, [range, rowIds, columnIds]);
8403
+ const selectionAddresses = useCallback7(() => {
8404
+ if (!range || !bounds) return { rowIds: [], columnIds: [] };
8405
+ return {
8406
+ rowIds: rowIds.slice(bounds.r0, bounds.r1 + 1),
8407
+ columnIds: columnIds.slice(bounds.c0, bounds.c1 + 1)
8408
+ };
8409
+ }, [range, bounds, rowIds, columnIds]);
8127
8410
  const onCopy = useCallback7(
8128
8411
  (event) => {
8129
8412
  if (!optionsRef.current || !range || targetsAnEditor(event.target)) return;
@@ -8179,11 +8462,12 @@ function useSpreadsheetGrid(options) {
8179
8462
  patches.push({ rowId, columnId: columnId2, value: line[c] ?? null });
8180
8463
  }
8181
8464
  }
8182
- void applyPatches(patches, "paste", "Pasted");
8465
+ const screened = screenPatches(patches);
8466
+ void applyPatches(screened.accepted, "paste", "Pasted");
8183
8467
  const tail = outside > 0 ? ` ${outside} landed past the edge of the table and were ignored.` : "";
8184
- announceSkips(skipped, `Pasted ${patches.length} cells.${tail}`);
8468
+ announceSkips([...skipped, ...screened.refusals], `Pasted ${screened.accepted.length} cells.${tail}`);
8185
8469
  },
8186
- [range, rowIds, columnIds, applyPatches, announceSkips]
8470
+ [range, rowIds, columnIds, applyPatches, announceSkips, screenPatches]
8187
8471
  );
8188
8472
  const cellProps = useCallback7(
8189
8473
  (rowId, columnId2) => {
@@ -8202,6 +8486,10 @@ function useSpreadsheetGrid(options) {
8202
8486
  onMouseDown: (event) => {
8203
8487
  if (event.button !== 0) return;
8204
8488
  event.preventDefault();
8489
+ const origin = event.currentTarget;
8490
+ if (typeof HTMLElement !== "undefined" && origin instanceof HTMLElement) {
8491
+ origin.closest('[data-matrx-table-spreadsheet="true"]')?.focus({ preventScroll: true });
8492
+ }
8205
8493
  setRange(
8206
8494
  (previous) => event.shiftKey && previous ? { anchor: previous.anchor, focus: address } : { anchor: address, focus: address }
8207
8495
  );
@@ -8242,9 +8530,9 @@ function useSpreadsheetGrid(options) {
8242
8530
  },
8243
8531
  []
8244
8532
  );
8245
- const fillTargetRef = useRef11(null);
8533
+ const fillTargetRef = useRef12(null);
8246
8534
  fillTargetRef.current = fillTarget;
8247
- const boundsRef = useRef11(null);
8535
+ const boundsRef = useRef12(null);
8248
8536
  boundsRef.current = bounds;
8249
8537
  useEffect11(() => {
8250
8538
  if (!options) return;
@@ -8263,12 +8551,13 @@ function useSpreadsheetGrid(options) {
8263
8551
  source: { top: source.r0, left: source.c0, bottom: source.r1, right: source.c1 },
8264
8552
  target
8265
8553
  });
8266
- void applyPatches(result.patches, "fill", "Filled");
8267
- announceSkips(result.skipped, result.summary);
8554
+ const screened = screenPatches(result.patches);
8555
+ void applyPatches(screened.accepted, "fill", "Filled");
8556
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8268
8557
  };
8269
8558
  window.addEventListener("mouseup", commit);
8270
8559
  return () => window.removeEventListener("mouseup", commit);
8271
- }, [options, fillRequestBase, applyPatches, announceSkips]);
8560
+ }, [options, fillRequestBase, applyPatches, announceSkips, screenPatches]);
8272
8561
  return {
8273
8562
  range,
8274
8563
  bounds,
@@ -8284,17 +8573,19 @@ function useSpreadsheetGrid(options) {
8284
8573
  isFillHandleCell,
8285
8574
  startFillDrag,
8286
8575
  canUndo: stack.past.length > 0,
8287
- canRedo: stack.future.length > 0
8576
+ canRedo: stack.future.length > 0,
8577
+ selectionTsv: selectionAsTsv,
8578
+ selectionAddresses
8288
8579
  };
8289
8580
  }
8290
8581
 
8291
8582
  // src/data-table/coverage.ts
8292
8583
  var PARTIAL_BADGE = "Partial";
8293
8584
  var UNKNOWN_TOTAL_BADGE = "Unknown total";
8294
- var NUMBERS = new Intl.NumberFormat("en-US");
8585
+ var NUMBERS2 = new Intl.NumberFormat("en-US");
8295
8586
  function formatNumber(value) {
8296
8587
  try {
8297
- return NUMBERS.format(value);
8588
+ return NUMBERS2.format(value);
8298
8589
  } catch {
8299
8590
  return String(value);
8300
8591
  }
@@ -8987,7 +9278,7 @@ function parseTableViewSnapshot(raw) {
8987
9278
  // src/data-table/MatrxUuidCell.tsx
8988
9279
  import {
8989
9280
  useEffect as useEffect13,
8990
- useRef as useRef12,
9281
+ useRef as useRef13,
8991
9282
  useState as useState18
8992
9283
  } from "react";
8993
9284
  import { jsx as jsx34, jsxs as jsxs24 } from "react/jsx-runtime";
@@ -9032,7 +9323,7 @@ function MatrxUuidCell({
9032
9323
  const [copied, setCopied] = useState18(false);
9033
9324
  const [opening, setOpening] = useState18(false);
9034
9325
  const [peekOpen, setPeekOpen] = useState18(false);
9035
- const timer = useRef12(null);
9326
+ const timer = useRef13(null);
9036
9327
  useEffect13(
9037
9328
  () => () => {
9038
9329
  if (timer.current) clearTimeout(timer.current);
@@ -9500,8 +9791,8 @@ function toNumber(value) {
9500
9791
  if (typeof value === "string") {
9501
9792
  const cleaned = value.replace(/[,\s$€£¥%]/g, "");
9502
9793
  if (cleaned === "") return null;
9503
- const n = Number(cleaned);
9504
- return Number.isFinite(n) ? n : null;
9794
+ const n2 = Number(cleaned);
9795
+ return Number.isFinite(n2) ? n2 : null;
9505
9796
  }
9506
9797
  return null;
9507
9798
  }
@@ -9539,14 +9830,14 @@ function toList(value) {
9539
9830
  }
9540
9831
  return null;
9541
9832
  }
9542
- function groupedNumber(n, options) {
9833
+ function groupedNumber(n2, options) {
9543
9834
  return new Intl.NumberFormat(void 0, {
9544
9835
  useGrouping: options.useGrouping !== false,
9545
9836
  ...options.precision != null ? {
9546
9837
  minimumFractionDigits: options.precision,
9547
9838
  maximumFractionDigits: options.precision
9548
9839
  } : { maximumFractionDigits: 10 }
9549
- }).format(n);
9840
+ }).format(n2);
9550
9841
  }
9551
9842
  function affix(text, options) {
9552
9843
  return `${options.prefix ?? ""}${text}${options.suffix ?? ""}`;
@@ -9680,8 +9971,8 @@ var DEFS = [
9680
9971
  numericAlign: true,
9681
9972
  optionKeys: ["useGrouping", "precision", "prefix", "suffix"],
9682
9973
  format: (v, o) => {
9683
- const n = toNumber(v);
9684
- return n === null ? null : affix(groupedNumber(n, o), o);
9974
+ const n2 = toNumber(v);
9975
+ return n2 === null ? null : affix(groupedNumber(n2, o), o);
9685
9976
  },
9686
9977
  parse: (raw) => toNumber(raw)
9687
9978
  },
@@ -9696,9 +9987,9 @@ var DEFS = [
9696
9987
  numericAlign: true,
9697
9988
  optionKeys: ["precision", "useGrouping", "prefix", "suffix"],
9698
9989
  format: (v, o) => {
9699
- const n = toNumber(v);
9700
- if (n === null) return null;
9701
- return affix(groupedNumber(n, { ...o, precision: o.precision ?? 2 }), o);
9990
+ const n2 = toNumber(v);
9991
+ if (n2 === null) return null;
9992
+ return affix(groupedNumber(n2, { ...o, precision: o.precision ?? 2 }), o);
9702
9993
  },
9703
9994
  parse: (raw) => toNumber(raw)
9704
9995
  },
@@ -9713,8 +10004,8 @@ var DEFS = [
9713
10004
  numericAlign: true,
9714
10005
  optionKeys: ["currency", "precision"],
9715
10006
  format: (v, o) => {
9716
- const n = toNumber(v);
9717
- if (n === null) return null;
10007
+ const n2 = toNumber(v);
10008
+ if (n2 === null) return null;
9718
10009
  const currency = (o.currency || "USD").toUpperCase();
9719
10010
  try {
9720
10011
  return new Intl.NumberFormat(void 0, {
@@ -9724,9 +10015,9 @@ var DEFS = [
9724
10015
  minimumFractionDigits: o.precision,
9725
10016
  maximumFractionDigits: o.precision
9726
10017
  } : {}
9727
- }).format(n);
10018
+ }).format(n2);
9728
10019
  } catch {
9729
- return `${groupedNumber(n, { precision: o.precision ?? 2 })} ${currency}`;
10020
+ return `${groupedNumber(n2, { precision: o.precision ?? 2 })} ${currency}`;
9730
10021
  }
9731
10022
  },
9732
10023
  parse: (raw) => toNumber(raw)
@@ -9742,9 +10033,9 @@ var DEFS = [
9742
10033
  numericAlign: true,
9743
10034
  optionKeys: ["percentScale", "precision"],
9744
10035
  format: (v, o) => {
9745
- const n = toNumber(v);
9746
- if (n === null) return null;
9747
- const shown = o.percentScale === "fraction" ? n * 100 : n;
10036
+ const n2 = toNumber(v);
10037
+ if (n2 === null) return null;
10038
+ const shown = o.percentScale === "fraction" ? n2 * 100 : n2;
9748
10039
  return `${groupedNumber(shown, o.precision === void 0 ? {} : { precision: o.precision })}%`;
9749
10040
  },
9750
10041
  parse: (raw) => toNumber(raw)
@@ -9760,9 +10051,9 @@ var DEFS = [
9760
10051
  numericAlign: true,
9761
10052
  optionKeys: ["durationUnit"],
9762
10053
  format: (v, o) => {
9763
- const n = toNumber(v);
9764
- if (n === null) return null;
9765
- return formatSignedDuration(n * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
10054
+ const n2 = toNumber(v);
10055
+ if (n2 === null) return null;
10056
+ return formatSignedDuration(n2 * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
9766
10057
  },
9767
10058
  parse: (raw) => toNumber(raw)
9768
10059
  },
@@ -9777,13 +10068,13 @@ var DEFS = [
9777
10068
  numericAlign: true,
9778
10069
  optionKeys: ["useGrouping", "prefix", "suffix"],
9779
10070
  format: (v, o) => {
9780
- const n = toNumber(v);
9781
- if (n === null) return null;
9782
- return affix(groupedNumber(Math.trunc(n), { ...o, precision: 0 }), o);
10071
+ const n2 = toNumber(v);
10072
+ if (n2 === null) return null;
10073
+ return affix(groupedNumber(Math.trunc(n2), { ...o, precision: 0 }), o);
9783
10074
  },
9784
10075
  parse: (raw) => {
9785
- const n = toNumber(raw);
9786
- return n === null ? null : Math.trunc(n);
10076
+ const n2 = toNumber(raw);
10077
+ return n2 === null ? null : Math.trunc(n2);
9787
10078
  }
9788
10079
  },
9789
10080
  {
@@ -9797,15 +10088,15 @@ var DEFS = [
9797
10088
  rich: true,
9798
10089
  optionKeys: ["ratingMax"],
9799
10090
  format: (v, o) => {
9800
- const n = toNumber(v);
9801
- if (n === null) return null;
10091
+ const n2 = toNumber(v);
10092
+ if (n2 === null) return null;
9802
10093
  const max = o.ratingMax ?? 5;
9803
- if (n < 0 || n > max) return null;
9804
- return `${n} / ${max}`;
10094
+ if (n2 < 0 || n2 > max) return null;
10095
+ return `${n2} / ${max}`;
9805
10096
  },
9806
10097
  parse: (raw) => {
9807
- const n = toNumber(raw);
9808
- return n === null ? null : Math.round(n);
10098
+ const n2 = toNumber(raw);
10099
+ return n2 === null ? null : Math.round(n2);
9809
10100
  }
9810
10101
  },
9811
10102
  {
@@ -9818,12 +10109,12 @@ var DEFS = [
9818
10109
  editor: "number",
9819
10110
  numericAlign: true,
9820
10111
  format: (v) => {
9821
- const n = toNumber(v);
9822
- return n === null ? null : formatFileSize(n);
10112
+ const n2 = toNumber(v);
10113
+ return n2 === null ? null : formatFileSize(n2);
9823
10114
  },
9824
10115
  parse: (raw) => {
9825
- const n = toNumber(raw);
9826
- return n === null ? null : Math.trunc(n);
10116
+ const n2 = toNumber(raw);
10117
+ return n2 === null ? null : Math.trunc(n2);
9827
10118
  }
9828
10119
  },
9829
10120
  // ── Choice ────────────────────────────────────────────────────────────────
@@ -10233,8 +10524,19 @@ function DirtySavePill({
10233
10524
  }
10234
10525
 
10235
10526
  // src/data-table/EditableTableCell.tsx
10236
- import { useEffect as useEffect15, useRef as useRef13, useState as useState20 } from "react";
10527
+ import { useEffect as useEffect15, useRef as useRef14, useState as useState20 } from "react";
10237
10528
  import { jsx as jsx39, jsxs as jsxs29 } from "react/jsx-runtime";
10529
+ function CellRefusal({ reason }) {
10530
+ return /* @__PURE__ */ jsx39(
10531
+ "p",
10532
+ {
10533
+ role: "alert",
10534
+ "data-matrx-cell-refusal": "true",
10535
+ className: "mt-1 text-[11px] leading-snug text-amber-700 dark:text-amber-400",
10536
+ children: reason
10537
+ }
10538
+ );
10539
+ }
10238
10540
  function CellEditShell({
10239
10541
  href,
10240
10542
  display,
@@ -10281,12 +10583,17 @@ function EditableTableCell({
10281
10583
  className,
10282
10584
  href,
10283
10585
  editTrigger = "click",
10284
- editRequest
10586
+ editRequest,
10587
+ editSeed
10285
10588
  }) {
10286
10589
  const [open, setOpen] = useState20(false);
10590
+ const [refusal, setRefusal] = useState20(null);
10287
10591
  useEffect15(() => {
10288
10592
  if (editRequest !== void 0) setOpen(true);
10289
10593
  }, [editRequest]);
10594
+ useEffect15(() => {
10595
+ if (!open) setRefusal(null);
10596
+ }, [open]);
10290
10597
  const usePencil = Boolean(href) || editTrigger === "pencil";
10291
10598
  if (editType === "string") {
10292
10599
  return /* @__PURE__ */ jsx39(
@@ -10299,7 +10606,8 @@ function EditableTableCell({
10299
10606
  ...className === void 0 ? {} : { className },
10300
10607
  ...href === void 0 ? {} : { href },
10301
10608
  usePencil,
10302
- ...editRequest === void 0 ? {} : { editRequest }
10609
+ ...editRequest === void 0 ? {} : { editRequest },
10610
+ ...editSeed === void 0 || editSeed === null ? {} : { editSeed }
10303
10611
  }
10304
10612
  );
10305
10613
  }
@@ -10344,25 +10652,33 @@ function EditableTableCell({
10344
10652
  ) }) }),
10345
10653
  /* @__PURE__ */ jsx39(TooltipContent, { children: "Edit value" })
10346
10654
  ] }),
10347
- /* @__PURE__ */ jsx39(
10655
+ /* @__PURE__ */ jsxs29(
10348
10656
  PopoverContent,
10349
10657
  {
10350
10658
  align: "start",
10351
10659
  className: "w-56 p-2",
10352
10660
  onClick: (e) => e.stopPropagation(),
10353
- children: /* @__PURE__ */ jsx39(
10354
- TypedEditor,
10355
- {
10356
- editType,
10357
- value,
10358
- editOptions,
10359
- onCommit: (next) => {
10360
- onCommit(next);
10361
- setOpen(false);
10362
- },
10363
- onCancel: () => setOpen(false)
10364
- }
10365
- )
10661
+ children: [
10662
+ /* @__PURE__ */ jsx39(
10663
+ TypedEditor,
10664
+ {
10665
+ editType,
10666
+ value,
10667
+ editOptions,
10668
+ onCommit: (next) => {
10669
+ const refused = onCommit(next);
10670
+ if (typeof refused === "string" && refused) {
10671
+ setRefusal(refused);
10672
+ return;
10673
+ }
10674
+ setRefusal(null);
10675
+ setOpen(false);
10676
+ },
10677
+ onCancel: () => setOpen(false)
10678
+ }
10679
+ ),
10680
+ refusal ? /* @__PURE__ */ jsx39(CellRefusal, { reason: refusal }) : null
10681
+ ]
10366
10682
  }
10367
10683
  )
10368
10684
  ] });
@@ -10375,17 +10691,31 @@ function InlineStringEditor({
10375
10691
  className,
10376
10692
  href,
10377
10693
  usePencil,
10378
- editRequest
10694
+ editRequest,
10695
+ editSeed
10379
10696
  }) {
10380
10697
  const [editing, setEditing] = useState20(false);
10381
10698
  const [draft, setDraft] = useState20(value);
10382
- const handledEditRequest = useRef13(void 0);
10699
+ const [refusal, setRefusal] = useState20(null);
10700
+ const handledEditRequest = useRef14(void 0);
10701
+ const seedRef = useRef14(editSeed);
10702
+ seedRef.current = editSeed;
10383
10703
  useEffect15(() => {
10384
10704
  if (editRequest === void 0 || handledEditRequest.current === editRequest) return;
10385
10705
  handledEditRequest.current = editRequest;
10386
- setDraft(value);
10706
+ setDraft(seedRef.current ?? value);
10707
+ setRefusal(null);
10387
10708
  setEditing(true);
10388
10709
  }, [editRequest, value]);
10710
+ const commit = (next) => {
10711
+ const refused = onCommit(next);
10712
+ if (typeof refused === "string" && refused) {
10713
+ setRefusal(refused);
10714
+ return false;
10715
+ }
10716
+ setRefusal(null);
10717
+ return true;
10718
+ };
10389
10719
  if (!editing) {
10390
10720
  if (usePencil) {
10391
10721
  return /* @__PURE__ */ jsx39(
@@ -10436,32 +10766,38 @@ function InlineStringEditor({
10436
10766
  /* @__PURE__ */ jsx39(TooltipContent, { children: "Click to edit" })
10437
10767
  ] });
10438
10768
  }
10439
- return /* @__PURE__ */ jsx39(
10440
- Input,
10441
- {
10442
- autoFocus: true,
10443
- value: draft,
10444
- onClick: (e) => e.stopPropagation(),
10445
- onChange: (e) => setDraft(e.target.value),
10446
- onBlur: () => {
10447
- onCommit(draft);
10448
- setEditing(false);
10449
- },
10450
- onKeyDown: (e) => {
10451
- if (e.key === "Enter") {
10452
- e.preventDefault();
10453
- onCommit(draft);
10454
- setEditing(false);
10455
- }
10456
- if (e.key === "Escape") {
10457
- e.preventDefault();
10458
- setEditing(false);
10459
- }
10460
- },
10461
- className: "h-7 text-sm",
10462
- style: { fontSize: "16px" }
10463
- }
10464
- );
10769
+ return /* @__PURE__ */ jsxs29("span", { className: "block w-full", children: [
10770
+ /* @__PURE__ */ jsx39(
10771
+ Input,
10772
+ {
10773
+ autoFocus: true,
10774
+ value: draft,
10775
+ onClick: (e) => e.stopPropagation(),
10776
+ onChange: (e) => {
10777
+ setDraft(e.target.value);
10778
+ if (refusal) setRefusal(null);
10779
+ },
10780
+ onBlur: () => {
10781
+ if (commit(draft)) setEditing(false);
10782
+ },
10783
+ onKeyDown: (e) => {
10784
+ if (e.key === "Enter") {
10785
+ e.preventDefault();
10786
+ if (commit(draft)) setEditing(false);
10787
+ }
10788
+ if (e.key === "Escape") {
10789
+ e.preventDefault();
10790
+ setRefusal(null);
10791
+ setEditing(false);
10792
+ }
10793
+ },
10794
+ "aria-invalid": refusal ? true : void 0,
10795
+ className: cn("h-7 text-sm", refusal && "border-amber-500 focus-visible:ring-amber-500"),
10796
+ style: { fontSize: "16px" }
10797
+ }
10798
+ ),
10799
+ refusal ? /* @__PURE__ */ jsx39(CellRefusal, { reason: refusal }) : null
10800
+ ] });
10465
10801
  }
10466
10802
  function TypedEditor({
10467
10803
  editType,
@@ -10646,8 +10982,8 @@ function NumberEditor({
10646
10982
  style: { fontSize: "16px" },
10647
10983
  onKeyDown: (e) => {
10648
10984
  if (e.key === "Enter") {
10649
- const n = Number(draft);
10650
- if (Number.isFinite(n)) onCommit(n);
10985
+ const n2 = Number(draft);
10986
+ if (Number.isFinite(n2)) onCommit(n2);
10651
10987
  }
10652
10988
  if (e.key === "Escape") onCancel();
10653
10989
  }
@@ -10674,8 +11010,8 @@ function NumberEditor({
10674
11010
  className: "h-7",
10675
11011
  "aria-label": "Apply value",
10676
11012
  onClick: () => {
10677
- const n = Number(draft);
10678
- if (Number.isFinite(n)) onCommit(n);
11013
+ const n2 = Number(draft);
11014
+ if (Number.isFinite(n2)) onCommit(n2);
10679
11015
  },
10680
11016
  children: /* @__PURE__ */ jsx39(Check, { className: "h-3.5 w-3.5" })
10681
11017
  }
@@ -11483,11 +11819,12 @@ function MatrxDataTableCore({
11483
11819
  virtualize,
11484
11820
  coverage,
11485
11821
  spreadsheet,
11822
+ facets,
11486
11823
  rowHeight: explicitRowHeight
11487
11824
  }) {
11488
11825
  const appendQuery = query?.mode === "controlled-append" ? query : null;
11489
11826
  const appendQueryKey = appendQuery?.pagination.queryKey ?? null;
11490
- const committedAppendQueryKey = useRef14(appendQueryKey);
11827
+ const committedAppendQueryKey = useRef15(appendQueryKey);
11491
11828
  const appendSourceChanged = appendQuery !== null && committedAppendQueryKey.current !== appendQueryKey;
11492
11829
  const data = (appendQuery ? appendQuery.pagination.rows : suppliedData) ?? [];
11493
11830
  const isLoading = appendQuery ? appendQuery.pagination.loading : suppliedLoading;
@@ -11502,7 +11839,7 @@ function MatrxDataTableCore({
11502
11839
  const [savedViewError, setSavedViewError] = useState22(null);
11503
11840
  const [toolbarActionError, setToolbarActionError] = useState22(null);
11504
11841
  const [toolbarActionPending, setToolbarActionPending] = useState22({ refresh: false, add: false });
11505
- const toolbarActionPendingRef = useRef14({ refresh: false, add: false });
11842
+ const toolbarActionPendingRef = useRef15({ refresh: false, add: false });
11506
11843
  const CHECKBOX_TAP_AREA = "h-4 w-4 touch-manipulation";
11507
11844
  const mobileCardsClass = mobileCardsBreakpoint === "lg" ? "lg:hidden" : "sm:hidden";
11508
11845
  const tableWithCardsClass = mobileCardsBreakpoint === "lg" ? "max-lg:hidden" : "max-sm:hidden";
@@ -11640,9 +11977,9 @@ function MatrxDataTableCore({
11640
11977
  const [windowRowSnapshot, setWindowRowSnapshot] = useState22(null);
11641
11978
  const editScopeKey = appendQueryKey === null ? "\0local" : `append:${appendQueryKey}`;
11642
11979
  const [editsByScope, setEditsByScope] = useState22({});
11643
- const editRevisionRef = useRef14({});
11980
+ const editRevisionRef = useRef15({});
11644
11981
  const [savingByScope, setSavingByScope] = useState22({});
11645
- const savingScopesRef = useRef14(/* @__PURE__ */ new Set());
11982
+ const savingScopesRef = useRef15(/* @__PURE__ */ new Set());
11646
11983
  const [editRequest, setEditRequest] = useState22(null);
11647
11984
  const [confirmForeignDraftDiscard, setConfirmForeignDraftDiscard] = useState22(false);
11648
11985
  const edits = editsByScope[editScopeKey]?.values ?? {};
@@ -11729,13 +12066,13 @@ function MatrxDataTableCore({
11729
12066
  onWindowRowIdChange,
11730
12067
  selectionConfig
11731
12068
  ]);
11732
- const scrollRef = useRef14(null);
11733
- const tableRef = useRef14(null);
11734
- const tableHeaderRef = useRef14(null);
11735
- const cardsScrollRef = useRef14(null);
12069
+ const scrollRef = useRef15(null);
12070
+ const tableRef = useRef15(null);
12071
+ const tableHeaderRef = useRef15(null);
12072
+ const cardsScrollRef = useRef15(null);
11736
12073
  const [documentStickyHeader, setDocumentStickyHeader] = useState22(false);
11737
- const documentStickyHeaderRef = useRef14(false);
11738
- const previousAppendKeyForScrollReset = useRef14(appendQueryKey);
12074
+ const documentStickyHeaderRef = useRef15(false);
12075
+ const previousAppendKeyForScrollReset = useRef15(appendQueryKey);
11739
12076
  useLayoutEffect2(() => {
11740
12077
  const previous = previousAppendKeyForScrollReset.current;
11741
12078
  previousAppendKeyForScrollReset.current = appendQueryKey;
@@ -11761,7 +12098,7 @@ function MatrxDataTableCore({
11761
12098
  ...appendScrollPolicy?.mode === "scroll" && appendScrollPolicy.thresholdPx !== void 0 ? { thresholdPx: appendScrollPolicy.thresholdPx } : {},
11762
12099
  ...appendScrollPolicy?.mode === "scroll" && appendScrollPolicy.intentTimeoutMs !== void 0 ? { intentTimeoutMs: appendScrollPolicy.intentTimeoutMs } : {}
11763
12100
  });
11764
- const rootRef = useRef14(null);
12101
+ const rootRef = useRef15(null);
11765
12102
  useClippedContentGuard(rootRef, {
11766
12103
  label: typeof copy === "object" && copy.listLabel ? `Table: ${copy.listLabel}` : "MatrxDataTable"
11767
12104
  });
@@ -11863,11 +12200,11 @@ function MatrxDataTableCore({
11863
12200
  () => normalizeColumnWidthsForColumns(columnState?.widths, columns)
11864
12201
  );
11865
12202
  const [fixedTableGeometry, setFixedTableGeometry] = useState22(null);
11866
- const knownColumnIds = useRef14(new Set(columns.map(columnId)));
11867
- const draggedColumnId = useRef14(null);
11868
- const columnDragActive = useRef14(false);
11869
- const columnDragRaf = useRef14(null);
11870
- const columnDragScrollDirection = useRef14(0);
12203
+ const knownColumnIds = useRef15(new Set(columns.map(columnId)));
12204
+ const draggedColumnId = useRef15(null);
12205
+ const columnDragActive = useRef15(false);
12206
+ const columnDragRaf = useRef15(null);
12207
+ const columnDragScrollDirection = useRef15(0);
11871
12208
  const [columnDragPreview, setColumnDragPreview] = useState22(null);
11872
12209
  const stopColumnDragScroll = useCallback8(() => {
11873
12210
  if (columnDragRaf.current !== null) cancelAnimationFrame(columnDragRaf.current);
@@ -11922,7 +12259,7 @@ function MatrxDataTableCore({
11922
12259
  () => normalizeColumnWidthsForColumns(columnState?.widths ?? columnWidths, columns),
11923
12260
  [columns, columnState?.widths, columnWidths]
11924
12261
  );
11925
- const resizeGestureSnapshotRef = useRef14(null);
12262
+ const resizeGestureSnapshotRef = useRef15(null);
11926
12263
  const measureVisibleColumnWidths = useCallback8(() => {
11927
12264
  const measured = {};
11928
12265
  const headers = tableRef.current ? Array.from(tableRef.current.querySelectorAll(
@@ -12228,7 +12565,7 @@ function MatrxDataTableCore({
12228
12565
  intelligentSearchEnabled,
12229
12566
  searchScope
12230
12567
  ]);
12231
- const lastNotifiedViewRows = useRef14(null);
12568
+ const lastNotifiedViewRows = useRef15(null);
12232
12569
  useEffect16(() => {
12233
12570
  if (!onViewChange) {
12234
12571
  lastNotifiedViewRows.current = null;
@@ -12348,7 +12685,7 @@ function MatrxDataTableCore({
12348
12685
  const columnVirtualizeActive = virtualizeConfig !== null && virtualizeConfig.enabled !== false && virtualizeConfig.columns === true && visibleColumns.length - frozenRunLength > 30 && !hierarchy && mobileCards === void 0;
12349
12686
  const [rowWindow, setRowWindow] = useState22(null);
12350
12687
  const [columnWindow, setColumnWindow] = useState22(null);
12351
- const windowInputs = useRef14({
12688
+ const windowInputs = useRef15({
12352
12689
  virtualizeActive: false,
12353
12690
  bodyEntryCount: 0,
12354
12691
  virtualRowHeight: 40,
@@ -12498,7 +12835,7 @@ function MatrxDataTableCore({
12498
12835
  windowed.push({ kind: "spacer", height: effectiveRowWindow.padEnd, place: "end" });
12499
12836
  return windowed;
12500
12837
  }, [groupedRenderRows, collapsedGroups, paginated, effectiveRowWindow]);
12501
- const getRowIdRef = useRef14(getRowId);
12838
+ const getRowIdRef = useRef15(getRowId);
12502
12839
  getRowIdRef.current = getRowId;
12503
12840
  const spreadsheetRowIds = useMemo8(
12504
12841
  () => paginated.map((row) => getRowIdRef.current(row)),
@@ -12540,9 +12877,92 @@ function MatrxDataTableCore({
12540
12877
  ...spreadsheet.undoDepth === void 0 ? {} : { undoDepth: spreadsheet.undoDepth },
12541
12878
  ...spreadsheet.fillHandle === void 0 ? {} : { fillHandle: spreadsheet.fillHandle },
12542
12879
  ...spreadsheet.paste === void 0 ? {} : { paste: spreadsheet.paste },
12543
- notify: spreadsheet.notify ?? ((message) => toast.success(message))
12880
+ notify: spreadsheet.notify ?? ((message) => toast.success(message)),
12881
+ onEditRequest: (address, seed) => requestCellEditRef.current(address, seed),
12882
+ ...spreadsheet.validateCell ? { validateCell: spreadsheet.validateCell } : {}
12544
12883
  } : null
12545
12884
  );
12885
+ const sheetRef = useRef15(sheet);
12886
+ sheetRef.current = sheet;
12887
+ const searchValueRef = useRef15(searchValue);
12888
+ searchValueRef.current = searchValue;
12889
+ const facetsEnabled = Boolean(facets && facets.enabled !== false);
12890
+ const [facetState, setFacetState] = useState22({});
12891
+ const facetReadCell = useCallback8(
12892
+ (row, colId) => {
12893
+ if (facets?.readCell) return facets.readCell(row, colId);
12894
+ const col = visibleColumns.find((candidate) => columnId(candidate) === colId);
12895
+ return col ? getFilterValue(row, col) : void 0;
12896
+ },
12897
+ [facets, visibleColumns]
12898
+ );
12899
+ const facetInputsRef = useRef15({ data, facets, facetReadCell });
12900
+ facetInputsRef.current = { data, facets, facetReadCell };
12901
+ const facetRequestRef = useRef15(/* @__PURE__ */ new Map());
12902
+ const requestFacets = useCallback8((colId) => {
12903
+ const { data: rows, facets: config, facetReadCell: readCell } = facetInputsRef.current;
12904
+ if (!config || config.enabled === false) return;
12905
+ const complete = localFacetsAreComplete(rows.length, config.totalRows);
12906
+ const limit = config.limit;
12907
+ if (complete || !config.source) {
12908
+ setFacetState((current) => ({
12909
+ ...current,
12910
+ [colId]: {
12911
+ loading: false,
12912
+ data: computeColumnFacets({
12913
+ columnId: colId,
12914
+ rows,
12915
+ readValue: readCell,
12916
+ ...limit === void 0 ? {} : { limit },
12917
+ complete
12918
+ })
12919
+ }
12920
+ }));
12921
+ return;
12922
+ }
12923
+ const ticket = (facetRequestRef.current.get(colId) ?? 0) + 1;
12924
+ facetRequestRef.current.set(colId, ticket);
12925
+ setFacetState((current) => ({
12926
+ ...current,
12927
+ [colId]: { loading: true, data: current[colId]?.data ?? null }
12928
+ }));
12929
+ void Promise.resolve().then(
12930
+ () => config.source?.({ columnId: colId, search: searchValueRef.current, limit: limit ?? 200 })
12931
+ ).then((answer) => {
12932
+ if (facetRequestRef.current.get(colId) !== ticket) return;
12933
+ if (!answer) {
12934
+ setFacetState((current) => ({
12935
+ ...current,
12936
+ [colId]: {
12937
+ loading: false,
12938
+ data: computeColumnFacets({
12939
+ columnId: colId,
12940
+ rows: facetInputsRef.current.data,
12941
+ readValue: facetInputsRef.current.facetReadCell,
12942
+ ...limit === void 0 ? {} : { limit },
12943
+ complete: false
12944
+ })
12945
+ }
12946
+ }));
12947
+ return;
12948
+ }
12949
+ setFacetState((current) => ({
12950
+ ...current,
12951
+ [colId]: { loading: false, data: { ...answer, answeredBy: "source", complete: true } }
12952
+ }));
12953
+ }).catch((error) => {
12954
+ if (facetRequestRef.current.get(colId) !== ticket) return;
12955
+ setFacetState((current) => ({
12956
+ ...current,
12957
+ [colId]: { loading: false, data: null, error: describeFacetFailure(error) }
12958
+ }));
12959
+ });
12960
+ }, []);
12961
+ useEffect16(() => {
12962
+ if (!facetsEnabled) return;
12963
+ setFacetState({});
12964
+ facetRequestRef.current = /* @__PURE__ */ new Map();
12965
+ }, [facetsEnabled, data, searchValue]);
12546
12966
  useEffect16(() => {
12547
12967
  if (!controlledQuery || controlledQuery.state.page === safePage) return;
12548
12968
  emitControlledQueryChange({ page: safePage });
@@ -12587,11 +13007,11 @@ function MatrxDataTableCore({
12587
13007
  });
12588
13008
  const hasActiveFilters = activeFilterCount > 0 || Boolean(searchValue.trim()) || intelligentSearchEnabled && searchScope !== "all" || Boolean(anyOfValue.trim()) || completeLayeredFilterRules(layeredFilters).length > 0 || facetActive;
12589
13009
  const changeCount = useMemo8(() => {
12590
- let n = 0;
13010
+ let n2 = 0;
12591
13011
  for (const fields of Object.values(edits)) {
12592
- n += Object.keys(fields).length;
13012
+ n2 += Object.keys(fields).length;
12593
13013
  }
12594
- return n;
13014
+ return n2;
12595
13015
  }, [edits]);
12596
13016
  const showSearch = toolbar?.search !== false;
12597
13017
  const openDetail = (row) => {
@@ -12670,6 +13090,23 @@ function MatrxDataTableCore({
12670
13090
  icon: void 0
12671
13091
  } : emptyState;
12672
13092
  const commitCell = (rowId, field, next) => {
13093
+ if (edit?.validate) {
13094
+ const row = data.find((candidate) => getRowId(candidate) === rowId);
13095
+ if (row) {
13096
+ let refusal;
13097
+ try {
13098
+ refusal = edit.validate({
13099
+ row: applyRowEdits(row, edits[rowId]),
13100
+ rowId,
13101
+ columnId: field,
13102
+ value: next
13103
+ });
13104
+ } catch {
13105
+ refusal = void 0;
13106
+ }
13107
+ if (refusal) return refusal;
13108
+ }
13109
+ }
12673
13110
  const scope = editScopeKey;
12674
13111
  const nextEdits = { [rowId]: { [field]: next } };
12675
13112
  const revision = setCellEdit(rowId, field, next);
@@ -12714,9 +13151,29 @@ function MatrxDataTableCore({
12714
13151
  setEditRequest((current) => ({
12715
13152
  rowId: getRowId(row),
12716
13153
  field,
12717
- nonce: (current?.nonce ?? 0) + 1
13154
+ nonce: (current?.nonce ?? 0) + 1,
13155
+ seed: null
12718
13156
  }));
12719
13157
  };
13158
+ const requestCellEdit = (address, seed) => {
13159
+ if (!editEnabled) return false;
13160
+ const row = data.find((candidate) => getRowId(candidate) === address.rowId);
13161
+ if (!row) return false;
13162
+ const column = visibleColumns.find(
13163
+ (candidate) => columnId(candidate) === address.fieldName
13164
+ );
13165
+ if (!column?.editable || !(column.editableIf?.(row) ?? true)) return false;
13166
+ const field = column.accessorKey ? String(column.accessorKey) : columnId(column);
13167
+ setEditRequest((current) => ({
13168
+ rowId: address.rowId,
13169
+ field,
13170
+ nonce: (current?.nonce ?? 0) + 1,
13171
+ seed
13172
+ }));
13173
+ return true;
13174
+ };
13175
+ const requestCellEditRef = useRef15(requestCellEdit);
13176
+ requestCellEditRef.current = requestCellEdit;
12720
13177
  const buildRecordControls = (row) => {
12721
13178
  const rowId = getRowId(row);
12722
13179
  const pending = edits[rowId];
@@ -12957,17 +13414,78 @@ function MatrxDataTableCore({
12957
13414
  setEdits({});
12958
13415
  edit?.onCancel?.();
12959
13416
  };
12960
- useEffect16(() => {
12961
- if (!host.rowContextRegistry || !contextMenu && !host.createDefaultRowContext) return;
12962
- return host.rowContextRegistry.register(tableContextMenuInstanceId, (rowId) => {
13417
+ const resolveMenuLevel = (target) => {
13418
+ const rowFor = (rowId) => {
12963
13419
  const row = data.find((candidate) => getRowId(candidate) === rowId);
12964
13420
  if (!row) return null;
12965
- const pending = edits[rowId];
12966
- const displayRow = applyRowEdits(row, pending);
12967
- const controls = buildRecordControls(displayRow);
12968
- return contextMenu?.resolveRowContext(displayRow, controls) ?? host.createDefaultRowContext?.(displayRow, controls) ?? null;
12969
- });
12970
- }, [buildRecordControls, contextMenu, data, getRowId, host.createDefaultRowContext, host.rowContextRegistry, tableContextMenuInstanceId]);
13421
+ return applyRowEdits(row, edits[rowId]);
13422
+ };
13423
+ const columnFor = (id) => visibleColumns.find((col) => columnId(col) === id);
13424
+ const labelFor = (col, id) => col?.label ?? (typeof col?.header === "string" ? col.header : id);
13425
+ const fromHost = (payload) => host.createDefaultMenuContext?.(payload) ?? null;
13426
+ switch (target.level) {
13427
+ case "row": {
13428
+ if (!target.rowId) return null;
13429
+ const displayRow = rowFor(target.rowId);
13430
+ if (!displayRow) return null;
13431
+ const controls = buildRecordControls(displayRow);
13432
+ return contextMenu?.resolveRowContext?.(displayRow, controls) ?? fromHost({ level: "row", row: displayRow, rowId: target.rowId, controls });
13433
+ }
13434
+ case "cell": {
13435
+ if (!target.rowId || !target.columnId) return null;
13436
+ const displayRow = rowFor(target.rowId);
13437
+ const col = columnFor(target.columnId);
13438
+ if (!displayRow || !col) return null;
13439
+ const controls = buildRecordControls(displayRow);
13440
+ const value = spreadsheet?.readCell ? spreadsheet.readCell(displayRow, target.columnId) : getCellValue(displayRow, col);
13441
+ const args = {
13442
+ row: displayRow,
13443
+ rowId: target.rowId,
13444
+ columnId: target.columnId,
13445
+ columnLabel: labelFor(col, target.columnId),
13446
+ value,
13447
+ text: stringifyCellValue(value),
13448
+ controls
13449
+ };
13450
+ return contextMenu?.resolveCellContext?.(args) ?? fromHost({ level: "cell", ...args });
13451
+ }
13452
+ case "column": {
13453
+ if (!target.columnId) return null;
13454
+ const col = columnFor(target.columnId);
13455
+ if (!col) return null;
13456
+ const args = {
13457
+ columnId: target.columnId,
13458
+ columnLabel: labelFor(col, target.columnId),
13459
+ values: paginated.map(
13460
+ (row) => getCellValue(applyRowEdits(row, edits[getRowId(row)]), col)
13461
+ )
13462
+ };
13463
+ return contextMenu?.resolveColumnContext?.(args) ?? fromHost({ level: "column", ...args });
13464
+ }
13465
+ case "selection": {
13466
+ const addresses = sheetRef.current.selectionAddresses();
13467
+ const cellCount = addresses.rowIds.length * addresses.columnIds.length;
13468
+ if (cellCount <= 1) {
13469
+ return resolveMenuLevel({ ...target, level: "cell" });
13470
+ }
13471
+ const args = { ...addresses, tsv: sheetRef.current.selectionTsv(), cellCount };
13472
+ return contextMenu?.resolveSelectionContext?.(args) ?? fromHost({ level: "selection", ...args });
13473
+ }
13474
+ case "table": {
13475
+ const args = { rowCount: data.length, columnIds: visibleColumns.map((col) => columnId(col)) };
13476
+ return contextMenu?.resolveTableContext?.(args) ?? fromHost({ level: "table", ...args });
13477
+ }
13478
+ }
13479
+ };
13480
+ const resolveMenuLevelRef = useRef15(resolveMenuLevel);
13481
+ resolveMenuLevelRef.current = resolveMenuLevel;
13482
+ useEffect16(() => {
13483
+ if (!host.rowContextRegistry) return;
13484
+ return host.rowContextRegistry.register(
13485
+ tableContextMenuInstanceId,
13486
+ (target) => resolveMenuLevelRef.current(target)
13487
+ );
13488
+ }, [host.rowContextRegistry, tableContextMenuInstanceId]);
12971
13489
  const showActionsCol = windowEnabled || Boolean(rowActions) || showInlineRowCopy;
12972
13490
  const selectedIdSet = useMemo8(
12973
13491
  () => new Set(selection?.selectedIds ?? []),
@@ -12985,7 +13503,7 @@ function MatrxDataTableCore({
12985
13503
  () => data.filter((row) => selectedIdSet.has(getRowId(row))),
12986
13504
  [data, selectedIdSet, getRowId]
12987
13505
  );
12988
- const lastToggledIndex = useRef14(null);
13506
+ const lastToggledIndex = useRef15(null);
12989
13507
  const setSelectedIds = (next) => selection?.onSelectedIdsChange([...next]);
12990
13508
  const toggleRowSelected = (index, shiftKey) => {
12991
13509
  if (!selection) return;
@@ -13049,8 +13567,19 @@ function MatrxDataTableCore({
13049
13567
  dirty: Boolean(rowEdits && field in rowEdits),
13050
13568
  onCommit: (next) => commitCell(id, field, next),
13051
13569
  ...href === void 0 ? {} : { href },
13052
- ...col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13053
- ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce } : {}
13570
+ ...spreadsheetEnabled ? {
13571
+ // 🚨 THE CLICK LAW, enforced where the two paths meet. In a grid
13572
+ // with spreadsheet gestures a single click SELECTS a cell — it
13573
+ // may never drop a person into a free-text buffer, because the
13574
+ // same click is how they start a range, and an editor that opens
13575
+ // under the pointer eats the drag and the next keystroke. So the
13576
+ // editor opens the way it does in Excel: Enter, F2, the hover
13577
+ // pencil, or simply typing. An explicit `editTrigger: "click"`
13578
+ // does not override this; the law is the table's, not the
13579
+ // column's.
13580
+ editTrigger: "pencil"
13581
+ } : col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13582
+ ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce, editSeed: editRequest.seed } : {}
13054
13583
  }
13055
13584
  );
13056
13585
  }
@@ -13808,6 +14337,7 @@ function MatrxDataTableCore({
13808
14337
  "th",
13809
14338
  {
13810
14339
  "data-matrx-table-column-id": id,
14340
+ "data-matrx-header-column-id": id,
13811
14341
  "data-matrx-column-drag-shadow": isColumnDragShadow ? "true" : void 0,
13812
14342
  "aria-sort": isSorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
13813
14343
  className: cn(
@@ -13844,6 +14374,12 @@ function MatrxDataTableCore({
13844
14374
  {
13845
14375
  label: col.header,
13846
14376
  labelText: headerLabel,
14377
+ ...facetsEnabled ? {
14378
+ facets: facetState[id]?.data ?? null,
14379
+ facetsLoading: facetState[id]?.loading ?? false,
14380
+ ...facetState[id]?.error === void 0 ? {} : { facetsError: facetState[id]?.error },
14381
+ onFilterOpen: () => requestFacets(id)
14382
+ } : {},
13847
14383
  sortable: col.sortable !== false,
13848
14384
  isSorted: Boolean(isSorted),
13849
14385
  sortDirection: sort?.direction ?? defaultSortDirection,
@@ -14518,8 +15054,8 @@ function MatrxDataTableCore({
14518
15054
  itemsPerPage: pageSize === 0 && allRowsAreLoaded ? 0 : effectivePageSize,
14519
15055
  currentPage: safePage,
14520
15056
  onPageChange: setPage,
14521
- onItemsPerPageChange: (n) => {
14522
- setPageSize(n);
15057
+ onItemsPerPageChange: (n2) => {
15058
+ setPageSize(n2);
14523
15059
  if (!controlledQuery) setPage(1);
14524
15060
  },
14525
15061
  pageSizeOptions: pageSizeOptions.filter((size) => size > 0),