@ai-matrx/design-system 0.49.11 → 0.49.18

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 (39) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/data-table/copy-helpers.d.cts +1 -1
  3. package/dist/data-table/copy-helpers.d.ts +1 -1
  4. package/dist/data-table/facets.cjs +2 -0
  5. package/dist/data-table/facets.cjs.map +1 -1
  6. package/dist/data-table/facets.d.cts +3 -1
  7. package/dist/data-table/facets.d.ts +3 -1
  8. package/dist/data-table/facets.js +2 -0
  9. package/dist/data-table/facets.js.map +1 -1
  10. package/dist/data-table/filter-engine.d.cts +1 -1
  11. package/dist/data-table/filter-engine.d.ts +1 -1
  12. package/dist/data-table/host.d.cts +1 -1
  13. package/dist/data-table/host.d.ts +1 -1
  14. package/dist/data-table/index.cjs +505 -328
  15. package/dist/data-table/index.cjs.map +1 -1
  16. package/dist/data-table/index.d.cts +7 -3
  17. package/dist/data-table/index.d.ts +7 -3
  18. package/dist/data-table/index.js +436 -259
  19. package/dist/data-table/index.js.map +1 -1
  20. package/dist/data-table/infer-filter.d.cts +1 -1
  21. package/dist/data-table/infer-filter.d.ts +1 -1
  22. package/dist/data-table/layered-filters.d.cts +1 -1
  23. package/dist/data-table/layered-filters.d.ts +1 -1
  24. package/dist/data-table/menu-targets.cjs.map +1 -1
  25. package/dist/data-table/menu-targets.d.cts +5 -0
  26. package/dist/data-table/menu-targets.d.ts +5 -0
  27. package/dist/data-table/menu-targets.js.map +1 -1
  28. package/dist/data-table/query-control.d.cts +1 -1
  29. package/dist/data-table/query-control.d.ts +1 -1
  30. package/dist/data-table/types.cjs.map +1 -1
  31. package/dist/data-table/types.d.cts +1 -1
  32. package/dist/data-table/types.d.ts +1 -1
  33. package/dist/data-table/url-state.d.cts +1 -1
  34. package/dist/data-table/url-state.d.ts +1 -1
  35. package/dist/data-table/xlsx.d.cts +1 -1
  36. package/dist/data-table/xlsx.d.ts +1 -1
  37. package/dist/{layered-filters-B-znwvFV.d.cts → layered-filters-BltsILT3.d.cts} +7 -1
  38. package/dist/{layered-filters-Dhfam7uI.d.ts → layered-filters-DrGJ_nuG.d.ts} +7 -1
  39. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.49.18
4
+ ### Unreleased
5
+
6
+ - **A single-row toolbar's pinned controls are whole or behind "…" at every container width** (merged-grid leftovers, 2026-09-28: "New record" read "N" in a chat artifact and a narrow table window). The pinned group is now the toolbar row's own last child (sticky could not carry it out of the actions box), a `singleRow` row is `max-w-full` (inside a host's slot it is the scroller), and `usePinnedFold` measures the row — the container, never the viewport — folding the controls, the pinned ones and the extras behind ONE "…" when the group cannot be drawn whole. Proof: `MatrxDataTable.pinned-whole-or-folded.test.tsx`, red on 0.49.13 (4 of 5). Consumer action: none.
7
+
8
+ ## 0.49.17
9
+
10
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
11
+ `git diff npm/design-system/v0.49.12..npm/design-system/v0.49.17 -- apps/shared/design-system`).
12
+ No source changes intended and no consumer action required.
13
+
14
+ ## 0.49.16
15
+
16
+ - **Absent table rows keep a stable source identity.** `MatrxDataTable` reuses one empty-array sentinel when neither supplied data nor an appended query supplies rows, preventing data-dependent effects from seeing a new source on every render and scheduling an infinite loop. No consumer action required.
17
+
18
+ ## 0.49.15
19
+
20
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
21
+ `git diff npm/design-system/v0.49.12..npm/design-system/v0.49.15 -- apps/shared/design-system`).
22
+ No source changes intended and no consumer action required.
23
+
24
+ ## 0.49.14
25
+
26
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
27
+ `git diff npm/design-system/v0.49.12..npm/design-system/v0.49.14 -- apps/shared/design-system`).
28
+ No source changes intended and no consumer action required.
29
+
30
+ ## 0.49.13
31
+
32
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
33
+ `git diff npm/design-system/v0.49.12..npm/design-system/v0.49.13 -- apps/shared/design-system`).
34
+ No source changes intended and no consumer action required.
35
+
36
+ ## 0.49.12
37
+
38
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
39
+ `git diff npm/design-system/v0.49.11..npm/design-system/v0.49.12 -- apps/shared/design-system`).
40
+ No source changes intended and no consumer action required.
41
+
3
42
  ## 0.49.11
4
43
 
5
44
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -1,5 +1,5 @@
1
1
  import { AgentPayloadInput } from './copy-types.cjs';
2
- import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../layered-filters-B-znwvFV.cjs';
2
+ import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../layered-filters-BltsILT3.cjs';
3
3
  import '../content-transfer.cjs';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
@@ -1,5 +1,5 @@
1
1
  import { AgentPayloadInput } from './copy-types.js';
2
- import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../layered-filters-Dhfam7uI.js';
2
+ import { g as MatrxDataTableCopyConfig, M as MatrxColumnDef } from '../layered-filters-DrGJ_nuG.js';
3
3
  import '../content-transfer.js';
4
4
  import 'react';
5
5
  import '@ai-matrx/kit/content-transfer';
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/data-table/facets.ts
21
21
  var facets_exports = {};
22
22
  __export(facets_exports, {
23
+ CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT: () => CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT,
23
24
  FACETS_LOADING_NOTICE: () => FACETS_LOADING_NOTICE,
24
25
  MAX_LISTABLE_FACET_LENGTH: () => MAX_LISTABLE_FACET_LENGTH,
25
26
  computeColumnFacets: () => computeColumnFacets,
@@ -31,6 +32,7 @@ module.exports = __toCommonJS(facets_exports);
31
32
  var MAX_LISTABLE_FACET_LENGTH = 300;
32
33
  var DEFAULT_FACET_LIMIT = 200;
33
34
  var MAX_FACET_LIMIT = 500;
35
+ var CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;
34
36
  function facetText(raw) {
35
37
  if (raw === null || raw === void 0) return "";
36
38
  if (typeof raw === "object") {
@@ -1 +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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmEO,IAAM,4BAA4B;AAEzC,IAAM,sBAAsB;AAC5B,IAAM,kBAAkB;AAExB,SAAS,UAAU,KAAsB;AACvC,MAAI,QAAQ,QAAQ,QAAQ,OAAW,QAAO;AAC9C,MAAI,OAAO,QAAQ,UAAU;AAC3B,QAAI;AACF,aAAO,KAAK,UAAU,GAAG;AAAA,IAC3B,QAAQ;AACN,aAAO,OAAO,GAAG;AAAA,IACnB;AAAA,EACF;AACA,SAAO,OAAO,GAAG;AACnB;AAUO,SAAS,oBAAoB,MAMnB;AACf,QAAM,EAAE,UAAU,MAAM,UAAU,IAAI;AACtC,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,SAAS,qBAAqB,CAAC,GAAG,eAAe;AAEtF,QAAM,SAAS,oBAAI,IAAoB;AACvC,QAAM,aAAa,oBAAI,IAAY;AACnC,MAAI,QAAQ;AACZ,MAAI,YAAY;AAEhB,aAAW,OAAO,MAAM;AACtB,QAAI;AACJ,QAAI;AACF,YAAM,UAAU,KAAK,QAAQ;AAAA,IAC/B,QAAQ;AACN,YAAM;AAAA,IACR;AACA,UAAM,UAAU,UAAU,GAAG,EAAE,KAAK;AACpC,QAAI,YAAY,IAAI;AAClB,eAAS;AACT;AAAA,IACF;AACA,QAAI,QAAQ,SAAS,UAAW,aAAY,QAAQ;AACpD,QAAI,QAAQ,SAAS,2BAA2B;AAC9C,iBAAW,IAAI,OAAO;AACtB;AAAA,IACF;AACA,WAAO,IAAI,UAAU,OAAO,IAAI,OAAO,KAAK,KAAK,CAAC;AAAA,EACpD;AAEA,QAAM,SAA6B,CAAC,GAAG,OAAO,QAAQ,CAAC,EACpD,IAAI,CAAC,CAAC,OAAO,KAAK,OAAO,EAAE,OAAO,MAAM,EAAE,EAC1C,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,cAAc,EAAE,KAAK,CAAC,EAClE,MAAM,GAAG,KAAK;AAEjB,SAAO;AAAA,IACL;AAAA,IACA,WAAW,KAAK;AAAA,IAChB,QAAQ,KAAK,SAAS;AAAA,IACtB;AAAA,IACA,eAAe,OAAO,OAAO,WAAW;AAAA,IACxC;AAAA,IACA,YAAY,WAAW;AAAA,IACvB;AAAA,IACA,WAAW,OAAO,OAAO;AAAA,IACzB;AAAA,IACA,YAAY;AAAA,IACZ,UAAU,KAAK,YAAY;AAAA,EAC7B;AACF;AAUO,SAAS,uBACd,gBACA,OACS;AACT,MAAI,UAAU,UAAa,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,EAAG,QAAO;AACxE,SAAO,kBAAkB;AAC3B;AAEA,IAAM,UAAU,IAAI,KAAK,aAAa,OAAO;AAE7C,SAAS,EAAE,OAAuB;AAChC,MAAI;AACF,WAAO,QAAQ,OAAO,KAAK;AAAA,EAC7B,QAAQ;AACN,WAAO,OAAO,KAAK;AAAA,EACrB;AACF;AAOO,SAAS,oBACd,QACA,OAAO,OACC;AACR,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,SAAS,GAAG,IAAI;AACtB,MAAI,CAAC,OAAO,UAAU;AACpB,WAAO,0BAA0B,EAAE,OAAO,SAAS,CAAC,IAAI,MAAM;AAAA,EAChE;AACA,MAAI,OAAO,WAAW;AACpB,WAAO,eAAe,EAAE,OAAO,OAAO,MAAM,CAAC,mBAAmB,EAAE,OAAO,aAAa,CAAC;AAAA,EACzF;AACA,MAAI,OAAO,aAAa,GAAG;AACzB,WAAO,GAAG,EAAE,OAAO,UAAU,CAAC,IAAI,OAAO,eAAe,IAAI,aAAa,YAAY,kDAAkD,OAAO,eAAe,IAAI,OAAO,MAAM;AAAA,EAChL;AACA,SAAO;AACT;AAGO,IAAM,wBAAwB;AAC9B,SAAS,qBAAqB,OAAwB;AAC3D,QAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,SAAS,EAAE;AAC1E,SAAO,SACH,mDAAmD,MAAM,yBACzD;AACN;","names":[]}
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\n/** Core loaded-row suggestions stop at this exclusive distinct-value boundary. */\nexport const CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;\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":[]}
@@ -63,6 +63,8 @@ interface ColumnFacets {
63
63
  }
64
64
  /** A value longer than this is real data, not a pickable option. */
65
65
  declare const MAX_LISTABLE_FACET_LENGTH = 300;
66
+ /** Core loaded-row suggestions stop at this exclusive distinct-value boundary. */
67
+ declare const CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;
66
68
  /**
67
69
  * Compute a column's facets from rows already in memory.
68
70
  *
@@ -97,4 +99,4 @@ declare function describeFacetSource(facets: ColumnFacets | null | undefined, no
97
99
  declare const FACETS_LOADING_NOTICE = "Counting values\u2026";
98
100
  declare function describeFacetFailure(error: unknown): string;
99
101
 
100
- export { type ColumnFacetValue, type ColumnFacets, FACETS_LOADING_NOTICE, MAX_LISTABLE_FACET_LENGTH, computeColumnFacets, describeFacetFailure, describeFacetSource, localFacetsAreComplete };
102
+ export { CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT, type ColumnFacetValue, type ColumnFacets, FACETS_LOADING_NOTICE, MAX_LISTABLE_FACET_LENGTH, computeColumnFacets, describeFacetFailure, describeFacetSource, localFacetsAreComplete };
@@ -63,6 +63,8 @@ interface ColumnFacets {
63
63
  }
64
64
  /** A value longer than this is real data, not a pickable option. */
65
65
  declare const MAX_LISTABLE_FACET_LENGTH = 300;
66
+ /** Core loaded-row suggestions stop at this exclusive distinct-value boundary. */
67
+ declare const CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;
66
68
  /**
67
69
  * Compute a column's facets from rows already in memory.
68
70
  *
@@ -97,4 +99,4 @@ declare function describeFacetSource(facets: ColumnFacets | null | undefined, no
97
99
  declare const FACETS_LOADING_NOTICE = "Counting values\u2026";
98
100
  declare function describeFacetFailure(error: unknown): string;
99
101
 
100
- export { type ColumnFacetValue, type ColumnFacets, FACETS_LOADING_NOTICE, MAX_LISTABLE_FACET_LENGTH, computeColumnFacets, describeFacetFailure, describeFacetSource, localFacetsAreComplete };
102
+ export { CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT, type ColumnFacetValue, type ColumnFacets, FACETS_LOADING_NOTICE, MAX_LISTABLE_FACET_LENGTH, computeColumnFacets, describeFacetFailure, describeFacetSource, localFacetsAreComplete };
@@ -3,6 +3,7 @@
3
3
  var MAX_LISTABLE_FACET_LENGTH = 300;
4
4
  var DEFAULT_FACET_LIMIT = 200;
5
5
  var MAX_FACET_LIMIT = 500;
6
+ var CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;
6
7
  function facetText(raw) {
7
8
  if (raw === null || raw === void 0) return "";
8
9
  if (typeof raw === "object") {
@@ -88,6 +89,7 @@ function describeFacetFailure(error) {
88
89
  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
  }
90
91
  export {
92
+ CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT,
91
93
  FACETS_LOADING_NOTICE,
92
94
  MAX_LISTABLE_FACET_LENGTH,
93
95
  computeColumnFacets,
@@ -1 +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":[]}
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\n/** Core loaded-row suggestions stop at this exclusive distinct-value boundary. */\nexport const CORE_SUGGESTION_DISTINCT_EXCLUSIVE_LIMIT = 15;\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":[]}
@@ -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 '../layered-filters-B-znwvFV.cjs';
1
+ import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../layered-filters-BltsILT3.cjs';
2
2
  import '../content-transfer.cjs';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -1,4 +1,4 @@
1
- import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../layered-filters-Dhfam7uI.js';
1
+ import { M as MatrxColumnDef, C as ColumnFiltersState, S as SortState, L as LayeredFilterRule, T as TableSearchMatchMode, a as ColumnFilterValue } from '../layered-filters-DrGJ_nuG.js';
2
2
  import '../content-transfer.js';
3
3
  import 'react';
4
4
  import '@ai-matrx/kit/content-transfer';
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { ComponentType, ReactNode, MouseEventHandler } from 'react';
3
3
  import { CopyControlProps } from './copy-types.cjs';
4
4
  export { AgentPayloadInput, AiCustomSource, AiVariant, CopyExportConfig } from './copy-types.cjs';
5
- import { b as TableSavedViewsProps, c as MatrxDataTableDensity, d as MatrxDataTableRecordControls } from '../layered-filters-B-znwvFV.cjs';
5
+ import { b as TableSavedViewsProps, c as MatrxDataTableDensity, d as MatrxDataTableRecordControls } from '../layered-filters-BltsILT3.cjs';
6
6
  import { MatrxTableMenuTarget, MatrxTableMenuPayload, MatrxTableMenuSectionsResolver, MatrxTableMenuIconName } from './menu-targets.cjs';
7
7
  export { MatrxTableMenuItem, MatrxTableMenuSection } from './menu-targets.cjs';
8
8
  import '../content-transfer.cjs';
@@ -2,7 +2,7 @@ import * as React from 'react';
2
2
  import { ComponentType, ReactNode, MouseEventHandler } from 'react';
3
3
  import { CopyControlProps } from './copy-types.js';
4
4
  export { AgentPayloadInput, AiCustomSource, AiVariant, CopyExportConfig } from './copy-types.js';
5
- import { b as TableSavedViewsProps, c as MatrxDataTableDensity, d as MatrxDataTableRecordControls } from '../layered-filters-Dhfam7uI.js';
5
+ import { b as TableSavedViewsProps, c as MatrxDataTableDensity, d as MatrxDataTableRecordControls } from '../layered-filters-DrGJ_nuG.js';
6
6
  import { MatrxTableMenuTarget, MatrxTableMenuPayload, MatrxTableMenuSectionsResolver, MatrxTableMenuIconName } from './menu-targets.js';
7
7
  export { MatrxTableMenuItem, MatrxTableMenuSection } from './menu-targets.js';
8
8
  import '../content-transfer.js';