@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts","../../src/data-table/query-control.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import {\n collectSelectOptions,\n resolveFilterKind,\n type ResolvedFilterKind,\n} from \"./infer-filter\";\nimport type { MatrxColumnDef, MatrxDataTableQueryState } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\n\nexport interface QueryFilterMeta {\n kind: ResolvedFilterKind | null;\n options: Array<{ value: string; label: string }>;\n}\n\n/**\n * Resolve header-filter metadata without inferring remote cardinality from the\n * current page. Controlled callers must declare select options explicitly;\n * local tables preserve the original inference behavior.\n */\nexport function resolveQueryFilterMeta<T>(\n column: MatrxColumnDef<T>,\n rows: T[],\n controlled: boolean,\n): QueryFilterMeta {\n if (!controlled) {\n const kind = resolveFilterKind(column, rows);\n return {\n kind,\n options: kind === \"select\" ? collectSelectOptions(column, rows) : [],\n };\n }\n if (column.filter === false) return { kind: null, options: [] };\n if (resolveColumnMarker(column) && (column.filter === undefined || column.filter === \"auto\")) {\n return { kind: \"boolean\", options: [] };\n }\n const declared = column.filter;\n const kind: ResolvedFilterKind =\n declared && declared !== \"auto\"\n ? declared\n : column.filterOptions?.length\n ? \"select\"\n : \"text\";\n return {\n kind,\n options: kind === \"select\" ? (column.filterOptions ?? []) : [],\n };\n}\n\nexport interface QueryStateChangeOptions {\n /** Reset to the first page after a query-shape change. */\n resetPage?: boolean;\n}\n\n/**\n * Build the next controlled query state without mutating the caller's object.\n * Search, filters, sort, facets, and page-size changes pass `resetPage: true`;\n * direct pagination passes a page patch without resetting it.\n */\nexport function nextQueryState(\n current: MatrxDataTableQueryState,\n patch: Partial<MatrxDataTableQueryState>,\n options: QueryStateChangeOptions = {},\n): MatrxDataTableQueryState {\n return {\n ...current,\n ...patch,\n page: options.resetPage ? 1 : (patch.page ?? current.page),\n };\n}\n\n/** Resolve a safe one-based page for the latest remote total. */\nexport function safeQueryPage(\n requestedPage: number,\n totalItems: number,\n pageSize: number,\n): number {\n const safeTotal = Math.max(0, totalItems);\n const safeSize = Math.max(1, pageSize);\n const pageCount = Math.max(1, Math.ceil(safeTotal / safeSize));\n return Math.min(Math.max(1, requestedPage), pageCount);\n}\n"],"mappings":"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","names":["kind"]}
1
+ {"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts","../../src/data-table/query-control.ts"],"sourcesContent":["import type { ColumnMarker, MatrxColumnDef } from \"./types\";\n\nexport type ResolvedColumnMarker = Exclude<ColumnMarker, false>;\n\nconst MARKER_ACCESSORS: Readonly<Record<string, ResolvedColumnMarker>> = {\n favorite: \"favorite\",\n is_favorite: \"favorite\",\n favourite: \"favorite\",\n is_favourite: \"favorite\",\n pinned: \"pin\",\n is_pinned: \"pin\",\n};\n\n/**\n * Resolve the semantic without guessing from partial names. A field such as\n * `favorite_count` is not a marker; consumers can always opt in explicitly.\n */\nexport function resolveColumnMarker<T>(\n column: MatrxColumnDef<T>,\n): ResolvedColumnMarker | undefined {\n if (column.marker === false) return undefined;\n if (column.marker) return column.marker;\n return column.accessorKey ? MARKER_ACCESSORS[column.accessorKey] : undefined;\n}\n\nexport function markerCopy(marker: ResolvedColumnMarker): {\n label: string;\n lowerLabel: string;\n onlyLabel: string;\n notLabel: string;\n} {\n return marker === \"favorite\"\n ? {\n label: \"Favorites\",\n lowerLabel: \"favorites\",\n onlyLabel: \"Favorites only\",\n notLabel: \"Not favorites\",\n }\n : {\n label: \"Pins\",\n lowerLabel: \"pins\",\n onlyLabel: \"Pins only\",\n notLabel: \"Not pinned\",\n };\n}\n","import type {\n ColumnFilterValue,\n ColumnFiltersState,\n MatrxColumnDef,\n SortState,\n TableSearchMatchMode,\n} from \"./types\";\nimport {\n completeLayeredFilterRules,\n layeredFilterMatchesValue,\n type LayeredFilterRule,\n} from \"./layered-filters\";\nimport { dateFilterBounds } from \"./date-filter\";\n\n/**\n * Sentinel select-filter values for \"no value\" / \"any value\" — every select\n * filter offers them automatically when the column has empty cells, so any\n * table can isolate rows missing a value (or having one) without extra config.\n */\nexport const SELECT_EMPTY_VALUE = \"__empty__\";\nexport const SELECT_NOT_EMPTY_VALUE = \"__not_empty__\";\n/** Exact SQL-style NULL selection, distinct from the empty-cell option. */\nexport const SELECT_NULL_VALUE = \"__null__\";\n\nexport function columnId<T>(col: MatrxColumnDef<T>): string {\n if (col.id) return col.id;\n if (col.accessorKey) return String(col.accessorKey);\n throw new Error(\"MatrxDataTable column requires `id` or `accessorKey`\");\n}\n\nexport function getCellValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n if (col.accessorFn) return col.accessorFn(row);\n if (col.accessorKey) {\n return (row as Record<string, unknown>)[col.accessorKey];\n }\n return undefined;\n}\n\nexport function getFilterValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.filterValue ? col.filterValue(row) : getCellValue(row, col);\n}\n\nexport function getSortValue<T>(row: T, col: MatrxColumnDef<T>): unknown {\n return col.sortValue ? col.sortValue(row) : getCellValue(row, col);\n}\n\nexport function stringifyCellValue(value: unknown): string {\n if (value == null) return \"\";\n if (typeof value === \"string\") return value;\n if (typeof value === \"number\" || typeof value === \"boolean\") {\n return String(value);\n }\n if (value instanceof Date) return value.toISOString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\n/** Unicode-aware tokenization shared by whole-word table search. */\nexport function tableSearchWords(value: string): string[] {\n return value\n .toLocaleLowerCase()\n .split(/[^\\p{L}\\p{N}]+/u)\n .filter(Boolean);\n}\n\n/**\n * Match the primary table search against one row's searchable text. In\n * whole-word mode every entered word must exist as a complete token, so\n * `are` matches `we are ready` but never `hardware`.\n */\nexport function matchesTableSearch(\n searchableText: string,\n query: string,\n mode: TableSearchMatchMode = \"contains\",\n): boolean {\n const normalizedQuery = query.trim().toLocaleLowerCase();\n if (!normalizedQuery) return true;\n if (mode === \"contains\") {\n return searchableText.toLocaleLowerCase().includes(normalizedQuery);\n }\n const actualWords = new Set(tableSearchWords(searchableText));\n const expectedWords = tableSearchWords(normalizedQuery);\n return (\n expectedWords.length > 0 &&\n expectedWords.every((word) => actualWords.has(word))\n );\n}\n\nfunction compareValues(a: unknown, b: unknown): number {\n if (a == null && b == null) return 0;\n if (a == null) return -1;\n if (b == null) return 1;\n if (typeof a === \"number\" && typeof b === \"number\") return a - b;\n if (typeof a === \"boolean\" && typeof b === \"boolean\") {\n return Number(a) - Number(b);\n }\n return stringifyCellValue(a).localeCompare(stringifyCellValue(b), undefined, {\n numeric: true,\n sensitivity: \"base\",\n });\n}\n\n/** Select filters treat array-valued cells as sets, never as serialized JSON choices. */\nexport function selectCellValues(value: unknown): string[] {\n return (Array.isArray(value) ? value : [value])\n .map(stringifyCellValue)\n .filter((item) => item !== \"\");\n}\n\nexport function passesColumnFilter(\n value: unknown,\n filter: ColumnFilterValue,\n): boolean {\n const matches = (() => {\n switch (filter.kind) {\n case \"text\": {\n const s = stringifyCellValue(value);\n if (filter.mode === \"null\") return value == null;\n if (filter.mode === \"not_null\") return value != null;\n if (filter.mode === \"empty\") return s.trim() === \"\";\n if (filter.mode === \"not_empty\") return s.trim() !== \"\";\n const q = filter.value.trim().toLowerCase();\n if (!q) return true;\n return s.toLowerCase().includes(q);\n }\n case \"select\": {\n // `values: []` is an explicit multi-select state: the user deselected\n // every option, so it must produce an empty result. Legacy single-value\n // filters and an absent `values` field retain their clear/all behavior.\n if (filter.values !== undefined && filter.values.length === 0) {\n return false;\n }\n const selected = filter.values ??\n (filter.value && filter.value !== \"__all__\" ? [filter.value] : []);\n if (selected.length === 0) return true;\n const values = selectCellValues(value);\n // OR semantics across the set; sentinels compose (\"A or (empty)\").\n return selected.some((v) =>\n v === SELECT_NULL_VALUE\n ? value == null\n : v === SELECT_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : v === \"\"\n ? value === \"\" || (Array.isArray(value) && value.includes(\"\"))\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n // A missing cell is not zero. Without this guard, Number(\"\") made a\n // blank numeric value pass filters whose range included zero.\n const raw = stringifyCellValue(value).trim();\n if (raw === \"\") return false;\n const normalized = raw.replace(/[^0-9.-]/g, \"\");\n if (normalized === \"\") return false;\n const n = typeof value === \"number\"\n ? value\n : Number(normalized);\n if (!Number.isFinite(n)) return false;\n if (filter.min !== undefined && n < filter.min) return false;\n if (filter.max !== undefined && n > filter.max) return false;\n return true;\n }\n case \"date\": {\n const raw = value instanceof Date ? value : new Date(stringifyCellValue(value));\n if (Number.isNaN(raw.valueOf())) return false;\n const { since, until } = dateFilterBounds(filter);\n const timestamp = raw.valueOf();\n if (since !== undefined) {\n const sinceTimestamp = new Date(since).valueOf();\n if (Number.isNaN(sinceTimestamp) || timestamp < sinceTimestamp) return false;\n }\n if (until !== undefined) {\n const untilTimestamp = new Date(until).valueOf();\n if (Number.isNaN(untilTimestamp) || timestamp >= untilTimestamp) return false;\n }\n return true;\n }\n }\n })();\n return filter.negated ? !matches : matches;\n}\n\nexport function filterAndSortRows<T>(\n rows: T[],\n columns: MatrxColumnDef<T>[],\n columnFilters: ColumnFiltersState,\n sort: SortState | null,\n globalSearch: string,\n anyOf?: { columnIds: string[]; query: string },\n layeredFilters?: readonly LayeredFilterRule[],\n globalSearchMode: TableSearchMatchMode = \"contains\",\n searchText?: (row: T) => string,\n): T[] {\n const colById = new Map(columns.map((c) => [columnId(c), c]));\n const q = globalSearch.trim().toLowerCase();\n const anyQ = anyOf?.query.trim().toLowerCase() ?? \"\";\n const activeLayeredFilters = completeLayeredFilterRules(layeredFilters);\n\n let result = rows.filter((row) => {\n for (const [id, filter] of Object.entries(columnFilters)) {\n if (!filter) continue;\n const col = colById.get(id);\n if (!col || col.filter === false) continue;\n if (!passesColumnFilter(getFilterValue(row, col), filter)) return false;\n }\n\n for (const rule of activeLayeredFilters) {\n const col = colById.get(rule.field);\n if (!col || !layeredFilterMatchesValue(getFilterValue(row, col), rule)) {\n return false;\n }\n }\n\n if (anyQ && anyOf) {\n const hit = anyOf.columnIds.some((id) => {\n const col = colById.get(id);\n if (!col) return false;\n return stringifyCellValue(getFilterValue(row, col))\n .toLowerCase()\n .includes(anyQ);\n });\n if (!hit) return false;\n }\n\n if (!q) return true;\n const searchableValues = columns\n .filter(\n (col) =>\n col.filter !== false || Boolean(col.accessorKey || col.accessorFn),\n )\n .map((col) => stringifyCellValue(getFilterValue(row, col)));\n const additionalSearchText = searchText?.(row);\n if (additionalSearchText) searchableValues.push(additionalSearchText);\n if (globalSearchMode === \"contains\") {\n return searchableValues.some((value) =>\n matchesTableSearch(value, q, \"contains\"),\n );\n }\n return matchesTableSearch(searchableValues.join(\" \"), q, \"whole_words\");\n });\n\n if (sort) {\n const col = colById.get(sort.id);\n if (col && col.sortable !== false) {\n const dir = sort.direction === \"asc\" ? 1 : -1;\n result = [...result].sort((a, b) => {\n const va = getSortValue(a, col);\n const vb = getSortValue(b, col);\n // Empty cells (null / \"\") sort LAST in both directions — sorting a\n // column should surface its values, never a wall of blanks.\n const ea = va == null || stringifyCellValue(va) === \"\";\n const eb = vb == null || stringifyCellValue(vb) === \"\";\n if (ea !== eb) return ea ? 1 : -1;\n return compareValues(va, vb) * dir;\n });\n }\n }\n\n return result;\n}\n\nexport function countActiveColumnFilters(filters: ColumnFiltersState): number {\n let n = 0;\n for (const f of Object.values(filters)) {\n if (!f) continue;\n switch (f.kind) {\n case \"text\":\n if (f.value.trim() || (f.mode && f.mode !== \"contains\")) n += 1;\n break;\n case \"select\":\n if (f.values !== undefined || (f.value && f.value !== \"__all__\"))\n n += 1;\n break;\n case \"boolean\":\n n += 1;\n break;\n case \"number\":\n if (f.min !== undefined || f.max !== undefined) n += 1;\n break;\n case \"date\":\n if (isColumnFilterActive(f)) n += 1;\n break;\n }\n }\n return n;\n}\n\nexport function isColumnFilterActive(\n filter: ColumnFilterValue | undefined,\n): boolean {\n if (!filter) return false;\n switch (filter.kind) {\n case \"text\":\n return (\n filter.value.trim().length > 0 ||\n (filter.mode !== undefined && filter.mode !== \"contains\")\n );\n case \"select\":\n return filter.values !== undefined\n ? true\n : Boolean(filter.value) && filter.value !== \"__all__\";\n case \"boolean\":\n return true;\n case \"number\":\n return filter.min !== undefined || filter.max !== undefined;\n case \"date\": {\n const bounds = dateFilterBounds(filter);\n return bounds.since !== undefined || bounds.until !== undefined;\n }\n }\n}\n\n/** Apply draft edits onto a row for display (shallow field merge). */\nexport function applyRowEdits<T>(\n row: T,\n edits: Record<string, unknown> | undefined,\n): T {\n if (!edits || Object.keys(edits).length === 0) return row;\n return { ...row, ...edits };\n}\n","import type { ColumnFilterKind, MatrxColumnDef } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\nimport {\n SELECT_EMPTY_VALUE,\n SELECT_NOT_EMPTY_VALUE,\n SELECT_NULL_VALUE,\n getCellValue,\n selectCellValues,\n} from \"./filter-engine\";\n\n/** Auto-inference switches to text above this many distinct values… */\nconst SELECT_CARDINALITY_CAP = 24;\n\nexport type ResolvedFilterKind = \"text\" | \"select\" | \"boolean\" | \"number\" | \"date\";\n\n/**\n * Infer a filter UI from sample row values. Prefer select for low-cardinality\n * string/enum columns, boolean for booleans, number for numeric, else text.\n */\nexport function resolveFilterKind<T>(\n col: MatrxColumnDef<T>,\n sampleRows: T[],\n): ResolvedFilterKind | null {\n if (col.filter === false) return null;\n if (col.filter && col.filter !== \"auto\") return col.filter;\n if (resolveColumnMarker(col)) return \"boolean\";\n\n const values = sampleRows\n .slice(0, 200)\n .map((row) => getCellValue(row, col))\n .filter((v) => v != null);\n\n if (values.length === 0) return \"text\";\n\n if (values.every((v) => typeof v === \"boolean\")) return \"boolean\";\n if (values.every((v) => typeof v === \"number\" && Number.isFinite(v))) {\n return \"number\";\n }\n\n const strings = values.flatMap(selectCellValues);\n const unique = new Set(strings);\n if (\n unique.size >= 1 &&\n unique.size <= SELECT_CARDINALITY_CAP &&\n unique.size < strings.length * 0.6\n ) {\n return \"select\";\n }\n\n return \"text\";\n}\n\nexport function collectSelectOptions<T>(\n col: MatrxColumnDef<T>,\n rows: T[],\n): Array<{ value: string; label: string }> {\n const seen = new Set<string>();\n const options: Array<{ value: string; label: string }> = [];\n let hasEmpty = false;\n let hasEmptyCollection = false;\n for (const row of rows) {\n const cell = getCellValue(row, col);\n const values = selectCellValues(cell);\n if (values.length === 0) {\n hasEmpty = true;\n if (Array.isArray(cell)) hasEmptyCollection = true;\n continue;\n }\n for (const value of values) {\n if (col.filterOptions?.length || seen.has(value)) continue;\n seen.add(value);\n options.push({ value, label: value });\n }\n }\n const base = col.filterOptions?.length\n ? col.filterOptions.map((option) => ({\n ...option,\n label: option.label.trim() ? option.label : option.value === \"\" ? \"(empty string)\" : option.value.trim() ? option.value : \"(whitespace)\",\n }))\n : options.sort((a, b) => a.label.localeCompare(b.label));\n // Emptiness sentinels: whenever the column has blank cells, every select\n // filter can isolate them — or exclude them — with zero per-table config.\n if (hasEmpty) {\n // An explicit literal-empty choice must remain independently deselectable.\n // The combined empty sentinel would overlap it after All -> uncheck.\n // Empty collections still require that broader option: neither null nor\n // a literal empty string represents an empty array.\n const emptyOption = !hasEmptyCollection && base.some((option) => option.value === \"\")\n ? { value: SELECT_NULL_VALUE, label: \"(null)\" }\n : { value: SELECT_EMPTY_VALUE, label: \"(empty)\" };\n const additions = [];\n if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);\n if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {\n additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" });\n }\n base.unshift(...additions);\n }\n return base;\n}\n\nexport function filterKindLabel(\n kind: ColumnFilterKind | ResolvedFilterKind,\n): string {\n switch (kind) {\n case \"text\":\n return \"Text\";\n case \"select\":\n return \"Select\";\n case \"boolean\":\n return \"Boolean\";\n case \"number\":\n return \"Number\";\n case \"date\":\n return \"Date\";\n case \"auto\":\n return \"Auto\";\n case false:\n return \"Off\";\n }\n}\n","import {\n collectSelectOptions,\n resolveFilterKind,\n type ResolvedFilterKind,\n} from \"./infer-filter\";\nimport type { MatrxColumnDef, MatrxDataTableQueryState } from \"./types\";\nimport { resolveColumnMarker } from \"./column-marker\";\n\nexport interface QueryFilterMeta {\n kind: ResolvedFilterKind | null;\n options: Array<{ value: string; label: string }>;\n}\n\n/**\n * Resolve header-filter metadata without inferring remote cardinality from the\n * current page. Controlled callers must declare select options explicitly;\n * local tables preserve the original inference behavior.\n */\nexport function resolveQueryFilterMeta<T>(\n column: MatrxColumnDef<T>,\n rows: T[],\n controlled: boolean,\n): QueryFilterMeta {\n if (!controlled) {\n const kind = resolveFilterKind(column, rows);\n return {\n kind,\n options: kind === \"select\" ? collectSelectOptions(column, rows) : [],\n };\n }\n if (column.filter === false) return { kind: null, options: [] };\n if (resolveColumnMarker(column) && (column.filter === undefined || column.filter === \"auto\")) {\n return { kind: \"boolean\", options: [] };\n }\n const declared = column.filter;\n const kind: ResolvedFilterKind =\n declared && declared !== \"auto\"\n ? declared\n : column.filterOptions?.length\n ? \"select\"\n : \"text\";\n return {\n kind,\n options: kind === \"select\" ? (column.filterOptions ?? []) : [],\n };\n}\n\nexport interface QueryStateChangeOptions {\n /** Reset to the first page after a query-shape change. */\n resetPage?: boolean;\n}\n\n/**\n * Build the next controlled query state without mutating the caller's object.\n * Search, filters, sort, facets, and page-size changes pass `resetPage: true`;\n * direct pagination passes a page patch without resetting it.\n */\nexport function nextQueryState(\n current: MatrxDataTableQueryState,\n patch: Partial<MatrxDataTableQueryState>,\n options: QueryStateChangeOptions = {},\n): MatrxDataTableQueryState {\n return {\n ...current,\n ...patch,\n page: options.resetPage ? 1 : (patch.page ?? current.page),\n };\n}\n\n/** Resolve a safe one-based page for the latest remote total. */\nexport function safeQueryPage(\n requestedPage: number,\n totalItems: number,\n pageSize: number,\n): number {\n const safeTotal = Math.max(0, totalItems);\n const safeSize = Math.max(1, pageSize);\n const pageCount = Math.max(1, Math.ceil(safeTotal / safeSize));\n return Math.min(Math.max(1, requestedPage), pageCount);\n}\n"],"mappings":"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","names":["kind"]}
@@ -668,6 +668,26 @@ function useSpreadsheetGrid(options) {
668
668
  for (let i = 0; i < columnIds.length; i += 1) map.set(columnIds[i], i);
669
669
  return map;
670
670
  }, [columnIds]);
671
+ const screenPatches = (0, import_react.useCallback)(
672
+ (patches) => {
673
+ const validate = optionsRef.current?.validateCell;
674
+ if (!validate) return { accepted: patches, refusals: [] };
675
+ const accepted = [];
676
+ const refusals = [];
677
+ for (const patch of patches) {
678
+ let reason;
679
+ try {
680
+ reason = validate({ rowId: patch.rowId, columnId: patch.columnId, value: patch.value });
681
+ } catch {
682
+ reason = void 0;
683
+ }
684
+ if (reason) refusals.push({ columnId: patch.columnId, reason });
685
+ else accepted.push(patch);
686
+ }
687
+ return { accepted, refusals };
688
+ },
689
+ []
690
+ );
671
691
  const applyPatches = (0, import_react.useCallback)(
672
692
  async (patches, intent, label) => {
673
693
  const current = optionsRef.current;
@@ -724,9 +744,10 @@ function useSpreadsheetGrid(options) {
724
744
  ...base,
725
745
  range: { top: bounds.r0, left: bounds.c0, bottom: bounds.r1, right: bounds.c1 }
726
746
  });
727
- await applyPatches(result.patches, "fill-down", "Filled down");
728
- announceSkips(result.skipped, result.summary);
729
- }, [fillRequestBase, bounds, applyPatches, announceSkips]);
747
+ const screened = screenPatches(result.patches);
748
+ await applyPatches(screened.accepted, "fill-down", "Filled down");
749
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
750
+ }, [fillRequestBase, bounds, applyPatches, announceSkips, screenPatches]);
730
751
  const doClear = (0, import_react.useCallback)(async () => {
731
752
  const current = optionsRef.current;
732
753
  if (!current || !bounds) return;
@@ -747,9 +768,13 @@ function useSpreadsheetGrid(options) {
747
768
  patches.push({ rowId, columnId, value: null });
748
769
  }
749
770
  }
750
- await applyPatches(patches, "clear", "Cleared");
751
- announceSkips(skipped, `Cleared ${patches.length} cells.`);
752
- }, [bounds, applyPatches, announceSkips]);
771
+ const screened = screenPatches(patches);
772
+ await applyPatches(screened.accepted, "clear", "Cleared");
773
+ announceSkips(
774
+ [...skipped, ...screened.refusals],
775
+ `Cleared ${screened.accepted.length} cells.`
776
+ );
777
+ }, [bounds, applyPatches, announceSkips, screenPatches]);
753
778
  const doUndo = (0, import_react.useCallback)(async () => {
754
779
  const taken = popUndo(stack);
755
780
  if (!taken.entry) return;
@@ -834,6 +859,14 @@ function useSpreadsheetGrid(options) {
834
859
  case "escape":
835
860
  setRange(null);
836
861
  return;
862
+ case "edit":
863
+ case "editSeeded": {
864
+ const focused = range?.focus;
865
+ if (!focused || !current.onEditRequest) return;
866
+ const seed = action.kind === "editSeeded" ? action.seed : null;
867
+ if (current.onEditRequest(focused, seed)) event.preventDefault();
868
+ return;
869
+ }
837
870
  default:
838
871
  return;
839
872
  }
@@ -848,6 +881,13 @@ function useSpreadsheetGrid(options) {
848
881
  );
849
882
  return gridToTsv(grid);
850
883
  }, [range, rowIds, columnIds]);
884
+ const selectionAddresses = (0, import_react.useCallback)(() => {
885
+ if (!range || !bounds) return { rowIds: [], columnIds: [] };
886
+ return {
887
+ rowIds: rowIds.slice(bounds.r0, bounds.r1 + 1),
888
+ columnIds: columnIds.slice(bounds.c0, bounds.c1 + 1)
889
+ };
890
+ }, [range, bounds, rowIds, columnIds]);
851
891
  const onCopy = (0, import_react.useCallback)(
852
892
  (event) => {
853
893
  if (!optionsRef.current || !range || targetsAnEditor(event.target)) return;
@@ -903,11 +943,12 @@ function useSpreadsheetGrid(options) {
903
943
  patches.push({ rowId, columnId, value: line[c] ?? null });
904
944
  }
905
945
  }
906
- void applyPatches(patches, "paste", "Pasted");
946
+ const screened = screenPatches(patches);
947
+ void applyPatches(screened.accepted, "paste", "Pasted");
907
948
  const tail = outside > 0 ? ` ${outside} landed past the edge of the table and were ignored.` : "";
908
- announceSkips(skipped, `Pasted ${patches.length} cells.${tail}`);
949
+ announceSkips([...skipped, ...screened.refusals], `Pasted ${screened.accepted.length} cells.${tail}`);
909
950
  },
910
- [range, rowIds, columnIds, applyPatches, announceSkips]
951
+ [range, rowIds, columnIds, applyPatches, announceSkips, screenPatches]
911
952
  );
912
953
  const cellProps = (0, import_react.useCallback)(
913
954
  (rowId, columnId) => {
@@ -926,6 +967,10 @@ function useSpreadsheetGrid(options) {
926
967
  onMouseDown: (event) => {
927
968
  if (event.button !== 0) return;
928
969
  event.preventDefault();
970
+ const origin = event.currentTarget;
971
+ if (typeof HTMLElement !== "undefined" && origin instanceof HTMLElement) {
972
+ origin.closest('[data-matrx-table-spreadsheet="true"]')?.focus({ preventScroll: true });
973
+ }
929
974
  setRange(
930
975
  (previous) => event.shiftKey && previous ? { anchor: previous.anchor, focus: address } : { anchor: address, focus: address }
931
976
  );
@@ -987,12 +1032,13 @@ function useSpreadsheetGrid(options) {
987
1032
  source: { top: source.r0, left: source.c0, bottom: source.r1, right: source.c1 },
988
1033
  target
989
1034
  });
990
- void applyPatches(result.patches, "fill", "Filled");
991
- announceSkips(result.skipped, result.summary);
1035
+ const screened = screenPatches(result.patches);
1036
+ void applyPatches(screened.accepted, "fill", "Filled");
1037
+ announceSkips([...result.skipped, ...screened.refusals], result.summary);
992
1038
  };
993
1039
  window.addEventListener("mouseup", commit);
994
1040
  return () => window.removeEventListener("mouseup", commit);
995
- }, [options, fillRequestBase, applyPatches, announceSkips]);
1041
+ }, [options, fillRequestBase, applyPatches, announceSkips, screenPatches]);
996
1042
  return {
997
1043
  range,
998
1044
  bounds,
@@ -1008,7 +1054,9 @@ function useSpreadsheetGrid(options) {
1008
1054
  isFillHandleCell,
1009
1055
  startFillDrag,
1010
1056
  canUndo: stack.past.length > 0,
1011
- canRedo: stack.future.length > 0
1057
+ canRedo: stack.future.length > 0,
1058
+ selectionTsv: selectionAsTsv,
1059
+ selectionAddresses
1012
1060
  };
1013
1061
  }
1014
1062
  //# sourceMappingURL=spreadsheet.cjs.map