@ai-matrx/design-system 0.21.8 → 0.21.10

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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/data-table/copy-helpers.cjs.map +1 -1
  3. package/dist/data-table/copy-helpers.d.cts +1 -1
  4. package/dist/data-table/copy-helpers.d.ts +1 -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/filter-engine.cjs +8 -3
  11. package/dist/data-table/filter-engine.cjs.map +1 -1
  12. package/dist/data-table/filter-engine.d.cts +4 -2
  13. package/dist/data-table/filter-engine.d.ts +4 -2
  14. package/dist/data-table/filter-engine.js +8 -3
  15. package/dist/data-table/filter-engine.js.map +1 -1
  16. package/dist/data-table/host.d.cts +1 -1
  17. package/dist/data-table/host.d.ts +1 -1
  18. package/dist/data-table/index.cjs +107 -32
  19. package/dist/data-table/index.cjs.map +1 -1
  20. package/dist/data-table/index.d.cts +2 -2
  21. package/dist/data-table/index.d.ts +2 -2
  22. package/dist/data-table/index.js +107 -32
  23. package/dist/data-table/index.js.map +1 -1
  24. package/dist/data-table/infer-filter.cjs +16 -6
  25. package/dist/data-table/infer-filter.cjs.map +1 -1
  26. package/dist/data-table/infer-filter.d.cts +1 -1
  27. package/dist/data-table/infer-filter.d.ts +1 -1
  28. package/dist/data-table/infer-filter.js +16 -6
  29. package/dist/data-table/infer-filter.js.map +1 -1
  30. package/dist/data-table/layered-filters.d.cts +1 -1
  31. package/dist/data-table/layered-filters.d.ts +1 -1
  32. package/dist/data-table/pagination.cjs +28 -14
  33. package/dist/data-table/pagination.cjs.map +1 -1
  34. package/dist/data-table/pagination.d.cts +7 -1
  35. package/dist/data-table/pagination.d.ts +7 -1
  36. package/dist/data-table/pagination.js +28 -14
  37. package/dist/data-table/pagination.js.map +1 -1
  38. package/dist/data-table/query-control.cjs +16 -6
  39. package/dist/data-table/query-control.cjs.map +1 -1
  40. package/dist/data-table/query-control.d.cts +1 -1
  41. package/dist/data-table/query-control.d.ts +1 -1
  42. package/dist/data-table/query-control.js +16 -6
  43. package/dist/data-table/query-control.js.map +1 -1
  44. package/dist/data-table/types.cjs.map +1 -1
  45. package/dist/data-table/types.d.cts +1 -1
  46. package/dist/data-table/types.d.ts +1 -1
  47. package/dist/data-table/url-state.d.cts +1 -1
  48. package/dist/data-table/url-state.d.ts +1 -1
  49. package/dist/data-table/xlsx.cjs.map +1 -1
  50. package/dist/data-table/xlsx.d.cts +1 -1
  51. package/dist/data-table/xlsx.d.ts +1 -1
  52. package/dist/data-table/xlsx.js.map +1 -1
  53. package/dist/{types-DsmrqJHN.d.cts → types-BAJiOaQL.d.cts} +8 -1
  54. package/dist/{types-Dpl8OW_m.d.ts → types-Da781-Wq.d.ts} +8 -1
  55. package/package.json +1 -1
@@ -44,6 +44,7 @@ function resolveColumnMarker(column) {
44
44
  // src/data-table/filter-engine.ts
45
45
  var SELECT_EMPTY_VALUE = "__empty__";
46
46
  var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
47
+ var SELECT_NULL_VALUE = "__null__";
47
48
  function getCellValue(row, col) {
48
49
  if (col.accessorFn) return col.accessorFn(row);
49
50
  if (col.accessorKey) {
@@ -91,10 +92,13 @@ function collectSelectOptions(col, rows) {
91
92
  const seen = /* @__PURE__ */ new Set();
92
93
  const options = [];
93
94
  let hasEmpty = false;
95
+ let hasEmptyCollection = false;
94
96
  for (const row of rows) {
95
- const values = selectCellValues(getCellValue(row, col));
97
+ const cell = getCellValue(row, col);
98
+ const values = selectCellValues(cell);
96
99
  if (values.length === 0) {
97
100
  hasEmpty = true;
101
+ if (Array.isArray(cell)) hasEmptyCollection = true;
98
102
  continue;
99
103
  }
100
104
  for (const value of values) {
@@ -103,12 +107,18 @@ function collectSelectOptions(col, rows) {
103
107
  options.push({ value, label: value });
104
108
  }
105
109
  }
106
- const base = col.filterOptions?.length ? [...col.filterOptions] : options.sort((a, b) => a.label.localeCompare(b.label));
110
+ const base = col.filterOptions?.length ? col.filterOptions.map((option) => ({
111
+ ...option,
112
+ label: option.label.trim() ? option.label : option.value === "" ? "(empty string)" : option.value.trim() ? option.value : "(whitespace)"
113
+ })) : options.sort((a, b) => a.label.localeCompare(b.label));
107
114
  if (hasEmpty) {
108
- base.unshift(
109
- { value: SELECT_EMPTY_VALUE, label: "(empty)" },
110
- { value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" }
111
- );
115
+ const emptyOption = !hasEmptyCollection && base.some((option) => option.value === "") ? { value: SELECT_NULL_VALUE, label: "(null)" } : { value: SELECT_EMPTY_VALUE, label: "(empty)" };
116
+ const additions = [];
117
+ if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);
118
+ if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {
119
+ additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" });
120
+ }
121
+ base.unshift(...additions);
112
122
  }
113
123
  return base;
114
124
  }
@@ -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 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 for (const row of rows) {\n const values = selectCellValues(getCellValue(row, col));\n if (values.length === 0) {\n hasEmpty = 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]\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 base.unshift(\n { value: SELECT_EMPTY_VALUE, label: \"(empty)\" },\n { value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" },\n );\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\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 const selected =\n 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_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(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 ? f.values.length > 0 : 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\n ? filter.values.length > 0\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\";\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 const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(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":";;;;;;;;;;;;;;;;;;;;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;;;AFlGA,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,4 +1,4 @@
1
- import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-DsmrqJHN.cjs';
1
+ import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-BAJiOaQL.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-Dpl8OW_m.js';
1
+ import { M as MatrxColumnDef, f as ColumnFilterKind } from '../types-Da781-Wq.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -17,6 +17,7 @@ function resolveColumnMarker(column) {
17
17
  // src/data-table/filter-engine.ts
18
18
  var SELECT_EMPTY_VALUE = "__empty__";
19
19
  var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
20
+ var SELECT_NULL_VALUE = "__null__";
20
21
  function getCellValue(row, col) {
21
22
  if (col.accessorFn) return col.accessorFn(row);
22
23
  if (col.accessorKey) {
@@ -64,10 +65,13 @@ function collectSelectOptions(col, rows) {
64
65
  const seen = /* @__PURE__ */ new Set();
65
66
  const options = [];
66
67
  let hasEmpty = false;
68
+ let hasEmptyCollection = false;
67
69
  for (const row of rows) {
68
- const values = selectCellValues(getCellValue(row, col));
70
+ const cell = getCellValue(row, col);
71
+ const values = selectCellValues(cell);
69
72
  if (values.length === 0) {
70
73
  hasEmpty = true;
74
+ if (Array.isArray(cell)) hasEmptyCollection = true;
71
75
  continue;
72
76
  }
73
77
  for (const value of values) {
@@ -76,12 +80,18 @@ function collectSelectOptions(col, rows) {
76
80
  options.push({ value, label: value });
77
81
  }
78
82
  }
79
- const base = col.filterOptions?.length ? [...col.filterOptions] : options.sort((a, b) => a.label.localeCompare(b.label));
83
+ const base = col.filterOptions?.length ? col.filterOptions.map((option) => ({
84
+ ...option,
85
+ label: option.label.trim() ? option.label : option.value === "" ? "(empty string)" : option.value.trim() ? option.value : "(whitespace)"
86
+ })) : options.sort((a, b) => a.label.localeCompare(b.label));
80
87
  if (hasEmpty) {
81
- base.unshift(
82
- { value: SELECT_EMPTY_VALUE, label: "(empty)" },
83
- { value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" }
84
- );
88
+ const emptyOption = !hasEmptyCollection && base.some((option) => option.value === "") ? { value: SELECT_NULL_VALUE, label: "(null)" } : { value: SELECT_EMPTY_VALUE, label: "(empty)" };
89
+ const additions = [];
90
+ if (!base.some((option) => option.value === emptyOption.value)) additions.push(emptyOption);
91
+ if (!base.some((option) => option.value === SELECT_NOT_EMPTY_VALUE)) {
92
+ additions.push({ value: SELECT_NOT_EMPTY_VALUE, label: "(not empty)" });
93
+ }
94
+ base.unshift(...additions);
85
95
  }
86
96
  return base;
87
97
  }
@@ -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\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 const selected =\n 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_EMPTY_VALUE\n ? values.length === 0\n : v === SELECT_NOT_EMPTY_VALUE\n ? values.length > 0\n : values.includes(v),\n );\n }\n case \"boolean\": {\n return Boolean(value) === filter.value;\n }\n case \"number\": {\n const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(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 ? f.values.length > 0 : 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\n ? filter.values.length > 0\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 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 for (const row of rows) {\n const values = selectCellValues(getCellValue(row, col));\n if (values.length === 0) {\n hasEmpty = 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]\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 base.unshift(\n { value: SELECT_EMPTY_VALUE, label: \"(empty)\" },\n { value: SELECT_NOT_EMPTY_VALUE, label: \"(not empty)\" },\n );\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":"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","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\";\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 const n =\n typeof value === \"number\"\n ? value\n : Number(stringifyCellValue(value).replace(/[^0-9.-]/g, \"\"));\n if (Number.isNaN(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":"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","names":[]}
@@ -1,4 +1,4 @@
1
- export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-DsmrqJHN.cjs';
1
+ export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-BAJiOaQL.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -1,4 +1,4 @@
1
- export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-Dpl8OW_m.js';
1
+ export { H as LAYERED_FILTER_OPERATORS, I as LAYERED_FILTER_OPERATOR_LABELS, J as LayeredFilterField, K as LayeredFilterOperator, L as LayeredFilterRule, N as columnFiltersToLayeredRules, O as completeLayeredFilterRules, P as decodeLayeredFilterRules, Q as encodeLayeredFilterRules, R as isCompleteLayeredFilterRule, U as isLayeredFilterOperator, V as layeredFilterMatchesValue, W as layeredFilterNeedsValue, X as layeredFilterRuleSummary, Y as operatorsForLayeredField } from '../types-Da781-Wq.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -451,6 +451,7 @@ function PageNavigationControl({
451
451
  }
452
452
  function PaginationAction({
453
453
  HostAction,
454
+ compact = false,
454
455
  active = false,
455
456
  disabledHint,
456
457
  visibilityClassName = "inline-flex",
@@ -479,6 +480,7 @@ function PaginationAction({
479
480
  onClick: props.onClick,
480
481
  className: cn(
481
482
  PAGINATION_ACTION_CLASS,
483
+ compact && "h-8 w-8 min-h-8 min-w-8",
482
484
  active && PAGINATION_ACTIVE_CLASS,
483
485
  fallbackClassName
484
486
  ),
@@ -492,6 +494,8 @@ var PAGINATION_ACTION_CLASS = "h-11 w-11 min-h-11 min-w-11 rounded-md border bor
492
494
  var PAGINATION_ACTIVE_CLASS = "border-primary bg-primary text-primary-foreground hover:bg-primary/90";
493
495
  function GenericTablePagination({
494
496
  totalItems,
497
+ unknownTotal,
498
+ isFetching = false,
495
499
  itemsPerPage,
496
500
  currentPage,
497
501
  onPageChange,
@@ -532,11 +536,12 @@ function GenericTablePagination({
532
536
  (_, i) => startPage + i
533
537
  );
534
538
  };
535
- const isFirstPageDisabled = currentPage <= 1;
536
- const isLastPageDisabled = currentPage >= totalPages;
539
+ const isFirstPageDisabled = isFetching || currentPage <= 1;
540
+ const isLastPageDisabled = isFetching || (unknownTotal ? !unknownTotal.hasNextPage : currentPage >= totalPages);
537
541
  const startItem = totalItems === 0 ? 0 : progressive ? 1 : (currentPage - 1) * effectiveItemsPerPage + 1;
538
542
  const endItem = Math.min(progressive?.shownCount ?? currentPage * effectiveItemsPerPage, totalItems);
539
543
  const defaultLabelFormat = (start, end, total) => {
544
+ if (unknownTotal) return `${unknownTotal.pageRowCount.toLocaleString()} on page ${currentPage.toLocaleString()}`;
540
545
  if (progressive) return progressive.loadedCount !== void 0 && progressive.loadedCount !== total ? `${end} shown / ${total} matching \xB7 ${progressive.loadedCount} loaded` : `${end} shown / ${total} loaded`;
541
546
  return compact ? `${start}-${end} of ${total}` : `Showing ${start} to ${end} of ${total} entries`;
542
547
  };
@@ -561,6 +566,7 @@ function GenericTablePagination({
561
566
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
562
567
  Select,
563
568
  {
569
+ disabled: isFetching,
564
570
  value: itemsPerPage.toString(),
565
571
  onValueChange: (value) => onItemsPerPageChange(parseInt(value)),
566
572
  children: [
@@ -577,7 +583,7 @@ function GenericTablePagination({
577
583
  ),
578
584
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(SelectContent, { children: [
579
585
  availablePageSizes.map((size) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: size.toString(), children: size }, size)),
580
- showAllOption ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: allValue.toString(), children: allOptionLabel }) : null
586
+ showAllOption && !unknownTotal ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SelectItem, { value: allValue.toString(), children: allOptionLabel }) : null
581
587
  ] })
582
588
  ]
583
589
  }
@@ -589,7 +595,7 @@ function GenericTablePagination({
589
595
  {
590
596
  className: cn("flex min-w-0 justify-center", infoContainerClassName),
591
597
  children: showPageInfo && !hideEntriesInfo && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
592
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate text-xs tabular-nums text-muted-foreground sm:hidden", children: progressive ? `${endItem}/${totalItems} ${progressive.loadedCount !== void 0 && progressive.loadedCount !== totalItems ? "matching" : "loaded"}` : `${startItem}-${endItem} / ${totalItems}` }),
598
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "truncate text-xs tabular-nums text-muted-foreground sm:hidden", children: labelFormat ? formatLabel(startItem, endItem, totalItems) : unknownTotal ? defaultLabelFormat(startItem, endItem, totalItems) : progressive ? `${endItem}/${totalItems} ${progressive.loadedCount !== void 0 && progressive.loadedCount !== totalItems ? "matching" : "loaded"}` : `${startItem}-${endItem} / ${totalItems}` }),
593
599
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
594
600
  "span",
595
601
  {
@@ -614,7 +620,7 @@ function GenericTablePagination({
614
620
  }
615
621
  ) }),
616
622
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(TooltipContent, { children: "Show more already-loaded rows" })
617
- ] }) : null : showPageControls && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
623
+ ] }) : null : showPageControls && (unknownTotal ? currentPage > 1 || unknownTotal.hasNextPage : totalPages > 1) && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
618
624
  "div",
619
625
  {
620
626
  className: cn(
@@ -627,8 +633,9 @@ function GenericTablePagination({
627
633
  PaginationAction,
628
634
  {
629
635
  HostAction: host.ToolbarAction,
636
+ compact,
630
637
  ariaLabel: "First page",
631
- disabledHint: "Already on the first page",
638
+ disabledHint: isFetching ? "Loading source page" : "Already on the first page",
632
639
  visibilityClassName: "hidden sm:inline-flex",
633
640
  fallbackClassName: navButtonClassName,
634
641
  onClick: () => onPageChange(1),
@@ -640,41 +647,46 @@ function GenericTablePagination({
640
647
  PaginationAction,
641
648
  {
642
649
  HostAction: host.ToolbarAction,
650
+ compact,
643
651
  ariaLabel: "Previous page",
644
- disabledHint: "Already on the first page",
652
+ disabledHint: isFetching ? "Loading source page" : "Already on the first page",
645
653
  fallbackClassName: navButtonClassName,
646
654
  onClick: () => onPageChange(currentPage - 1),
647
655
  disabled: isFirstPageDisabled,
648
656
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChevronLeft, { className: "h-5 w-5" })
649
657
  }
650
658
  ),
651
- getPageNumbers(3).map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
659
+ !unknownTotal && getPageNumbers(3).map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
652
660
  PaginationAction,
653
661
  {
654
662
  HostAction: host.ToolbarAction,
663
+ compact,
655
664
  ariaLabel: `Page ${page}`,
656
665
  active: currentPage === page,
657
- disabledHint: `Already on page ${page}`,
666
+ disabledHint: isFetching ? "Loading source page" : `Already on page ${page}`,
658
667
  visibilityClassName: cn(
659
668
  "sm:hidden",
660
669
  page === currentPage ? "inline-flex" : "hidden min-[360px]:inline-flex"
661
670
  ),
662
671
  fallbackClassName: page === currentPage ? pageActiveButtonClassName : pageButtonClassName,
663
672
  onClick: () => onPageChange(page),
673
+ disabled: isFetching,
664
674
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: page })
665
675
  },
666
676
  `mobile-${page}`
667
677
  )),
668
- getPageNumbers().map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
678
+ !unknownTotal && getPageNumbers().map((page) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
669
679
  PaginationAction,
670
680
  {
671
681
  HostAction: host.ToolbarAction,
682
+ compact,
672
683
  ariaLabel: `Page ${page}`,
673
684
  active: currentPage === page,
674
- disabledHint: `Already on page ${page}`,
685
+ disabledHint: isFetching ? "Loading source page" : `Already on page ${page}`,
675
686
  visibilityClassName: "hidden sm:inline-flex",
676
687
  fallbackClassName: currentPage === page ? pageActiveButtonClassName : pageButtonClassName,
677
688
  onClick: () => onPageChange(page),
689
+ disabled: isFetching,
678
690
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { children: page })
679
691
  },
680
692
  `desktop-${page}`
@@ -683,20 +695,22 @@ function GenericTablePagination({
683
695
  PaginationAction,
684
696
  {
685
697
  HostAction: host.ToolbarAction,
698
+ compact,
686
699
  ariaLabel: "Next page",
687
- disabledHint: "Already on the last page",
700
+ disabledHint: isFetching ? "Loading source page" : "Already on the last page",
688
701
  fallbackClassName: navButtonClassName,
689
702
  onClick: () => onPageChange(currentPage + 1),
690
703
  disabled: isLastPageDisabled,
691
704
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChevronRight, { className: "h-5 w-5" })
692
705
  }
693
706
  ),
694
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
707
+ !unknownTotal && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
695
708
  PaginationAction,
696
709
  {
697
710
  HostAction: host.ToolbarAction,
711
+ compact,
698
712
  ariaLabel: "Last page",
699
- disabledHint: "Already on the last page",
713
+ disabledHint: isFetching ? "Loading source page" : "Already on the last page",
700
714
  visibilityClassName: "hidden sm:inline-flex",
701
715
  fallbackClassName: navButtonClassName,
702
716
  onClick: () => onPageChange(totalPages),