gbs-add-block 2.0.4 → 2.3.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 (129) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +190 -134
  2. package/.gbs/skills/gbs-components/references/install.md +25 -3
  3. package/.gbs/skills/gbs-components/references/styling.md +246 -201
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +157 -11
  6. package/index.cjs +212 -3
  7. package/package.json +41 -10
  8. package/schema/passport-v1.schema.json +204 -0
  9. package/source/beta-components/accordion/passport.json +259 -0
  10. package/source/beta-components/accordion/styles.css +207 -208
  11. package/source/beta-components/alert/passport.json +250 -0
  12. package/source/beta-components/alert/styles.css +154 -155
  13. package/source/beta-components/avatar/passport.json +294 -0
  14. package/source/beta-components/avatar/styles.css +201 -203
  15. package/source/beta-components/badge/passport.json +332 -0
  16. package/source/beta-components/badge/styles.css +203 -204
  17. package/source/beta-components/breadcrumb/passport.json +243 -0
  18. package/source/beta-components/breadcrumb/styles.css +138 -139
  19. package/source/beta-components/button/passport.json +402 -0
  20. package/source/beta-components/button/passport.manual.json +31 -0
  21. package/source/beta-components/button/styles.css +232 -233
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -231
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -212
  26. package/source/beta-components/combobox/passport.json +456 -0
  27. package/source/beta-components/combobox/styles.css +419 -417
  28. package/source/beta-components/data-grid/agent/coerce.ts +368 -0
  29. package/source/beta-components/data-grid/agent/contract.ts +410 -0
  30. package/source/beta-components/data-grid/agent/dataset.ts +92 -0
  31. package/source/beta-components/data-grid/agent/engine.ts +470 -0
  32. package/source/beta-components/data-grid/agent/executors.ts +155 -0
  33. package/source/beta-components/data-grid/agent/index.ts +79 -0
  34. package/source/beta-components/data-grid/agent/intent.ts +324 -0
  35. package/source/beta-components/data-grid/agent/operations.ts +335 -0
  36. package/source/beta-components/data-grid/agent/validate.ts +630 -0
  37. package/source/beta-components/data-grid/agent/webmcp.ts +107 -0
  38. package/source/beta-components/data-grid/index.ts +14 -7
  39. package/source/beta-components/data-grid/passport.json +1051 -0
  40. package/source/beta-components/data-grid/passport.manual.json +255 -0
  41. package/source/beta-components/data-grid/react/AskGrid.tsx +164 -0
  42. package/source/beta-components/data-grid/react/DataGrid.tsx +39 -0
  43. package/source/beta-components/data-grid/styles.css +874 -717
  44. package/source/beta-components/date-picker/passport.json +407 -0
  45. package/source/beta-components/date-picker/styles.css +445 -446
  46. package/source/beta-components/dialog/passport.json +344 -0
  47. package/source/beta-components/dialog/styles.css +280 -278
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -395
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -296
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -222
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -239
  56. package/source/beta-components/number-input/passport.json +541 -0
  57. package/source/beta-components/number-input/styles.css +230 -231
  58. package/source/beta-components/popover/passport.json +238 -0
  59. package/source/beta-components/popover/styles.css +148 -146
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -202
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +241 -242
  64. package/source/beta-components/shared/core/agent/adapter.ts +65 -0
  65. package/source/beta-components/shared/core/agent/history.ts +120 -0
  66. package/source/beta-components/shared/core/agent/index.ts +46 -0
  67. package/source/beta-components/shared/core/agent/numbers.ts +217 -0
  68. package/source/beta-components/shared/core/agent/schema.ts +180 -0
  69. package/source/beta-components/shared/core/agent/types.ts +169 -0
  70. package/source/beta-components/shared/core/agent/webmcp.ts +328 -0
  71. package/source/beta-components/shared/index.ts +9 -0
  72. package/source/beta-components/shared/react/GramproAIProvider.tsx +50 -0
  73. package/source/beta-components/shared/react/useAskAgent.ts +217 -0
  74. package/source/beta-components/shared/styles.css +79 -0
  75. package/source/beta-components/shared/version.json +4 -4
  76. package/source/beta-components/shared/version.ts +6 -6
  77. package/source/beta-components/skeleton/passport.json +251 -0
  78. package/source/beta-components/skeleton/styles.css +185 -186
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +145 -146
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +194 -196
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +234 -235
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -159
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -283
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -72
  91. package/tools/env.cjs +61 -0
  92. package/tools/passport/cli.cjs +79 -0
  93. package/tools/passport/extract.cjs +493 -0
  94. package/tools/passport/index.cjs +185 -0
  95. package/tools/passport/merge.cjs +131 -0
  96. package/tools/passport/policy.cjs +65 -0
  97. package/tools/passport/validate.cjs +277 -0
  98. package/tools/ts-require.cjs +79 -0
  99. package/source/beta-components/accordion/__tests__/core.test.ts +0 -58
  100. package/source/beta-components/alert/__tests__/core.test.ts +0 -17
  101. package/source/beta-components/avatar/__tests__/core.test.ts +0 -88
  102. package/source/beta-components/badge/__tests__/core.test.ts +0 -46
  103. package/source/beta-components/breadcrumb/__tests__/core.test.ts +0 -58
  104. package/source/beta-components/button/__tests__/core.test.ts +0 -31
  105. package/source/beta-components/card/__tests__/core.test.ts +0 -57
  106. package/source/beta-components/checkbox/__tests__/core.test.ts +0 -40
  107. package/source/beta-components/combobox/__tests__/core.test.ts +0 -134
  108. package/source/beta-components/data-grid/__tests__/core.test.ts +0 -356
  109. package/source/beta-components/data-grid/__tests__/export.test.ts +0 -70
  110. package/source/beta-components/data-grid/__tests__/pdf.test.ts +0 -209
  111. package/source/beta-components/date-picker/__tests__/core.test.ts +0 -273
  112. package/source/beta-components/dialog/__tests__/core.test.ts +0 -86
  113. package/source/beta-components/file-uploader/__tests__/core.test.ts +0 -395
  114. package/source/beta-components/input/__tests__/core.test.ts +0 -75
  115. package/source/beta-components/menu/__tests__/core.test.ts +0 -120
  116. package/source/beta-components/modal/__tests__/core.test.ts +0 -55
  117. package/source/beta-components/number-input/__tests__/core.test.ts +0 -151
  118. package/source/beta-components/progress/__tests__/core.test.ts +0 -56
  119. package/source/beta-components/radio-group/__tests__/core.test.ts +0 -64
  120. package/source/beta-components/shared/__tests__/boundaries.test.ts +0 -95
  121. package/source/beta-components/shared/__tests__/core.test.ts +0 -55
  122. package/source/beta-components/shared/__tests__/position.test.ts +0 -143
  123. package/source/beta-components/skeleton/__tests__/core.test.ts +0 -41
  124. package/source/beta-components/spinner/__tests__/core.test.ts +0 -48
  125. package/source/beta-components/switch/__tests__/core.test.ts +0 -64
  126. package/source/beta-components/tabs/__tests__/core.test.ts +0 -51
  127. package/source/beta-components/textarea/__tests__/core.test.ts +0 -38
  128. package/source/beta-components/toaster/__tests__/core.test.ts +0 -256
  129. package/source/beta-components/tooltip/__tests__/core.test.ts +0 -42
@@ -0,0 +1,410 @@
1
+ /*
2
+ * The runtime contract: what *this* grid can do, right now.
3
+ *
4
+ * The passport says "DataGrid can filter, sort, search, select and export".
5
+ * That is true of every DataGrid ever rendered and so tells a caller nothing
6
+ * about the one in front of it. The runtime contract is the other half:
7
+ *
8
+ * columns revenue is a number, filterable, measured in INR
9
+ * capabilities this instance has selection off and pinning on
10
+ * state two filters and a descending sort are active
11
+ * stats 1,842 of 9,310 rows match
12
+ *
13
+ * Three rules hold throughout:
14
+ *
15
+ * - No rows. Counts and bounded summaries only. A contract is the thing
16
+ * most likely to be handed to a model, and rows are the thing most likely
17
+ * to be regretted.
18
+ * - Nothing invented. Every capability traces to a grid option or a
19
+ * resolved column flag. There is no `grouping`, because there is no
20
+ * grouping.
21
+ * - Effective, not nominal. Policy is applied here, so a column the host
22
+ * forbade reports `filterable: false` and carries no summary.
23
+ */
24
+
25
+ import { getFilterOperators } from "../core/filtering";
26
+ import type {
27
+ ColumnOption,
28
+ ColumnType,
29
+ Density,
30
+ FilterOperator,
31
+ FilterValue,
32
+ GridOptions,
33
+ ResolvedColumn,
34
+ } from "../core/types";
35
+ import { toDateInputValue, toTime } from "../core/values";
36
+ import type { GridDataset } from "./dataset";
37
+ import { GRID_OPERATIONS, type GridOperationName } from "./operations";
38
+
39
+ export const GRID_CONTRACT_VERSION = "1.0.0";
40
+
41
+ // ------------------------------------------------------------------ authored
42
+
43
+ /**
44
+ * What a machine cannot read off the column definition. Supplied by the host;
45
+ * nothing here is guessed, and an absent entry simply means less context.
46
+ */
47
+ export interface GridColumnSemantics {
48
+ /** What the column means, in words, for a model's prompt. */
49
+ description?: string;
50
+ /** Display unit. An ISO 4217 code such as `INR` also drives money formatting. */
51
+ unit?: string;
52
+ /** How a percent column stores its numbers: 20 (`whole`) or 0.2 (`fraction`). */
53
+ percentBasis?: "whole" | "fraction";
54
+ /** Which end is the good end. Lets "worst performing" resolve to a direction. */
55
+ higherIsBetter?: boolean;
56
+ /** Other words a person might use for this column. */
57
+ synonyms?: readonly string[];
58
+ /** Personal data. Denied by default — see `GridAgentPolicy.denyPii`. */
59
+ pii?: boolean;
60
+ }
61
+
62
+ export interface GridAgentPolicy {
63
+ /** Columns no operation may touch. */
64
+ deny?: readonly string[];
65
+ /** Columns that may not be filtered or searched on. */
66
+ denyFilter?: readonly string[];
67
+ /** Treat columns marked `pii` as denied. Default true. */
68
+ denyPii?: boolean;
69
+ /** Operations withheld from this instance. */
70
+ denyOperations?: readonly GridOperationName[];
71
+ /** Refuse an export of more rows than this. */
72
+ maxExportRows?: number;
73
+ /** Ask before exporting more rows than this. Default 5000. */
74
+ confirmExportRows?: number;
75
+ allowExportFormats?: readonly ("csv" | "excel" | "pdf")[];
76
+ /** Rows one `selectRows` may name. Default 1000. */
77
+ maxSelectRows?: number;
78
+ /** Warn when a filter would leave nothing. Default true. */
79
+ warnOnEmptyResult?: boolean;
80
+ }
81
+
82
+ // ------------------------------------------------------------------ contract
83
+
84
+ export type GridColumnStats =
85
+ | { kind: "number"; min: number; max: number; nulls: number }
86
+ | { kind: "date"; min: string; max: string; nulls: number }
87
+ | { kind: "boolean"; true: number; false: number; nulls: number }
88
+ | { kind: "string"; distinct: number; nulls: number; values?: string[] };
89
+
90
+ export interface GridContractColumn {
91
+ id: string;
92
+ label: string;
93
+ type: ColumnType;
94
+ visible: boolean;
95
+ pinned: "left" | "right" | null;
96
+ sortable: boolean;
97
+ filterable: boolean;
98
+ searchable: boolean;
99
+ hideable: boolean;
100
+ pinnable: boolean;
101
+ reorderable: boolean;
102
+ resizable: boolean;
103
+ exportable: boolean;
104
+ editable: boolean;
105
+ /** Operators legal for this column's type. Empty when it cannot be filtered. */
106
+ operators: FilterOperator[];
107
+ /** Fixed choices from the column definition. */
108
+ options?: ColumnOption[];
109
+ unit?: string;
110
+ percentBasis?: "whole" | "fraction";
111
+ higherIsBetter?: boolean;
112
+ synonyms?: string[];
113
+ description?: string;
114
+ /** Present when policy, not the grid, is what closed this column down. */
115
+ restricted?: "policy";
116
+ stats?: GridColumnStats;
117
+ }
118
+
119
+ export interface GridCapabilities {
120
+ mode: "client" | "server";
121
+ sorting: boolean;
122
+ multiSort: boolean;
123
+ filtering: boolean;
124
+ globalSearch: boolean;
125
+ pagination: boolean;
126
+ selection: boolean;
127
+ selectionMode: "single" | "multiple";
128
+ columnResizing: boolean;
129
+ columnReordering: boolean;
130
+ columnPinning: boolean;
131
+ columnHiding: boolean;
132
+ /** Cells can be edited in place. No operation exposes this; it is context. */
133
+ editing: boolean;
134
+ exportFormats: ("csv" | "excel" | "pdf")[];
135
+ print: boolean;
136
+ clipboard: boolean;
137
+ undo: boolean;
138
+ }
139
+
140
+ export interface GridContractState {
141
+ sorting: { column: string; direction: "asc" | "desc" }[];
142
+ filters: { column: string; operator: FilterOperator; value?: FilterValue; value2?: FilterValue }[];
143
+ search: string;
144
+ page: { index: number; size: number; count: number };
145
+ selection: { count: number; all: boolean };
146
+ density: Density;
147
+ hiddenColumns: string[];
148
+ pinnedColumns: { left: string[]; right: string[] };
149
+ canUndo: boolean;
150
+ canRedo: boolean;
151
+ }
152
+
153
+ export interface GridContractStats {
154
+ /** Rows behind the grid. Null in server mode unless `rowCount` was given. */
155
+ totalRows: number | null;
156
+ /** Rows matching the current query. Null in server mode. */
157
+ filteredRows: number | null;
158
+ pageRows: number;
159
+ selectedRows: number;
160
+ }
161
+
162
+ export interface GridRuntimeContract {
163
+ contract: "gbs.datagrid";
164
+ contractVersion: string;
165
+ component: "DataGrid";
166
+ /** Stable for a given set of columns; pass your own to make it meaningful. */
167
+ instanceId: string;
168
+ capabilities: GridCapabilities;
169
+ columns: GridContractColumn[];
170
+ /** Operation names this instance offers, after capabilities and policy. */
171
+ operations: GridOperationName[];
172
+ state: GridContractState;
173
+ stats: GridContractStats;
174
+ }
175
+
176
+ // --------------------------------------------------------------------- build
177
+
178
+ /** FNV-1a. Short, stable, and not a security boundary. */
179
+ function hash(text: string): string {
180
+ let h = 0x811c9dc5;
181
+ for (let i = 0; i < text.length; i++) {
182
+ h ^= text.charCodeAt(i);
183
+ h = Math.imul(h, 0x01000193) >>> 0;
184
+ }
185
+ return h.toString(36);
186
+ }
187
+
188
+ function summarise<T>(
189
+ column: ResolvedColumn<T>,
190
+ values: unknown[],
191
+ maxEnumValues: number,
192
+ ): GridColumnStats | undefined {
193
+ const present = values.filter((v) => v !== null && v !== undefined && v !== "");
194
+ const nulls = values.length - present.length;
195
+ if (present.length === 0) return undefined;
196
+
197
+ switch (column.type) {
198
+ case "number": {
199
+ const numbers = present.map(Number).filter((n) => !Number.isNaN(n));
200
+ if (numbers.length === 0) return undefined;
201
+ return { kind: "number", min: Math.min(...numbers), max: Math.max(...numbers), nulls };
202
+ }
203
+ case "date": {
204
+ const times = present.map(toTime).filter((t): t is number => t !== null);
205
+ if (times.length === 0) return undefined;
206
+ // Local, not UTC: a date-only value is read as a local date, so
207
+ // formatting it back through UTC would shift it east of Greenwich.
208
+ return {
209
+ kind: "date",
210
+ min: toDateInputValue(Math.min(...times)),
211
+ max: toDateInputValue(Math.max(...times)),
212
+ nulls,
213
+ };
214
+ }
215
+ case "boolean": {
216
+ let yes = 0;
217
+ for (const value of present) if (value) yes += 1;
218
+ return { kind: "boolean", true: yes, false: present.length - yes, nulls };
219
+ }
220
+ default: {
221
+ const distinct = new Set(present.map(String));
222
+ const stats: GridColumnStats = { kind: "string", distinct: distinct.size, nulls };
223
+ /*
224
+ * Category columns get their values listed, which is what stops a model
225
+ * inventing a region that does not exist. Identifier columns must not:
226
+ * a name or an invoice number is the data itself, and in a small table
227
+ * every column looks low-cardinality. The test is whether any value
228
+ * repeats — a category has fewer distinct values than it has rows, an
229
+ * identifier has exactly as many.
230
+ */
231
+ if (distinct.size <= maxEnumValues && distinct.size < present.length) {
232
+ stats.values = [...distinct].sort();
233
+ }
234
+ return stats;
235
+ }
236
+ }
237
+ }
238
+
239
+ export interface BuildContractOptions<T> {
240
+ options: GridOptions<T>;
241
+ dataset: GridDataset<T>;
242
+ state: import("../core/types").GridState;
243
+ instanceId?: string;
244
+ semantics?: Readonly<Record<string, GridColumnSemantics>>;
245
+ policy?: GridAgentPolicy;
246
+ /** Column summaries and derived enum values. Default true. */
247
+ stats?: boolean;
248
+ /** Above this many distinct values a string column stops listing them. Default 20. */
249
+ maxEnumValues?: number;
250
+ canUndo?: boolean;
251
+ canRedo?: boolean;
252
+ }
253
+
254
+ export function buildGridContract<T>(input: BuildContractOptions<T>): GridRuntimeContract {
255
+ const {
256
+ options,
257
+ dataset,
258
+ state,
259
+ semantics = {},
260
+ policy = {},
261
+ stats: wantStats = true,
262
+ maxEnumValues = 20,
263
+ canUndo = false,
264
+ canRedo = false,
265
+ } = input;
266
+
267
+ const denyPii = policy.denyPii !== false;
268
+ const denied = new Set(policy.deny ?? []);
269
+ const deniedFilter = new Set(policy.denyFilter ?? []);
270
+
271
+ const pagination = options.enablePagination !== false;
272
+ const selection = options.enableRowSelection !== undefined && options.enableRowSelection !== false;
273
+ const filtering = options.enableFiltering !== false;
274
+
275
+ const columns: GridContractColumn[] = dataset.columns.map((column) => {
276
+ const meta = semantics[column.id] ?? {};
277
+ const off = denied.has(column.id) || (denyPii && meta.pii === true);
278
+ const noFilter = off || deniedFilter.has(column.id);
279
+ const visible = state.columnVisibility[column.id] !== false;
280
+ const pinned = state.columnPinning.left.includes(column.id)
281
+ ? "left"
282
+ : state.columnPinning.right.includes(column.id)
283
+ ? "right"
284
+ : null;
285
+
286
+ const filterable = column.filterable && filtering && !noFilter;
287
+ const entry: GridContractColumn = {
288
+ id: column.id,
289
+ label: column.header,
290
+ type: column.type,
291
+ visible,
292
+ pinned,
293
+ sortable: column.sortable && options.enableSorting !== false && !off,
294
+ filterable,
295
+ searchable: column.searchable && filtering && !noFilter,
296
+ hideable: column.hideable && options.enableColumnHiding !== false,
297
+ pinnable: column.pinnable && options.enableColumnPinning !== false,
298
+ reorderable: column.reorderable && options.enableColumnReordering !== false,
299
+ resizable: column.resizable && options.enableColumnResizing !== false,
300
+ exportable: column.exportable && !off,
301
+ editable: column.def.editable !== undefined && column.def.editable !== false,
302
+ operators: filterable ? getFilterOperators(column) : [],
303
+ };
304
+
305
+ if (column.def.options) entry.options = [...column.def.options];
306
+ if (meta.unit) entry.unit = meta.unit;
307
+ if (meta.percentBasis) entry.percentBasis = meta.percentBasis;
308
+ if (meta.higherIsBetter !== undefined) entry.higherIsBetter = meta.higherIsBetter;
309
+ if (meta.synonyms?.length) entry.synonyms = [...meta.synonyms];
310
+ if (meta.description) entry.description = meta.description;
311
+ if (off) entry.restricted = "policy";
312
+
313
+ // A restricted column is named so a caller knows why it cannot be used,
314
+ // and summarised never, because a summary is the data in miniature.
315
+ if (wantStats && !off && !entry.options) {
316
+ const summary = summarise(column, dataset.values(column.id), maxEnumValues);
317
+ if (summary) entry.stats = summary;
318
+ }
319
+
320
+ return entry;
321
+ });
322
+
323
+ const capabilities: GridCapabilities = {
324
+ mode: options.mode ?? "client",
325
+ sorting: options.enableSorting !== false,
326
+ multiSort: options.enableSorting !== false && options.enableMultiSort !== false,
327
+ filtering,
328
+ globalSearch: filtering,
329
+ pagination,
330
+ selection,
331
+ selectionMode: options.selectionMode ?? "multiple",
332
+ columnResizing: options.enableColumnResizing !== false,
333
+ columnReordering: options.enableColumnReordering !== false,
334
+ columnPinning: options.enableColumnPinning !== false,
335
+ columnHiding: options.enableColumnHiding !== false,
336
+ editing: columns.some((column) => column.editable) && options.onCellEdit !== undefined,
337
+ exportFormats: [...(policy.allowExportFormats ?? ["csv", "excel", "pdf"])],
338
+ print: true,
339
+ clipboard: true,
340
+ undo: true,
341
+ };
342
+
343
+ const available: Record<NonNullable<import("./operations").GridOperationDefinition["requires"]>, boolean> = {
344
+ sorting: capabilities.sorting,
345
+ filtering: capabilities.filtering,
346
+ selection: capabilities.selection,
347
+ pagination: capabilities.pagination,
348
+ columnHiding: capabilities.columnHiding,
349
+ columnPinning: capabilities.columnPinning,
350
+ columnReordering: capabilities.columnReordering,
351
+ columnResizing: capabilities.columnResizing,
352
+ };
353
+ const withheld = new Set(policy.denyOperations ?? []);
354
+ if (capabilities.exportFormats.length === 0) withheld.add("export");
355
+
356
+ const operations = (Object.values(GRID_OPERATIONS) as (typeof GRID_OPERATIONS)[GridOperationName][])
357
+ .filter((op) => !withheld.has(op.name) && (op.requires === undefined || available[op.requires]))
358
+ .map((op) => op.name);
359
+
360
+ const filteredRows = dataset.count(state.filters, state.globalFilter);
361
+ const totalRows = dataset.total;
362
+ const pageSize = Math.max(1, state.pagination.pageSize);
363
+ const effectiveTotal = filteredRows ?? totalRows ?? dataset.rows.length;
364
+ const pageCount = pagination ? Math.max(1, Math.ceil(effectiveTotal / pageSize)) : 1;
365
+ const pageIndex = Math.min(Math.max(0, state.pagination.pageIndex), pageCount - 1);
366
+ const selectedRows = Object.values(state.rowSelection).filter(Boolean).length;
367
+
368
+ const contractState: GridContractState = {
369
+ sorting: state.sorting.map((item) => ({
370
+ column: item.columnId,
371
+ direction: item.desc ? "desc" : "asc",
372
+ })),
373
+ filters: state.filters.map((filter) => ({
374
+ column: filter.columnId,
375
+ operator: filter.operator,
376
+ ...(filter.value !== undefined ? { value: filter.value } : {}),
377
+ ...(filter.value2 !== undefined ? { value2: filter.value2 } : {}),
378
+ })),
379
+ search: state.globalFilter,
380
+ page: { index: pageIndex, size: pageSize, count: pageCount },
381
+ selection: {
382
+ count: selectedRows,
383
+ all: filteredRows !== null && filteredRows > 0 && selectedRows >= filteredRows,
384
+ },
385
+ density: state.density,
386
+ hiddenColumns: columns.filter((column) => !column.visible).map((column) => column.id),
387
+ pinnedColumns: { left: [...state.columnPinning.left], right: [...state.columnPinning.right] },
388
+ canUndo,
389
+ canRedo,
390
+ };
391
+
392
+ return {
393
+ contract: "gbs.datagrid",
394
+ contractVersion: GRID_CONTRACT_VERSION,
395
+ component: "DataGrid",
396
+ instanceId: input.instanceId ?? `dg-${hash(dataset.columns.map((c) => c.id).join("|"))}`,
397
+ capabilities,
398
+ columns,
399
+ operations,
400
+ state: contractState,
401
+ stats: {
402
+ totalRows,
403
+ filteredRows,
404
+ pageRows: pagination
405
+ ? Math.min(pageSize, Math.max(0, effectiveTotal - pageIndex * pageSize))
406
+ : effectiveTotal,
407
+ selectedRows,
408
+ },
409
+ };
410
+ }
@@ -0,0 +1,92 @@
1
+ /*
2
+ * The rows and columns the agent layer is allowed to look at.
3
+ *
4
+ * Everything else in `agent/` goes through this: it is the one place that
5
+ * touches the data, which makes the privacy rule easy to hold — the runtime
6
+ * contract reports counts and bounded summaries from here, never rows.
7
+ *
8
+ * The grid's own `resolveColumns`, `filterRows` and `paginate` are reused
9
+ * rather than reimplemented, so a dry run counts exactly the rows the grid
10
+ * would show.
11
+ */
12
+
13
+ import { resolveColumns } from "../core/columns";
14
+ import { filterRows } from "../core/filtering";
15
+ import { buildRows, createRowIdGetter } from "../core/rows";
16
+ import type {
17
+ ColumnFilter,
18
+ GridOptions,
19
+ GridRow,
20
+ ResolvedColumn,
21
+ RowId,
22
+ } from "../core/types";
23
+ import { createFormatters, type Formatters } from "../core/values";
24
+
25
+ export interface GridDataset<T> {
26
+ columns: ResolvedColumn<T>[];
27
+ columnById: Map<string, ResolvedColumn<T>>;
28
+ rows: GridRow<T>[];
29
+ formatters: Formatters;
30
+ /** True in server mode, where `rows` is only the page the server sent. */
31
+ partial: boolean;
32
+ /** Total rows behind the grid, or null when only the server knows. */
33
+ total: number | null;
34
+ hasRowId(id: RowId): boolean;
35
+ /** How many rows a query would show. Null when the data is only a page. */
36
+ count(filters: readonly ColumnFilter[], globalFilter: string): number | null;
37
+ /** Raw values of one column, for summary statistics. */
38
+ values(columnId: string): unknown[];
39
+ }
40
+
41
+ const keyOf = (filters: readonly ColumnFilter[], globalFilter: string) =>
42
+ `${globalFilter}\u0000${JSON.stringify(filters)}`;
43
+
44
+ export function createDataset<T>(options: GridOptions<T>, locale?: string): GridDataset<T> {
45
+ const columns = resolveColumns(options.columns);
46
+ const columnById = new Map(columns.map((column) => [column.id, column]));
47
+ const rows = buildRows(options.data, createRowIdGetter(options.getRowId));
48
+ const formatters = createFormatters(locale);
49
+ const partial = options.mode === "server";
50
+
51
+ let ids: Set<RowId> | null = null;
52
+ const counts = new Map<string, number>();
53
+ const valueCache = new Map<string, unknown[]>();
54
+
55
+ return {
56
+ columns,
57
+ columnById,
58
+ rows,
59
+ formatters,
60
+ partial,
61
+ total: partial ? (options.rowCount ?? null) : rows.length,
62
+
63
+ hasRowId(id) {
64
+ ids ??= new Set(rows.map((row) => row.id));
65
+ return ids.has(id);
66
+ },
67
+
68
+ count(filters, globalFilter) {
69
+ // In server mode the client holds one page, so any count would be a lie.
70
+ if (partial) return null;
71
+ const key = keyOf(filters, globalFilter);
72
+ let hit = counts.get(key);
73
+ if (hit === undefined) {
74
+ hit = filterRows(rows, columns, filters, globalFilter, formatters).length;
75
+ // A handful of distinct queries per interaction; this is a guard, not a cache policy.
76
+ if (counts.size > 64) counts.clear();
77
+ counts.set(key, hit);
78
+ }
79
+ return hit;
80
+ },
81
+
82
+ values(columnId) {
83
+ let hit = valueCache.get(columnId);
84
+ if (!hit) {
85
+ const column = columnById.get(columnId);
86
+ hit = column ? rows.map((row) => column.getValue(row.original)) : [];
87
+ valueCache.set(columnId, hit);
88
+ }
89
+ return hit;
90
+ },
91
+ };
92
+ }