@grampro/kit 2.7.1 → 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 (80) hide show
  1. package/.gbs/skills/gbs-components/SKILL.md +233 -207
  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 +42 -5
  5. package/CHANGELOG.md +963 -798
  6. package/README.md +13 -4
  7. package/index.cjs +250 -138
  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 +228 -0
  69. package/tools/doctor/contract.cjs +252 -0
  70. package/tools/doctor/format.cjs +160 -0
  71. package/tools/doctor/index.cjs +246 -0
  72. package/tools/doctor/usage.cjs +95 -0
  73. package/tools/jsx-tags.cjs +120 -0
  74. package/tools/mcp/server.cjs +60 -41
  75. package/tools/passport/extract.cjs +22 -40
  76. package/tools/passport/hash.cjs +56 -0
  77. package/tools/passport/index.cjs +4 -2
  78. package/tools/passport/typescript.cjs +220 -0
  79. package/tools/prompt.cjs +205 -0
  80. package/tools/ui.cjs +67 -0
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.7.1",
13
+ "libraryVersion": "2.7.3",
14
14
  "files": [
15
15
  "core/agent.ts",
16
16
  "core/calendar.ts",
@@ -28,9 +28,10 @@
28
28
  "react/icons.tsx",
29
29
  "react/locale.ts",
30
30
  "react/props.ts",
31
+ "react/surface.ts",
31
32
  "react/useDatePicker.ts"
32
33
  ],
33
- "sourceHash": "sha256:abe15583c9bca1d5cce8aa9a3cbbcfcfd29ad44b7e63a3b66956089dc2d18f72"
34
+ "sourceHash": "sha256:3e136ee827a03bb66165debb1fd40926d6265f0fb017ef3b7cff60588140d741"
34
35
  },
35
36
  "purpose": "DatePicker and DateRangePicker: a text field people can type dates into, plus a calendar, both honouring the locale's order and separators.",
36
37
  "props": [
@@ -6,7 +6,7 @@
6
6
  "TemporalInterpretation",
7
7
  "ActionIntent"
8
8
  ],
9
- "$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.",
9
+ "$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.",
10
10
  "operations": {
11
11
  "setValue": {
12
12
  "summary": "Write a value into the field, as though a person had entered it.",
@@ -18,6 +18,7 @@ import { CalendarIcon, XIcon } from "./icons";
18
18
  import { defaultDatePickerText } from "./locale";
19
19
  import { DatePickerPopover } from "./Popover";
20
20
  import { cx, type DatePickerHandle, type DatePickerSharedProps } from "./props";
21
+ import { useDateSurface } from "./surface";
21
22
  import { useDatePicker } from "./useDatePicker";
22
23
 
23
24
  export interface DatePickerProps extends DatePickerSharedProps {
@@ -116,8 +117,8 @@ export function DatePicker(props: DatePickerProps) {
116
117
  const message = error ?? picker.inputError;
117
118
  const invalid = Boolean(message);
118
119
 
119
- useImperativeHandle(
120
- ref,
120
+ // One handle, for the ref and for an <AgentSurface>.
121
+ const handle = useMemo<DatePickerHandle<Date | null>>(
121
122
  () => ({
122
123
  open: () => picker.setOpen(true),
123
124
  close: () => picker.setOpen(false),
@@ -134,6 +135,19 @@ export function DatePicker(props: DatePickerProps) {
134
135
  }),
135
136
  [picker],
136
137
  );
138
+ useImperativeHandle(ref, () => handle, [handle]);
139
+ useDateSurface({
140
+ component: "DatePicker",
141
+ label,
142
+ name,
143
+ required,
144
+ disabled,
145
+ readOnly,
146
+ min: minDate,
147
+ max: maxDate,
148
+ isDateDisabled,
149
+ handle,
150
+ });
137
151
 
138
152
  const onInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
139
153
  if (event.key === "ArrowDown" || (event.key === "Enter" && !picker.open)) {
@@ -18,6 +18,7 @@ import { CalendarIcon, XIcon } from "./icons";
18
18
  import { defaultDatePickerText } from "./locale";
19
19
  import { DatePickerPopover } from "./Popover";
20
20
  import { cx, type DatePickerHandle, type DatePickerSharedProps } from "./props";
21
+ import { useDateSurface } from "./surface";
21
22
  import { useDatePicker, type RangeField } from "./useDatePicker";
22
23
 
23
24
  /** A range as it comes in from props: each end may be a Date, an ISO string or a timestamp. */
@@ -128,8 +129,8 @@ export function DateRangePicker(props: DateRangePickerProps) {
128
129
  const invalid = Boolean(message);
129
130
  const hasValue = start !== null || end !== null;
130
131
 
131
- useImperativeHandle(
132
- ref,
132
+ // One handle, for the ref and for an <AgentSurface>.
133
+ const handle = useMemo<DatePickerHandle<DateRange>>(
133
134
  () => ({
134
135
  open: () => picker.setOpen(true),
135
136
  close: () => picker.setOpen(false),
@@ -144,6 +145,19 @@ export function DateRangePicker(props: DateRangePickerProps) {
144
145
  }),
145
146
  [picker],
146
147
  );
148
+ useImperativeHandle(ref, () => handle, [handle]);
149
+ useDateSurface({
150
+ component: "DateRangePicker",
151
+ label,
152
+ name,
153
+ required,
154
+ disabled,
155
+ readOnly,
156
+ min: minDate,
157
+ max: maxDate,
158
+ isDateDisabled,
159
+ handle,
160
+ });
147
161
 
148
162
  const onInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
149
163
  if (event.key === "ArrowDown" || (event.key === "Enter" && !picker.open)) {
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
4
+ import { toDate, toISODate } from "../core/calendar";
5
+ import type { DateInput } from "../core/types";
6
+
7
+ interface DateSurfaceParams {
8
+ component: "DatePicker" | "DateRangePicker";
9
+ label: unknown;
10
+ name?: string;
11
+ required: boolean;
12
+ disabled: boolean;
13
+ readOnly: boolean;
14
+ min: Date | null;
15
+ max: Date | null;
16
+ isDateDisabled?: (date: Date) => boolean;
17
+ handle: {
18
+ setValue(value: unknown): void;
19
+ clear(): void;
20
+ focus(): void;
21
+ open(): void;
22
+ close(): void;
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Inside an <AgentSurface>, offer a date or range field with its bounds.
28
+ *
29
+ * `min` and `max` go in the contract, where the validator refuses an
30
+ * out-of-range day with the reason. `isDateDisabled` is a function and cannot
31
+ * be described, so it is checked at the write instead: a disabled day is
32
+ * refused there rather than quietly chosen.
33
+ */
34
+ export function useDateSurface(params: DateSurfaceParams): void {
35
+ useAgentSurfaceField(() => {
36
+ const { component, handle, isDateDisabled } = params;
37
+ const refuseDisabled = (value: DateInput | null | undefined) => {
38
+ const date = toDate(value ?? null);
39
+ if (date && isDateDisabled?.(date)) throw new Error(`${toISODate(date)} is not available in this field.`);
40
+ };
41
+ return {
42
+ contract: {
43
+ component,
44
+ kind: component === "DateRangePicker" ? "dateRange" : "date",
45
+ label: labelText(params.label, params.name),
46
+ disabled: params.disabled,
47
+ readOnly: params.readOnly,
48
+ required: params.required,
49
+ ...(params.min ? { min: toISODate(params.min) } : {}),
50
+ ...(params.max ? { max: toISODate(params.max) } : {}),
51
+ operations: ["setValue", "clear", "focus", "open", "close"],
52
+ },
53
+ target: () => ({
54
+ ...handle,
55
+ setValue: (next) => {
56
+ if (next && typeof next === "object") {
57
+ const range = next as { start?: DateInput; end?: DateInput };
58
+ refuseDisabled(range.start);
59
+ refuseDisabled(range.end);
60
+ } else {
61
+ refuseDisabled(next as DateInput);
62
+ }
63
+ handle.setValue(next);
64
+ },
65
+ }),
66
+ };
67
+ });
68
+ }
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.1",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/api.ts",
15
15
  "core/dialog.ts",
@@ -13,7 +13,7 @@
13
13
  ]
14
14
  },
15
15
  "source": {
16
- "libraryVersion": "2.7.1",
16
+ "libraryVersion": "2.7.3",
17
17
  "files": [
18
18
  "core/format.ts",
19
19
  "core/index.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.1",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/agent.ts",
15
15
  "core/count.ts",
@@ -24,7 +24,7 @@
24
24
  "react/locale.ts",
25
25
  "react/props.ts"
26
26
  ],
27
- "sourceHash": "sha256:6d1ef1229a82962e2bd3bf32a15de6f7d9e43c2270e65df9f7303cd74dd479d4"
27
+ "sourceHash": "sha256:747f35b5f70457677fbc0a0ce1b8c47404e45b4e30a37c4567871fa3b21e16f6"
28
28
  },
29
29
  "purpose": "A single-line text field with its label, hint and error wired together. OtpInput is the same field split into one box per character for a one-time code.",
30
30
  "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.",
@@ -13,6 +13,7 @@ import {
13
13
  } from "react";
14
14
  import { useControllableState } from "../../shared/react/useControllableState";
15
15
  import { describeField } from "../../shared/core/field";
16
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
16
17
  import { countCharacters } from "../core/count";
17
18
  import type { FieldSize, InputLocaleText } from "../core/types";
18
19
  import { setNativeValue } from "./dom";
@@ -95,6 +96,26 @@ export function Input(props: InputProps) {
95
96
  const [current, setCurrent] = useControllableState(valueProp, defaultValue ?? "");
96
97
  const [revealed, setRevealed] = useState(false);
97
98
 
99
+ // Inside an <AgentSurface>, offered to an agent with what it accepts now.
100
+ // A password, a hidden or a file input never is.
101
+ useAgentSurfaceField(
102
+ () => ({
103
+ contract: {
104
+ component: "Input",
105
+ kind: "text",
106
+ label: labelText(label, inputProps["aria-label"], inputProps.name, inputProps.placeholder),
107
+ disabled,
108
+ readOnly,
109
+ required,
110
+ maxLength,
111
+ pattern: inputProps.pattern,
112
+ operations: ["setValue", "clear", "focus"],
113
+ },
114
+ target: () => inputRef.current,
115
+ }),
116
+ type !== "password" && type !== "hidden" && type !== "file",
117
+ );
118
+
98
119
  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
99
120
  setCurrent(event.target.value);
100
121
  onChange?.(event);
@@ -16,7 +16,7 @@
16
16
  ]
17
17
  },
18
18
  "source": {
19
- "libraryVersion": "2.7.1",
19
+ "libraryVersion": "2.7.3",
20
20
  "files": [
21
21
  "core/index.ts",
22
22
  "core/navigation.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.1",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/dismiss.ts",
14
14
  "core/index.ts",
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.1",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/agent.ts",
14
14
  "core/format.ts",
@@ -22,7 +22,7 @@
22
22
  "react/locale.ts",
23
23
  "react/props.ts"
24
24
  ],
25
- "sourceHash": "sha256:b62e20ffb71a95c6e30e7c55f895750de1015364734a3cceacea972e3dc89e6a"
25
+ "sourceHash": "sha256:e29f16b8317ad5e1ce54a08a1705e24fd0be033344ba8509048b4c168d2f64a4"
26
26
  },
27
27
  "purpose": "A numeric field that reads and writes numbers the way the locale does, with a stepper, clamping and currency formatting.",
28
28
  "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.",
@@ -16,6 +16,7 @@ import {
16
16
  type Ref,
17
17
  } from "react";
18
18
  import { describeField } from "../../shared/core/field";
19
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
19
20
  import { formatNumber } from "../core/format";
20
21
  import { parseNumber, toEditText } from "../core/parse";
21
22
  import { clamp, round, snapToStep, stepBy } from "../core/step";
@@ -195,6 +196,35 @@ export function NumberInput(props: NumberInputProps) {
195
196
  [decimals, snap, step, min, max, clampBehavior],
196
197
  );
197
198
 
199
+ // Inside an <AgentSurface>, offered to an agent as a number with its bounds.
200
+ // Written as a number through `commit`, not as text into the element: text
201
+ // would be read in this field's locale, where "1.5" may mean fifteen.
202
+ useAgentSurfaceField(() => ({
203
+ contract: {
204
+ component: "NumberInput",
205
+ kind: "number",
206
+ label: labelText(label, inputProps["aria-label"], inputProps.name, placeholder),
207
+ disabled,
208
+ readOnly,
209
+ required,
210
+ min,
211
+ max,
212
+ ...(snap ? { step } : {}),
213
+ operations: ["setValue", "clear", "focus"],
214
+ },
215
+ target: () => ({
216
+ setValue: (next) => {
217
+ setDraft(null);
218
+ commit(next === null || next === undefined ? null : settle(Number(next)));
219
+ },
220
+ clear: () => {
221
+ setDraft(null);
222
+ commit(null);
223
+ },
224
+ focus: () => inputRef.current?.focus(),
225
+ }),
226
+ }));
227
+
198
228
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
199
229
  const typed = event.target.value;
200
230
  setDraft(typed);
@@ -8,7 +8,7 @@
8
8
  ]
9
9
  },
10
10
  "source": {
11
- "libraryVersion": "2.7.1",
11
+ "libraryVersion": "2.7.3",
12
12
  "files": [
13
13
  "core/index.ts",
14
14
  "core/types.ts",
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "source": {
12
- "libraryVersion": "2.7.1",
12
+ "libraryVersion": "2.7.3",
13
13
  "files": [
14
14
  "core/index.ts",
15
15
  "core/progress.ts",
@@ -10,7 +10,7 @@
10
10
  ]
11
11
  },
12
12
  "source": {
13
- "libraryVersion": "2.7.1",
13
+ "libraryVersion": "2.7.3",
14
14
  "files": [
15
15
  "core/agent.ts",
16
16
  "core/group.ts",
@@ -23,7 +23,7 @@
23
23
  "react/locale.ts",
24
24
  "react/props.ts"
25
25
  ],
26
- "sourceHash": "sha256:8630723accbd754b823c4f2d7f5c1c0da02252d3a51f87b8abeaad117bd75064"
26
+ "sourceHash": "sha256:bcc1323c5e328b91461ef96c6901d41bf0eca3ec6dcf7b96c6ec45a9efa2a155"
27
27
  },
28
28
  "purpose": "One choice out of a few, on native radios: a single tab stop, arrow keys between the choices, and a card variant for richer options.",
29
29
  "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.",
@@ -3,6 +3,7 @@
3
3
  import { useCallback, useId, useMemo, type CSSProperties, type ReactNode } from "react";
4
4
  import { useControllableState } from "../../shared/react/useControllableState";
5
5
  import { describeField } from "../../shared/core/field";
6
+ import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
6
7
  import { hasEnabledOption, normalizeOptions, resolveValue } from "../core/group";
7
8
  import type {
8
9
  RadioGroupLocaleText,
@@ -109,6 +110,38 @@ export function RadioGroup(props: RadioGroupProps) {
109
110
  [setCurrent, onValueChange],
110
111
  );
111
112
 
113
+ // Inside an <AgentSurface>, offered to an agent as one choice among the
114
+ // options. Written through `commit`, as a click is: a radio element would be
115
+ // only one of the choices.
116
+ useAgentSurfaceField(() => ({
117
+ contract: {
118
+ component: "RadioGroup",
119
+ kind: "choice",
120
+ label: labelText(label, nameProp),
121
+ disabled,
122
+ required,
123
+ ...(items
124
+ ? {
125
+ options: items.map((option) => ({
126
+ value: option.value,
127
+ label: labelText(option.label) ?? option.value,
128
+ ...(option.disabled ? { disabled: true } : {}),
129
+ })),
130
+ }
131
+ : {}),
132
+ operations: ["setValue", "clear", "focus"],
133
+ },
134
+ target: () => ({
135
+ setValue: (next) => commit(next === null || next === undefined ? null : String(next)),
136
+ clear: () => commit(null),
137
+ focus: () => {
138
+ const group = document.getElementById(id);
139
+ const radio = group?.querySelector<HTMLInputElement>("input:checked") ?? group?.querySelector("input");
140
+ radio?.focus();
141
+ },
142
+ }),
143
+ }));
144
+
112
145
  const context = useMemo<RadioGroupContextValue>(
113
146
  () => ({
114
147
  value: selected,