gbs-add-block 2.1.0 → 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 -207
  4. package/CHANGELOG.md +74 -0
  5. package/README.md +154 -23
  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 +225 -226
  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 -140
  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 -234
  22. package/source/beta-components/card/passport.json +337 -0
  23. package/source/beta-components/card/styles.css +230 -232
  24. package/source/beta-components/checkbox/passport.json +456 -0
  25. package/source/beta-components/checkbox/styles.css +211 -213
  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 -716
  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 -279
  48. package/source/beta-components/file-uploader/passport.json +518 -0
  49. package/source/beta-components/file-uploader/styles.css +394 -396
  50. package/source/beta-components/input/passport.json +536 -0
  51. package/source/beta-components/input/styles.css +295 -297
  52. package/source/beta-components/menu/passport.json +322 -0
  53. package/source/beta-components/menu/styles.css +224 -223
  54. package/source/beta-components/modal/passport.json +289 -0
  55. package/source/beta-components/modal/styles.css +241 -240
  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 -147
  60. package/source/beta-components/progress/passport.json +270 -0
  61. package/source/beta-components/progress/styles.css +200 -201
  62. package/source/beta-components/radio-group/passport.json +477 -0
  63. package/source/beta-components/radio-group/styles.css +269 -270
  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 -187
  79. package/source/beta-components/spinner/passport.json +245 -0
  80. package/source/beta-components/spinner/styles.css +173 -174
  81. package/source/beta-components/switch/passport.json +421 -0
  82. package/source/beta-components/switch/styles.css +227 -229
  83. package/source/beta-components/tabs/passport.json +315 -0
  84. package/source/beta-components/tabs/styles.css +263 -264
  85. package/source/beta-components/textarea/passport.json +382 -0
  86. package/source/beta-components/textarea/styles.css +158 -160
  87. package/source/beta-components/toaster/passport.json +221 -0
  88. package/source/beta-components/toaster/styles.css +282 -282
  89. package/source/beta-components/tooltip/passport.json +170 -0
  90. package/source/beta-components/tooltip/styles.css +71 -73
  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,255 @@
1
+ {
2
+ "$schema": "../../../schema/passport-v1.schema.json",
3
+ "component": "DataGrid",
4
+ "purpose": "Display and operate on tabular data. Virtualized; holds its own sort, filter, search, selection, column and pagination state in an external store exposed through GridApi.",
5
+ "intentDomains": ["Query", "ActionIntent", "Extraction"],
6
+
7
+ "$operationsNote": "Mirrors agent/operations.ts, which is what the runtime actually dispatches on. tools/eval/__tests__/corpus.test.ts fails if the two drift. `apiMethod` is present only where one GridApi method performs the operation.",
8
+
9
+ "operations": {
10
+ "search": {
11
+ "summary": "Free-text match across every searchable column.",
12
+ "apiMethod": "setGlobalFilter",
13
+ "input": { "type": "object", "required": ["text"], "properties": { "text": { "type": "string", "maxLength": 200 } } },
14
+ "effects": ["filters visible rows", "resets to page 0"],
15
+ "reversible": true
16
+ },
17
+ "filter": {
18
+ "summary": "Apply a typed filter to one column. Operators are constrained by the column's type.",
19
+ "apiMethod": "setFilter",
20
+ "input": {
21
+ "type": "object",
22
+ "required": ["column", "operator"],
23
+ "properties": {
24
+ "column": { "type": "string", "description": "Column id from the runtime contract." },
25
+ "operator": {
26
+ "enum": ["contains", "notContains", "equals", "notEquals", "startsWith", "endsWith",
27
+ "gt", "gte", "lt", "lte", "between", "before", "after", "in", "isEmpty", "isNotEmpty"]
28
+ },
29
+ "value": {},
30
+ "value2": { "description": "Upper bound; only for the `between` operator." }
31
+ }
32
+ },
33
+ "effects": ["filters visible rows", "resets to page 0"],
34
+ "reversible": true
35
+ },
36
+ "clearFilters": {
37
+ "summary": "Remove every column filter and the search text.",
38
+ "apiMethod": "clearFilters",
39
+ "effects": ["restores every row"],
40
+ "reversible": true
41
+ },
42
+ "sort": {
43
+ "summary": "Sort by one column, replacing the current sort unless `append` is set.",
44
+ "apiMethod": "setSorting",
45
+ "input": {
46
+ "type": "object",
47
+ "required": ["column", "direction"],
48
+ "properties": {
49
+ "column": { "type": "string" },
50
+ "direction": { "enum": ["asc", "desc"] },
51
+ "append": { "type": "boolean", "default": false, "description": "Add to the existing sort instead of replacing it." }
52
+ }
53
+ },
54
+ "effects": ["reorders visible rows"],
55
+ "reversible": true
56
+ },
57
+ "clearSort": {
58
+ "summary": "Return to the data's original order.",
59
+ "apiMethod": "setSorting",
60
+ "effects": ["reorders visible rows"],
61
+ "reversible": true
62
+ },
63
+ "selectRows": {
64
+ "summary": "Select or deselect rows by id.",
65
+ "apiMethod": "toggleRowSelected",
66
+ "input": {
67
+ "type": "object",
68
+ "required": ["rowIds"],
69
+ "properties": {
70
+ "rowIds": { "type": "array", "items": { "type": "string" }, "minItems": 1 },
71
+ "value": { "type": "boolean", "default": true }
72
+ }
73
+ },
74
+ "effects": ["changes rowSelection"],
75
+ "reversible": true
76
+ },
77
+ "selectAll": {
78
+ "summary": "Select, or clear, every row matching the current filters.",
79
+ "apiMethod": "toggleAllRowsSelected",
80
+ "input": { "type": "object", "properties": { "value": { "type": "boolean", "default": true } } },
81
+ "effects": ["changes rowSelection"],
82
+ "reversible": true
83
+ },
84
+ "clearSelection": {
85
+ "summary": "Deselect every row.",
86
+ "apiMethod": "clearSelection",
87
+ "effects": ["changes rowSelection"],
88
+ "reversible": true
89
+ },
90
+ "setColumnVisibility": {
91
+ "summary": "Show or hide one column.",
92
+ "apiMethod": "setColumnVisibility",
93
+ "input": {
94
+ "type": "object",
95
+ "required": ["column", "visible"],
96
+ "properties": { "column": { "type": "string" }, "visible": { "type": "boolean" } }
97
+ },
98
+ "effects": ["changes columnVisibility"],
99
+ "reversible": true
100
+ },
101
+ "pinColumn": {
102
+ "summary": "Pin a column to an edge, or unpin it with a null side.",
103
+ "apiMethod": "pinColumn",
104
+ "input": {
105
+ "type": "object",
106
+ "required": ["column", "side"],
107
+ "properties": { "column": { "type": "string" }, "side": { "enum": ["left", "right", null] } }
108
+ },
109
+ "effects": ["changes columnPinning"],
110
+ "reversible": true
111
+ },
112
+ "moveColumn": {
113
+ "summary": "Move a column before or after another one.",
114
+ "apiMethod": "moveColumn",
115
+ "input": {
116
+ "type": "object",
117
+ "required": ["column", "target", "placement"],
118
+ "properties": {
119
+ "column": { "type": "string" },
120
+ "target": { "type": "string" },
121
+ "placement": { "enum": ["before", "after"] }
122
+ }
123
+ },
124
+ "effects": ["changes columnOrder"],
125
+ "reversible": true
126
+ },
127
+ "setColumnWidth": {
128
+ "summary": "Set a column's width in pixels, or null to restore its default.",
129
+ "apiMethod": "setColumnWidth",
130
+ "input": {
131
+ "type": "object",
132
+ "required": ["column", "width"],
133
+ "properties": { "column": { "type": "string" }, "width": { "type": ["integer", "null"], "minimum": 1 } }
134
+ },
135
+ "effects": ["changes columnSizing"],
136
+ "reversible": true
137
+ },
138
+ "resetColumns": {
139
+ "summary": "Restore column order, width, visibility and pinning to their defaults.",
140
+ "apiMethod": "resetColumns",
141
+ "effects": ["changes columnOrder", "changes columnVisibility", "changes columnSizing", "changes columnPinning"],
142
+ "reversible": true
143
+ },
144
+ "setPage": {
145
+ "summary": "Jump to a page, counting from zero.",
146
+ "apiMethod": "setPageIndex",
147
+ "input": { "type": "object", "required": ["index"], "properties": { "index": { "type": "integer", "minimum": 0 } } },
148
+ "effects": ["changes the visible page"],
149
+ "reversible": true
150
+ },
151
+ "setPageSize": {
152
+ "summary": "Change how many rows a page holds.",
153
+ "apiMethod": "setPageSize",
154
+ "input": { "type": "object", "required": ["size"], "properties": { "size": { "type": "integer", "minimum": 1 } } },
155
+ "effects": ["changes the visible page"],
156
+ "reversible": true
157
+ },
158
+ "setDensity": {
159
+ "summary": "Change row height.",
160
+ "apiMethod": "setDensity",
161
+ "input": {
162
+ "type": "object",
163
+ "required": ["density"],
164
+ "properties": { "density": { "enum": ["compact", "standard", "comfortable"] } }
165
+ },
166
+ "effects": ["changes row height"],
167
+ "reversible": true
168
+ },
169
+ "export": {
170
+ "summary": "Download the chosen rows as a file.",
171
+ "input": {
172
+ "type": "object",
173
+ "required": ["format"],
174
+ "properties": {
175
+ "format": { "enum": ["csv", "excel", "pdf"] },
176
+ "scope": { "enum": ["filtered", "all", "selected", "page"], "default": "filtered" },
177
+ "fileName": { "type": "string", "maxLength": 120 }
178
+ }
179
+ },
180
+ "effects": ["downloads a file"],
181
+ "reversible": false,
182
+ "requiresConfirmation": true,
183
+ "note": "Three formats, three GridApi methods (exportCsv, exportExcel, exportPdf), so no single apiMethod applies."
184
+ },
185
+ "print": {
186
+ "summary": "Open the browser's print dialog with the table laid out for paper.",
187
+ "apiMethod": "print",
188
+ "input": {
189
+ "type": "object",
190
+ "properties": {
191
+ "scope": { "enum": ["filtered", "all", "selected", "page"], "default": "filtered" },
192
+ "title": { "type": "string", "maxLength": 120 }
193
+ }
194
+ },
195
+ "effects": ["opens the print dialog"],
196
+ "reversible": false,
197
+ "requiresConfirmation": true
198
+ },
199
+ "copy": {
200
+ "summary": "Copy the selected rows to the clipboard as tab-separated text.",
201
+ "apiMethod": "copyToClipboard",
202
+ "effects": ["writes to the clipboard"],
203
+ "reversible": false,
204
+ "requiresConfirmation": true
205
+ },
206
+ "undo": {
207
+ "summary": "Restore the state from before the last operation.",
208
+ "effects": ["restores a previous state"],
209
+ "reversible": false,
210
+ "note": "Performed by the agent runtime's snapshot history, not by GridApi."
211
+ },
212
+ "redo": {
213
+ "summary": "Re-apply the operation that was last undone.",
214
+ "effects": ["restores a later state"],
215
+ "reversible": false,
216
+ "note": "Performed by the agent runtime's snapshot history, not by GridApi."
217
+ }
218
+ },
219
+
220
+ "accessibility": {
221
+ "supplied": [
222
+ "WAI-ARIA grid pattern: role=row/rowgroup, aria-colindex, aria-rowindex",
223
+ "aria-sort on sortable header cells",
224
+ "aria-multiselectable when row selection is enabled",
225
+ "full cell keyboard navigation, including the header row"
226
+ ],
227
+ "required": [
228
+ "aria-label on the grid when its purpose is not obvious from the page",
229
+ "a visible heading or caption nearby for screen-reader context"
230
+ ]
231
+ },
232
+
233
+ "composition": {
234
+ "notes": "Self-contained. Columns are configuration, not children. Keep `data`, `columns` and `getRowId` referentially stable — the grid memoizes on their identity."
235
+ },
236
+
237
+ "safeMutations": {
238
+ "allowed": ["height", "autoHeight", "rowHeight", "headerHeight", "toolbar", "pageSizeOptions", "emptyState", "locale", "localeText", "aria-label"],
239
+ "forbidden": ["data", "columns", "getRowId", "mode", "state", "onStateChange", "onQueryChange"],
240
+ "rationale": "Presentation is safe to change. `data`/`columns`/`getRowId` are memoization keys — replacing them re-creates the grid model. `mode` and the state callbacks define the client/server contract."
241
+ },
242
+
243
+ "propNotes": {
244
+ "mode": "`client` lets the grid sort, filter and paginate. `server` means `data` is already the current page and the host answers `onQueryChange`.",
245
+ "state": "Makes the grid controlled. Prefer `initialState` unless the host genuinely owns the state."
246
+ },
247
+
248
+ "notImplemented": [
249
+ "grouping — no API, no state field, no operation",
250
+ "aggregation — no API, no state field, no operation",
251
+ "row-level mutation — no add, delete or set-cell API; editing is a UI affordance only",
252
+ "notIn — a column with fixed options filters by membership only; an exclusion must list the complement",
253
+ "multi-value filter on a plain string column — one column holds one filter"
254
+ ]
255
+ }
@@ -0,0 +1,164 @@
1
+ "use client";
2
+
3
+ import { useCallback, useId, useRef, useState, type FormEvent, type KeyboardEvent } from "react";
4
+ import { cx } from "../../shared/core/cx";
5
+ import { useAgentAdapter } from "../../shared/react/GramproAIProvider";
6
+ import { useAskAgent, type AskableAgent } from "../../shared/react/useAskAgent";
7
+ import type { GridAgent } from "../agent/engine";
8
+
9
+ /*
10
+ * The box a person types into.
11
+ *
12
+ * It owns no grid semantics. Everything it shows — what happened, what was
13
+ * refused and why, which question to ask back — comes from the validator, in
14
+ * the validator's own words. That matters more than it sounds: a message this
15
+ * component invented would be a second source of truth about what the grid
16
+ * can do, and would drift.
17
+ *
18
+ * Renders nothing at all when no adapter is in scope, so a grid in an
19
+ * application with no AI configured is the grid it has always been.
20
+ */
21
+
22
+ export interface AskGridProps<T> {
23
+ agent: GridAgent<T> | null;
24
+ placeholder?: string;
25
+ suggestions?: readonly string[];
26
+ className?: string;
27
+ "aria-label"?: string;
28
+ }
29
+
30
+ const TONE: Record<string, string> = {
31
+ done: "ask-ok",
32
+ clarify: "ask-ask",
33
+ refused: "ask-no",
34
+ error: "ask-err",
35
+ };
36
+
37
+ export function AskGrid<T>({
38
+ agent,
39
+ placeholder,
40
+ suggestions,
41
+ className,
42
+ "aria-label": ariaLabel,
43
+ }: AskGridProps<T>) {
44
+ const context = useAgentAdapter();
45
+ const [text, setText] = useState("");
46
+ const inputRef = useRef<HTMLInputElement>(null);
47
+ const statusId = useId();
48
+
49
+ const { ask, confirm, reset, outcome, busy, pending } = useAskAgent(
50
+ agent as AskableAgent | null,
51
+ context.adapter,
52
+ );
53
+
54
+ const submit = useCallback(
55
+ (event: FormEvent) => {
56
+ event.preventDefault();
57
+ void ask(text);
58
+ },
59
+ [ask, text],
60
+ );
61
+
62
+ const onKeyDown = useCallback(
63
+ (event: KeyboardEvent<HTMLInputElement>) => {
64
+ if (event.key === "Escape" && outcome.phase !== "idle") {
65
+ event.stopPropagation();
66
+ reset();
67
+ }
68
+ },
69
+ [outcome.phase, reset],
70
+ );
71
+
72
+ const useSuggestion = useCallback(
73
+ (value: string) => {
74
+ setText(value);
75
+ inputRef.current?.focus();
76
+ void ask(value);
77
+ },
78
+ [ask],
79
+ );
80
+
81
+ // No adapter configured: the feature is simply not here.
82
+ if (!context.adapter) return null;
83
+
84
+ const hints = suggestions ?? context.suggestions ?? [];
85
+ const showHints = hints.length > 0 && text === "" && outcome.phase === "idle";
86
+
87
+ return (
88
+ <div className={cx("ask-root", className)}>
89
+ <form className="ask-form" onSubmit={submit}>
90
+ <input
91
+ ref={inputRef}
92
+ type="text"
93
+ className="ask-input"
94
+ value={text}
95
+ disabled={!agent}
96
+ placeholder={placeholder ?? context.placeholder ?? "Ask about this data…"}
97
+ aria-label={ariaLabel ?? "Ask about this data"}
98
+ aria-describedby={outcome.phase === "idle" ? undefined : statusId}
99
+ aria-busy={busy || undefined}
100
+ onChange={(event) => setText(event.target.value)}
101
+ onKeyDown={onKeyDown}
102
+ />
103
+ <button type="submit" className="ask-send" disabled={busy || !agent || text.trim() === ""}>
104
+ {busy ? "…" : "Ask"}
105
+ </button>
106
+ </form>
107
+
108
+ {showHints && (
109
+ <div className="ask-hints">
110
+ {hints.map((hint) => (
111
+ <button
112
+ key={hint}
113
+ type="button"
114
+ className="ask-hint"
115
+ onClick={() => useSuggestion(hint)}
116
+ >
117
+ {hint}
118
+ </button>
119
+ ))}
120
+ </div>
121
+ )}
122
+
123
+ {outcome.phase !== "idle" && (
124
+ /*
125
+ * `polite`, not `assertive`: the grid itself has already changed, and
126
+ * a screen reader interrupting to repeat it would be noise.
127
+ */
128
+ <div id={statusId} className={cx("ask-status", TONE[outcome.phase])} role="status" aria-live="polite">
129
+ {outcome.phase === "thinking" ? (
130
+ <span className="ask-thinking">Thinking…</span>
131
+ ) : (
132
+ <>
133
+ <span className="ask-message">{outcome.message}</span>
134
+
135
+ {outcome.suggestion && <span className="ask-suggestion">{outcome.suggestion}</span>}
136
+
137
+ {outcome.layer && (
138
+ <span className="ask-layer" title={`Refused by the ${outcome.layer} layer`}>
139
+ {outcome.layer}
140
+ </span>
141
+ )}
142
+
143
+ {outcome.warnings?.map((warning) => (
144
+ <span key={warning} className="ask-warning">
145
+ {warning}
146
+ </span>
147
+ ))}
148
+
149
+ {pending !== null && (
150
+ <button type="button" className="ask-confirm" onClick={() => void confirm()}>
151
+ Yes, do it
152
+ </button>
153
+ )}
154
+
155
+ <button type="button" className="ask-dismiss" onClick={reset} aria-label="Dismiss">
156
+ ×
157
+ </button>
158
+ </>
159
+ )}
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ }
@@ -25,6 +25,9 @@ import { defaultLocaleText, type LocaleText } from "./locale";
25
25
  import { Pagination } from "./Pagination";
26
26
  import { Popover } from "./Popover";
27
27
  import { Toolbar, type ToolbarOptions } from "./Toolbar";
28
+ import { AskGrid } from "./AskGrid";
29
+ import { createGridAgent, type GridAgent } from "../agent/engine";
30
+ import type { GridAgentPolicy, GridColumnSemantics } from "../agent/contract";
28
31
  import { GridViewport } from "./Viewport";
29
32
 
30
33
  export interface DataGridProps<T> extends GridOptions<T> {
@@ -48,6 +51,18 @@ export interface DataGridProps<T> extends GridOptions<T> {
48
51
  locale?: string;
49
52
  localeText?: Partial<LocaleText>;
50
53
  "aria-label"?: string;
54
+ /**
55
+ * Render a natural-language box above the grid.
56
+ *
57
+ * Needs a `<GramproAIProvider>` above it supplying an adapter; without one
58
+ * this renders nothing and the grid is unchanged. No model is bundled or
59
+ * downloaded — the application brings its own.
60
+ */
61
+ ai?: boolean;
62
+ /** What a machine cannot read off a column definition: units, synonyms, PII. */
63
+ semantics?: Readonly<Record<string, GridColumnSemantics>>;
64
+ /** Columns and operations withheld from whatever is driving the grid. */
65
+ agentPolicy?: GridAgentPolicy;
51
66
  }
52
67
 
53
68
  const ROW_HEIGHTS: Record<Density, number> = { compact: 32, standard: 40, comfortable: 52 };
@@ -96,11 +111,33 @@ export function DataGrid<T>(props: DataGridProps<T>) {
96
111
  style,
97
112
  locale,
98
113
  localeText: localeOverrides,
114
+ ai = false,
115
+ semantics,
116
+ agentPolicy,
99
117
  } = props;
100
118
 
101
119
  const [engine] = useState(() => createGridEngine<T>(props));
102
120
  useImperativeHandle(ref, () => engine.api, [engine]);
103
121
 
122
+ /*
123
+ * The agent is built once, from the same engine the grid renders, so the
124
+ * contract it advertises is this instance's real state rather than a copy.
125
+ * Only when `ai` is on: an agent walks the data to derive column stats and
126
+ * enum values, which a grid nobody is going to ask questions of should not
127
+ * pay for.
128
+ */
129
+ const [agent] = useState<GridAgent<T> | null>(() =>
130
+ ai
131
+ ? createGridAgent<T>({
132
+ api: engine.api,
133
+ options: props,
134
+ semantics,
135
+ policy: agentPolicy,
136
+ locale,
137
+ })
138
+ : null,
139
+ );
140
+
104
141
  // Selection and UI state are deliberately not selected here: changing them
105
142
  // re-renders only the rows and cells that subscribe to them.
106
143
  const internal = useStoreSelector(engine.store, selectModelState, shallowEqual);
@@ -243,6 +280,8 @@ export function DataGrid<T>(props: DataGridProps<T>) {
243
280
  data-density={density}
244
281
  data-stale={stale || undefined}
245
282
  >
283
+ {ai && <AskGrid agent={agent} aria-label={props['aria-label']} />}
284
+
246
285
  {toolbarOptions && (
247
286
  <Toolbar
248
287
  options={toolbarOptions}