@assistant-ui/react-generative-ui 0.0.17 → 0.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/JSONGenerativeUI.client.d.ts +1 -2
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +1 -2
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +8 -9
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/a2ui/convert.d.ts +1 -2
- package/dist/a2ui/convert.d.ts.map +1 -1
- package/dist/a2ui/convert.js +11 -2
- package/dist/a2ui/convert.js.map +1 -1
- package/dist/a2ui/reducer.d.ts +1 -2
- package/dist/a2ui/reducer.d.ts.map +1 -1
- package/dist/a2ui/reducer.js +63 -10
- package/dist/a2ui/reducer.js.map +1 -1
- package/dist/a2ui/types.d.ts +17 -18
- package/dist/a2ui/types.d.ts.map +1 -1
- package/dist/actionRegistry.d.ts +5 -6
- package/dist/actionRegistry.d.ts.map +1 -1
- package/dist/buildPresentParameters.d.ts +2 -3
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +4 -3
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts +2 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/convert/boundSpec.d.ts +7 -8
- package/dist/convert/boundSpec.d.ts.map +1 -1
- package/dist/convert/copyBounded.d.ts +1 -2
- package/dist/convert/copyBounded.d.ts.map +1 -1
- package/dist/convert/isElement.d.ts +1 -2
- package/dist/convert/isElement.d.ts.map +1 -1
- package/dist/convert/takeRun.d.ts +1 -2
- package/dist/convert/takeRun.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +1 -2
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +2 -3
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/ir.d.ts +28 -29
- package/dist/ir.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +1 -2
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/renderGenerativeUI.js +1 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +35 -36
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/decodeBlockAction.d.ts +1 -2
- package/dist/slack/decodeBlockAction.d.ts.map +1 -1
- package/dist/slack/fromSlackBlocks.d.ts +1 -2
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.d.ts +1 -2
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/types.d.ts +32 -33
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +14 -15
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/decodeSubmitData.d.ts +1 -2
- package/dist/teams/decodeSubmitData.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +6 -7
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts +1 -2
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/types.d.ts +29 -30
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/types.d.ts +9 -10
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +2 -3
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +21 -16
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +6 -5
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -1
- package/dist/vocabulary/collectFormValues.js +3 -2
- package/dist/vocabulary/collectFormValues.js.map +1 -1
- package/dist/vocabulary/data.d.ts +1 -2
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +20 -12
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +2 -3
- package/dist/vocabulary/dispatch.d.ts.map +1 -1
- package/dist/vocabulary/fact.d.ts +1 -2
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/fact.js +3 -2
- package/dist/vocabulary/fact.js.map +1 -1
- package/dist/vocabulary/form.d.ts +1 -2
- package/dist/vocabulary/form.d.ts.map +1 -1
- package/dist/vocabulary/icon.d.ts +1 -2
- package/dist/vocabulary/icon.d.ts.map +1 -1
- package/dist/vocabulary/index.d.ts +1 -2
- package/dist/vocabulary/index.d.ts.map +1 -1
- package/dist/vocabulary/interactive.d.ts +10 -9
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +30 -23
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts +1 -2
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +7 -5
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.d.ts +1 -2
- package/dist/vocabulary/list.d.ts.map +1 -1
- package/dist/vocabulary/media.d.ts +1 -2
- package/dist/vocabulary/media.d.ts.map +1 -1
- package/dist/vocabulary/text.d.ts +2 -3
- package/dist/vocabulary/text.d.ts.map +1 -1
- package/dist/vocabulary/text.js +4 -3
- package/dist/vocabulary/text.js.map +1 -1
- package/dist/vocabulary/toTextContent.d.ts +4 -0
- package/dist/vocabulary/toTextContent.d.ts.map +1 -0
- package/dist/vocabulary/toTextContent.js +6 -0
- package/dist/vocabulary/toTextContent.js.map +1 -0
- package/package.json +12 -10
- package/src/a2ui/convert.test.ts +36 -0
- package/src/a2ui/convert.ts +21 -2
- package/src/a2ui/reducer.test.ts +223 -0
- package/src/a2ui/reducer.ts +76 -12
- package/src/buildPresentParameters.ts +7 -4
- package/src/constants.ts +2 -0
- package/src/renderGenerativeUI.test.tsx +57 -0
- package/src/renderGenerativeUI.tsx +3 -1
- package/src/slack/fromSlackBlocks.test.ts +4 -1
- package/src/slack/toSlackBlocks.test.ts +8 -4
- package/src/teams/toAdaptiveCard.test.ts +5 -3
- package/src/vocabulary/alert.test.tsx +10 -0
- package/src/vocabulary/alert.tsx +16 -7
- package/src/vocabulary/collectFormValues.test.ts +38 -0
- package/src/vocabulary/collectFormValues.ts +11 -2
- package/src/vocabulary/data.test.tsx +40 -0
- package/src/vocabulary/data.tsx +47 -26
- package/src/vocabulary/fact.test.tsx +12 -0
- package/src/vocabulary/fact.tsx +3 -2
- package/src/vocabulary/form.test.tsx +1 -0
- package/src/vocabulary/interactive.test.tsx +55 -1
- package/src/vocabulary/interactive.tsx +50 -29
- package/src/vocabulary/layout.test.tsx +17 -0
- package/src/vocabulary/layout.tsx +8 -4
- package/src/vocabulary/list.test.tsx +5 -4
- package/src/vocabulary/text.test.tsx +29 -0
- package/src/vocabulary/text.tsx +4 -3
- package/src/vocabulary/toTextContent.ts +2 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +0 -141
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FormEvent } from "react";
|
|
2
|
+
import { GENERATED_NAME_ATTR } from "../constants";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The subset of `HTMLInputElement`/`HTMLSelectElement`/`HTMLTextAreaElement` that {@link collectFormValues} reads. A structural type rather than the DOM interfaces themselves, so a plain object can stand in for a form control in tests.
|
|
@@ -9,10 +10,12 @@ export type FormControlElementLike = {
|
|
|
9
10
|
readonly value: string;
|
|
10
11
|
readonly checked?: boolean;
|
|
11
12
|
readonly disabled: boolean;
|
|
13
|
+
readonly hasAttribute: (name: string) => boolean;
|
|
14
|
+
readonly matches?: ((selector: string) => boolean) | undefined;
|
|
12
15
|
};
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
|
-
* Collects a submitted form's named control values into a plain object, keyed by `name`, in document order. Reads each control's live DOM state rather than `FormData`, so a checkbox resolves to its `checked` boolean instead of an on/off string. A radio group resolves to its checked option's `value`, or `undefined` if none is checked. Any other repeated `name` resolves to an array of its controls' values, in document order. Controls without a `name`, or that are
|
|
18
|
+
* Collects a submitted form's named control values into a plain object, keyed by `name`, in document order. Reads each control's live DOM state rather than `FormData`, so a checkbox resolves to its `checked` boolean instead of an on/off string. A radio group resolves to its checked option's `value`, or `undefined` if none is checked. Any other repeated `name` resolves to an array of its controls' values, in document order. Controls without a `name`, that carry `data-aui-generated-name`, or that are effectively disabled, including through an ancestor disabled fieldset outside its first legend, are skipped entirely.
|
|
16
19
|
*/
|
|
17
20
|
export function collectFormValues(
|
|
18
21
|
elements: ArrayLike<FormControlElementLike>,
|
|
@@ -21,7 +24,13 @@ export function collectFormValues(
|
|
|
21
24
|
|
|
22
25
|
for (const element of Array.from(elements)) {
|
|
23
26
|
const { name, disabled } = element;
|
|
24
|
-
if (
|
|
27
|
+
if (
|
|
28
|
+
!name ||
|
|
29
|
+
disabled ||
|
|
30
|
+
element.hasAttribute(GENERATED_NAME_ATTR) ||
|
|
31
|
+
element.matches?.(":disabled")
|
|
32
|
+
)
|
|
33
|
+
continue;
|
|
25
34
|
|
|
26
35
|
if (element.type === "radio") {
|
|
27
36
|
if (element.checked) values[name] = element.value;
|
|
@@ -32,6 +32,32 @@ describe("dataVocabulary", () => {
|
|
|
32
32
|
expect(html).toContain("<td>only</td>");
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it("Table ignores malformed collections instead of throwing", () => {
|
|
36
|
+
expect(
|
|
37
|
+
render({
|
|
38
|
+
$type: "Table",
|
|
39
|
+
columns: "not-an-array",
|
|
40
|
+
rows: [["kept", null, {}, false], null],
|
|
41
|
+
}),
|
|
42
|
+
).toBe(
|
|
43
|
+
'<table data-aui="table"><tbody><tr><td>kept</td><td></td><td></td><td>false</td></tr></tbody></table>',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(render({ $type: "Table", columns: [null], rows: [null] })).toBe(
|
|
47
|
+
'<table data-aui="table"></table>',
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
render({
|
|
52
|
+
$type: "Table",
|
|
53
|
+
columns: [null, { label: "Name" }],
|
|
54
|
+
rows: "not-an-array",
|
|
55
|
+
}),
|
|
56
|
+
).toBe(
|
|
57
|
+
'<table data-aui="table"><thead><tr><th data-aui="table-col"></th><th data-aui="table-col">Name</th></tr></thead></table>',
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
35
61
|
it("Markdown renders the value in a div", () => {
|
|
36
62
|
expect(render({ $type: "Markdown", value: "# hi" })).toBe(
|
|
37
63
|
'<div data-aui="markdown"># hi</div>',
|
|
@@ -52,6 +78,20 @@ describe("dataVocabulary", () => {
|
|
|
52
78
|
).toBe('<div data-aui="markdown">partial</div>');
|
|
53
79
|
});
|
|
54
80
|
|
|
81
|
+
it("Markdown ignores malformed streaming values", () => {
|
|
82
|
+
expect(
|
|
83
|
+
renderToStaticMarkup(
|
|
84
|
+
<>
|
|
85
|
+
{renderGenerativeUI(
|
|
86
|
+
{ $type: "Markdown", value: { unexpected: true } },
|
|
87
|
+
dataVocabulary,
|
|
88
|
+
{ status: "streaming" },
|
|
89
|
+
)}
|
|
90
|
+
</>,
|
|
91
|
+
),
|
|
92
|
+
).toBe('<div data-aui="markdown"></div>');
|
|
93
|
+
});
|
|
94
|
+
|
|
55
95
|
it("Chart bar variant renders one rect per data point", () => {
|
|
56
96
|
const html = render({
|
|
57
97
|
$type: "Chart",
|
package/src/vocabulary/data.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { GenerativeUILibrary } from "../types";
|
|
3
|
+
import { toTextContent } from "./toTextContent";
|
|
3
4
|
|
|
4
5
|
const columnSchema = z.object({
|
|
5
6
|
label: z.string().describe("Column header label."),
|
|
@@ -9,6 +10,20 @@ const cellSchema = z
|
|
|
9
10
|
.union([z.string(), z.number(), z.boolean()])
|
|
10
11
|
.describe("A cell value.");
|
|
11
12
|
|
|
13
|
+
type TableColumn = { label: string };
|
|
14
|
+
type TableCell = string | number | boolean;
|
|
15
|
+
|
|
16
|
+
const isTableColumn = (column: unknown): column is TableColumn =>
|
|
17
|
+
column !== null &&
|
|
18
|
+
typeof column === "object" &&
|
|
19
|
+
"label" in column &&
|
|
20
|
+
typeof column.label === "string";
|
|
21
|
+
|
|
22
|
+
const isTableCell = (cell: unknown): cell is TableCell =>
|
|
23
|
+
typeof cell === "string" ||
|
|
24
|
+
typeof cell === "number" ||
|
|
25
|
+
typeof cell === "boolean";
|
|
26
|
+
|
|
12
27
|
const CHART_HEIGHT = 40;
|
|
13
28
|
const CHART_WIDTH = 100;
|
|
14
29
|
|
|
@@ -282,33 +297,39 @@ export const dataVocabulary = {
|
|
|
282
297
|
columns: z.array(columnSchema).optional().describe("Column definitions."),
|
|
283
298
|
rows: z.array(z.array(cellSchema)).optional().describe("Rows of cells."),
|
|
284
299
|
}),
|
|
285
|
-
render: ({ columns, rows, children }) =>
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
<tbody>
|
|
300
|
-
{rows.map((row: (string | number | boolean)[], r: number) => (
|
|
301
|
-
<tr key={r}>
|
|
302
|
-
{row.map((cell: string | number | boolean, c: number) => (
|
|
303
|
-
<td key={c}>{String(cell)}</td>
|
|
300
|
+
render: ({ columns, rows, children }) => {
|
|
301
|
+
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
302
|
+
const hasColumns = safeColumns.some(isTableColumn);
|
|
303
|
+
const safeRows = Array.isArray(rows) ? rows.filter(Array.isArray) : [];
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
<table data-aui="table">
|
|
307
|
+
{hasColumns ? (
|
|
308
|
+
<thead>
|
|
309
|
+
<tr>
|
|
310
|
+
{safeColumns.map((column, i) => (
|
|
311
|
+
<th key={i} data-aui="table-col">
|
|
312
|
+
{isTableColumn(column) ? column.label : ""}
|
|
313
|
+
</th>
|
|
304
314
|
))}
|
|
305
315
|
</tr>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
316
|
+
</thead>
|
|
317
|
+
) : null}
|
|
318
|
+
{safeRows.length ? (
|
|
319
|
+
<tbody>
|
|
320
|
+
{safeRows.map((row, r) => (
|
|
321
|
+
<tr key={r}>
|
|
322
|
+
{row.map((cell, c) => (
|
|
323
|
+
<td key={c}>{isTableCell(cell) ? String(cell) : ""}</td>
|
|
324
|
+
))}
|
|
325
|
+
</tr>
|
|
326
|
+
))}
|
|
327
|
+
</tbody>
|
|
328
|
+
) : null}
|
|
329
|
+
{children}
|
|
330
|
+
</table>
|
|
331
|
+
);
|
|
332
|
+
},
|
|
312
333
|
},
|
|
313
334
|
Markdown: {
|
|
314
335
|
description:
|
|
@@ -319,7 +340,7 @@ export const dataVocabulary = {
|
|
|
319
340
|
streamProperties: true,
|
|
320
341
|
render: ({ value, children }) => (
|
|
321
342
|
<div data-aui="markdown">
|
|
322
|
-
{value}
|
|
343
|
+
{toTextContent(value)}
|
|
323
344
|
{children}
|
|
324
345
|
</div>
|
|
325
346
|
),
|
|
@@ -12,4 +12,16 @@ describe("factVocabulary", () => {
|
|
|
12
12
|
'<dl data-aui="fact"><dt data-aui="fact-label">Status</dt><dd data-aui="fact-value">open</dd></dl>',
|
|
13
13
|
);
|
|
14
14
|
});
|
|
15
|
+
|
|
16
|
+
it("Fact ignores malformed text properties", () => {
|
|
17
|
+
expect(
|
|
18
|
+
render({
|
|
19
|
+
$type: "Fact",
|
|
20
|
+
label: { unexpected: true },
|
|
21
|
+
value: { unexpected: true },
|
|
22
|
+
}),
|
|
23
|
+
).toBe(
|
|
24
|
+
'<dl data-aui="fact"><dt data-aui="fact-label"></dt><dd data-aui="fact-value"></dd></dl>',
|
|
25
|
+
);
|
|
26
|
+
});
|
|
15
27
|
});
|
package/src/vocabulary/fact.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { GenerativeUILibrary } from "../types";
|
|
3
|
+
import { toTextContent } from "./toTextContent";
|
|
3
4
|
|
|
4
5
|
export const factVocabulary = {
|
|
5
6
|
Fact: {
|
|
@@ -11,9 +12,9 @@ export const factVocabulary = {
|
|
|
11
12
|
}),
|
|
12
13
|
render: ({ label, value, children }) => (
|
|
13
14
|
<dl data-aui="fact">
|
|
14
|
-
<dt data-aui="fact-label">{label}</dt>
|
|
15
|
+
<dt data-aui="fact-label">{toTextContent(label)}</dt>
|
|
15
16
|
<dd data-aui="fact-value">
|
|
16
|
-
{value}
|
|
17
|
+
{toTextContent(value)}
|
|
17
18
|
{children}
|
|
18
19
|
</dd>
|
|
19
20
|
</dl>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
|
+
import { GENERATED_NAME_ATTR } from "../constants";
|
|
3
4
|
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
4
5
|
import { interactiveVocabulary } from "./interactive";
|
|
5
6
|
|
|
@@ -103,6 +104,26 @@ describe("interactiveVocabulary", () => {
|
|
|
103
104
|
expect(html).toContain('<option value="b">B</option>');
|
|
104
105
|
});
|
|
105
106
|
|
|
107
|
+
it("Select ignores malformed options instead of throwing", () => {
|
|
108
|
+
expect(render({ $type: "Select" })).toBe(
|
|
109
|
+
'<select data-aui="select"></select>',
|
|
110
|
+
);
|
|
111
|
+
expect(render({ $type: "Select", options: "not-an-array" })).toBe(
|
|
112
|
+
'<select data-aui="select"></select>',
|
|
113
|
+
);
|
|
114
|
+
expect(
|
|
115
|
+
render({
|
|
116
|
+
$type: "Select",
|
|
117
|
+
options: [
|
|
118
|
+
null,
|
|
119
|
+
{ label: "Missing value" },
|
|
120
|
+
{ value: "Missing label" },
|
|
121
|
+
{ label: "A", value: "a" },
|
|
122
|
+
],
|
|
123
|
+
}),
|
|
124
|
+
).toBe('<select data-aui="select"><option value="a">A</option></select>');
|
|
125
|
+
});
|
|
126
|
+
|
|
106
127
|
it("Input renders a single-line input by default", () => {
|
|
107
128
|
expect(render({ $type: "Input", placeholder: "type here" })).toBe(
|
|
108
129
|
'<input data-aui="input" placeholder="type here"/>',
|
|
@@ -157,6 +178,27 @@ describe("interactiveVocabulary", () => {
|
|
|
157
178
|
expect(html).toContain('checked=""');
|
|
158
179
|
});
|
|
159
180
|
|
|
181
|
+
it.each([
|
|
182
|
+
[
|
|
183
|
+
{ $type: "Button", label: { unexpected: true } },
|
|
184
|
+
'<button type="button" data-aui="button"></button>',
|
|
185
|
+
],
|
|
186
|
+
[
|
|
187
|
+
{
|
|
188
|
+
$type: "Select",
|
|
189
|
+
placeholder: { unexpected: true },
|
|
190
|
+
options: [],
|
|
191
|
+
},
|
|
192
|
+
'<select data-aui="select"></select>',
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
{ $type: "Checkbox", label: { unexpected: true } },
|
|
196
|
+
'<label data-aui="checkbox"><input type="checkbox"/><span data-aui="checkbox-label"></span></label>',
|
|
197
|
+
],
|
|
198
|
+
])("ignores malformed control text properties", (node, expected) => {
|
|
199
|
+
expect(render(node)).toBe(expected);
|
|
200
|
+
});
|
|
201
|
+
|
|
160
202
|
it("RadioGroup renders a fieldset with one radio per option", () => {
|
|
161
203
|
const html = render({
|
|
162
204
|
$type: "RadioGroup",
|
|
@@ -191,6 +233,7 @@ describe("interactiveVocabulary", () => {
|
|
|
191
233
|
],
|
|
192
234
|
});
|
|
193
235
|
expect((html.match(/name="size"/g) ?? []).length).toBe(2);
|
|
236
|
+
expect(html).not.toContain(GENERATED_NAME_ATTR);
|
|
194
237
|
});
|
|
195
238
|
|
|
196
239
|
it("RadioGroup falls back to a generated shared name when name is omitted", () => {
|
|
@@ -201,10 +244,21 @@ describe("interactiveVocabulary", () => {
|
|
|
201
244
|
{ label: "Large", value: "lg" },
|
|
202
245
|
],
|
|
203
246
|
});
|
|
204
|
-
const names = [...html.matchAll(/name="([^"]*)"/g)].map((m) => m[1]);
|
|
247
|
+
const names = [...html.matchAll(/ name="([^"]*)"/g)].map((m) => m[1]);
|
|
205
248
|
expect(names.length).toBe(2);
|
|
206
249
|
expect(names[0]).toBe(names[1]);
|
|
207
250
|
expect(names[0]).toBeTruthy();
|
|
251
|
+
expect(html.split(`${GENERATED_NAME_ATTR}=""`).length - 1).toBe(2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("RadioGroup marks a generated shared name when name is null", () => {
|
|
255
|
+
const html = render({
|
|
256
|
+
$type: "RadioGroup",
|
|
257
|
+
name: null,
|
|
258
|
+
options: [{ label: "Small", value: "sm" }],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(html).toContain(`${GENERATED_NAME_ATTR}=""`);
|
|
208
262
|
});
|
|
209
263
|
|
|
210
264
|
it("RadioGroup marks the option matching defaultValue as checked", () => {
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { useId } from "react";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { GENERATED_NAME_ATTR } from "../constants";
|
|
3
4
|
import type { Action } from "../ir";
|
|
4
5
|
import { BUTTON_STYLES } from "../ir";
|
|
5
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
GenerativeUIDispatch,
|
|
8
|
+
GenerativeUILibrary,
|
|
9
|
+
GenerativeUIStatus,
|
|
10
|
+
} from "../types";
|
|
6
11
|
import { actionAttr, fire } from "./dispatch";
|
|
12
|
+
import { toTextContent } from "./toTextContent";
|
|
7
13
|
|
|
8
14
|
const optionSchema = z.object({
|
|
9
15
|
label: z.string(),
|
|
10
16
|
value: z.string(),
|
|
11
17
|
});
|
|
12
18
|
|
|
19
|
+
type Option = { label: string; value: string };
|
|
20
|
+
|
|
21
|
+
const isOption = (option: unknown): option is Option =>
|
|
22
|
+
option !== null &&
|
|
23
|
+
typeof option === "object" &&
|
|
24
|
+
"label" in option &&
|
|
25
|
+
typeof option.label === "string" &&
|
|
26
|
+
"value" in option &&
|
|
27
|
+
typeof option.value === "string";
|
|
28
|
+
|
|
13
29
|
type RadioGroupRenderProps = {
|
|
14
|
-
options:
|
|
30
|
+
options: Option[];
|
|
15
31
|
name?: string;
|
|
16
32
|
label?: string;
|
|
17
33
|
defaultValue?: string;
|
|
34
|
+
$status: GenerativeUIStatus;
|
|
18
35
|
$action?: Action;
|
|
19
36
|
$dispatch?: GenerativeUIDispatch;
|
|
20
37
|
};
|
|
@@ -37,13 +54,12 @@ function RadioGroupRender({
|
|
|
37
54
|
aria-label={label}
|
|
38
55
|
>
|
|
39
56
|
{safeOptions.map((option, i) =>
|
|
40
|
-
option
|
|
41
|
-
typeof option.label === "string" &&
|
|
42
|
-
typeof option.value === "string" ? (
|
|
57
|
+
isOption(option) ? (
|
|
43
58
|
<label key={i} data-aui="radiogroup-option">
|
|
44
59
|
<input
|
|
45
60
|
type="radio"
|
|
46
61
|
name={fieldName}
|
|
62
|
+
{...(name == null ? { [GENERATED_NAME_ATTR]: "" } : {})}
|
|
47
63
|
value={option.value}
|
|
48
64
|
defaultChecked={defaultValue === option.value}
|
|
49
65
|
onChange={() => fire($action, $dispatch, option.value)}
|
|
@@ -92,7 +108,7 @@ export const interactiveVocabulary = {
|
|
|
92
108
|
data-aui-action={actionAttr($action)}
|
|
93
109
|
onClick={submit ? undefined : () => fire($action, $dispatch)}
|
|
94
110
|
>
|
|
95
|
-
{label}
|
|
111
|
+
{toTextContent(label)}
|
|
96
112
|
{children}
|
|
97
113
|
</button>
|
|
98
114
|
),
|
|
@@ -120,28 +136,33 @@ export const interactiveVocabulary = {
|
|
|
120
136
|
$action,
|
|
121
137
|
$dispatch,
|
|
122
138
|
children,
|
|
123
|
-
}) =>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
}) => {
|
|
140
|
+
const placeholderText = toTextContent(placeholder);
|
|
141
|
+
return (
|
|
142
|
+
<select
|
|
143
|
+
data-aui="select"
|
|
144
|
+
data-aui-action={actionAttr($action)}
|
|
145
|
+
name={name}
|
|
146
|
+
aria-label={label}
|
|
147
|
+
defaultValue=""
|
|
148
|
+
onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
|
|
149
|
+
>
|
|
150
|
+
{placeholderText ? (
|
|
151
|
+
<option value="" disabled>
|
|
152
|
+
{placeholderText}
|
|
153
|
+
</option>
|
|
154
|
+
) : null}
|
|
155
|
+
{(Array.isArray(options) ? options : []).map((option, i) =>
|
|
156
|
+
isOption(option) ? (
|
|
157
|
+
<option key={i} value={option.value}>
|
|
158
|
+
{option.label}
|
|
159
|
+
</option>
|
|
160
|
+
) : null,
|
|
161
|
+
)}
|
|
162
|
+
{children}
|
|
163
|
+
</select>
|
|
164
|
+
);
|
|
165
|
+
},
|
|
145
166
|
},
|
|
146
167
|
Input: {
|
|
147
168
|
description:
|
|
@@ -251,7 +272,7 @@ export const interactiveVocabulary = {
|
|
|
251
272
|
defaultChecked={defaultChecked}
|
|
252
273
|
onChange={(e) => fire($action, $dispatch, e.currentTarget.checked)}
|
|
253
274
|
/>
|
|
254
|
-
<span data-aui="checkbox-label">{label}</span>
|
|
275
|
+
<span data-aui="checkbox-label">{toTextContent(label)}</span>
|
|
255
276
|
</label>
|
|
256
277
|
),
|
|
257
278
|
},
|
|
@@ -16,6 +16,7 @@ const el = (
|
|
|
16
16
|
type: "text",
|
|
17
17
|
value: "",
|
|
18
18
|
disabled: false,
|
|
19
|
+
hasAttribute: () => false,
|
|
19
20
|
...partial,
|
|
20
21
|
});
|
|
21
22
|
|
|
@@ -114,6 +115,22 @@ describe("layoutVocabulary", () => {
|
|
|
114
115
|
'<span data-aui="badge" data-aui-variant="success">new</span>',
|
|
115
116
|
);
|
|
116
117
|
});
|
|
118
|
+
|
|
119
|
+
it("ignores malformed card and badge text properties", () => {
|
|
120
|
+
expect(
|
|
121
|
+
render({
|
|
122
|
+
$type: "Card",
|
|
123
|
+
title: { unexpected: true },
|
|
124
|
+
confirm: { label: { unexpected: true } },
|
|
125
|
+
cancel: { label: { unexpected: true } },
|
|
126
|
+
}),
|
|
127
|
+
).toBe(
|
|
128
|
+
'<section data-aui="card" data-aui-surface=""><footer data-aui="card-footer"><button type="button" data-aui="card-confirm"></button><button type="button" data-aui="card-cancel"></button></footer></section>',
|
|
129
|
+
);
|
|
130
|
+
expect(render({ $type: "Badge", value: { unexpected: true } })).toBe(
|
|
131
|
+
'<span data-aui="badge"></span>',
|
|
132
|
+
);
|
|
133
|
+
});
|
|
117
134
|
});
|
|
118
135
|
|
|
119
136
|
describe("layoutVocabulary Box", () => {
|
|
@@ -4,6 +4,7 @@ import type { GenerativeUILibrary } from "../types";
|
|
|
4
4
|
import { ALIGNS, JUSTIFIES } from "../ir";
|
|
5
5
|
import { fire } from "./dispatch";
|
|
6
6
|
import { collectFormValuesFromEvent } from "./collectFormValues";
|
|
7
|
+
import { toTextContent } from "./toTextContent";
|
|
7
8
|
|
|
8
9
|
const toCssLength = (value: string | number): string =>
|
|
9
10
|
typeof value === "number" ? `${value}px` : value;
|
|
@@ -65,6 +66,7 @@ export const layoutVocabulary = {
|
|
|
65
66
|
children,
|
|
66
67
|
}) => {
|
|
67
68
|
const Root = asForm ? "form" : "section";
|
|
69
|
+
const cardTitle = toTextContent(title);
|
|
68
70
|
const footer =
|
|
69
71
|
confirm || cancel ? (
|
|
70
72
|
<footer data-aui="card-footer">
|
|
@@ -76,7 +78,7 @@ export const layoutVocabulary = {
|
|
|
76
78
|
asForm ? undefined : () => fire(confirm.$action, $dispatch)
|
|
77
79
|
}
|
|
78
80
|
>
|
|
79
|
-
{confirm.label}
|
|
81
|
+
{toTextContent(confirm.label)}
|
|
80
82
|
</button>
|
|
81
83
|
) : null}
|
|
82
84
|
{cancel ? (
|
|
@@ -85,7 +87,7 @@ export const layoutVocabulary = {
|
|
|
85
87
|
data-aui="card-cancel"
|
|
86
88
|
onClick={() => fire(cancel.$action, $dispatch)}
|
|
87
89
|
>
|
|
88
|
-
{cancel.label}
|
|
90
|
+
{toTextContent(cancel.label)}
|
|
89
91
|
</button>
|
|
90
92
|
) : null}
|
|
91
93
|
</footer>
|
|
@@ -118,7 +120,9 @@ export const layoutVocabulary = {
|
|
|
118
120
|
: undefined
|
|
119
121
|
}
|
|
120
122
|
>
|
|
121
|
-
{
|
|
123
|
+
{cardTitle ? (
|
|
124
|
+
<header data-aui="card-title">{cardTitle}</header>
|
|
125
|
+
) : null}
|
|
122
126
|
{children}
|
|
123
127
|
{footer}
|
|
124
128
|
</Root>
|
|
@@ -182,7 +186,7 @@ export const layoutVocabulary = {
|
|
|
182
186
|
}),
|
|
183
187
|
render: ({ value, variant, children }) => (
|
|
184
188
|
<span data-aui="badge" data-aui-variant={variant}>
|
|
185
|
-
{value}
|
|
189
|
+
{toTextContent(value)}
|
|
186
190
|
{children}
|
|
187
191
|
</span>
|
|
188
192
|
),
|
|
@@ -3,6 +3,7 @@ import type { ReactElement } from "react";
|
|
|
3
3
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
4
4
|
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
5
5
|
import { createActionRegistry } from "../actionRegistry";
|
|
6
|
+
import type { GenerativeUIDispatch } from "../types";
|
|
6
7
|
import { listVocabulary } from "./list";
|
|
7
8
|
|
|
8
9
|
const render = (node: unknown) =>
|
|
@@ -55,13 +56,13 @@ describe("listVocabulary", () => {
|
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
describe("listVocabulary $action dispatch", () => {
|
|
58
|
-
const rowOut = (dispatch:
|
|
59
|
+
const rowOut = (dispatch: GenerativeUIDispatch) =>
|
|
59
60
|
listVocabulary.ListViewItem.render({
|
|
60
61
|
$status: "done",
|
|
61
62
|
$action: { type: "open" },
|
|
62
63
|
$dispatch: dispatch,
|
|
63
64
|
children: "row",
|
|
64
|
-
}) as ReactElement
|
|
65
|
+
}) as ReactElement<{ children: ReactElement }>;
|
|
65
66
|
|
|
66
67
|
it("clicking the trigger fires $action with no $input", () => {
|
|
67
68
|
const handler = vi.fn();
|
|
@@ -83,8 +84,8 @@ describe("listVocabulary $action dispatch", () => {
|
|
|
83
84
|
preventDefault: () => void;
|
|
84
85
|
};
|
|
85
86
|
|
|
86
|
-
const getOnKeyDown = (dispatch:
|
|
87
|
-
const trigger = rowOut(dispatch).props.children
|
|
87
|
+
const getOnKeyDown = (dispatch: GenerativeUIDispatch) => {
|
|
88
|
+
const trigger = rowOut(dispatch).props.children;
|
|
88
89
|
return (trigger.props as { onKeyDown: (e: KeyDownEvent) => void })
|
|
89
90
|
.onKeyDown;
|
|
90
91
|
};
|
|
@@ -70,4 +70,33 @@ describe("textVocabulary", () => {
|
|
|
70
70
|
'<p data-aui="caption">muted note</p>',
|
|
71
71
|
);
|
|
72
72
|
});
|
|
73
|
+
|
|
74
|
+
it.each([
|
|
75
|
+
[
|
|
76
|
+
{ $type: "Header", text: { unexpected: true } },
|
|
77
|
+
undefined,
|
|
78
|
+
'<h2 data-aui="header" data-aui-size="lg"></h2>',
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
{ $type: "Text", value: { unexpected: true } },
|
|
82
|
+
{ status: "streaming" as const },
|
|
83
|
+
'<span data-aui="text" data-aui-size="md"></span>',
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
{ $type: "Caption", value: { unexpected: true } },
|
|
87
|
+
{ status: "streaming" as const },
|
|
88
|
+
'<p data-aui="caption"></p>',
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
{ $type: "Text", value: 42 },
|
|
92
|
+
{ status: "streaming" as const },
|
|
93
|
+
'<span data-aui="text" data-aui-size="md">42</span>',
|
|
94
|
+
],
|
|
95
|
+
])("renders only supported text properties", (node, options, expected) => {
|
|
96
|
+
expect(
|
|
97
|
+
renderToStaticMarkup(
|
|
98
|
+
<>{renderGenerativeUI(node, textVocabulary, options)}</>,
|
|
99
|
+
),
|
|
100
|
+
).toBe(expected);
|
|
101
|
+
});
|
|
73
102
|
});
|
package/src/vocabulary/text.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { GenerativeUILibrary } from "../types";
|
|
3
3
|
import { COLORS, TEXT_SIZES, WEIGHTS } from "../ir";
|
|
4
|
+
import { toTextContent } from "./toTextContent";
|
|
4
5
|
|
|
5
6
|
export const textVocabulary = {
|
|
6
7
|
Header: {
|
|
@@ -14,7 +15,7 @@ export const textVocabulary = {
|
|
|
14
15
|
}),
|
|
15
16
|
render: ({ text, size, children }) => (
|
|
16
17
|
<h2 data-aui="header" data-aui-size={size ?? "lg"}>
|
|
17
|
-
{text}
|
|
18
|
+
{toTextContent(text)}
|
|
18
19
|
{children}
|
|
19
20
|
</h2>
|
|
20
21
|
),
|
|
@@ -38,7 +39,7 @@ export const textVocabulary = {
|
|
|
38
39
|
data-aui-weight={weight}
|
|
39
40
|
data-aui-color={color}
|
|
40
41
|
>
|
|
41
|
-
{value}
|
|
42
|
+
{toTextContent(value)}
|
|
42
43
|
{children}
|
|
43
44
|
</span>
|
|
44
45
|
),
|
|
@@ -51,7 +52,7 @@ export const textVocabulary = {
|
|
|
51
52
|
streamProperties: true,
|
|
52
53
|
render: ({ value, children }) => (
|
|
53
54
|
<p data-aui="caption">
|
|
54
|
-
{value}
|
|
55
|
+
{toTextContent(value)}
|
|
55
56
|
{children}
|
|
56
57
|
</p>
|
|
57
58
|
),
|