@grampro/kit 2.7.2 → 2.7.3

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 (75) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +233 -209
  2. package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
  3. package/.gbs/skills/gbs-components/references/forms.md +12 -0
  4. package/.gbs/skills/gbs-components/references/install.md +14 -3
  5. package/CHANGELOG.md +963 -861
  6. package/README.md +11 -3
  7. package/index.cjs +108 -123
  8. package/package.json +1 -1
  9. package/source/components/accordion/passport.json +1 -1
  10. package/source/components/alert/passport.json +1 -1
  11. package/source/components/avatar/passport.json +1 -1
  12. package/source/components/badge/passport.json +1 -1
  13. package/source/components/breadcrumb/passport.json +1 -1
  14. package/source/components/button/passport.json +1 -1
  15. package/source/components/card/passport.json +1 -1
  16. package/source/components/checkbox/passport.json +2 -2
  17. package/source/components/checkbox/passport.manual.json +1 -1
  18. package/source/components/checkbox/react/Checkbox.tsx +14 -0
  19. package/source/components/combobox/passport.json +3 -2
  20. package/source/components/combobox/passport.manual.json +1 -1
  21. package/source/components/combobox/react/MultiSelect.tsx +13 -0
  22. package/source/components/combobox/react/Select.tsx +13 -0
  23. package/source/components/combobox/react/surface.ts +70 -0
  24. package/source/components/data-grid/passport.json +6 -6
  25. package/source/components/data-grid/react/DataGrid.tsx +353 -351
  26. package/source/components/date-picker/passport.json +3 -2
  27. package/source/components/date-picker/passport.manual.json +1 -1
  28. package/source/components/date-picker/react/DatePicker.tsx +16 -2
  29. package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
  30. package/source/components/date-picker/react/surface.ts +68 -0
  31. package/source/components/dialog/passport.json +1 -1
  32. package/source/components/file-uploader/passport.json +1 -1
  33. package/source/components/input/passport.json +2 -2
  34. package/source/components/input/passport.manual.json +1 -1
  35. package/source/components/input/react/Input.tsx +21 -0
  36. package/source/components/menu/passport.json +1 -1
  37. package/source/components/modal/passport.json +1 -1
  38. package/source/components/number-input/passport.json +2 -2
  39. package/source/components/number-input/passport.manual.json +1 -1
  40. package/source/components/number-input/react/NumberInput.tsx +30 -0
  41. package/source/components/popover/passport.json +1 -1
  42. package/source/components/progress/passport.json +1 -1
  43. package/source/components/radio-group/passport.json +2 -2
  44. package/source/components/radio-group/passport.manual.json +1 -1
  45. package/source/components/radio-group/react/RadioGroup.tsx +33 -0
  46. package/source/components/shared/core/agent/field-executors.ts +173 -137
  47. package/source/components/shared/core/agent/index.ts +21 -0
  48. package/source/components/shared/core/agent/surface.ts +437 -0
  49. package/source/components/shared/core/agent/webmcp.ts +332 -328
  50. package/source/components/shared/core/agent/write.ts +62 -58
  51. package/source/components/shared/index.ts +7 -0
  52. package/source/components/shared/react/AgentSurface.tsx +145 -0
  53. package/source/components/shared/react/agentSurfaceContext.ts +92 -0
  54. package/source/components/shared/react/useFieldAgent.ts +62 -88
  55. package/source/components/shared/version.json +4 -4
  56. package/source/components/shared/version.ts +6 -6
  57. package/source/components/skeleton/passport.json +1 -1
  58. package/source/components/spinner/passport.json +1 -1
  59. package/source/components/switch/passport.json +2 -2
  60. package/source/components/switch/passport.manual.json +1 -1
  61. package/source/components/switch/react/Switch.tsx +16 -0
  62. package/source/components/tabs/passport.json +1 -1
  63. package/source/components/textarea/passport.json +2 -2
  64. package/source/components/textarea/passport.manual.json +1 -1
  65. package/source/components/textarea/react/Textarea.tsx +16 -0
  66. package/source/components/toaster/passport.json +1 -1
  67. package/source/components/tooltip/passport.json +1 -1
  68. package/tools/doctor/baseline.cjs +5 -4
  69. package/tools/mcp/server.cjs +13 -1
  70. package/tools/passport/extract.cjs +20 -65
  71. package/tools/passport/hash.cjs +18 -1
  72. package/tools/passport/index.cjs +4 -2
  73. package/tools/passport/typescript.cjs +220 -0
  74. package/tools/prompt.cjs +205 -0
  75. package/tools/ui.cjs +67 -0
@@ -1,351 +1,353 @@
1
- "use client";
2
-
3
- import {
4
- useDeferredValue,
5
- useImperativeHandle,
6
- useLayoutEffect,
7
- useMemo,
8
- useState,
9
- type CSSProperties,
10
- type ReactNode,
11
- type Ref,
12
- } from "react";
13
- import { computeLayout, createSelectionColumn, resolveColumns } from "../core/columns";
14
- import { filterRows } from "../core/filtering";
15
- import { createGridEngine, type GridApi, type GridSnapshot } from "../core/grid";
16
- import { buildRows, createRowIdGetter, paginate } from "../core/rows";
17
- import { sortRows } from "../core/sorting";
18
- import { shallowEqual } from "../core/store";
19
- import type { Density, GridOptions } from "../core/types";
20
- import { createFormatters } from "../core/values";
21
- import { ColumnMenu } from "./ColumnMenu";
22
- import { GridContext, cx, type GridContextValue, type GridSlot } from "./context";
23
- import { usePopoverState, useStoreSelector } from "./hooks";
24
- import { defaultLocaleText, type LocaleText } from "./locale";
25
- import { Pagination } from "./Pagination";
26
- import { Popover } from "./Popover";
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";
31
- import { GridViewport } from "./Viewport";
32
-
33
- export interface DataGridProps<T> extends GridOptions<T> {
34
- ref?: Ref<GridApi<T>>;
35
- /** Height of the scrolling area. Ignored with `autoHeight`. Default 520. */
36
- height?: number | string;
37
- /** Grow to fit all rows instead of scrolling (use with pagination or small data). */
38
- autoHeight?: boolean;
39
- /** Fixed row height in px. Defaults to the density's height. */
40
- rowHeight?: number;
41
- /** Fixed header height in px. Defaults to the density's row height. */
42
- headerHeight?: number;
43
- /**
44
- * Show the loading treatment over the grid. The existing rows stay in place
45
- * underneath, so a refresh does not empty the screen.
46
- */
47
- loading?: boolean;
48
- /**
49
- * The toolbar above the grid. `true` for all of it, `false` for none, or an
50
- * object to choose parts — `{ search: true, export: false }` — and to put
51
- * your own controls at its `start` or `end`.
52
- */
53
- toolbar?: boolean | ToolbarOptions;
54
- /** Page sizes offered in the pager. Default `[25, 50, 100, 250]`. */
55
- pageSizeOptions?: number[];
56
- /**
57
- * Shown in place of rows when there are none. Worth distinguishing "nothing
58
- * here yet" from "nothing matched your filters", which the default does not.
59
- */
60
- emptyState?: ReactNode;
61
- /** A class per row, for status colouring. Return undefined for no class. */
62
- getRowClassName?(row: T, rowIndex: number): string | undefined;
63
- classNames?: Partial<Record<GridSlot, string>>;
64
- className?: string;
65
- style?: CSSProperties;
66
- /** BCP 47 locale for number and date formatting. */
67
- locale?: string;
68
- localeText?: Partial<LocaleText>;
69
- "aria-label"?: string;
70
- /**
71
- * Render a natural-language box above the grid.
72
- *
73
- * Needs a `<GramproAIProvider>` above it supplying an adapter; without one
74
- * this renders nothing and the grid is unchanged. No model is bundled or
75
- * downloaded — the application brings its own.
76
- */
77
- ai?: boolean;
78
- /** What a machine cannot read off a column definition: units, synonyms, PII. */
79
- semantics?: Readonly<Record<string, GridColumnSemantics>>;
80
- /** Columns and operations withheld from whatever is driving the grid. */
81
- agentPolicy?: GridAgentPolicy;
82
- }
83
-
84
- const ROW_HEIGHTS: Record<Density, number> = { compact: 32, standard: 40, comfortable: 52 };
85
- const DEFAULT_PAGE_SIZES = [25, 50, 100, 250];
86
- const EMPTY_CLASSNAMES: Partial<Record<GridSlot, string>> = {};
87
-
88
- const selectModelState = ({ state }: GridSnapshot) => ({
89
- sorting: state.sorting,
90
- filters: state.filters,
91
- globalFilter: state.globalFilter,
92
- pagination: state.pagination,
93
- columnOrder: state.columnOrder,
94
- columnVisibility: state.columnVisibility,
95
- columnSizing: state.columnSizing,
96
- columnPinning: state.columnPinning,
97
- density: state.density,
98
- });
99
-
100
- export function DataGrid<T>(props: DataGridProps<T>) {
101
- const {
102
- ref,
103
- data,
104
- columns: columnDefs,
105
- getRowId,
106
- mode = "client",
107
- rowCount,
108
- state: controlled,
109
- enablePagination = true,
110
- enableSorting = true,
111
- enableFiltering = true,
112
- enableColumnResizing = true,
113
- enableColumnReordering = true,
114
- enableColumnPinning = true,
115
- enableColumnHiding = true,
116
- enableRowSelection = false,
117
- selectionMode = "multiple",
118
- height = 520,
119
- autoHeight = false,
120
- loading = false,
121
- toolbar = true,
122
- pageSizeOptions = DEFAULT_PAGE_SIZES,
123
- emptyState,
124
- getRowClassName,
125
- classNames = EMPTY_CLASSNAMES,
126
- className,
127
- style,
128
- locale,
129
- localeText: localeOverrides,
130
- ai = false,
131
- semantics,
132
- agentPolicy,
133
- } = props;
134
-
135
- const [engine] = useState(() => createGridEngine<T>(props));
136
- useImperativeHandle(ref, () => engine.api, [engine]);
137
-
138
- /*
139
- * The agent is built once, from the same engine the grid renders, so the
140
- * contract it advertises is this instance's real state rather than a copy.
141
- * Only when `ai` is on: an agent walks the data to derive column stats and
142
- * enum values, which a grid nobody is going to ask questions of should not
143
- * pay for.
144
- */
145
- const [agent] = useState<GridAgent<T> | null>(() =>
146
- ai
147
- ? createGridAgent<T>({
148
- api: engine.api,
149
- options: props,
150
- semantics,
151
- policy: agentPolicy,
152
- locale,
153
- })
154
- : null,
155
- );
156
-
157
- // Selection and UI state are deliberately not selected here: changing them
158
- // re-renders only the rows and cells that subscribe to them.
159
- const internal = useStoreSelector(engine.store, selectModelState, shallowEqual);
160
- const sorting = controlled?.sorting ?? internal.sorting;
161
- const filters = controlled?.filters ?? internal.filters;
162
- const globalFilter = controlled?.globalFilter ?? internal.globalFilter;
163
- const pagination = controlled?.pagination ?? internal.pagination;
164
- const columnOrder = controlled?.columnOrder ?? internal.columnOrder;
165
- const columnVisibility = controlled?.columnVisibility ?? internal.columnVisibility;
166
- const columnSizing = controlled?.columnSizing ?? internal.columnSizing;
167
- const columnPinning = controlled?.columnPinning ?? internal.columnPinning;
168
- const density = controlled?.density ?? internal.density;
169
-
170
- const localeText = useMemo(() => ({ ...defaultLocaleText, ...localeOverrides }), [localeOverrides]);
171
- const formatters = useMemo(
172
- () => createFormatters(locale, { yes: localeText.yes, no: localeText.no }),
173
- [locale, localeText.yes, localeText.no],
174
- );
175
- const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
176
-
177
- // Row pipeline: each stage recomputes only when its own inputs change.
178
- const server = mode === "server";
179
- const rowIdGetter = useMemo(() => createRowIdGetter(getRowId), [getRowId]);
180
- const columns = useMemo(() => resolveColumns(columnDefs), [columnDefs]);
181
- const coreRows = useMemo(() => buildRows(data, rowIdGetter), [data, rowIdGetter]);
182
-
183
- const liveQuery = useMemo(
184
- () => ({ sorting, filters, globalFilter, pagination }),
185
- [sorting, filters, globalFilter, pagination],
186
- );
187
- // Filtering and sorting large data render at low priority, so typing stays responsive.
188
- const query = useDeferredValue(liveQuery);
189
- const stale = query !== liveQuery;
190
-
191
- const filteredRows = useMemo(
192
- () => (server ? coreRows : filterRows(coreRows, columns, query.filters, query.globalFilter, formatters)),
193
- [server, coreRows, columns, query.filters, query.globalFilter, formatters],
194
- );
195
- const sortedRows = useMemo(
196
- () => (server ? filteredRows : sortRows(filteredRows, columns, query.sorting, locale)),
197
- [server, filteredRows, columns, query.sorting, locale],
198
- );
199
- const page = useMemo(
200
- () => paginate(sortedRows, query.pagination, { enabled: enablePagination, server, rowCount }),
201
- [sortedRows, query.pagination, enablePagination, server, rowCount],
202
- );
203
-
204
- const hasSelection = enableRowSelection !== false;
205
- const leadingColumns = useMemo(() => (hasSelection ? [createSelectionColumn<T>()] : []), [hasSelection]);
206
- const layout = useMemo(
207
- () => computeLayout(columns, { columnOrder, columnVisibility, columnSizing, columnPinning }, leadingColumns),
208
- [columns, columnOrder, columnVisibility, columnSizing, columnPinning, leadingColumns],
209
- );
210
-
211
- const rowHeight = props.rowHeight ?? ROW_HEIGHTS[density];
212
- const headerHeight = props.headerHeight ?? Math.max(40, rowHeight);
213
-
214
- // Hand the committed props and model to the engine for event handlers.
215
- useLayoutEffect(() => {
216
- engine.sync(props, { columns, coreRows, sortedRows, page, layout, rowHeight, headerHeight, formatters });
217
- });
218
-
219
- const menu = usePopoverState<string>();
220
- const openMenu = menu.open;
221
-
222
- const isRowSelectable = useMemo(
223
- () =>
224
- typeof enableRowSelection === "function" ? enableRowSelection : () => enableRowSelection === true,
225
- [enableRowSelection],
226
- );
227
-
228
- const context = useMemo<GridContextValue<T>>(
229
- () => ({
230
- engine,
231
- locale: localeText,
232
- classNames,
233
- formatters,
234
- numberFormat,
235
- controlledSelection: controlled?.rowSelection,
236
- isRowSelectable,
237
- openColumnMenu: (columnId, anchor) => openMenu(anchor, columnId),
238
- features: {
239
- sorting: enableSorting,
240
- filtering: enableFiltering,
241
- resizing: enableColumnResizing,
242
- reordering: enableColumnReordering,
243
- pinning: enableColumnPinning,
244
- hiding: enableColumnHiding,
245
- selection: hasSelection,
246
- selectionMode,
247
- },
248
- }),
249
- [
250
- engine,
251
- localeText,
252
- classNames,
253
- formatters,
254
- numberFormat,
255
- controlled?.rowSelection,
256
- isRowSelectable,
257
- openMenu,
258
- enableSorting,
259
- enableFiltering,
260
- enableColumnResizing,
261
- enableColumnReordering,
262
- enableColumnPinning,
263
- enableColumnHiding,
264
- hasSelection,
265
- selectionMode,
266
- ],
267
- );
268
-
269
- const toolbarOptions: ToolbarOptions | null =
270
- toolbar === false
271
- ? null
272
- : {
273
- search: true,
274
- filterChips: true,
275
- columns: true,
276
- density: true,
277
- export: true,
278
- ...(toolbar === true ? {} : toolbar),
279
- };
280
-
281
- const menuAnchor = menu.state?.anchor ?? null;
282
- const menuItem = menu.state ? layout.columns.find((i) => i.column.id === menu.state?.payload) : undefined;
283
- const closeMenu = () => menu.close(menuAnchor);
284
-
285
- return (
286
- <GridContext value={context as unknown as GridContextValue<unknown>}>
287
- <div
288
- className={cx("dg-root", classNames.root, className)}
289
- style={
290
- {
291
- ...style,
292
- "--dg-row-height": `${rowHeight}px`,
293
- "--dg-header-height": `${headerHeight}px`,
294
- } as CSSProperties
295
- }
296
- data-density={density}
297
- data-stale={stale || undefined}
298
- >
299
- {ai && <AskGrid agent={agent} aria-label={props['aria-label']} />}
300
-
301
- {toolbarOptions && (
302
- <Toolbar
303
- options={toolbarOptions}
304
- globalFilter={globalFilter}
305
- filters={filters}
306
- density={density}
307
- columns={columns}
308
- columnVisibility={columnVisibility}
309
- />
310
- )}
311
-
312
- <GridViewport
313
- layout={layout}
314
- page={page}
315
- sortedRows={sortedRows}
316
- sorting={sorting}
317
- filters={filters}
318
- hasQuery={filters.length > 0 || globalFilter !== ""}
319
- rowHeight={rowHeight}
320
- headerHeight={headerHeight}
321
- height={autoHeight ? undefined : height}
322
- loading={loading}
323
- emptyState={emptyState}
324
- ariaLabel={props["aria-label"] ?? localeText.gridLabel}
325
- getRowClassName={getRowClassName}
326
- />
327
-
328
- {enablePagination && (
329
- <Pagination page={page} pageSize={pagination.pageSize} pageSizeOptions={pageSizeOptions} />
330
- )}
331
-
332
- {menuAnchor && menuItem && (
333
- <Popover
334
- anchor={menuAnchor}
335
- onClose={closeMenu}
336
- label={localeText.columnMenu(menuItem.column.header)}
337
- className="dg-column-menu-popover"
338
- >
339
- <ColumnMenu
340
- item={menuItem}
341
- layout={layout}
342
- sort={sorting.find((s) => s.columnId === menuItem.column.id)}
343
- filter={filters.find((f) => f.columnId === menuItem.column.id)}
344
- onClose={closeMenu}
345
- />
346
- </Popover>
347
- )}
348
- </div>
349
- </GridContext>
350
- );
351
- }
1
+ "use client";
2
+
3
+ import {
4
+ useDeferredValue,
5
+ useImperativeHandle,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useState,
9
+ type CSSProperties,
10
+ type ReactNode,
11
+ type Ref,
12
+ } from "react";
13
+ import { computeLayout, createSelectionColumn, resolveColumns } from "../core/columns";
14
+ import { filterRows } from "../core/filtering";
15
+ import { createGridEngine, type GridApi, type GridSnapshot } from "../core/grid";
16
+ import { buildRows, createRowIdGetter, paginate } from "../core/rows";
17
+ import { sortRows } from "../core/sorting";
18
+ import { shallowEqual } from "../core/store";
19
+ import type { Density, GridOptions } from "../core/types";
20
+ import { createFormatters } from "../core/values";
21
+ import { ColumnMenu } from "./ColumnMenu";
22
+ import { GridContext, cx, type GridContextValue, type GridSlot } from "./context";
23
+ import { usePopoverState, useStoreSelector } from "./hooks";
24
+ import { defaultLocaleText, type LocaleText } from "./locale";
25
+ import { Pagination } from "./Pagination";
26
+ import { Popover } from "./Popover";
27
+ import { Toolbar, type ToolbarOptions } from "./Toolbar";
28
+ import { AskGrid } from "./AskGrid";
29
+ import { createGridAgent, type GridAgent } from "../agent/engine";
30
+ import { useAgentSurfaceGrid, useInAgentSurface } from "../../shared/react/agentSurfaceContext";
31
+ import type { GridAgentPolicy, GridColumnSemantics } from "../agent/contract";
32
+ import { GridViewport } from "./Viewport";
33
+
34
+ export interface DataGridProps<T> extends GridOptions<T> {
35
+ ref?: Ref<GridApi<T>>;
36
+ /** Height of the scrolling area. Ignored with `autoHeight`. Default 520. */
37
+ height?: number | string;
38
+ /** Grow to fit all rows instead of scrolling (use with pagination or small data). */
39
+ autoHeight?: boolean;
40
+ /** Fixed row height in px. Defaults to the density's height. */
41
+ rowHeight?: number;
42
+ /** Fixed header height in px. Defaults to the density's row height. */
43
+ headerHeight?: number;
44
+ /**
45
+ * Show the loading treatment over the grid. The existing rows stay in place
46
+ * underneath, so a refresh does not empty the screen.
47
+ */
48
+ loading?: boolean;
49
+ /**
50
+ * The toolbar above the grid. `true` for all of it, `false` for none, or an
51
+ * object to choose parts — `{ search: true, export: false }` — and to put
52
+ * your own controls at its `start` or `end`.
53
+ */
54
+ toolbar?: boolean | ToolbarOptions;
55
+ /** Page sizes offered in the pager. Default `[25, 50, 100, 250]`. */
56
+ pageSizeOptions?: number[];
57
+ /**
58
+ * Shown in place of rows when there are none. Worth distinguishing "nothing
59
+ * here yet" from "nothing matched your filters", which the default does not.
60
+ */
61
+ emptyState?: ReactNode;
62
+ /** A class per row, for status colouring. Return undefined for no class. */
63
+ getRowClassName?(row: T, rowIndex: number): string | undefined;
64
+ classNames?: Partial<Record<GridSlot, string>>;
65
+ className?: string;
66
+ style?: CSSProperties;
67
+ /** BCP 47 locale for number and date formatting. */
68
+ locale?: string;
69
+ localeText?: Partial<LocaleText>;
70
+ "aria-label"?: string;
71
+ /**
72
+ * Render a natural-language box above the grid.
73
+ *
74
+ * Needs a `<GramproAIProvider>` above it supplying an adapter; without one
75
+ * this renders nothing and the grid is unchanged. No model is bundled or
76
+ * downloaded — the application brings its own.
77
+ */
78
+ ai?: boolean;
79
+ /** What a machine cannot read off a column definition: units, synonyms, PII. */
80
+ semantics?: Readonly<Record<string, GridColumnSemantics>>;
81
+ /** Columns and operations withheld from whatever is driving the grid. */
82
+ agentPolicy?: GridAgentPolicy;
83
+ }
84
+
85
+ const ROW_HEIGHTS: Record<Density, number> = { compact: 32, standard: 40, comfortable: 52 };
86
+ const DEFAULT_PAGE_SIZES = [25, 50, 100, 250];
87
+ const EMPTY_CLASSNAMES: Partial<Record<GridSlot, string>> = {};
88
+
89
+ const selectModelState = ({ state }: GridSnapshot) => ({
90
+ sorting: state.sorting,
91
+ filters: state.filters,
92
+ globalFilter: state.globalFilter,
93
+ pagination: state.pagination,
94
+ columnOrder: state.columnOrder,
95
+ columnVisibility: state.columnVisibility,
96
+ columnSizing: state.columnSizing,
97
+ columnPinning: state.columnPinning,
98
+ density: state.density,
99
+ });
100
+
101
+ export function DataGrid<T>(props: DataGridProps<T>) {
102
+ const {
103
+ ref,
104
+ data,
105
+ columns: columnDefs,
106
+ getRowId,
107
+ mode = "client",
108
+ rowCount,
109
+ state: controlled,
110
+ enablePagination = true,
111
+ enableSorting = true,
112
+ enableFiltering = true,
113
+ enableColumnResizing = true,
114
+ enableColumnReordering = true,
115
+ enableColumnPinning = true,
116
+ enableColumnHiding = true,
117
+ enableRowSelection = false,
118
+ selectionMode = "multiple",
119
+ height = 520,
120
+ autoHeight = false,
121
+ loading = false,
122
+ toolbar = true,
123
+ pageSizeOptions = DEFAULT_PAGE_SIZES,
124
+ emptyState,
125
+ getRowClassName,
126
+ classNames = EMPTY_CLASSNAMES,
127
+ className,
128
+ style,
129
+ locale,
130
+ localeText: localeOverrides,
131
+ ai = false,
132
+ semantics,
133
+ agentPolicy,
134
+ } = props;
135
+
136
+ const [engine] = useState(() => createGridEngine<T>(props));
137
+ useImperativeHandle(ref, () => engine.api, [engine]);
138
+
139
+ /*
140
+ * The agent is built once, from the same engine the grid renders, so the
141
+ * contract it advertises is this instance's real state rather than a copy.
142
+ * Only when something will use it — `ai`, or an <AgentSurface> around the
143
+ * grid: an agent walks the data to derive column stats and enum values, which
144
+ * a grid nobody is going to ask questions of should not pay for.
145
+ *
146
+ * Imported statically. A dynamic import would not split it out anyway — the
147
+ * data-grid index re-exports the runtime for `createGridAgent` — and every
148
+ * host build would print a warning saying so.
149
+ */
150
+ const inSurface = useInAgentSurface();
151
+ const wantsAgent = ai || inSurface;
152
+ const build = () =>
153
+ createGridAgent<T>({ api: engine.api, options: props, semantics, policy: agentPolicy, locale });
154
+ const [agent, setAgent] = useState<GridAgent<T> | null>(() => (wantsAgent ? build() : null));
155
+ // A grid that comes to need one later — moved into a surface — gets it then.
156
+ if (wantsAgent && !agent) setAgent(build());
157
+ useAgentSurfaceGrid(agent, props["aria-label"]);
158
+
159
+ // Selection and UI state are deliberately not selected here: changing them
160
+ // re-renders only the rows and cells that subscribe to them.
161
+ const internal = useStoreSelector(engine.store, selectModelState, shallowEqual);
162
+ const sorting = controlled?.sorting ?? internal.sorting;
163
+ const filters = controlled?.filters ?? internal.filters;
164
+ const globalFilter = controlled?.globalFilter ?? internal.globalFilter;
165
+ const pagination = controlled?.pagination ?? internal.pagination;
166
+ const columnOrder = controlled?.columnOrder ?? internal.columnOrder;
167
+ const columnVisibility = controlled?.columnVisibility ?? internal.columnVisibility;
168
+ const columnSizing = controlled?.columnSizing ?? internal.columnSizing;
169
+ const columnPinning = controlled?.columnPinning ?? internal.columnPinning;
170
+ const density = controlled?.density ?? internal.density;
171
+
172
+ const localeText = useMemo(() => ({ ...defaultLocaleText, ...localeOverrides }), [localeOverrides]);
173
+ const formatters = useMemo(
174
+ () => createFormatters(locale, { yes: localeText.yes, no: localeText.no }),
175
+ [locale, localeText.yes, localeText.no],
176
+ );
177
+ const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
178
+
179
+ // Row pipeline: each stage recomputes only when its own inputs change.
180
+ const server = mode === "server";
181
+ const rowIdGetter = useMemo(() => createRowIdGetter(getRowId), [getRowId]);
182
+ const columns = useMemo(() => resolveColumns(columnDefs), [columnDefs]);
183
+ const coreRows = useMemo(() => buildRows(data, rowIdGetter), [data, rowIdGetter]);
184
+
185
+ const liveQuery = useMemo(
186
+ () => ({ sorting, filters, globalFilter, pagination }),
187
+ [sorting, filters, globalFilter, pagination],
188
+ );
189
+ // Filtering and sorting large data render at low priority, so typing stays responsive.
190
+ const query = useDeferredValue(liveQuery);
191
+ const stale = query !== liveQuery;
192
+
193
+ const filteredRows = useMemo(
194
+ () => (server ? coreRows : filterRows(coreRows, columns, query.filters, query.globalFilter, formatters)),
195
+ [server, coreRows, columns, query.filters, query.globalFilter, formatters],
196
+ );
197
+ const sortedRows = useMemo(
198
+ () => (server ? filteredRows : sortRows(filteredRows, columns, query.sorting, locale)),
199
+ [server, filteredRows, columns, query.sorting, locale],
200
+ );
201
+ const page = useMemo(
202
+ () => paginate(sortedRows, query.pagination, { enabled: enablePagination, server, rowCount }),
203
+ [sortedRows, query.pagination, enablePagination, server, rowCount],
204
+ );
205
+
206
+ const hasSelection = enableRowSelection !== false;
207
+ const leadingColumns = useMemo(() => (hasSelection ? [createSelectionColumn<T>()] : []), [hasSelection]);
208
+ const layout = useMemo(
209
+ () => computeLayout(columns, { columnOrder, columnVisibility, columnSizing, columnPinning }, leadingColumns),
210
+ [columns, columnOrder, columnVisibility, columnSizing, columnPinning, leadingColumns],
211
+ );
212
+
213
+ const rowHeight = props.rowHeight ?? ROW_HEIGHTS[density];
214
+ const headerHeight = props.headerHeight ?? Math.max(40, rowHeight);
215
+
216
+ // Hand the committed props and model to the engine for event handlers.
217
+ useLayoutEffect(() => {
218
+ engine.sync(props, { columns, coreRows, sortedRows, page, layout, rowHeight, headerHeight, formatters });
219
+ });
220
+
221
+ const menu = usePopoverState<string>();
222
+ const openMenu = menu.open;
223
+
224
+ const isRowSelectable = useMemo(
225
+ () =>
226
+ typeof enableRowSelection === "function" ? enableRowSelection : () => enableRowSelection === true,
227
+ [enableRowSelection],
228
+ );
229
+
230
+ const context = useMemo<GridContextValue<T>>(
231
+ () => ({
232
+ engine,
233
+ locale: localeText,
234
+ classNames,
235
+ formatters,
236
+ numberFormat,
237
+ controlledSelection: controlled?.rowSelection,
238
+ isRowSelectable,
239
+ openColumnMenu: (columnId, anchor) => openMenu(anchor, columnId),
240
+ features: {
241
+ sorting: enableSorting,
242
+ filtering: enableFiltering,
243
+ resizing: enableColumnResizing,
244
+ reordering: enableColumnReordering,
245
+ pinning: enableColumnPinning,
246
+ hiding: enableColumnHiding,
247
+ selection: hasSelection,
248
+ selectionMode,
249
+ },
250
+ }),
251
+ [
252
+ engine,
253
+ localeText,
254
+ classNames,
255
+ formatters,
256
+ numberFormat,
257
+ controlled?.rowSelection,
258
+ isRowSelectable,
259
+ openMenu,
260
+ enableSorting,
261
+ enableFiltering,
262
+ enableColumnResizing,
263
+ enableColumnReordering,
264
+ enableColumnPinning,
265
+ enableColumnHiding,
266
+ hasSelection,
267
+ selectionMode,
268
+ ],
269
+ );
270
+
271
+ const toolbarOptions: ToolbarOptions | null =
272
+ toolbar === false
273
+ ? null
274
+ : {
275
+ search: true,
276
+ filterChips: true,
277
+ columns: true,
278
+ density: true,
279
+ export: true,
280
+ ...(toolbar === true ? {} : toolbar),
281
+ };
282
+
283
+ const menuAnchor = menu.state?.anchor ?? null;
284
+ const menuItem = menu.state ? layout.columns.find((i) => i.column.id === menu.state?.payload) : undefined;
285
+ const closeMenu = () => menu.close(menuAnchor);
286
+
287
+ return (
288
+ <GridContext value={context as unknown as GridContextValue<unknown>}>
289
+ <div
290
+ className={cx("dg-root", classNames.root, className)}
291
+ style={
292
+ {
293
+ ...style,
294
+ "--dg-row-height": `${rowHeight}px`,
295
+ "--dg-header-height": `${headerHeight}px`,
296
+ } as CSSProperties
297
+ }
298
+ data-density={density}
299
+ data-stale={stale || undefined}
300
+ >
301
+ {ai && <AskGrid agent={agent} aria-label={props['aria-label']} />}
302
+
303
+ {toolbarOptions && (
304
+ <Toolbar
305
+ options={toolbarOptions}
306
+ globalFilter={globalFilter}
307
+ filters={filters}
308
+ density={density}
309
+ columns={columns}
310
+ columnVisibility={columnVisibility}
311
+ />
312
+ )}
313
+
314
+ <GridViewport
315
+ layout={layout}
316
+ page={page}
317
+ sortedRows={sortedRows}
318
+ sorting={sorting}
319
+ filters={filters}
320
+ hasQuery={filters.length > 0 || globalFilter !== ""}
321
+ rowHeight={rowHeight}
322
+ headerHeight={headerHeight}
323
+ height={autoHeight ? undefined : height}
324
+ loading={loading}
325
+ emptyState={emptyState}
326
+ ariaLabel={props["aria-label"] ?? localeText.gridLabel}
327
+ getRowClassName={getRowClassName}
328
+ />
329
+
330
+ {enablePagination && (
331
+ <Pagination page={page} pageSize={pagination.pageSize} pageSizeOptions={pageSizeOptions} />
332
+ )}
333
+
334
+ {menuAnchor && menuItem && (
335
+ <Popover
336
+ anchor={menuAnchor}
337
+ onClose={closeMenu}
338
+ label={localeText.columnMenu(menuItem.column.header)}
339
+ className="dg-column-menu-popover"
340
+ >
341
+ <ColumnMenu
342
+ item={menuItem}
343
+ layout={layout}
344
+ sort={sorting.find((s) => s.columnId === menuItem.column.id)}
345
+ filter={filters.find((f) => f.columnId === menuItem.column.id)}
346
+ onClose={closeMenu}
347
+ />
348
+ </Popover>
349
+ )}
350
+ </div>
351
+ </GridContext>
352
+ );
353
+ }