@ai-matrx/design-system 0.22.0 → 0.26.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.
- package/CHANGELOG.md +227 -0
- package/dist/data-table/cell-undo.cjs +98 -0
- package/dist/data-table/cell-undo.cjs.map +1 -0
- package/dist/data-table/cell-undo.d.cts +85 -0
- package/dist/data-table/cell-undo.d.ts +85 -0
- package/dist/data-table/cell-undo.js +78 -0
- package/dist/data-table/cell-undo.js.map +1 -0
- package/dist/data-table/copy-helpers.d.cts +7 -2
- package/dist/data-table/copy-helpers.d.ts +7 -2
- package/dist/data-table/coverage.cjs +116 -0
- package/dist/data-table/coverage.cjs.map +1 -0
- package/dist/data-table/coverage.d.cts +65 -0
- package/dist/data-table/coverage.d.ts +65 -0
- package/dist/data-table/coverage.js +96 -0
- package/dist/data-table/coverage.js.map +1 -0
- package/dist/data-table/data-row-inspector.cjs.map +1 -1
- package/dist/data-table/data-row-inspector.js.map +1 -1
- package/dist/data-table/data-row-window.cjs.map +1 -1
- package/dist/data-table/data-row-window.js.map +1 -1
- package/dist/data-table/facets.cjs +118 -0
- package/dist/data-table/facets.cjs.map +1 -0
- package/dist/data-table/facets.d.cts +100 -0
- package/dist/data-table/facets.d.ts +100 -0
- package/dist/data-table/facets.js +98 -0
- package/dist/data-table/facets.js.map +1 -0
- package/dist/data-table/fill.cjs +299 -0
- package/dist/data-table/fill.cjs.map +1 -0
- package/dist/data-table/fill.d.cts +129 -0
- package/dist/data-table/fill.d.ts +129 -0
- package/dist/data-table/fill.js +277 -0
- package/dist/data-table/fill.js.map +1 -0
- package/dist/data-table/filter-engine.d.cts +7 -2
- package/dist/data-table/filter-engine.d.ts +7 -2
- package/dist/data-table/grid-clipboard.cjs +165 -0
- package/dist/data-table/grid-clipboard.cjs.map +1 -0
- package/dist/data-table/grid-clipboard.d.cts +85 -0
- package/dist/data-table/grid-clipboard.d.ts +85 -0
- package/dist/data-table/grid-clipboard.js +145 -0
- package/dist/data-table/grid-clipboard.js.map +1 -0
- package/dist/data-table/grid-selection.cjs +258 -0
- package/dist/data-table/grid-selection.cjs.map +1 -0
- package/dist/data-table/grid-selection.d.cts +166 -0
- package/dist/data-table/grid-selection.d.ts +166 -0
- package/dist/data-table/grid-selection.js +238 -0
- package/dist/data-table/grid-selection.js.map +1 -0
- package/dist/data-table/grouping.cjs +302 -0
- package/dist/data-table/grouping.cjs.map +1 -0
- package/dist/data-table/grouping.d.cts +119 -0
- package/dist/data-table/grouping.d.ts +119 -0
- package/dist/data-table/grouping.js +282 -0
- package/dist/data-table/grouping.js.map +1 -0
- package/dist/data-table/host.cjs.map +1 -1
- package/dist/data-table/host.d.cts +18 -4
- package/dist/data-table/host.d.ts +18 -4
- package/dist/data-table/host.js.map +1 -1
- package/dist/data-table/index.cjs +3173 -451
- package/dist/data-table/index.cjs.map +1 -1
- package/dist/data-table/index.d.cts +13 -3
- package/dist/data-table/index.d.ts +13 -3
- package/dist/data-table/index.js +3174 -451
- package/dist/data-table/index.js.map +1 -1
- package/dist/data-table/infer-filter.d.cts +7 -2
- package/dist/data-table/infer-filter.d.ts +7 -2
- package/dist/data-table/layered-filters.d.cts +7 -2
- package/dist/data-table/layered-filters.d.ts +7 -2
- package/dist/data-table/menu-targets.cjs +77 -0
- package/dist/data-table/menu-targets.cjs.map +1 -0
- package/dist/data-table/menu-targets.d.cts +108 -0
- package/dist/data-table/menu-targets.d.ts +108 -0
- package/dist/data-table/menu-targets.js +57 -0
- package/dist/data-table/menu-targets.js.map +1 -0
- package/dist/data-table/pagination.cjs.map +1 -1
- package/dist/data-table/pagination.js.map +1 -1
- package/dist/data-table/query-control.d.cts +7 -2
- package/dist/data-table/query-control.d.ts +7 -2
- package/dist/data-table/spreadsheet.cjs +1062 -0
- package/dist/data-table/spreadsheet.cjs.map +1 -0
- package/dist/data-table/spreadsheet.d.cts +153 -0
- package/dist/data-table/spreadsheet.d.ts +153 -0
- package/dist/data-table/spreadsheet.js +1042 -0
- package/dist/data-table/spreadsheet.js.map +1 -0
- package/dist/data-table/types.cjs.map +1 -1
- package/dist/data-table/types.d.cts +7 -2
- package/dist/data-table/types.d.ts +7 -2
- package/dist/data-table/url-state.d.cts +7 -2
- package/dist/data-table/url-state.d.ts +7 -2
- package/dist/data-table/uuid-cell.cjs.map +1 -1
- package/dist/data-table/uuid-cell.js.map +1 -1
- package/dist/data-table/validation.cjs +229 -0
- package/dist/data-table/validation.cjs.map +1 -0
- package/dist/data-table/validation.d.cts +125 -0
- package/dist/data-table/validation.d.ts +125 -0
- package/dist/data-table/validation.js +209 -0
- package/dist/data-table/validation.js.map +1 -0
- package/dist/data-table/virtual-window.cjs +203 -0
- package/dist/data-table/virtual-window.cjs.map +1 -0
- package/dist/data-table/virtual-window.d.cts +84 -0
- package/dist/data-table/virtual-window.d.ts +84 -0
- package/dist/data-table/virtual-window.js +183 -0
- package/dist/data-table/virtual-window.js.map +1 -0
- package/dist/data-table/xlsx.d.cts +7 -2
- package/dist/data-table/xlsx.d.ts +7 -2
- package/dist/field-formats/index.d.cts +2 -2
- package/dist/field-formats/index.d.ts +2 -2
- package/dist/formulas/index.cjs +1041 -0
- package/dist/formulas/index.cjs.map +1 -0
- package/dist/formulas/index.d.cts +389 -0
- package/dist/formulas/index.d.ts +389 -0
- package/dist/formulas/index.js +1019 -0
- package/dist/formulas/index.js.map +1 -0
- package/dist/{types-BdbvlF9-.d.cts → types-CJWU1Gae.d.cts} +1 -1
- package/dist/{types-BdbvlF9-.d.ts → types-CJWU1Gae.d.ts} +1 -1
- package/dist/{types-D-flTpK0.d.ts → types-Cu5_eUG1.d.ts} +268 -4
- package/dist/{types-Sn249eoy.d.cts → types-DrJXAAoD.d.cts} +268 -4
- package/package.json +129 -2
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/data-table/facets.ts
|
|
21
|
+
var facets_exports = {};
|
|
22
|
+
__export(facets_exports, {
|
|
23
|
+
FACETS_LOADING_NOTICE: () => FACETS_LOADING_NOTICE,
|
|
24
|
+
MAX_LISTABLE_FACET_LENGTH: () => MAX_LISTABLE_FACET_LENGTH,
|
|
25
|
+
computeColumnFacets: () => computeColumnFacets,
|
|
26
|
+
describeFacetFailure: () => describeFacetFailure,
|
|
27
|
+
describeFacetSource: () => describeFacetSource,
|
|
28
|
+
localFacetsAreComplete: () => localFacetsAreComplete
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(facets_exports);
|
|
31
|
+
var MAX_LISTABLE_FACET_LENGTH = 300;
|
|
32
|
+
var DEFAULT_FACET_LIMIT = 200;
|
|
33
|
+
var MAX_FACET_LIMIT = 500;
|
|
34
|
+
function facetText(raw) {
|
|
35
|
+
if (raw === null || raw === void 0) return "";
|
|
36
|
+
if (typeof raw === "object") {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(raw);
|
|
39
|
+
} catch {
|
|
40
|
+
return String(raw);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return String(raw);
|
|
44
|
+
}
|
|
45
|
+
function computeColumnFacets(args) {
|
|
46
|
+
const { columnId, rows, readValue } = args;
|
|
47
|
+
const limit = Math.min(Math.max(args.limit ?? DEFAULT_FACET_LIMIT, 1), MAX_FACET_LIMIT);
|
|
48
|
+
const counts = /* @__PURE__ */ new Map();
|
|
49
|
+
const longValues = /* @__PURE__ */ new Set();
|
|
50
|
+
let blank = 0;
|
|
51
|
+
let maxLength = 0;
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
let raw;
|
|
54
|
+
try {
|
|
55
|
+
raw = readValue(row, columnId);
|
|
56
|
+
} catch {
|
|
57
|
+
raw = void 0;
|
|
58
|
+
}
|
|
59
|
+
const trimmed = facetText(raw).trim();
|
|
60
|
+
if (trimmed === "") {
|
|
61
|
+
blank += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (trimmed.length > maxLength) maxLength = trimmed.length;
|
|
65
|
+
if (trimmed.length > MAX_LISTABLE_FACET_LENGTH) {
|
|
66
|
+
longValues.add(trimmed);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
70
|
+
}
|
|
71
|
+
const values = [...counts.entries()].map(([value, count]) => ({ value, count })).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)).slice(0, limit);
|
|
72
|
+
return {
|
|
73
|
+
columnId,
|
|
74
|
+
totalRows: rows.length,
|
|
75
|
+
filled: rows.length - blank,
|
|
76
|
+
blank,
|
|
77
|
+
distinctCount: counts.size + longValues.size,
|
|
78
|
+
maxLength,
|
|
79
|
+
unlistable: longValues.size,
|
|
80
|
+
limit,
|
|
81
|
+
truncated: counts.size > limit,
|
|
82
|
+
values,
|
|
83
|
+
answeredBy: "local",
|
|
84
|
+
complete: args.complete ?? true
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function localFacetsAreComplete(loadedRowCount, total) {
|
|
88
|
+
if (total === void 0 || !Number.isFinite(total) || total < 0) return false;
|
|
89
|
+
return loadedRowCount >= total;
|
|
90
|
+
}
|
|
91
|
+
var NUMBERS = new Intl.NumberFormat("en-US");
|
|
92
|
+
function n(value) {
|
|
93
|
+
try {
|
|
94
|
+
return NUMBERS.format(value);
|
|
95
|
+
} catch {
|
|
96
|
+
return String(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function describeFacetSource(facets, noun = "row") {
|
|
100
|
+
if (!facets) return "";
|
|
101
|
+
const plural = `${noun}s`;
|
|
102
|
+
if (!facets.complete) {
|
|
103
|
+
return `These counts cover the ${n(facets.totalRows)} ${plural} loaded here, not the whole table.`;
|
|
104
|
+
}
|
|
105
|
+
if (facets.truncated) {
|
|
106
|
+
return `Showing the ${n(facets.values.length)} most common of ${n(facets.distinctCount)} values.`;
|
|
107
|
+
}
|
|
108
|
+
if (facets.unlistable > 0) {
|
|
109
|
+
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"}.`;
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
var FACETS_LOADING_NOTICE = "Counting values\u2026";
|
|
114
|
+
function describeFacetFailure(error) {
|
|
115
|
+
const detail = error instanceof Error ? error.message : String(error ?? "");
|
|
116
|
+
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.";
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=facets.cjs.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":[]}
|
|
@@ -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,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":[]}
|