@assistant-ui/react-generative-ui 0.0.6 → 0.0.8

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 (201) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +4 -2
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +9 -2
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  7. package/dist/JSONGenerativeUI.shared.d.ts +8 -1
  8. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  9. package/dist/JSONGenerativeUI.shared.js.map +1 -1
  10. package/dist/actionRegistry.d.ts +32 -0
  11. package/dist/actionRegistry.d.ts.map +1 -0
  12. package/dist/actionRegistry.js +41 -0
  13. package/dist/actionRegistry.js.map +1 -0
  14. package/dist/buildPresentParameters.d.ts +5 -5
  15. package/dist/buildPresentParameters.d.ts.map +1 -1
  16. package/dist/buildPresentParameters.js +20 -6
  17. package/dist/buildPresentParameters.js.map +1 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/defineGenerativeComponents.d.ts +0 -1
  20. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  21. package/dist/generativeUIToJSX.d.ts +15 -10
  22. package/dist/generativeUIToJSX.d.ts.map +1 -1
  23. package/dist/generativeUIToJSX.js +47 -17
  24. package/dist/generativeUIToJSX.js.map +1 -1
  25. package/dist/index.d.ts +8 -4
  26. package/dist/index.js +5 -2
  27. package/dist/index.server.d.ts +13 -0
  28. package/dist/index.server.js +10 -0
  29. package/dist/index.shared.d.ts +11 -0
  30. package/dist/index.shared.js +9 -0
  31. package/dist/ir.d.ts +115 -0
  32. package/dist/ir.d.ts.map +1 -0
  33. package/dist/ir.js +175 -0
  34. package/dist/ir.js.map +1 -0
  35. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  36. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  37. package/dist/renderGenerativeUI.d.ts +5 -5
  38. package/dist/renderGenerativeUI.d.ts.map +1 -1
  39. package/dist/renderGenerativeUI.js +18 -45
  40. package/dist/renderGenerativeUI.js.map +1 -1
  41. package/dist/slack/constants.d.ts +111 -0
  42. package/dist/slack/constants.d.ts.map +1 -0
  43. package/dist/slack/constants.js +168 -0
  44. package/dist/slack/constants.js.map +1 -0
  45. package/dist/slack/decodeBlockAction.d.ts +11 -0
  46. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  47. package/dist/slack/decodeBlockAction.js +40 -0
  48. package/dist/slack/decodeBlockAction.js.map +1 -0
  49. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  50. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  51. package/dist/slack/fromSlackBlocks.js +447 -0
  52. package/dist/slack/fromSlackBlocks.js.map +1 -0
  53. package/dist/slack/toSlackBlocks.d.ts +7 -0
  54. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  55. package/dist/slack/toSlackBlocks.js +649 -0
  56. package/dist/slack/toSlackBlocks.js.map +1 -0
  57. package/dist/slack/types.d.ts +178 -0
  58. package/dist/slack/types.d.ts.map +1 -0
  59. package/dist/slack/types.js +0 -0
  60. package/dist/slack.d.ts +5 -0
  61. package/dist/slack.js +4 -0
  62. package/dist/teams/boundSpec.d.ts +20 -0
  63. package/dist/teams/boundSpec.d.ts.map +1 -0
  64. package/dist/teams/boundSpec.js +67 -0
  65. package/dist/teams/boundSpec.js.map +1 -0
  66. package/dist/teams/constants.d.ts +57 -0
  67. package/dist/teams/constants.d.ts.map +1 -0
  68. package/dist/teams/constants.js +92 -0
  69. package/dist/teams/constants.js.map +1 -0
  70. package/dist/teams/decodeSubmitData.d.ts +25 -0
  71. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  72. package/dist/teams/decodeSubmitData.js +44 -0
  73. package/dist/teams/decodeSubmitData.js.map +1 -0
  74. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  75. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  76. package/dist/teams/toAdaptiveCard.js +496 -0
  77. package/dist/teams/toAdaptiveCard.js.map +1 -0
  78. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  79. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  80. package/dist/teams/toTeamsAttachments.js +95 -0
  81. package/dist/teams/toTeamsAttachments.js.map +1 -0
  82. package/dist/teams/types.d.ts +196 -0
  83. package/dist/teams/types.d.ts.map +1 -0
  84. package/dist/teams/types.js +0 -0
  85. package/dist/teams.d.ts +5 -0
  86. package/dist/teams.js +4 -0
  87. package/dist/types.d.ts +41 -34
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vocabulary/alert.d.ts +28 -0
  90. package/dist/vocabulary/alert.d.ts.map +1 -0
  91. package/dist/vocabulary/alert.js +58 -0
  92. package/dist/vocabulary/alert.js.map +1 -0
  93. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  94. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  95. package/dist/vocabulary/collectFormValues.js +32 -0
  96. package/dist/vocabulary/collectFormValues.js.map +1 -0
  97. package/dist/vocabulary/data.d.ts +52 -0
  98. package/dist/vocabulary/data.d.ts.map +1 -0
  99. package/dist/vocabulary/data.js +294 -0
  100. package/dist/vocabulary/data.js.map +1 -0
  101. package/dist/vocabulary/dispatch.d.ts +11 -0
  102. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  103. package/dist/vocabulary/dispatch.js +21 -0
  104. package/dist/vocabulary/dispatch.js.map +1 -0
  105. package/dist/vocabulary/fact.d.ts +15 -0
  106. package/dist/vocabulary/fact.d.ts.map +1 -0
  107. package/dist/vocabulary/fact.js +24 -0
  108. package/dist/vocabulary/fact.js.map +1 -0
  109. package/dist/vocabulary/form.d.ts +14 -0
  110. package/dist/vocabulary/form.d.ts.map +1 -0
  111. package/dist/vocabulary/form.js +23 -0
  112. package/dist/vocabulary/form.js.map +1 -0
  113. package/dist/vocabulary/icon.d.ts +44 -0
  114. package/dist/vocabulary/icon.d.ts.map +1 -0
  115. package/dist/vocabulary/icon.js +272 -0
  116. package/dist/vocabulary/icon.js.map +1 -0
  117. package/dist/vocabulary/index.d.ts +15 -0
  118. package/dist/vocabulary/index.d.ts.map +1 -0
  119. package/dist/vocabulary/index.js +36 -0
  120. package/dist/vocabulary/index.js.map +1 -0
  121. package/dist/vocabulary/interactive.d.ts +93 -0
  122. package/dist/vocabulary/interactive.d.ts.map +1 -0
  123. package/dist/vocabulary/interactive.js +172 -0
  124. package/dist/vocabulary/interactive.js.map +1 -0
  125. package/dist/vocabulary/layout.d.ts +82 -0
  126. package/dist/vocabulary/layout.d.ts.map +1 -0
  127. package/dist/vocabulary/layout.js +136 -0
  128. package/dist/vocabulary/layout.js.map +1 -0
  129. package/dist/vocabulary/list.d.ts +17 -0
  130. package/dist/vocabulary/list.d.ts.map +1 -0
  131. package/dist/vocabulary/list.js +48 -0
  132. package/dist/vocabulary/list.js.map +1 -0
  133. package/dist/vocabulary/media.d.ts +28 -0
  134. package/dist/vocabulary/media.d.ts.map +1 -0
  135. package/dist/vocabulary/media.js +42 -0
  136. package/dist/vocabulary/media.js.map +1 -0
  137. package/dist/vocabulary/text.d.ts +60 -0
  138. package/dist/vocabulary/text.d.ts.map +1 -0
  139. package/dist/vocabulary/text.js +48 -0
  140. package/dist/vocabulary/text.js.map +1 -0
  141. package/package.json +21 -13
  142. package/src/JSONGenerativeUI.client.tsx +10 -2
  143. package/src/JSONGenerativeUI.shared.ts +8 -0
  144. package/src/actionRegistry.test.ts +90 -0
  145. package/src/actionRegistry.ts +88 -0
  146. package/src/buildPresentParameters.ts +38 -16
  147. package/src/generativeUIToJSX.test.ts +186 -0
  148. package/src/generativeUIToJSX.ts +112 -21
  149. package/src/index.server.ts +2 -0
  150. package/src/index.shared.ts +61 -0
  151. package/src/index.ts +2 -26
  152. package/src/ir.test.ts +203 -0
  153. package/src/ir.ts +279 -0
  154. package/src/renderGenerativeUI.test.tsx +69 -2
  155. package/src/renderGenerativeUI.tsx +49 -82
  156. package/src/slack/constants.ts +217 -0
  157. package/src/slack/decodeBlockAction.test.ts +135 -0
  158. package/src/slack/decodeBlockAction.ts +66 -0
  159. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  160. package/src/slack/fromSlackBlocks.ts +709 -0
  161. package/src/slack/toSlackBlocks.test.ts +1936 -0
  162. package/src/slack/toSlackBlocks.ts +1319 -0
  163. package/src/slack/types.ts +230 -0
  164. package/src/slack.ts +37 -0
  165. package/src/teams/boundSpec.ts +102 -0
  166. package/src/teams/constants.ts +127 -0
  167. package/src/teams/decodeSubmitData.test.ts +148 -0
  168. package/src/teams/decodeSubmitData.ts +57 -0
  169. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  170. package/src/teams/toAdaptiveCard.ts +762 -0
  171. package/src/teams/toTeamsAttachments.test.ts +29 -0
  172. package/src/teams/toTeamsAttachments.ts +137 -0
  173. package/src/teams/types.ts +237 -0
  174. package/src/teams.ts +34 -0
  175. package/src/types.ts +57 -42
  176. package/src/vocabulary/alert.test.tsx +136 -0
  177. package/src/vocabulary/alert.tsx +66 -0
  178. package/src/vocabulary/collectFormValues.test.ts +127 -0
  179. package/src/vocabulary/collectFormValues.ts +58 -0
  180. package/src/vocabulary/data.test.tsx +567 -0
  181. package/src/vocabulary/data.tsx +494 -0
  182. package/src/vocabulary/dispatch.ts +22 -0
  183. package/src/vocabulary/fact.test.tsx +15 -0
  184. package/src/vocabulary/fact.tsx +22 -0
  185. package/src/vocabulary/form.test.tsx +112 -0
  186. package/src/vocabulary/form.tsx +34 -0
  187. package/src/vocabulary/icon.test.tsx +64 -0
  188. package/src/vocabulary/icon.tsx +174 -0
  189. package/src/vocabulary/index.test.ts +63 -0
  190. package/src/vocabulary/index.ts +33 -0
  191. package/src/vocabulary/interactive.dispatch.test.tsx +352 -0
  192. package/src/vocabulary/interactive.test.tsx +248 -0
  193. package/src/vocabulary/interactive.tsx +269 -0
  194. package/src/vocabulary/layout.test.tsx +289 -0
  195. package/src/vocabulary/layout.tsx +229 -0
  196. package/src/vocabulary/list.test.tsx +195 -0
  197. package/src/vocabulary/list.tsx +48 -0
  198. package/src/vocabulary/media.test.tsx +85 -0
  199. package/src/vocabulary/media.tsx +56 -0
  200. package/src/vocabulary/text.test.tsx +73 -0
  201. package/src/vocabulary/text.tsx +59 -0
@@ -0,0 +1,248 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { renderGenerativeUI } from "../renderGenerativeUI";
4
+ import { interactiveVocabulary } from "./interactive";
5
+
6
+ const render = (node: unknown) =>
7
+ renderToStaticMarkup(<>{renderGenerativeUI(node, interactiveVocabulary)}</>);
8
+
9
+ describe("interactiveVocabulary", () => {
10
+ it("Button renders a button with style/block hooks and the label", () => {
11
+ expect(
12
+ render({
13
+ $type: "Button",
14
+ label: "Go",
15
+ buttonStyle: "primary",
16
+ block: true,
17
+ }),
18
+ ).toBe(
19
+ '<button type="button" data-aui="button" data-aui-style="primary" data-aui-block="true">Go</button>',
20
+ );
21
+ });
22
+
23
+ it("Button omits data-aui-block when block is false or omitted", () => {
24
+ expect(render({ $type: "Button", label: "Go", block: false })).toBe(
25
+ '<button type="button" data-aui="button">Go</button>',
26
+ );
27
+ });
28
+
29
+ it('Button renders type="button" by default and type="submit" with the submit prop', () => {
30
+ expect(render({ $type: "Button", label: "Go" })).toContain('type="button"');
31
+ const html = render({ $type: "Button", label: "Go", submit: true });
32
+ expect(html).toContain('type="submit"');
33
+ expect(html).toContain("data-aui-submit");
34
+ });
35
+
36
+ it("Button omits data-aui-submit when submit is false or omitted", () => {
37
+ expect(
38
+ render({ $type: "Button", label: "Go", submit: false }),
39
+ ).not.toContain("data-aui-submit");
40
+ });
41
+
42
+ it("Select renders an aria-label when label is provided", () => {
43
+ const html = render({
44
+ $type: "Select",
45
+ label: "Choose",
46
+ options: [{ label: "A", value: "a" }],
47
+ });
48
+ expect(html).toContain('aria-label="Choose"');
49
+ });
50
+
51
+ it("Select keys options by index so duplicate values do not collide", () => {
52
+ const html = render({
53
+ $type: "Select",
54
+ options: [
55
+ { label: "A", value: "x" },
56
+ { label: "B", value: "x" },
57
+ ],
58
+ });
59
+ expect(html).toContain('<option value="x">A</option>');
60
+ expect(html).toContain('<option value="x">B</option>');
61
+ });
62
+
63
+ it("Select renders a name attribute when provided", () => {
64
+ const html = render({
65
+ $type: "Select",
66
+ name: "role",
67
+ options: [{ label: "A", value: "a" }],
68
+ });
69
+ expect(html).toContain('name="role"');
70
+ });
71
+
72
+ it("Button stashes $action on a data-aui-action attribute", () => {
73
+ const html = render({
74
+ $type: "Button",
75
+ label: "Buy",
76
+ $action: { type: "purchase", itemId: "sku-1" },
77
+ });
78
+ expect(html).toContain('data-aui-action="');
79
+ expect(html).toContain("&quot;type&quot;:&quot;purchase&quot;");
80
+ expect(html).toContain("&quot;itemId&quot;:&quot;sku-1&quot;");
81
+ });
82
+
83
+ it("Button without $action omits the data-aui-action attribute", () => {
84
+ expect(render({ $type: "Button", label: "Go" })).toBe(
85
+ '<button type="button" data-aui="button">Go</button>',
86
+ );
87
+ });
88
+
89
+ it("Select renders options with values and a placeholder", () => {
90
+ const html = render({
91
+ $type: "Select",
92
+ placeholder: "Pick one",
93
+ options: [
94
+ { label: "A", value: "a" },
95
+ { label: "B", value: "b" },
96
+ ],
97
+ });
98
+ expect(html).toContain('data-aui="select"');
99
+ expect(html).toContain(
100
+ 'value="" disabled="" selected="">Pick one</option>',
101
+ );
102
+ expect(html).toContain('<option value="a">A</option>');
103
+ expect(html).toContain('<option value="b">B</option>');
104
+ });
105
+
106
+ it("Input renders a single-line input by default", () => {
107
+ expect(render({ $type: "Input", placeholder: "type here" })).toBe(
108
+ '<input data-aui="input" placeholder="type here"/>',
109
+ );
110
+ });
111
+
112
+ it("Input renders a name attribute when provided", () => {
113
+ expect(render({ $type: "Input", name: "email" })).toContain('name="email"');
114
+ });
115
+
116
+ it("Input multiline renders a textarea with the multiline hook", () => {
117
+ const html = render({ $type: "Input", multiline: true });
118
+ expect(html).toContain("data-aui-multiline");
119
+ expect(html).toContain("<textarea");
120
+ });
121
+
122
+ it("DatePicker renders a date input with min/max", () => {
123
+ const html = render({
124
+ $type: "DatePicker",
125
+ value: "2026-01-01",
126
+ min: "2025-01-01",
127
+ max: "2027-01-01",
128
+ });
129
+ expect(html).toContain('type="date"');
130
+ expect(html).toContain('value="2026-01-01"');
131
+ expect(html).toContain('min="2025-01-01"');
132
+ expect(html).toContain('max="2027-01-01"');
133
+ });
134
+
135
+ it("DatePicker renders a name attribute when provided", () => {
136
+ expect(render({ $type: "DatePicker", name: "dob" })).toContain(
137
+ 'name="dob"',
138
+ );
139
+ });
140
+
141
+ it("Checkbox renders a label wrapping the input and a checkbox-label span", () => {
142
+ const html = render({ $type: "Checkbox", label: "Accept terms" });
143
+ expect(html).toContain('data-aui="checkbox"');
144
+ expect(html).toContain('type="checkbox"');
145
+ expect(html).toContain('data-aui="checkbox-label"');
146
+ expect(html).toContain("Accept terms");
147
+ });
148
+
149
+ it("Checkbox renders name and defaultChecked when provided", () => {
150
+ const html = render({
151
+ $type: "Checkbox",
152
+ label: "Accept",
153
+ name: "agree",
154
+ defaultChecked: true,
155
+ });
156
+ expect(html).toContain('name="agree"');
157
+ expect(html).toContain('checked=""');
158
+ });
159
+
160
+ it("RadioGroup renders a fieldset with one radio per option", () => {
161
+ const html = render({
162
+ $type: "RadioGroup",
163
+ options: [
164
+ { label: "Small", value: "sm" },
165
+ { label: "Large", value: "lg" },
166
+ ],
167
+ });
168
+ expect(html).toContain('data-aui="radiogroup"');
169
+ expect((html.match(/type="radio"/g) ?? []).length).toBe(2);
170
+ expect(html).toContain('data-aui="radiogroup-option"');
171
+ expect(html).toContain("Small");
172
+ expect(html).toContain("Large");
173
+ });
174
+
175
+ it("RadioGroup renders an aria-label from the label prop", () => {
176
+ const html = render({
177
+ $type: "RadioGroup",
178
+ label: "Size",
179
+ options: [{ label: "Small", value: "sm" }],
180
+ });
181
+ expect(html).toContain('aria-label="Size"');
182
+ });
183
+
184
+ it("RadioGroup radios share the given name", () => {
185
+ const html = render({
186
+ $type: "RadioGroup",
187
+ name: "size",
188
+ options: [
189
+ { label: "Small", value: "sm" },
190
+ { label: "Large", value: "lg" },
191
+ ],
192
+ });
193
+ expect((html.match(/name="size"/g) ?? []).length).toBe(2);
194
+ });
195
+
196
+ it("RadioGroup falls back to a generated shared name when name is omitted", () => {
197
+ const html = render({
198
+ $type: "RadioGroup",
199
+ options: [
200
+ { label: "Small", value: "sm" },
201
+ { label: "Large", value: "lg" },
202
+ ],
203
+ });
204
+ const names = [...html.matchAll(/name="([^"]*)"/g)].map((m) => m[1]);
205
+ expect(names.length).toBe(2);
206
+ expect(names[0]).toBe(names[1]);
207
+ expect(names[0]).toBeTruthy();
208
+ });
209
+
210
+ it("RadioGroup marks the option matching defaultValue as checked", () => {
211
+ const html = render({
212
+ $type: "RadioGroup",
213
+ defaultValue: "lg",
214
+ options: [
215
+ { label: "Small", value: "sm" },
216
+ { label: "Large", value: "lg" },
217
+ ],
218
+ });
219
+ expect(html.match(/checked=""/g)?.length).toBe(1);
220
+ });
221
+
222
+ it("RadioGroup renders nothing for malformed entries without throwing", () => {
223
+ expect(() =>
224
+ render({
225
+ $type: "RadioGroup",
226
+ options: [
227
+ { label: "Small", value: "sm" },
228
+ null,
229
+ { value: "no-label" },
230
+ { label: "no-value" },
231
+ "not-an-object",
232
+ ],
233
+ }),
234
+ ).not.toThrow();
235
+ const html = render({
236
+ $type: "RadioGroup",
237
+ options: [{ label: "Small", value: "sm" }, null],
238
+ });
239
+ expect((html.match(/type="radio"/g) ?? []).length).toBe(1);
240
+ });
241
+
242
+ it("RadioGroup with missing options renders an empty fieldset without throwing", () => {
243
+ expect(() => render({ $type: "RadioGroup" })).not.toThrow();
244
+ expect(render({ $type: "RadioGroup" })).toBe(
245
+ '<fieldset data-aui="radiogroup"></fieldset>',
246
+ );
247
+ });
248
+ });
@@ -0,0 +1,269 @@
1
+ import { useId } from "react";
2
+ import { z } from "zod";
3
+ import type { Action } from "../ir";
4
+ import { BUTTON_STYLES } from "../ir";
5
+ import type { GenerativeUIDispatch, GenerativeUILibrary } from "../types";
6
+ import { actionAttr, fire } from "./dispatch";
7
+
8
+ const optionSchema = z.object({
9
+ label: z.string(),
10
+ value: z.string(),
11
+ });
12
+
13
+ type RadioGroupRenderProps = {
14
+ options: { label: string; value: string }[];
15
+ name?: string;
16
+ label?: string;
17
+ defaultValue?: string;
18
+ $action?: Action;
19
+ $dispatch?: GenerativeUIDispatch;
20
+ };
21
+
22
+ function RadioGroupRender({
23
+ options,
24
+ name,
25
+ label,
26
+ defaultValue,
27
+ $action,
28
+ $dispatch,
29
+ }: RadioGroupRenderProps) {
30
+ const generatedName = useId();
31
+ const fieldName = name ?? generatedName;
32
+ const safeOptions = Array.isArray(options) ? options : [];
33
+ return (
34
+ <fieldset
35
+ data-aui="radiogroup"
36
+ data-aui-action={actionAttr($action)}
37
+ aria-label={label}
38
+ >
39
+ {safeOptions.map((option, i) =>
40
+ option &&
41
+ typeof option.label === "string" &&
42
+ typeof option.value === "string" ? (
43
+ <label key={i} data-aui="radiogroup-option">
44
+ <input
45
+ type="radio"
46
+ name={fieldName}
47
+ value={option.value}
48
+ defaultChecked={defaultValue === option.value}
49
+ onChange={() => fire($action, $dispatch, option.value)}
50
+ />
51
+ {option.label}
52
+ </label>
53
+ ) : null,
54
+ )}
55
+ </fieldset>
56
+ );
57
+ }
58
+
59
+ export const interactiveVocabulary = {
60
+ Button: {
61
+ description:
62
+ "A clickable button. Carries `$action` describing the side effect or resume value. Set `submit` to submit an ancestor Form/Card instead of firing `$action` on click.",
63
+ properties: z.object({
64
+ label: z.string().describe("Button label."),
65
+ buttonStyle: z.enum(BUTTON_STYLES).optional().describe("Visual style."),
66
+ block: z
67
+ .boolean()
68
+ .optional()
69
+ .describe("Whether the button spans the full width."),
70
+ submit: z
71
+ .boolean()
72
+ .optional()
73
+ .describe(
74
+ "Render as a submit button for an ancestor Form/Card instead of a click button.",
75
+ ),
76
+ }),
77
+ render: ({
78
+ label,
79
+ buttonStyle,
80
+ block,
81
+ submit,
82
+ $action,
83
+ $dispatch,
84
+ children,
85
+ }) => (
86
+ <button
87
+ type={submit ? "submit" : "button"}
88
+ data-aui="button"
89
+ data-aui-style={buttonStyle}
90
+ data-aui-block={block || undefined}
91
+ data-aui-submit={submit || undefined}
92
+ data-aui-action={actionAttr($action)}
93
+ onClick={submit ? undefined : () => fire($action, $dispatch)}
94
+ >
95
+ {label}
96
+ {children}
97
+ </button>
98
+ ),
99
+ },
100
+ Select: {
101
+ description:
102
+ "A dropdown selector. Carries `$action` describing the on-select behavior.",
103
+ properties: z.object({
104
+ options: z.array(optionSchema).describe("Selectable options."),
105
+ placeholder: z
106
+ .string()
107
+ .optional()
108
+ .describe("Placeholder shown when nothing is selected."),
109
+ label: z
110
+ .string()
111
+ .optional()
112
+ .describe("Accessible label for the control."),
113
+ name: z.string().optional().describe("Field name used inside a Form."),
114
+ }),
115
+ render: ({
116
+ options,
117
+ placeholder,
118
+ label,
119
+ name,
120
+ $action,
121
+ $dispatch,
122
+ children,
123
+ }) => (
124
+ <select
125
+ data-aui="select"
126
+ data-aui-action={actionAttr($action)}
127
+ name={name}
128
+ aria-label={label}
129
+ defaultValue=""
130
+ onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
131
+ >
132
+ {placeholder ? (
133
+ <option value="" disabled>
134
+ {placeholder}
135
+ </option>
136
+ ) : null}
137
+ {options.map((o: { label: string; value: string }, i: number) => (
138
+ <option key={i} value={o.value}>
139
+ {o.label}
140
+ </option>
141
+ ))}
142
+ {children}
143
+ </select>
144
+ ),
145
+ },
146
+ Input: {
147
+ description:
148
+ "A text input. Carries `$action` describing the on-submit behavior.",
149
+ properties: z.object({
150
+ placeholder: z.string().optional().describe("Placeholder text."),
151
+ multiline: z
152
+ .boolean()
153
+ .optional()
154
+ .describe("Render a textarea instead of a single-line input."),
155
+ label: z
156
+ .string()
157
+ .optional()
158
+ .describe("Accessible label for the control."),
159
+ name: z.string().optional().describe("Field name used inside a Form."),
160
+ }),
161
+ render: ({ placeholder, multiline, label, name, $action, $dispatch }) => {
162
+ const submit = (v: string) => fire($action, $dispatch, v);
163
+ return multiline ? (
164
+ <textarea
165
+ data-aui="input"
166
+ data-aui-multiline
167
+ data-aui-action={actionAttr($action)}
168
+ name={name}
169
+ aria-label={label}
170
+ placeholder={placeholder}
171
+ onKeyDown={(e) => {
172
+ if (
173
+ e.key !== "Enter" ||
174
+ !(e.ctrlKey || e.metaKey) ||
175
+ e.nativeEvent.isComposing
176
+ )
177
+ return;
178
+ if (e.currentTarget.form) {
179
+ e.preventDefault();
180
+ HTMLFormElement.prototype.requestSubmit.call(
181
+ e.currentTarget.form,
182
+ );
183
+ } else {
184
+ submit(e.currentTarget.value);
185
+ }
186
+ }}
187
+ />
188
+ ) : (
189
+ <input
190
+ data-aui="input"
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.nativeEvent.isComposing &&
199
+ !e.currentTarget.form
200
+ )
201
+ submit(e.currentTarget.value);
202
+ }}
203
+ />
204
+ );
205
+ },
206
+ },
207
+ DatePicker: {
208
+ description:
209
+ "A date input. Carries `$action` describing the on-select behavior.",
210
+ properties: z.object({
211
+ value: z.string().optional().describe("Initial date (YYYY-MM-DD)."),
212
+ min: z.string().optional().describe("Minimum date (YYYY-MM-DD)."),
213
+ max: z.string().optional().describe("Maximum date (YYYY-MM-DD)."),
214
+ label: z
215
+ .string()
216
+ .optional()
217
+ .describe("Accessible label for the control."),
218
+ name: z.string().optional().describe("Field name used inside a Form."),
219
+ }),
220
+ render: ({ value, min, max, label, name, $action, $dispatch }) => (
221
+ <input
222
+ type="date"
223
+ data-aui="datepicker"
224
+ data-aui-action={actionAttr($action)}
225
+ name={name}
226
+ aria-label={label}
227
+ defaultValue={value}
228
+ min={min}
229
+ max={max}
230
+ onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
231
+ />
232
+ ),
233
+ },
234
+ Checkbox: {
235
+ description:
236
+ "A checkbox with a label. Carries `$action` describing the on-change behavior.",
237
+ properties: z.object({
238
+ label: z.string().describe("Label text next to the checkbox."),
239
+ name: z.string().optional().describe("Field name used inside a Form."),
240
+ defaultChecked: z
241
+ .boolean()
242
+ .optional()
243
+ .describe("Whether the checkbox starts checked."),
244
+ }),
245
+ render: ({ label, name, defaultChecked, $action, $dispatch }) => (
246
+ <label data-aui="checkbox">
247
+ <input
248
+ type="checkbox"
249
+ data-aui-action={actionAttr($action)}
250
+ name={name}
251
+ defaultChecked={defaultChecked}
252
+ onChange={(e) => fire($action, $dispatch, e.currentTarget.checked)}
253
+ />
254
+ <span data-aui="checkbox-label">{label}</span>
255
+ </label>
256
+ ),
257
+ },
258
+ RadioGroup: {
259
+ description:
260
+ "A group of mutually exclusive radio options. Carries `$action` describing the on-change behavior.",
261
+ properties: z.object({
262
+ options: z.array(optionSchema).describe("Selectable options."),
263
+ name: z.string().optional().describe("Field name used inside a Form."),
264
+ label: z.string().optional().describe("Accessible name for the group."),
265
+ defaultValue: z.string().optional().describe("Initially selected value."),
266
+ }),
267
+ render: RadioGroupRender,
268
+ },
269
+ } satisfies GenerativeUILibrary;