@ai-matrx/design-system 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/dist/data-table/copy-helpers.cjs.map +1 -1
  3. package/dist/data-table/copy-helpers.d.cts +3 -1
  4. package/dist/data-table/copy-helpers.d.ts +3 -1
  5. package/dist/data-table/copy-helpers.js.map +1 -1
  6. package/dist/data-table/data-row-inspector.cjs.map +1 -1
  7. package/dist/data-table/data-row-inspector.js.map +1 -1
  8. package/dist/data-table/data-row-window.cjs.map +1 -1
  9. package/dist/data-table/data-row-window.js.map +1 -1
  10. package/dist/data-table/date-filter.cjs +145 -0
  11. package/dist/data-table/date-filter.cjs.map +1 -0
  12. package/dist/data-table/date-filter.d.cts +31 -0
  13. package/dist/data-table/date-filter.d.ts +31 -0
  14. package/dist/data-table/date-filter.js +125 -0
  15. package/dist/data-table/date-filter.js.map +1 -0
  16. package/dist/data-table/facets.cjs +118 -0
  17. package/dist/data-table/facets.cjs.map +1 -0
  18. package/dist/data-table/facets.d.cts +100 -0
  19. package/dist/data-table/facets.d.ts +100 -0
  20. package/dist/data-table/facets.js +98 -0
  21. package/dist/data-table/facets.js.map +1 -0
  22. package/dist/data-table/filter-engine.cjs +88 -0
  23. package/dist/data-table/filter-engine.cjs.map +1 -1
  24. package/dist/data-table/filter-engine.d.cts +3 -1
  25. package/dist/data-table/filter-engine.d.ts +3 -1
  26. package/dist/data-table/filter-engine.js +88 -0
  27. package/dist/data-table/filter-engine.js.map +1 -1
  28. package/dist/data-table/host.cjs.map +1 -1
  29. package/dist/data-table/host.d.cts +14 -3
  30. package/dist/data-table/host.d.ts +14 -3
  31. package/dist/data-table/host.js.map +1 -1
  32. package/dist/data-table/index.cjs +918 -152
  33. package/dist/data-table/index.cjs.map +1 -1
  34. package/dist/data-table/index.d.cts +5 -2
  35. package/dist/data-table/index.d.ts +5 -2
  36. package/dist/data-table/index.js +956 -189
  37. package/dist/data-table/index.js.map +1 -1
  38. package/dist/data-table/infer-filter.cjs +2 -0
  39. package/dist/data-table/infer-filter.cjs.map +1 -1
  40. package/dist/data-table/infer-filter.d.cts +4 -2
  41. package/dist/data-table/infer-filter.d.ts +4 -2
  42. package/dist/data-table/infer-filter.js +2 -0
  43. package/dist/data-table/infer-filter.js.map +1 -1
  44. package/dist/data-table/layered-filters.d.cts +3 -1
  45. package/dist/data-table/layered-filters.d.ts +3 -1
  46. package/dist/data-table/menu-targets.cjs +77 -0
  47. package/dist/data-table/menu-targets.cjs.map +1 -0
  48. package/dist/data-table/menu-targets.d.cts +108 -0
  49. package/dist/data-table/menu-targets.d.ts +108 -0
  50. package/dist/data-table/menu-targets.js +57 -0
  51. package/dist/data-table/menu-targets.js.map +1 -0
  52. package/dist/data-table/pagination.cjs.map +1 -1
  53. package/dist/data-table/pagination.js.map +1 -1
  54. package/dist/data-table/query-control.cjs.map +1 -1
  55. package/dist/data-table/query-control.d.cts +3 -1
  56. package/dist/data-table/query-control.d.ts +3 -1
  57. package/dist/data-table/query-control.js.map +1 -1
  58. package/dist/data-table/spreadsheet.cjs +61 -13
  59. package/dist/data-table/spreadsheet.cjs.map +1 -1
  60. package/dist/data-table/spreadsheet.d.cts +36 -1
  61. package/dist/data-table/spreadsheet.d.ts +36 -1
  62. package/dist/data-table/spreadsheet.js +61 -13
  63. package/dist/data-table/spreadsheet.js.map +1 -1
  64. package/dist/data-table/types.cjs.map +1 -1
  65. package/dist/data-table/types.d.cts +3 -1
  66. package/dist/data-table/types.d.ts +3 -1
  67. package/dist/data-table/url-state.d.cts +3 -1
  68. package/dist/data-table/url-state.d.ts +3 -1
  69. package/dist/data-table/uuid-cell.cjs.map +1 -1
  70. package/dist/data-table/uuid-cell.js.map +1 -1
  71. package/dist/data-table/xlsx.cjs.map +1 -1
  72. package/dist/data-table/xlsx.d.cts +3 -1
  73. package/dist/data-table/xlsx.d.ts +3 -1
  74. package/dist/data-table/xlsx.js.map +1 -1
  75. package/dist/field-formats/index.cjs +15 -0
  76. package/dist/field-formats/index.cjs.map +1 -1
  77. package/dist/field-formats/index.js +15 -0
  78. package/dist/field-formats/index.js.map +1 -1
  79. package/dist/{types-nlLCMiqd.d.ts → types-D9JBR6ZU.d.ts} +118 -5
  80. package/dist/{types-CmQrOz-T.d.cts → types-jQddSUnj.d.cts} +118 -5
  81. package/package.json +31 -1
@@ -0,0 +1,100 @@
1
+ /**
2
+ * @ai-matrx/design-system/data-table — COLUMN FACETS: the values a column
3
+ * actually holds, and how many rows carry each one.
4
+ *
5
+ * A filter box that says "Contains…" asks the reader to already know what is in
6
+ * the column. A checklist of the real values, each with its count, tells them —
7
+ * and the values that surprise you (the one-off typo, the stray casing, the
8
+ * blank nobody filled in) are exactly the ones a text box hides.
9
+ *
10
+ * THE TWO LAWS this module exists to keep:
11
+ *
12
+ * 1. **Counts are computed locally ONLY when the browser holds every row the
13
+ * counts describe.** `localFacetsAreComplete` is the test, and it compares
14
+ * the loaded row count against the total AFTER the active search, because
15
+ * that is the population the counts claim to describe. One page of rows
16
+ * would produce counts that look authoritative and are not. When the browser
17
+ * does not hold them all, the consumer's `source` answers from the database.
18
+ *
19
+ * 2. **A partial answer says so, in a sentence.** `describeFacetSource` writes
20
+ * the one line the popover shows. There is no silent fallback: if the
21
+ * database could not be asked, or answered only part of the truth, the
22
+ * reader is told before they act on the list.
23
+ *
24
+ * Pure: no React, no DOM, no network. Nothing here throws on any input.
25
+ */
26
+ /** One distinct value of a column and how many rows carry it. */
27
+ interface ColumnFacetValue {
28
+ value: string;
29
+ count: number;
30
+ }
31
+ /**
32
+ * What one column holds. `values` is the top slice by frequency; the counts
33
+ * that describe the WHOLE column — `totalRows`, `filled`, `blank`,
34
+ * `distinctCount` — are always about every row considered, never about the
35
+ * slice, which is what makes `truncated` meaningful rather than decorative.
36
+ */
37
+ interface ColumnFacets {
38
+ columnId: string;
39
+ /** Rows considered (after the active search, if any). */
40
+ totalRows: number;
41
+ /** Rows whose cell is non-empty. */
42
+ filled: number;
43
+ /** Rows whose cell is null or whitespace-only — a real filter target. */
44
+ blank: number;
45
+ /** Distinct non-empty values across EVERY row, not just `values`. */
46
+ distinctCount: number;
47
+ /** Longest value; a caller refuses a picker on a column of long prose. */
48
+ maxLength: number;
49
+ /** Distinct values too long to offer as options. */
50
+ unlistable: number;
51
+ limit: number;
52
+ /** True when `values` does not carry every listable distinct value. */
53
+ truncated: boolean;
54
+ /** Top values by frequency, descending; ties broken by the value itself. */
55
+ values: ColumnFacetValue[];
56
+ /** Who counted. "source" is the database; "local" is the rows in the browser. */
57
+ answeredBy: "source" | "local";
58
+ /**
59
+ * True when the counts describe every row of the column. False means the
60
+ * reader is looking at a partial answer and the popover must say so.
61
+ */
62
+ complete: boolean;
63
+ }
64
+ /** A value longer than this is real data, not a pickable option. */
65
+ declare const MAX_LISTABLE_FACET_LENGTH = 300;
66
+ /**
67
+ * Compute a column's facets from rows already in memory.
68
+ *
69
+ * `rows` MUST be every row the facets are meant to describe — the caller
70
+ * decides that with `localFacetsAreComplete`. `complete` carries the caller's
71
+ * verdict through to the popover so a partial answer can never be printed as a
72
+ * whole one.
73
+ */
74
+ declare function computeColumnFacets(args: {
75
+ columnId: string;
76
+ rows: readonly unknown[];
77
+ readValue: (row: unknown, columnId: string) => unknown;
78
+ limit?: number | undefined;
79
+ complete?: boolean | undefined;
80
+ }): ColumnFacets;
81
+ /**
82
+ * May facets be computed from these rows, or must the source be asked?
83
+ *
84
+ * The ONLY safe condition is holding every row the facets describe. `total` is
85
+ * the row count AFTER the active search, which is exactly what the loaded rows
86
+ * also reflect — so comparing counts is a true completeness test, not a guess.
87
+ * An unknown total is never treated as "small enough".
88
+ */
89
+ declare function localFacetsAreComplete(loadedRowCount: number, total: number | undefined): boolean;
90
+ /**
91
+ * The one sentence under the value list. Empty when the answer is whole and
92
+ * nothing needs saying — the popover never nags a reader whose counts are
93
+ * exact.
94
+ */
95
+ declare function describeFacetSource(facets: ColumnFacets | null | undefined, noun?: string): string;
96
+ /** The sentence shown while the source is being asked, and when it refuses. */
97
+ declare const FACETS_LOADING_NOTICE = "Counting values\u2026";
98
+ declare function describeFacetFailure(error: unknown): string;
99
+
100
+ export { type ColumnFacetValue, type ColumnFacets, FACETS_LOADING_NOTICE, MAX_LISTABLE_FACET_LENGTH, computeColumnFacets, describeFacetFailure, describeFacetSource, localFacetsAreComplete };
@@ -0,0 +1,98 @@
1
+
2
+ // src/data-table/facets.ts
3
+ var MAX_LISTABLE_FACET_LENGTH = 300;
4
+ var DEFAULT_FACET_LIMIT = 200;
5
+ var MAX_FACET_LIMIT = 500;
6
+ function facetText(raw) {
7
+ if (raw === null || raw === void 0) return "";
8
+ if (typeof raw === "object") {
9
+ try {
10
+ return JSON.stringify(raw);
11
+ } catch {
12
+ return String(raw);
13
+ }
14
+ }
15
+ return String(raw);
16
+ }
17
+ function computeColumnFacets(args) {
18
+ const { columnId, rows, readValue } = args;
19
+ const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);
20
+ const counts = /* @__PURE__ */ new Map();
21
+ const longValues = /* @__PURE__ */ new Set();
22
+ let blank = 0;
23
+ let maxLength = 0;
24
+ for (const row of rows) {
25
+ let raw;
26
+ try {
27
+ raw = readValue(row, columnId);
28
+ } catch {
29
+ raw = void 0;
30
+ }
31
+ const trimmed = facetText(raw).trim();
32
+ if (trimmed === "") {
33
+ blank += 1;
34
+ continue;
35
+ }
36
+ if (trimmed.length > maxLength) maxLength = trimmed.length;
37
+ if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {
38
+ longValues.add(trimmed);
39
+ continue;
40
+ }
41
+ counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
42
+ }
43
+ const values = [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, limit);
44
+ return {
45
+ columnId,
46
+ totalRows: rows.length,
47
+ filled: rows.length - blank,
48
+ blank,
49
+ distinctCount: counts.size + longValues.size,
50
+ maxLength,
51
+ unlistable: longValues.size,
52
+ limit,
53
+ truncated: counts.size > limit,
54
+ values,
55
+ answeredBy: "local",
56
+ complete: args.complete ?? true
57
+ };
58
+ }
59
+ function localFacetsAreComplete(loadedRowCount, total) {
60
+ if (total === void 0 || !Number.isFinite(total) || total < 0) return false;
61
+ return loadedRowCount >= total;
62
+ }
63
+ var NUMBERS = new Intl.NumberFormat("en-US");
64
+ function n(value) {
65
+ try {
66
+ return NUMBERS.format(value);
67
+ } catch {
68
+ return String(value);
69
+ }
70
+ }
71
+ function describeFacetSource(facets, noun = "row") {
72
+ if (!facets) return "";
73
+ const plural = `${noun}s`;
74
+ if (!facets.complete) {
75
+ return `These counts cover the ${n(facets.totalRows)} ${plural} loaded here, not the whole table.`;
76
+ }
77
+ if (facets.truncated) {
78
+ return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;
79
+ }
80
+ if (facets.unlistable > 0) {
81
+ return `${n(facets.unlistable)} ${facets.unlistable === 1 ? "value is" : "values are"} too long to list. Match text instead to reach ${facets.unlistable === 1 ? "it" : "them"}.`;
82
+ }
83
+ return "";
84
+ }
85
+ var FACETS_LOADING_NOTICE = "Counting values\u2026";
86
+ function describeFacetFailure(error) {
87
+ const detail = error instanceof Error ? error.message : String(error ?? "");
88
+ return detail ? `The values in this column could not be counted: ${detail} Match text instead.` : "The values in this column could not be counted. Match text instead.";
89
+ }
90
+ export {
91
+ FACETS_LOADING_NOTICE,
92
+ MAX_LISTABLE_FACET_LENGTH,
93
+ computeColumnFacets,
94
+ describeFacetFailure,
95
+ describeFacetSource,
96
+ localFacetsAreComplete
97
+ };
98
+ //# sourceMappingURL=facets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/facets.ts"],"sourcesContent":["/**\n * @ai-matrx/design-system/data-table — COLUMN FACETS: the values a column\n * actually holds, and how many rows carry each one.\n *\n * A filter box that says \"Contains…\" asks the reader to already know what is in\n * the column. A checklist of the real values, each with its count, tells them —\n * and the values that surprise you (the one-off typo, the stray casing, the\n * blank nobody filled in) are exactly the ones a text box hides.\n *\n * THE TWO LAWS this module exists to keep:\n *\n * 1. **Counts are computed locally ONLY when the browser holds every row the\n * counts describe.** `localFacetsAreComplete` is the test, and it compares\n * the loaded row count against the total AFTER the active search, because\n * that is the population the counts claim to describe. One page of rows\n * would produce counts that look authoritative and are not. When the browser\n * does not hold them all, the consumer's `source` answers from the database.\n *\n * 2. **A partial answer says so, in a sentence.** `describeFacetSource` writes\n * the one line the popover shows. There is no silent fallback: if the\n * database could not be asked, or answered only part of the truth, the\n * reader is told before they act on the list.\n *\n * Pure: no React, no DOM, no network. Nothing here throws on any input.\n */\n\n/** One distinct value of a column and how many rows carry it. */\nexport interface ColumnFacetValue {\n value: string;\n count: number;\n}\n\n/**\n * What one column holds. `values` is the top slice by frequency; the counts\n * that describe the WHOLE column — `totalRows`, `filled`, `blank`,\n * `distinctCount` — are always about every row considered, never about the\n * slice, which is what makes `truncated` meaningful rather than decorative.\n */\nexport interface ColumnFacets {\n columnId: string;\n /** Rows considered (after the active search, if any). */\n totalRows: number;\n /** Rows whose cell is non-empty. */\n filled: number;\n /** Rows whose cell is null or whitespace-only — a real filter target. */\n blank: number;\n /** Distinct non-empty values across EVERY row, not just `values`. */\n distinctCount: number;\n /** Longest value; a caller refuses a picker on a column of long prose. */\n maxLength: number;\n /** Distinct values too long to offer as options. */\n unlistable: number;\n limit: number;\n /** True when `values` does not carry every listable distinct value. */\n truncated: boolean;\n /** Top values by frequency, descending; ties broken by the value itself. */\n values: ColumnFacetValue[];\n /** Who counted. \"source\" is the database; \"local\" is the rows in the browser. */\n answeredBy: \"source\" | \"local\";\n /**\n * True when the counts describe every row of the column. False means the\n * reader is looking at a partial answer and the popover must say so.\n */\n complete: boolean;\n}\n\n/** A value longer than this is real data, not a pickable option. */\nexport const MAX_LISTABLE_FACET_LENGTH = 300;\n\nconst DEFAULT_FACET_LIMIT = 200;\nconst MAX_FACET_LIMIT = 500;\n\nfunction facetText(raw: unknown): string {\n if (raw === null || raw === undefined) return \"\";\n if (typeof raw === \"object\") {\n try {\n return JSON.stringify(raw);\n } catch {\n return String(raw);\n }\n }\n return String(raw);\n}\n\n/**\n * Compute a column's facets from rows already in memory.\n *\n * `rows` MUST be every row the facets are meant to describe — the caller\n * decides that with `localFacetsAreComplete`. `complete` carries the caller's\n * verdict through to the popover so a partial answer can never be printed as a\n * whole one.\n */\nexport function computeColumnFacets(args: {\n columnId: string;\n rows: readonly unknown[];\n readValue: (row: unknown, columnId: string) => unknown;\n limit?: number | undefined;\n complete?: boolean | undefined;\n}): ColumnFacets {\n const { columnId, rows, readValue } = args;\n const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);\n\n const counts = new Map<string, number>();\n const longValues = new Set<string>();\n let blank = 0;\n let maxLength = 0;\n\n for (const row of rows) {\n let raw: unknown;\n try {\n raw = readValue(row, columnId);\n } catch {\n raw = undefined;\n }\n const trimmed = facetText(raw).trim();\n if (trimmed === \"\") {\n blank += 1;\n continue;\n }\n if (trimmed.length > maxLength) maxLength = trimmed.length;\n if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {\n longValues.add(trimmed);\n continue;\n }\n counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);\n }\n\n const values: ColumnFacetValue[] = [...counts.entries()]\n .map(([value, count]) => ({ value, count }))\n .sort((a, b) => b.count - a.count || a.value.localeCompare(b.value))\n .slice(0, limit);\n\n return {\n columnId,\n totalRows: rows.length,\n filled: rows.length - blank,\n blank,\n distinctCount: counts.size + longValues.size,\n maxLength,\n unlistable: longValues.size,\n limit,\n truncated: counts.size > limit,\n values,\n answeredBy: \"local\",\n complete: args.complete ?? true,\n };\n}\n\n/**\n * May facets be computed from these rows, or must the source be asked?\n *\n * The ONLY safe condition is holding every row the facets describe. `total` is\n * the row count AFTER the active search, which is exactly what the loaded rows\n * also reflect — so comparing counts is a true completeness test, not a guess.\n * An unknown total is never treated as \"small enough\".\n */\nexport function localFacetsAreComplete(\n loadedRowCount: number,\n total: number | undefined,\n): boolean {\n if (total === undefined || !Number.isFinite(total) || total < 0) return false;\n return loadedRowCount >= total;\n}\n\nconst NUMBERS = new Intl.NumberFormat(\"en-US\");\n\nfunction n(value: number): string {\n try {\n return NUMBERS.format(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * The one sentence under the value list. Empty when the answer is whole and\n * nothing needs saying — the popover never nags a reader whose counts are\n * exact.\n */\nexport function describeFacetSource(\n facets: ColumnFacets | null | undefined,\n noun = \"row\",\n): string {\n if (!facets) return \"\";\n const plural = `${noun}s`;\n if (!facets.complete) {\n return `These counts cover the ${n(facets.totalRows)} ${plural} loaded here, not the whole table.`;\n }\n if (facets.truncated) {\n return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;\n }\n if (facets.unlistable > 0) {\n return `${n(facets.unlistable)} ${facets.unlistable === 1 ? \"value is\" : \"values are\"} too long to list. Match text instead to reach ${facets.unlistable === 1 ? \"it\" : \"them\"}.`;\n }\n return \"\";\n}\n\n/** The sentence shown while the source is being asked, and when it refuses. */\nexport const FACETS_LOADING_NOTICE = \"Counting values…\";\nexport function describeFacetFailure(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error ?? \"\");\n return detail\n ? `The values in this column could not be counted: ${detail} Match text instead.`\n : \"The values in this column could not be counted. Match text instead.\";\n}\n"],"mappings":"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","names":[]}
@@ -124,6 +124,72 @@ function layeredFilterMatchesValue(value, rule) {
124
124
  }
125
125
  }
126
126
 
127
+ // src/data-table/date-filter.ts
128
+ function startOfDay(date) {
129
+ const next = new Date(date);
130
+ next.setHours(0, 0, 0, 0);
131
+ return next;
132
+ }
133
+ function startOfWeek(date) {
134
+ const next = startOfDay(date);
135
+ next.setDate(next.getDate() - (next.getDay() === 0 ? 6 : next.getDay() - 1));
136
+ return next;
137
+ }
138
+ function startOfMonth(date) {
139
+ return new Date(date.getFullYear(), date.getMonth(), 1);
140
+ }
141
+ function startOfYear(date) {
142
+ return new Date(date.getFullYear(), 0, 1);
143
+ }
144
+ function validDate(value) {
145
+ if (value === void 0) return void 0;
146
+ const date = value instanceof Date ? new Date(value) : new Date(value);
147
+ return Number.isNaN(date.valueOf()) ? void 0 : date;
148
+ }
149
+ function dateFilterBounds(filter, now = /* @__PURE__ */ new Date()) {
150
+ if (!filter.preset) {
151
+ return {
152
+ ...filter.since === void 0 ? {} : { since: filter.since },
153
+ ...filter.until === void 0 ? {} : { until: filter.until }
154
+ };
155
+ }
156
+ if (filter.preset === "all") return {};
157
+ const anchor = validDate(filter.anchor) ?? validDate(now) ?? /* @__PURE__ */ new Date();
158
+ const ago = (milliseconds) => new Date(anchor.valueOf() - milliseconds).toISOString();
159
+ const recent = {
160
+ "1m": 6e4,
161
+ "15m": 15 * 6e4,
162
+ "30m": 30 * 6e4,
163
+ "1h": 60 * 6e4,
164
+ "2h": 2 * 60 * 6e4,
165
+ "6h": 6 * 60 * 6e4,
166
+ "12h": 12 * 60 * 6e4
167
+ };
168
+ const duration = recent[filter.preset.replace("older_", "")];
169
+ if (duration !== void 0) {
170
+ return filter.preset.startsWith("older_") ? { until: ago(duration) } : { since: ago(duration) };
171
+ }
172
+ const starts = {
173
+ today: startOfDay(anchor),
174
+ this_week: startOfWeek(anchor),
175
+ this_month: startOfMonth(anchor),
176
+ this_year: startOfYear(anchor),
177
+ older_today: startOfDay(anchor),
178
+ older_this_week: startOfWeek(anchor),
179
+ older_this_month: startOfMonth(anchor),
180
+ older_this_year: startOfYear(anchor)
181
+ };
182
+ if (filter.preset === "yesterday") {
183
+ const until = startOfDay(anchor);
184
+ const since = new Date(until);
185
+ since.setDate(since.getDate() - 1);
186
+ return { since: since.toISOString(), until: until.toISOString() };
187
+ }
188
+ const start = starts[filter.preset];
189
+ if (!start) return {};
190
+ return filter.preset.startsWith("older_") ? { until: start.toISOString() } : { since: start.toISOString() };
191
+ }
192
+
127
193
  // src/data-table/filter-engine.ts
128
194
  var SELECT_EMPTY_VALUE = "__empty__";
129
195
  var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
@@ -226,6 +292,21 @@ function passesColumnFilter(value, filter) {
226
292
  if (filter.max !== void 0 && n > filter.max) return false;
227
293
  return true;
228
294
  }
295
+ case "date": {
296
+ const raw = value instanceof Date ? value : new Date(stringifyCellValue(value));
297
+ if (Number.isNaN(raw.valueOf())) return false;
298
+ const { since, until } = dateFilterBounds(filter);
299
+ const timestamp = raw.valueOf();
300
+ if (since !== void 0) {
301
+ const sinceTimestamp = new Date(since).valueOf();
302
+ if (Number.isNaN(sinceTimestamp) || timestamp < sinceTimestamp) return false;
303
+ }
304
+ if (until !== void 0) {
305
+ const untilTimestamp = new Date(until).valueOf();
306
+ if (Number.isNaN(untilTimestamp) || timestamp >= untilTimestamp) return false;
307
+ }
308
+ return true;
309
+ }
229
310
  }
230
311
  })();
231
312
  return filter.negated ? !matches : matches;
@@ -303,6 +384,9 @@ function countActiveColumnFilters(filters) {
303
384
  case "number":
304
385
  if (f.min !== void 0 || f.max !== void 0) n += 1;
305
386
  break;
387
+ case "date":
388
+ if (isColumnFilterActive(f)) n += 1;
389
+ break;
306
390
  }
307
391
  }
308
392
  return n;
@@ -318,6 +402,10 @@ function isColumnFilterActive(filter) {
318
402
  return true;
319
403
  case "number":
320
404
  return filter.min !== void 0 || filter.max !== void 0;
405
+ case "date": {
406
+ const bounds = dateFilterBounds(filter);
407
+ return bounds.since !== void 0 || bounds.until !== void 0;
408
+ }
321
409
  }
322
410
  }
323
411
  function applyRowEdits(row, edits) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/data-table/filter-engine.ts","../../src/data-table/layered-filters.ts"],"sourcesContent":["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","/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACOA,iBAAkB;AAGX,IAAM,2BAA2B;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAkCA,IAAM,0BAA0B,aAAE,OAAO;AAAA,EACvC,IAAI,aAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACpB,OAAO,aAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACvB,UAAU,aAAE,KAAK,wBAAwB;AAAA,EACzC,OAAO,aAAE,OAAO;AAAA,EAChB,SAAS,aAAE,OAAO,EAAE,SAAS;AAC/B,CAAC;AAED,IAAM,2BAA2B,aAAE,MAAM,uBAAuB,EAAE,IAAI,EAAE;AAgEjE,SAAS,wBACd,UACS;AACT,SAAO,aAAa,cAAc,aAAa;AACjD;AAEO,SAAS,4BAA4B,MAAkC;AAC5E,MAAI,CAAC,wBAAwB,KAAK,QAAQ,EAAG,QAAO;AACpD,MAAI,KAAK,MAAM,KAAK,MAAM,GAAI,QAAO;AACrC,SAAO,KAAK,aAAa,aAAa,QAAQ,KAAK,SAAS,KAAK,CAAC;AACpE;AAEO,SAAS,2BACd,OACqB;AACrB,UAAQ,SAAS,CAAC,GAAG,OAAO,2BAA2B;AACzD;AAmGA,SAAS,eAAe,OAAwB;AAC9C,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,SAAO,OAAO,KAAK,EAAE,KAAK,EAAE,kBAAkB;AAChD;AAEA,SAAS,MAAM,OAAyB;AACtC,SAAO,MAAM,MAAM,iBAAiB,EAAE,OAAO,OAAO;AACtD;AAGO,SAAS,0BACd,OACA,MACS;AACT,QAAM,SAAS,eAAe,KAAK;AACnC,QAAM,WAAW,KAAK,MAAM,KAAK,EAAE,kBAAkB;AACrD,QAAM,aAAa,KAAK,SAAS,KAAK,EAAE,kBAAkB,KAAK;AAE/D,UAAQ,KAAK,UAAU;AAAA,IACrB,KAAK;AACH,aAAO,OAAO,SAAS,QAAQ;AAAA,IACjC,KAAK;AACH,aAAO,CAAC,OAAO,SAAS,QAAQ;AAAA,IAClC,KAAK;AACH,aAAO,WAAW;AAAA,IACpB,KAAK;AACH,aAAO,WAAW;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,WAAW,QAAQ;AAAA,IACnC,KAAK;AACH,aAAO,OAAO,SAAS,QAAQ;AAAA,IACjC,KAAK;AACH,aAAO,MAAM,MAAM,EAAE,SAAS,QAAQ;AAAA,IACxC,KAAK;AACH,aAAO,CAAC,MAAM,MAAM,EAAE,SAAS,QAAQ;AAAA,IACzC,KAAK;AACH,aAAO,WAAW;AAAA,IACpB,KAAK;AACH,aAAO,WAAW;AAAA,IACpB,KAAK;AACH,aAAO,OAAO,MAAM,IAAI,OAAO,QAAQ;AAAA,IACzC,KAAK;AACH,aAAO,OAAO,MAAM,KAAK,OAAO,QAAQ;AAAA,IAC1C,KAAK;AACH,aAAO,OAAO,MAAM,IAAI,OAAO,QAAQ;AAAA,IACzC,KAAK;AACH,aAAO,OAAO,MAAM,KAAK,OAAO,QAAQ;AAAA,IAC1C,KAAK,WAAW;AACd,YAAM,UAAU,OAAO,MAAM;AAC7B,aAAO,WAAW,OAAO,QAAQ,KAAK,WAAW,OAAO,UAAU;AAAA,IACpE;AAAA,EACF;AACF;;;ADzRO,IAAM,qBAAqB;AAC3B,IAAM,yBAAyB;AAE/B,IAAM,oBAAoB;AAE1B,SAAS,SAAY,KAAgC;AAC1D,MAAI,IAAI,GAAI,QAAO,IAAI;AACvB,MAAI,IAAI,YAAa,QAAO,OAAO,IAAI,WAAW;AAClD,QAAM,IAAI,MAAM,sDAAsD;AACxE;AAEO,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;AAEO,SAAS,eAAkB,KAAQ,KAAiC;AACzE,SAAO,IAAI,cAAc,IAAI,YAAY,GAAG,IAAI,aAAa,KAAK,GAAG;AACvE;AAEO,SAAS,aAAgB,KAAQ,KAAiC;AACvE,SAAO,IAAI,YAAY,IAAI,UAAU,GAAG,IAAI,aAAa,KAAK,GAAG;AACnE;AAEO,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;AAGO,SAAS,iBAAiB,OAAyB;AACxD,SAAO,MACJ,kBAAkB,EAClB,MAAM,iBAAiB,EACvB,OAAO,OAAO;AACnB;AAOO,SAAS,mBACd,gBACA,OACA,OAA6B,YACpB;AACT,QAAM,kBAAkB,MAAM,KAAK,EAAE,kBAAkB;AACvD,MAAI,CAAC,gBAAiB,QAAO;AAC7B,MAAI,SAAS,YAAY;AACvB,WAAO,eAAe,kBAAkB,EAAE,SAAS,eAAe;AAAA,EACpE;AACA,QAAM,cAAc,IAAI,IAAI,iBAAiB,cAAc,CAAC;AAC5D,QAAM,gBAAgB,iBAAiB,eAAe;AACtD,SACE,cAAc,SAAS,KACvB,cAAc,MAAM,CAAC,SAAS,YAAY,IAAI,IAAI,CAAC;AAEvD;AAEA,SAAS,cAAc,GAAY,GAAoB;AACrD,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AACnC,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,KAAK,KAAM,QAAO;AACtB,MAAI,OAAO,MAAM,YAAY,OAAO,MAAM,SAAU,QAAO,IAAI;AAC/D,MAAI,OAAO,MAAM,aAAa,OAAO,MAAM,WAAW;AACpD,WAAO,OAAO,CAAC,IAAI,OAAO,CAAC;AAAA,EAC7B;AACA,SAAO,mBAAmB,CAAC,EAAE,cAAc,mBAAmB,CAAC,GAAG,QAAW;AAAA,IAC3E,SAAS;AAAA,IACT,aAAa;AAAA,EACf,CAAC;AACH;AAGO,SAAS,iBAAiB,OAA0B;AACzD,UAAQ,MAAM,QAAQ,KAAK,IAAI,QAAQ,CAAC,KAAK,GAC1C,IAAI,kBAAkB,EACtB,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;AAEO,SAAS,mBACd,OACA,QACS;AACT,QAAM,WAAW,MAAM;AACrB,YAAQ,OAAO,MAAM;AAAA,MACrB,KAAK,QAAQ;AACX,cAAM,IAAI,mBAAmB,KAAK;AAClC,YAAI,OAAO,SAAS,OAAQ,QAAO,SAAS;AAC5C,YAAI,OAAO,SAAS,WAAY,QAAO,SAAS;AAChD,YAAI,OAAO,SAAS,QAAS,QAAO,EAAE,KAAK,MAAM;AACjD,YAAI,OAAO,SAAS,YAAa,QAAO,EAAE,KAAK,MAAM;AACrD,cAAM,IAAI,OAAO,MAAM,KAAK,EAAE,YAAY;AAC1C,YAAI,CAAC,EAAG,QAAO;AACf,eAAO,EAAE,YAAY,EAAE,SAAS,CAAC;AAAA,MACnC;AAAA,MACA,KAAK,UAAU;AAIb,YAAI,OAAO,WAAW,UAAa,OAAO,OAAO,WAAW,GAAG;AAC7D,iBAAO;AAAA,QACT;AACA,cAAM,WAAW,OAAO,WACrB,OAAO,SAAS,OAAO,UAAU,YAAY,CAAC,OAAO,KAAK,IAAI,CAAC;AAClE,YAAI,SAAS,WAAW,EAAG,QAAO;AAClC,cAAM,SAAS,iBAAiB,KAAK;AAErC,eAAO,SAAS;AAAA,UAAK,CAAC,MACpB,MAAM,oBACF,SAAS,OACT,MAAM,qBACN,OAAO,WAAW,IAClB,MAAM,yBACJ,OAAO,SAAS,IAChB,MAAM,KACJ,UAAU,MAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,EAAE,IAC1D,OAAO,SAAS,CAAC;AAAA,QAC3B;AAAA,MACF;AAAA,MACA,KAAK,WAAW;AACd,eAAO,QAAQ,KAAK,MAAM,OAAO;AAAA,MACnC;AAAA,MACA,KAAK,UAAU;AAGb,cAAM,MAAM,mBAAmB,KAAK,EAAE,KAAK;AAC3C,YAAI,QAAQ,GAAI,QAAO;AACvB,cAAM,aAAa,IAAI,QAAQ,aAAa,EAAE;AAC9C,YAAI,eAAe,GAAI,QAAO;AAC9B,cAAM,IAAI,OAAO,UAAU,WACvB,QACA,OAAO,UAAU;AACrB,YAAI,CAAC,OAAO,SAAS,CAAC,EAAG,QAAO;AAChC,YAAI,OAAO,QAAQ,UAAa,IAAI,OAAO,IAAK,QAAO;AACvD,YAAI,OAAO,QAAQ,UAAa,IAAI,OAAO,IAAK,QAAO;AACvD,eAAO;AAAA,MACT;AAAA,IACA;AAAA,EACF,GAAG;AACH,SAAO,OAAO,UAAU,CAAC,UAAU;AACrC;AAEO,SAAS,kBACd,MACA,SACA,eACA,MACA,cACA,OACA,gBACA,mBAAyC,YACzC,YACK;AACL,QAAM,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;AAC5D,QAAM,IAAI,aAAa,KAAK,EAAE,YAAY;AAC1C,QAAM,OAAO,OAAO,MAAM,KAAK,EAAE,YAAY,KAAK;AAClD,QAAM,uBAAuB,2BAA2B,cAAc;AAEtE,MAAI,SAAS,KAAK,OAAO,CAAC,QAAQ;AAChC,eAAW,CAAC,IAAI,MAAM,KAAK,OAAO,QAAQ,aAAa,GAAG;AACxD,UAAI,CAAC,OAAQ;AACb,YAAM,MAAM,QAAQ,IAAI,EAAE;AAC1B,UAAI,CAAC,OAAO,IAAI,WAAW,MAAO;AAClC,UAAI,CAAC,mBAAmB,eAAe,KAAK,GAAG,GAAG,MAAM,EAAG,QAAO;AAAA,IACpE;AAEA,eAAW,QAAQ,sBAAsB;AACvC,YAAM,MAAM,QAAQ,IAAI,KAAK,KAAK;AAClC,UAAI,CAAC,OAAO,CAAC,0BAA0B,eAAe,KAAK,GAAG,GAAG,IAAI,GAAG;AACtE,eAAO;AAAA,MACT;AAAA,IACF;AAEA,QAAI,QAAQ,OAAO;AACjB,YAAM,MAAM,MAAM,UAAU,KAAK,CAAC,OAAO;AACvC,cAAM,MAAM,QAAQ,IAAI,EAAE;AAC1B,YAAI,CAAC,IAAK,QAAO;AACjB,eAAO,mBAAmB,eAAe,KAAK,GAAG,CAAC,EAC/C,YAAY,EACZ,SAAS,IAAI;AAAA,MAClB,CAAC;AACD,UAAI,CAAC,IAAK,QAAO;AAAA,IACnB;AAEA,QAAI,CAAC,EAAG,QAAO;AACf,UAAM,mBAAmB,QACtB;AAAA,MACC,CAAC,QACC,IAAI,WAAW,SAAS,QAAQ,IAAI,eAAe,IAAI,UAAU;AAAA,IACrE,EACC,IAAI,CAAC,QAAQ,mBAAmB,eAAe,KAAK,GAAG,CAAC,CAAC;AAC5D,UAAM,uBAAuB,aAAa,GAAG;AAC7C,QAAI,qBAAsB,kBAAiB,KAAK,oBAAoB;AACpE,QAAI,qBAAqB,YAAY;AACnC,aAAO,iBAAiB;AAAA,QAAK,CAAC,UAC5B,mBAAmB,OAAO,GAAG,UAAU;AAAA,MACzC;AAAA,IACF;AACA,WAAO,mBAAmB,iBAAiB,KAAK,GAAG,GAAG,GAAG,aAAa;AAAA,EACxE,CAAC;AAED,MAAI,MAAM;AACR,UAAM,MAAM,QAAQ,IAAI,KAAK,EAAE;AAC/B,QAAI,OAAO,IAAI,aAAa,OAAO;AACjC,YAAM,MAAM,KAAK,cAAc,QAAQ,IAAI;AAC3C,eAAS,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM;AAClC,cAAM,KAAK,aAAa,GAAG,GAAG;AAC9B,cAAM,KAAK,aAAa,GAAG,GAAG;AAG9B,cAAM,KAAK,MAAM,QAAQ,mBAAmB,EAAE,MAAM;AACpD,cAAM,KAAK,MAAM,QAAQ,mBAAmB,EAAE,MAAM;AACpD,YAAI,OAAO,GAAI,QAAO,KAAK,IAAI;AAC/B,eAAO,cAAc,IAAI,EAAE,IAAI;AAAA,MACjC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEO,SAAS,yBAAyB,SAAqC;AAC5E,MAAI,IAAI;AACR,aAAW,KAAK,OAAO,OAAO,OAAO,GAAG;AACtC,QAAI,CAAC,EAAG;AACR,YAAQ,EAAE,MAAM;AAAA,MACd,KAAK;AACH,YAAI,EAAE,MAAM,KAAK,KAAM,EAAE,QAAQ,EAAE,SAAS,WAAa,MAAK;AAC9D;AAAA,MACF,KAAK;AACH,YAAI,EAAE,WAAW,UAAc,EAAE,SAAS,EAAE,UAAU;AACpD,eAAK;AACP;AAAA,MACF,KAAK;AACH,aAAK;AACL;AAAA,MACF,KAAK;AACH,YAAI,EAAE,QAAQ,UAAa,EAAE,QAAQ,OAAW,MAAK;AACrD;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,qBACd,QACS;AACT,MAAI,CAAC,OAAQ,QAAO;AACpB,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aACE,OAAO,MAAM,KAAK,EAAE,SAAS,KAC5B,OAAO,SAAS,UAAa,OAAO,SAAS;AAAA,IAElD,KAAK;AACH,aAAO,OAAO,WAAW,SACrB,OACA,QAAQ,OAAO,KAAK,KAAK,OAAO,UAAU;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,OAAO,QAAQ,UAAa,OAAO,QAAQ;AAAA,EACtD;AACF;AAGO,SAAS,cACd,KACA,OACG;AACH,MAAI,CAAC,SAAS,OAAO,KAAK,KAAK,EAAE,WAAW,EAAG,QAAO;AACtD,SAAO,EAAE,GAAG,KAAK,GAAG,MAAM;AAC5B;","names":[]}
1
+ {"version":3,"sources":["../../src/data-table/filter-engine.ts","../../src/data-table/layered-filters.ts","../../src/data-table/date-filter.ts"],"sourcesContent":["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","/**\n * Shared vocabulary for ordered, AND-combined table filters. The same rule\n * shape is URL-safe, can be evaluated by local tables, and can be handed to a\n * controlled table's server query without a surface inventing another query\n * builder.\n */\n\nimport { z } from \"zod\";\nimport type { ColumnFiltersState } from \"./types\";\n\nexport const LAYERED_FILTER_OPERATORS = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n] as const;\n\nexport type LayeredFilterOperator = (typeof LAYERED_FILTER_OPERATORS)[number];\n\nexport interface LayeredFilterRule {\n id: string;\n field: string;\n operator: LayeredFilterOperator;\n value: string;\n valueTo?: string | undefined;\n}\n\nexport type LayeredFilterField =\n | {\n id: string;\n label: string;\n kind: \"text\";\n operators?: LayeredFilterOperator[];\n }\n | {\n id: string;\n label: string;\n kind: \"number\";\n operators?: LayeredFilterOperator[];\n placeholder?: string;\n }\n | {\n id: string;\n label: string;\n kind: \"select\";\n operators?: LayeredFilterOperator[];\n options: Array<{ value: string; label: string }>;\n };\n\nconst layeredFilterRuleSchema = z.object({\n id: z.string().min(1),\n field: z.string().min(1),\n operator: z.enum(LAYERED_FILTER_OPERATORS),\n value: z.string(),\n valueTo: z.string().optional(),\n});\n\nconst layeredFilterRulesSchema = z.array(layeredFilterRuleSchema).max(20);\n\nconst TEXT_OPERATORS: LayeredFilterOperator[] = [\n \"contains\",\n \"not_contains\",\n \"equals\",\n \"not_equals\",\n \"starts_with\",\n \"ends_with\",\n \"word\",\n \"not_word\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst NUMBER_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"greater_than\",\n \"greater_or_equal\",\n \"less_than\",\n \"less_or_equal\",\n \"between\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nconst SELECT_OPERATORS: LayeredFilterOperator[] = [\n \"equals\",\n \"not_equals\",\n \"is_empty\",\n \"is_not_empty\",\n];\n\nexport const LAYERED_FILTER_OPERATOR_LABELS: Record<\n LayeredFilterOperator,\n string\n> = {\n contains: \"contains\",\n not_contains: \"doesn't contain\",\n equals: \"is\",\n not_equals: \"is not\",\n starts_with: \"starts with\",\n ends_with: \"ends with\",\n word: \"has the word\",\n not_word: \"doesn't have the word\",\n is_empty: \"is empty\",\n is_not_empty: \"is not empty\",\n greater_than: \"is greater than\",\n greater_or_equal: \"is at least\",\n less_than: \"is less than\",\n less_or_equal: \"is at most\",\n between: \"is between\",\n};\n\nexport function operatorsForLayeredField(\n field: LayeredFilterField,\n): LayeredFilterOperator[] {\n if (field.operators) return field.operators;\n if (field.kind === \"number\") return NUMBER_OPERATORS;\n if (field.kind === \"select\") return SELECT_OPERATORS;\n return TEXT_OPERATORS;\n}\n\nexport function layeredFilterNeedsValue(\n operator: LayeredFilterOperator,\n): boolean {\n return operator !== \"is_empty\" && operator !== \"is_not_empty\";\n}\n\nexport function isCompleteLayeredFilterRule(rule: LayeredFilterRule): boolean {\n if (!layeredFilterNeedsValue(rule.operator)) return true;\n if (rule.value.trim() === \"\") return false;\n return rule.operator !== \"between\" || Boolean(rule.valueTo?.trim());\n}\n\nexport function completeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): LayeredFilterRule[] {\n return (rules ?? []).filter(isCompleteLayeredFilterRule);\n}\n\nexport function isLayeredFilterOperator(\n value: string,\n): value is LayeredFilterOperator {\n return LAYERED_FILTER_OPERATORS.some((operator) => operator === value);\n}\n\n/** Decode URL-owned rules without trusting a hand-edited link. */\nexport function decodeLayeredFilterRules(\n raw: string | null,\n): LayeredFilterRule[] {\n if (!raw) return [];\n try {\n const parsed: unknown = JSON.parse(raw);\n const result = layeredFilterRulesSchema.safeParse(parsed);\n return result.success ? result.data : [];\n } catch {\n return [];\n }\n}\n\n/** Stable compact URL/wire form; rule order is preserved. */\nexport function encodeLayeredFilterRules(\n rules: readonly LayeredFilterRule[] | undefined,\n): string | null {\n return rules?.length ? JSON.stringify(rules) : null;\n}\n\n/**\n * Translate ordinary per-column controls into the same ordered rule language\n * used by the advanced builder. Controlled tables can send these rules to one\n * server evaluator instead of rebuilding numeric/text filter semantics.\n * Select filters are intentionally omitted because their multi-select control\n * is OR-combined and should stay in its native query parameter.\n */\nexport function columnFiltersToLayeredRules(\n filters: ColumnFiltersState,\n fieldIds: readonly string[],\n): LayeredFilterRule[] {\n const rules: LayeredFilterRule[] = [];\n for (const field of fieldIds) {\n const filter = filters[field];\n if (!filter) continue;\n\n if (filter.kind === \"text\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator:\n filter.mode === \"empty\"\n ? \"is_empty\"\n : filter.mode === \"not_empty\"\n ? \"is_not_empty\"\n : \"contains\",\n value: filter.value,\n });\n continue;\n }\n\n if (filter.kind === \"number\") {\n if (filter.min !== undefined && filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"between\",\n value: String(filter.min),\n valueTo: String(filter.max),\n });\n } else if (filter.min !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"greater_or_equal\",\n value: String(filter.min),\n });\n } else if (filter.max !== undefined) {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"less_or_equal\",\n value: String(filter.max),\n });\n }\n continue;\n }\n\n if (filter.kind === \"boolean\") {\n rules.push({\n id: `column-${field}`,\n field,\n operator: \"equals\",\n value: String(filter.value),\n });\n }\n }\n return rules;\n}\n\nfunction normalizedText(value: unknown): string {\n if (value === null || value === undefined) return \"\";\n return String(value).trim().toLocaleLowerCase();\n}\n\nfunction words(value: string): string[] {\n return value.split(/[^\\p{L}\\p{N}]+/u).filter(Boolean);\n}\n\n/** Local-table evaluator. Controlled tables send the same rule shape server-side. */\nexport function layeredFilterMatchesValue(\n value: unknown,\n rule: LayeredFilterRule,\n): boolean {\n const actual = normalizedText(value);\n const expected = rule.value.trim().toLocaleLowerCase();\n const expectedTo = rule.valueTo?.trim().toLocaleLowerCase() ?? \"\";\n\n switch (rule.operator) {\n case \"contains\":\n return actual.includes(expected);\n case \"not_contains\":\n return !actual.includes(expected);\n case \"equals\":\n return actual === expected;\n case \"not_equals\":\n return actual !== expected;\n case \"starts_with\":\n return actual.startsWith(expected);\n case \"ends_with\":\n return actual.endsWith(expected);\n case \"word\":\n return words(actual).includes(expected);\n case \"not_word\":\n return !words(actual).includes(expected);\n case \"is_empty\":\n return actual === \"\";\n case \"is_not_empty\":\n return actual !== \"\";\n case \"greater_than\":\n return Number(actual) > Number(expected);\n case \"greater_or_equal\":\n return Number(actual) >= Number(expected);\n case \"less_than\":\n return Number(actual) < Number(expected);\n case \"less_or_equal\":\n return Number(actual) <= Number(expected);\n case \"between\": {\n const numeric = Number(actual);\n return numeric >= Number(expected) && numeric <= Number(expectedTo);\n }\n }\n}\n\nexport function layeredFilterRuleSummary(\n rule: LayeredFilterRule,\n fields: readonly LayeredFilterField[],\n): string {\n const field = fields.find((candidate) => candidate.id === rule.field);\n const fieldLabel = field?.label ?? rule.field;\n const operator = LAYERED_FILTER_OPERATOR_LABELS[rule.operator];\n if (!layeredFilterNeedsValue(rule.operator))\n return `${fieldLabel} ${operator}`;\n const selectedLabel =\n field?.kind === \"select\"\n ? field.options.find((option) => option.value === rule.value)?.label\n : undefined;\n const value = selectedLabel ?? rule.value;\n return rule.operator === \"between\"\n ? `${fieldLabel} ${operator} ${value} and ${rule.valueTo ?? \"\"}`\n : `${fieldLabel} ${operator} ${value}`;\n}\n","/** Relative time ranges shared by every canonical table date column. */\nexport const TIME_RANGE_PRESET_VALUES = [\n \"all\", \"1m\", \"15m\", \"30m\", \"1h\", \"2h\", \"6h\", \"12h\",\n \"today\", \"yesterday\", \"this_week\", \"this_month\", \"this_year\",\n \"older_1m\", \"older_15m\", \"older_30m\", \"older_1h\", \"older_2h\", \"older_6h\", \"older_12h\",\n \"older_today\", \"older_this_week\", \"older_this_month\", \"older_this_year\",\n] as const;\n\nexport type TimeRangePreset = (typeof TIME_RANGE_PRESET_VALUES)[number];\n\nexport interface TimeRangePresetOption {\n value: TimeRangePreset;\n label: string;\n group: \"all\" | \"recent\" | \"older\" | \"calendar\";\n}\n\n/** A contiguous range: `since` is inclusive and `until` is exclusive. */\nexport interface DateFilterBounds {\n since?: string;\n until?: string;\n}\n\nexport interface DateColumnFilterValue extends DateFilterBounds {\n kind: \"date\";\n /** Date ranges are contiguous only; complement queries need a source contract first. */\n negated?: never;\n /** A relative range. Omit for explicit custom bounds. */\n preset?: TimeRangePreset;\n /** ISO instant captured when a relative preset was chosen. */\n anchor?: string;\n}\n\nexport const TIME_RANGE_PRESETS: readonly TimeRangePresetOption[] = [\n { value: \"all\", label: \"All time\", group: \"all\" },\n { value: \"1m\", label: \"Last minute\", group: \"recent\" },\n { value: \"15m\", label: \"Last 15 minutes\", group: \"recent\" },\n { value: \"30m\", label: \"Last 30 minutes\", group: \"recent\" },\n { value: \"1h\", label: \"Last hour\", group: \"recent\" },\n { value: \"2h\", label: \"Last 2 hours\", group: \"recent\" },\n { value: \"6h\", label: \"Last 6 hours\", group: \"recent\" },\n { value: \"12h\", label: \"Last 12 hours\", group: \"recent\" },\n { value: \"today\", label: \"Today\", group: \"calendar\" },\n { value: \"yesterday\", label: \"Yesterday\", group: \"calendar\" },\n { value: \"this_week\", label: \"This week\", group: \"calendar\" },\n { value: \"this_month\", label: \"This month\", group: \"calendar\" },\n { value: \"this_year\", label: \"This year\", group: \"calendar\" },\n { value: \"older_1m\", label: \"Older than 1 minute\", group: \"older\" },\n { value: \"older_15m\", label: \"Older than 15 minutes\", group: \"older\" },\n { value: \"older_30m\", label: \"Older than 30 minutes\", group: \"older\" },\n { value: \"older_1h\", label: \"Older than 1 hour\", group: \"older\" },\n { value: \"older_2h\", label: \"Older than 2 hours\", group: \"older\" },\n { value: \"older_6h\", label: \"Older than 6 hours\", group: \"older\" },\n { value: \"older_12h\", label: \"Older than 12 hours\", group: \"older\" },\n { value: \"older_today\", label: \"Before today\", group: \"older\" },\n { value: \"older_this_week\", label: \"Before this week\", group: \"older\" },\n { value: \"older_this_month\", label: \"Before this month\", group: \"older\" },\n { value: \"older_this_year\", label: \"Before this year\", group: \"older\" },\n];\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date);\n next.setHours(0, 0, 0, 0);\n return next;\n}\n\nfunction startOfWeek(date: Date): Date {\n const next = startOfDay(date);\n next.setDate(next.getDate() - (next.getDay() === 0 ? 6 : next.getDay() - 1));\n return next;\n}\n\nfunction startOfMonth(date: Date): Date {\n return new Date(date.getFullYear(), date.getMonth(), 1);\n}\n\nfunction startOfYear(date: Date): Date {\n return new Date(date.getFullYear(), 0, 1);\n}\n\nfunction validDate(value: Date | string | undefined): Date | undefined {\n if (value === undefined) return undefined;\n const date = value instanceof Date ? new Date(value) : new Date(value);\n return Number.isNaN(date.valueOf()) ? undefined : date;\n}\n\n/**\n * Resolve a date filter without reading the clock implicitly. Relative presets\n * use their captured `anchor`; callers may provide `now` only for legacy or\n * programmatic values without one. `until` remains exclusive throughout.\n */\nexport function dateFilterBounds(\n filter: DateColumnFilterValue,\n now: Date | string = new Date(),\n): DateFilterBounds {\n if (!filter.preset) {\n return {\n ...(filter.since === undefined ? {} : { since: filter.since }),\n ...(filter.until === undefined ? {} : { until: filter.until }),\n };\n }\n if (filter.preset === \"all\") return {};\n\n const anchor = validDate(filter.anchor) ?? validDate(now) ?? new Date();\n const ago = (milliseconds: number) =>\n new Date(anchor.valueOf() - milliseconds).toISOString();\n const recent: Partial<Record<TimeRangePreset, number>> = {\n \"1m\": 60_000,\n \"15m\": 15 * 60_000,\n \"30m\": 30 * 60_000,\n \"1h\": 60 * 60_000,\n \"2h\": 2 * 60 * 60_000,\n \"6h\": 6 * 60 * 60_000,\n \"12h\": 12 * 60 * 60_000,\n };\n const duration = recent[filter.preset.replace(\"older_\", \"\") as TimeRangePreset];\n if (duration !== undefined) {\n return filter.preset.startsWith(\"older_\") ? { until: ago(duration) } : { since: ago(duration) };\n }\n\n const starts: Partial<Record<TimeRangePreset, Date>> = {\n today: startOfDay(anchor),\n this_week: startOfWeek(anchor),\n this_month: startOfMonth(anchor),\n this_year: startOfYear(anchor),\n older_today: startOfDay(anchor),\n older_this_week: startOfWeek(anchor),\n older_this_month: startOfMonth(anchor),\n older_this_year: startOfYear(anchor),\n };\n if (filter.preset === \"yesterday\") {\n const until = startOfDay(anchor);\n const since = new Date(until);\n since.setDate(since.getDate() - 1);\n return { since: since.toISOString(), until: until.toISOString() };\n }\n const start = starts[filter.preset];\n if (!start) return {};\n return filter.preset.startsWith(\"older_\")\n ? { until: start.toISOString() }\n : { since: start.toISOString() };\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-CmQrOz-T.cjs';
1
+ import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-jQddSUnj.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -7,8 +7,10 @@ import './copy-types.cjs';
7
7
  import '../types-CJWU1Gae.cjs';
8
8
  import './grouping.cjs';
9
9
  import './fill.cjs';
10
+ import './facets.cjs';
10
11
  import './spreadsheet.cjs';
11
12
  import './grid-selection.cjs';
13
+ import './date-filter.cjs';
12
14
 
13
15
  /**
14
16
  * Sentinel select-filter values for "no value" / "any value" — every select
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-nlLCMiqd.js';
1
+ import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../types-D9JBR6ZU.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -7,8 +7,10 @@ import './copy-types.js';
7
7
  import '../types-CJWU1Gae.js';
8
8
  import './grouping.js';
9
9
  import './fill.js';
10
+ import './facets.js';
10
11
  import './spreadsheet.js';
11
12
  import './grid-selection.js';
13
+ import './date-filter.js';
12
14
 
13
15
  /**
14
16
  * Sentinel select-filter values for "no value" / "any value" — every select
@@ -84,6 +84,72 @@ function layeredFilterMatchesValue(value, rule) {
84
84
  }
85
85
  }
86
86
 
87
+ // src/data-table/date-filter.ts
88
+ function startOfDay(date) {
89
+ const next = new Date(date);
90
+ next.setHours(0, 0, 0, 0);
91
+ return next;
92
+ }
93
+ function startOfWeek(date) {
94
+ const next = startOfDay(date);
95
+ next.setDate(next.getDate() - (next.getDay() === 0 ? 6 : next.getDay() - 1));
96
+ return next;
97
+ }
98
+ function startOfMonth(date) {
99
+ return new Date(date.getFullYear(), date.getMonth(), 1);
100
+ }
101
+ function startOfYear(date) {
102
+ return new Date(date.getFullYear(), 0, 1);
103
+ }
104
+ function validDate(value) {
105
+ if (value === void 0) return void 0;
106
+ const date = value instanceof Date ? new Date(value) : new Date(value);
107
+ return Number.isNaN(date.valueOf()) ? void 0 : date;
108
+ }
109
+ function dateFilterBounds(filter, now = /* @__PURE__ */ new Date()) {
110
+ if (!filter.preset) {
111
+ return {
112
+ ...filter.since === void 0 ? {} : { since: filter.since },
113
+ ...filter.until === void 0 ? {} : { until: filter.until }
114
+ };
115
+ }
116
+ if (filter.preset === "all") return {};
117
+ const anchor = validDate(filter.anchor) ?? validDate(now) ?? /* @__PURE__ */ new Date();
118
+ const ago = (milliseconds) => new Date(anchor.valueOf() - milliseconds).toISOString();
119
+ const recent = {
120
+ "1m": 6e4,
121
+ "15m": 15 * 6e4,
122
+ "30m": 30 * 6e4,
123
+ "1h": 60 * 6e4,
124
+ "2h": 2 * 60 * 6e4,
125
+ "6h": 6 * 60 * 6e4,
126
+ "12h": 12 * 60 * 6e4
127
+ };
128
+ const duration = recent[filter.preset.replace("older_", "")];
129
+ if (duration !== void 0) {
130
+ return filter.preset.startsWith("older_") ? { until: ago(duration) } : { since: ago(duration) };
131
+ }
132
+ const starts = {
133
+ today: startOfDay(anchor),
134
+ this_week: startOfWeek(anchor),
135
+ this_month: startOfMonth(anchor),
136
+ this_year: startOfYear(anchor),
137
+ older_today: startOfDay(anchor),
138
+ older_this_week: startOfWeek(anchor),
139
+ older_this_month: startOfMonth(anchor),
140
+ older_this_year: startOfYear(anchor)
141
+ };
142
+ if (filter.preset === "yesterday") {
143
+ const until = startOfDay(anchor);
144
+ const since = new Date(until);
145
+ since.setDate(since.getDate() - 1);
146
+ return { since: since.toISOString(), until: until.toISOString() };
147
+ }
148
+ const start = starts[filter.preset];
149
+ if (!start) return {};
150
+ return filter.preset.startsWith("older_") ? { until: start.toISOString() } : { since: start.toISOString() };
151
+ }
152
+
87
153
  // src/data-table/filter-engine.ts
88
154
  var SELECT_EMPTY_VALUE = "__empty__";
89
155
  var SELECT_NOT_EMPTY_VALUE = "__not_empty__";
@@ -186,6 +252,21 @@ function passesColumnFilter(value, filter) {
186
252
  if (filter.max !== void 0 && n > filter.max) return false;
187
253
  return true;
188
254
  }
255
+ case "date": {
256
+ const raw = value instanceof Date ? value : new Date(stringifyCellValue(value));
257
+ if (Number.isNaN(raw.valueOf())) return false;
258
+ const { since, until } = dateFilterBounds(filter);
259
+ const timestamp = raw.valueOf();
260
+ if (since !== void 0) {
261
+ const sinceTimestamp = new Date(since).valueOf();
262
+ if (Number.isNaN(sinceTimestamp) || timestamp < sinceTimestamp) return false;
263
+ }
264
+ if (until !== void 0) {
265
+ const untilTimestamp = new Date(until).valueOf();
266
+ if (Number.isNaN(untilTimestamp) || timestamp >= untilTimestamp) return false;
267
+ }
268
+ return true;
269
+ }
189
270
  }
190
271
  })();
191
272
  return filter.negated ? !matches : matches;
@@ -263,6 +344,9 @@ function countActiveColumnFilters(filters) {
263
344
  case "number":
264
345
  if (f.min !== void 0 || f.max !== void 0) n += 1;
265
346
  break;
347
+ case "date":
348
+ if (isColumnFilterActive(f)) n += 1;
349
+ break;
266
350
  }
267
351
  }
268
352
  return n;
@@ -278,6 +362,10 @@ function isColumnFilterActive(filter) {
278
362
  return true;
279
363
  case "number":
280
364
  return filter.min !== void 0 || filter.max !== void 0;
365
+ case "date": {
366
+ const bounds = dateFilterBounds(filter);
367
+ return bounds.since !== void 0 || bounds.until !== void 0;
368
+ }
281
369
  }
282
370
  }
283
371
  function applyRowEdits(row, edits) {