@ai-matrx/design-system 0.26.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 (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/data-table/copy-helpers.cjs.map +1 -1
  3. package/dist/data-table/copy-helpers.d.cts +2 -1
  4. package/dist/data-table/copy-helpers.d.ts +2 -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/filter-engine.cjs +88 -0
  17. package/dist/data-table/filter-engine.cjs.map +1 -1
  18. package/dist/data-table/filter-engine.d.cts +2 -1
  19. package/dist/data-table/filter-engine.d.ts +2 -1
  20. package/dist/data-table/filter-engine.js +88 -0
  21. package/dist/data-table/filter-engine.js.map +1 -1
  22. package/dist/data-table/host.d.cts +2 -1
  23. package/dist/data-table/host.d.ts +2 -1
  24. package/dist/data-table/index.cjs +238 -7
  25. package/dist/data-table/index.cjs.map +1 -1
  26. package/dist/data-table/index.d.cts +3 -2
  27. package/dist/data-table/index.d.ts +3 -2
  28. package/dist/data-table/index.js +238 -7
  29. package/dist/data-table/index.js.map +1 -1
  30. package/dist/data-table/infer-filter.cjs +2 -0
  31. package/dist/data-table/infer-filter.cjs.map +1 -1
  32. package/dist/data-table/infer-filter.d.cts +3 -2
  33. package/dist/data-table/infer-filter.d.ts +3 -2
  34. package/dist/data-table/infer-filter.js +2 -0
  35. package/dist/data-table/infer-filter.js.map +1 -1
  36. package/dist/data-table/layered-filters.d.cts +2 -1
  37. package/dist/data-table/layered-filters.d.ts +2 -1
  38. package/dist/data-table/query-control.cjs.map +1 -1
  39. package/dist/data-table/query-control.d.cts +2 -1
  40. package/dist/data-table/query-control.d.ts +2 -1
  41. package/dist/data-table/query-control.js.map +1 -1
  42. package/dist/data-table/types.cjs.map +1 -1
  43. package/dist/data-table/types.d.cts +2 -1
  44. package/dist/data-table/types.d.ts +2 -1
  45. package/dist/data-table/url-state.d.cts +2 -1
  46. package/dist/data-table/url-state.d.ts +2 -1
  47. package/dist/data-table/xlsx.cjs.map +1 -1
  48. package/dist/data-table/xlsx.d.cts +2 -1
  49. package/dist/data-table/xlsx.d.ts +2 -1
  50. package/dist/data-table/xlsx.js.map +1 -1
  51. package/dist/field-formats/index.cjs +15 -0
  52. package/dist/field-formats/index.cjs.map +1 -1
  53. package/dist/field-formats/index.js +15 -0
  54. package/dist/field-formats/index.js.map +1 -1
  55. package/dist/{types-Cu5_eUG1.d.ts → types-D9JBR6ZU.d.ts} +3 -2
  56. package/dist/{types-DrJXAAoD.d.cts → types-jQddSUnj.d.cts} +3 -2
  57. package/package.json +11 -1
@@ -132,6 +132,8 @@ function filterKindLabel(kind) {
132
132
  return "Boolean";
133
133
  case "number":
134
134
  return "Number";
135
+ case "date":
136
+ return "Date";
135
137
  case "auto":
136
138
  return "Auto";
137
139
  case false:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-table/infer-filter.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["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 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/data-table/infer-filter.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts"],"sourcesContent":["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 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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,IAAM,mBAAmE;AAAA,EACvE,UAAU;AAAA,EACV,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,WAAW;AACb;AAMO,SAAS,oBACd,QACkC;AAClC,MAAI,OAAO,WAAW,MAAO,QAAO;AACpC,MAAI,OAAO,OAAQ,QAAO,OAAO;AACjC,SAAO,OAAO,cAAc,iBAAiB,OAAO,WAAW,IAAI;AACrE;;;ACJO,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,IAAM,oBAAoB;AAQ1B,SAAS,aAAgB,KAAQ,KAAiC;AACvE,MAAI,IAAI,WAAY,QAAO,IAAI,WAAW,GAAG;AAC7C,MAAI,IAAI,aAAa;AACnB,WAAQ,IAAgC,IAAI,WAAW;AAAA,EACzD;AACA,SAAO;AACT;AAUO,SAAS,mBAAmB,OAAwB;AACzD,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AAC3D,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAgDO,SAAS,iBAAiB,OAA0B;AACzD,UAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAC1C,IAAI,kBAAkB,EACtB,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;;;AFnGA,IAAM,yBAAyB;AAQxB,SAAS,kBACd,KACA,YAC2B;AAC3B,MAAI,IAAI,WAAW,MAAO,QAAO;AACjC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,QAAO,IAAI;AACpD,MAAI,oBAAoB,GAAG,EAAG,QAAO;AAErC,QAAM,SAAS,WACZ,MAAM,GAAG,GAAG,EACZ,IAAI,CAAC,QAAQ,aAAa,KAAK,GAAG,CAAC,EACnC,OAAO,CAAC,MAAM,KAAK,IAAI;AAE1B,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,SAAS,EAAG,QAAO;AACxD,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,CAAC,GAAG;AACpE,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,OAAO,QAAQ,gBAAgB;AAC/C,QAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,MACE,OAAO,QAAQ,KACf,OAAO,QAAQ,0BACf,OAAO,OAAO,QAAQ,SAAS,KAC/B;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,qBACd,KACA,MACyC;AACzC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,UAAmD,CAAC;AAC1D,MAAI,WAAW;AACf,MAAI,qBAAqB;AACzB,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,aAAa,KAAK,GAAG;AAClC,UAAM,SAAS,iBAAiB,IAAI;AACpC,QAAI,OAAO,WAAW,GAAG;AACvB,iBAAW;AACX,UAAI,MAAM,QAAQ,IAAI,EAAG,sBAAqB;AAC9C;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,UAAI,IAAI,eAAe,UAAU,KAAK,IAAI,KAAK,EAAG;AAClD,WAAK,IAAI,KAAK;AACd,cAAQ,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IACtC;AAAA,EACF;AACA,QAAM,OAAO,IAAI,eAAe,SAC5B,IAAI,cAAc,IAAI,CAAC,YAAY;AAAA,IACjC,GAAG;AAAA,IACH,OAAO,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,UAAU,KAAK,mBAAmB,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ;AAAA,EAC5H,EAAE,IACF,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;AAGzD,MAAI,UAAU;AAKZ,UAAM,cAAc,CAAC,sBAAsB,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,EAAE,IAChF,EAAE,OAAO,mBAAmB,OAAO,SAAS,IAC5C,EAAE,OAAO,oBAAoB,OAAO,UAAU;AAClD,UAAM,YAAY,CAAC;AACnB,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,YAAY,KAAK,EAAG,WAAU,KAAK,WAAW;AAC1F,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,sBAAsB,GAAG;AACnE,gBAAU,KAAK,EAAE,OAAO,wBAAwB,OAAO,cAAc,CAAC;AAAA,IACxE;AACA,SAAK,QAAQ,GAAG,SAAS;AAAA,EAC3B;AACA,SAAO;AACT;AAEO,SAAS,gBACd,MACQ;AACR,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;","names":[]}
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-DrJXAAoD.cjs';
1
+ import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-jQddSUnj.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -10,8 +10,9 @@ import './fill.cjs';
10
10
  import './facets.cjs';
11
11
  import './spreadsheet.cjs';
12
12
  import './grid-selection.cjs';
13
+ import './date-filter.cjs';
13
14
 
14
- type ResolvedFilterKind = "text" | "select" | "boolean" | "number";
15
+ type ResolvedFilterKind = "text" | "select" | "boolean" | "number" | "date";
15
16
  /**
16
17
  * Infer a filter UI from sample row values. Prefer select for low-cardinality
17
18
  * string/enum columns, boolean for booleans, number for numeric, else text.
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-Cu5_eUG1.js';
1
+ import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-D9JBR6ZU.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -10,8 +10,9 @@ import './fill.js';
10
10
  import './facets.js';
11
11
  import './spreadsheet.js';
12
12
  import './grid-selection.js';
13
+ import './date-filter.js';
13
14
 
14
- type ResolvedFilterKind = "text" | "select" | "boolean" | "number";
15
+ type ResolvedFilterKind = "text" | "select" | "boolean" | "number" | "date";
15
16
  /**
16
17
  * Infer a filter UI from sample row values. Prefer select for low-cardinality
17
18
  * string/enum columns, boolean for booleans, number for numeric, else text.
@@ -105,6 +105,8 @@ function filterKindLabel(kind) {
105
105
  return "Boolean";
106
106
  case "number":
107
107
  return "Number";
108
+ case "date":
109
+ return "Date";
108
110
  case "auto":
109
111
  return "Auto";
110
112
  case false:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.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"],"mappings":";;AAIA,IAAM,mBAAmE;AAAA,EACvE,UAAU;AAAA,EACV,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,WAAW;AACb;AAMO,SAAS,oBACd,QACkC;AAClC,MAAI,OAAO,WAAW,MAAO,QAAO;AACpC,MAAI,OAAO,OAAQ,QAAO,OAAO;AACjC,SAAO,OAAO,cAAc,iBAAiB,OAAO,WAAW,IAAI;AACrE;;;ACLO,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,IAAM,oBAAoB;AAQ1B,SAAS,aAAgB,KAAQ,KAAiC;AACvE,MAAI,IAAI,WAAY,QAAO,IAAI,WAAW,GAAG;AAC7C,MAAI,IAAI,aAAa;AACnB,WAAQ,IAAgC,IAAI,WAAW;AAAA,EACzD;AACA,SAAO;AACT;AAUO,SAAS,mBAAmB,OAAwB;AACzD,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AAC3D,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAgDO,SAAS,iBAAiB,OAA0B;AACzD,UAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAC1C,IAAI,kBAAkB,EACtB,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;;;AClGA,IAAM,yBAAyB;AAQxB,SAAS,kBACd,KACA,YAC2B;AAC3B,MAAI,IAAI,WAAW,MAAO,QAAO;AACjC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,QAAO,IAAI;AACpD,MAAI,oBAAoB,GAAG,EAAG,QAAO;AAErC,QAAM,SAAS,WACZ,MAAM,GAAG,GAAG,EACZ,IAAI,CAAC,QAAQ,aAAa,KAAK,GAAG,CAAC,EACnC,OAAO,CAAC,MAAM,KAAK,IAAI;AAE1B,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,SAAS,EAAG,QAAO;AACxD,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,CAAC,GAAG;AACpE,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,OAAO,QAAQ,gBAAgB;AAC/C,QAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,MACE,OAAO,QAAQ,KACf,OAAO,QAAQ,0BACf,OAAO,OAAO,QAAQ,SAAS,KAC/B;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,qBACd,KACA,MACyC;AACzC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,UAAmD,CAAC;AAC1D,MAAI,WAAW;AACf,MAAI,qBAAqB;AACzB,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,aAAa,KAAK,GAAG;AAClC,UAAM,SAAS,iBAAiB,IAAI;AACpC,QAAI,OAAO,WAAW,GAAG;AACvB,iBAAW;AACX,UAAI,MAAM,QAAQ,IAAI,EAAG,sBAAqB;AAC9C;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,UAAI,IAAI,eAAe,UAAU,KAAK,IAAI,KAAK,EAAG;AAClD,WAAK,IAAI,KAAK;AACd,cAAQ,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IACtC;AAAA,EACF;AACA,QAAM,OAAO,IAAI,eAAe,SAC5B,IAAI,cAAc,IAAI,CAAC,YAAY;AAAA,IACjC,GAAG;AAAA,IACH,OAAO,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,UAAU,KAAK,mBAAmB,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ;AAAA,EAC5H,EAAE,IACF,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;AAGzD,MAAI,UAAU;AAKZ,UAAM,cAAc,CAAC,sBAAsB,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,EAAE,IAChF,EAAE,OAAO,mBAAmB,OAAO,SAAS,IAC5C,EAAE,OAAO,oBAAoB,OAAO,UAAU;AAClD,UAAM,YAAY,CAAC;AACnB,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,YAAY,KAAK,EAAG,WAAU,KAAK,WAAW;AAC1F,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,sBAAsB,GAAG;AACnE,gBAAU,KAAK,EAAE,OAAO,wBAAwB,OAAO,cAAc,CAAC;AAAA,IACxE;AACA,SAAK,QAAQ,GAAG,SAAS;AAAA,EAC3B;AACA,SAAO;AACT;AAEO,SAAS,gBACd,MACQ;AACR,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,EACX;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.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"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- export { aq as LAYERED_FILTER_OPERATORS, ar as LAYERED_FILTER_OPERATOR_LABELS, as as LayeredFilterField, at as LayeredFilterOperator, L as LayeredFilterRule, au as columnFiltersToLayeredRules, av as completeLayeredFilterRules, aw as decodeLayeredFilterRules, ax as encodeLayeredFilterRules, ay as isCompleteLayeredFilterRule, az as isLayeredFilterOperator, aA as layeredFilterMatchesValue, aB as layeredFilterNeedsValue, aC as layeredFilterRuleSummary, aD as operatorsForLayeredField } from '../types-DrJXAAoD.cjs';
1
+ export { aq as LAYERED_FILTER_OPERATORS, ar as LAYERED_FILTER_OPERATOR_LABELS, as as LayeredFilterField, at as LayeredFilterOperator, L as LayeredFilterRule, au as columnFiltersToLayeredRules, av as completeLayeredFilterRules, aw as decodeLayeredFilterRules, ax as encodeLayeredFilterRules, ay as isCompleteLayeredFilterRule, az as isLayeredFilterOperator, aA as layeredFilterMatchesValue, aB as layeredFilterNeedsValue, aC as layeredFilterRuleSummary, aD as operatorsForLayeredField } from '../types-jQddSUnj.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -10,3 +10,4 @@ import './fill.cjs';
10
10
  import './facets.cjs';
11
11
  import './spreadsheet.cjs';
12
12
  import './grid-selection.cjs';
13
+ import './date-filter.cjs';
@@ -1,4 +1,4 @@
1
- export { aq as LAYERED_FILTER_OPERATORS, ar as LAYERED_FILTER_OPERATOR_LABELS, as as LayeredFilterField, at as LayeredFilterOperator, L as LayeredFilterRule, au as columnFiltersToLayeredRules, av as completeLayeredFilterRules, aw as decodeLayeredFilterRules, ax as encodeLayeredFilterRules, ay as isCompleteLayeredFilterRule, az as isLayeredFilterOperator, aA as layeredFilterMatchesValue, aB as layeredFilterNeedsValue, aC as layeredFilterRuleSummary, aD as operatorsForLayeredField } from '../types-Cu5_eUG1.js';
1
+ export { aq as LAYERED_FILTER_OPERATORS, ar as LAYERED_FILTER_OPERATOR_LABELS, as as LayeredFilterField, at as LayeredFilterOperator, L as LayeredFilterRule, au as columnFiltersToLayeredRules, av as completeLayeredFilterRules, aw as decodeLayeredFilterRules, ax as encodeLayeredFilterRules, ay as isCompleteLayeredFilterRule, az as isLayeredFilterOperator, aA as layeredFilterMatchesValue, aB as layeredFilterNeedsValue, aC as layeredFilterRuleSummary, aD as operatorsForLayeredField } from '../types-D9JBR6ZU.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -10,3 +10,4 @@ import './fill.js';
10
10
  import './facets.js';
11
11
  import './spreadsheet.js';
12
12
  import './grid-selection.js';
13
+ import './date-filter.js';
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-table/query-control.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts"],"sourcesContent":["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","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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIA,IAAM,mBAAmE;AAAA,EACvE,UAAU;AAAA,EACV,aAAa;AAAA,EACb,WAAW;AAAA,EACX,cAAc;AAAA,EACd,QAAQ;AAAA,EACR,WAAW;AACb;AAMO,SAAS,oBACd,QACkC;AAClC,MAAI,OAAO,WAAW,MAAO,QAAO;AACpC,MAAI,OAAO,OAAQ,QAAO,OAAO;AACjC,SAAO,OAAO,cAAc,iBAAiB,OAAO,WAAW,IAAI;AACrE;;;ACLO,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,IAAM,oBAAoB;AAQ1B,SAAS,aAAgB,KAAQ,KAAiC;AACvE,MAAI,IAAI,WAAY,QAAO,IAAI,WAAW,GAAG;AAC7C,MAAI,IAAI,aAAa;AACnB,WAAQ,IAAgC,IAAI,WAAW;AAAA,EACzD;AACA,SAAO;AACT;AAUO,SAAS,mBAAmB,OAAwB;AACzD,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AAC3D,WAAO,OAAO,KAAK;AAAA,EACrB;AACA,MAAI,iBAAiB,KAAM,QAAO,MAAM,YAAY;AACpD,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAgDO,SAAS,iBAAiB,OAA0B;AACzD,UAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAC1C,IAAI,kBAAkB,EACtB,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;;;AClGA,IAAM,yBAAyB;AAQxB,SAAS,kBACd,KACA,YAC2B;AAC3B,MAAI,IAAI,WAAW,MAAO,QAAO;AACjC,MAAI,IAAI,UAAU,IAAI,WAAW,OAAQ,QAAO,IAAI;AACpD,MAAI,oBAAoB,GAAG,EAAG,QAAO;AAErC,QAAM,SAAS,WACZ,MAAM,GAAG,GAAG,EACZ,IAAI,CAAC,QAAQ,aAAa,KAAK,GAAG,CAAC,EACnC,OAAO,CAAC,MAAM,KAAK,IAAI;AAE1B,MAAI,OAAO,WAAW,EAAG,QAAO;AAEhC,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,SAAS,EAAG,QAAO;AACxD,MAAI,OAAO,MAAM,CAAC,MAAM,OAAO,MAAM,YAAY,OAAO,SAAS,CAAC,CAAC,GAAG;AACpE,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,OAAO,QAAQ,gBAAgB;AAC/C,QAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,MACE,OAAO,QAAQ,KACf,OAAO,QAAQ,0BACf,OAAO,OAAO,QAAQ,SAAS,KAC/B;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAEO,SAAS,qBACd,KACA,MACyC;AACzC,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,UAAmD,CAAC;AAC1D,MAAI,WAAW;AACf,MAAI,qBAAqB;AACzB,aAAW,OAAO,MAAM;AACtB,UAAM,OAAO,aAAa,KAAK,GAAG;AAClC,UAAM,SAAS,iBAAiB,IAAI;AACpC,QAAI,OAAO,WAAW,GAAG;AACvB,iBAAW;AACX,UAAI,MAAM,QAAQ,IAAI,EAAG,sBAAqB;AAC9C;AAAA,IACF;AACA,eAAW,SAAS,QAAQ;AAC1B,UAAI,IAAI,eAAe,UAAU,KAAK,IAAI,KAAK,EAAG;AAClD,WAAK,IAAI,KAAK;AACd,cAAQ,KAAK,EAAE,OAAO,OAAO,MAAM,CAAC;AAAA,IACtC;AAAA,EACF;AACA,QAAM,OAAO,IAAI,eAAe,SAC5B,IAAI,cAAc,IAAI,CAAC,YAAY;AAAA,IACjC,GAAG;AAAA,IACH,OAAO,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ,OAAO,UAAU,KAAK,mBAAmB,OAAO,MAAM,KAAK,IAAI,OAAO,QAAQ;AAAA,EAC5H,EAAE,IACF,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC;AAGzD,MAAI,UAAU;AAKZ,UAAM,cAAc,CAAC,sBAAsB,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,EAAE,IAChF,EAAE,OAAO,mBAAmB,OAAO,SAAS,IAC5C,EAAE,OAAO,oBAAoB,OAAO,UAAU;AAClD,UAAM,YAAY,CAAC;AACnB,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,YAAY,KAAK,EAAG,WAAU,KAAK,WAAW;AAC1F,QAAI,CAAC,KAAK,KAAK,CAAC,WAAW,OAAO,UAAU,sBAAsB,GAAG;AACnE,gBAAU,KAAK,EAAE,OAAO,wBAAwB,OAAO,cAAc,CAAC;AAAA,IACxE;AACA,SAAK,QAAQ,GAAG,SAAS;AAAA,EAC3B;AACA,SAAO;AACT;;;AHhFO,SAAS,uBACd,QACA,MACA,YACiB;AACjB,MAAI,CAAC,YAAY;AACf,UAAMA,QAAO,kBAAkB,QAAQ,IAAI;AAC3C,WAAO;AAAA,MACL,MAAAA;AAAA,MACA,SAASA,UAAS,WAAW,qBAAqB,QAAQ,IAAI,IAAI,CAAC;AAAA,IACrE;AAAA,EACF;AACA,MAAI,OAAO,WAAW,MAAO,QAAO,EAAE,MAAM,MAAM,SAAS,CAAC,EAAE;AAC9D,MAAI,oBAAoB,MAAM,MAAM,OAAO,WAAW,UAAa,OAAO,WAAW,SAAS;AAC5F,WAAO,EAAE,MAAM,WAAW,SAAS,CAAC,EAAE;AAAA,EACxC;AACA,QAAM,WAAW,OAAO;AACxB,QAAM,OACJ,YAAY,aAAa,SACrB,WACA,OAAO,eAAe,SACpB,WACA;AACR,SAAO;AAAA,IACL;AAAA,IACA,SAAS,SAAS,WAAY,OAAO,iBAAiB,CAAC,IAAK,CAAC;AAAA,EAC/D;AACF;AAYO,SAAS,eACd,SACA,OACA,UAAmC,CAAC,GACV;AAC1B,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA,IACH,MAAM,QAAQ,YAAY,IAAK,MAAM,QAAQ,QAAQ;AAAA,EACvD;AACF;AAGO,SAAS,cACd,eACA,YACA,UACQ;AACR,QAAM,YAAY,KAAK,IAAI,GAAG,UAAU;AACxC,QAAM,WAAW,KAAK,IAAI,GAAG,QAAQ;AACrC,QAAM,YAAY,KAAK,IAAI,GAAG,KAAK,KAAK,YAAY,QAAQ,CAAC;AAC7D,SAAO,KAAK,IAAI,KAAK,IAAI,GAAG,aAAa,GAAG,SAAS;AACvD;","names":["kind"]}
1
+ {"version":3,"sources":["../../src/data-table/query-control.ts","../../src/data-table/column-marker.ts","../../src/data-table/filter-engine.ts","../../src/data-table/infer-filter.ts"],"sourcesContent":["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","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"],"mappings":"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","names":["kind"]}
@@ -1,5 +1,5 @@
1
1
  import { ResolvedFilterKind } from './infer-filter.cjs';
2
- import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../types-DrJXAAoD.cjs';
2
+ import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../types-jQddSUnj.cjs';
3
3
  import '../content-transfer.cjs';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
@@ -11,6 +11,7 @@ import './fill.cjs';
11
11
  import './facets.cjs';
12
12
  import './spreadsheet.cjs';
13
13
  import './grid-selection.cjs';
14
+ import './date-filter.cjs';
14
15
 
15
16
  interface QueryFilterMeta {
16
17
  kind: ResolvedFilterKind | null;
@@ -1,5 +1,5 @@
1
1
  import { ResolvedFilterKind } from './infer-filter.js';
2
- import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../types-Cu5_eUG1.js';
2
+ import { e as MatrxDataTableQueryState, M as MatrxColumnDef } from '../types-D9JBR6ZU.js';
3
3
  import '../content-transfer.js';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
@@ -11,6 +11,7 @@ import './fill.js';
11
11
  import './facets.js';
12
12
  import './spreadsheet.js';
13
13
  import './grid-selection.js';
14
+ import './date-filter.js';
14
15
 
15
16
  interface QueryFilterMeta {
16
17
  kind: ResolvedFilterKind | null;
@@ -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"]}