@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,58 +1,62 @@
1
- /*
2
- * Writing to a field the way a person does.
3
- *
4
- * This is the one file under `agent/` that needs a browser. Everything else
5
- * here validates, and validation runs anywhere; executing has to touch
6
- * something real. It is kept separate so the split is visible rather than
7
- * discovered when a server build fails.
8
- *
9
- * Why not simply assign `input.value = x`: React tracks the last value it
10
- * wrote on the DOM node, and an assignment that bypasses the prototype setter
11
- * leaves that tracker in step with the new value. React then concludes nothing
12
- * changed and never fires `onChange`, so a controlled field snaps back on the
13
- * next render. Going through the prototype setter and dispatching a real
14
- * bubbling event is what makes the change indistinguishable from typing — to
15
- * React, to form libraries, and to any plain listener.
16
- *
17
- * `Input.clear()` has worked this way since before there was an agent runtime.
18
- * This is the same technique, generalised to textareas and to checkedness.
19
- */
20
-
21
- type Writable = HTMLInputElement | HTMLTextAreaElement;
22
-
23
- /** The prototype that owns the real setter for this element. */
24
- const prototypeOf = (element: Writable) =>
25
- element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
26
-
27
- function nativeSetter(element: Writable, property: "value" | "checked") {
28
- return Object.getOwnPropertyDescriptor(prototypeOf(element), property)?.set;
29
- }
30
-
31
- /**
32
- * Set a field's text through the prototype setter, then fire the events a
33
- * person typing would have fired.
34
- */
35
- export function writeValue(element: Writable, value: string): void {
36
- nativeSetter(element, "value")?.call(element, value);
37
- element.dispatchEvent(new Event("input", { bubbles: true }));
38
- element.dispatchEvent(new Event("change", { bubbles: true }));
39
- }
40
-
41
- /**
42
- * Set a checkbox or radio, as a click would.
43
- *
44
- * `checked` rather than `value`, and a `click` is not used: clicking a radio
45
- * that is already selected does nothing, and clicking a disabled one is
46
- * silently dropped. Setting the property and announcing it is both more
47
- * predictable and easier to reason about from a validator's point of view —
48
- * by the time this runs, the policy layer has already decided it is allowed.
49
- */
50
- export function writeChecked(element: HTMLInputElement, checked: boolean): void {
51
- nativeSetter(element, "checked")?.call(element, checked);
52
- element.dispatchEvent(new Event("input", { bubbles: true }));
53
- element.dispatchEvent(new Event("change", { bubbles: true }));
54
- }
55
-
56
- /** True when the element can still be written to at all. */
57
- export const writable = (element: Writable | null | undefined): element is Writable =>
58
- Boolean(element) && !(element as Writable).disabled && !(element as Writable).readOnly;
1
+ /*
2
+ * Writing to a field the way a person does.
3
+ *
4
+ * This is the one file under `agent/` that needs a browser. Everything else
5
+ * here validates, and validation runs anywhere; executing has to touch
6
+ * something real. It is kept separate so the split is visible rather than
7
+ * discovered when a server build fails.
8
+ *
9
+ * Why not simply assign `input.value = x`: React tracks the last value it
10
+ * wrote on the DOM node, and an assignment that bypasses the prototype setter
11
+ * leaves that tracker in step with the new value. React then concludes nothing
12
+ * changed and never fires `onChange`, so a controlled field snaps back on the
13
+ * next render. Going through the prototype setter and dispatching a real
14
+ * bubbling event is what makes the change indistinguishable from typing — to
15
+ * React, to form libraries, and to any plain listener.
16
+ *
17
+ * `Input.clear()` has worked this way since before there was an agent runtime.
18
+ * This is the same technique, generalised to textareas and to checkedness.
19
+ */
20
+
21
+ type Writable = HTMLInputElement | HTMLTextAreaElement;
22
+
23
+ /** The prototype that owns the real setter for this element. */
24
+ const prototypeOf = (element: Writable) =>
25
+ element instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
26
+
27
+ function nativeSetter(element: Writable, property: "value" | "checked") {
28
+ return Object.getOwnPropertyDescriptor(prototypeOf(element), property)?.set;
29
+ }
30
+
31
+ /**
32
+ * Set a field's text through the prototype setter, then fire the events a
33
+ * person typing would have fired.
34
+ */
35
+ export function writeValue(element: Writable, value: string): void {
36
+ nativeSetter(element, "value")?.call(element, value);
37
+ element.dispatchEvent(new Event("input", { bubbles: true }));
38
+ element.dispatchEvent(new Event("change", { bubbles: true }));
39
+ }
40
+
41
+ /**
42
+ * Set a checkbox or radio by clicking it, when it is not already as asked.
43
+ *
44
+ * Not the setter-plus-events used for text. React hears a checkbox change
45
+ * through its `click` event, not `change`, so a set property announced with
46
+ * `change` updates the box and leaves a controlled one's state behind — the
47
+ * write reports success and the application never learns of it. 2.7.0 shipped
48
+ * that way; the e2e surface test is what caught it.
49
+ *
50
+ * A click is what a person does, so everything downstream sees exactly that:
51
+ * React, form libraries, a host that cancels the click. Already in the asked
52
+ * state is a no-op, as clicking a selected radio is. A disabled element was
53
+ * refused before this ran.
54
+ */
55
+ export function writeChecked(element: HTMLInputElement, checked: boolean): void {
56
+ if (element.checked === checked) return;
57
+ element.click();
58
+ }
59
+
60
+ /** True when the element can still be written to at all. */
61
+ export const writable = (element: Writable | null | undefined): element is Writable =>
62
+ Boolean(element) && !(element as Writable).disabled && !(element as Writable).readOnly;
@@ -15,3 +15,10 @@ export {
15
15
  type GramproAIProviderProps,
16
16
  } from "./react/GramproAIProvider";
17
17
  export { useAskAgent, type AskableAgent, type AskExecution, type UseAskAgent } from "./react/useAskAgent";
18
+ export { useFieldAgent, type UseFieldAgentOptions } from "./react/useFieldAgent";
19
+ export { AgentSurface, useAgentSurface, type AgentSurfaceProps } from "./react/AgentSurface";
20
+ export {
21
+ useInAgentSurface,
22
+ type SurfaceFieldSource,
23
+ type SurfaceGridAgent,
24
+ } from "./react/agentSurfaceContext";
@@ -0,0 +1,145 @@
1
+ "use client";
2
+
3
+ /*
4
+ * <AgentSurface>: the page describes itself to an agent.
5
+ *
6
+ * <AgentSurface>
7
+ * <Input label="Name" />
8
+ * <Select label="Team" options={teams} />
9
+ * <DatePicker label="Due date" minDate={today} />
10
+ * <DataGrid data={rows} columns={columns} />
11
+ * </AgentSurface>
12
+ *
13
+ * Every operable component inside registers itself, with what it holds and
14
+ * accepts as it is rendered now. In a browser with WebMCP, one tool,
15
+ * `operate_page`, is registered: `describe` returns that live manifest,
16
+ * `operate` sends intents to one component by id. Each request goes through
17
+ * that component's own validator; nothing reaches the DOM that the component
18
+ * would not accept from a person.
19
+ *
20
+ * Without WebMCP nothing is registered and nothing breaks. `useAgentSurface()`
21
+ * hands the same surface to an application that runs its own agent loop.
22
+ */
23
+
24
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
25
+ import {
26
+ autoFieldExecutors,
27
+ createAgentSurface,
28
+ createFieldAgent,
29
+ describeFieldContract,
30
+ projectFieldAgent,
31
+ registerSurfaceTool,
32
+ type AgentSurfaceHandle as Surface,
33
+ type ToolCallLog,
34
+ } from "../core/agent";
35
+ import {
36
+ AgentSurfaceRegistrar,
37
+ type SurfaceFieldSource,
38
+ type SurfaceGridAgent,
39
+ type SurfaceRegistrar,
40
+ } from "./agentSurfaceContext";
41
+
42
+ const SurfaceContext = createContext<Surface | null>(null);
43
+
44
+ export interface AgentSurfaceProps {
45
+ children: ReactNode;
46
+ /** Register the `operate_page` WebMCP tool when the browser has it. Default true. */
47
+ webmcp?: boolean;
48
+ /** Override the tool name. One name per page: WebMCP cannot unregister a tool. */
49
+ toolName?: string;
50
+ /** Observe every tool call, for a UI log or a test. */
51
+ onCall?: (entry: ToolCallLog) => void;
52
+ }
53
+
54
+ /** A field as a member: rebuilt from its current source on every request. */
55
+ function fieldMember(source: () => SurfaceFieldSource) {
56
+ const first = source().contract;
57
+ const agentNow = () => {
58
+ const { contract, target } = source();
59
+ return createFieldAgent({
60
+ contract,
61
+ executors: autoFieldExecutors(target, {
62
+ checkable: contract.kind === "boolean",
63
+ component: contract.component,
64
+ }),
65
+ });
66
+ };
67
+ return {
68
+ kind: "field" as const,
69
+ component: first.component,
70
+ label: first.label,
71
+ agent: projectFieldAgent(agentNow),
72
+ describe: () => describeFieldContract(source().contract),
73
+ };
74
+ }
75
+
76
+ /** A grid as a member: its columns, operations and the schema of one intent. */
77
+ function gridMember(label: string | undefined, agent: SurfaceGridAgent) {
78
+ return {
79
+ kind: "grid" as const,
80
+ component: "DataGrid",
81
+ label,
82
+ agent,
83
+ describe: () => {
84
+ const contract = agent.contract();
85
+ return {
86
+ columns: contract.columns
87
+ // A policy-restricted column is not offered; the grid refuses it anyway.
88
+ .filter((column) => !column.restricted)
89
+ .map((column) => ({
90
+ id: column.id,
91
+ label: column.label,
92
+ type: column.type,
93
+ ...(column.sortable ? { sortable: true } : {}),
94
+ ...(column.filterable ? { filterable: true, operators: column.operators } : {}),
95
+ ...(column.options ? { options: column.options } : {}),
96
+ })),
97
+ operations: contract.operations,
98
+ rows: { showing: contract.stats?.filteredRows, total: contract.stats?.totalRows },
99
+ input: agent.schema(),
100
+ };
101
+ },
102
+ summarise: () => {
103
+ const contract = agent.contract();
104
+ return {
105
+ rowsShowing: contract.stats?.filteredRows,
106
+ rowsTotal: contract.stats?.totalRows,
107
+ canUndo: contract.state?.canUndo,
108
+ };
109
+ },
110
+ };
111
+ }
112
+
113
+ export function AgentSurface({ children, webmcp = true, toolName, onCall }: AgentSurfaceProps) {
114
+ const [surface] = useState(createAgentSurface);
115
+
116
+ const registrar = useMemo<SurfaceRegistrar>(
117
+ () => ({
118
+ field: (source) => surface.register(fieldMember(source)).unregister,
119
+ grid: (label, agent) => surface.register(gridMember(label, agent)).unregister,
120
+ }),
121
+ [surface],
122
+ );
123
+
124
+ useEffect(() => {
125
+ if (!webmcp) return undefined;
126
+ const registration = registerSurfaceTool(() => surface, { name: toolName, onCall });
127
+ return registration.release;
128
+ }, [surface, webmcp, toolName, onCall]);
129
+
130
+ return (
131
+ <SurfaceContext.Provider value={surface}>
132
+ <AgentSurfaceRegistrar.Provider value={registrar}>{children}</AgentSurfaceRegistrar.Provider>
133
+ </SurfaceContext.Provider>
134
+ );
135
+ }
136
+
137
+ /**
138
+ * The surface in scope, or null outside an <AgentSurface>.
139
+ *
140
+ * For an application that runs its own agent: `manifest()` is what to put in
141
+ * front of the model, `run(id, intents)` is how to act on its answer.
142
+ */
143
+ export function useAgentSurface(): Surface | null {
144
+ return useContext(SurfaceContext);
145
+ }
@@ -0,0 +1,92 @@
1
+ "use client";
2
+
3
+ /*
4
+ * How a component joins an <AgentSurface>, if there is one.
5
+ *
6
+ * Every operable component imports this, so it is kept to a context and two
7
+ * hooks with type-only imports: an app that never renders an <AgentSurface>
8
+ * pays for a `useContext` per component and nothing else. The validators,
9
+ * executors and WebMCP code live behind AgentSurface.tsx, and are only in a
10
+ * bundle that imports it.
11
+ *
12
+ * Components register *themselves*. That is the point: a page gets an agent
13
+ * interface without its author writing a single tool definition, and without
14
+ * one drifting from the UI, because the description is the component's own.
15
+ */
16
+ import { createContext, useContext, useEffect, useRef } from "react";
17
+ import type { FieldContract, FieldHandleTarget, ProjectableAgent } from "../core/agent";
18
+
19
+ /** What a field hands the surface: what it accepts now, and how to write to it. */
20
+ export interface SurfaceFieldSource {
21
+ readonly contract: FieldContract;
22
+ /** The element or handle to write through, read when an action runs. */
23
+ readonly target: () => HTMLInputElement | HTMLTextAreaElement | FieldHandleTarget | null;
24
+ }
25
+
26
+ /**
27
+ * A grid's agent, described structurally: `shared/` cannot import the
28
+ * DataGrid, and does not need to — anything with a schema, a validator and a
29
+ * live contract of columns can join.
30
+ */
31
+ export interface SurfaceGridAgent extends ProjectableAgent {
32
+ contract(): {
33
+ readonly columns: readonly {
34
+ readonly id: string;
35
+ readonly label: string;
36
+ readonly type: string;
37
+ readonly sortable: boolean;
38
+ readonly filterable: boolean;
39
+ readonly operators?: readonly string[];
40
+ readonly options?: readonly unknown[];
41
+ readonly restricted?: string;
42
+ }[];
43
+ readonly operations: readonly string[];
44
+ readonly stats?: { readonly filteredRows?: number | null; readonly totalRows?: number | null };
45
+ readonly state?: { readonly canUndo?: boolean };
46
+ };
47
+ }
48
+
49
+ export interface SurfaceRegistrar {
50
+ field(source: () => SurfaceFieldSource): () => void;
51
+ grid(label: string | undefined, agent: SurfaceGridAgent): () => void;
52
+ }
53
+
54
+ export const AgentSurfaceRegistrar = createContext<SurfaceRegistrar | null>(null);
55
+
56
+ /** Whether this component is inside an <AgentSurface>. */
57
+ export const useInAgentSurface = (): boolean => useContext(AgentSurfaceRegistrar) !== null;
58
+
59
+ /**
60
+ * Join the surface as a field, for as long as this component is mounted.
61
+ *
62
+ * `build` is read when an agent asks, not when the component renders, so a
63
+ * field that has just become disabled is refused on the very next request.
64
+ * `enabled: false` keeps a field out entirely — a password input is never
65
+ * offered to an agent.
66
+ */
67
+ export function useAgentSurfaceField(build: () => SurfaceFieldSource, enabled = true): void {
68
+ const registrar = useContext(AgentSurfaceRegistrar);
69
+ const latest = useRef(build);
70
+ latest.current = build;
71
+ useEffect(() => {
72
+ if (!registrar || !enabled) return undefined;
73
+ return registrar.field(() => latest.current());
74
+ }, [registrar, enabled]);
75
+ }
76
+
77
+ /** Join the surface as a grid, once its agent exists. */
78
+ export function useAgentSurfaceGrid(agent: SurfaceGridAgent | null, label: string | undefined): void {
79
+ const registrar = useContext(AgentSurfaceRegistrar);
80
+ useEffect(() => {
81
+ if (!registrar || !agent) return undefined;
82
+ return registrar.grid(label, agent);
83
+ }, [registrar, agent, label]);
84
+ }
85
+
86
+ /** The text of a label prop, when it is text. A ReactNode label has none to offer. */
87
+ export const labelText = (...candidates: unknown[]): string | undefined => {
88
+ for (const candidate of candidates) {
89
+ if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
90
+ }
91
+ return undefined;
92
+ };
@@ -1,88 +1,62 @@
1
- "use client";
2
-
3
- /*
4
- * Make a field agent-operable without changing the field.
5
- *
6
- * The DataGrid takes an `agent` prop because a grid's runtime needs to see the
7
- * data, the columns and the live state to say anything useful. A field needs
8
- * none of that: its contract is its own props, and its executor is the ref the
9
- * caller already holds. So this is a hook rather than a prop, and no component
10
- * had to grow an API to support it.
11
- *
12
- * const ref = useRef<HTMLInputElement>(null);
13
- * const agent = useFieldAgent({
14
- * contract: { component: "Input", kind: "text", operations: ["setValue", "clear", "focus"], maxLength: 80 },
15
- * target: ref,
16
- * });
17
- *
18
- * await agent.run({ action: "setValue", value: "Ada Lovelace" });
19
- *
20
- * `target` is whatever that component's ref gives you. Input, Textarea,
21
- * NumberInput, Checkbox, Switch and Radio hand back the DOM element; Combobox
22
- * and DatePicker hand back a handle. The hook tells them apart and nothing
23
- * above it has to care.
24
- */
25
-
26
- import { useMemo, useRef } from "react";
27
- import {
28
- createFieldAgent,
29
- domFieldExecutors,
30
- handleFieldExecutors,
31
- type FieldAgent,
32
- type FieldContract,
33
- type FieldHandleTarget,
34
- type RegisteredExecutor,
35
- } from "../core/agent";
36
-
37
- type Target =
38
- | { readonly current: HTMLInputElement | HTMLTextAreaElement | null }
39
- | { readonly current: FieldHandleTarget | null };
40
-
41
- export interface UseFieldAgentOptions {
42
- /** What this instance will accept. Rebuild it from props as they change. */
43
- readonly contract: FieldContract;
44
- /** The component's ref: a DOM element for most fields, a handle for some. */
45
- readonly target: Target;
46
- }
47
-
48
- /** A handle, as opposed to a DOM node. Checked by capability, not by instanceof. */
49
- const isHandle = (value: unknown): value is FieldHandleTarget =>
50
- Boolean(value) && typeof (value as FieldHandleTarget).setValue === "function";
51
-
52
- export function useFieldAgent({ contract, target }: UseFieldAgentOptions): FieldAgent {
53
- // The ref object is stable; its contents are not, and the executors read
54
- // them at call time rather than closing over whatever was there on mount.
55
- const latest = useRef(target);
56
- latest.current = target;
57
-
58
- return useMemo(() => {
59
- const read = () => latest.current.current as unknown;
60
-
61
- /*
62
- * Which kind of target this is cannot be decided here: a ref is null on
63
- * the first render, and deciding then would type every component as a DOM
64
- * field and never revisit it. Both registries are built, and the choice is
65
- * made inside `execute`, by which point the ref is populated.
66
- */
67
- const dom = domFieldExecutors(() => read() as HTMLInputElement | HTMLTextAreaElement | null, {
68
- checkable: contract.kind === "boolean",
69
- });
70
- const handle = handleFieldExecutors(() => read() as FieldHandleTarget | null);
71
-
72
- const executors = new Map<string, RegisteredExecutor<FieldContract>>();
73
- for (const name of new Set([...dom.keys(), ...handle.keys()])) {
74
- executors.set(name, {
75
- operation: name,
76
- execute: (input, context) => {
77
- const chosen = isHandle(read()) ? handle.get(name) : dom.get(name);
78
- if (!chosen) throw new Error(`${contract.component} cannot ${name}.`);
79
- return chosen.execute(input, context);
80
- },
81
- } as RegisteredExecutor<FieldContract>);
82
- }
83
-
84
- return createFieldAgent({ contract, executors });
85
- // The contract is the dependency that matters: a field that has just
86
- // become read-only must stop accepting writes on the very next render.
87
- }, [contract]);
88
- }
1
+ "use client";
2
+
3
+ /*
4
+ * Make a field agent-operable without changing the field.
5
+ *
6
+ * The DataGrid takes an `agent` prop because a grid's runtime needs to see the
7
+ * data, the columns and the live state to say anything useful. A field needs
8
+ * none of that: its contract is its own props, and its executor is the ref the
9
+ * caller already holds. So this is a hook rather than a prop, and no component
10
+ * had to grow an API to support it.
11
+ *
12
+ * const ref = useRef<HTMLInputElement>(null);
13
+ * const agent = useFieldAgent({
14
+ * contract: { component: "Input", kind: "text", operations: ["setValue", "clear", "focus"], maxLength: 80 },
15
+ * target: ref,
16
+ * });
17
+ *
18
+ * await agent.run({ action: "setValue", value: "Ada Lovelace" });
19
+ *
20
+ * `target` is whatever that component's ref gives you. Input, Textarea,
21
+ * NumberInput, Checkbox, Switch and Radio hand back the DOM element; Combobox
22
+ * and DatePicker hand back a handle. The hook tells them apart and nothing
23
+ * above it has to care.
24
+ */
25
+
26
+ import { useMemo, useRef } from "react";
27
+ import {
28
+ autoFieldExecutors,
29
+ createFieldAgent,
30
+ type FieldAgent,
31
+ type FieldContract,
32
+ type FieldHandleTarget,
33
+ } from "../core/agent";
34
+
35
+ type Target =
36
+ | { readonly current: HTMLInputElement | HTMLTextAreaElement | null }
37
+ | { readonly current: FieldHandleTarget | null };
38
+
39
+ export interface UseFieldAgentOptions {
40
+ /** What this instance will accept. Rebuild it from props as they change. */
41
+ readonly contract: FieldContract;
42
+ /** The component's ref: a DOM element for most fields, a handle for some. */
43
+ readonly target: Target;
44
+ }
45
+
46
+ export function useFieldAgent({ contract, target }: UseFieldAgentOptions): FieldAgent {
47
+ // The ref object is stable; its contents are not, and the executors read
48
+ // them at call time rather than closing over whatever was there on mount.
49
+ const latest = useRef(target);
50
+ latest.current = target;
51
+
52
+ return useMemo(() => {
53
+ // DOM element or handle is decided per call; see autoFieldExecutors.
54
+ const executors = autoFieldExecutors(() => latest.current.current, {
55
+ checkable: contract.kind === "boolean",
56
+ component: contract.component,
57
+ });
58
+ return createFieldAgent({ contract, executors });
59
+ // The contract is the dependency that matters: a field that has just
60
+ // become read-only must stop accepting writes on the very next render.
61
+ }, [contract]);
62
+ }
@@ -1,4 +1,4 @@
1
- {
2
- "name": "shared",
3
- "version": "1.3.0"
4
- }
1
+ {
2
+ "name": "shared",
3
+ "version": "1.4.0"
4
+ }
@@ -1,6 +1,6 @@
1
- /**
2
- * Bumped whenever anything in `shared/` changes. The installer copies
3
- * `version.json` next to it and compares the two, so a project is never left
4
- * with components built against a newer `shared/` than the one on disk.
5
- */
6
- export const version = "1.3.0";
1
+ /**
2
+ * Bumped whenever anything in `shared/` changes. The installer copies
3
+ * `version.json` next to it and compares the two, so a project is never left
4
+ * with components built against a newer `shared/` than the one on disk.
5
+ */
6
+ export const version = "1.4.0";
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.2",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/lines.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/agent.ts",
14
14
  "core/index.ts",
@@ -19,7 +19,7 @@
19
19
  "react/locale.ts",
20
20
  "react/props.ts"
21
21
  ],
22
- "sourceHash": "sha256:4e168aeb5ca0aef334176cc4edb1b15c63546bf415df867085dadf871f9e24af"
22
+ "sourceHash": "sha256:a6a81781252ff87f408c06ac48e31dcd38c85d879d15fae37647205527630667"
23
23
  },
24
24
  "purpose": "A setting that takes effect as it is flipped, on a native checkbox with role=switch, with an optimistic pending state while the change is saved.",
25
25
  "props": [
@@ -5,7 +5,7 @@
5
5
  "intentDomains": [
6
6
  "ActionIntent"
7
7
  ],
8
- "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
8
+ "$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap. Inside an <AgentSurface> the component registers itself with these operations; NumberInput and RadioGroup are then written through their own commit path rather than the element.",
9
9
  "operations": {
10
10
  "setValue": {
11
11
  "summary": "Write a value into the field, as though a person had entered it.",
@@ -15,6 +15,7 @@ import {
15
15
  type Ref,
16
16
  } from "react";
17
17
  import { describeField } from "../../shared/core/field";
18
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
18
19
  import { adopt, idleToggle, request, settle } from "../core/toggle";
19
20
  import type { SwitchLabelPosition, SwitchLocaleText, SwitchSize } from "../core/types";
20
21
  import { defaultSwitchText } from "./locale";
@@ -164,6 +165,21 @@ export function Switch(props: SwitchProps) {
164
165
  if (next !== checked) change(next);
165
166
  };
166
167
 
168
+ // Inside an <AgentSurface>, offered to an agent as a setting it can turn on
169
+ // or off. A switch still saving refuses, as it does a click.
170
+ useAgentSurfaceField(() => ({
171
+ contract: {
172
+ component: "Switch",
173
+ kind: "boolean",
174
+ label: labelText(label, inputProps["aria-label"], name, value),
175
+ disabled,
176
+ readOnly: readOnly || saving,
177
+ required,
178
+ operations: ["setValue", "clear", "focus"],
179
+ },
180
+ target: () => inputRef.current,
181
+ }));
182
+
167
183
  const invalid = Boolean(error);
168
184
  const describedBy = describeField(id, { extra: describedByProp, description, error });
169
185
 
@@ -12,7 +12,7 @@
12
12
  ]
13
13
  },
14
14
  "source": {
15
- "libraryVersion": "2.7.2",
15
+ "libraryVersion": "2.7.3",
16
16
  "files": [
17
17
  "core/index.ts",
18
18
  "core/navigation.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.2",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/agent.ts",
14
14
  "core/count.ts",
@@ -20,7 +20,7 @@
20
20
  "react/locale.ts",
21
21
  "react/props.ts"
22
22
  ],
23
- "sourceHash": "sha256:161bed57c97bbdede609bde754dcbae8452083825fea7749ee035ce1ace510dc"
23
+ "sourceHash": "sha256:32d4cdc40c633e35034d2b20b829e916b3caee74abe1af0ce3235e1348fead9e"
24
24
  },
25
25
  "purpose": "A multi-line text field that can grow with its content, with the same label, hint, error and counter wiring as Input.",
26
26
  "props": [