@assistant-ui/react-generative-ui 0.0.21 → 0.0.23

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 (212) hide show
  1. package/LICENSE +1 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +30 -13
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/RadioGroupScope.d.ts +6 -0
  6. package/dist/RadioGroupScope.d.ts.map +1 -0
  7. package/dist/RadioGroupScope.js +25 -0
  8. package/dist/RadioGroupScope.js.map +1 -0
  9. package/dist/a2ui/BindingContext.d.ts +11 -0
  10. package/dist/a2ui/BindingContext.d.ts.map +1 -0
  11. package/dist/a2ui/BindingContext.js +12 -0
  12. package/dist/a2ui/BindingContext.js.map +1 -0
  13. package/dist/a2ui/PresentRenderer.d.ts +11 -0
  14. package/dist/a2ui/PresentRenderer.d.ts.map +1 -0
  15. package/dist/a2ui/PresentRenderer.js +39 -0
  16. package/dist/a2ui/PresentRenderer.js.map +1 -0
  17. package/dist/a2ui/convert.d.ts +6 -0
  18. package/dist/a2ui/convert.d.ts.map +1 -1
  19. package/dist/a2ui/convert.js +363 -85
  20. package/dist/a2ui/convert.js.map +1 -1
  21. package/dist/a2ui/dataModel.d.ts +9 -0
  22. package/dist/a2ui/dataModel.d.ts.map +1 -0
  23. package/dist/a2ui/dataModel.js +44 -0
  24. package/dist/a2ui/dataModel.js.map +1 -0
  25. package/dist/a2ui/reducer.d.ts +5 -0
  26. package/dist/a2ui/reducer.d.ts.map +1 -1
  27. package/dist/a2ui/reducer.js +3 -3
  28. package/dist/a2ui/reducer.js.map +1 -1
  29. package/dist/a2ui/types.d.ts +8 -2
  30. package/dist/a2ui/types.d.ts.map +1 -1
  31. package/dist/a2ui/types.js.map +1 -1
  32. package/dist/a2ui/valueFunctions.d.ts +16 -0
  33. package/dist/a2ui/valueFunctions.d.ts.map +1 -0
  34. package/dist/a2ui/valueFunctions.js +332 -0
  35. package/dist/a2ui/valueFunctions.js.map +1 -0
  36. package/dist/actionRegistry.d.ts +1 -1
  37. package/dist/actionRegistry.js.map +1 -1
  38. package/dist/answeredValues.d.ts +16 -0
  39. package/dist/answeredValues.d.ts.map +1 -0
  40. package/dist/answeredValues.js +37 -0
  41. package/dist/answeredValues.js.map +1 -0
  42. package/dist/constants.d.ts +3 -0
  43. package/dist/constants.d.ts.map +1 -1
  44. package/dist/constants.js +4 -1
  45. package/dist/constants.js.map +1 -1
  46. package/dist/fieldReferences.d.ts +9 -0
  47. package/dist/fieldReferences.d.ts.map +1 -0
  48. package/dist/fieldReferences.js +53 -0
  49. package/dist/fieldReferences.js.map +1 -0
  50. package/dist/generativeUIToJSX.js +3 -3
  51. package/dist/generativeUIToJSX.js.map +1 -1
  52. package/dist/index.js +2 -2
  53. package/dist/index.server.js +2 -2
  54. package/dist/index.shared.js +2 -2
  55. package/dist/ir.d.ts +1 -1
  56. package/dist/ir.js.map +1 -1
  57. package/dist/renderGenerativeUI.d.ts.map +1 -1
  58. package/dist/renderGenerativeUI.js +12 -1
  59. package/dist/renderGenerativeUI.js.map +1 -1
  60. package/dist/slack/constants.d.ts +2 -0
  61. package/dist/slack/constants.d.ts.map +1 -1
  62. package/dist/slack/constants.js +3 -1
  63. package/dist/slack/constants.js.map +1 -1
  64. package/dist/slack/decodeBlockAction.d.ts +3 -1
  65. package/dist/slack/decodeBlockAction.d.ts.map +1 -1
  66. package/dist/slack/decodeBlockAction.js +96 -7
  67. package/dist/slack/decodeBlockAction.js.map +1 -1
  68. package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
  69. package/dist/slack/fromSlackBlocks.js +43 -3
  70. package/dist/slack/fromSlackBlocks.js.map +1 -1
  71. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  72. package/dist/slack/toSlackBlocks.js +244 -20
  73. package/dist/slack/toSlackBlocks.js.map +1 -1
  74. package/dist/slack/types.d.ts +24 -1
  75. package/dist/slack/types.d.ts.map +1 -1
  76. package/dist/slack.d.ts +1 -1
  77. package/dist/slack.d.ts.map +1 -1
  78. package/dist/teams/constants.d.ts +1 -1
  79. package/dist/teams/constants.d.ts.map +1 -1
  80. package/dist/teams/constants.js +1 -1
  81. package/dist/teams/constants.js.map +1 -1
  82. package/dist/teams/decodeSubmitData.d.ts +4 -0
  83. package/dist/teams/decodeSubmitData.d.ts.map +1 -1
  84. package/dist/teams/decodeSubmitData.js +29 -3
  85. package/dist/teams/decodeSubmitData.js.map +1 -1
  86. package/dist/teams/temporalId.d.ts +5 -0
  87. package/dist/teams/temporalId.d.ts.map +1 -0
  88. package/dist/teams/temporalId.js +28 -0
  89. package/dist/teams/temporalId.js.map +1 -0
  90. package/dist/teams/toAdaptiveCard.d.ts +5 -4
  91. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  92. package/dist/teams/toAdaptiveCard.js +136 -18
  93. package/dist/teams/toAdaptiveCard.js.map +1 -1
  94. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  95. package/dist/teams/toTeamsAttachments.js.map +1 -1
  96. package/dist/teams/types.d.ts +23 -2
  97. package/dist/teams/types.d.ts.map +1 -1
  98. package/dist/teams.d.ts +1 -1
  99. package/dist/teams.d.ts.map +1 -1
  100. package/dist/temporal.d.ts +33 -0
  101. package/dist/temporal.d.ts.map +1 -0
  102. package/dist/temporal.js +152 -0
  103. package/dist/temporal.js.map +1 -0
  104. package/dist/vocabulary/alert.d.ts +9 -2
  105. package/dist/vocabulary/alert.d.ts.map +1 -1
  106. package/dist/vocabulary/alert.js +116 -19
  107. package/dist/vocabulary/alert.js.map +1 -1
  108. package/dist/vocabulary/collectFormValues.d.ts +2 -1
  109. package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
  110. package/dist/vocabulary/collectFormValues.js +15 -4
  111. package/dist/vocabulary/collectFormValues.js.map +1 -1
  112. package/dist/vocabulary/data.d.ts +22 -5
  113. package/dist/vocabulary/data.d.ts.map +1 -1
  114. package/dist/vocabulary/data.js +278 -83
  115. package/dist/vocabulary/data.js.map +1 -1
  116. package/dist/vocabulary/dispatch.d.ts +3 -2
  117. package/dist/vocabulary/dispatch.d.ts.map +1 -1
  118. package/dist/vocabulary/dispatch.js +33 -4
  119. package/dist/vocabulary/dispatch.js.map +1 -1
  120. package/dist/vocabulary/fact.d.ts +8 -1
  121. package/dist/vocabulary/fact.d.ts.map +1 -1
  122. package/dist/vocabulary/fact.js +37 -11
  123. package/dist/vocabulary/fact.js.map +1 -1
  124. package/dist/vocabulary/form.d.ts.map +1 -1
  125. package/dist/vocabulary/form.js +3 -3
  126. package/dist/vocabulary/form.js.map +1 -1
  127. package/dist/vocabulary/formatValue.d.ts +7 -0
  128. package/dist/vocabulary/formatValue.d.ts.map +1 -0
  129. package/dist/vocabulary/formatValue.js +76 -0
  130. package/dist/vocabulary/formatValue.js.map +1 -0
  131. package/dist/vocabulary/icon.d.ts +1 -1
  132. package/dist/vocabulary/interactive.d.ts +143 -8
  133. package/dist/vocabulary/interactive.d.ts.map +1 -1
  134. package/dist/vocabulary/interactive.js +458 -107
  135. package/dist/vocabulary/interactive.js.map +1 -1
  136. package/dist/vocabulary/layout.d.ts.map +1 -1
  137. package/dist/vocabulary/layout.js +4 -4
  138. package/dist/vocabulary/layout.js.map +1 -1
  139. package/dist/vocabulary/list.js +2 -2
  140. package/dist/vocabulary/list.js.map +1 -1
  141. package/dist/vocabulary/text.d.ts +1 -1
  142. package/package.json +8 -5
  143. package/src/JSONGenerativeUI.client.tsx +84 -40
  144. package/src/JSONGenerativeUI.test.tsx +964 -68
  145. package/src/RadioGroupScope.server.test.tsx +24 -0
  146. package/src/RadioGroupScope.tsx +34 -0
  147. package/src/a2ui/BindingContext.ts +26 -0
  148. package/src/a2ui/PresentRenderer.tsx +83 -0
  149. package/src/a2ui/convert.test.ts +2054 -229
  150. package/src/a2ui/convert.ts +665 -134
  151. package/src/a2ui/dataModel.ts +69 -0
  152. package/src/a2ui/reducer.test.ts +35 -0
  153. package/src/a2ui/reducer.ts +5 -4
  154. package/src/a2ui/types.ts +9 -2
  155. package/src/a2ui/valueFunctions.test.ts +271 -0
  156. package/src/a2ui/valueFunctions.ts +523 -0
  157. package/src/actionRegistry.ts +1 -1
  158. package/src/answeredValues.test.ts +33 -0
  159. package/src/answeredValues.tsx +70 -0
  160. package/src/constants.ts +5 -0
  161. package/src/fieldReferences.test.ts +66 -0
  162. package/src/fieldReferences.ts +83 -0
  163. package/src/generativeUIToJSX.test.ts +18 -0
  164. package/src/generativeUIToJSX.ts +4 -6
  165. package/src/index.server.test.ts +106 -0
  166. package/src/ir.ts +1 -1
  167. package/src/renderGenerativeUI.test.tsx +14 -5
  168. package/src/renderGenerativeUI.tsx +30 -1
  169. package/src/slack/a2uiSlack.test.ts +198 -0
  170. package/src/slack/constants.ts +3 -0
  171. package/src/slack/datePicker.test.ts +612 -0
  172. package/src/slack/decodeBlockAction.test.ts +98 -0
  173. package/src/slack/decodeBlockAction.ts +182 -16
  174. package/src/slack/fromSlackBlocks.test.ts +222 -3
  175. package/src/slack/fromSlackBlocks.ts +82 -2
  176. package/src/slack/toSlackBlocks.test.ts +682 -3
  177. package/src/slack/toSlackBlocks.ts +463 -25
  178. package/src/slack/types.ts +28 -0
  179. package/src/slack.ts +2 -0
  180. package/src/teams/constants.ts +1 -1
  181. package/src/teams/decodeSubmitData.test.ts +249 -0
  182. package/src/teams/decodeSubmitData.ts +57 -4
  183. package/src/teams/temporal.test.ts +766 -0
  184. package/src/teams/temporalId.ts +56 -0
  185. package/src/teams/toAdaptiveCard.test.ts +219 -0
  186. package/src/teams/toAdaptiveCard.ts +259 -21
  187. package/src/teams/toTeamsAttachments.ts +4 -1
  188. package/src/teams/types.ts +21 -1
  189. package/src/teams.ts +1 -0
  190. package/src/temporal.test.ts +319 -0
  191. package/src/temporal.ts +259 -0
  192. package/src/vocabulary/alert.test.tsx +288 -7
  193. package/src/vocabulary/alert.tsx +168 -26
  194. package/src/vocabulary/collectFormValues.test.ts +225 -1
  195. package/src/vocabulary/collectFormValues.ts +54 -5
  196. package/src/vocabulary/data.test.tsx +248 -6
  197. package/src/vocabulary/data.tsx +398 -100
  198. package/src/vocabulary/datepicker.dom.test.tsx +1148 -0
  199. package/src/vocabulary/dispatch.ts +63 -2
  200. package/src/vocabulary/fact.test.tsx +67 -0
  201. package/src/vocabulary/fact.tsx +40 -9
  202. package/src/vocabulary/form.tsx +7 -2
  203. package/src/vocabulary/formatValue.test.ts +72 -0
  204. package/src/vocabulary/formatValue.ts +127 -0
  205. package/src/vocabulary/index.test.ts +2 -0
  206. package/src/vocabulary/interactive.dispatch.test.tsx +167 -79
  207. package/src/vocabulary/interactive.dom.test.tsx +1747 -0
  208. package/src/vocabulary/interactive.test.tsx +60 -4
  209. package/src/vocabulary/interactive.tsx +910 -155
  210. package/src/vocabulary/layout.test.tsx +6 -4
  211. package/src/vocabulary/layout.tsx +13 -2
  212. package/src/vocabulary/list.tsx +2 -2
@@ -1,6 +1,11 @@
1
- import { useId, type ReactNode } from "react";
1
+ import * as React from "react";
2
2
  import { z } from "zod";
3
- import { GENERATED_NAME_ATTR } from "../constants";
3
+ import {
4
+ CHECKBOX_GROUP_ATTR,
5
+ FIELD_NAME_ATTR,
6
+ FIELD_VALUE_ATTR,
7
+ GENERATED_NAME_ATTR,
8
+ } from "../constants";
4
9
  import type { Action } from "../ir";
5
10
  import { BUTTON_STYLES } from "../ir";
6
11
  import type {
@@ -8,36 +13,67 @@ import type {
8
13
  GenerativeUILibrary,
9
14
  GenerativeUIStatus,
10
15
  } from "../types";
16
+ import { A2uiBindingContext, useA2uiBinding } from "../a2ui/BindingContext";
17
+ import { useAnsweredValue } from "../answeredValues";
11
18
  import { actionAttr, fire } from "./dispatch";
12
19
  import { toTextContent } from "./toTextContent";
20
+ import { useRadioGroupName } from "../RadioGroupScope";
21
+ import {
22
+ classifyTemporal,
23
+ fromLocalDateTime,
24
+ getTemporalInputStep,
25
+ normalizeTemporalInputValue,
26
+ toLocalDateTime,
27
+ toPickerLocalDateTime,
28
+ } from "../temporal";
13
29
 
14
30
  const optionSchema = z.object({
15
31
  label: z.string(),
16
32
  value: z.string(),
17
33
  });
18
34
 
19
- type Option = { label: string; value: string };
35
+ const describedOptionSchema = optionSchema.extend({
36
+ description: z.string().optional(),
37
+ });
38
+
39
+ type Option = { label: string; description?: string; value: string };
20
40
 
21
41
  const isOption = (option: unknown): option is Option =>
22
42
  option !== null &&
23
43
  typeof option === "object" &&
24
44
  "label" in option &&
25
45
  typeof option.label === "string" &&
46
+ (!("description" in option) || typeof option.description === "string") &&
26
47
  "value" in option &&
27
48
  typeof option.value === "string";
28
49
 
50
+ const mapOptions = (
51
+ options: unknown,
52
+ render: (option: Option, key: string) => React.ReactNode,
53
+ ) => {
54
+ const occurrences = new Map<string, number>();
55
+ return (Array.isArray(options) ? options : []).map((option) => {
56
+ if (!isOption(option)) return null;
57
+ const occurrence = occurrences.get(option.value) ?? 0;
58
+ occurrences.set(option.value, occurrence + 1);
59
+ return render(option, JSON.stringify([option.value, occurrence]));
60
+ });
61
+ };
62
+
29
63
  type RadioGroupRenderProps = {
64
+ value?: string;
30
65
  options: Option[];
31
66
  name?: string;
32
67
  label?: string;
33
68
  defaultValue?: string;
34
- children?: ReactNode;
69
+ children?: React.ReactNode;
35
70
  $status: GenerativeUIStatus;
36
71
  $action?: Action;
37
72
  $dispatch?: GenerativeUIDispatch;
38
73
  };
39
74
 
40
75
  function RadioGroupRender({
76
+ value,
41
77
  options,
42
78
  name,
43
79
  label,
@@ -46,35 +82,806 @@ function RadioGroupRender({
46
82
  $action,
47
83
  $dispatch,
48
84
  }: RadioGroupRenderProps) {
49
- const generatedName = useId();
50
- const fieldName = name ?? generatedName;
51
- const safeOptions = Array.isArray(options) ? options : [];
85
+ const groupName = useRadioGroupName(name);
86
+ const answeredValue = useAnsweredValue(name);
87
+ const updateBinding = useA2uiBinding(name);
88
+ const isBound = updateBinding !== undefined;
89
+ const initialValue =
90
+ typeof answeredValue === "string" ? answeredValue : defaultValue;
52
91
  return (
53
92
  <fieldset
93
+ key={initialValue}
54
94
  data-aui="radiogroup"
55
95
  data-aui-action={actionAttr($action)}
56
96
  aria-label={label}
57
97
  >
58
- {safeOptions.map((option, i) =>
59
- isOption(option) ? (
60
- <label key={i} data-aui="radiogroup-option">
61
- <input
62
- type="radio"
63
- name={fieldName}
64
- {...(name == null ? { [GENERATED_NAME_ATTR]: "" } : {})}
65
- value={option.value}
66
- defaultChecked={defaultValue === option.value}
67
- onChange={() => fire($action, $dispatch, option.value)}
68
- />
69
- {option.label}
70
- </label>
71
- ) : null,
72
- )}
98
+ {mapOptions(options, (option, key) => (
99
+ <label key={key} data-aui="radiogroup-option">
100
+ <input
101
+ type="radio"
102
+ name={groupName}
103
+ {...{ [FIELD_NAME_ATTR]: name }}
104
+ {...(name == null ? { [GENERATED_NAME_ATTR]: "" } : {})}
105
+ value={option.value}
106
+ {...(isBound
107
+ ? { checked: (answeredValue ?? value) === option.value }
108
+ : { defaultChecked: initialValue === option.value })}
109
+ onChange={(e) => {
110
+ updateBinding?.(option.value);
111
+ fire($action, $dispatch, option.value, e.currentTarget);
112
+ }}
113
+ />
114
+ {option.description ? (
115
+ <span data-aui="option-content">
116
+ <span data-aui="option-label">{option.label}</span>
117
+ <span data-aui="option-description">{option.description}</span>
118
+ </span>
119
+ ) : (
120
+ option.label
121
+ )}
122
+ </label>
123
+ ))}
73
124
  {children}
74
125
  </fieldset>
75
126
  );
76
127
  }
77
128
 
129
+ type CheckboxGroupRenderProps = {
130
+ value?: string[];
131
+ options: Option[];
132
+ name?: string;
133
+ label?: string;
134
+ defaultValue?: string[];
135
+ children?: React.ReactNode;
136
+ $status: GenerativeUIStatus;
137
+ $action?: Action;
138
+ $dispatch?: GenerativeUIDispatch;
139
+ };
140
+
141
+ const checkedGroupValues = (input: HTMLInputElement): string[] =>
142
+ Array.from(
143
+ input
144
+ .closest("fieldset")
145
+ ?.querySelectorAll<HTMLInputElement>(
146
+ `:scope > label > input[${CHECKBOX_GROUP_ATTR}]:checked`,
147
+ ) ?? [],
148
+ (checkbox) => checkbox.value,
149
+ );
150
+
151
+ function CheckboxGroupRender({
152
+ value,
153
+ options,
154
+ name,
155
+ label,
156
+ defaultValue,
157
+ children,
158
+ $action,
159
+ $dispatch,
160
+ }: CheckboxGroupRenderProps) {
161
+ const generatedName = React.useId();
162
+ const fieldName = name ?? generatedName;
163
+ const answeredValue = useAnsweredValue(name);
164
+ const updateBinding = useA2uiBinding(name);
165
+ const isBound = updateBinding !== undefined;
166
+ const checkedValues =
167
+ Array.isArray(answeredValue) &&
168
+ answeredValue.every((value): value is string => typeof value === "string")
169
+ ? answeredValue
170
+ : Array.isArray(defaultValue)
171
+ ? defaultValue
172
+ : [];
173
+ return (
174
+ <fieldset
175
+ key={JSON.stringify(checkedValues)}
176
+ data-aui="checkboxgroup"
177
+ data-aui-action={actionAttr($action)}
178
+ aria-label={label}
179
+ >
180
+ {mapOptions(options, (option, key) => (
181
+ <label key={key} data-aui="checkboxgroup-option">
182
+ <input
183
+ type="checkbox"
184
+ name={fieldName}
185
+ {...{ [CHECKBOX_GROUP_ATTR]: "" }}
186
+ {...(name == null ? { [GENERATED_NAME_ATTR]: "" } : {})}
187
+ value={option.value}
188
+ {...(isBound
189
+ ? {
190
+ checked: (Array.isArray(answeredValue)
191
+ ? checkedValues
192
+ : (value ?? [])
193
+ ).includes(option.value),
194
+ }
195
+ : { defaultChecked: checkedValues.includes(option.value) })}
196
+ onChange={(e) => {
197
+ const selected = checkedGroupValues(e.currentTarget);
198
+ updateBinding?.(selected);
199
+ fire($action, $dispatch, selected, e.currentTarget);
200
+ }}
201
+ />
202
+ {option.description ? (
203
+ <span data-aui="option-content">
204
+ <span data-aui="option-label">{option.label}</span>
205
+ <span data-aui="option-description">{option.description}</span>
206
+ </span>
207
+ ) : (
208
+ option.label
209
+ )}
210
+ </label>
211
+ ))}
212
+ {children}
213
+ </fieldset>
214
+ );
215
+ }
216
+
217
+ type SelectRenderProps = {
218
+ value?: string;
219
+ options: Option[];
220
+ placeholder?: string;
221
+ label?: string;
222
+ name?: string;
223
+ defaultValue?: string;
224
+ children?: React.ReactNode;
225
+ $status: GenerativeUIStatus;
226
+ $action?: Action;
227
+ $dispatch?: GenerativeUIDispatch;
228
+ };
229
+
230
+ function SelectRender({
231
+ value,
232
+ options,
233
+ placeholder,
234
+ label,
235
+ name,
236
+ defaultValue,
237
+ $action,
238
+ $dispatch,
239
+ children,
240
+ }: SelectRenderProps) {
241
+ const answeredValue = useAnsweredValue(name);
242
+ const updateBinding = useA2uiBinding(name);
243
+ const isBound = updateBinding !== undefined;
244
+ const placeholderText = toTextContent(placeholder);
245
+ const initialValue =
246
+ typeof answeredValue === "string"
247
+ ? answeredValue
248
+ : typeof defaultValue === "string"
249
+ ? defaultValue
250
+ : "";
251
+ return (
252
+ <select
253
+ key={initialValue}
254
+ data-aui="select"
255
+ data-aui-action={actionAttr($action)}
256
+ name={name}
257
+ aria-label={label}
258
+ {...(isBound
259
+ ? {
260
+ value:
261
+ typeof answeredValue === "string" ? answeredValue : (value ?? ""),
262
+ }
263
+ : { defaultValue: initialValue })}
264
+ onChange={(e) => {
265
+ updateBinding?.(e.currentTarget.value);
266
+ fire($action, $dispatch, e.currentTarget.value, e.currentTarget);
267
+ }}
268
+ >
269
+ {placeholderText ? (
270
+ <option value="" disabled>
271
+ {placeholderText}
272
+ </option>
273
+ ) : null}
274
+ {mapOptions(options, (option, key) => (
275
+ <option key={key} value={option.value}>
276
+ {option.label}
277
+ </option>
278
+ ))}
279
+ {children}
280
+ </select>
281
+ );
282
+ }
283
+
284
+ type InputRenderProps = {
285
+ value?: string;
286
+ inputType?: "text" | "password" | "number";
287
+ placeholder?: string;
288
+ multiline?: boolean;
289
+ label?: string;
290
+ name?: string;
291
+ defaultValue?: string;
292
+ $status: GenerativeUIStatus;
293
+ $action?: Action;
294
+ $dispatch?: GenerativeUIDispatch;
295
+ };
296
+
297
+ function InputRender({
298
+ value,
299
+ inputType,
300
+ placeholder,
301
+ multiline,
302
+ label,
303
+ name,
304
+ defaultValue,
305
+ $action,
306
+ $dispatch,
307
+ }: InputRenderProps) {
308
+ const answeredValue = useAnsweredValue(name);
309
+ const updateBinding = useA2uiBinding(name);
310
+ const isBound = updateBinding !== undefined;
311
+ const initialValue =
312
+ typeof answeredValue === "string" ? answeredValue : defaultValue;
313
+ const submit = (control: HTMLInputElement | HTMLTextAreaElement) =>
314
+ fire($action, $dispatch, control.value, control);
315
+ return multiline && (inputType === undefined || inputType === "text") ? (
316
+ <textarea
317
+ key={initialValue}
318
+ data-aui="input"
319
+ data-aui-multiline
320
+ data-aui-action={actionAttr($action)}
321
+ name={name}
322
+ aria-label={label}
323
+ placeholder={placeholder}
324
+ {...(isBound
325
+ ? {
326
+ value:
327
+ typeof answeredValue === "string" ? answeredValue : (value ?? ""),
328
+ }
329
+ : { defaultValue: initialValue })}
330
+ onChange={
331
+ updateBinding ? (e) => updateBinding(e.currentTarget.value) : undefined
332
+ }
333
+ onKeyDown={(e) => {
334
+ if (
335
+ e.key !== "Enter" ||
336
+ !(e.ctrlKey || e.metaKey) ||
337
+ e.nativeEvent.isComposing
338
+ )
339
+ return;
340
+ if (e.currentTarget.form) {
341
+ e.preventDefault();
342
+ HTMLFormElement.prototype.requestSubmit.call(e.currentTarget.form);
343
+ } else {
344
+ submit(e.currentTarget);
345
+ }
346
+ }}
347
+ />
348
+ ) : (
349
+ <input
350
+ key={initialValue}
351
+ type={inputType}
352
+ step={inputType === "number" ? "any" : undefined}
353
+ data-aui="input"
354
+ data-aui-action={actionAttr($action)}
355
+ name={name}
356
+ aria-label={label}
357
+ placeholder={placeholder}
358
+ {...(isBound
359
+ ? {
360
+ value:
361
+ typeof answeredValue === "string" ? answeredValue : (value ?? ""),
362
+ }
363
+ : { defaultValue: initialValue })}
364
+ onChange={
365
+ updateBinding ? (e) => updateBinding(e.currentTarget.value) : undefined
366
+ }
367
+ onKeyDown={(e) => {
368
+ if (
369
+ e.key === "Enter" &&
370
+ !e.nativeEvent.isComposing &&
371
+ !e.currentTarget.form
372
+ )
373
+ submit(e.currentTarget);
374
+ }}
375
+ />
376
+ );
377
+ }
378
+
379
+ type DatePickerRenderProps = {
380
+ inputType?: "date" | "datetime" | "time";
381
+ value?: string;
382
+ min?: string;
383
+ max?: string;
384
+ label?: string;
385
+ name?: string;
386
+ $status: GenerativeUIStatus;
387
+ $action?: Action;
388
+ $dispatch?: GenerativeUIDispatch;
389
+ };
390
+
391
+ const subscribeToHydration = () => () => {};
392
+ const clientHydrationSnapshot = () => true;
393
+ const serverHydrationSnapshot = () => false;
394
+
395
+ function DatePickerRender({
396
+ inputType = "date",
397
+ value,
398
+ min,
399
+ max,
400
+ label,
401
+ name,
402
+ $action,
403
+ $dispatch,
404
+ }: DatePickerRenderProps) {
405
+ const answeredValue = useAnsweredValue(name);
406
+ const updateBinding = useA2uiBinding(name);
407
+ const isBound = updateBinding !== undefined;
408
+ const initialValue =
409
+ typeof answeredValue === "string" ? answeredValue : value;
410
+ const [selection, setSelection] = React.useState({
411
+ initialValue,
412
+ value: initialValue,
413
+ anchor: initialValue,
414
+ });
415
+ if (selection.initialValue !== initialValue) {
416
+ setSelection({
417
+ initialValue,
418
+ value: initialValue,
419
+ anchor:
420
+ isBound && initialValue === selection.value
421
+ ? selection.anchor
422
+ : initialValue,
423
+ });
424
+ }
425
+ const currentValue = isBound ? initialValue : selection.value;
426
+ const temporal = classifyTemporal(currentValue ?? "");
427
+ const anchor = classifyTemporal(selection.anchor ?? "");
428
+ const hasInstantAnchor =
429
+ inputType === "datetime" && anchor.kind === "instant";
430
+ const canonicalFieldValue = (value: string | undefined) => {
431
+ const current = value ?? "";
432
+ const kind = classifyTemporal(current).kind;
433
+ if (inputType === "time")
434
+ return kind === "time" && normalizeTemporalInputValue(current) !== current
435
+ ? value
436
+ : undefined;
437
+ if (inputType !== "datetime") return undefined;
438
+ if (kind === "instant") return value;
439
+ if (hasInstantAnchor) return selection.anchor;
440
+ return kind === "floating" &&
441
+ normalizeTemporalInputValue(current) !== current
442
+ ? value
443
+ : undefined;
444
+ };
445
+ const minimum = classifyTemporal(min ?? "");
446
+ const maximum = classifyTemporal(max ?? "");
447
+ const hydrated = React.useSyncExternalStore(
448
+ subscribeToHydration,
449
+ clientHydrationSnapshot,
450
+ serverHydrationSnapshot,
451
+ );
452
+ const showRawValue =
453
+ inputType === "datetime" && temporal.kind === "instant" && !hydrated;
454
+ const displayValue =
455
+ currentValue === undefined
456
+ ? undefined
457
+ : inputType === "datetime" && hydrated
458
+ ? toPickerLocalDateTime(currentValue)
459
+ : normalizeTemporalInputValue(currentValue);
460
+ return (
461
+ <input
462
+ key={isBound ? undefined : initialValue}
463
+ type={
464
+ showRawValue
465
+ ? "text"
466
+ : inputType === "datetime"
467
+ ? "datetime-local"
468
+ : inputType
469
+ }
470
+ readOnly={showRawValue || undefined}
471
+ data-aui="datepicker"
472
+ data-aui-action={actionAttr($action)}
473
+ {...{
474
+ [FIELD_VALUE_ATTR]: canonicalFieldValue(currentValue),
475
+ }}
476
+ name={name}
477
+ aria-label={label}
478
+ {...(isBound || inputType === "datetime"
479
+ ? { value: displayValue ?? "" }
480
+ : { defaultValue: displayValue })}
481
+ min={
482
+ inputType === "datetime" && hydrated && min !== undefined
483
+ ? normalizeTemporalInputValue(
484
+ toLocalDateTime(
485
+ min,
486
+ minimum.kind === "instant" ? minimum.precision : undefined,
487
+ ),
488
+ )
489
+ : min === undefined
490
+ ? undefined
491
+ : normalizeTemporalInputValue(min)
492
+ }
493
+ max={
494
+ inputType === "datetime" && hydrated && max !== undefined
495
+ ? normalizeTemporalInputValue(
496
+ toLocalDateTime(
497
+ max,
498
+ maximum.kind === "instant" ? maximum.precision : undefined,
499
+ ),
500
+ )
501
+ : max === undefined
502
+ ? undefined
503
+ : normalizeTemporalInputValue(max)
504
+ }
505
+ step={
506
+ inputType !== "date"
507
+ ? getTemporalInputStep(selection.anchor, currentValue, min, max)
508
+ : undefined
509
+ }
510
+ onChange={(e) => {
511
+ const inputValue = normalizeTemporalInputValue(e.currentTarget.value);
512
+ const nextValue =
513
+ inputType === "datetime"
514
+ ? fromLocalDateTime(inputValue, selection.anchor)
515
+ : inputValue;
516
+ const canonical = canonicalFieldValue(nextValue);
517
+ if (canonical !== undefined) {
518
+ e.currentTarget.setAttribute(FIELD_VALUE_ATTR, canonical);
519
+ } else {
520
+ e.currentTarget.removeAttribute(FIELD_VALUE_ATTR);
521
+ }
522
+ setSelection({
523
+ initialValue,
524
+ value: nextValue,
525
+ anchor: selection.anchor,
526
+ });
527
+ updateBinding?.(nextValue);
528
+ fire($action, $dispatch, nextValue, e.currentTarget);
529
+ }}
530
+ />
531
+ );
532
+ }
533
+
534
+ type CheckboxRenderProps = {
535
+ checked?: boolean;
536
+ label: string;
537
+ name?: string;
538
+ defaultChecked?: boolean;
539
+ variant?: "checkbox" | "switch";
540
+ $status: GenerativeUIStatus;
541
+ $action?: Action;
542
+ $dispatch?: GenerativeUIDispatch;
543
+ };
544
+
545
+ function CheckboxRender({
546
+ checked,
547
+ label,
548
+ name,
549
+ defaultChecked,
550
+ variant = "checkbox",
551
+ $action,
552
+ $dispatch,
553
+ }: CheckboxRenderProps) {
554
+ const answeredValue = useAnsweredValue(name);
555
+ const updateBinding = useA2uiBinding(name);
556
+ const isBound = updateBinding !== undefined;
557
+ const initialChecked =
558
+ typeof answeredValue === "boolean" ? answeredValue : defaultChecked;
559
+ return (
560
+ <label
561
+ data-aui="checkbox"
562
+ data-aui-variant={variant === "switch" ? variant : undefined}
563
+ >
564
+ <input
565
+ key={String(initialChecked)}
566
+ type="checkbox"
567
+ role={variant === "switch" ? "switch" : undefined}
568
+ data-aui-action={actionAttr($action)}
569
+ name={name}
570
+ {...(isBound
571
+ ? {
572
+ checked:
573
+ typeof answeredValue === "boolean"
574
+ ? answeredValue
575
+ : checked === true,
576
+ }
577
+ : { defaultChecked: initialChecked })}
578
+ onChange={(e) => {
579
+ updateBinding?.(e.currentTarget.checked);
580
+ fire($action, $dispatch, e.currentTarget.checked, e.currentTarget);
581
+ }}
582
+ />
583
+ <span data-aui="checkbox-label">{toTextContent(label)}</span>
584
+ </label>
585
+ );
586
+ }
587
+
588
+ const finiteNumber = (value: unknown): value is number =>
589
+ typeof value === "number" && Number.isFinite(value);
590
+
591
+ const clamp = (value: number, min: number, max: number): number =>
592
+ Math.min(Math.max(value, min), max);
593
+
594
+ type SliderRenderProps = {
595
+ value?: number;
596
+ name?: string;
597
+ label?: string;
598
+ min: number;
599
+ max: number;
600
+ step?: number;
601
+ defaultValue?: number;
602
+ unit?: string;
603
+ $status: GenerativeUIStatus;
604
+ $action?: Action;
605
+ $dispatch?: GenerativeUIDispatch;
606
+ };
607
+
608
+ type SliderControlProps = {
609
+ controlledValue?: number;
610
+ name?: string;
611
+ label?: string;
612
+ min: number;
613
+ max: number;
614
+ step: number;
615
+ initialValue: number;
616
+ unit?: string;
617
+ $action?: Action;
618
+ $dispatch?: GenerativeUIDispatch;
619
+ };
620
+
621
+ function SliderControl({
622
+ controlledValue,
623
+ name,
624
+ label,
625
+ min,
626
+ max,
627
+ step,
628
+ initialValue,
629
+ unit,
630
+ $action,
631
+ $dispatch,
632
+ }: SliderControlProps) {
633
+ const updateBinding = useA2uiBinding(name);
634
+ const [localValue, setValue] = React.useState(initialValue);
635
+ const value = controlledValue ?? localValue;
636
+ const pointerActive = React.useRef(false);
637
+ const keyboardActive = React.useRef(false);
638
+ const lastCommitted = React.useRef(initialValue);
639
+ const currentValue = (input: HTMLInputElement) =>
640
+ clamp(Number(input.value), min, max);
641
+ const commit = (input: HTMLInputElement) => {
642
+ if (controlledValue !== undefined) return;
643
+ const nextValue = currentValue(input);
644
+ setValue(nextValue);
645
+ if (lastCommitted.current === nextValue) return;
646
+ lastCommitted.current = nextValue;
647
+ fire($action, $dispatch, nextValue, input);
648
+ };
649
+
650
+ return (
651
+ <label data-aui="slider-field">
652
+ {label ? <span data-aui="slider-label">{label}</span> : null}
653
+ <input
654
+ type="range"
655
+ data-aui="slider"
656
+ data-aui-action={actionAttr($action)}
657
+ name={name}
658
+ aria-label={label}
659
+ aria-valuetext={unit ? `${value} ${unit}` : String(value)}
660
+ min={min}
661
+ max={max}
662
+ step={step}
663
+ {...(controlledValue !== undefined
664
+ ? { value }
665
+ : { defaultValue: initialValue })}
666
+ onInput={(e) => {
667
+ const nextValue = currentValue(e.currentTarget);
668
+ if (controlledValue !== undefined) updateBinding?.(nextValue);
669
+ else setValue(nextValue);
670
+ }}
671
+ onPointerDown={() => {
672
+ pointerActive.current = true;
673
+ }}
674
+ onPointerUp={(e) => {
675
+ pointerActive.current = false;
676
+ commit(e.currentTarget);
677
+ }}
678
+ onKeyDown={(e) => {
679
+ keyboardActive.current = [
680
+ "ArrowDown",
681
+ "ArrowLeft",
682
+ "ArrowRight",
683
+ "ArrowUp",
684
+ "End",
685
+ "Home",
686
+ "PageDown",
687
+ "PageUp",
688
+ ].includes(e.key);
689
+ }}
690
+ onKeyUp={(e) => {
691
+ if (!keyboardActive.current) return;
692
+ keyboardActive.current = false;
693
+ commit(e.currentTarget);
694
+ }}
695
+ onChange={(e) => {
696
+ if (!pointerActive.current && !keyboardActive.current)
697
+ commit(e.currentTarget);
698
+ }}
699
+ />
700
+ <output data-aui="slider-value">
701
+ {value}
702
+ {unit ? ` ${unit}` : ""}
703
+ </output>
704
+ </label>
705
+ );
706
+ }
707
+
708
+ function SliderRender({
709
+ value,
710
+ name,
711
+ label,
712
+ min,
713
+ max,
714
+ step,
715
+ defaultValue,
716
+ unit,
717
+ $action,
718
+ $dispatch,
719
+ }: SliderRenderProps) {
720
+ const answeredValue = useAnsweredValue(name);
721
+ const updateBinding = useA2uiBinding(name);
722
+ const isBound = updateBinding !== undefined;
723
+ const safeMin = finiteNumber(min) ? min : 0;
724
+ const safeMax = finiteNumber(max) ? Math.max(max, safeMin) : safeMin + 100;
725
+ const safeStep = finiteNumber(step) && step > 0 ? step : 1;
726
+ const defaultNumber = finiteNumber(isBound ? value : defaultValue)
727
+ ? isBound
728
+ ? value!
729
+ : defaultValue!
730
+ : safeMin;
731
+ const initialValue = clamp(
732
+ finiteNumber(answeredValue) ? answeredValue : defaultNumber,
733
+ safeMin,
734
+ safeMax,
735
+ );
736
+ return (
737
+ <SliderControl
738
+ key={`${safeMin}:${safeMax}:${safeStep}:${isBound ? "bound" : initialValue}`}
739
+ {...(isBound ? { controlledValue: initialValue } : {})}
740
+ min={safeMin}
741
+ max={safeMax}
742
+ step={safeStep}
743
+ initialValue={initialValue}
744
+ {...(name !== undefined ? { name } : {})}
745
+ {...(label !== undefined ? { label } : {})}
746
+ {...(unit !== undefined ? { unit } : {})}
747
+ {...($action !== undefined ? { $action } : {})}
748
+ {...($dispatch !== undefined ? { $dispatch } : {})}
749
+ />
750
+ );
751
+ }
752
+
753
+ const UNDO_WINDOW_SECONDS = 5;
754
+
755
+ type ButtonRenderProps = {
756
+ label: string;
757
+ buttonStyle?: (typeof BUTTON_STYLES)[number];
758
+ block?: boolean;
759
+ submit?: boolean;
760
+ undoable?: boolean;
761
+ children?: React.ReactNode;
762
+ $status: GenerativeUIStatus;
763
+ $action?: Action;
764
+ $dispatch?: GenerativeUIDispatch;
765
+ };
766
+
767
+ function ButtonRender({
768
+ label,
769
+ buttonStyle,
770
+ block,
771
+ submit,
772
+ undoable,
773
+ children,
774
+ $action,
775
+ $dispatch,
776
+ }: ButtonRenderProps) {
777
+ const bindings = React.useContext(A2uiBindingContext!)?.fields;
778
+ const bindingsRef = React.useRef(bindings);
779
+ bindingsRef.current = bindings;
780
+ const [remaining, setRemaining] = React.useState<number | undefined>(
781
+ undefined,
782
+ );
783
+ const labelText = String(toTextContent(label) ?? "");
784
+ const undoLabel = labelText.trim();
785
+ const pendingAction = React.useRef<
786
+ | {
787
+ $action: Action;
788
+ $dispatch: GenerativeUIDispatch;
789
+ source: HTMLButtonElement;
790
+ }
791
+ | undefined
792
+ >(undefined);
793
+ const canUndo =
794
+ undoable && !submit && $action !== undefined && $dispatch !== undefined;
795
+
796
+ React.useEffect(() => {
797
+ if (remaining === undefined) return;
798
+ const timer = setTimeout(() => {
799
+ if (remaining > 1) {
800
+ setRemaining(remaining - 1);
801
+ return;
802
+ }
803
+ const action = pendingAction.current;
804
+ pendingAction.current = undefined;
805
+ setRemaining(undefined);
806
+ if (action)
807
+ fire(
808
+ action.$action,
809
+ action.$dispatch,
810
+ undefined,
811
+ action.source,
812
+ bindingsRef.current,
813
+ );
814
+ }, 1_000);
815
+ return () => clearTimeout(timer);
816
+ }, [remaining]);
817
+
818
+ const cancelUndo = () => {
819
+ pendingAction.current = undefined;
820
+ setRemaining(undefined);
821
+ };
822
+
823
+ return (
824
+ <button
825
+ type={submit ? "submit" : "button"}
826
+ data-aui="button"
827
+ data-aui-style={buttonStyle}
828
+ data-aui-block={block || undefined}
829
+ data-aui-submit={submit || undefined}
830
+ data-aui-state={remaining === undefined ? undefined : "pending"}
831
+ data-aui-action={actionAttr($action)}
832
+ aria-label={
833
+ remaining === undefined
834
+ ? undefined
835
+ : undoLabel
836
+ ? `Undo ${undoLabel}`
837
+ : "Undo"
838
+ }
839
+ onClick={
840
+ submit
841
+ ? undefined
842
+ : canUndo
843
+ ? (e) => {
844
+ if (remaining !== undefined) {
845
+ cancelUndo();
846
+ return;
847
+ }
848
+ pendingAction.current = {
849
+ $action,
850
+ $dispatch,
851
+ source: e.currentTarget,
852
+ };
853
+ setRemaining(UNDO_WINDOW_SECONDS);
854
+ }
855
+ : (e) =>
856
+ fire($action, $dispatch, undefined, e.currentTarget, bindings)
857
+ }
858
+ onKeyDown={
859
+ canUndo
860
+ ? (e) => {
861
+ if (e.key !== "Escape" || remaining === undefined) return;
862
+ e.preventDefault();
863
+ cancelUndo();
864
+ }
865
+ : undefined
866
+ }
867
+ >
868
+ {remaining === undefined ? (
869
+ <>
870
+ {labelText}
871
+ {children}
872
+ </>
873
+ ) : (
874
+ <>
875
+ Undo <span data-aui="button-countdown">{remaining}</span>
876
+ <span data-aui="button-undo-status" role="status">
877
+ {labelText} in {UNDO_WINDOW_SECONDS} seconds
878
+ </span>
879
+ </>
880
+ )}
881
+ </button>
882
+ );
883
+ }
884
+
78
885
  export const interactiveVocabulary = {
79
886
  Button: {
80
887
  description:
@@ -92,29 +899,14 @@ export const interactiveVocabulary = {
92
899
  .describe(
93
900
  "Render as a submit button for an ancestor Form/Card instead of a click button.",
94
901
  ),
902
+ undoable: z
903
+ .boolean()
904
+ .optional()
905
+ .describe(
906
+ "Delay the action with an undo control, for sends and other hard-to-reverse actions.",
907
+ ),
95
908
  }),
96
- render: ({
97
- label,
98
- buttonStyle,
99
- block,
100
- submit,
101
- $action,
102
- $dispatch,
103
- children,
104
- }) => (
105
- <button
106
- type={submit ? "submit" : "button"}
107
- data-aui="button"
108
- data-aui-style={buttonStyle}
109
- data-aui-block={block || undefined}
110
- data-aui-submit={submit || undefined}
111
- data-aui-action={actionAttr($action)}
112
- onClick={submit ? undefined : () => fire($action, $dispatch)}
113
- >
114
- {toTextContent(label)}
115
- {children}
116
- </button>
117
- ),
909
+ render: ButtonRender,
118
910
  },
119
911
  Select: {
120
912
  description:
@@ -130,130 +922,69 @@ export const interactiveVocabulary = {
130
922
  .optional()
131
923
  .describe("Accessible label for the control."),
132
924
  name: z.string().optional().describe("Field name used inside a Form."),
925
+ defaultValue: z.string().optional().describe("Initially selected value."),
133
926
  }),
134
- render: ({
135
- options,
136
- placeholder,
137
- label,
138
- name,
139
- $action,
140
- $dispatch,
141
- children,
142
- }) => {
143
- const placeholderText = toTextContent(placeholder);
144
- return (
145
- <select
146
- data-aui="select"
147
- data-aui-action={actionAttr($action)}
148
- name={name}
149
- aria-label={label}
150
- defaultValue=""
151
- onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
152
- >
153
- {placeholderText ? (
154
- <option value="" disabled>
155
- {placeholderText}
156
- </option>
157
- ) : null}
158
- {(Array.isArray(options) ? options : []).map((option, i) =>
159
- isOption(option) ? (
160
- <option key={i} value={option.value}>
161
- {option.label}
162
- </option>
163
- ) : null,
164
- )}
165
- {children}
166
- </select>
167
- );
168
- },
927
+ render: SelectRender,
169
928
  },
170
929
  Input: {
171
930
  description:
172
931
  "A text input. Carries `$action` describing the on-submit behavior.",
173
932
  properties: z.object({
933
+ inputType: z
934
+ .enum(["text", "password", "number"])
935
+ .optional()
936
+ .describe("Single-line input type. Values remain strings."),
174
937
  placeholder: z.string().optional().describe("Placeholder text."),
175
938
  multiline: z
176
939
  .boolean()
177
940
  .optional()
178
- .describe("Render a textarea instead of a single-line input."),
941
+ .describe(
942
+ 'Render a textarea instead of a single-line input; ignored when `inputType` is `"password"` or `"number"`.',
943
+ ),
179
944
  label: z
180
945
  .string()
181
946
  .optional()
182
947
  .describe("Accessible label for the control."),
183
948
  name: z.string().optional().describe("Field name used inside a Form."),
949
+ defaultValue: z.string().optional().describe("Initial text."),
184
950
  }),
185
- render: ({ placeholder, multiline, label, name, $action, $dispatch }) => {
186
- const submit = (v: string) => fire($action, $dispatch, v);
187
- return multiline ? (
188
- <textarea
189
- data-aui="input"
190
- data-aui-multiline
191
- data-aui-action={actionAttr($action)}
192
- name={name}
193
- aria-label={label}
194
- placeholder={placeholder}
195
- onKeyDown={(e) => {
196
- if (
197
- e.key !== "Enter" ||
198
- !(e.ctrlKey || e.metaKey) ||
199
- e.nativeEvent.isComposing
200
- )
201
- return;
202
- if (e.currentTarget.form) {
203
- e.preventDefault();
204
- HTMLFormElement.prototype.requestSubmit.call(
205
- e.currentTarget.form,
206
- );
207
- } else {
208
- submit(e.currentTarget.value);
209
- }
210
- }}
211
- />
212
- ) : (
213
- <input
214
- data-aui="input"
215
- data-aui-action={actionAttr($action)}
216
- name={name}
217
- aria-label={label}
218
- placeholder={placeholder}
219
- onKeyDown={(e) => {
220
- if (
221
- e.key === "Enter" &&
222
- !e.nativeEvent.isComposing &&
223
- !e.currentTarget.form
224
- )
225
- submit(e.currentTarget.value);
226
- }}
227
- />
228
- );
229
- },
951
+ render: InputRender,
230
952
  },
231
953
  DatePicker: {
232
954
  description:
233
- "A date input. Carries `$action` describing the on-select behavior.",
955
+ "A date, datetime, or time input. Carries `$action` describing the on-select behavior.",
234
956
  properties: z.object({
235
- value: z.string().optional().describe("Initial date (YYYY-MM-DD)."),
236
- min: z.string().optional().describe("Minimum date (YYYY-MM-DD)."),
237
- max: z.string().optional().describe("Maximum date (YYYY-MM-DD)."),
957
+ inputType: z
958
+ .enum(["date", "datetime", "time"])
959
+ .optional()
960
+ .describe(
961
+ 'Temporal input type. Defaults to "date". Values remain strings.',
962
+ ),
963
+ value: z
964
+ .string()
965
+ .optional()
966
+ .describe(
967
+ "Initial value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. A datetime with Z or ±HH:mm is an instant, displayed in the viewer's time zone and submitted with the same offset and precision. A datetime without an offset is local and submitted unchanged. An empty datetime submits with the viewer's offset and seconds.",
968
+ ),
969
+ min: z
970
+ .string()
971
+ .optional()
972
+ .describe(
973
+ "Minimum value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. Datetimes with Z or ±HH:mm are converted to the viewer's time zone.",
974
+ ),
975
+ max: z
976
+ .string()
977
+ .optional()
978
+ .describe(
979
+ "Maximum value: YYYY-MM-DD for date, HH:mm or HH:mm:ss with optional fraction of at most 9 digits for time, YYYY-MM-DDTHH:mm with optional :ss and fraction of at most 9 digits for datetime. Datetimes with Z or ±HH:mm are converted to the viewer's time zone.",
980
+ ),
238
981
  label: z
239
982
  .string()
240
983
  .optional()
241
984
  .describe("Accessible label for the control."),
242
985
  name: z.string().optional().describe("Field name used inside a Form."),
243
986
  }),
244
- render: ({ value, min, max, label, name, $action, $dispatch }) => (
245
- <input
246
- type="date"
247
- data-aui="datepicker"
248
- data-aui-action={actionAttr($action)}
249
- name={name}
250
- aria-label={label}
251
- defaultValue={value}
252
- min={min}
253
- max={max}
254
- onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
255
- />
256
- ),
987
+ render: DatePickerRender,
257
988
  },
258
989
  Checkbox: {
259
990
  description:
@@ -265,29 +996,53 @@ export const interactiveVocabulary = {
265
996
  .boolean()
266
997
  .optional()
267
998
  .describe("Whether the checkbox starts checked."),
999
+ variant: z
1000
+ .enum(["checkbox", "switch"])
1001
+ .optional()
1002
+ .describe("Whether to render a checkbox or switch."),
1003
+ }),
1004
+ render: CheckboxRender,
1005
+ },
1006
+ Slider: {
1007
+ description:
1008
+ "A numeric range control. Carries `$action` describing the committed value.",
1009
+ properties: z.object({
1010
+ name: z.string().optional().describe("Field name used inside a Form."),
1011
+ label: z
1012
+ .string()
1013
+ .optional()
1014
+ .describe("Accessible label for the control."),
1015
+ min: z.number().describe("Minimum value."),
1016
+ max: z.number().describe("Maximum value."),
1017
+ step: z.number().optional().describe("Value increment. Defaults to 1."),
1018
+ defaultValue: z.number().optional().describe("Initial value."),
1019
+ unit: z.string().optional().describe("Unit shown with the value."),
268
1020
  }),
269
- render: ({ label, name, defaultChecked, $action, $dispatch }) => (
270
- <label data-aui="checkbox">
271
- <input
272
- type="checkbox"
273
- data-aui-action={actionAttr($action)}
274
- name={name}
275
- defaultChecked={defaultChecked}
276
- onChange={(e) => fire($action, $dispatch, e.currentTarget.checked)}
277
- />
278
- <span data-aui="checkbox-label">{toTextContent(label)}</span>
279
- </label>
280
- ),
1021
+ render: SliderRender,
281
1022
  },
282
1023
  RadioGroup: {
283
1024
  description:
284
1025
  "A group of mutually exclusive radio options. Carries `$action` describing the on-change behavior.",
285
1026
  properties: z.object({
286
- options: z.array(optionSchema).describe("Selectable options."),
1027
+ options: z.array(describedOptionSchema).describe("Selectable options."),
287
1028
  name: z.string().optional().describe("Field name used inside a Form."),
288
1029
  label: z.string().optional().describe("Accessible name for the group."),
289
1030
  defaultValue: z.string().optional().describe("Initially selected value."),
290
1031
  }),
291
1032
  render: RadioGroupRender,
292
1033
  },
1034
+ CheckboxGroup: {
1035
+ description:
1036
+ "A group of checkbox options where any number can be checked. Carries `$action` describing the on-change behavior.",
1037
+ properties: z.object({
1038
+ options: z.array(describedOptionSchema).describe("Selectable options."),
1039
+ name: z.string().optional().describe("Field name used inside a Form."),
1040
+ label: z.string().optional().describe("Accessible name for the group."),
1041
+ defaultValue: z
1042
+ .array(z.string())
1043
+ .optional()
1044
+ .describe("Initially checked values."),
1045
+ }),
1046
+ render: CheckboxGroupRender,
1047
+ },
293
1048
  } satisfies GenerativeUILibrary;