@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.
- package/.gbs/skills/gbs-components/SKILL.md +233 -209
- package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
- package/.gbs/skills/gbs-components/references/forms.md +12 -0
- package/.gbs/skills/gbs-components/references/install.md +14 -3
- package/CHANGELOG.md +963 -861
- package/README.md +11 -3
- package/index.cjs +108 -123
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/checkbox/passport.manual.json +1 -1
- package/source/components/checkbox/react/Checkbox.tsx +14 -0
- package/source/components/combobox/passport.json +3 -2
- package/source/components/combobox/passport.manual.json +1 -1
- package/source/components/combobox/react/MultiSelect.tsx +13 -0
- package/source/components/combobox/react/Select.tsx +13 -0
- package/source/components/combobox/react/surface.ts +70 -0
- package/source/components/data-grid/passport.json +6 -6
- package/source/components/data-grid/react/DataGrid.tsx +353 -351
- package/source/components/date-picker/passport.json +3 -2
- package/source/components/date-picker/passport.manual.json +1 -1
- package/source/components/date-picker/react/DatePicker.tsx +16 -2
- package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
- package/source/components/date-picker/react/surface.ts +68 -0
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +2 -2
- package/source/components/input/passport.manual.json +1 -1
- package/source/components/input/react/Input.tsx +21 -0
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +2 -2
- package/source/components/number-input/passport.manual.json +1 -1
- package/source/components/number-input/react/NumberInput.tsx +30 -0
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/radio-group/passport.manual.json +1 -1
- package/source/components/radio-group/react/RadioGroup.tsx +33 -0
- package/source/components/shared/core/agent/field-executors.ts +173 -137
- package/source/components/shared/core/agent/index.ts +21 -0
- package/source/components/shared/core/agent/surface.ts +437 -0
- package/source/components/shared/core/agent/webmcp.ts +332 -328
- package/source/components/shared/core/agent/write.ts +62 -58
- package/source/components/shared/index.ts +7 -0
- package/source/components/shared/react/AgentSurface.tsx +145 -0
- package/source/components/shared/react/agentSurfaceContext.ts +92 -0
- package/source/components/shared/react/useFieldAgent.ts +62 -88
- package/source/components/shared/version.json +4 -4
- package/source/components/shared/version.ts +6 -6
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +2 -2
- package/source/components/switch/passport.manual.json +1 -1
- package/source/components/switch/react/Switch.tsx +16 -0
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +2 -2
- package/source/components/textarea/passport.manual.json +1 -1
- package/source/components/textarea/react/Textarea.tsx +16 -0
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/doctor/baseline.cjs +5 -4
- package/tools/mcp/server.cjs +13 -1
- package/tools/passport/extract.cjs +20 -65
- package/tools/passport/hash.cjs +18 -1
- package/tools/passport/index.cjs +4 -2
- package/tools/passport/typescript.cjs +220 -0
- package/tools/prompt.cjs +205 -0
- 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,
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
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.
|
|
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";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
"source": {
|
|
11
|
-
"libraryVersion": "2.7.
|
|
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:
|
|
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
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
]
|
|
9
9
|
},
|
|
10
10
|
"source": {
|
|
11
|
-
"libraryVersion": "2.7.
|
|
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:
|
|
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": [
|