@ai-matrx/design-system 0.21.14 → 0.25.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 (97) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/data-table/cell-undo.cjs +98 -0
  3. package/dist/data-table/cell-undo.cjs.map +1 -0
  4. package/dist/data-table/cell-undo.d.cts +85 -0
  5. package/dist/data-table/cell-undo.d.ts +85 -0
  6. package/dist/data-table/cell-undo.js +78 -0
  7. package/dist/data-table/cell-undo.js.map +1 -0
  8. package/dist/data-table/copy-helpers.d.cts +6 -1
  9. package/dist/data-table/copy-helpers.d.ts +6 -1
  10. package/dist/data-table/coverage.cjs +116 -0
  11. package/dist/data-table/coverage.cjs.map +1 -0
  12. package/dist/data-table/coverage.d.cts +65 -0
  13. package/dist/data-table/coverage.d.ts +65 -0
  14. package/dist/data-table/coverage.js +96 -0
  15. package/dist/data-table/coverage.js.map +1 -0
  16. package/dist/data-table/fill.cjs +299 -0
  17. package/dist/data-table/fill.cjs.map +1 -0
  18. package/dist/data-table/fill.d.cts +129 -0
  19. package/dist/data-table/fill.d.ts +129 -0
  20. package/dist/data-table/fill.js +277 -0
  21. package/dist/data-table/fill.js.map +1 -0
  22. package/dist/data-table/filter-engine.d.cts +6 -1
  23. package/dist/data-table/filter-engine.d.ts +6 -1
  24. package/dist/data-table/grid-clipboard.cjs +165 -0
  25. package/dist/data-table/grid-clipboard.cjs.map +1 -0
  26. package/dist/data-table/grid-clipboard.d.cts +85 -0
  27. package/dist/data-table/grid-clipboard.d.ts +85 -0
  28. package/dist/data-table/grid-clipboard.js +145 -0
  29. package/dist/data-table/grid-clipboard.js.map +1 -0
  30. package/dist/data-table/grid-selection.cjs +258 -0
  31. package/dist/data-table/grid-selection.cjs.map +1 -0
  32. package/dist/data-table/grid-selection.d.cts +166 -0
  33. package/dist/data-table/grid-selection.d.ts +166 -0
  34. package/dist/data-table/grid-selection.js +238 -0
  35. package/dist/data-table/grid-selection.js.map +1 -0
  36. package/dist/data-table/grouping.cjs +302 -0
  37. package/dist/data-table/grouping.cjs.map +1 -0
  38. package/dist/data-table/grouping.d.cts +119 -0
  39. package/dist/data-table/grouping.d.ts +119 -0
  40. package/dist/data-table/grouping.js +282 -0
  41. package/dist/data-table/grouping.js.map +1 -0
  42. package/dist/data-table/host.d.cts +6 -1
  43. package/dist/data-table/host.d.ts +6 -1
  44. package/dist/data-table/index.cjs +4120 -806
  45. package/dist/data-table/index.cjs.map +1 -1
  46. package/dist/data-table/index.d.cts +99 -3
  47. package/dist/data-table/index.d.ts +99 -3
  48. package/dist/data-table/index.js +3971 -653
  49. package/dist/data-table/index.js.map +1 -1
  50. package/dist/data-table/infer-filter.d.cts +6 -1
  51. package/dist/data-table/infer-filter.d.ts +6 -1
  52. package/dist/data-table/layered-filters.d.cts +6 -1
  53. package/dist/data-table/layered-filters.d.ts +6 -1
  54. package/dist/data-table/query-control.d.cts +6 -1
  55. package/dist/data-table/query-control.d.ts +6 -1
  56. package/dist/data-table/spreadsheet.cjs +1014 -0
  57. package/dist/data-table/spreadsheet.cjs.map +1 -0
  58. package/dist/data-table/spreadsheet.d.cts +118 -0
  59. package/dist/data-table/spreadsheet.d.ts +118 -0
  60. package/dist/data-table/spreadsheet.js +994 -0
  61. package/dist/data-table/spreadsheet.js.map +1 -0
  62. package/dist/data-table/types.cjs.map +1 -1
  63. package/dist/data-table/types.d.cts +6 -1
  64. package/dist/data-table/types.d.ts +6 -1
  65. package/dist/data-table/url-state.d.cts +6 -1
  66. package/dist/data-table/url-state.d.ts +6 -1
  67. package/dist/data-table/validation.cjs +229 -0
  68. package/dist/data-table/validation.cjs.map +1 -0
  69. package/dist/data-table/validation.d.cts +125 -0
  70. package/dist/data-table/validation.d.ts +125 -0
  71. package/dist/data-table/validation.js +209 -0
  72. package/dist/data-table/validation.js.map +1 -0
  73. package/dist/data-table/virtual-window.cjs +203 -0
  74. package/dist/data-table/virtual-window.cjs.map +1 -0
  75. package/dist/data-table/virtual-window.d.cts +84 -0
  76. package/dist/data-table/virtual-window.d.ts +84 -0
  77. package/dist/data-table/virtual-window.js +183 -0
  78. package/dist/data-table/virtual-window.js.map +1 -0
  79. package/dist/data-table/xlsx.d.cts +6 -1
  80. package/dist/data-table/xlsx.d.ts +6 -1
  81. package/dist/field-formats/index.cjs +741 -0
  82. package/dist/field-formats/index.cjs.map +1 -0
  83. package/dist/field-formats/index.d.cts +45 -0
  84. package/dist/field-formats/index.d.ts +45 -0
  85. package/dist/field-formats/index.js +723 -0
  86. package/dist/field-formats/index.js.map +1 -0
  87. package/dist/formulas/index.cjs +1041 -0
  88. package/dist/formulas/index.cjs.map +1 -0
  89. package/dist/formulas/index.d.cts +389 -0
  90. package/dist/formulas/index.d.ts +389 -0
  91. package/dist/formulas/index.js +1019 -0
  92. package/dist/formulas/index.js.map +1 -0
  93. package/dist/types-CJWU1Gae.d.cts +204 -0
  94. package/dist/types-CJWU1Gae.d.ts +204 -0
  95. package/dist/{types-BgqIKbhq.d.cts → types-CmQrOz-T.d.cts} +382 -1
  96. package/dist/{types-emLxze_m.d.ts → types-nlLCMiqd.d.ts} +382 -1
  97. package/package.json +119 -2
@@ -0,0 +1,209 @@
1
+
2
+ // src/data-table/validation.ts
3
+ var OK = { ok: true };
4
+ var STORED_KEYS = [
5
+ "min",
6
+ "max",
7
+ "minLength",
8
+ "maxLength",
9
+ "pattern",
10
+ "patternHint",
11
+ "allowedValues",
12
+ "unique"
13
+ ];
14
+ function finiteNumber(raw) {
15
+ if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
16
+ if (typeof raw === "string" && raw.trim() !== "") {
17
+ const n = Number(raw);
18
+ return Number.isFinite(n) ? n : void 0;
19
+ }
20
+ return void 0;
21
+ }
22
+ function nonNegativeInteger(raw) {
23
+ const n = finiteNumber(raw);
24
+ if (n === void 0) return void 0;
25
+ const i = Math.trunc(n);
26
+ return i >= 0 ? i : void 0;
27
+ }
28
+ function parseValidationRules(raw) {
29
+ let source = raw;
30
+ if (typeof source === "string") {
31
+ const text = source.trim();
32
+ if (text === "") return {};
33
+ try {
34
+ source = JSON.parse(text);
35
+ } catch {
36
+ return {};
37
+ }
38
+ }
39
+ if (!source || typeof source !== "object" || Array.isArray(source)) return {};
40
+ const obj = source;
41
+ const out = {};
42
+ const min = finiteNumber(obj.min);
43
+ if (min !== void 0) out.min = min;
44
+ const max = finiteNumber(obj.max);
45
+ if (max !== void 0) out.max = max;
46
+ const minLength = nonNegativeInteger(obj.minLength);
47
+ if (minLength !== void 0) out.minLength = minLength;
48
+ const maxLength = nonNegativeInteger(obj.maxLength);
49
+ if (maxLength !== void 0) out.maxLength = maxLength;
50
+ if (typeof obj.pattern === "string" && obj.pattern.trim() !== "") {
51
+ out.pattern = obj.pattern;
52
+ if (typeof obj.patternHint === "string" && obj.patternHint.trim() !== "") {
53
+ out.patternHint = obj.patternHint.trim();
54
+ }
55
+ }
56
+ if (Array.isArray(obj.allowedValues)) {
57
+ const values = obj.allowedValues.filter(
58
+ (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean"
59
+ ).map((v) => String(v).trim()).filter((v) => v !== "");
60
+ const seen = /* @__PURE__ */ new Set();
61
+ const unique = values.filter((v) => {
62
+ const key = v.toLowerCase();
63
+ if (seen.has(key)) return false;
64
+ seen.add(key);
65
+ return true;
66
+ });
67
+ if (unique.length > 0) out.allowedValues = unique;
68
+ }
69
+ if (obj.unique === true) out.unique = true;
70
+ return out;
71
+ }
72
+ function hasValidationRules(rules) {
73
+ if (!rules) return false;
74
+ return STORED_KEYS.some((key) => rules[key] !== void 0);
75
+ }
76
+ function serializeValidationRules(rules) {
77
+ const out = {};
78
+ if (!rules) return out;
79
+ for (const key of STORED_KEYS) {
80
+ const value = rules[key];
81
+ if (value === void 0) continue;
82
+ if (key === "allowedValues" && (!Array.isArray(value) || value.length === 0)) continue;
83
+ if (key === "unique" && value !== true) continue;
84
+ if ((key === "pattern" || key === "patternHint") && String(value).trim() === "") continue;
85
+ out[key] = value;
86
+ }
87
+ if (out.patternHint !== void 0 && out.pattern === void 0) delete out.patternHint;
88
+ return out;
89
+ }
90
+ function describeValidationRules(rules) {
91
+ const out = [];
92
+ if (rules.min !== void 0 && rules.max !== void 0) {
93
+ out.push(`Between ${rules.min} and ${rules.max}`);
94
+ } else if (rules.min !== void 0) {
95
+ out.push(`At least ${rules.min}`);
96
+ } else if (rules.max !== void 0) {
97
+ out.push(`At most ${rules.max}`);
98
+ }
99
+ if (rules.minLength !== void 0 && rules.maxLength !== void 0) {
100
+ out.push(`${rules.minLength}\u2013${rules.maxLength} characters`);
101
+ } else if (rules.minLength !== void 0) {
102
+ out.push(`At least ${rules.minLength} characters`);
103
+ } else if (rules.maxLength !== void 0) {
104
+ out.push(`At most ${rules.maxLength} characters`);
105
+ }
106
+ if (rules.pattern) {
107
+ out.push(rules.patternHint ? `Pattern ${rules.patternHint}` : `Matches ${rules.pattern}`);
108
+ }
109
+ if (rules.allowedValues?.length) {
110
+ out.push(`One of: ${rules.allowedValues.join(", ")}`);
111
+ }
112
+ if (rules.unique) out.push("Unique across rows");
113
+ return out;
114
+ }
115
+ var CHOICE_FORMATS = /* @__PURE__ */ new Set(["choice", "multi_choice"]);
116
+ function isEmptyValue(value) {
117
+ if (value === null || value === void 0) return true;
118
+ if (typeof value === "string" && value.trim() === "") return true;
119
+ if (Array.isArray(value) && value.length === 0) return true;
120
+ return false;
121
+ }
122
+ function asText(value) {
123
+ if (typeof value === "string") return value;
124
+ if (Array.isArray(value)) return value.map((v) => String(v)).join(", ");
125
+ if (value !== null && typeof value === "object") {
126
+ try {
127
+ return JSON.stringify(value);
128
+ } catch {
129
+ return String(value);
130
+ }
131
+ }
132
+ return String(value);
133
+ }
134
+ function validateCellValue(args) {
135
+ const { rules, dataType, format, value, existingValues } = args;
136
+ if (!rules || !hasValidationRules(rules)) return OK;
137
+ if (isEmptyValue(value)) return OK;
138
+ const text = asText(value);
139
+ const numeric = finiteNumber(value);
140
+ if (numeric !== void 0) {
141
+ if (rules.min !== void 0 && numeric < rules.min) {
142
+ return { ok: false, reason: `Must be at least ${rules.min}` };
143
+ }
144
+ if (rules.max !== void 0 && numeric > rules.max) {
145
+ return { ok: false, reason: `Must be at most ${rules.max}` };
146
+ }
147
+ }
148
+ if (rules.minLength !== void 0 && text.length < rules.minLength) {
149
+ return {
150
+ ok: false,
151
+ reason: `Must be at least ${rules.minLength} character${rules.minLength === 1 ? "" : "s"} (this is ${text.length})`
152
+ };
153
+ }
154
+ if (rules.maxLength !== void 0 && text.length > rules.maxLength) {
155
+ return {
156
+ ok: false,
157
+ reason: `Must be at most ${rules.maxLength} character${rules.maxLength === 1 ? "" : "s"} (this is ${text.length})`
158
+ };
159
+ }
160
+ if (rules.pattern) {
161
+ let re = null;
162
+ try {
163
+ re = new RegExp(rules.pattern);
164
+ } catch {
165
+ re = null;
166
+ }
167
+ if (re && !re.test(text)) {
168
+ return {
169
+ ok: false,
170
+ reason: rules.patternHint ? `Must match the pattern ${rules.patternHint}` : `Must match the pattern ${rules.pattern}`
171
+ };
172
+ }
173
+ }
174
+ const isChoiceColumn = format ? CHOICE_FORMATS.has(format.id) : false;
175
+ if (rules.allowedValues?.length && !isChoiceColumn) {
176
+ const target = text.trim();
177
+ const hit = rules.allowedValues.some(
178
+ (v) => v === target || v.toLowerCase() === target.toLowerCase()
179
+ );
180
+ if (!hit) {
181
+ return {
182
+ ok: false,
183
+ reason: `Must be one of: ${rules.allowedValues.join(", ")}`
184
+ };
185
+ }
186
+ }
187
+ if (rules.unique && existingValues && existingValues.length > 0) {
188
+ const target = text.trim().toLowerCase();
189
+ const clash = existingValues.some(
190
+ (other) => !isEmptyValue(other) && asText(other).trim().toLowerCase() === target
191
+ );
192
+ if (clash) {
193
+ return {
194
+ ok: false,
195
+ reason: `Must be unique \u2014 another row already has "${text}"`
196
+ };
197
+ }
198
+ }
199
+ void dataType;
200
+ return OK;
201
+ }
202
+ export {
203
+ describeValidationRules,
204
+ hasValidationRules,
205
+ parseValidationRules,
206
+ serializeValidationRules,
207
+ validateCellValue
208
+ };
209
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/validation.ts"],"sourcesContent":["/**\n * Column validation rules for user data tables — THE one rule model.\n *\n * A rule lives on `workbench.udt_dataset_fields.validation_rules` (jsonb) and\n * answers one question: may this value be written into this column? The\n * champions are Excel's data validation (a rule per column; an invalid entry is\n * refused WITH the reason) and Airtable (type-level only). We take Excel's\n * posture: the reason is the product.\n *\n * THREE LAWS this module exists to keep:\n *\n * 1. **`required` is NOT stored here.** The column already declares it as\n * `is_required`, and a second copy is a second truth. `ValidationRules.required`\n * is a read-only MIRROR for callers that want one object to reason about —\n * `parseValidationRules` never invents it and the editor never writes it.\n *\n * 2. **A rule judges a value the user MEANT to write; it never judges what is\n * already stored.** Existing values that violate a rule are legal, are never\n * rewritten, and render in the SAME amber the format system already uses for\n * a mismatch (`<FormattedFieldValue validationRules>`). Declaring a rule is\n * how a user FINDS the stray values in their data, exactly as declaring a\n * choice column's options is.\n *\n * 3. **An empty value is never a rule violation.** Emptiness is `is_required`'s\n * question, asked once, by the callers that own the whole row. A `min: 0`\n * rule that refused an empty cell would make every optional number column\n * mandatory by accident.\n *\n * `unique` is the one rule this module cannot answer alone: it is a statement\n * about the OTHER rows, so the caller passes `existingValues` (the loaded rows'\n * values for that column) when it holds them. Without them the rule is skipped\n * rather than guessed — and it is deliberately NOT enforced by the database\n * trigger, because a cross-row check inside a per-row BEFORE trigger is the\n * wrong shape (it cannot see concurrent inserts and it walks the table on every\n * write). See `migrations/udt_validation_rules_strict_enforcement.sql`.\n *\n * Moved here from the app (`@ai-matrx/design-system/data-table/validation`) so\n * every table in the product enforces and explains validation the same way.\n */\n\nimport type { FieldFormatConfig } from \"../field-formats/types\";\n\nexport type ValidationRules = {\n /**\n * MIRROR of the column's `is_required`. Never stored in `validation_rules`,\n * never written by the editor — present so a caller can carry one object.\n */\n required?: boolean;\n /** Smallest number accepted. Number-ish columns only. */\n min?: number;\n /** Largest number accepted. Number-ish columns only. */\n max?: number;\n /** Fewest characters accepted. Text-ish columns only. */\n minLength?: number;\n /** Most characters accepted. Text-ish columns only. */\n maxLength?: number;\n /**\n * A regular expression the value must match. Written in the dialect BOTH\n * JavaScript and Postgres understand, and **anchored by the author** — an\n * unanchored `\\d{3}` matches \"abc123def\" in both engines, which is almost\n * never what a person drawing a rule means.\n */\n pattern?: string;\n /** Plain-English shape shown when `pattern` refuses, e.g. `###-####`. */\n patternHint?: string;\n /**\n * The exact values accepted. Only for a column that is NOT a choice column —\n * a choice column already carries its options in its format, and a second\n * list would be a second vocabulary for the same column.\n */\n allowedValues?: string[];\n /** No two rows may carry the same value. Checked by the caller, never by the trigger. */\n unique?: boolean;\n};\n\nexport type ValidationOk = { ok: true };\nexport type ValidationFailure = { ok: false; reason: string };\nexport type ValidationResult = ValidationOk | ValidationFailure;\n\nconst OK: ValidationOk = { ok: true };\n\n/** Keys the editor may write. `required` is deliberately absent — law 1. */\nconst STORED_KEYS = [\n \"min\",\n \"max\",\n \"minLength\",\n \"maxLength\",\n \"pattern\",\n \"patternHint\",\n \"allowedValues\",\n \"unique\",\n] as const;\n\nfunction finiteNumber(raw: unknown): number | undefined {\n if (typeof raw === \"number\") return Number.isFinite(raw) ? raw : undefined;\n if (typeof raw === \"string\" && raw.trim() !== \"\") {\n const n = Number(raw);\n return Number.isFinite(n) ? n : undefined;\n }\n return undefined;\n}\n\nfunction nonNegativeInteger(raw: unknown): number | undefined {\n const n = finiteNumber(raw);\n if (n === undefined) return undefined;\n const i = Math.trunc(n);\n return i >= 0 ? i : undefined;\n}\n\n/**\n * Read whatever is stored on the column into the rule model. TOLERANT by\n * design: this reads a jsonb column that predates the feature, that an agent or\n * an import may have written, and that a future version may extend. Anything it\n * cannot understand is dropped, never thrown on — a column with a garbled rule\n * must still be editable.\n */\nexport function parseValidationRules(raw: unknown): ValidationRules {\n let source: unknown = raw;\n if (typeof source === \"string\") {\n const text = source.trim();\n if (text === \"\") return {};\n try {\n source = JSON.parse(text);\n } catch {\n return {};\n }\n }\n if (!source || typeof source !== \"object\" || Array.isArray(source)) return {};\n const obj = source as Record<string, unknown>;\n const out: ValidationRules = {};\n\n const min = finiteNumber(obj.min);\n if (min !== undefined) out.min = min;\n const max = finiteNumber(obj.max);\n if (max !== undefined) out.max = max;\n\n const minLength = nonNegativeInteger(obj.minLength);\n if (minLength !== undefined) out.minLength = minLength;\n const maxLength = nonNegativeInteger(obj.maxLength);\n if (maxLength !== undefined) out.maxLength = maxLength;\n\n if (typeof obj.pattern === \"string\" && obj.pattern.trim() !== \"\") {\n out.pattern = obj.pattern;\n if (typeof obj.patternHint === \"string\" && obj.patternHint.trim() !== \"\") {\n out.patternHint = obj.patternHint.trim();\n }\n }\n\n if (Array.isArray(obj.allowedValues)) {\n const values = obj.allowedValues\n .filter(\n (v): v is string | number | boolean =>\n typeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\",\n )\n .map((v) => String(v).trim())\n .filter((v) => v !== \"\");\n // De-duplicate, keeping the author's order.\n const seen = new Set<string>();\n const unique = values.filter((v) => {\n const key = v.toLowerCase();\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n if (unique.length > 0) out.allowedValues = unique;\n }\n\n if (obj.unique === true) out.unique = true;\n\n return out;\n}\n\n/** True when the column actually constrains anything. */\nexport function hasValidationRules(rules: ValidationRules | null | undefined): boolean {\n if (!rules) return false;\n return STORED_KEYS.some((key) => rules[key] !== undefined);\n}\n\n/**\n * The object to STORE. Strips `required` (law 1) and every empty key, so a\n * column whose rules were all cleared stores `{}` rather than a husk of nulls\n * — which also matters because `update_user_table_config` COALESCEs the column,\n * so `{}` is the only way to clear it.\n */\nexport function serializeValidationRules(\n rules: ValidationRules | null | undefined,\n): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n if (!rules) return out;\n for (const key of STORED_KEYS) {\n const value = rules[key];\n if (value === undefined) continue;\n if (key === \"allowedValues\" && (!Array.isArray(value) || value.length === 0)) continue;\n if (key === \"unique\" && value !== true) continue;\n if ((key === \"pattern\" || key === \"patternHint\") && String(value).trim() === \"\") continue;\n out[key] = value;\n }\n // A hint with no pattern says nothing; drop it rather than store a dangling half-rule.\n if (out.patternHint !== undefined && out.pattern === undefined) delete out.patternHint;\n return out;\n}\n\n/** Plain-English list of what a column requires — for tooltips and for agents. */\nexport function describeValidationRules(rules: ValidationRules): string[] {\n const out: string[] = [];\n if (rules.min !== undefined && rules.max !== undefined) {\n out.push(`Between ${rules.min} and ${rules.max}`);\n } else if (rules.min !== undefined) {\n out.push(`At least ${rules.min}`);\n } else if (rules.max !== undefined) {\n out.push(`At most ${rules.max}`);\n }\n if (rules.minLength !== undefined && rules.maxLength !== undefined) {\n out.push(`${rules.minLength}–${rules.maxLength} characters`);\n } else if (rules.minLength !== undefined) {\n out.push(`At least ${rules.minLength} characters`);\n } else if (rules.maxLength !== undefined) {\n out.push(`At most ${rules.maxLength} characters`);\n }\n if (rules.pattern) {\n out.push(rules.patternHint ? `Pattern ${rules.patternHint}` : `Matches ${rules.pattern}`);\n }\n if (rules.allowedValues?.length) {\n out.push(`One of: ${rules.allowedValues.join(\", \")}`);\n }\n if (rules.unique) out.push(\"Unique across rows\");\n return out;\n}\n\nconst CHOICE_FORMATS = new Set([\"choice\", \"multi_choice\"]);\n\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined) return true;\n if (typeof value === \"string\" && value.trim() === \"\") return true;\n if (Array.isArray(value) && value.length === 0) return true;\n return false;\n}\n\n/** The value as the user reads it — the basis for every text rule. */\nfunction asText(value: unknown): string {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) return value.map((v) => String(v)).join(\", \");\n if (value !== null && typeof value === \"object\") {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n}\n\nexport type ValidateCellArgs = {\n rules: ValidationRules | null | undefined;\n /** The column's storage type (`string` | `number` | `integer` | …). */\n dataType: string;\n /** The column's declared display format, when it has one. */\n format?: FieldFormatConfig | null;\n /** The value the user is trying to write, already type-normalized. */\n value: unknown;\n /**\n * Every OTHER row's value for this column, when the caller holds them. Only\n * `unique` reads this; omit it and `unique` is skipped rather than guessed.\n */\n existingValues?: unknown[];\n};\n\n/**\n * Judge one value against one column's rules.\n *\n * Returns the FIRST reason it fails, in the order a person would check them\n * (range, length, shape, membership, uniqueness) — a wall of five complaints\n * about one cell is not more helpful than the first one.\n */\nexport function validateCellValue(args: ValidateCellArgs): ValidationResult {\n const { rules, dataType, format, value, existingValues } = args;\n if (!rules || !hasValidationRules(rules)) return OK;\n\n // Law 3 — emptiness belongs to `is_required`, not to a rule.\n if (isEmptyValue(value)) return OK;\n\n const text = asText(value);\n const numeric = finiteNumber(value);\n\n // ── range ───────────────────────────────────────────────────────────────\n // Applied only when a number can honestly be read out of the value. A\n // non-numeric value in a number column is a TYPE problem, already reported by\n // the format layer and by strict mode; saying \"Must be at least 0\" about the\n // word \"pending\" would be a second, worse answer to a question already asked.\n if (numeric !== undefined) {\n if (rules.min !== undefined && numeric < rules.min) {\n return { ok: false, reason: `Must be at least ${rules.min}` };\n }\n if (rules.max !== undefined && numeric > rules.max) {\n return { ok: false, reason: `Must be at most ${rules.max}` };\n }\n }\n\n // ── length ──────────────────────────────────────────────────────────────\n if (rules.minLength !== undefined && text.length < rules.minLength) {\n return {\n ok: false,\n reason: `Must be at least ${rules.minLength} character${rules.minLength === 1 ? \"\" : \"s\"} (this is ${text.length})`,\n };\n }\n if (rules.maxLength !== undefined && text.length > rules.maxLength) {\n return {\n ok: false,\n reason: `Must be at most ${rules.maxLength} character${rules.maxLength === 1 ? \"\" : \"s\"} (this is ${text.length})`,\n };\n }\n\n // ── shape ───────────────────────────────────────────────────────────────\n if (rules.pattern) {\n let re: RegExp | null = null;\n try {\n re = new RegExp(rules.pattern);\n } catch {\n // An unparseable pattern is the AUTHOR's defect, not this value's. It is\n // reported where it is written (the editor refuses to save one); here it\n // must never turn into a refusal the typist cannot act on.\n re = null;\n }\n if (re && !re.test(text)) {\n return {\n ok: false,\n reason: rules.patternHint\n ? `Must match the pattern ${rules.patternHint}`\n : `Must match the pattern ${rules.pattern}`,\n };\n }\n }\n\n // ── membership ──────────────────────────────────────────────────────────\n // Skipped on a choice column by construction: its options are its format's\n // job, they are offered in the picker, and an off-list value there is legal\n // and amber by design. A second list here would contradict that.\n const isChoiceColumn = format ? CHOICE_FORMATS.has(format.id) : false;\n if (rules.allowedValues?.length && !isChoiceColumn) {\n const target = text.trim();\n const hit = rules.allowedValues.some(\n (v) => v === target || v.toLowerCase() === target.toLowerCase(),\n );\n if (!hit) {\n return {\n ok: false,\n reason: `Must be one of: ${rules.allowedValues.join(\", \")}`,\n };\n }\n }\n\n // ── uniqueness ──────────────────────────────────────────────────────────\n if (rules.unique && existingValues && existingValues.length > 0) {\n const target = text.trim().toLowerCase();\n const clash = existingValues.some(\n (other) => !isEmptyValue(other) && asText(other).trim().toLowerCase() === target,\n );\n if (clash) {\n return {\n ok: false,\n reason: `Must be unique — another row already has \"${text}\"`,\n };\n }\n }\n\n // `dataType` is not read by any rule today: every rule above is answered by\n // the VALUE, and the storage type is already enforced by strict mode and by\n // the format layer. It stays in the signature because it is the one fact a\n // future rule (a date range, say) cannot be written without, and adding it\n // later would change every callsite.\n void dataType;\n\n return OK;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,203 @@
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/virtual-window.ts
21
+ var virtual_window_exports = {};
22
+ __export(virtual_window_exports, {
23
+ DEFAULT_OVERSCAN: () => DEFAULT_OVERSCAN,
24
+ computeVariableWindow: () => computeVariableWindow,
25
+ computeWindow: () => computeWindow,
26
+ scrollOffsetForIndex: () => scrollOffsetForIndex
27
+ });
28
+ module.exports = __toCommonJS(virtual_window_exports);
29
+ var DEFAULT_OVERSCAN = 6;
30
+ function isNum(value) {
31
+ return typeof value === "number" && Number.isFinite(value);
32
+ }
33
+ function toCount(value) {
34
+ return isNum(value) && value > 0 ? Math.floor(value) : 0;
35
+ }
36
+ function toLength(value) {
37
+ return isNum(value) && value > 0 ? value : 0;
38
+ }
39
+ function toOverscan(value) {
40
+ if (!isNum(value)) return DEFAULT_OVERSCAN;
41
+ return value > 0 ? Math.floor(value) : 0;
42
+ }
43
+ function toPinned(value, count) {
44
+ if (!isNum(value) || value <= 0) return 0;
45
+ return Math.min(Math.floor(value), count);
46
+ }
47
+ function assemble(args) {
48
+ const { pinnedCount, start, end, padStart, body, total } = args;
49
+ let padEnd = total - (padStart + body);
50
+ if (!Number.isFinite(padEnd)) padEnd = 0;
51
+ const sum = padStart + body + padEnd;
52
+ if (sum !== total) padEnd = padEnd + (total - sum);
53
+ const pinned = [];
54
+ for (let i = 0; i < pinnedCount; i += 1) pinned.push(i);
55
+ return { start, end, padStart, padEnd, total, pinned };
56
+ }
57
+ function emptyWindow(pinnedCount) {
58
+ const pinned = [];
59
+ for (let i = 0; i < pinnedCount; i += 1) pinned.push(i);
60
+ return { start: pinnedCount, end: pinnedCount, padStart: 0, padEnd: 0, total: 0, pinned };
61
+ }
62
+ function band(firstVisible, lastVisible, scrollCount, overscan) {
63
+ let first = firstVisible;
64
+ if (first > scrollCount - 1) first = scrollCount - 1;
65
+ if (first < 0) first = 0;
66
+ let last = lastVisible;
67
+ if (last < first + 1) last = first + 1;
68
+ if (last > scrollCount) last = scrollCount;
69
+ const start = Math.max(0, first - overscan);
70
+ let end = Math.min(scrollCount, last + overscan);
71
+ if (end < start) end = start;
72
+ return { start, end };
73
+ }
74
+ function computeWindow(input) {
75
+ const count = toCount(input?.count);
76
+ const pinnedCount = toPinned(input?.pinnedStart, count);
77
+ if (count <= 0) return emptyWindow(0);
78
+ const itemSize = toLength(input?.itemSize);
79
+ const viewport = toLength(input?.viewport);
80
+ const overscan = toOverscan(input?.overscan);
81
+ const scrollCount = count - pinnedCount;
82
+ if (scrollCount <= 0) return emptyWindow(pinnedCount);
83
+ if (itemSize <= 0) {
84
+ return assemble({ pinnedCount, start: pinnedCount, end: count, padStart: 0, body: 0, total: 0 });
85
+ }
86
+ const total = scrollCount * itemSize;
87
+ const maxOffset = Math.max(0, total - viewport);
88
+ const offset = Math.min(toLength(input?.offset), maxOffset);
89
+ const { start: relStart, end: relEnd } = band(
90
+ Math.floor(offset / itemSize),
91
+ Math.ceil((offset + viewport) / itemSize),
92
+ scrollCount,
93
+ overscan
94
+ );
95
+ return assemble({
96
+ pinnedCount,
97
+ start: pinnedCount + relStart,
98
+ end: pinnedCount + relEnd,
99
+ padStart: relStart * itemSize,
100
+ body: (relEnd - relStart) * itemSize,
101
+ total
102
+ });
103
+ }
104
+ var PREFIX_CACHE = /* @__PURE__ */ new WeakMap();
105
+ function prefixFor(sizeAt, count) {
106
+ let byCount = PREFIX_CACHE.get(sizeAt);
107
+ if (!byCount) {
108
+ byCount = /* @__PURE__ */ new Map();
109
+ PREFIX_CACHE.set(sizeAt, byCount);
110
+ }
111
+ const cached = byCount.get(count);
112
+ if (cached) return cached;
113
+ const prefix = new Float64Array(count + 1);
114
+ let running = 0;
115
+ for (let i = 0; i < count; i += 1) {
116
+ let size = 0;
117
+ try {
118
+ size = sizeAt(i);
119
+ } catch {
120
+ size = 0;
121
+ }
122
+ running += isNum(size) && size > 0 ? size : 0;
123
+ prefix[i + 1] = running;
124
+ }
125
+ byCount.set(count, prefix);
126
+ return prefix;
127
+ }
128
+ function at(prefix, index) {
129
+ return prefix[index] ?? 0;
130
+ }
131
+ function lastAtOrBefore(prefix, lo, hi, target) {
132
+ let a = lo;
133
+ let b = hi;
134
+ while (a < b) {
135
+ const mid = a + Math.ceil((b - a) / 2);
136
+ if (at(prefix, mid) <= target) a = mid;
137
+ else b = mid - 1;
138
+ }
139
+ return a;
140
+ }
141
+ function firstAtOrAfter(prefix, lo, hi, target) {
142
+ let a = lo;
143
+ let b = hi;
144
+ while (a < b) {
145
+ const mid = a + Math.floor((b - a) / 2);
146
+ if (at(prefix, mid) >= target) b = mid;
147
+ else a = mid + 1;
148
+ }
149
+ return a;
150
+ }
151
+ function computeVariableWindow(input) {
152
+ const count = toCount(input?.count);
153
+ const pinnedCount = toPinned(input?.pinnedStart, count);
154
+ if (count <= 0) return emptyWindow(0);
155
+ const sizeAt = typeof input?.sizeAt === "function" ? input.sizeAt : () => 0;
156
+ const viewport = toLength(input?.viewport);
157
+ const overscan = toOverscan(input?.overscan);
158
+ const scrollCount = count - pinnedCount;
159
+ if (scrollCount <= 0) return emptyWindow(pinnedCount);
160
+ const prefix = prefixFor(sizeAt, count);
161
+ const pinnedLength = at(prefix, pinnedCount);
162
+ const total = at(prefix, count) - pinnedLength;
163
+ if (total <= 0) {
164
+ return assemble({ pinnedCount, start: pinnedCount, end: count, padStart: 0, body: 0, total: total > 0 ? total : 0 });
165
+ }
166
+ const maxOffset = Math.max(0, total - viewport);
167
+ const offset = Math.min(toLength(input?.offset), maxOffset);
168
+ const firstVisible = lastAtOrBefore(prefix, pinnedCount, count, pinnedLength + offset) - pinnedCount;
169
+ const lastVisible = firstAtOrAfter(prefix, pinnedCount, count, pinnedLength + offset + viewport) - pinnedCount;
170
+ const { start: relStart, end: relEnd } = band(firstVisible, lastVisible, scrollCount, overscan);
171
+ const absStart = pinnedCount + relStart;
172
+ const absEnd = pinnedCount + relEnd;
173
+ return assemble({
174
+ pinnedCount,
175
+ start: absStart,
176
+ end: absEnd,
177
+ padStart: at(prefix, absStart) - pinnedLength,
178
+ body: at(prefix, absEnd) - at(prefix, absStart),
179
+ total
180
+ });
181
+ }
182
+ function scrollOffsetForIndex(input) {
183
+ const count = toCount(input?.count);
184
+ if (count <= 0) return null;
185
+ const pinnedCount = toPinned(input?.pinnedStart, count);
186
+ const itemSize = toLength(input?.itemSize);
187
+ const viewport = toLength(input?.viewport);
188
+ const scrollCount = count - pinnedCount;
189
+ if (scrollCount <= 0 || itemSize <= 0) return null;
190
+ const rawIndex = isNum(input?.index) ? Math.floor(input.index) : 0;
191
+ const index = Math.min(Math.max(rawIndex, 0), count - 1);
192
+ if (index < pinnedCount) return null;
193
+ const total = scrollCount * itemSize;
194
+ const maxOffset = Math.max(0, total - viewport);
195
+ const offset = Math.min(toLength(input?.offset), maxOffset);
196
+ const itemStart = (index - pinnedCount) * itemSize;
197
+ const itemEnd = itemStart + itemSize;
198
+ if (itemStart >= offset && itemEnd <= offset + viewport) return null;
199
+ const next = itemStart < offset ? itemStart : itemEnd - viewport;
200
+ const clamped = Math.min(Math.max(next, 0), maxOffset);
201
+ return clamped === offset ? null : clamped;
202
+ }
203
+ //# sourceMappingURL=virtual-window.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data-table/virtual-window.ts"],"sourcesContent":["/**\n * Row / column windowing math for the canonical data table.\n *\n * Pure arithmetic: no React, no DOM, no measurement. The table hands in what it\n * knows (how many items, how big they are, how much of them the viewport shows,\n * where the scroller is) and gets back the slice it should actually render plus\n * the two spacers that keep the scrollbar honest.\n *\n * THE INVARIANT this module exists to hold:\n *\n * padStart + (end - start) * itemSize + padEnd === total\n *\n * exactly, for every input. A scrollbar whose thumb size disagrees with the\n * content it can reach is a lie on screen, so `padEnd` is computed as the\n * residual of `total` rather than as an independent multiplication, and the\n * sum is re-checked and corrected once against floating-point drift.\n *\n * PINNED ITEMS (frozen columns) live OUTSIDE the scrollable length. They are\n * always rendered, they are never counted in `padStart`, and the scrollable\n * `total` covers only the items after them. `offset` is therefore measured\n * inside the scrollable region: offset 0 shows item index `pinnedStart`.\n */\n\n/** Items rendered outside the viewport on each side when the caller says nothing. */\nexport const DEFAULT_OVERSCAN = 6;\n\nexport interface WindowInput {\n /** Total item count. */\n count: number;\n /** Fixed item size in px (row height, or column width when windowing columns). */\n itemSize: number;\n /** Visible viewport length in px along the windowed axis. */\n viewport: number;\n /** Current scroll offset in px along that axis. */\n offset: number;\n /** Extra items rendered outside the viewport on each side. Default 6. */\n overscan?: number;\n /** Items pinned at the start that never leave the DOM (frozen columns). Default 0. */\n pinnedStart?: number;\n}\n\nexport interface WindowResult {\n /** First rendered index, inclusive. */\n start: number;\n /** Last rendered index, EXCLUSIVE. */\n end: number;\n /** Spacer length in px before `start` (excluding pinned items). */\n padStart: number;\n /** Spacer length in px after `end`. */\n padEnd: number;\n /** Total scrollable length in px. */\n total: number;\n /** Indexes pinned at the start that are always rendered, ascending. */\n pinned: number[];\n}\n\nexport interface VariableWindowInput extends Omit<WindowInput, \"itemSize\"> {\n /** Size in px of the item at `index`. Must be pure and stable for a given index. */\n sizeAt: (index: number) => number;\n}\n\n/* ------------------------------------------------------------------ */\n/* clamping — nothing here throws, every hostile number is flattened */\n/* ------------------------------------------------------------------ */\n\nfunction isNum(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\n/** Whole, non-negative item count. NaN / Infinity / negatives collapse to 0. */\nfunction toCount(value: number): number {\n return isNum(value) && value > 0 ? Math.floor(value) : 0;\n}\n\n/** Non-negative px length. NaN / Infinity / negatives collapse to 0. */\nfunction toLength(value: number): number {\n return isNum(value) && value > 0 ? value : 0;\n}\n\nfunction toOverscan(value: number | undefined): number {\n if (!isNum(value)) return DEFAULT_OVERSCAN;\n return value > 0 ? Math.floor(value) : 0;\n}\n\nfunction toPinned(value: number | undefined, count: number): number {\n if (!isNum(value) || value <= 0) return 0;\n return Math.min(Math.floor(value), count);\n}\n\n/**\n * Assemble the result, deriving `padEnd` as the residual of `total` so the\n * invariant holds bit-for-bit, then correcting one rounding step if the sum\n * the caller will compute still drifts.\n */\nfunction assemble(args: {\n pinnedCount: number;\n start: number;\n end: number;\n padStart: number;\n body: number;\n total: number;\n}): WindowResult {\n const { pinnedCount, start, end, padStart, body, total } = args;\n let padEnd = total - (padStart + body);\n if (!Number.isFinite(padEnd)) padEnd = 0;\n const sum = padStart + body + padEnd;\n if (sum !== total) padEnd = padEnd + (total - sum);\n const pinned: number[] = [];\n for (let i = 0; i < pinnedCount; i += 1) pinned.push(i);\n return { start, end, padStart, padEnd, total, pinned };\n}\n\n/** A window that renders nothing but still names whatever is pinned. */\nfunction emptyWindow(pinnedCount: number): WindowResult {\n const pinned: number[] = [];\n for (let i = 0; i < pinnedCount; i += 1) pinned.push(i);\n return { start: pinnedCount, end: pinnedCount, padStart: 0, padEnd: 0, total: 0, pinned };\n}\n\n/**\n * The visible band, in RELATIVE indexes inside the scrollable region, widened\n * by overscan and clamped by both ends. Shared by the fixed and variable paths\n * so the two agree exactly when sizes are constant.\n */\nfunction band(firstVisible: number, lastVisible: number, scrollCount: number, overscan: number): { start: number; end: number } {\n let first = firstVisible;\n if (first > scrollCount - 1) first = scrollCount - 1;\n if (first < 0) first = 0;\n let last = lastVisible;\n if (last < first + 1) last = first + 1;\n if (last > scrollCount) last = scrollCount;\n const start = Math.max(0, first - overscan);\n let end = Math.min(scrollCount, last + overscan);\n if (end < start) end = start;\n return { start, end };\n}\n\n/* ------------------------------------------------------------------ */\n/* fixed-size windowing */\n/* ------------------------------------------------------------------ */\n\n/**\n * The slice to render for uniformly sized items.\n *\n * O(1). Never returns `start < 0`, `end > count`, or `end < start`.\n */\nexport function computeWindow(input: WindowInput): WindowResult {\n const count = toCount(input?.count as number);\n const pinnedCount = toPinned(input?.pinnedStart, count);\n if (count <= 0) return emptyWindow(0);\n\n const itemSize = toLength(input?.itemSize as number);\n const viewport = toLength(input?.viewport as number);\n const overscan = toOverscan(input?.overscan);\n const scrollCount = count - pinnedCount;\n\n if (scrollCount <= 0) return emptyWindow(pinnedCount);\n\n if (itemSize <= 0) {\n // Zero-height items: nothing to scroll, so render the whole scrollable run.\n return assemble({ pinnedCount, start: pinnedCount, end: count, padStart: 0, body: 0, total: 0 });\n }\n\n const total = scrollCount * itemSize;\n const maxOffset = Math.max(0, total - viewport);\n const offset = Math.min(toLength(input?.offset as number), maxOffset);\n\n const { start: relStart, end: relEnd } = band(\n Math.floor(offset / itemSize),\n Math.ceil((offset + viewport) / itemSize),\n scrollCount,\n overscan,\n );\n\n return assemble({\n pinnedCount,\n start: pinnedCount + relStart,\n end: pinnedCount + relEnd,\n padStart: relStart * itemSize,\n body: (relEnd - relStart) * itemSize,\n total,\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* variable-size windowing */\n/* ------------------------------------------------------------------ */\n\n/**\n * Prefix sums keyed by the `sizeAt` identity and the item count, so repeated\n * calls during a scroll do NOT walk the list again. `sizeAt` must be pure:\n * a different measurement for the same index needs a new function identity.\n */\nconst PREFIX_CACHE = new WeakMap<(index: number) => number, Map<number, Float64Array>>();\n\nfunction prefixFor(sizeAt: (index: number) => number, count: number): Float64Array {\n let byCount = PREFIX_CACHE.get(sizeAt);\n if (!byCount) {\n byCount = new Map<number, Float64Array>();\n PREFIX_CACHE.set(sizeAt, byCount);\n }\n const cached = byCount.get(count);\n if (cached) return cached;\n const prefix = new Float64Array(count + 1);\n let running = 0;\n for (let i = 0; i < count; i += 1) {\n let size = 0;\n try {\n size = sizeAt(i);\n } catch {\n size = 0;\n }\n running += isNum(size) && size > 0 ? size : 0;\n prefix[i + 1] = running;\n }\n byCount.set(count, prefix);\n return prefix;\n}\n\nfunction at(prefix: Float64Array, index: number): number {\n return prefix[index] ?? 0;\n}\n\n/** Largest `i` in `[lo, hi]` whose prefix is <= `target`. */\nfunction lastAtOrBefore(prefix: Float64Array, lo: number, hi: number, target: number): number {\n let a = lo;\n let b = hi;\n while (a < b) {\n const mid = a + Math.ceil((b - a) / 2);\n if (at(prefix, mid) <= target) a = mid;\n else b = mid - 1;\n }\n return a;\n}\n\n/** Smallest `j` in `[lo, hi]` whose prefix is >= `target`. */\nfunction firstAtOrAfter(prefix: Float64Array, lo: number, hi: number, target: number): number {\n let a = lo;\n let b = hi;\n while (a < b) {\n const mid = a + Math.floor((b - a) / 2);\n if (at(prefix, mid) >= target) b = mid;\n else a = mid + 1;\n }\n return a;\n}\n\n/**\n * The slice to render for items whose size varies by index.\n *\n * Agrees with `computeWindow` exactly when `sizeAt` is constant. The prefix-sum\n * array is built once per `(sizeAt, count)` pair and then binary-searched, so a\n * scroll costs O(log count) per call after the first O(count) build.\n */\nexport function computeVariableWindow(input: VariableWindowInput): WindowResult {\n const count = toCount(input?.count as number);\n const pinnedCount = toPinned(input?.pinnedStart, count);\n if (count <= 0) return emptyWindow(0);\n\n const sizeAt = typeof input?.sizeAt === \"function\" ? input.sizeAt : () => 0;\n const viewport = toLength(input?.viewport as number);\n const overscan = toOverscan(input?.overscan);\n const scrollCount = count - pinnedCount;\n\n if (scrollCount <= 0) return emptyWindow(pinnedCount);\n\n const prefix = prefixFor(sizeAt, count);\n const pinnedLength = at(prefix, pinnedCount);\n const total = at(prefix, count) - pinnedLength;\n\n if (total <= 0) {\n return assemble({ pinnedCount, start: pinnedCount, end: count, padStart: 0, body: 0, total: total > 0 ? total : 0 });\n }\n\n const maxOffset = Math.max(0, total - viewport);\n const offset = Math.min(toLength(input?.offset as number), maxOffset);\n\n const firstVisible = lastAtOrBefore(prefix, pinnedCount, count, pinnedLength + offset) - pinnedCount;\n const lastVisible = firstAtOrAfter(prefix, pinnedCount, count, pinnedLength + offset + viewport) - pinnedCount;\n\n const { start: relStart, end: relEnd } = band(firstVisible, lastVisible, scrollCount, overscan);\n const absStart = pinnedCount + relStart;\n const absEnd = pinnedCount + relEnd;\n\n return assemble({\n pinnedCount,\n start: absStart,\n end: absEnd,\n padStart: at(prefix, absStart) - pinnedLength,\n body: at(prefix, absEnd) - at(prefix, absStart),\n total,\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* scroll-to-index */\n/* ------------------------------------------------------------------ */\n\nexport interface ScrollToIndexInput extends WindowInput {\n /** The item to bring into view. */\n index: number;\n}\n\n/**\n * The scroll offset that brings `index` fully into view, or `null` when it is\n * already fully visible (pinned items are always visible, so they return\n * `null` too). Moves the minimum distance: up to the item's own start when it\n * sits above the viewport, down until its end meets the viewport end when it\n * sits below.\n */\nexport function scrollOffsetForIndex(input: ScrollToIndexInput): number | null {\n const count = toCount(input?.count as number);\n if (count <= 0) return null;\n const pinnedCount = toPinned(input?.pinnedStart, count);\n const itemSize = toLength(input?.itemSize as number);\n const viewport = toLength(input?.viewport as number);\n const scrollCount = count - pinnedCount;\n if (scrollCount <= 0 || itemSize <= 0) return null;\n\n const rawIndex = isNum(input?.index) ? Math.floor(input.index) : 0;\n const index = Math.min(Math.max(rawIndex, 0), count - 1);\n if (index < pinnedCount) return null; // pinned: never leaves the DOM\n\n const total = scrollCount * itemSize;\n const maxOffset = Math.max(0, total - viewport);\n const offset = Math.min(toLength(input?.offset as number), maxOffset);\n\n const itemStart = (index - pinnedCount) * itemSize;\n const itemEnd = itemStart + itemSize;\n\n if (itemStart >= offset && itemEnd <= offset + viewport) return null;\n\n const next = itemStart < offset ? itemStart : itemEnd - viewport;\n const clamped = Math.min(Math.max(next, 0), maxOffset);\n return clamped === offset ? null : clamped;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBO,IAAM,mBAAmB;AAyChC,SAAS,MAAM,OAAiC;AAC9C,SAAO,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK;AAC3D;AAGA,SAAS,QAAQ,OAAuB;AACtC,SAAO,MAAM,KAAK,KAAK,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;AACzD;AAGA,SAAS,SAAS,OAAuB;AACvC,SAAO,MAAM,KAAK,KAAK,QAAQ,IAAI,QAAQ;AAC7C;AAEA,SAAS,WAAW,OAAmC;AACrD,MAAI,CAAC,MAAM,KAAK,EAAG,QAAO;AAC1B,SAAO,QAAQ,IAAI,KAAK,MAAM,KAAK,IAAI;AACzC;AAEA,SAAS,SAAS,OAA2B,OAAuB;AAClE,MAAI,CAAC,MAAM,KAAK,KAAK,SAAS,EAAG,QAAO;AACxC,SAAO,KAAK,IAAI,KAAK,MAAM,KAAK,GAAG,KAAK;AAC1C;AAOA,SAAS,SAAS,MAOD;AACf,QAAM,EAAE,aAAa,OAAO,KAAK,UAAU,MAAM,MAAM,IAAI;AAC3D,MAAI,SAAS,SAAS,WAAW;AACjC,MAAI,CAAC,OAAO,SAAS,MAAM,EAAG,UAAS;AACvC,QAAM,MAAM,WAAW,OAAO;AAC9B,MAAI,QAAQ,MAAO,UAAS,UAAU,QAAQ;AAC9C,QAAM,SAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,aAAa,KAAK,EAAG,QAAO,KAAK,CAAC;AACtD,SAAO,EAAE,OAAO,KAAK,UAAU,QAAQ,OAAO,OAAO;AACvD;AAGA,SAAS,YAAY,aAAmC;AACtD,QAAM,SAAmB,CAAC;AAC1B,WAAS,IAAI,GAAG,IAAI,aAAa,KAAK,EAAG,QAAO,KAAK,CAAC;AACtD,SAAO,EAAE,OAAO,aAAa,KAAK,aAAa,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO;AAC1F;AAOA,SAAS,KAAK,cAAsB,aAAqB,aAAqB,UAAkD;AAC9H,MAAI,QAAQ;AACZ,MAAI,QAAQ,cAAc,EAAG,SAAQ,cAAc;AACnD,MAAI,QAAQ,EAAG,SAAQ;AACvB,MAAI,OAAO;AACX,MAAI,OAAO,QAAQ,EAAG,QAAO,QAAQ;AACrC,MAAI,OAAO,YAAa,QAAO;AAC/B,QAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,QAAQ;AAC1C,MAAI,MAAM,KAAK,IAAI,aAAa,OAAO,QAAQ;AAC/C,MAAI,MAAM,MAAO,OAAM;AACvB,SAAO,EAAE,OAAO,IAAI;AACtB;AAWO,SAAS,cAAc,OAAkC;AAC9D,QAAM,QAAQ,QAAQ,OAAO,KAAe;AAC5C,QAAM,cAAc,SAAS,OAAO,aAAa,KAAK;AACtD,MAAI,SAAS,EAAG,QAAO,YAAY,CAAC;AAEpC,QAAM,WAAW,SAAS,OAAO,QAAkB;AACnD,QAAM,WAAW,SAAS,OAAO,QAAkB;AACnD,QAAM,WAAW,WAAW,OAAO,QAAQ;AAC3C,QAAM,cAAc,QAAQ;AAE5B,MAAI,eAAe,EAAG,QAAO,YAAY,WAAW;AAEpD,MAAI,YAAY,GAAG;AAEjB,WAAO,SAAS,EAAE,aAAa,OAAO,aAAa,KAAK,OAAO,UAAU,GAAG,MAAM,GAAG,OAAO,EAAE,CAAC;AAAA,EACjG;AAEA,QAAM,QAAQ,cAAc;AAC5B,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,QAAQ;AAC9C,QAAM,SAAS,KAAK,IAAI,SAAS,OAAO,MAAgB,GAAG,SAAS;AAEpE,QAAM,EAAE,OAAO,UAAU,KAAK,OAAO,IAAI;AAAA,IACvC,KAAK,MAAM,SAAS,QAAQ;AAAA,IAC5B,KAAK,MAAM,SAAS,YAAY,QAAQ;AAAA,IACxC;AAAA,IACA;AAAA,EACF;AAEA,SAAO,SAAS;AAAA,IACd;AAAA,IACA,OAAO,cAAc;AAAA,IACrB,KAAK,cAAc;AAAA,IACnB,UAAU,WAAW;AAAA,IACrB,OAAO,SAAS,YAAY;AAAA,IAC5B;AAAA,EACF,CAAC;AACH;AAWA,IAAM,eAAe,oBAAI,QAA8D;AAEvF,SAAS,UAAU,QAAmC,OAA6B;AACjF,MAAI,UAAU,aAAa,IAAI,MAAM;AACrC,MAAI,CAAC,SAAS;AACZ,cAAU,oBAAI,IAA0B;AACxC,iBAAa,IAAI,QAAQ,OAAO;AAAA,EAClC;AACA,QAAM,SAAS,QAAQ,IAAI,KAAK;AAChC,MAAI,OAAQ,QAAO;AACnB,QAAM,SAAS,IAAI,aAAa,QAAQ,CAAC;AACzC,MAAI,UAAU;AACd,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK,GAAG;AACjC,QAAI,OAAO;AACX,QAAI;AACF,aAAO,OAAO,CAAC;AAAA,IACjB,QAAQ;AACN,aAAO;AAAA,IACT;AACA,eAAW,MAAM,IAAI,KAAK,OAAO,IAAI,OAAO;AAC5C,WAAO,IAAI,CAAC,IAAI;AAAA,EAClB;AACA,UAAQ,IAAI,OAAO,MAAM;AACzB,SAAO;AACT;AAEA,SAAS,GAAG,QAAsB,OAAuB;AACvD,SAAO,OAAO,KAAK,KAAK;AAC1B;AAGA,SAAS,eAAe,QAAsB,IAAY,IAAY,QAAwB;AAC5F,MAAI,IAAI;AACR,MAAI,IAAI;AACR,SAAO,IAAI,GAAG;AACZ,UAAM,MAAM,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC;AACrC,QAAI,GAAG,QAAQ,GAAG,KAAK,OAAQ,KAAI;AAAA,QAC9B,KAAI,MAAM;AAAA,EACjB;AACA,SAAO;AACT;AAGA,SAAS,eAAe,QAAsB,IAAY,IAAY,QAAwB;AAC5F,MAAI,IAAI;AACR,MAAI,IAAI;AACR,SAAO,IAAI,GAAG;AACZ,UAAM,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC;AACtC,QAAI,GAAG,QAAQ,GAAG,KAAK,OAAQ,KAAI;AAAA,QAC9B,KAAI,MAAM;AAAA,EACjB;AACA,SAAO;AACT;AASO,SAAS,sBAAsB,OAA0C;AAC9E,QAAM,QAAQ,QAAQ,OAAO,KAAe;AAC5C,QAAM,cAAc,SAAS,OAAO,aAAa,KAAK;AACtD,MAAI,SAAS,EAAG,QAAO,YAAY,CAAC;AAEpC,QAAM,SAAS,OAAO,OAAO,WAAW,aAAa,MAAM,SAAS,MAAM;AAC1E,QAAM,WAAW,SAAS,OAAO,QAAkB;AACnD,QAAM,WAAW,WAAW,OAAO,QAAQ;AAC3C,QAAM,cAAc,QAAQ;AAE5B,MAAI,eAAe,EAAG,QAAO,YAAY,WAAW;AAEpD,QAAM,SAAS,UAAU,QAAQ,KAAK;AACtC,QAAM,eAAe,GAAG,QAAQ,WAAW;AAC3C,QAAM,QAAQ,GAAG,QAAQ,KAAK,IAAI;AAElC,MAAI,SAAS,GAAG;AACd,WAAO,SAAS,EAAE,aAAa,OAAO,aAAa,KAAK,OAAO,UAAU,GAAG,MAAM,GAAG,OAAO,QAAQ,IAAI,QAAQ,EAAE,CAAC;AAAA,EACrH;AAEA,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,QAAQ;AAC9C,QAAM,SAAS,KAAK,IAAI,SAAS,OAAO,MAAgB,GAAG,SAAS;AAEpE,QAAM,eAAe,eAAe,QAAQ,aAAa,OAAO,eAAe,MAAM,IAAI;AACzF,QAAM,cAAc,eAAe,QAAQ,aAAa,OAAO,eAAe,SAAS,QAAQ,IAAI;AAEnG,QAAM,EAAE,OAAO,UAAU,KAAK,OAAO,IAAI,KAAK,cAAc,aAAa,aAAa,QAAQ;AAC9F,QAAM,WAAW,cAAc;AAC/B,QAAM,SAAS,cAAc;AAE7B,SAAO,SAAS;AAAA,IACd;AAAA,IACA,OAAO;AAAA,IACP,KAAK;AAAA,IACL,UAAU,GAAG,QAAQ,QAAQ,IAAI;AAAA,IACjC,MAAM,GAAG,QAAQ,MAAM,IAAI,GAAG,QAAQ,QAAQ;AAAA,IAC9C;AAAA,EACF,CAAC;AACH;AAkBO,SAAS,qBAAqB,OAA0C;AAC7E,QAAM,QAAQ,QAAQ,OAAO,KAAe;AAC5C,MAAI,SAAS,EAAG,QAAO;AACvB,QAAM,cAAc,SAAS,OAAO,aAAa,KAAK;AACtD,QAAM,WAAW,SAAS,OAAO,QAAkB;AACnD,QAAM,WAAW,SAAS,OAAO,QAAkB;AACnD,QAAM,cAAc,QAAQ;AAC5B,MAAI,eAAe,KAAK,YAAY,EAAG,QAAO;AAE9C,QAAM,WAAW,MAAM,OAAO,KAAK,IAAI,KAAK,MAAM,MAAM,KAAK,IAAI;AACjE,QAAM,QAAQ,KAAK,IAAI,KAAK,IAAI,UAAU,CAAC,GAAG,QAAQ,CAAC;AACvD,MAAI,QAAQ,YAAa,QAAO;AAEhC,QAAM,QAAQ,cAAc;AAC5B,QAAM,YAAY,KAAK,IAAI,GAAG,QAAQ,QAAQ;AAC9C,QAAM,SAAS,KAAK,IAAI,SAAS,OAAO,MAAgB,GAAG,SAAS;AAEpE,QAAM,aAAa,QAAQ,eAAe;AAC1C,QAAM,UAAU,YAAY;AAE5B,MAAI,aAAa,UAAU,WAAW,SAAS,SAAU,QAAO;AAEhE,QAAM,OAAO,YAAY,SAAS,YAAY,UAAU;AACxD,QAAM,UAAU,KAAK,IAAI,KAAK,IAAI,MAAM,CAAC,GAAG,SAAS;AACrD,SAAO,YAAY,SAAS,OAAO;AACrC;","names":[]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Row / column windowing math for the canonical data table.
3
+ *
4
+ * Pure arithmetic: no React, no DOM, no measurement. The table hands in what it
5
+ * knows (how many items, how big they are, how much of them the viewport shows,
6
+ * where the scroller is) and gets back the slice it should actually render plus
7
+ * the two spacers that keep the scrollbar honest.
8
+ *
9
+ * THE INVARIANT this module exists to hold:
10
+ *
11
+ * padStart + (end - start) * itemSize + padEnd === total
12
+ *
13
+ * exactly, for every input. A scrollbar whose thumb size disagrees with the
14
+ * content it can reach is a lie on screen, so `padEnd` is computed as the
15
+ * residual of `total` rather than as an independent multiplication, and the
16
+ * sum is re-checked and corrected once against floating-point drift.
17
+ *
18
+ * PINNED ITEMS (frozen columns) live OUTSIDE the scrollable length. They are
19
+ * always rendered, they are never counted in `padStart`, and the scrollable
20
+ * `total` covers only the items after them. `offset` is therefore measured
21
+ * inside the scrollable region: offset 0 shows item index `pinnedStart`.
22
+ */
23
+ /** Items rendered outside the viewport on each side when the caller says nothing. */
24
+ declare const DEFAULT_OVERSCAN = 6;
25
+ interface WindowInput {
26
+ /** Total item count. */
27
+ count: number;
28
+ /** Fixed item size in px (row height, or column width when windowing columns). */
29
+ itemSize: number;
30
+ /** Visible viewport length in px along the windowed axis. */
31
+ viewport: number;
32
+ /** Current scroll offset in px along that axis. */
33
+ offset: number;
34
+ /** Extra items rendered outside the viewport on each side. Default 6. */
35
+ overscan?: number;
36
+ /** Items pinned at the start that never leave the DOM (frozen columns). Default 0. */
37
+ pinnedStart?: number;
38
+ }
39
+ interface WindowResult {
40
+ /** First rendered index, inclusive. */
41
+ start: number;
42
+ /** Last rendered index, EXCLUSIVE. */
43
+ end: number;
44
+ /** Spacer length in px before `start` (excluding pinned items). */
45
+ padStart: number;
46
+ /** Spacer length in px after `end`. */
47
+ padEnd: number;
48
+ /** Total scrollable length in px. */
49
+ total: number;
50
+ /** Indexes pinned at the start that are always rendered, ascending. */
51
+ pinned: number[];
52
+ }
53
+ interface VariableWindowInput extends Omit<WindowInput, "itemSize"> {
54
+ /** Size in px of the item at `index`. Must be pure and stable for a given index. */
55
+ sizeAt: (index: number) => number;
56
+ }
57
+ /**
58
+ * The slice to render for uniformly sized items.
59
+ *
60
+ * O(1). Never returns `start < 0`, `end > count`, or `end < start`.
61
+ */
62
+ declare function computeWindow(input: WindowInput): WindowResult;
63
+ /**
64
+ * The slice to render for items whose size varies by index.
65
+ *
66
+ * Agrees with `computeWindow` exactly when `sizeAt` is constant. The prefix-sum
67
+ * array is built once per `(sizeAt, count)` pair and then binary-searched, so a
68
+ * scroll costs O(log count) per call after the first O(count) build.
69
+ */
70
+ declare function computeVariableWindow(input: VariableWindowInput): WindowResult;
71
+ interface ScrollToIndexInput extends WindowInput {
72
+ /** The item to bring into view. */
73
+ index: number;
74
+ }
75
+ /**
76
+ * The scroll offset that brings `index` fully into view, or `null` when it is
77
+ * already fully visible (pinned items are always visible, so they return
78
+ * `null` too). Moves the minimum distance: up to the item's own start when it
79
+ * sits above the viewport, down until its end meets the viewport end when it
80
+ * sits below.
81
+ */
82
+ declare function scrollOffsetForIndex(input: ScrollToIndexInput): number | null;
83
+
84
+ export { DEFAULT_OVERSCAN, type ScrollToIndexInput, type VariableWindowInput, type WindowInput, type WindowResult, computeVariableWindow, computeWindow, scrollOffsetForIndex };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Row / column windowing math for the canonical data table.
3
+ *
4
+ * Pure arithmetic: no React, no DOM, no measurement. The table hands in what it
5
+ * knows (how many items, how big they are, how much of them the viewport shows,
6
+ * where the scroller is) and gets back the slice it should actually render plus
7
+ * the two spacers that keep the scrollbar honest.
8
+ *
9
+ * THE INVARIANT this module exists to hold:
10
+ *
11
+ * padStart + (end - start) * itemSize + padEnd === total
12
+ *
13
+ * exactly, for every input. A scrollbar whose thumb size disagrees with the
14
+ * content it can reach is a lie on screen, so `padEnd` is computed as the
15
+ * residual of `total` rather than as an independent multiplication, and the
16
+ * sum is re-checked and corrected once against floating-point drift.
17
+ *
18
+ * PINNED ITEMS (frozen columns) live OUTSIDE the scrollable length. They are
19
+ * always rendered, they are never counted in `padStart`, and the scrollable
20
+ * `total` covers only the items after them. `offset` is therefore measured
21
+ * inside the scrollable region: offset 0 shows item index `pinnedStart`.
22
+ */
23
+ /** Items rendered outside the viewport on each side when the caller says nothing. */
24
+ declare const DEFAULT_OVERSCAN = 6;
25
+ interface WindowInput {
26
+ /** Total item count. */
27
+ count: number;
28
+ /** Fixed item size in px (row height, or column width when windowing columns). */
29
+ itemSize: number;
30
+ /** Visible viewport length in px along the windowed axis. */
31
+ viewport: number;
32
+ /** Current scroll offset in px along that axis. */
33
+ offset: number;
34
+ /** Extra items rendered outside the viewport on each side. Default 6. */
35
+ overscan?: number;
36
+ /** Items pinned at the start that never leave the DOM (frozen columns). Default 0. */
37
+ pinnedStart?: number;
38
+ }
39
+ interface WindowResult {
40
+ /** First rendered index, inclusive. */
41
+ start: number;
42
+ /** Last rendered index, EXCLUSIVE. */
43
+ end: number;
44
+ /** Spacer length in px before `start` (excluding pinned items). */
45
+ padStart: number;
46
+ /** Spacer length in px after `end`. */
47
+ padEnd: number;
48
+ /** Total scrollable length in px. */
49
+ total: number;
50
+ /** Indexes pinned at the start that are always rendered, ascending. */
51
+ pinned: number[];
52
+ }
53
+ interface VariableWindowInput extends Omit<WindowInput, "itemSize"> {
54
+ /** Size in px of the item at `index`. Must be pure and stable for a given index. */
55
+ sizeAt: (index: number) => number;
56
+ }
57
+ /**
58
+ * The slice to render for uniformly sized items.
59
+ *
60
+ * O(1). Never returns `start < 0`, `end > count`, or `end < start`.
61
+ */
62
+ declare function computeWindow(input: WindowInput): WindowResult;
63
+ /**
64
+ * The slice to render for items whose size varies by index.
65
+ *
66
+ * Agrees with `computeWindow` exactly when `sizeAt` is constant. The prefix-sum
67
+ * array is built once per `(sizeAt, count)` pair and then binary-searched, so a
68
+ * scroll costs O(log count) per call after the first O(count) build.
69
+ */
70
+ declare function computeVariableWindow(input: VariableWindowInput): WindowResult;
71
+ interface ScrollToIndexInput extends WindowInput {
72
+ /** The item to bring into view. */
73
+ index: number;
74
+ }
75
+ /**
76
+ * The scroll offset that brings `index` fully into view, or `null` when it is
77
+ * already fully visible (pinned items are always visible, so they return
78
+ * `null` too). Moves the minimum distance: up to the item's own start when it
79
+ * sits above the viewport, down until its end meets the viewport end when it
80
+ * sits below.
81
+ */
82
+ declare function scrollOffsetForIndex(input: ScrollToIndexInput): number | null;
83
+
84
+ export { DEFAULT_OVERSCAN, type ScrollToIndexInput, type VariableWindowInput, type WindowInput, type WindowResult, computeVariableWindow, computeWindow, scrollOffsetForIndex };