@ai-matrx/design-system 0.25.0 → 0.27.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 (81) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/data-table/copy-helpers.cjs.map +1 -1
  3. package/dist/data-table/copy-helpers.d.cts +3 -1
  4. package/dist/data-table/copy-helpers.d.ts +3 -1
  5. package/dist/data-table/copy-helpers.js.map +1 -1
  6. package/dist/data-table/data-row-inspector.cjs.map +1 -1
  7. package/dist/data-table/data-row-inspector.js.map +1 -1
  8. package/dist/data-table/data-row-window.cjs.map +1 -1
  9. package/dist/data-table/data-row-window.js.map +1 -1
  10. package/dist/data-table/date-filter.cjs +145 -0
  11. package/dist/data-table/date-filter.cjs.map +1 -0
  12. package/dist/data-table/date-filter.d.cts +31 -0
  13. package/dist/data-table/date-filter.d.ts +31 -0
  14. package/dist/data-table/date-filter.js +125 -0
  15. package/dist/data-table/date-filter.js.map +1 -0
  16. package/dist/data-table/facets.cjs +118 -0
  17. package/dist/data-table/facets.cjs.map +1 -0
  18. package/dist/data-table/facets.d.cts +100 -0
  19. package/dist/data-table/facets.d.ts +100 -0
  20. package/dist/data-table/facets.js +98 -0
  21. package/dist/data-table/facets.js.map +1 -0
  22. package/dist/data-table/filter-engine.cjs +88 -0
  23. package/dist/data-table/filter-engine.cjs.map +1 -1
  24. package/dist/data-table/filter-engine.d.cts +3 -1
  25. package/dist/data-table/filter-engine.d.ts +3 -1
  26. package/dist/data-table/filter-engine.js +88 -0
  27. package/dist/data-table/filter-engine.js.map +1 -1
  28. package/dist/data-table/host.cjs.map +1 -1
  29. package/dist/data-table/host.d.cts +14 -3
  30. package/dist/data-table/host.d.ts +14 -3
  31. package/dist/data-table/host.js.map +1 -1
  32. package/dist/data-table/index.cjs +918 -152
  33. package/dist/data-table/index.cjs.map +1 -1
  34. package/dist/data-table/index.d.cts +5 -2
  35. package/dist/data-table/index.d.ts +5 -2
  36. package/dist/data-table/index.js +956 -189
  37. package/dist/data-table/index.js.map +1 -1
  38. package/dist/data-table/infer-filter.cjs +2 -0
  39. package/dist/data-table/infer-filter.cjs.map +1 -1
  40. package/dist/data-table/infer-filter.d.cts +4 -2
  41. package/dist/data-table/infer-filter.d.ts +4 -2
  42. package/dist/data-table/infer-filter.js +2 -0
  43. package/dist/data-table/infer-filter.js.map +1 -1
  44. package/dist/data-table/layered-filters.d.cts +3 -1
  45. package/dist/data-table/layered-filters.d.ts +3 -1
  46. package/dist/data-table/menu-targets.cjs +77 -0
  47. package/dist/data-table/menu-targets.cjs.map +1 -0
  48. package/dist/data-table/menu-targets.d.cts +108 -0
  49. package/dist/data-table/menu-targets.d.ts +108 -0
  50. package/dist/data-table/menu-targets.js +57 -0
  51. package/dist/data-table/menu-targets.js.map +1 -0
  52. package/dist/data-table/pagination.cjs.map +1 -1
  53. package/dist/data-table/pagination.js.map +1 -1
  54. package/dist/data-table/query-control.cjs.map +1 -1
  55. package/dist/data-table/query-control.d.cts +3 -1
  56. package/dist/data-table/query-control.d.ts +3 -1
  57. package/dist/data-table/query-control.js.map +1 -1
  58. package/dist/data-table/spreadsheet.cjs +61 -13
  59. package/dist/data-table/spreadsheet.cjs.map +1 -1
  60. package/dist/data-table/spreadsheet.d.cts +36 -1
  61. package/dist/data-table/spreadsheet.d.ts +36 -1
  62. package/dist/data-table/spreadsheet.js +61 -13
  63. package/dist/data-table/spreadsheet.js.map +1 -1
  64. package/dist/data-table/types.cjs.map +1 -1
  65. package/dist/data-table/types.d.cts +3 -1
  66. package/dist/data-table/types.d.ts +3 -1
  67. package/dist/data-table/url-state.d.cts +3 -1
  68. package/dist/data-table/url-state.d.ts +3 -1
  69. package/dist/data-table/uuid-cell.cjs.map +1 -1
  70. package/dist/data-table/uuid-cell.js.map +1 -1
  71. package/dist/data-table/xlsx.cjs.map +1 -1
  72. package/dist/data-table/xlsx.d.cts +3 -1
  73. package/dist/data-table/xlsx.d.ts +3 -1
  74. package/dist/data-table/xlsx.js.map +1 -1
  75. package/dist/field-formats/index.cjs +15 -0
  76. package/dist/field-formats/index.cjs.map +1 -1
  77. package/dist/field-formats/index.js +15 -0
  78. package/dist/field-formats/index.js.map +1 -1
  79. package/dist/{types-nlLCMiqd.d.ts → types-D9JBR6ZU.d.ts} +118 -5
  80. package/dist/{types-CmQrOz-T.d.cts → types-jQddSUnj.d.cts} +118 -5
  81. package/package.json +31 -1
@@ -58,6 +58,8 @@ __export(data_table_exports, {
58
58
  SavedViewsControl: () => SavedViewsControl,
59
59
  TABLE_STYLE_METADATA_KEY: () => TABLE_STYLE_METADATA_KEY,
60
60
  TABLE_STYLE_VERSION: () => TABLE_STYLE_VERSION,
61
+ TIME_RANGE_PRESETS: () => TIME_RANGE_PRESETS,
62
+ TIME_RANGE_PRESET_VALUES: () => TIME_RANGE_PRESET_VALUES,
61
63
  TableTitleRow: () => TableTitleRow,
62
64
  TableViewTabs: () => TableViewTabs,
63
65
  UNKNOWN_TOTAL_BADGE: () => UNKNOWN_TOTAL_BADGE,
@@ -81,6 +83,7 @@ __export(data_table_exports, {
81
83
  computeVariableWindow: () => computeVariableWindow,
82
84
  computeWindow: () => computeWindow,
83
85
  createUndoStack: () => createUndoStack,
86
+ dateFilterBounds: () => dateFilterBounds,
84
87
  describeCoverage: () => describeCoverage,
85
88
  detectSeries: () => detectSeries,
86
89
  directClickKinds: () => directClickKinds,
@@ -5131,6 +5134,124 @@ function layeredFilterRuleSummary(rule, fields) {
5131
5134
  return rule.operator === "between" ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? ""}` : `${fieldLabel} ${operator} ${value}`;
5132
5135
  }
5133
5136
 
5137
+ // src/data-table/date-filter.ts
5138
+ var TIME_RANGE_PRESET_VALUES = [
5139
+ "all",
5140
+ "1m",
5141
+ "15m",
5142
+ "30m",
5143
+ "1h",
5144
+ "2h",
5145
+ "6h",
5146
+ "12h",
5147
+ "today",
5148
+ "yesterday",
5149
+ "this_week",
5150
+ "this_month",
5151
+ "this_year",
5152
+ "older_1m",
5153
+ "older_15m",
5154
+ "older_30m",
5155
+ "older_1h",
5156
+ "older_2h",
5157
+ "older_6h",
5158
+ "older_12h",
5159
+ "older_today",
5160
+ "older_this_week",
5161
+ "older_this_month",
5162
+ "older_this_year"
5163
+ ];
5164
+ var TIME_RANGE_PRESETS = [
5165
+ { value: "all", label: "All time", group: "all" },
5166
+ { value: "1m", label: "Last minute", group: "recent" },
5167
+ { value: "15m", label: "Last 15 minutes", group: "recent" },
5168
+ { value: "30m", label: "Last 30 minutes", group: "recent" },
5169
+ { value: "1h", label: "Last hour", group: "recent" },
5170
+ { value: "2h", label: "Last 2 hours", group: "recent" },
5171
+ { value: "6h", label: "Last 6 hours", group: "recent" },
5172
+ { value: "12h", label: "Last 12 hours", group: "recent" },
5173
+ { value: "today", label: "Today", group: "calendar" },
5174
+ { value: "yesterday", label: "Yesterday", group: "calendar" },
5175
+ { value: "this_week", label: "This week", group: "calendar" },
5176
+ { value: "this_month", label: "This month", group: "calendar" },
5177
+ { value: "this_year", label: "This year", group: "calendar" },
5178
+ { value: "older_1m", label: "Older than 1 minute", group: "older" },
5179
+ { value: "older_15m", label: "Older than 15 minutes", group: "older" },
5180
+ { value: "older_30m", label: "Older than 30 minutes", group: "older" },
5181
+ { value: "older_1h", label: "Older than 1 hour", group: "older" },
5182
+ { value: "older_2h", label: "Older than 2 hours", group: "older" },
5183
+ { value: "older_6h", label: "Older than 6 hours", group: "older" },
5184
+ { value: "older_12h", label: "Older than 12 hours", group: "older" },
5185
+ { value: "older_today", label: "Before today", group: "older" },
5186
+ { value: "older_this_week", label: "Before this week", group: "older" },
5187
+ { value: "older_this_month", label: "Before this month", group: "older" },
5188
+ { value: "older_this_year", label: "Before this year", group: "older" }
5189
+ ];
5190
+ function startOfDay(date) {
5191
+ const next = new Date(date);
5192
+ next.setHours(0, 0, 0, 0);
5193
+ return next;
5194
+ }
5195
+ function startOfWeek(date) {
5196
+ const next = startOfDay(date);
5197
+ next.setDate(next.getDate() - (next.getDay() === 0 ? 6 : next.getDay() - 1));
5198
+ return next;
5199
+ }
5200
+ function startOfMonth(date) {
5201
+ return new Date(date.getFullYear(), date.getMonth(), 1);
5202
+ }
5203
+ function startOfYear(date) {
5204
+ return new Date(date.getFullYear(), 0, 1);
5205
+ }
5206
+ function validDate(value) {
5207
+ if (value === void 0) return void 0;
5208
+ const date = value instanceof Date ? new Date(value) : new Date(value);
5209
+ return Number.isNaN(date.valueOf()) ? void 0 : date;
5210
+ }
5211
+ function dateFilterBounds(filter, now = /* @__PURE__ */ new Date()) {
5212
+ if (!filter.preset) {
5213
+ return {
5214
+ ...filter.since === void 0 ? {} : { since: filter.since },
5215
+ ...filter.until === void 0 ? {} : { until: filter.until }
5216
+ };
5217
+ }
5218
+ if (filter.preset === "all") return {};
5219
+ const anchor = validDate(filter.anchor) ?? validDate(now) ?? /* @__PURE__ */ new Date();
5220
+ const ago = (milliseconds) => new Date(anchor.valueOf() - milliseconds).toISOString();
5221
+ const recent = {
5222
+ "1m": 6e4,
5223
+ "15m": 15 * 6e4,
5224
+ "30m": 30 * 6e4,
5225
+ "1h": 60 * 6e4,
5226
+ "2h": 2 * 60 * 6e4,
5227
+ "6h": 6 * 60 * 6e4,
5228
+ "12h": 12 * 60 * 6e4
5229
+ };
5230
+ const duration = recent[filter.preset.replace("older_", "")];
5231
+ if (duration !== void 0) {
5232
+ return filter.preset.startsWith("older_") ? { until: ago(duration) } : { since: ago(duration) };
5233
+ }
5234
+ const starts = {
5235
+ today: startOfDay(anchor),
5236
+ this_week: startOfWeek(anchor),
5237
+ this_month: startOfMonth(anchor),
5238
+ this_year: startOfYear(anchor),
5239
+ older_today: startOfDay(anchor),
5240
+ older_this_week: startOfWeek(anchor),
5241
+ older_this_month: startOfMonth(anchor),
5242
+ older_this_year: startOfYear(anchor)
5243
+ };
5244
+ if (filter.preset === "yesterday") {
5245
+ const until = startOfDay(anchor);
5246
+ const since = new Date(until);
5247
+ since.setDate(since.getDate() - 1);
5248
+ return { since: since.toISOString(), until: until.toISOString() };
5249
+ }
5250
+ const start = starts[filter.preset];
5251
+ if (!start) return {};
5252
+ return filter.preset.startsWith("older_") ? { until: start.toISOString() } : { since: start.toISOString() };
5253
+ }
5254
+
5134
5255
  // src/data-table/filter-engine.ts
5135
5256
  var SELECT_EMPTY_VALUE = "__empty__";
5136
5257
  var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
@@ -5227,10 +5348,25 @@ function passesColumnFilter(value, filter) {
5227
5348
  if (raw === "") return false;
5228
5349
  const normalized = raw.replace(/[^0-9.-]/g, "");
5229
5350
  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;
5351
+ const n2 = typeof value === "number" ? value : Number(normalized);
5352
+ if (!Number.isFinite(n2)) return false;
5353
+ if (filter.min !== void 0 && n2 < filter.min) return false;
5354
+ if (filter.max !== void 0 && n2 > filter.max) return false;
5355
+ return true;
5356
+ }
5357
+ case "date": {
5358
+ const raw = value instanceof Date ? value : new Date(stringifyCellValue(value));
5359
+ if (Number.isNaN(raw.valueOf())) return false;
5360
+ const { since, until } = dateFilterBounds(filter);
5361
+ const timestamp = raw.valueOf();
5362
+ if (since !== void 0) {
5363
+ const sinceTimestamp = new Date(since).valueOf();
5364
+ if (Number.isNaN(sinceTimestamp) || timestamp < sinceTimestamp) return false;
5365
+ }
5366
+ if (until !== void 0) {
5367
+ const untilTimestamp = new Date(until).valueOf();
5368
+ if (Number.isNaN(untilTimestamp) || timestamp >= untilTimestamp) return false;
5369
+ }
5234
5370
  return true;
5235
5371
  }
5236
5372
  }
@@ -5293,26 +5429,29 @@ function filterAndSortRows(rows, columns, columnFilters, sort, globalSearch, any
5293
5429
  return result;
5294
5430
  }
5295
5431
  function countActiveColumnFilters(filters) {
5296
- let n = 0;
5432
+ let n2 = 0;
5297
5433
  for (const f of Object.values(filters)) {
5298
5434
  if (!f) continue;
5299
5435
  switch (f.kind) {
5300
5436
  case "text":
5301
- if (f.value.trim() || f.mode && f.mode !== "contains") n += 1;
5437
+ if (f.value.trim() || f.mode && f.mode !== "contains") n2 += 1;
5302
5438
  break;
5303
5439
  case "select":
5304
5440
  if (f.values !== void 0 || f.value && f.value !== "__all__")
5305
- n += 1;
5441
+ n2 += 1;
5306
5442
  break;
5307
5443
  case "boolean":
5308
- n += 1;
5444
+ n2 += 1;
5309
5445
  break;
5310
5446
  case "number":
5311
- if (f.min !== void 0 || f.max !== void 0) n += 1;
5447
+ if (f.min !== void 0 || f.max !== void 0) n2 += 1;
5448
+ break;
5449
+ case "date":
5450
+ if (isColumnFilterActive(f)) n2 += 1;
5312
5451
  break;
5313
5452
  }
5314
5453
  }
5315
- return n;
5454
+ return n2;
5316
5455
  }
5317
5456
  function isColumnFilterActive(filter) {
5318
5457
  if (!filter) return false;
@@ -5325,6 +5464,10 @@ function isColumnFilterActive(filter) {
5325
5464
  return true;
5326
5465
  case "number":
5327
5466
  return filter.min !== void 0 || filter.max !== void 0;
5467
+ case "date": {
5468
+ const bounds = dateFilterBounds(filter);
5469
+ return bounds.since !== void 0 || bounds.until !== void 0;
5470
+ }
5328
5471
  }
5329
5472
  }
5330
5473
  function applyRowEdits(row, edits) {
@@ -5591,8 +5734,8 @@ function parseLayeredFilters(raw) {
5591
5734
  }
5592
5735
  }
5593
5736
  function positiveInt(raw, fallback) {
5594
- const n = raw ? Number.parseInt(raw, 10) : Number.NaN;
5595
- return Number.isFinite(n) && n > 0 ? n : fallback;
5737
+ const n2 = raw ? Number.parseInt(raw, 10) : Number.NaN;
5738
+ return Number.isFinite(n2) && n2 > 0 ? n2 : fallback;
5596
5739
  }
5597
5740
  function tableUrlParamPrefix(tableId) {
5598
5741
  if (!/^[a-z][a-z0-9-]{0,63}$/.test(tableId)) {
@@ -5931,6 +6074,95 @@ var CommandShortcut = ({
5931
6074
  };
5932
6075
  CommandShortcut.displayName = "CommandShortcut";
5933
6076
 
6077
+ // src/data-table/facets.ts
6078
+ var MAX_LISTABLE_FACET_LENGTH = 300;
6079
+ var DEFAULT_FACET_LIMIT = 200;
6080
+ var MAX_FACET_LIMIT = 500;
6081
+ function facetText(raw) {
6082
+ if (raw === null || raw === void 0) return "";
6083
+ if (typeof raw === "object") {
6084
+ try {
6085
+ return JSON.stringify(raw);
6086
+ } catch {
6087
+ return String(raw);
6088
+ }
6089
+ }
6090
+ return String(raw);
6091
+ }
6092
+ function computeColumnFacets(args) {
6093
+ const { columnId: columnId2, rows, readValue: readValue2 } = args;
6094
+ const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);
6095
+ const counts = /* @__PURE__ */ new Map();
6096
+ const longValues = /* @__PURE__ */ new Set();
6097
+ let blank = 0;
6098
+ let maxLength = 0;
6099
+ for (const row of rows) {
6100
+ let raw;
6101
+ try {
6102
+ raw = readValue2(row, columnId2);
6103
+ } catch {
6104
+ raw = void 0;
6105
+ }
6106
+ const trimmed = facetText(raw).trim();
6107
+ if (trimmed === "") {
6108
+ blank += 1;
6109
+ continue;
6110
+ }
6111
+ if (trimmed.length > maxLength) maxLength = trimmed.length;
6112
+ if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {
6113
+ longValues.add(trimmed);
6114
+ continue;
6115
+ }
6116
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
6117
+ }
6118
+ const values = [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, limit);
6119
+ return {
6120
+ columnId: columnId2,
6121
+ totalRows: rows.length,
6122
+ filled: rows.length - blank,
6123
+ blank,
6124
+ distinctCount: counts.size + longValues.size,
6125
+ maxLength,
6126
+ unlistable: longValues.size,
6127
+ limit,
6128
+ truncated: counts.size > limit,
6129
+ values,
6130
+ answeredBy: "local",
6131
+ complete: args.complete ?? true
6132
+ };
6133
+ }
6134
+ function localFacetsAreComplete(loadedRowCount, total) {
6135
+ if (total === void 0 || !Number.isFinite(total) || total < 0) return false;
6136
+ return loadedRowCount >= total;
6137
+ }
6138
+ var NUMBERS = new Intl.NumberFormat("en-US");
6139
+ function n(value) {
6140
+ try {
6141
+ return NUMBERS.format(value);
6142
+ } catch {
6143
+ return String(value);
6144
+ }
6145
+ }
6146
+ function describeFacetSource(facets, noun = "row") {
6147
+ if (!facets) return "";
6148
+ const plural2 = `${noun}s`;
6149
+ if (!facets.complete) {
6150
+ return `These counts cover the ${n(facets.totalRows)} ${plural2} loaded here, not the whole table.`;
6151
+ }
6152
+ if (facets.truncated) {
6153
+ return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;
6154
+ }
6155
+ if (facets.unlistable > 0) {
6156
+ 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"}.`;
6157
+ }
6158
+ return "";
6159
+ }
6160
+ var FACETS_LOADING_NOTICE = "Counting values\u2026";
6161
+ function describeFacetFailure(error) {
6162
+ const detail = error instanceof Error ? error.message : String(error ?? "");
6163
+ 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.";
6164
+ }
6165
+
5934
6166
  // src/data-table/column-marker.ts
5935
6167
  var MARKER_ACCESSORS = {
5936
6168
  favorite: "favorite",
@@ -5998,6 +6230,10 @@ function ColumnHeaderCell({
5998
6230
  filterFooterHint,
5999
6231
  allowNegation = false,
6000
6232
  allowNullModes = false,
6233
+ facets,
6234
+ facetsLoading = false,
6235
+ facetsError,
6236
+ onFilterOpen,
6001
6237
  reorderColumn
6002
6238
  }) {
6003
6239
  const reorderEnabled = reorderColumn !== void 0;
@@ -6032,6 +6268,11 @@ function ColumnHeaderCell({
6032
6268
  setPendingNegation(filterValue?.negated ?? false);
6033
6269
  }
6034
6270
  }, [filterOpen, filterValue]);
6271
+ const notifyOpen = (0, import_react13.useRef)(onFilterOpen);
6272
+ notifyOpen.current = onFilterOpen;
6273
+ (0, import_react13.useEffect)(() => {
6274
+ if (filterOpen) notifyOpen.current?.();
6275
+ }, [filterOpen]);
6035
6276
  const applyFilter = () => {
6036
6277
  onFilterChange(draftFilter);
6037
6278
  setFilterOpen(false);
@@ -6043,7 +6284,7 @@ function ColumnHeaderCell({
6043
6284
  const visibleFilter = deferredFilter ? draftFilter : filterValue;
6044
6285
  const setNegated = (negated) => {
6045
6286
  setPendingNegation(negated);
6046
- if (!visibleFilter) return;
6287
+ if (!visibleFilter || visibleFilter.kind === "date") return;
6047
6288
  const next = { ...visibleFilter, ...negated ? { negated: true } : {} };
6048
6289
  if (!negated) delete next.negated;
6049
6290
  if (deferredFilter) setDraftFilter(next);
@@ -6051,7 +6292,7 @@ function ColumnHeaderCell({
6051
6292
  };
6052
6293
  const markerLabels = marker ? markerCopy(marker) : void 0;
6053
6294
  const accessibleLabel = markerLabels?.label ?? labelText;
6054
- const menuLabel = accessibleLabel ? `Sort or filter ${accessibleLabel}` : "Sort or filter column";
6295
+ const menuLabel = filterKind ? accessibleLabel ? `Sort or filter ${accessibleLabel}` : "Sort or filter column" : accessibleLabel ? `Sort ${accessibleLabel}` : "Sort column";
6055
6296
  const menuContent = /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
6056
6297
  PopoverContent,
6057
6298
  {
@@ -6119,13 +6360,16 @@ function ColumnHeaderCell({
6119
6360
  kind: filterKind,
6120
6361
  value: visibleFilter,
6121
6362
  onChange: (next) => {
6122
- const preservingNegation = next && (visibleFilter?.negated ?? pendingNegation) ? { ...next, negated: true } : next;
6363
+ const preservingNegation = next && next.kind !== "date" && (visibleFilter?.negated ?? pendingNegation) ? { ...next, negated: true } : next;
6123
6364
  if (deferredFilter) setDraftFilter(preservingNegation);
6124
6365
  else onFilterChange(preservingNegation);
6125
6366
  },
6126
6367
  selectOptions,
6127
6368
  selectSingle,
6128
6369
  selectAllowCustom,
6370
+ facets: facets ?? null,
6371
+ facetsLoading,
6372
+ ...facetsError === void 0 ? {} : { facetsError },
6129
6373
  ...markerLabels ? {
6130
6374
  booleanLabels: {
6131
6375
  all: "All",
@@ -6136,7 +6380,7 @@ function ColumnHeaderCell({
6136
6380
  allowNullModes
6137
6381
  }
6138
6382
  ) : null,
6139
- filterKind && allowNegation ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "mt-2 border-t border-border pt-2", children: [
6383
+ filterKind && filterKind !== "date" && allowNegation ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "mt-2 border-t border-border pt-2", children: [
6140
6384
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "mb-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: "Match mode" }),
6141
6385
  /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "grid grid-cols-2 gap-1 rounded-md border p-0.5", children: [
6142
6386
  /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
@@ -6350,8 +6594,43 @@ function FilterBody({
6350
6594
  selectSingle = false,
6351
6595
  selectAllowCustom = false,
6352
6596
  allowNullModes = false,
6353
- booleanLabels
6597
+ booleanLabels,
6598
+ facets = null,
6599
+ facetsLoading = false,
6600
+ facetsError
6354
6601
  }) {
6602
+ const checklist = facetsLoading || facetsError || facets && (facets.values.length > 0 || facets.blank > 0) ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6603
+ FacetChecklist,
6604
+ {
6605
+ facets,
6606
+ loading: facetsLoading,
6607
+ ...facetsError === void 0 ? {} : { error: facetsError },
6608
+ value,
6609
+ onChange
6610
+ }
6611
+ ) : null;
6612
+ if (checklist && kind === "select" && facets && facets.values.length > 0) {
6613
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "space-y-2", children: checklist });
6614
+ }
6615
+ if (checklist) {
6616
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "space-y-2", children: [
6617
+ checklist,
6618
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("div", { className: "h-px bg-border" }),
6619
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6620
+ FilterBody,
6621
+ {
6622
+ kind,
6623
+ value,
6624
+ onChange,
6625
+ selectOptions,
6626
+ selectSingle,
6627
+ selectAllowCustom,
6628
+ allowNullModes,
6629
+ ...booleanLabels === void 0 ? {} : { booleanLabels }
6630
+ }
6631
+ )
6632
+ ] });
6633
+ }
6355
6634
  if (kind === "text") {
6356
6635
  const text = value?.kind === "text" ? value.value : "";
6357
6636
  const mode = (value?.kind === "text" ? value.mode : void 0) ?? "contains";
@@ -6460,8 +6739,183 @@ function FilterBody({
6460
6739
  )
6461
6740
  ] });
6462
6741
  }
6742
+ if (kind === "date") {
6743
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(DateFilterBody, { value, onChange });
6744
+ }
6463
6745
  return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(NumberFilterBody, { value, onChange });
6464
6746
  }
6747
+ function FacetChecklist({
6748
+ facets,
6749
+ loading,
6750
+ error,
6751
+ value,
6752
+ onChange
6753
+ }) {
6754
+ const [query, setQuery] = (0, import_react13.useState)("");
6755
+ const selected = value?.kind === "select" ? value.values ?? (value.value && value.value !== "__all__" ? [value.value] : []) : [];
6756
+ const selectedSet = new Set(selected);
6757
+ const rows = (0, import_react13.useMemo)(() => {
6758
+ if (!facets) return [];
6759
+ const listed = facets.values.map((entry) => ({
6760
+ value: entry.value,
6761
+ label: entry.value,
6762
+ count: entry.count
6763
+ }));
6764
+ const withBlank = facets.blank > 0 ? [{ value: "", label: "(empty)", count: facets.blank }, ...listed] : listed;
6765
+ const q = query.trim().toLowerCase();
6766
+ if (!q) return withBlank;
6767
+ return withBlank.filter((row) => row.label.toLowerCase().includes(q));
6768
+ }, [facets, query]);
6769
+ const toggle = (candidate) => {
6770
+ const next = new Set(selectedSet);
6771
+ if (next.has(candidate)) next.delete(candidate);
6772
+ else next.add(candidate);
6773
+ const values = [...next];
6774
+ onChange(
6775
+ values.length === 0 ? void 0 : { kind: "select", value: values[0] ?? "", values }
6776
+ );
6777
+ };
6778
+ const notice = error ?? (loading ? FACETS_LOADING_NOTICE : describeFacetSource(facets));
6779
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "space-y-1.5", children: [
6780
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-1 text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: "Values" }),
6781
+ facets && facets.values.length + (facets.blank > 0 ? 1 : 0) > 8 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6782
+ Input,
6783
+ {
6784
+ "data-matrx-filter-autofocus": true,
6785
+ value: query,
6786
+ onChange: (event) => setQuery(event.target.value),
6787
+ placeholder: "Find a value\u2026",
6788
+ className: "h-7 text-sm",
6789
+ style: { fontSize: "16px" }
6790
+ }
6791
+ ) : null,
6792
+ 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) => {
6793
+ const checked = selectedSet.has(row.value);
6794
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
6795
+ "button",
6796
+ {
6797
+ type: "button",
6798
+ role: "checkbox",
6799
+ "aria-checked": checked,
6800
+ "data-matrx-facet-value": row.value === "" ? "__blank__" : row.value,
6801
+ onClick: () => toggle(row.value),
6802
+ className: cn(
6803
+ "flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-xs hover:bg-muted",
6804
+ checked && "bg-primary/10"
6805
+ ),
6806
+ children: [
6807
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6808
+ "span",
6809
+ {
6810
+ "aria-hidden": true,
6811
+ className: cn(
6812
+ "flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-[3px] border",
6813
+ checked ? "border-primary bg-primary text-primary-foreground" : "border-border"
6814
+ ),
6815
+ children: checked ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Check, { className: "h-2.5 w-2.5" }) : null
6816
+ }
6817
+ ),
6818
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("span", { className: cn("min-w-0 flex-1 truncate", row.value === "" && "italic text-muted-foreground"), children: row.label }),
6819
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6820
+ "span",
6821
+ {
6822
+ "data-matrx-facet-count": String(row.count),
6823
+ className: "shrink-0 tabular-nums text-[11px] text-muted-foreground",
6824
+ children: row.count.toLocaleString("en-US")
6825
+ }
6826
+ )
6827
+ ]
6828
+ }
6829
+ ) }, row.value === "" ? "__blank__" : row.value);
6830
+ }) }) : 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." }),
6831
+ notice ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6832
+ "p",
6833
+ {
6834
+ "data-matrx-facet-notice": "true",
6835
+ className: cn(
6836
+ "px-1 text-[11px] leading-snug",
6837
+ error ? "text-amber-700 dark:text-amber-400" : "text-muted-foreground"
6838
+ ),
6839
+ children: notice
6840
+ }
6841
+ ) : null,
6842
+ selected.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(
6843
+ "button",
6844
+ {
6845
+ type: "button",
6846
+ onClick: () => onChange(void 0),
6847
+ className: "px-1 text-[11px] text-muted-foreground underline-offset-2 hover:underline",
6848
+ children: "Clear selection"
6849
+ }
6850
+ ) : null
6851
+ ] });
6852
+ }
6853
+ function DateFilterBody({
6854
+ value,
6855
+ onChange
6856
+ }) {
6857
+ const dateValue = value?.kind === "date" ? value : void 0;
6858
+ const isCustom = dateValue !== void 0 && dateValue.preset === void 0;
6859
+ const selected = dateValue?.preset ?? (isCustom ? "__custom__" : "all");
6860
+ const setBound = (bound, raw) => {
6861
+ const next = raw ? new Date(raw) : void 0;
6862
+ const iso = next && !Number.isNaN(next.valueOf()) ? next.toISOString() : void 0;
6863
+ const current = dateValue && dateValue.preset === void 0 ? dateValue : { kind: "date" };
6864
+ const updated = { ...current, [bound]: iso };
6865
+ if (updated.since === void 0 && updated.until === void 0) onChange(void 0);
6866
+ else onChange(updated);
6867
+ };
6868
+ return /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "space-y-2 px-1", children: [
6869
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "flex items-center justify-between", children: [
6870
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "text-[11px] font-medium uppercase tracking-wider text-muted-foreground", children: "Date range" }),
6871
+ dateValue ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("button", { type: "button", className: "text-[11px] text-muted-foreground hover:text-foreground", onClick: () => onChange(void 0), children: "clear" }) : null
6872
+ ] }),
6873
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(
6874
+ Select,
6875
+ {
6876
+ value: selected,
6877
+ onValueChange: (next) => {
6878
+ if (next === "all") {
6879
+ onChange(void 0);
6880
+ return;
6881
+ }
6882
+ if (next === "__custom__") {
6883
+ onChange({ kind: "date" });
6884
+ return;
6885
+ }
6886
+ onChange({ kind: "date", preset: next, anchor: (/* @__PURE__ */ new Date()).toISOString() });
6887
+ },
6888
+ children: [
6889
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SelectTrigger, { className: "h-8 text-xs", children: /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SelectValue, {}) }),
6890
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)(SelectContent, { children: [
6891
+ ["all", "recent", "calendar", "older"].map((group) => /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { children: [
6892
+ group !== "all" ? /* @__PURE__ */ (0, import_jsx_runtime31.jsx)("p", { className: "px-2 py-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground", children: group }) : null,
6893
+ TIME_RANGE_PRESETS.filter((option) => option.group === group).map((option) => /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SelectItem, { value: option.value, children: option.label }, option.value))
6894
+ ] }, group)),
6895
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(SelectItem, { value: "__custom__", children: "Custom range" })
6896
+ ] })
6897
+ ]
6898
+ }
6899
+ ),
6900
+ isCustom ? /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("div", { className: "grid gap-1.5", children: [
6901
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("label", { className: "grid gap-0.5 text-[11px] text-muted-foreground", children: [
6902
+ "On or after",
6903
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Input, { type: "datetime-local", value: dateTimeLocal(dateValue?.since), onChange: (event) => setBound("since", event.target.value), className: "h-8 text-xs" })
6904
+ ] }),
6905
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsxs)("label", { className: "grid gap-0.5 text-[11px] text-muted-foreground", children: [
6906
+ "Before",
6907
+ /* @__PURE__ */ (0, import_jsx_runtime31.jsx)(Input, { type: "datetime-local", value: dateTimeLocal(dateValue?.until), onChange: (event) => setBound("until", event.target.value), className: "h-8 text-xs" })
6908
+ ] })
6909
+ ] }) : null
6910
+ ] });
6911
+ }
6912
+ function dateTimeLocal(value) {
6913
+ if (!value) return "";
6914
+ const date = new Date(value);
6915
+ if (Number.isNaN(date.valueOf())) return "";
6916
+ const offset = date.getTimezoneOffset() * 6e4;
6917
+ return new Date(date.valueOf() - offset).toISOString().slice(0, 16);
6918
+ }
6465
6919
  function SearchableSelectFilter({
6466
6920
  selected,
6467
6921
  options,
@@ -6648,8 +7102,8 @@ function NumberFilterBody({
6648
7102
  }
6649
7103
  function parseOptionalNumber(raw) {
6650
7104
  if (raw.trim() === "") return void 0;
6651
- const n = Number(raw.replace(/[^0-9.-]/g, ""));
6652
- return Number.isFinite(n) ? n : void 0;
7105
+ const n2 = Number(raw.replace(/[^0-9.-]/g, ""));
7106
+ return Number.isFinite(n2) ? n2 : void 0;
6653
7107
  }
6654
7108
 
6655
7109
  // src/data-table/ColumnResizeHandle.tsx
@@ -6961,16 +7415,16 @@ function computeAggregate(rows, column) {
6961
7415
  value = total;
6962
7416
  break;
6963
7417
  case "sum":
6964
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0);
7418
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0);
6965
7419
  break;
6966
7420
  case "average":
6967
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => acc + n, 0) / numbers.length;
7421
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => acc + n2, 0) / numbers.length;
6968
7422
  break;
6969
7423
  case "min":
6970
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n < acc ? n : acc);
7424
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 < acc ? n2 : acc);
6971
7425
  break;
6972
7426
  case "max":
6973
- value = numbers.length === 0 ? null : numbers.reduce((acc, n) => n > acc ? n : acc);
7427
+ value = numbers.length === 0 ? null : numbers.reduce((acc, n2) => n2 > acc ? n2 : acc);
6974
7428
  break;
6975
7429
  case "empty":
6976
7430
  value = total - filled.length;
@@ -7394,7 +7848,7 @@ function moveSelection(current, move, rowIds, fieldNames) {
7394
7848
  if (r === -1 || c === -1) return first;
7395
7849
  const lastRow = rowIds.length - 1;
7396
7850
  const lastCol = fieldNames.length - 1;
7397
- const clamp = (n, max) => Math.min(Math.max(n, 0), max);
7851
+ const clamp = (n2, max) => Math.min(Math.max(n2, 0), max);
7398
7852
  switch (move) {
7399
7853
  case "up":
7400
7854
  r = clamp(r - 1, lastRow);
@@ -7526,14 +7980,14 @@ function parseClipboardGrid(text) {
7526
7980
  let row = [];
7527
7981
  let cell = "";
7528
7982
  let i = 0;
7529
- const n = text.length;
7530
- while (i < n) {
7983
+ const n2 = text.length;
7984
+ while (i < n2) {
7531
7985
  const ch = text[i];
7532
7986
  if (cell === "" && ch === '"') {
7533
7987
  let j = i + 1;
7534
7988
  let out = "";
7535
7989
  let closed = false;
7536
- while (j < n) {
7990
+ while (j < n2) {
7537
7991
  const c = text[j];
7538
7992
  if (c === '"') {
7539
7993
  if (text[j + 1] === '"') {
@@ -7548,10 +8002,10 @@ function parseClipboardGrid(text) {
7548
8002
  out += c;
7549
8003
  j += 1;
7550
8004
  }
7551
- if (closed && (j >= n || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
8005
+ if (closed && (j >= n2 || text[j] === " " || text[j] === "\n" || text[j] === "\r")) {
7552
8006
  cell = out;
7553
8007
  i = j;
7554
- if (i >= n) break;
8008
+ if (i >= n2) break;
7555
8009
  const d = text[i];
7556
8010
  if (d === " ") {
7557
8011
  row.push(cell);
@@ -7587,7 +8041,7 @@ function parseClipboardGrid(text) {
7587
8041
  cell += ch;
7588
8042
  i += 1;
7589
8043
  }
7590
- const endedOnBreak = n > 0 && (text[n - 1] === "\n" || text[n - 1] === "\r");
8044
+ const endedOnBreak = n2 > 0 && (text[n2 - 1] === "\n" || text[n2 - 1] === "\r");
7591
8045
  if (!endedOnBreak) {
7592
8046
  row.push(cell);
7593
8047
  rows.push(row);
@@ -7666,11 +8120,11 @@ function directionOf(source, target) {
7666
8120
  return null;
7667
8121
  }
7668
8122
  function tileIndex(i, a, b) {
7669
- const n = b - a + 1;
7670
- if (n <= 0) return a;
8123
+ const n2 = b - a + 1;
8124
+ if (n2 <= 0) return a;
7671
8125
  if (i >= a && i <= b) return i;
7672
- if (i > b) return a + (i - a) % n;
7673
- return b - (a - 1 - i) % n;
8126
+ if (i > b) return a + (i - a) % n2;
8127
+ return b - (a - 1 - i) % n2;
7674
8128
  }
7675
8129
  function isFiniteNumber(v) {
7676
8130
  return typeof v === "number" && Number.isFinite(v);
@@ -8015,6 +8469,26 @@ function useSpreadsheetGrid(options) {
8015
8469
  for (let i = 0; i < columnIds.length; i += 1) map.set(columnIds[i], i);
8016
8470
  return map;
8017
8471
  }, [columnIds]);
8472
+ const screenPatches = (0, import_react15.useCallback)(
8473
+ (patches) => {
8474
+ const validate = optionsRef.current?.validateCell;
8475
+ if (!validate) return { accepted: patches, refusals: [] };
8476
+ const accepted = [];
8477
+ const refusals = [];
8478
+ for (const patch of patches) {
8479
+ let reason;
8480
+ try {
8481
+ reason = validate({ rowId: patch.rowId, columnId: patch.columnId, value: patch.value });
8482
+ } catch {
8483
+ reason = void 0;
8484
+ }
8485
+ if (reason) refusals.push({ columnId: patch.columnId, reason });
8486
+ else accepted.push(patch);
8487
+ }
8488
+ return { accepted, refusals };
8489
+ },
8490
+ []
8491
+ );
8018
8492
  const applyPatches = (0, import_react15.useCallback)(
8019
8493
  async (patches, intent, label) => {
8020
8494
  const current = optionsRef.current;
@@ -8071,9 +8545,10 @@ function useSpreadsheetGrid(options) {
8071
8545
  ...base,
8072
8546
  range: { top: bounds.r0, left: bounds.c0, bottom: bounds.r1, right: bounds.c1 }
8073
8547
  });
8074
- await applyPatches(result.patches, "fill-down", "Filled down");
8075
- announceSkips(result.skipped, result.summary);
8076
- }, [fillRequestBase, bounds, applyPatches, announceSkips]);
8548
+ const screened = screenPatches(result.patches);
8549
+ await applyPatches(screened.accepted, "fill-down", "Filled down");
8550
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8551
+ }, [fillRequestBase, bounds, applyPatches, announceSkips, screenPatches]);
8077
8552
  const doClear = (0, import_react15.useCallback)(async () => {
8078
8553
  const current = optionsRef.current;
8079
8554
  if (!current || !bounds) return;
@@ -8094,9 +8569,13 @@ function useSpreadsheetGrid(options) {
8094
8569
  patches.push({ rowId, columnId: columnId2, value: null });
8095
8570
  }
8096
8571
  }
8097
- await applyPatches(patches, "clear", "Cleared");
8098
- announceSkips(skipped, `Cleared ${patches.length} cells.`);
8099
- }, [bounds, applyPatches, announceSkips]);
8572
+ const screened = screenPatches(patches);
8573
+ await applyPatches(screened.accepted, "clear", "Cleared");
8574
+ announceSkips(
8575
+ [...skipped, ...screened.refusals],
8576
+ `Cleared ${screened.accepted.length} cells.`
8577
+ );
8578
+ }, [bounds, applyPatches, announceSkips, screenPatches]);
8100
8579
  const doUndo = (0, import_react15.useCallback)(async () => {
8101
8580
  const taken = popUndo(stack);
8102
8581
  if (!taken.entry) return;
@@ -8181,6 +8660,14 @@ function useSpreadsheetGrid(options) {
8181
8660
  case "escape":
8182
8661
  setRange(null);
8183
8662
  return;
8663
+ case "edit":
8664
+ case "editSeeded": {
8665
+ const focused = range?.focus;
8666
+ if (!focused || !current.onEditRequest) return;
8667
+ const seed = action.kind === "editSeeded" ? action.seed : null;
8668
+ if (current.onEditRequest(focused, seed)) event.preventDefault();
8669
+ return;
8670
+ }
8184
8671
  default:
8185
8672
  return;
8186
8673
  }
@@ -8195,6 +8682,13 @@ function useSpreadsheetGrid(options) {
8195
8682
  );
8196
8683
  return gridToTsv(grid);
8197
8684
  }, [range, rowIds, columnIds]);
8685
+ const selectionAddresses = (0, import_react15.useCallback)(() => {
8686
+ if (!range || !bounds) return { rowIds: [], columnIds: [] };
8687
+ return {
8688
+ rowIds: rowIds.slice(bounds.r0, bounds.r1 + 1),
8689
+ columnIds: columnIds.slice(bounds.c0, bounds.c1 + 1)
8690
+ };
8691
+ }, [range, bounds, rowIds, columnIds]);
8198
8692
  const onCopy = (0, import_react15.useCallback)(
8199
8693
  (event) => {
8200
8694
  if (!optionsRef.current || !range || targetsAnEditor(event.target)) return;
@@ -8250,11 +8744,12 @@ function useSpreadsheetGrid(options) {
8250
8744
  patches.push({ rowId, columnId: columnId2, value: line[c] ?? null });
8251
8745
  }
8252
8746
  }
8253
- void applyPatches(patches, "paste", "Pasted");
8747
+ const screened = screenPatches(patches);
8748
+ void applyPatches(screened.accepted, "paste", "Pasted");
8254
8749
  const tail = outside > 0 ? ` ${outside} landed past the edge of the table and were ignored.` : "";
8255
- announceSkips(skipped, `Pasted ${patches.length} cells.${tail}`);
8750
+ announceSkips([...skipped, ...screened.refusals], `Pasted ${screened.accepted.length} cells.${tail}`);
8256
8751
  },
8257
- [range, rowIds, columnIds, applyPatches, announceSkips]
8752
+ [range, rowIds, columnIds, applyPatches, announceSkips, screenPatches]
8258
8753
  );
8259
8754
  const cellProps = (0, import_react15.useCallback)(
8260
8755
  (rowId, columnId2) => {
@@ -8273,6 +8768,10 @@ function useSpreadsheetGrid(options) {
8273
8768
  onMouseDown: (event) => {
8274
8769
  if (event.button !== 0) return;
8275
8770
  event.preventDefault();
8771
+ const origin = event.currentTarget;
8772
+ if (typeof HTMLElement !== "undefined" && origin instanceof HTMLElement) {
8773
+ origin.closest('[data-matrx-table-spreadsheet="true"]')?.focus({ preventScroll: true });
8774
+ }
8276
8775
  setRange(
8277
8776
  (previous) => event.shiftKey && previous ? { anchor: previous.anchor, focus: address } : { anchor: address, focus: address }
8278
8777
  );
@@ -8334,12 +8833,13 @@ function useSpreadsheetGrid(options) {
8334
8833
  source: { top: source.r0, left: source.c0, bottom: source.r1, right: source.c1 },
8335
8834
  target
8336
8835
  });
8337
- void applyPatches(result.patches, "fill", "Filled");
8338
- announceSkips(result.skipped, result.summary);
8836
+ const screened = screenPatches(result.patches);
8837
+ void applyPatches(screened.accepted, "fill", "Filled");
8838
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
8339
8839
  };
8340
8840
  window.addEventListener("mouseup", commit);
8341
8841
  return () => window.removeEventListener("mouseup", commit);
8342
- }, [options, fillRequestBase, applyPatches, announceSkips]);
8842
+ }, [options, fillRequestBase, applyPatches, announceSkips, screenPatches]);
8343
8843
  return {
8344
8844
  range,
8345
8845
  bounds,
@@ -8355,17 +8855,19 @@ function useSpreadsheetGrid(options) {
8355
8855
  isFillHandleCell,
8356
8856
  startFillDrag,
8357
8857
  canUndo: stack.past.length > 0,
8358
- canRedo: stack.future.length > 0
8858
+ canRedo: stack.future.length > 0,
8859
+ selectionTsv: selectionAsTsv,
8860
+ selectionAddresses
8359
8861
  };
8360
8862
  }
8361
8863
 
8362
8864
  // src/data-table/coverage.ts
8363
8865
  var PARTIAL_BADGE = "Partial";
8364
8866
  var UNKNOWN_TOTAL_BADGE = "Unknown total";
8365
- var NUMBERS = new Intl.NumberFormat("en-US");
8867
+ var NUMBERS2 = new Intl.NumberFormat("en-US");
8366
8868
  function formatNumber(value) {
8367
8869
  try {
8368
- return NUMBERS.format(value);
8870
+ return NUMBERS2.format(value);
8369
8871
  } catch {
8370
8872
  return String(value);
8371
8873
  }
@@ -8996,7 +9498,8 @@ var filterSchema = import_zod2.z.union([
8996
9498
  import_zod2.z.object({ kind: import_zod2.z.literal("text"), value: import_zod2.z.string(), mode: import_zod2.z.enum(["contains", "empty", "not_empty", "null", "not_null"]).optional(), negated: import_zod2.z.boolean().optional() }),
8997
9499
  import_zod2.z.object({ kind: import_zod2.z.literal("select"), value: import_zod2.z.string(), values: import_zod2.z.array(import_zod2.z.string()).optional(), negated: import_zod2.z.boolean().optional() }),
8998
9500
  import_zod2.z.object({ kind: import_zod2.z.literal("boolean"), value: import_zod2.z.boolean(), negated: import_zod2.z.boolean().optional() }),
8999
- import_zod2.z.object({ kind: import_zod2.z.literal("number"), min: import_zod2.z.number().optional(), max: import_zod2.z.number().optional(), negated: import_zod2.z.boolean().optional() })
9501
+ import_zod2.z.object({ kind: import_zod2.z.literal("number"), min: import_zod2.z.number().optional(), max: import_zod2.z.number().optional(), negated: import_zod2.z.boolean().optional() }),
9502
+ import_zod2.z.object({ kind: import_zod2.z.literal("date"), preset: import_zod2.z.enum(TIME_RANGE_PRESET_VALUES).optional(), since: import_zod2.z.string().datetime({ offset: true }).optional(), until: import_zod2.z.string().datetime({ offset: true }).optional(), anchor: import_zod2.z.string().datetime({ offset: true }).optional() })
9000
9503
  ]);
9001
9504
  var layeredRuleSchema = import_zod2.z.object({ id: import_zod2.z.string().min(1), field: import_zod2.z.string().min(1), operator: import_zod2.z.enum(LAYERED_FILTER_OPERATORS), value: import_zod2.z.string(), valueTo: import_zod2.z.string().optional() });
9002
9505
  var snapshotSchema = import_zod2.z.object({
@@ -9024,7 +9527,8 @@ function normalizeFilters(raw) {
9024
9527
  if (value.kind === "text") filters[id] = { kind: "text", value: value.value, ...value.mode === void 0 ? {} : { mode: value.mode }, ...value.negated === void 0 ? {} : { negated: value.negated } };
9025
9528
  else if (value.kind === "select") filters[id] = { kind: "select", value: value.value, ...value.values === void 0 ? {} : { values: value.values }, ...value.negated === void 0 ? {} : { negated: value.negated } };
9026
9529
  else if (value.kind === "boolean") filters[id] = { kind: "boolean", value: value.value, ...value.negated === void 0 ? {} : { negated: value.negated } };
9027
- else filters[id] = { kind: "number", ...value.min === void 0 ? {} : { min: value.min }, ...value.max === void 0 ? {} : { max: value.max }, ...value.negated === void 0 ? {} : { negated: value.negated } };
9530
+ else if (value.kind === "number") filters[id] = { kind: "number", ...value.min === void 0 ? {} : { min: value.min }, ...value.max === void 0 ? {} : { max: value.max }, ...value.negated === void 0 ? {} : { negated: value.negated } };
9531
+ else filters[id] = { kind: "date", ...value.preset === void 0 ? {} : { preset: value.preset }, ...value.since === void 0 ? {} : { since: value.since }, ...value.until === void 0 ? {} : { until: value.until }, ...value.anchor === void 0 ? {} : { anchor: value.anchor } };
9028
9532
  }
9029
9533
  return filters;
9030
9534
  }
@@ -9563,8 +10067,8 @@ function toNumber(value) {
9563
10067
  if (typeof value === "string") {
9564
10068
  const cleaned = value.replace(/[,\s$€£¥%]/g, "");
9565
10069
  if (cleaned === "") return null;
9566
- const n = Number(cleaned);
9567
- return Number.isFinite(n) ? n : null;
10070
+ const n2 = Number(cleaned);
10071
+ return Number.isFinite(n2) ? n2 : null;
9568
10072
  }
9569
10073
  return null;
9570
10074
  }
@@ -9574,6 +10078,7 @@ function toText(value) {
9574
10078
  return String(value);
9575
10079
  return null;
9576
10080
  }
10081
+ var BARE_CALENDAR_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ]00:00(?::00(?:\.0+)?)?)?$/;
9577
10082
  function toDate(value) {
9578
10083
  if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
9579
10084
  if (typeof value === "number") {
@@ -9581,6 +10086,20 @@ function toDate(value) {
9581
10086
  return Number.isNaN(d.getTime()) ? null : d;
9582
10087
  }
9583
10088
  if (typeof value === "string" && value.trim()) {
10089
+ const bare = BARE_CALENDAR_DATE.exec(value.trim());
10090
+ if (bare) {
10091
+ const [year, month, day] = [
10092
+ Number(bare[1]),
10093
+ Number(bare[2]),
10094
+ Number(bare[3])
10095
+ ];
10096
+ const d2 = new Date(year, month - 1, day);
10097
+ d2.setFullYear(year);
10098
+ if (d2.getFullYear() !== year || d2.getMonth() !== month - 1 || d2.getDate() !== day) {
10099
+ return null;
10100
+ }
10101
+ return d2;
10102
+ }
9584
10103
  const d = new Date(value);
9585
10104
  return Number.isNaN(d.getTime()) ? null : d;
9586
10105
  }
@@ -9602,14 +10121,14 @@ function toList(value) {
9602
10121
  }
9603
10122
  return null;
9604
10123
  }
9605
- function groupedNumber(n, options) {
10124
+ function groupedNumber(n2, options) {
9606
10125
  return new Intl.NumberFormat(void 0, {
9607
10126
  useGrouping: options.useGrouping !== false,
9608
10127
  ...options.precision != null ? {
9609
10128
  minimumFractionDigits: options.precision,
9610
10129
  maximumFractionDigits: options.precision
9611
10130
  } : { maximumFractionDigits: 10 }
9612
- }).format(n);
10131
+ }).format(n2);
9613
10132
  }
9614
10133
  function affix(text, options) {
9615
10134
  return `${options.prefix ?? ""}${text}${options.suffix ?? ""}`;
@@ -9743,8 +10262,8 @@ var DEFS = [
9743
10262
  numericAlign: true,
9744
10263
  optionKeys: ["useGrouping", "precision", "prefix", "suffix"],
9745
10264
  format: (v, o) => {
9746
- const n = toNumber(v);
9747
- return n === null ? null : affix(groupedNumber(n, o), o);
10265
+ const n2 = toNumber(v);
10266
+ return n2 === null ? null : affix(groupedNumber(n2, o), o);
9748
10267
  },
9749
10268
  parse: (raw) => toNumber(raw)
9750
10269
  },
@@ -9759,9 +10278,9 @@ var DEFS = [
9759
10278
  numericAlign: true,
9760
10279
  optionKeys: ["precision", "useGrouping", "prefix", "suffix"],
9761
10280
  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);
10281
+ const n2 = toNumber(v);
10282
+ if (n2 === null) return null;
10283
+ return affix(groupedNumber(n2, { ...o, precision: o.precision ?? 2 }), o);
9765
10284
  },
9766
10285
  parse: (raw) => toNumber(raw)
9767
10286
  },
@@ -9776,8 +10295,8 @@ var DEFS = [
9776
10295
  numericAlign: true,
9777
10296
  optionKeys: ["currency", "precision"],
9778
10297
  format: (v, o) => {
9779
- const n = toNumber(v);
9780
- if (n === null) return null;
10298
+ const n2 = toNumber(v);
10299
+ if (n2 === null) return null;
9781
10300
  const currency = (o.currency || "USD").toUpperCase();
9782
10301
  try {
9783
10302
  return new Intl.NumberFormat(void 0, {
@@ -9787,9 +10306,9 @@ var DEFS = [
9787
10306
  minimumFractionDigits: o.precision,
9788
10307
  maximumFractionDigits: o.precision
9789
10308
  } : {}
9790
- }).format(n);
10309
+ }).format(n2);
9791
10310
  } catch {
9792
- return `${groupedNumber(n, { precision: o.precision ?? 2 })} ${currency}`;
10311
+ return `${groupedNumber(n2, { precision: o.precision ?? 2 })} ${currency}`;
9793
10312
  }
9794
10313
  },
9795
10314
  parse: (raw) => toNumber(raw)
@@ -9805,9 +10324,9 @@ var DEFS = [
9805
10324
  numericAlign: true,
9806
10325
  optionKeys: ["percentScale", "precision"],
9807
10326
  format: (v, o) => {
9808
- const n = toNumber(v);
9809
- if (n === null) return null;
9810
- const shown = o.percentScale === "fraction" ? n * 100 : n;
10327
+ const n2 = toNumber(v);
10328
+ if (n2 === null) return null;
10329
+ const shown = o.percentScale === "fraction" ? n2 * 100 : n2;
9811
10330
  return `${groupedNumber(shown, o.precision === void 0 ? {} : { precision: o.precision })}%`;
9812
10331
  },
9813
10332
  parse: (raw) => toNumber(raw)
@@ -9823,9 +10342,9 @@ var DEFS = [
9823
10342
  numericAlign: true,
9824
10343
  optionKeys: ["durationUnit"],
9825
10344
  format: (v, o) => {
9826
- const n = toNumber(v);
9827
- if (n === null) return null;
9828
- return formatSignedDuration(n * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
10345
+ const n2 = toNumber(v);
10346
+ if (n2 === null) return null;
10347
+ return formatSignedDuration(n2 * DURATION_TO_SECONDS[o.durationUnit ?? "seconds"]);
9829
10348
  },
9830
10349
  parse: (raw) => toNumber(raw)
9831
10350
  },
@@ -9840,13 +10359,13 @@ var DEFS = [
9840
10359
  numericAlign: true,
9841
10360
  optionKeys: ["useGrouping", "prefix", "suffix"],
9842
10361
  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);
10362
+ const n2 = toNumber(v);
10363
+ if (n2 === null) return null;
10364
+ return affix(groupedNumber(Math.trunc(n2), { ...o, precision: 0 }), o);
9846
10365
  },
9847
10366
  parse: (raw) => {
9848
- const n = toNumber(raw);
9849
- return n === null ? null : Math.trunc(n);
10367
+ const n2 = toNumber(raw);
10368
+ return n2 === null ? null : Math.trunc(n2);
9850
10369
  }
9851
10370
  },
9852
10371
  {
@@ -9860,15 +10379,15 @@ var DEFS = [
9860
10379
  rich: true,
9861
10380
  optionKeys: ["ratingMax"],
9862
10381
  format: (v, o) => {
9863
- const n = toNumber(v);
9864
- if (n === null) return null;
10382
+ const n2 = toNumber(v);
10383
+ if (n2 === null) return null;
9865
10384
  const max = o.ratingMax ?? 5;
9866
- if (n < 0 || n > max) return null;
9867
- return `${n} / ${max}`;
10385
+ if (n2 < 0 || n2 > max) return null;
10386
+ return `${n2} / ${max}`;
9868
10387
  },
9869
10388
  parse: (raw) => {
9870
- const n = toNumber(raw);
9871
- return n === null ? null : Math.round(n);
10389
+ const n2 = toNumber(raw);
10390
+ return n2 === null ? null : Math.round(n2);
9872
10391
  }
9873
10392
  },
9874
10393
  {
@@ -9881,12 +10400,12 @@ var DEFS = [
9881
10400
  editor: "number",
9882
10401
  numericAlign: true,
9883
10402
  format: (v) => {
9884
- const n = toNumber(v);
9885
- return n === null ? null : (0, import_format3.formatFileSize)(n);
10403
+ const n2 = toNumber(v);
10404
+ return n2 === null ? null : (0, import_format3.formatFileSize)(n2);
9886
10405
  },
9887
10406
  parse: (raw) => {
9888
- const n = toNumber(raw);
9889
- return n === null ? null : Math.trunc(n);
10407
+ const n2 = toNumber(raw);
10408
+ return n2 === null ? null : Math.trunc(n2);
9890
10409
  }
9891
10410
  },
9892
10411
  // ── Choice ────────────────────────────────────────────────────────────────
@@ -10298,6 +10817,17 @@ function DirtySavePill({
10298
10817
  // src/data-table/EditableTableCell.tsx
10299
10818
  var import_react19 = require("react");
10300
10819
  var import_jsx_runtime39 = require("react/jsx-runtime");
10820
+ function CellRefusal({ reason }) {
10821
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10822
+ "p",
10823
+ {
10824
+ role: "alert",
10825
+ "data-matrx-cell-refusal": "true",
10826
+ className: "mt-1 text-[11px] leading-snug text-amber-700 dark:text-amber-400",
10827
+ children: reason
10828
+ }
10829
+ );
10830
+ }
10301
10831
  function CellEditShell({
10302
10832
  href,
10303
10833
  display,
@@ -10344,12 +10874,17 @@ function EditableTableCell({
10344
10874
  className,
10345
10875
  href,
10346
10876
  editTrigger = "click",
10347
- editRequest
10877
+ editRequest,
10878
+ editSeed
10348
10879
  }) {
10349
10880
  const [open, setOpen] = (0, import_react19.useState)(false);
10881
+ const [refusal, setRefusal] = (0, import_react19.useState)(null);
10350
10882
  (0, import_react19.useEffect)(() => {
10351
10883
  if (editRequest !== void 0) setOpen(true);
10352
10884
  }, [editRequest]);
10885
+ (0, import_react19.useEffect)(() => {
10886
+ if (!open) setRefusal(null);
10887
+ }, [open]);
10353
10888
  const usePencil = Boolean(href) || editTrigger === "pencil";
10354
10889
  if (editType === "string") {
10355
10890
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
@@ -10362,7 +10897,8 @@ function EditableTableCell({
10362
10897
  ...className === void 0 ? {} : { className },
10363
10898
  ...href === void 0 ? {} : { href },
10364
10899
  usePencil,
10365
- ...editRequest === void 0 ? {} : { editRequest }
10900
+ ...editRequest === void 0 ? {} : { editRequest },
10901
+ ...editSeed === void 0 || editSeed === null ? {} : { editSeed }
10366
10902
  }
10367
10903
  );
10368
10904
  }
@@ -10407,25 +10943,33 @@ function EditableTableCell({
10407
10943
  ) }) }),
10408
10944
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(TooltipContent, { children: "Edit value" })
10409
10945
  ] }),
10410
- /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10946
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)(
10411
10947
  PopoverContent,
10412
10948
  {
10413
10949
  align: "start",
10414
10950
  className: "w-56 p-2",
10415
10951
  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
- )
10952
+ children: [
10953
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
10954
+ TypedEditor,
10955
+ {
10956
+ editType,
10957
+ value,
10958
+ editOptions,
10959
+ onCommit: (next) => {
10960
+ const refused = onCommit(next);
10961
+ if (typeof refused === "string" && refused) {
10962
+ setRefusal(refused);
10963
+ return;
10964
+ }
10965
+ setRefusal(null);
10966
+ setOpen(false);
10967
+ },
10968
+ onCancel: () => setOpen(false)
10969
+ }
10970
+ ),
10971
+ refusal ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CellRefusal, { reason: refusal }) : null
10972
+ ]
10429
10973
  }
10430
10974
  )
10431
10975
  ] });
@@ -10438,17 +10982,31 @@ function InlineStringEditor({
10438
10982
  className,
10439
10983
  href,
10440
10984
  usePencil,
10441
- editRequest
10985
+ editRequest,
10986
+ editSeed
10442
10987
  }) {
10443
10988
  const [editing, setEditing] = (0, import_react19.useState)(false);
10444
10989
  const [draft, setDraft] = (0, import_react19.useState)(value);
10990
+ const [refusal, setRefusal] = (0, import_react19.useState)(null);
10445
10991
  const handledEditRequest = (0, import_react19.useRef)(void 0);
10992
+ const seedRef = (0, import_react19.useRef)(editSeed);
10993
+ seedRef.current = editSeed;
10446
10994
  (0, import_react19.useEffect)(() => {
10447
10995
  if (editRequest === void 0 || handledEditRequest.current === editRequest) return;
10448
10996
  handledEditRequest.current = editRequest;
10449
- setDraft(value);
10997
+ setDraft(seedRef.current ?? value);
10998
+ setRefusal(null);
10450
10999
  setEditing(true);
10451
11000
  }, [editRequest, value]);
11001
+ const commit = (next) => {
11002
+ const refused = onCommit(next);
11003
+ if (typeof refused === "string" && refused) {
11004
+ setRefusal(refused);
11005
+ return false;
11006
+ }
11007
+ setRefusal(null);
11008
+ return true;
11009
+ };
10452
11010
  if (!editing) {
10453
11011
  if (usePencil) {
10454
11012
  return /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
@@ -10499,32 +11057,38 @@ function InlineStringEditor({
10499
11057
  /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(TooltipContent, { children: "Click to edit" })
10500
11058
  ] });
10501
11059
  }
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
- );
11060
+ return /* @__PURE__ */ (0, import_jsx_runtime39.jsxs)("span", { className: "block w-full", children: [
11061
+ /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(
11062
+ Input,
11063
+ {
11064
+ autoFocus: true,
11065
+ value: draft,
11066
+ onClick: (e) => e.stopPropagation(),
11067
+ onChange: (e) => {
11068
+ setDraft(e.target.value);
11069
+ if (refusal) setRefusal(null);
11070
+ },
11071
+ onBlur: () => {
11072
+ if (commit(draft)) setEditing(false);
11073
+ },
11074
+ onKeyDown: (e) => {
11075
+ if (e.key === "Enter") {
11076
+ e.preventDefault();
11077
+ if (commit(draft)) setEditing(false);
11078
+ }
11079
+ if (e.key === "Escape") {
11080
+ e.preventDefault();
11081
+ setRefusal(null);
11082
+ setEditing(false);
11083
+ }
11084
+ },
11085
+ "aria-invalid": refusal ? true : void 0,
11086
+ className: cn("h-7 text-sm", refusal && "border-amber-500 focus-visible:ring-amber-500"),
11087
+ style: { fontSize: "16px" }
11088
+ }
11089
+ ),
11090
+ refusal ? /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(CellRefusal, { reason: refusal }) : null
11091
+ ] });
10528
11092
  }
10529
11093
  function TypedEditor({
10530
11094
  editType,
@@ -10709,8 +11273,8 @@ function NumberEditor({
10709
11273
  style: { fontSize: "16px" },
10710
11274
  onKeyDown: (e) => {
10711
11275
  if (e.key === "Enter") {
10712
- const n = Number(draft);
10713
- if (Number.isFinite(n)) onCommit(n);
11276
+ const n2 = Number(draft);
11277
+ if (Number.isFinite(n2)) onCommit(n2);
10714
11278
  }
10715
11279
  if (e.key === "Escape") onCancel();
10716
11280
  }
@@ -10737,8 +11301,8 @@ function NumberEditor({
10737
11301
  className: "h-7",
10738
11302
  "aria-label": "Apply value",
10739
11303
  onClick: () => {
10740
- const n = Number(draft);
10741
- if (Number.isFinite(n)) onCommit(n);
11304
+ const n2 = Number(draft);
11305
+ if (Number.isFinite(n2)) onCommit(n2);
10742
11306
  },
10743
11307
  children: /* @__PURE__ */ (0, import_jsx_runtime39.jsx)(Check, { className: "h-3.5 w-3.5" })
10744
11308
  }
@@ -11258,6 +11822,8 @@ function filterKindLabel(kind) {
11258
11822
  return "Boolean";
11259
11823
  case "number":
11260
11824
  return "Number";
11825
+ case "date":
11826
+ return "Date";
11261
11827
  case "auto":
11262
11828
  return "Auto";
11263
11829
  case false:
@@ -11546,6 +12112,7 @@ function MatrxDataTableCore({
11546
12112
  virtualize,
11547
12113
  coverage,
11548
12114
  spreadsheet,
12115
+ facets,
11549
12116
  rowHeight: explicitRowHeight
11550
12117
  }) {
11551
12118
  const appendQuery = query?.mode === "controlled-append" ? query : null;
@@ -12603,9 +13170,92 @@ function MatrxDataTableCore({
12603
13170
  ...spreadsheet.undoDepth === void 0 ? {} : { undoDepth: spreadsheet.undoDepth },
12604
13171
  ...spreadsheet.fillHandle === void 0 ? {} : { fillHandle: spreadsheet.fillHandle },
12605
13172
  ...spreadsheet.paste === void 0 ? {} : { paste: spreadsheet.paste },
12606
- notify: spreadsheet.notify ?? ((message) => toast.success(message))
13173
+ notify: spreadsheet.notify ?? ((message) => toast.success(message)),
13174
+ onEditRequest: (address, seed) => requestCellEditRef.current(address, seed),
13175
+ ...spreadsheet.validateCell ? { validateCell: spreadsheet.validateCell } : {}
12607
13176
  } : null
12608
13177
  );
13178
+ const sheetRef = (0, import_react21.useRef)(sheet);
13179
+ sheetRef.current = sheet;
13180
+ const searchValueRef = (0, import_react21.useRef)(searchValue);
13181
+ searchValueRef.current = searchValue;
13182
+ const facetsEnabled = Boolean(facets && facets.enabled !== false);
13183
+ const [facetState, setFacetState] = (0, import_react21.useState)({});
13184
+ const facetReadCell = (0, import_react21.useCallback)(
13185
+ (row, colId) => {
13186
+ if (facets?.readCell) return facets.readCell(row, colId);
13187
+ const col = visibleColumns.find((candidate) => columnId(candidate) === colId);
13188
+ return col ? getFilterValue(row, col) : void 0;
13189
+ },
13190
+ [facets, visibleColumns]
13191
+ );
13192
+ const facetInputsRef = (0, import_react21.useRef)({ data, facets, facetReadCell });
13193
+ facetInputsRef.current = { data, facets, facetReadCell };
13194
+ const facetRequestRef = (0, import_react21.useRef)(/* @__PURE__ */ new Map());
13195
+ const requestFacets = (0, import_react21.useCallback)((colId) => {
13196
+ const { data: rows, facets: config, facetReadCell: readCell } = facetInputsRef.current;
13197
+ if (!config || config.enabled === false) return;
13198
+ const complete = localFacetsAreComplete(rows.length, config.totalRows);
13199
+ const limit = config.limit;
13200
+ if (complete || !config.source) {
13201
+ setFacetState((current) => ({
13202
+ ...current,
13203
+ [colId]: {
13204
+ loading: false,
13205
+ data: computeColumnFacets({
13206
+ columnId: colId,
13207
+ rows,
13208
+ readValue: readCell,
13209
+ ...limit === void 0 ? {} : { limit },
13210
+ complete
13211
+ })
13212
+ }
13213
+ }));
13214
+ return;
13215
+ }
13216
+ const ticket = (facetRequestRef.current.get(colId) ?? 0) + 1;
13217
+ facetRequestRef.current.set(colId, ticket);
13218
+ setFacetState((current) => ({
13219
+ ...current,
13220
+ [colId]: { loading: true, data: current[colId]?.data ?? null }
13221
+ }));
13222
+ void Promise.resolve().then(
13223
+ () => config.source?.({ columnId: colId, search: searchValueRef.current, limit: limit ?? 200 })
13224
+ ).then((answer) => {
13225
+ if (facetRequestRef.current.get(colId) !== ticket) return;
13226
+ if (!answer) {
13227
+ setFacetState((current) => ({
13228
+ ...current,
13229
+ [colId]: {
13230
+ loading: false,
13231
+ data: computeColumnFacets({
13232
+ columnId: colId,
13233
+ rows: facetInputsRef.current.data,
13234
+ readValue: facetInputsRef.current.facetReadCell,
13235
+ ...limit === void 0 ? {} : { limit },
13236
+ complete: false
13237
+ })
13238
+ }
13239
+ }));
13240
+ return;
13241
+ }
13242
+ setFacetState((current) => ({
13243
+ ...current,
13244
+ [colId]: { loading: false, data: { ...answer, answeredBy: "source", complete: true } }
13245
+ }));
13246
+ }).catch((error) => {
13247
+ if (facetRequestRef.current.get(colId) !== ticket) return;
13248
+ setFacetState((current) => ({
13249
+ ...current,
13250
+ [colId]: { loading: false, data: null, error: describeFacetFailure(error) }
13251
+ }));
13252
+ });
13253
+ }, []);
13254
+ (0, import_react21.useEffect)(() => {
13255
+ if (!facetsEnabled) return;
13256
+ setFacetState({});
13257
+ facetRequestRef.current = /* @__PURE__ */ new Map();
13258
+ }, [facetsEnabled, data, searchValue]);
12609
13259
  (0, import_react21.useEffect)(() => {
12610
13260
  if (!controlledQuery || controlledQuery.state.page === safePage) return;
12611
13261
  emitControlledQueryChange({ page: safePage });
@@ -12650,11 +13300,11 @@ function MatrxDataTableCore({
12650
13300
  });
12651
13301
  const hasActiveFilters = activeFilterCount > 0 || Boolean(searchValue.trim()) || intelligentSearchEnabled && searchScope !== "all" || Boolean(anyOfValue.trim()) || completeLayeredFilterRules(layeredFilters).length > 0 || facetActive;
12652
13302
  const changeCount = (0, import_react21.useMemo)(() => {
12653
- let n = 0;
13303
+ let n2 = 0;
12654
13304
  for (const fields of Object.values(edits)) {
12655
- n += Object.keys(fields).length;
13305
+ n2 += Object.keys(fields).length;
12656
13306
  }
12657
- return n;
13307
+ return n2;
12658
13308
  }, [edits]);
12659
13309
  const showSearch = toolbar?.search !== false;
12660
13310
  const openDetail = (row) => {
@@ -12733,6 +13383,23 @@ function MatrxDataTableCore({
12733
13383
  icon: void 0
12734
13384
  } : emptyState;
12735
13385
  const commitCell = (rowId, field, next) => {
13386
+ if (edit?.validate) {
13387
+ const row = data.find((candidate) => getRowId(candidate) === rowId);
13388
+ if (row) {
13389
+ let refusal;
13390
+ try {
13391
+ refusal = edit.validate({
13392
+ row: applyRowEdits(row, edits[rowId]),
13393
+ rowId,
13394
+ columnId: field,
13395
+ value: next
13396
+ });
13397
+ } catch {
13398
+ refusal = void 0;
13399
+ }
13400
+ if (refusal) return refusal;
13401
+ }
13402
+ }
12736
13403
  const scope = editScopeKey;
12737
13404
  const nextEdits = { [rowId]: { [field]: next } };
12738
13405
  const revision = setCellEdit(rowId, field, next);
@@ -12777,9 +13444,29 @@ function MatrxDataTableCore({
12777
13444
  setEditRequest((current) => ({
12778
13445
  rowId: getRowId(row),
12779
13446
  field,
12780
- nonce: (current?.nonce ?? 0) + 1
13447
+ nonce: (current?.nonce ?? 0) + 1,
13448
+ seed: null
12781
13449
  }));
12782
13450
  };
13451
+ const requestCellEdit = (address, seed) => {
13452
+ if (!editEnabled) return false;
13453
+ const row = data.find((candidate) => getRowId(candidate) === address.rowId);
13454
+ if (!row) return false;
13455
+ const column = visibleColumns.find(
13456
+ (candidate) => columnId(candidate) === address.fieldName
13457
+ );
13458
+ if (!column?.editable || !(column.editableIf?.(row) ?? true)) return false;
13459
+ const field = column.accessorKey ? String(column.accessorKey) : columnId(column);
13460
+ setEditRequest((current) => ({
13461
+ rowId: address.rowId,
13462
+ field,
13463
+ nonce: (current?.nonce ?? 0) + 1,
13464
+ seed
13465
+ }));
13466
+ return true;
13467
+ };
13468
+ const requestCellEditRef = (0, import_react21.useRef)(requestCellEdit);
13469
+ requestCellEditRef.current = requestCellEdit;
12783
13470
  const buildRecordControls = (row) => {
12784
13471
  const rowId = getRowId(row);
12785
13472
  const pending = edits[rowId];
@@ -13020,17 +13707,78 @@ function MatrxDataTableCore({
13020
13707
  setEdits({});
13021
13708
  edit?.onCancel?.();
13022
13709
  };
13023
- (0, import_react21.useEffect)(() => {
13024
- if (!host.rowContextRegistry || !contextMenu && !host.createDefaultRowContext) return;
13025
- return host.rowContextRegistry.register(tableContextMenuInstanceId, (rowId) => {
13710
+ const resolveMenuLevel = (target) => {
13711
+ const rowFor = (rowId) => {
13026
13712
  const row = data.find((candidate) => getRowId(candidate) === rowId);
13027
13713
  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]);
13714
+ return applyRowEdits(row, edits[rowId]);
13715
+ };
13716
+ const columnFor = (id) => visibleColumns.find((col) => columnId(col) === id);
13717
+ const labelFor = (col, id) => col?.label ?? (typeof col?.header === "string" ? col.header : id);
13718
+ const fromHost = (payload) => host.createDefaultMenuContext?.(payload) ?? null;
13719
+ switch (target.level) {
13720
+ case "row": {
13721
+ if (!target.rowId) return null;
13722
+ const displayRow = rowFor(target.rowId);
13723
+ if (!displayRow) return null;
13724
+ const controls = buildRecordControls(displayRow);
13725
+ return contextMenu?.resolveRowContext?.(displayRow, controls) ?? fromHost({ level: "row", row: displayRow, rowId: target.rowId, controls });
13726
+ }
13727
+ case "cell": {
13728
+ if (!target.rowId || !target.columnId) return null;
13729
+ const displayRow = rowFor(target.rowId);
13730
+ const col = columnFor(target.columnId);
13731
+ if (!displayRow || !col) return null;
13732
+ const controls = buildRecordControls(displayRow);
13733
+ const value = spreadsheet?.readCell ? spreadsheet.readCell(displayRow, target.columnId) : getCellValue(displayRow, col);
13734
+ const args = {
13735
+ row: displayRow,
13736
+ rowId: target.rowId,
13737
+ columnId: target.columnId,
13738
+ columnLabel: labelFor(col, target.columnId),
13739
+ value,
13740
+ text: stringifyCellValue(value),
13741
+ controls
13742
+ };
13743
+ return contextMenu?.resolveCellContext?.(args) ?? fromHost({ level: "cell", ...args });
13744
+ }
13745
+ case "column": {
13746
+ if (!target.columnId) return null;
13747
+ const col = columnFor(target.columnId);
13748
+ if (!col) return null;
13749
+ const args = {
13750
+ columnId: target.columnId,
13751
+ columnLabel: labelFor(col, target.columnId),
13752
+ values: paginated.map(
13753
+ (row) => getCellValue(applyRowEdits(row, edits[getRowId(row)]), col)
13754
+ )
13755
+ };
13756
+ return contextMenu?.resolveColumnContext?.(args) ?? fromHost({ level: "column", ...args });
13757
+ }
13758
+ case "selection": {
13759
+ const addresses = sheetRef.current.selectionAddresses();
13760
+ const cellCount = addresses.rowIds.length * addresses.columnIds.length;
13761
+ if (cellCount <= 1) {
13762
+ return resolveMenuLevel({ ...target, level: "cell" });
13763
+ }
13764
+ const args = { ...addresses, tsv: sheetRef.current.selectionTsv(), cellCount };
13765
+ return contextMenu?.resolveSelectionContext?.(args) ?? fromHost({ level: "selection", ...args });
13766
+ }
13767
+ case "table": {
13768
+ const args = { rowCount: data.length, columnIds: visibleColumns.map((col) => columnId(col)) };
13769
+ return contextMenu?.resolveTableContext?.(args) ?? fromHost({ level: "table", ...args });
13770
+ }
13771
+ }
13772
+ };
13773
+ const resolveMenuLevelRef = (0, import_react21.useRef)(resolveMenuLevel);
13774
+ resolveMenuLevelRef.current = resolveMenuLevel;
13775
+ (0, import_react21.useEffect)(() => {
13776
+ if (!host.rowContextRegistry) return;
13777
+ return host.rowContextRegistry.register(
13778
+ tableContextMenuInstanceId,
13779
+ (target) => resolveMenuLevelRef.current(target)
13780
+ );
13781
+ }, [host.rowContextRegistry, tableContextMenuInstanceId]);
13034
13782
  const showActionsCol = windowEnabled || Boolean(rowActions) || showInlineRowCopy;
13035
13783
  const selectedIdSet = (0, import_react21.useMemo)(
13036
13784
  () => new Set(selection?.selectedIds ?? []),
@@ -13112,8 +13860,19 @@ function MatrxDataTableCore({
13112
13860
  dirty: Boolean(rowEdits && field in rowEdits),
13113
13861
  onCommit: (next) => commitCell(id, field, next),
13114
13862
  ...href === void 0 ? {} : { href },
13115
- ...col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13116
- ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce } : {}
13863
+ ...spreadsheetEnabled ? {
13864
+ // 🚨 THE CLICK LAW, enforced where the two paths meet. In a grid
13865
+ // with spreadsheet gestures a single click SELECTS a cell — it
13866
+ // may never drop a person into a free-text buffer, because the
13867
+ // same click is how they start a range, and an editor that opens
13868
+ // under the pointer eats the drag and the next keystroke. So the
13869
+ // editor opens the way it does in Excel: Enter, F2, the hover
13870
+ // pencil, or simply typing. An explicit `editTrigger: "click"`
13871
+ // does not override this; the law is the table's, not the
13872
+ // column's.
13873
+ editTrigger: "pencil"
13874
+ } : col.editTrigger === void 0 ? {} : { editTrigger: col.editTrigger },
13875
+ ...editRequest?.rowId === id && editRequest.field === field ? { editRequest: editRequest.nonce, editSeed: editRequest.seed } : {}
13117
13876
  }
13118
13877
  );
13119
13878
  }
@@ -13871,6 +14630,7 @@ function MatrxDataTableCore({
13871
14630
  "th",
13872
14631
  {
13873
14632
  "data-matrx-table-column-id": id,
14633
+ "data-matrx-header-column-id": id,
13874
14634
  "data-matrx-column-drag-shadow": isColumnDragShadow ? "true" : void 0,
13875
14635
  "aria-sort": isSorted ? sort?.direction === "asc" ? "ascending" : "descending" : void 0,
13876
14636
  className: cn(
@@ -13907,6 +14667,12 @@ function MatrxDataTableCore({
13907
14667
  {
13908
14668
  label: col.header,
13909
14669
  labelText: headerLabel,
14670
+ ...facetsEnabled ? {
14671
+ facets: facetState[id]?.data ?? null,
14672
+ facetsLoading: facetState[id]?.loading ?? false,
14673
+ ...facetState[id]?.error === void 0 ? {} : { facetsError: facetState[id]?.error },
14674
+ onFilterOpen: () => requestFacets(id)
14675
+ } : {},
13910
14676
  sortable: col.sortable !== false,
13911
14677
  isSorted: Boolean(isSorted),
13912
14678
  sortDirection: sort?.direction ?? defaultSortDirection,
@@ -14233,7 +14999,7 @@ function MatrxDataTableCore({
14233
14999
  "div",
14234
15000
  {
14235
15001
  "data-matrx-cell-content": true,
14236
- className: "min-w-0 max-w-full flex-1 whitespace-normal break-words [overflow-wrap:anywhere] sm:[&_*]:min-w-0 sm:[&_*]:whitespace-normal sm:[&_*]:[overflow-wrap:anywhere] sm:[&_.truncate]:whitespace-nowrap sm:[&_[data-slot=badge]]:whitespace-nowrap sm:[&_[data-slot=badge]]:[overflow-wrap:normal] sm:[&_[data-matrx-cell-control]]:w-max sm:[&_[data-matrx-cell-control]]:min-w-max sm:[&_[data-matrx-cell-control]]:max-w-none sm:[&_[data-matrx-cell-control]]:whitespace-nowrap sm:[&_[data-matrx-cell-control]]:[overflow-wrap:normal] sm:[&_[data-matrx-cell-control]_*]:whitespace-nowrap sm:[&_[data-matrx-cell-control]_*]:[overflow-wrap:normal]",
15002
+ className: "min-w-0 max-w-full flex-1 whitespace-normal break-words [overflow-wrap:anywhere] sm:[&_*]:min-w-0 sm:[&_*]:whitespace-normal sm:[&_*]:[overflow-wrap:anywhere] sm:[&_.truncate]:whitespace-nowrap sm:[&_[data-slot=badge]]:whitespace-nowrap sm:[&_[data-slot=badge]]:[overflow-wrap:normal] sm:[&_[data-matrx-cell-control]]:w-max sm:[&_[data-matrx-cell-control]]:min-w-max sm:[&_[data-matrx-cell-control]]:max-w-none sm:[&_[data-matrx-cell-control]]:whitespace-nowrap sm:[&_[data-matrx-cell-control]]:[overflow-wrap:normal] sm:[&_[data-matrx-cell-control]_*]:whitespace-nowrap sm:[&_[data-matrx-cell-control]_*]:[overflow-wrap:normal] [&_[data-matrx-cell-fill]]:!w-full [&_[data-matrx-cell-fill]]:!min-w-0 [&_[data-matrx-cell-fill]]:!max-w-full sm:[&_[data-matrx-cell-fill]]:!w-full sm:[&_[data-matrx-cell-fill]]:!min-w-0 sm:[&_[data-matrx-cell-fill]]:!max-w-full",
14237
15003
  children: renderRecordCell(row, col, index)
14238
15004
  }
14239
15005
  )
@@ -14581,8 +15347,8 @@ function MatrxDataTableCore({
14581
15347
  itemsPerPage: pageSize === 0 && allRowsAreLoaded ? 0 : effectivePageSize,
14582
15348
  currentPage: safePage,
14583
15349
  onPageChange: setPage,
14584
- onItemsPerPageChange: (n) => {
14585
- setPageSize(n);
15350
+ onItemsPerPageChange: (n2) => {
15351
+ setPageSize(n2);
14586
15352
  if (!controlledQuery) setPage(1);
14587
15353
  },
14588
15354
  pageSizeOptions: pageSizeOptions.filter((size) => size > 0),