@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
@@ -5227,10 +5227,10 @@ function passesColumnFilter(value, filter) {
5227
5227
  if (raw === "") return false;
5228
5228
  const normalized = raw.replace(/[^0-9.-]/g, "");
5229
5229
  if (normalized === "") return false;
5230
- const n = typeof value === "number" ? value : Number(normalized);
5231
- if (!Number.isFinite(n)) return false;
5232
- if (filter.min !== void 0 && n < filter.min) return false;
5233
- if (filter.max !== void 0 && n > filter.max) return false;
5230
+ const n2 = typeof value === "number" ? value : Number(normalized);
5231
+ if (!Number.isFinite(n2)) return false;
5232
+ if (filter.min !== void 0 && n2 < filter.min) return false;
5233
+ if (filter.max !== void 0 && n2 > filter.max) return false;
5234
5234
  return true;
5235
5235
  }
5236
5236
  }
@@ -5293,26 +5293,26 @@ function filterAndSortRows(rows, columns, columnFilters, sort, globalSearch, any
5293
5293
  return result;
5294
5294
  }
5295
5295
  function countActiveColumnFilters(filters) {
5296
- let n = 0;
5296
+ let n2 = 0;
5297
5297
  for (const f of Object.values(filters)) {
5298
5298
  if (!f) continue;
5299
5299
  switch (f.kind) {
5300
5300
  case "text":
5301
- if (f.value.trim() || f.mode && f.mode !== "contains") n += 1;
5301
+ if (f.value.trim() || f.mode && f.mode !== "contains") n2 += 1;
5302
5302
  break;
5303
5303
  case "select":
5304
5304
  if (f.values !== void 0 || f.value && f.value !== "__all__")
5305
- n += 1;
5305
+ n2 += 1;
5306
5306
  break;
5307
5307
  case "boolean":
5308
- n += 1;
5308
+ n2 += 1;
5309
5309
  break;
5310
5310
  case "number":
5311
- if (f.min !== void 0 || f.max !== void 0) n += 1;
5311
+ if (f.min !== void 0 || f.max !== void 0) n2 += 1;
5312
5312
  break;
5313
5313
  }
5314
5314
  }
5315
- return n;
5315
+ return n2;
5316
5316
  }
5317
5317
  function isColumnFilterActive(filter) {
5318
5318
  if (!filter) return false;
@@ -5591,8 +5591,8 @@ function parseLayeredFilters(raw) {
5591
5591
  }
5592
5592
  }
5593
5593
  function positiveInt(raw, fallback) {
5594
- const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
5595
- return Number.isFinite(n) && n > 0 ? n : fallback;
5594
+ const n2 = raw ? Number.parseInt(raw, 10) : Number.NaN;
5595
+ return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
5596
5596
  }
5597
5597
  function tableUrlParamPrefix(tableId) {
5598
5598
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(tableId)) {
@@ -5931,6 +5931,95 @@ var CommandShortcut = ({
5931
5931
  };
5932
5932
  CommandShortcut.displayName = "CommandShortcut";
5933
5933
 
5934
+ // src/data-table/facets.ts
5935
+ var MAX_LISTABLE_FACET_LENGTH = 300;
5936
+ var DEFAULT_FACET_LIMIT = 200;
5937
+ var MAX_FACET_LIMIT = 500;
5938
+ function facetText(raw) {
5939
+ if (raw === null || raw === void 0) return "";
5940
+ if (typeof raw === "object") {
5941
+ try {
5942
+ return JSON.stringify(raw);
5943
+ } catch {
5944
+ return String(raw);
5945
+ }
5946
+ }
5947
+ return String(raw);
5948
+ }
5949
+ function computeColumnFacets(args) {
5950
+ const { columnId: columnId2, rows, readValue: readValue2 } = args;
5951
+ const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);
5952
+ const counts = /* @__PURE__ */ new Map();
5953
+ const longValues = /* @__PURE__ */ new Set();
5954
+ let blank = 0;
5955
+ let maxLength = 0;
5956
+ for (const row of rows) {
5957
+ let raw;
5958
+ try {
5959
+ raw = readValue2(row, columnId2);
5960
+ } catch {
5961
+ raw = void 0;
5962
+ }
5963
+ const trimmed = facetText(raw).trim();
5964
+ if (trimmed === "") {
5965
+ blank += 1;
5966
+ continue;
5967
+ }
5968
+ if (trimmed.length > maxLength) maxLength = trimmed.length;
5969
+ if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {
5970
+ longValues.add(trimmed);
5971
+ continue;
5972
+ }
5973
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
5974
+ }
5975
+ const values = [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, limit);
5976
+ return {
5977
+ columnId: columnId2,
5978
+ totalRows: rows.length,
5979
+ filled: rows.length - blank,
5980
+ blank,
5981
+ distinctCount: counts.size + longValues.size,
5982
+ maxLength,
5983
+ unlistable: longValues.size,
5984
+ limit,
5985
+ truncated: counts.size > limit,
5986
+ values,
5987
+ answeredBy: "local",
5988
+ complete: args.complete ?? true
5989
+ };
5990
+ }
5991
+ function localFacetsAreComplete(loadedRowCount, total) {
5992
+ if (total === void 0 || !Number.isFinite(total) || total < 0) return false;
5993
+ return loadedRowCount >= total;
5994
+ }
5995
+ var NUMBERS = new Intl.NumberFormat("en-US");
5996
+ function n(value) {
5997
+ try {
5998
+ return NUMBERS.format(value);
5999
+ } catch {
6000
+ return String(value);
6001
+ }
6002
+ }
6003
+ function describeFacetSource(facets, noun = "row") {
6004
+ if (!facets) return "";
6005
+ const plural2 = `${noun}s`;
6006
+ if (!facets.complete) {
6007
+ return `These counts cover the ${n(facets.totalRows)} ${plural2} loaded here, not the whole table.`;
6008
+ }
6009
+ if (facets.truncated) {
6010
+ return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;
6011
+ }
6012
+ if (facets.unlistable > 0) {
6013
+ 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"}.`;
6014
+ }
6015
+ return "";
6016
+ }
6017
+ var FACETS_LOADING_NOTICE = "Counting values\u2026";
6018
+ function describeFacetFailure(error) {
6019
+ const detail = error instanceof Error ? error.message : String(error ?? "");
6020
+ 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.";
6021
+ }
6022
+
5934
6023
  // src/data-table/column-marker.ts
5935
6024
  var MARKER_ACCESSORS = {
5936
6025
  favorite: "favorite",
@@ -5998,6 +6087,10 @@ function ColumnHeaderCell({
5998
6087
  filterFooterHint,
5999
6088
  allowNegation = false,
6000
6089
  allowNullModes = false,
6090
+ facets,
6091
+ facetsLoading = false,
6092
+ facetsError,
6093
+ onFilterOpen,
6001
6094
  reorderColumn
6002
6095
  }) {
6003
6096
  const reorderEnabled = reorderColumn !== void 0;
@@ -6032,6 +6125,11 @@ function ColumnHeaderCell({
6032
6125
  setPendingNegation(filterValue?.negated ?? false);
6033
6126
  }
6034
6127
  }, [filterOpen, filterValue]);
6128
+ const notifyOpen = (0, import_react13.useRef)(onFilterOpen);
6129
+ notifyOpen.current = onFilterOpen;
6130
+ (0, import_react13.useEffect)(() => {
6131
+ if (filterOpen) notifyOpen.current?.();
6132
+ }, [filterOpen]);
6035
6133
  const applyFilter = () => {
6036
6134
  onFilterChange(draftFilter);
6037
6135
  setFilterOpen(false);
@@ -6126,6 +6224,9 @@ function ColumnHeaderCell({
6126
6224
  selectOptions,
6127
6225
  selectSingle,
6128
6226
  selectAllowCustom,
6227
+ facets: facets ?? null,
6228
+ facetsLoading,
6229
+ ...facetsError === void 0 ? {} : { facetsError },
6129
6230
  ...markerLabels ? {
6130
6231
  booleanLabels: {
6131
6232
  all: "All",
@@ -6350,8 +6451,43 @@ function FilterBody({
6350
6451
  selectSingle = false,
6351
6452
  selectAllowCustom = false,
6352
6453
  allowNullModes = false,
6353
- booleanLabels
6454
+ booleanLabels,
6455
+ facets = null,
6456
+ facetsLoading = false,
6457
+ facetsError
6354
6458
  }) {
6459
+ const checklist = facetsLoading || facetsError || facets && (facets.values.length > 0 || facets.blank > 0) ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6460
+ FacetChecklist,
6461
+ {
6462
+ facets,
6463
+ loading: facetsLoading,
6464
+ ...facetsError === void 0 ? {} : { error: facetsError },
6465
+ value,
6466
+ onChange
6467
+ }
6468
+ ) : null;
6469
+ if (checklist && kind === "select" && facets && facets.values.length > 0) {
6470
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "space-y-2", children: checklist });
6471
+ }
6472
+ if (checklist) {
6473
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "space-y-2", children: [
6474
+ checklist,
6475
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "h-px bg-border" }),
6476
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6477
+ FilterBody,
6478
+ {
6479
+ kind,
6480
+ value,
6481
+ onChange,
6482
+ selectOptions,
6483
+ selectSingle,
6484
+ selectAllowCustom,
6485
+ allowNullModes,
6486
+ ...booleanLabels === void 0 ? {} : { booleanLabels }
6487
+ }
6488
+ )
6489
+ ] });
6490
+ }
6355
6491
  if (kind === "text") {
6356
6492
  const text = value?.kind === "text" ? value.value : "";
6357
6493
  const mode = (value?.kind === "text" ? value.mode : void 0) ?? "contains";
@@ -6462,6 +6598,112 @@ function FilterBody({
6462
6598
  }
6463
6599
  return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(NumberFilterBody, { value, onChange });
6464
6600
  }
6601
+ function FacetChecklist({
6602
+ facets,
6603
+ loading,
6604
+ error,
6605
+ value,
6606
+ onChange
6607
+ }) {
6608
+ const [query, setQuery] = (0, import_react13.useState)("");
6609
+ const selected = value?.kind === "select" ? value.values ?? (value.value && value.value !== "__all__" ? [value.value] : []) : [];
6610
+ const selectedSet = new Set(selected);
6611
+ const rows = (0, import_react13.useMemo)(() => {
6612
+ if (!facets) return [];
6613
+ const listed = facets.values.map((entry) => ({
6614
+ value: entry.value,
6615
+ label: entry.value,
6616
+ count: entry.count
6617
+ }));
6618
+ const withBlank = facets.blank > 0 ? [{ value: "", label: "(empty)", count: facets.blank }, ...listed] : listed;
6619
+ const q = query.trim().toLowerCase();
6620
+ if (!q) return withBlank;
6621
+ return withBlank.filter((row) => row.label.toLowerCase().includes(q));
6622
+ }, [facets, query]);
6623
+ const toggle = (candidate) => {
6624
+ const next = new Set(selectedSet);
6625
+ if (next.has(candidate)) next.delete(candidate);
6626
+ else next.add(candidate);
6627
+ const values = [...next];
6628
+ onChange(
6629
+ values.length === 0 ? void 0 : { kind: "select", value: values[0] ?? "", values }
6630
+ );
6631
+ };
6632
+ const notice = error ?? (loading ? FACETS_LOADING_NOTICE : describeFacetSource(facets));
6633
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "space-y-1.5", children: [
6634
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: "Values" }),
6635
+ facets && facets.values.length + (facets.blank > 0 ? 1 : 0) > 8 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6636
+ Input,
6637
+ {
6638
+ "data-matrx-filter-autofocus": true,
6639
+ value: query,
6640
+ onChange: (event) => setQuery(event.target.value),
6641
+ placeholder: "Find a value\u2026",
6642
+ className: "h-7 text-sm",
6643
+ style: { fontSize: "16px" }
6644
+ }
6645
+ ) : null,
6646
+ rows.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("ul", { className: "max-h-56 space-y-0.5 overflow-y-auto", "data-matrx-facet-list": "true", children: rows.map((row) => {
6647
+ const checked = selectedSet.has(row.value);
6648
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
6649
+ "button",
6650
+ {
6651
+ type: "button",
6652
+ role: "checkbox",
6653
+ "aria-checked": checked,
6654
+ "data-matrx-facet-value": row.value === "" ? "__blank__" : row.value,
6655
+ onClick: () => toggle(row.value),
6656
+ className: cn(
6657
+ "flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-xs hover:bg-muted",
6658
+ checked && "bg-primary/10"
6659
+ ),
6660
+ children: [
6661
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6662
+ "span",
6663
+ {
6664
+ "aria-hidden": true,
6665
+ className: cn(
6666
+ "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-[3px] border",
6667
+ checked ? "border-primary bg-primary text-primary-foreground" : "border-border"
6668
+ ),
6669
+ children: checked ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Check, { className: "h-2.5 w-2.5" }) : null
6670
+ }
6671
+ ),
6672
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: cn("min-w-0 flex-1 truncate", row.value === "" && "italic text-muted-foreground"), children: row.label }),
6673
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6674
+ "span",
6675
+ {
6676
+ "data-matrx-facet-count": String(row.count),
6677
+ className: "shrink-0 tabular-nums text-[11px] text-muted-foreground",
6678
+ children: row.count.toLocaleString("en-US")
6679
+ }
6680
+ )
6681
+ ]
6682
+ }
6683
+ ) }, row.value === "" ? "__blank__" : row.value);
6684
+ }) }) : loading ? null : /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-1 text-[11px] text-muted-foreground", children: query.trim() ? "No value matches that." : "This column has no values yet." }),
6685
+ notice ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6686
+ "p",
6687
+ {
6688
+ "data-matrx-facet-notice": "true",
6689
+ className: cn(
6690
+ "px-1 text-[11px] leading-snug",
6691
+ error ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground"
6692
+ ),
6693
+ children: notice
6694
+ }
6695
+ ) : null,
6696
+ selected.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6697
+ "button",
6698
+ {
6699
+ type: "button",
6700
+ onClick: () => onChange(void 0),
6701
+ className: "px-1 text-[11px] text-muted-foreground underline-offset-2 hover:underline",
6702
+ children: "Clear selection"
6703
+ }
6704
+ ) : null
6705
+ ] });
6706
+ }
6465
6707
  function SearchableSelectFilter({
6466
6708
  selected,
6467
6709
  options,
@@ -6648,8 +6890,8 @@ function NumberFilterBody({
6648
6890
  }
6649
6891
  function parseOptionalNumber(raw) {
6650
6892
  if (raw.trim() === "") return void 0;
6651
- const n = Number(raw.replace(/[^0-9.-]/g, ""));
6652
- return Number.isFinite(n) ? n : void 0;
6893
+ const n2 = Number(raw.replace(/[^0-9.-]/g, ""));
6894
+ return Number.isFinite(n2) ? n2 : void 0;
6653
6895
  }
6654
6896
 
6655
6897
  // src/data-table/ColumnResizeHandle.tsx
@@ -6961,16 +7203,16 @@ function computeAggregate(rows, column) {
6961
7203
  value = total;
6962
7204
  break;
6963
7205
  case "sum":
6964
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0);
7206
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0);
6965
7207
  break;
6966
7208
  case "average":
6967
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0) / numbers.length;
7209
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0) / numbers.length;
6968
7210
  break;
6969
7211
  case "min":
6970
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n < acc ? n : acc);
7212
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 < acc ? n2 : acc);
6971
7213
  break;
6972
7214
  case "max":
6973
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n > acc ? n : acc);
7215
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 > acc ? n2 : acc);
6974
7216
  break;
6975
7217
  case "empty":
6976
7218
  value = total - filled.length;
@@ -7394,7 +7636,7 @@ function moveSelection(current, move, rowIds, fieldNames) {
7394
7636
  if (r === -1 || c === -1) return first;
7395
7637
  const lastRow = rowIds.length - 1;
7396
7638
  const lastCol = fieldNames.length - 1;
7397
- const clamp = (n, max) => Math.min(Math.max(n, 0), max);
7639
+ const clamp = (n2, max) => Math.min(Math.max(n2, 0), max);
7398
7640
  switch (move) {
7399
7641
  case "up":
7400
7642
  r = clamp(r - 1, lastRow);
@@ -7526,14 +7768,14 @@ function parseClipboardGrid(text) {
7526
7768
  let row = [];
7527
7769
  let cell = "";
7528
7770
  let i = 0;
7529
- const n = text.length;
7530
- while (i < n) {
7771
+ const n2 = text.length;
7772
+ while (i < n2) {
7531
7773
  const ch = text[i];
7532
7774
  if (cell === "" && ch === '"') {
7533
7775
  let j = i + 1;
7534
7776
  let out = "";
7535
7777
  let closed = false;
7536
- while (j < n) {
7778
+ while (j < n2) {
7537
7779
  const c = text[j];
7538
7780
  if (c === '"') {
7539
7781
  if (text[j + 1] === '"') {
@@ -7548,10 +7790,10 @@ function parseClipboardGrid(text) {
7548
7790
  out += c;
7549
7791
  j += 1;
7550
7792
  }
7551
- if (closed && (j >= n || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
7793
+ if (closed && (j >= n2 || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
7552
7794
  cell = out;
7553
7795
  i = j;
7554
- if (i >= n) break;
7796
+ if (i >= n2) break;
7555
7797
  const d = text[i];
7556
7798
  if (d === " ") {
7557
7799
  row.push(cell);
@@ -7587,7 +7829,7 @@ function parseClipboardGrid(text) {
7587
7829
  cell += ch;
7588
7830
  i += 1;
7589
7831
  }
7590
- const endedOnBreak = n > 0 && (text[n - 1] === "\n" || text[n - 1] === "\r");
7832
+ const endedOnBreak = n2 > 0 && (text[n2 - 1] === "\n" || text[n2 - 1] === "\r");
7591
7833
  if (!endedOnBreak) {
7592
7834
  row.push(cell);
7593
7835
  rows.push(row);
@@ -7666,11 +7908,11 @@ function directionOf(source, target) {
7666
7908
  return null;
7667
7909
  }
7668
7910
  function tileIndex(i, a, b) {
7669
- const n = b - a + 1;
7670
- if (n <= 0) return a;
7911
+ const n2 = b - a + 1;
7912
+ if (n2 <= 0) return a;
7671
7913
  if (i >= a && i <= b) return i;
7672
- if (i > b) return a + (i - a) % n;
7673
- return b - (a - 1 - i) % n;
7914
+ if (i > b) return a + (i - a) % n2;
7915
+ return b - (a - 1 - i) % n2;
7674
7916
  }
7675
7917
  function isFiniteNumber(v) {
7676
7918
  return typeof v === "number" && Number.isFinite(v);
@@ -8015,6 +8257,26 @@ function useSpreadsheetGrid(options) {
8015
8257
  for (let i = 0; i < columnIds.length; i += 1) map.set(columnIds[i], i);
8016
8258
  return map;
8017
8259
  }, [columnIds]);
8260
+ const screenPatches = (0, import_react15.useCallback)(
8261
+ (patches) => {
8262
+ const validate = optionsRef.current?.validateCell;
8263
+ if (!validate) return { accepted: patches, refusals: [] };
8264
+ const accepted = [];
8265
+ const refusals = [];
8266
+ for (const patch of patches) {
8267
+ let reason;
8268
+ try {
8269
+ reason = validate({ rowId: patch.rowId, columnId: patch.columnId, value: patch.value });
8270
+ } catch {
8271
+ reason = void 0;
8272
+ }
8273
+ if (reason) refusals.push({ columnId: patch.columnId, reason });
8274
+ else accepted.push(patch);
8275
+ }
8276
+ return { accepted, refusals };
8277
+ },
8278
+ []
8279
+ );
8018
8280
  const applyPatches = (0, import_react15.useCallback)(
8019
8281
  async (patches, intent, label) => {
8020
8282
  const current = optionsRef.current;
@@ -8071,9 +8333,10 @@ function useSpreadsheetGrid(options) {
8071
8333
  ...base,
8072
8334
  range: { top: bounds.r0, left: bounds.c0, bottom: bounds.r1, right: bounds.c1 }
8073
8335
  });
8074
- await applyPatches(result.patches, "fill-down", "Filled down");
8075
- announceSkips(result.skipped, result.summary);
8076
- }, [fillRequestBase, bounds, applyPatches, announceSkips]);
8336
+ const screened = screenPatches(result.patches);
8337
+ await applyPatches(screened.accepted, "fill-down", "Filled down");
8338
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8339
+ }, [fillRequestBase, bounds, applyPatches, announceSkips, screenPatches]);
8077
8340
  const doClear = (0, import_react15.useCallback)(async () => {
8078
8341
  const current = optionsRef.current;
8079
8342
  if (!current || !bounds) return;
@@ -8094,9 +8357,13 @@ function useSpreadsheetGrid(options) {
8094
8357
  patches.push({ rowId, columnId: columnId2, value: null });
8095
8358
  }
8096
8359
  }
8097
- await applyPatches(patches, "clear", "Cleared");
8098
- announceSkips(skipped, `Cleared ${patches.length} cells.`);
8099
- }, [bounds, applyPatches, announceSkips]);
8360
+ const screened = screenPatches(patches);
8361
+ await applyPatches(screened.accepted, "clear", "Cleared");
8362
+ announceSkips(
8363
+ [...skipped, ...screened.refusals],
8364
+ `Cleared ${screened.accepted.length} cells.`
8365
+ );
8366
+ }, [bounds, applyPatches, announceSkips, screenPatches]);
8100
8367
  const doUndo = (0, import_react15.useCallback)(async () => {
8101
8368
  const taken = popUndo(stack);
8102
8369
  if (!taken.entry) return;
@@ -8181,6 +8448,14 @@ function useSpreadsheetGrid(options) {
8181
8448
  case "escape":
8182
8449
  setRange(null);
8183
8450
  return;
8451
+ case "edit":
8452
+ case "editSeeded": {
8453
+ const focused = range?.focus;
8454
+ if (!focused || !current.onEditRequest) return;
8455
+ const seed = action.kind === "editSeeded" ? action.seed : null;
8456
+ if (current.onEditRequest(focused, seed)) event.preventDefault();
8457
+ return;
8458
+ }
8184
8459
  default:
8185
8460
  return;
8186
8461
  }
@@ -8195,6 +8470,13 @@ function useSpreadsheetGrid(options) {
8195
8470
  );
8196
8471
  return gridToTsv(grid);
8197
8472
  }, [range, rowIds, columnIds]);
8473
+ const selectionAddresses = (0, import_react15.useCallback)(() => {
8474
+ if (!range || !bounds) return { rowIds: [], columnIds: [] };
8475
+ return {
8476
+ rowIds: rowIds.slice(bounds.r0, bounds.r1 + 1),
8477
+ columnIds: columnIds.slice(bounds.c0, bounds.c1 + 1)
8478
+ };
8479
+ }, [range, bounds, rowIds, columnIds]);
8198
8480
  const onCopy = (0, import_react15.useCallback)(
8199
8481
  (event) => {
8200
8482
  if (!optionsRef.current || !range || targetsAnEditor(event.target)) return;
@@ -8250,11 +8532,12 @@ function useSpreadsheetGrid(options) {
8250
8532
  patches.push({ rowId, columnId: columnId2, value: line[c] ?? null });
8251
8533
  }
8252
8534
  }
8253
- void applyPatches(patches, "paste", "Pasted");
8535
+ const screened = screenPatches(patches);
8536
+ void applyPatches(screened.accepted, "paste", "Pasted");
8254
8537
  const tail = outside > 0 ? ` ${outside} landed past the edge of the table and were ignored.` : "";
8255
- announceSkips(skipped, `Pasted ${patches.length} cells.${tail}`);
8538
+ announceSkips([...skipped, ...screened.refusals], `Pasted ${screened.accepted.length} cells.${tail}`);
8256
8539
  },
8257
- [range, rowIds, columnIds, applyPatches, announceSkips]
8540
+ [range, rowIds, columnIds, applyPatches, announceSkips, screenPatches]
8258
8541
  );
8259
8542
  const cellProps = (0, import_react15.useCallback)(
8260
8543
  (rowId, columnId2) => {
@@ -8273,6 +8556,10 @@ function useSpreadsheetGrid(options) {
8273
8556
  onMouseDown: (event) => {
8274
8557
  if (event.button !== 0) return;
8275
8558
  event.preventDefault();
8559
+ const origin = event.currentTarget;
8560
+ if (typeof HTMLElement !== "undefined" && origin instanceof HTMLElement) {
8561
+ origin.closest('[data-matrx-table-spreadsheet="true"]')?.focus({ preventScroll: true });
8562
+ }
8276
8563
  setRange(
8277
8564
  (previous) => event.shiftKey && previous ? { anchor: previous.anchor, focus: address } : { anchor: address, focus: address }
8278
8565
  );
@@ -8334,12 +8621,13 @@ function useSpreadsheetGrid(options) {
8334
8621
  source: { top: source.r0, left: source.c0, bottom: source.r1, right: source.c1 },
8335
8622
  target
8336
8623
  });
8337
- void applyPatches(result.patches, "fill", "Filled");
8338
- announceSkips(result.skipped, result.summary);
8624
+ const screened = screenPatches(result.patches);
8625
+ void applyPatches(screened.accepted, "fill", "Filled");
8626
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8339
8627
  };
8340
8628
  window.addEventListener("mouseup", commit);
8341
8629
  return () => window.removeEventListener("mouseup", commit);
8342
- }, [options, fillRequestBase, applyPatches, announceSkips]);
8630
+ }, [options, fillRequestBase, applyPatches, announceSkips, screenPatches]);
8343
8631
  return {
8344
8632
  range,
8345
8633
  bounds,
@@ -8355,17 +8643,19 @@ function useSpreadsheetGrid(options) {
8355
8643
  isFillHandleCell,
8356
8644
  startFillDrag,
8357
8645
  canUndo: stack.past.length > 0,
8358
- canRedo: stack.future.length > 0
8646
+ canRedo: stack.future.length > 0,
8647
+ selectionTsv: selectionAsTsv,
8648
+ selectionAddresses
8359
8649
  };
8360
8650
  }
8361
8651
 
8362
8652
  // src/data-table/coverage.ts
8363
8653
  var PARTIAL_BADGE = "Partial";
8364
8654
  var UNKNOWN_TOTAL_BADGE = "Unknown total";
8365
- var NUMBERS = new Intl.NumberFormat("en-US");
8655
+ var NUMBERS2 = new Intl.NumberFormat("en-US");
8366
8656
  function formatNumber(value) {
8367
8657
  try {
8368
- return NUMBERS.format(value);
8658
+ return NUMBERS2.format(value);
8369
8659
  } catch {
8370
8660
  return String(value);
8371
8661
  }
@@ -9563,8 +9853,8 @@ function toNumber(value) {
9563
9853
  if (typeof value === "string") {
9564
9854
  const cleaned = value.replace(/[,\s$€£¥%]/g, "");
9565
9855
  if (cleaned === "") return null;
9566
- const n = Number(cleaned);
9567
- return Number.isFinite(n) ? n : null;
9856
+ const n2 = Number(cleaned);
9857
+ return Number.isFinite(n2) ? n2 : null;
9568
9858
  }
9569
9859
  return null;
9570
9860
  }
@@ -9602,14 +9892,14 @@ function toList(value) {
9602
9892
  }
9603
9893
  return null;
9604
9894
  }
9605
- function groupedNumber(n, options) {
9895
+ function groupedNumber(n2, options) {
9606
9896
  return new Intl.NumberFormat(void 0, {
9607
9897
  useGrouping: options.useGrouping !== false,
9608
9898
  ...options.precision != null ? {
9609
9899
  minimumFractionDigits: options.precision,
9610
9900
  maximumFractionDigits: options.precision
9611
9901
  } : { maximumFractionDigits: 10 }
9612
- }).format(n);
9902
+ }).format(n2);
9613
9903
  }
9614
9904
  function affix(text, options) {
9615
9905
  return `${options.prefix ?? ""}${text}${options.suffix ?? ""}`;
@@ -9743,8 +10033,8 @@ var DEFS = [
9743
10033
  numericAlign: true,
9744
10034
  optionKeys: ["useGrouping", "precision", "prefix", "suffix"],
9745
10035
  format: (v, o) => {
9746
- const n = toNumber(v);
9747
- return n === null ? null : affix(groupedNumber(n, o), o);
10036
+ const n2 = toNumber(v);
10037
+ return n2 === null ? null : affix(groupedNumber(n2, o), o);
9748
10038
  },
9749
10039
  parse: (raw) => toNumber(raw)
9750
10040
  },
@@ -9759,9 +10049,9 @@ var DEFS = [
9759
10049
  numericAlign: true,
9760
10050
  optionKeys: ["precision", "useGrouping", "prefix", "suffix"],
9761
10051
  format: (v, o) => {
9762
- const n = toNumber(v);
9763
- if (n === null) return null;
9764
- return affix(groupedNumber(n, { ...o, precision: o.precision ?? 2 }), o);
10052
+ const n2 = toNumber(v);
10053
+ if (n2 === null) return null;
10054
+ return affix(groupedNumber(n2, { ...o, precision: o.precision ?? 2 }), o);
9765
10055
  },
9766
10056
  parse: (raw) => toNumber(raw)
9767
10057
  },
@@ -9776,8 +10066,8 @@ var DEFS = [
9776
10066
  numericAlign: true,
9777
10067
  optionKeys: ["currency", "precision"],
9778
10068
  format: (v, o) => {
9779
- const n = toNumber(v);
9780
- if (n === null) return null;
10069
+ const n2 = toNumber(v);
10070
+ if (n2 === null) return null;
9781
10071
  const currency = (o.currency || "USD").toUpperCase();
9782
10072
  try {
9783
10073
  return new Intl.NumberFormat(void 0, {
@@ -9787,9 +10077,9 @@ var DEFS = [
9787
10077
  minimumFractionDigits: o.precision,
9788
10078
  maximumFractionDigits: o.precision
9789
10079
  } : {}
9790
- }).format(n);
10080
+ }).format(n2);
9791
10081
  } catch {
9792
- return `${groupedNumber(n, { precision: o.precision ?? 2 })} ${currency}`;
10082
+ return `${groupedNumber(n2, { precision: o.precision ?? 2 })} ${currency}`;
9793
10083
  }
9794
10084
  },
9795
10085
  parse: (raw) => toNumber(raw)
@@ -9805,9 +10095,9 @@ var DEFS = [
9805
10095
  numericAlign: true,
9806
10096
  optionKeys: ["percentScale", "precision"],
9807
10097
  format: (v, o) => {
9808
- const n = toNumber(v);
9809
- if (n === null) return null;
9810
- const shown = o.percentScale === "fraction" ? n * 100 : n;
10098
+ const n2 = toNumber(v);
10099
+ if (n2 === null) return null;
10100
+ const shown = o.percentScale === "fraction" ? n2 * 100 : n2;
9811
10101
  return `${groupedNumber(shown, o.precision === void 0 ? {} : { precision: o.precision })}%`;
9812
10102
  },
9813
10103
  parse: (raw) => toNumber(raw)
@@ -9823,9 +10113,9 @@ var DEFS = [
9823
10113
  numericAlign: true,
9824
10114
  optionKeys: ["durationUnit"],
9825
10115
  format: (v, o) => {
9826
- const n = toNumber(v);
9827
- if (n === null) return null;
9828
- return formatSignedDuration(n * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
10116
+ const n2 = toNumber(v);
10117
+ if (n2 === null) return null;
10118
+ return formatSignedDuration(n2 * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
9829
10119
  },
9830
10120
  parse: (raw) => toNumber(raw)
9831
10121
  },
@@ -9840,13 +10130,13 @@ var DEFS = [
9840
10130
  numericAlign: true,
9841
10131
  optionKeys: ["useGrouping", "prefix", "suffix"],
9842
10132
  format: (v, o) => {
9843
- const n = toNumber(v);
9844
- if (n === null) return null;
9845
- return affix(groupedNumber(Math.trunc(n), { ...o, precision: 0 }), o);
10133
+ const n2 = toNumber(v);
10134
+ if (n2 === null) return null;
10135
+ return affix(groupedNumber(Math.trunc(n2), { ...o, precision: 0 }), o);
9846
10136
  },
9847
10137
  parse: (raw) => {
9848
- const n = toNumber(raw);
9849
- return n === null ? null : Math.trunc(n);
10138
+ const n2 = toNumber(raw);
10139
+ return n2 === null ? null : Math.trunc(n2);
9850
10140
  }
9851
10141
  },
9852
10142
  {
@@ -9860,15 +10150,15 @@ var DEFS = [
9860
10150
  rich: true,
9861
10151
  optionKeys: ["ratingMax"],
9862
10152
  format: (v, o) => {
9863
- const n = toNumber(v);
9864
- if (n === null) return null;
10153
+ const n2 = toNumber(v);
10154
+ if (n2 === null) return null;
9865
10155
  const max = o.ratingMax ?? 5;
9866
- if (n < 0 || n > max) return null;
9867
- return `${n} / ${max}`;
10156
+ if (n2 < 0 || n2 > max) return null;
10157
+ return `${n2} / ${max}`;
9868
10158
  },
9869
10159
  parse: (raw) => {
9870
- const n = toNumber(raw);
9871
- return n === null ? null : Math.round(n);
10160
+ const n2 = toNumber(raw);
10161
+ return n2 === null ? null : Math.round(n2);
9872
10162
  }
9873
10163
  },
9874
10164
  {
@@ -9881,12 +10171,12 @@ var DEFS = [
9881
10171
  editor: "number",
9882
10172
  numericAlign: true,
9883
10173
  format: (v) => {
9884
- const n = toNumber(v);
9885
- return n === null ? null : (0, import_format3.formatFileSize)(n);
10174
+ const n2 = toNumber(v);
10175
+ return n2 === null ? null : (0, import_format3.formatFileSize)(n2);
9886
10176
  },
9887
10177
  parse: (raw) => {
9888
- const n = toNumber(raw);
9889
- return n === null ? null : Math.trunc(n);
10178
+ const n2 = toNumber(raw);
10179
+ return n2 === null ? null : Math.trunc(n2);
9890
10180
  }
9891
10181
  },
9892
10182
  // ── Choice ────────────────────────────────────────────────────────────────
@@ -10298,6 +10588,17 @@ function DirtySavePill({
10298
10588
  // src/data-table/EditableTableCell.tsx
10299
10589
  var import_react19 = require("react");
10300
10590
  var import_jsx_runtime39 = require("react/jsx-runtime");
10591
+ function CellRefusal({ reason }) {
10592
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10593
+ "p",
10594
+ {
10595
+ role: "alert",
10596
+ "data-matrx-cell-refusal": "true",
10597
+ className: "mt-1 text-[11px] leading-snug text-amber-700 dark:text-amber-400",
10598
+ children: reason
10599
+ }
10600
+ );
10601
+ }
10301
10602
  function CellEditShell({
10302
10603
  href,
10303
10604
  display,
@@ -10344,12 +10645,17 @@ function EditableTableCell({
10344
10645
  className,
10345
10646
  href,
10346
10647
  editTrigger = "click",
10347
- editRequest
10648
+ editRequest,
10649
+ editSeed
10348
10650
  }) {
10349
10651
  const [open, setOpen] = (0, import_react19.useState)(false);
10652
+ const [refusal, setRefusal] = (0, import_react19.useState)(null);
10350
10653
  (0, import_react19.useEffect)(() => {
10351
10654
  if (editRequest !== void 0) setOpen(true);
10352
10655
  }, [editRequest]);
10656
+ (0, import_react19.useEffect)(() => {
10657
+ if (!open) setRefusal(null);
10658
+ }, [open]);
10353
10659
  const usePencil = Boolean(href) || editTrigger === "pencil";
10354
10660
  if (editType === "string") {
10355
10661
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
@@ -10362,7 +10668,8 @@ function EditableTableCell({
10362
10668
  ...className === void 0 ? {} : { className },
10363
10669
  ...href === void 0 ? {} : { href },
10364
10670
  usePencil,
10365
- ...editRequest === void 0 ? {} : { editRequest }
10671
+ ...editRequest === void 0 ? {} : { editRequest },
10672
+ ...editSeed === void 0 || editSeed === null ? {} : { editSeed }
10366
10673
  }
10367
10674
  );
10368
10675
  }
@@ -10407,25 +10714,33 @@ function EditableTableCell({
10407
10714
  ) }) }),
10408
10715
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(TooltipContent, { children: "Edit value" })
10409
10716
  ] }),
10410
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10717
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
10411
10718
  PopoverContent,
10412
10719
  {
10413
10720
  align: "start",
10414
10721
  className: "w-56 p-2",
10415
10722
  onClick: (e) => e.stopPropagation(),
10416
- children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10417
- TypedEditor,
10418
- {
10419
- editType,
10420
- value,
10421
- editOptions,
10422
- onCommit: (next) => {
10423
- onCommit(next);
10424
- setOpen(false);
10425
- },
10426
- onCancel: () => setOpen(false)
10427
- }
10428
- )
10723
+ children: [
10724
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10725
+ TypedEditor,
10726
+ {
10727
+ editType,
10728
+ value,
10729
+ editOptions,
10730
+ onCommit: (next) => {
10731
+ const refused = onCommit(next);
10732
+ if (typeof refused === "string" && refused) {
10733
+ setRefusal(refused);
10734
+ return;
10735
+ }
10736
+ setRefusal(null);
10737
+ setOpen(false);
10738
+ },
10739
+ onCancel: () => setOpen(false)
10740
+ }
10741
+ ),
10742
+ refusal ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CellRefusal, { reason: refusal }) : null
10743
+ ]
10429
10744
  }
10430
10745
  )
10431
10746
  ] });
@@ -10438,17 +10753,31 @@ function InlineStringEditor({
10438
10753
  className,
10439
10754
  href,
10440
10755
  usePencil,
10441
- editRequest
10756
+ editRequest,
10757
+ editSeed
10442
10758
  }) {
10443
10759
  const [editing, setEditing] = (0, import_react19.useState)(false);
10444
10760
  const [draft, setDraft] = (0, import_react19.useState)(value);
10761
+ const [refusal, setRefusal] = (0, import_react19.useState)(null);
10445
10762
  const handledEditRequest = (0, import_react19.useRef)(void 0);
10763
+ const seedRef = (0, import_react19.useRef)(editSeed);
10764
+ seedRef.current = editSeed;
10446
10765
  (0, import_react19.useEffect)(() => {
10447
10766
  if (editRequest === void 0 || handledEditRequest.current === editRequest) return;
10448
10767
  handledEditRequest.current = editRequest;
10449
- setDraft(value);
10768
+ setDraft(seedRef.current ?? value);
10769
+ setRefusal(null);
10450
10770
  setEditing(true);
10451
10771
  }, [editRequest, value]);
10772
+ const commit = (next) => {
10773
+ const refused = onCommit(next);
10774
+ if (typeof refused === "string" && refused) {
10775
+ setRefusal(refused);
10776
+ return false;
10777
+ }
10778
+ setRefusal(null);
10779
+ return true;
10780
+ };
10452
10781
  if (!editing) {
10453
10782
  if (usePencil) {
10454
10783
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
@@ -10499,32 +10828,38 @@ function InlineStringEditor({
10499
10828
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(TooltipContent, { children: "Click to edit" })
10500
10829
  ] });
10501
10830
  }
10502
- return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10503
- Input,
10504
- {
10505
- autoFocus: true,
10506
- value: draft,
10507
- onClick: (e) => e.stopPropagation(),
10508
- onChange: (e) => setDraft(e.target.value),
10509
- onBlur: () => {
10510
- onCommit(draft);
10511
- setEditing(false);
10512
- },
10513
- onKeyDown: (e) => {
10514
- if (e.key === "Enter") {
10515
- e.preventDefault();
10516
- onCommit(draft);
10517
- setEditing(false);
10518
- }
10519
- if (e.key === "Escape") {
10520
- e.preventDefault();
10521
- setEditing(false);
10522
- }
10523
- },
10524
- className: "h-7 text-sm",
10525
- style: { fontSize: "16px" }
10526
- }
10527
- );
10831
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block w-full", children: [
10832
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10833
+ Input,
10834
+ {
10835
+ autoFocus: true,
10836
+ value: draft,
10837
+ onClick: (e) => e.stopPropagation(),
10838
+ onChange: (e) => {
10839
+ setDraft(e.target.value);
10840
+ if (refusal) setRefusal(null);
10841
+ },
10842
+ onBlur: () => {
10843
+ if (commit(draft)) setEditing(false);
10844
+ },
10845
+ onKeyDown: (e) => {
10846
+ if (e.key === "Enter") {
10847
+ e.preventDefault();
10848
+ if (commit(draft)) setEditing(false);
10849
+ }
10850
+ if (e.key === "Escape") {
10851
+ e.preventDefault();
10852
+ setRefusal(null);
10853
+ setEditing(false);
10854
+ }
10855
+ },
10856
+ "aria-invalid": refusal ? true : void 0,
10857
+ className: cn("h-7 text-sm", refusal && "border-amber-500 focus-visible:ring-amber-500"),
10858
+ style: { fontSize: "16px" }
10859
+ }
10860
+ ),
10861
+ refusal ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CellRefusal, { reason: refusal }) : null
10862
+ ] });
10528
10863
  }
10529
10864
  function TypedEditor({
10530
10865
  editType,
@@ -10709,8 +11044,8 @@ function NumberEditor({
10709
11044
  style: { fontSize: "16px" },
10710
11045
  onKeyDown: (e) => {
10711
11046
  if (e.key === "Enter") {
10712
- const n = Number(draft);
10713
- if (Number.isFinite(n)) onCommit(n);
11047
+ const n2 = Number(draft);
11048
+ if (Number.isFinite(n2)) onCommit(n2);
10714
11049
  }
10715
11050
  if (e.key === "Escape") onCancel();
10716
11051
  }
@@ -10737,8 +11072,8 @@ function NumberEditor({
10737
11072
  className: "h-7",
10738
11073
  "aria-label": "Apply value",
10739
11074
  onClick: () => {
10740
- const n = Number(draft);
10741
- if (Number.isFinite(n)) onCommit(n);
11075
+ const n2 = Number(draft);
11076
+ if (Number.isFinite(n2)) onCommit(n2);
10742
11077
  },
10743
11078
  children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Check, { className: "h-3.5 w-3.5" })
10744
11079
  }
@@ -11546,6 +11881,7 @@ function MatrxDataTableCore({
11546
11881
  virtualize,
11547
11882
  coverage,
11548
11883
  spreadsheet,
11884
+ facets,
11549
11885
  rowHeight: explicitRowHeight
11550
11886
  }) {
11551
11887
  const appendQuery = query?.mode === "controlled-append" ? query : null;
@@ -12603,9 +12939,92 @@ function MatrxDataTableCore({
12603
12939
  ...spreadsheet.undoDepth === void 0 ? {} : { undoDepth: spreadsheet.undoDepth },
12604
12940
  ...spreadsheet.fillHandle === void 0 ? {} : { fillHandle: spreadsheet.fillHandle },
12605
12941
  ...spreadsheet.paste === void 0 ? {} : { paste: spreadsheet.paste },
12606
- notify: spreadsheet.notify ?? ((message) => toast.success(message))
12942
+ notify: spreadsheet.notify ?? ((message) => toast.success(message)),
12943
+ onEditRequest: (address, seed) => requestCellEditRef.current(address, seed),
12944
+ ...spreadsheet.validateCell ? { validateCell: spreadsheet.validateCell } : {}
12607
12945
  } : null
12608
12946
  );
12947
+ const sheetRef = (0, import_react21.useRef)(sheet);
12948
+ sheetRef.current = sheet;
12949
+ const searchValueRef = (0, import_react21.useRef)(searchValue);
12950
+ searchValueRef.current = searchValue;
12951
+ const facetsEnabled = Boolean(facets && facets.enabled !== false);
12952
+ const [facetState, setFacetState] = (0, import_react21.useState)({});
12953
+ const facetReadCell = (0, import_react21.useCallback)(
12954
+ (row, colId) => {
12955
+ if (facets?.readCell) return facets.readCell(row, colId);
12956
+ const col = visibleColumns.find((candidate) => columnId(candidate) === colId);
12957
+ return col ? getFilterValue(row, col) : void 0;
12958
+ },
12959
+ [facets, visibleColumns]
12960
+ );
12961
+ const facetInputsRef = (0, import_react21.useRef)({ data, facets, facetReadCell });
12962
+ facetInputsRef.current = { data, facets, facetReadCell };
12963
+ const facetRequestRef = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
12964
+ const requestFacets = (0, import_react21.useCallback)((colId) => {
12965
+ const { data: rows, facets: config, facetReadCell: readCell } = facetInputsRef.current;
12966
+ if (!config || config.enabled === false) return;
12967
+ const complete = localFacetsAreComplete(rows.length, config.totalRows);
12968
+ const limit = config.limit;
12969
+ if (complete || !config.source) {
12970
+ setFacetState((current) => ({
12971
+ ...current,
12972
+ [colId]: {
12973
+ loading: false,
12974
+ data: computeColumnFacets({
12975
+ columnId: colId,
12976
+ rows,
12977
+ readValue: readCell,
12978
+ ...limit === void 0 ? {} : { limit },
12979
+ complete
12980
+ })
12981
+ }
12982
+ }));
12983
+ return;
12984
+ }
12985
+ const ticket = (facetRequestRef.current.get(colId) ?? 0) + 1;
12986
+ facetRequestRef.current.set(colId, ticket);
12987
+ setFacetState((current) => ({
12988
+ ...current,
12989
+ [colId]: { loading: true, data: current[colId]?.data ?? null }
12990
+ }));
12991
+ void Promise.resolve().then(
12992
+ () => config.source?.({ columnId: colId, search: searchValueRef.current, limit: limit ?? 200 })
12993
+ ).then((answer) => {
12994
+ if (facetRequestRef.current.get(colId) !== ticket) return;
12995
+ if (!answer) {
12996
+ setFacetState((current) => ({
12997
+ ...current,
12998
+ [colId]: {
12999
+ loading: false,
13000
+ data: computeColumnFacets({
13001
+ columnId: colId,
13002
+ rows: facetInputsRef.current.data,
13003
+ readValue: facetInputsRef.current.facetReadCell,
13004
+ ...limit === void 0 ? {} : { limit },
13005
+ complete: false
13006
+ })
13007
+ }
13008
+ }));
13009
+ return;
13010
+ }
13011
+ setFacetState((current) => ({
13012
+ ...current,
13013
+ [colId]: { loading: false, data: { ...answer, answeredBy: "source", complete: true } }
13014
+ }));
13015
+ }).catch((error) => {
13016
+ if (facetRequestRef.current.get(colId) !== ticket) return;
13017
+ setFacetState((current) => ({
13018
+ ...current,
13019
+ [colId]: { loading: false, data: null, error: describeFacetFailure(error) }
13020
+ }));
13021
+ });
13022
+ }, []);
13023
+ (0, import_react21.useEffect)(() => {
13024
+ if (!facetsEnabled) return;
13025
+ setFacetState({});
13026
+ facetRequestRef.current = /* @__PURE__ */ new Map();
13027
+ }, [facetsEnabled, data, searchValue]);
12609
13028
  (0, import_react21.useEffect)(() => {
12610
13029
  if (!controlledQuery || controlledQuery.state.page === safePage) return;
12611
13030
  emitControlledQueryChange({ page: safePage });
@@ -12650,11 +13069,11 @@ function MatrxDataTableCore({
12650
13069
  });
12651
13070
  const hasActiveFilters = activeFilterCount > 0 || Boolean(searchValue.trim()) || intelligentSearchEnabled && searchScope !== "all" || Boolean(anyOfValue.trim()) || completeLayeredFilterRules(layeredFilters).length > 0 || facetActive;
12652
13071
  const changeCount = (0, import_react21.useMemo)(() => {
12653
- let n = 0;
13072
+ let n2 = 0;
12654
13073
  for (const fields of Object.values(edits)) {
12655
- n += Object.keys(fields).length;
13074
+ n2 += Object.keys(fields).length;
12656
13075
  }
12657
- return n;
13076
+ return n2;
12658
13077
  }, [edits]);
12659
13078
  const showSearch = toolbar?.search !== false;
12660
13079
  const openDetail = (row) => {
@@ -12733,6 +13152,23 @@ function MatrxDataTableCore({
12733
13152
  icon: void 0
12734
13153
  } : emptyState;
12735
13154
  const commitCell = (rowId, field, next) => {
13155
+ if (edit?.validate) {
13156
+ const row = data.find((candidate) => getRowId(candidate) === rowId);
13157
+ if (row) {
13158
+ let refusal;
13159
+ try {
13160
+ refusal = edit.validate({
13161
+ row: applyRowEdits(row, edits[rowId]),
13162
+ rowId,
13163
+ columnId: field,
13164
+ value: next
13165
+ });
13166
+ } catch {
13167
+ refusal = void 0;
13168
+ }
13169
+ if (refusal) return refusal;
13170
+ }
13171
+ }
12736
13172
  const scope = editScopeKey;
12737
13173
  const nextEdits = { [rowId]: { [field]: next } };
12738
13174
  const revision = setCellEdit(rowId, field, next);
@@ -12777,9 +13213,29 @@ function MatrxDataTableCore({
12777
13213
  setEditRequest((current) => ({
12778
13214
  rowId: getRowId(row),
12779
13215
  field,
12780
- nonce: (current?.nonce ?? 0) + 1
13216
+ nonce: (current?.nonce ?? 0) + 1,
13217
+ seed: null
12781
13218
  }));
12782
13219
  };
13220
+ const requestCellEdit = (address, seed) => {
13221
+ if (!editEnabled) return false;
13222
+ const row = data.find((candidate) => getRowId(candidate) === address.rowId);
13223
+ if (!row) return false;
13224
+ const column = visibleColumns.find(
13225
+ (candidate) => columnId(candidate) === address.fieldName
13226
+ );
13227
+ if (!column?.editable || !(column.editableIf?.(row) ?? true)) return false;
13228
+ const field = column.accessorKey ? String(column.accessorKey) : columnId(column);
13229
+ setEditRequest((current) => ({
13230
+ rowId: address.rowId,
13231
+ field,
13232
+ nonce: (current?.nonce ?? 0) + 1,
13233
+ seed
13234
+ }));
13235
+ return true;
13236
+ };
13237
+ const requestCellEditRef = (0, import_react21.useRef)(requestCellEdit);
13238
+ requestCellEditRef.current = requestCellEdit;
12783
13239
  const buildRecordControls = (row) => {
12784
13240
  const rowId = getRowId(row);
12785
13241
  const pending = edits[rowId];
@@ -13020,17 +13476,78 @@ function MatrxDataTableCore({
13020
13476
  setEdits({});
13021
13477
  edit?.onCancel?.();
13022
13478
  };
13023
- (0, import_react21.useEffect)(() => {
13024
- if (!host.rowContextRegistry || !contextMenu && !host.createDefaultRowContext) return;
13025
- return host.rowContextRegistry.register(tableContextMenuInstanceId, (rowId) => {
13479
+ const resolveMenuLevel = (target) => {
13480
+ const rowFor = (rowId) => {
13026
13481
  const row = data.find((candidate) => getRowId(candidate) === rowId);
13027
13482
  if (!row) return null;
13028
- const pending = edits[rowId];
13029
- const displayRow = applyRowEdits(row, pending);
13030
- const controls = buildRecordControls(displayRow);
13031
- return contextMenu?.resolveRowContext(displayRow, controls) ?? host.createDefaultRowContext?.(displayRow, controls) ?? null;
13032
- });
13033
- }, [buildRecordControls, contextMenu, data, getRowId, host.createDefaultRowContext, host.rowContextRegistry, tableContextMenuInstanceId]);
13483
+ return applyRowEdits(row, edits[rowId]);
13484
+ };
13485
+ const columnFor = (id) => visibleColumns.find((col) => columnId(col) === id);
13486
+ const labelFor = (col, id) => col?.label ?? (typeof col?.header === "string" ? col.header : id);
13487
+ const fromHost = (payload) => host.createDefaultMenuContext?.(payload) ?? null;
13488
+ switch (target.level) {
13489
+ case "row": {
13490
+ if (!target.rowId) return null;
13491
+ const displayRow = rowFor(target.rowId);
13492
+ if (!displayRow) return null;
13493
+ const controls = buildRecordControls(displayRow);
13494
+ return contextMenu?.resolveRowContext?.(displayRow, controls) ?? fromHost({ level: "row", row: displayRow, rowId: target.rowId, controls });
13495
+ }
13496
+ case "cell": {
13497
+ if (!target.rowId || !target.columnId) return null;
13498
+ const displayRow = rowFor(target.rowId);
13499
+ const col = columnFor(target.columnId);
13500
+ if (!displayRow || !col) return null;
13501
+ const controls = buildRecordControls(displayRow);
13502
+ const value = spreadsheet?.readCell ? spreadsheet.readCell(displayRow, target.columnId) : getCellValue(displayRow, col);
13503
+ const args = {
13504
+ row: displayRow,
13505
+ rowId: target.rowId,
13506
+ columnId: target.columnId,
13507
+ columnLabel: labelFor(col, target.columnId),
13508
+ value,
13509
+ text: stringifyCellValue(value),
13510
+ controls
13511
+ };
13512
+ return contextMenu?.resolveCellContext?.(args) ?? fromHost({ level: "cell", ...args });
13513
+ }
13514
+ case "column": {
13515
+ if (!target.columnId) return null;
13516
+ const col = columnFor(target.columnId);
13517
+ if (!col) return null;
13518
+ const args = {
13519
+ columnId: target.columnId,
13520
+ columnLabel: labelFor(col, target.columnId),
13521
+ values: paginated.map(
13522
+ (row) => getCellValue(applyRowEdits(row, edits[getRowId(row)]), col)
13523
+ )
13524
+ };
13525
+ return contextMenu?.resolveColumnContext?.(args) ?? fromHost({ level: "column", ...args });
13526
+ }
13527
+ case "selection": {
13528
+ const addresses = sheetRef.current.selectionAddresses();
13529
+ const cellCount = addresses.rowIds.length * addresses.columnIds.length;
13530
+ if (cellCount <= 1) {
13531
+ return resolveMenuLevel({ ...target, level: "cell" });
13532
+ }
13533
+ const args = { ...addresses, tsv: sheetRef.current.selectionTsv(), cellCount };
13534
+ return contextMenu?.resolveSelectionContext?.(args) ?? fromHost({ level: "selection", ...args });
13535
+ }
13536
+ case "table": {
13537
+ const args = { rowCount: data.length, columnIds: visibleColumns.map((col) => columnId(col)) };
13538
+ return contextMenu?.resolveTableContext?.(args) ?? fromHost({ level: "table", ...args });
13539
+ }
13540
+ }
13541
+ };
13542
+ const resolveMenuLevelRef = (0, import_react21.useRef)(resolveMenuLevel);
13543
+ resolveMenuLevelRef.current = resolveMenuLevel;
13544
+ (0, import_react21.useEffect)(() => {
13545
+ if (!host.rowContextRegistry) return;
13546
+ return host.rowContextRegistry.register(
13547
+ tableContextMenuInstanceId,
13548
+ (target) => resolveMenuLevelRef.current(target)
13549
+ );
13550
+ }, [host.rowContextRegistry, tableContextMenuInstanceId]);
13034
13551
  const showActionsCol = windowEnabled || Boolean(rowActions) || showInlineRowCopy;
13035
13552
  const selectedIdSet = (0, import_react21.useMemo)(
13036
13553
  () => new Set(selection?.selectedIds ?? []),
@@ -13112,8 +13629,19 @@ function MatrxDataTableCore({
13112
13629
  dirty: Boolean(rowEdits && field in rowEdits),
13113
13630
  onCommit: (next) => commitCell(id, field, next),
13114
13631
  ...href === void 0 ? {} : { href },
13115
- ...col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13116
- ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce } : {}
13632
+ ...spreadsheetEnabled ? {
13633
+ // 🚨 THE CLICK LAW, enforced where the two paths meet. In a grid
13634
+ // with spreadsheet gestures a single click SELECTS a cell — it
13635
+ // may never drop a person into a free-text buffer, because the
13636
+ // same click is how they start a range, and an editor that opens
13637
+ // under the pointer eats the drag and the next keystroke. So the
13638
+ // editor opens the way it does in Excel: Enter, F2, the hover
13639
+ // pencil, or simply typing. An explicit `editTrigger: "click"`
13640
+ // does not override this; the law is the table's, not the
13641
+ // column's.
13642
+ editTrigger: "pencil"
13643
+ } : col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13644
+ ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce, editSeed: editRequest.seed } : {}
13117
13645
  }
13118
13646
  );
13119
13647
  }
@@ -13871,6 +14399,7 @@ function MatrxDataTableCore({
13871
14399
  "th",
13872
14400
  {
13873
14401
  "data-matrx-table-column-id": id,
14402
+ "data-matrx-header-column-id": id,
13874
14403
  "data-matrx-column-drag-shadow": isColumnDragShadow ? "true" : void 0,
13875
14404
  "aria-sort": isSorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
13876
14405
  className: cn(
@@ -13907,6 +14436,12 @@ function MatrxDataTableCore({
13907
14436
  {
13908
14437
  label: col.header,
13909
14438
  labelText: headerLabel,
14439
+ ...facetsEnabled ? {
14440
+ facets: facetState[id]?.data ?? null,
14441
+ facetsLoading: facetState[id]?.loading ?? false,
14442
+ ...facetState[id]?.error === void 0 ? {} : { facetsError: facetState[id]?.error },
14443
+ onFilterOpen: () => requestFacets(id)
14444
+ } : {},
13910
14445
  sortable: col.sortable !== false,
13911
14446
  isSorted: Boolean(isSorted),
13912
14447
  sortDirection: sort?.direction ?? defaultSortDirection,
@@ -14581,8 +15116,8 @@ function MatrxDataTableCore({
14581
15116
  itemsPerPage: pageSize === 0 && allRowsAreLoaded ? 0 : effectivePageSize,
14582
15117
  currentPage: safePage,
14583
15118
  onPageChange: setPage,
14584
- onItemsPerPageChange: (n) => {
14585
- setPageSize(n);
15119
+ onItemsPerPageChange: (n2) => {
15120
+ setPageSize(n2);
14586
15121
  if (!controlledQuery) setPage(1);
14587
15122
  },
14588
15123
  pageSizeOptions: pageSizeOptions.filter((size) => size > 0),