@assistant-ui/react-generative-ui 0.0.7 → 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.
- package/dist/JSONGenerativeUI.client.d.ts +0 -1
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.server.d.ts +0 -1
- package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.shared.d.ts +0 -1
- package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
- package/dist/actionRegistry.d.ts +2 -11
- package/dist/actionRegistry.d.ts.map +1 -1
- package/dist/actionRegistry.js.map +1 -1
- package/dist/buildPresentParameters.d.ts +5 -5
- package/dist/buildPresentParameters.d.ts.map +1 -1
- package/dist/buildPresentParameters.js +10 -5
- package/dist/buildPresentParameters.js.map +1 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/defineGenerativeComponents.d.ts +0 -1
- package/dist/defineGenerativeComponents.d.ts.map +1 -1
- package/dist/generativeUIToJSX.d.ts +15 -10
- package/dist/generativeUIToJSX.d.ts.map +1 -1
- package/dist/generativeUIToJSX.js +47 -17
- package/dist/generativeUIToJSX.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -2
- package/dist/index.server.d.ts +4 -3
- package/dist/index.server.js +2 -2
- package/dist/index.shared.d.ts +3 -3
- package/dist/index.shared.js +2 -2
- package/dist/ir.d.ts +6 -6
- package/dist/ir.d.ts.map +1 -1
- package/dist/ir.js +27 -1
- package/dist/ir.js.map +1 -1
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/renderGenerativeUI.d.ts +0 -1
- package/dist/renderGenerativeUI.d.ts.map +1 -1
- package/dist/slack/constants.d.ts +111 -0
- package/dist/slack/constants.d.ts.map +1 -0
- package/dist/slack/constants.js +168 -0
- package/dist/slack/constants.js.map +1 -0
- package/dist/slack/decodeBlockAction.d.ts +11 -0
- package/dist/slack/decodeBlockAction.d.ts.map +1 -0
- package/dist/slack/decodeBlockAction.js +40 -0
- package/dist/slack/decodeBlockAction.js.map +1 -0
- package/dist/slack/fromSlackBlocks.d.ts +30 -0
- package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
- package/dist/slack/fromSlackBlocks.js +447 -0
- package/dist/slack/fromSlackBlocks.js.map +1 -0
- package/dist/slack/toSlackBlocks.d.ts +7 -0
- package/dist/slack/toSlackBlocks.d.ts.map +1 -0
- package/dist/slack/toSlackBlocks.js +649 -0
- package/dist/slack/toSlackBlocks.js.map +1 -0
- package/dist/slack/types.d.ts +178 -0
- package/dist/slack/types.d.ts.map +1 -0
- package/dist/slack/types.js +0 -0
- package/dist/slack.d.ts +5 -0
- package/dist/slack.js +4 -0
- package/dist/teams/boundSpec.d.ts +20 -0
- package/dist/teams/boundSpec.d.ts.map +1 -0
- package/dist/teams/boundSpec.js +67 -0
- package/dist/teams/boundSpec.js.map +1 -0
- package/dist/teams/constants.d.ts +57 -0
- package/dist/teams/constants.d.ts.map +1 -0
- package/dist/teams/constants.js +92 -0
- package/dist/teams/constants.js.map +1 -0
- package/dist/teams/decodeSubmitData.d.ts +25 -0
- package/dist/teams/decodeSubmitData.d.ts.map +1 -0
- package/dist/teams/decodeSubmitData.js +44 -0
- package/dist/teams/decodeSubmitData.js.map +1 -0
- package/dist/teams/toAdaptiveCard.d.ts +39 -0
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
- package/dist/teams/toAdaptiveCard.js +496 -0
- package/dist/teams/toAdaptiveCard.js.map +1 -0
- package/dist/teams/toTeamsAttachments.d.ts +13 -0
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
- package/dist/teams/toTeamsAttachments.js +95 -0
- package/dist/teams/toTeamsAttachments.js.map +1 -0
- package/dist/teams/types.d.ts +196 -0
- package/dist/teams/types.d.ts.map +1 -0
- package/dist/teams/types.js +0 -0
- package/dist/teams.d.ts +5 -0
- package/dist/teams.js +4 -0
- package/dist/types.d.ts +7 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/vocabulary/alert.d.ts +6 -12
- package/dist/vocabulary/alert.d.ts.map +1 -1
- package/dist/vocabulary/alert.js +13 -4
- package/dist/vocabulary/alert.js.map +1 -1
- package/dist/vocabulary/collectFormValues.d.ts +23 -0
- package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
- package/dist/vocabulary/collectFormValues.js +32 -0
- package/dist/vocabulary/collectFormValues.js.map +1 -0
- package/dist/vocabulary/data.d.ts +16 -17
- package/dist/vocabulary/data.d.ts.map +1 -1
- package/dist/vocabulary/data.js +244 -13
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/dispatch.d.ts +11 -0
- package/dist/vocabulary/dispatch.d.ts.map +1 -0
- package/dist/vocabulary/dispatch.js +21 -0
- package/dist/vocabulary/dispatch.js.map +1 -0
- package/dist/vocabulary/fact.d.ts +1 -6
- package/dist/vocabulary/fact.d.ts.map +1 -1
- package/dist/vocabulary/form.d.ts +14 -0
- package/dist/vocabulary/form.d.ts.map +1 -0
- package/dist/vocabulary/form.js +23 -0
- package/dist/vocabulary/form.js.map +1 -0
- package/dist/vocabulary/icon.d.ts +44 -0
- package/dist/vocabulary/icon.d.ts.map +1 -0
- package/dist/vocabulary/icon.js +272 -0
- package/dist/vocabulary/icon.js.map +1 -0
- package/dist/vocabulary/index.d.ts +0 -1
- package/dist/vocabulary/index.d.ts.map +1 -1
- package/dist/vocabulary/index.js +7 -1
- package/dist/vocabulary/index.js.map +1 -1
- package/dist/vocabulary/interactive.d.ts +45 -33
- package/dist/vocabulary/interactive.d.ts.map +1 -1
- package/dist/vocabulary/interactive.js +78 -29
- package/dist/vocabulary/interactive.js.map +1 -1
- package/dist/vocabulary/layout.d.ts +27 -23
- package/dist/vocabulary/layout.d.ts.map +1 -1
- package/dist/vocabulary/layout.js +78 -14
- package/dist/vocabulary/layout.js.map +1 -1
- package/dist/vocabulary/list.d.ts +17 -0
- package/dist/vocabulary/list.d.ts.map +1 -0
- package/dist/vocabulary/list.js +48 -0
- package/dist/vocabulary/list.js.map +1 -0
- package/dist/vocabulary/media.d.ts +3 -9
- package/dist/vocabulary/media.d.ts.map +1 -1
- package/dist/vocabulary/media.js +17 -7
- package/dist/vocabulary/media.js.map +1 -1
- package/dist/vocabulary/text.d.ts +3 -17
- package/dist/vocabulary/text.d.ts.map +1 -1
- package/package.json +13 -5
- package/src/actionRegistry.ts +2 -10
- package/src/buildPresentParameters.ts +15 -9
- package/src/generativeUIToJSX.test.ts +186 -0
- package/src/generativeUIToJSX.ts +112 -21
- package/src/index.shared.ts +3 -0
- package/src/ir.ts +31 -5
- package/src/renderGenerativeUI.test.tsx +16 -2
- package/src/slack/constants.ts +217 -0
- package/src/slack/decodeBlockAction.test.ts +135 -0
- package/src/slack/decodeBlockAction.ts +66 -0
- package/src/slack/fromSlackBlocks.test.ts +1145 -0
- package/src/slack/fromSlackBlocks.ts +709 -0
- package/src/slack/toSlackBlocks.test.ts +1936 -0
- package/src/slack/toSlackBlocks.ts +1319 -0
- package/src/slack/types.ts +230 -0
- package/src/slack.ts +37 -0
- package/src/teams/boundSpec.ts +102 -0
- package/src/teams/constants.ts +127 -0
- package/src/teams/decodeSubmitData.test.ts +148 -0
- package/src/teams/decodeSubmitData.ts +57 -0
- package/src/teams/toAdaptiveCard.test.ts +1410 -0
- package/src/teams/toAdaptiveCard.ts +762 -0
- package/src/teams/toTeamsAttachments.test.ts +29 -0
- package/src/teams/toTeamsAttachments.ts +137 -0
- package/src/teams/types.ts +237 -0
- package/src/teams.ts +34 -0
- package/src/vocabulary/alert.test.tsx +72 -2
- package/src/vocabulary/alert.tsx +24 -5
- package/src/vocabulary/collectFormValues.test.ts +127 -0
- package/src/vocabulary/collectFormValues.ts +58 -0
- package/src/vocabulary/data.test.tsx +503 -3
- package/src/vocabulary/data.tsx +426 -18
- package/src/vocabulary/dispatch.ts +22 -0
- package/src/vocabulary/form.test.tsx +112 -0
- package/src/vocabulary/form.tsx +34 -0
- package/src/vocabulary/icon.test.tsx +64 -0
- package/src/vocabulary/icon.tsx +174 -0
- package/src/vocabulary/index.test.ts +7 -0
- package/src/vocabulary/index.ts +6 -0
- package/src/vocabulary/interactive.dispatch.test.tsx +243 -1
- package/src/vocabulary/interactive.test.tsx +143 -3
- package/src/vocabulary/interactive.tsx +134 -30
- package/src/vocabulary/layout.test.tsx +231 -3
- package/src/vocabulary/layout.tsx +168 -15
- package/src/vocabulary/list.test.tsx +195 -0
- package/src/vocabulary/list.tsx +48 -0
- package/src/vocabulary/media.test.tsx +36 -4
- package/src/vocabulary/media.tsx +27 -8
|
@@ -1,39 +1,65 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import type { Action } from "../ir";
|
|
3
4
|
import { BUTTON_STYLES } from "../ir";
|
|
4
5
|
import type { GenerativeUIDispatch, GenerativeUILibrary } from "../types";
|
|
6
|
+
import { actionAttr, fire } from "./dispatch";
|
|
5
7
|
|
|
6
8
|
const optionSchema = z.object({
|
|
7
9
|
label: z.string(),
|
|
8
10
|
value: z.string(),
|
|
9
11
|
});
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* re-thrown on a microtask so rejections surface rather than going unhandled. */
|
|
19
|
-
const fire = (
|
|
20
|
-
$action: Action | undefined,
|
|
21
|
-
$dispatch: GenerativeUIDispatch | undefined,
|
|
22
|
-
input?: unknown,
|
|
23
|
-
) => {
|
|
24
|
-
if (!$action || !$dispatch) return;
|
|
25
|
-
const payload = input === undefined ? $action : { ...$action, $input: input };
|
|
26
|
-
void Promise.resolve($dispatch(payload)).catch((error) => {
|
|
27
|
-
queueMicrotask(() => {
|
|
28
|
-
throw error;
|
|
29
|
-
});
|
|
30
|
-
});
|
|
13
|
+
type RadioGroupRenderProps = {
|
|
14
|
+
options: { label: string; value: string }[];
|
|
15
|
+
name?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
$action?: Action;
|
|
19
|
+
$dispatch?: GenerativeUIDispatch;
|
|
31
20
|
};
|
|
32
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
|
+
|
|
33
59
|
export const interactiveVocabulary = {
|
|
34
60
|
Button: {
|
|
35
61
|
description:
|
|
36
|
-
"A clickable button. Carries `$action` describing the side effect or resume value.",
|
|
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.",
|
|
37
63
|
properties: z.object({
|
|
38
64
|
label: z.string().describe("Button label."),
|
|
39
65
|
buttonStyle: z.enum(BUTTON_STYLES).optional().describe("Visual style."),
|
|
@@ -41,14 +67,30 @@ export const interactiveVocabulary = {
|
|
|
41
67
|
.boolean()
|
|
42
68
|
.optional()
|
|
43
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
|
+
),
|
|
44
76
|
}),
|
|
45
|
-
render: ({
|
|
77
|
+
render: ({
|
|
78
|
+
label,
|
|
79
|
+
buttonStyle,
|
|
80
|
+
block,
|
|
81
|
+
submit,
|
|
82
|
+
$action,
|
|
83
|
+
$dispatch,
|
|
84
|
+
children,
|
|
85
|
+
}) => (
|
|
46
86
|
<button
|
|
87
|
+
type={submit ? "submit" : "button"}
|
|
47
88
|
data-aui="button"
|
|
48
89
|
data-aui-style={buttonStyle}
|
|
49
90
|
data-aui-block={block || undefined}
|
|
91
|
+
data-aui-submit={submit || undefined}
|
|
50
92
|
data-aui-action={actionAttr($action)}
|
|
51
|
-
onClick={() => fire($action, $dispatch)}
|
|
93
|
+
onClick={submit ? undefined : () => fire($action, $dispatch)}
|
|
52
94
|
>
|
|
53
95
|
{label}
|
|
54
96
|
{children}
|
|
@@ -68,11 +110,21 @@ export const interactiveVocabulary = {
|
|
|
68
110
|
.string()
|
|
69
111
|
.optional()
|
|
70
112
|
.describe("Accessible label for the control."),
|
|
113
|
+
name: z.string().optional().describe("Field name used inside a Form."),
|
|
71
114
|
}),
|
|
72
|
-
render: ({
|
|
115
|
+
render: ({
|
|
116
|
+
options,
|
|
117
|
+
placeholder,
|
|
118
|
+
label,
|
|
119
|
+
name,
|
|
120
|
+
$action,
|
|
121
|
+
$dispatch,
|
|
122
|
+
children,
|
|
123
|
+
}) => (
|
|
73
124
|
<select
|
|
74
125
|
data-aui="select"
|
|
75
126
|
data-aui-action={actionAttr($action)}
|
|
127
|
+
name={name}
|
|
76
128
|
aria-label={label}
|
|
77
129
|
defaultValue=""
|
|
78
130
|
onChange={(e) => fire($action, $dispatch, e.currentTarget.value)}
|
|
@@ -104,33 +156,48 @@ export const interactiveVocabulary = {
|
|
|
104
156
|
.string()
|
|
105
157
|
.optional()
|
|
106
158
|
.describe("Accessible label for the control."),
|
|
159
|
+
name: z.string().optional().describe("Field name used inside a Form."),
|
|
107
160
|
}),
|
|
108
|
-
render: ({ placeholder, multiline, label, $action, $dispatch }) => {
|
|
161
|
+
render: ({ placeholder, multiline, label, name, $action, $dispatch }) => {
|
|
109
162
|
const submit = (v: string) => fire($action, $dispatch, v);
|
|
110
163
|
return multiline ? (
|
|
111
164
|
<textarea
|
|
112
165
|
data-aui="input"
|
|
113
166
|
data-aui-multiline
|
|
114
167
|
data-aui-action={actionAttr($action)}
|
|
168
|
+
name={name}
|
|
115
169
|
aria-label={label}
|
|
116
170
|
placeholder={placeholder}
|
|
117
171
|
onKeyDown={(e) => {
|
|
118
172
|
if (
|
|
119
|
-
e.key
|
|
120
|
-
(e.ctrlKey || e.metaKey)
|
|
121
|
-
|
|
173
|
+
e.key !== "Enter" ||
|
|
174
|
+
!(e.ctrlKey || e.metaKey) ||
|
|
175
|
+
e.nativeEvent.isComposing
|
|
122
176
|
)
|
|
177
|
+
return;
|
|
178
|
+
if (e.currentTarget.form) {
|
|
179
|
+
e.preventDefault();
|
|
180
|
+
HTMLFormElement.prototype.requestSubmit.call(
|
|
181
|
+
e.currentTarget.form,
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
123
184
|
submit(e.currentTarget.value);
|
|
185
|
+
}
|
|
124
186
|
}}
|
|
125
187
|
/>
|
|
126
188
|
) : (
|
|
127
189
|
<input
|
|
128
190
|
data-aui="input"
|
|
129
191
|
data-aui-action={actionAttr($action)}
|
|
192
|
+
name={name}
|
|
130
193
|
aria-label={label}
|
|
131
194
|
placeholder={placeholder}
|
|
132
195
|
onKeyDown={(e) => {
|
|
133
|
-
if (
|
|
196
|
+
if (
|
|
197
|
+
e.key === "Enter" &&
|
|
198
|
+
!e.nativeEvent.isComposing &&
|
|
199
|
+
!e.currentTarget.form
|
|
200
|
+
)
|
|
134
201
|
submit(e.currentTarget.value);
|
|
135
202
|
}}
|
|
136
203
|
/>
|
|
@@ -148,12 +215,14 @@ export const interactiveVocabulary = {
|
|
|
148
215
|
.string()
|
|
149
216
|
.optional()
|
|
150
217
|
.describe("Accessible label for the control."),
|
|
218
|
+
name: z.string().optional().describe("Field name used inside a Form."),
|
|
151
219
|
}),
|
|
152
|
-
render: ({ value, min, max, label, $action, $dispatch }) => (
|
|
220
|
+
render: ({ value, min, max, label, name, $action, $dispatch }) => (
|
|
153
221
|
<input
|
|
154
222
|
type="date"
|
|
155
223
|
data-aui="datepicker"
|
|
156
224
|
data-aui-action={actionAttr($action)}
|
|
225
|
+
name={name}
|
|
157
226
|
aria-label={label}
|
|
158
227
|
defaultValue={value}
|
|
159
228
|
min={min}
|
|
@@ -162,4 +231,39 @@ export const interactiveVocabulary = {
|
|
|
162
231
|
/>
|
|
163
232
|
),
|
|
164
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
|
+
},
|
|
165
269
|
} satisfies GenerativeUILibrary;
|
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
2
3
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
3
4
|
import { renderGenerativeUI } from "../renderGenerativeUI";
|
|
5
|
+
import { createActionRegistry } from "../actionRegistry";
|
|
4
6
|
import { layoutVocabulary } from "./layout";
|
|
7
|
+
import type { FormControlElementLike } from "./collectFormValues";
|
|
5
8
|
|
|
6
9
|
const render = (node: unknown) =>
|
|
7
10
|
renderToStaticMarkup(<>{renderGenerativeUI(node, layoutVocabulary)}</>);
|
|
8
11
|
|
|
12
|
+
const el = (
|
|
13
|
+
partial: Partial<FormControlElementLike>,
|
|
14
|
+
): FormControlElementLike => ({
|
|
15
|
+
name: "",
|
|
16
|
+
type: "text",
|
|
17
|
+
value: "",
|
|
18
|
+
disabled: false,
|
|
19
|
+
...partial,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const getFooterButtons = (out: ReactElement) => {
|
|
23
|
+
const footer = (out.props as { children: unknown[] })
|
|
24
|
+
.children[2] as ReactElement;
|
|
25
|
+
const [confirmBtn, cancelBtn] = (
|
|
26
|
+
footer.props as { children: [ReactElement | null, ReactElement | null] }
|
|
27
|
+
).children;
|
|
28
|
+
return { confirmBtn, cancelBtn };
|
|
29
|
+
};
|
|
30
|
+
|
|
9
31
|
describe("layoutVocabulary", () => {
|
|
10
32
|
it("Card renders a section with optional title and padding/background hooks", () => {
|
|
11
33
|
expect(
|
|
@@ -13,13 +35,32 @@ describe("layoutVocabulary", () => {
|
|
|
13
35
|
$type: "Card",
|
|
14
36
|
title: "T",
|
|
15
37
|
padding: 2,
|
|
16
|
-
background: "muted",
|
|
38
|
+
background: "var(--muted)",
|
|
17
39
|
}),
|
|
18
40
|
).toBe(
|
|
19
|
-
'<section data-aui="card" data-aui-padding="2" data-aui-background="muted"><header data-aui="card-title">T</header></section>',
|
|
41
|
+
'<section data-aui="card" data-aui-padding="2" data-aui-background="var(--muted)" style="background:var(--muted);color:white"><header data-aui="card-title">T</header></section>',
|
|
20
42
|
);
|
|
21
43
|
});
|
|
22
44
|
|
|
45
|
+
it("Card applies background as an inline style so arbitrary CSS values (colors, gradients) render", () => {
|
|
46
|
+
const html = render({
|
|
47
|
+
$type: "Card",
|
|
48
|
+
background: "linear-gradient(90deg, #3b82f6, #1d4ed8)",
|
|
49
|
+
});
|
|
50
|
+
expect(html).toContain(
|
|
51
|
+
'style="background:linear-gradient(90deg, #3b82f6, #1d4ed8);color:white"',
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("Card sets its text color to white whenever a background is provided", () => {
|
|
56
|
+
const html = render({ $type: "Card", background: "#1d4ed8" });
|
|
57
|
+
expect(html).toContain("color:white");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("Card omits the style attribute when background is not provided", () => {
|
|
61
|
+
expect(render({ $type: "Card", title: "T" })).not.toContain("style=");
|
|
62
|
+
});
|
|
63
|
+
|
|
23
64
|
it("Card without a title omits the header", () => {
|
|
24
65
|
expect(render({ $type: "Card" })).toBe(
|
|
25
66
|
'<section data-aui="card"></section>',
|
|
@@ -59,3 +100,190 @@ describe("layoutVocabulary", () => {
|
|
|
59
100
|
);
|
|
60
101
|
});
|
|
61
102
|
});
|
|
103
|
+
|
|
104
|
+
describe("layoutVocabulary Box", () => {
|
|
105
|
+
it("renders a bare div with no style attribute when no props are given", () => {
|
|
106
|
+
expect(render({ $type: "Box" })).toBe('<div data-aui="box"></div>');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("applies width/height as inline pixel lengths when given numbers", () => {
|
|
110
|
+
const html = render({ $type: "Box", width: 200, height: 8 });
|
|
111
|
+
expect(html).toContain('style="width:200px;height:8px"');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("applies width/height verbatim when given strings", () => {
|
|
115
|
+
const html = render({ $type: "Box", width: "100%", height: "1.5rem" });
|
|
116
|
+
expect(html).toContain('style="width:100%;height:1.5rem"');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("applies a numeric radius as an inline pixel border-radius", () => {
|
|
120
|
+
const html = render({ $type: "Box", radius: 6 });
|
|
121
|
+
expect(html).toContain('style="border-radius:6px"');
|
|
122
|
+
expect(html).not.toContain("data-aui-radius");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('applies radius "full" as a 9999px inline border-radius and a data-aui-radius hook', () => {
|
|
126
|
+
const html = render({ $type: "Box", radius: "full" });
|
|
127
|
+
expect(html).toContain('data-aui-radius="full"');
|
|
128
|
+
expect(html).toContain('style="border-radius:9999px"');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("applies background as an inline style", () => {
|
|
132
|
+
const html = render({ $type: "Box", background: "#3b82f6" });
|
|
133
|
+
expect(html).toContain('style="background:#3b82f6"');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("renders a progress bar as a track Box containing a partial-width fill Box", () => {
|
|
137
|
+
const html = render({
|
|
138
|
+
$type: "Box",
|
|
139
|
+
width: "100%",
|
|
140
|
+
height: 8,
|
|
141
|
+
radius: "full",
|
|
142
|
+
background: "var(--muted)",
|
|
143
|
+
children: {
|
|
144
|
+
$type: "Box",
|
|
145
|
+
width: "60%",
|
|
146
|
+
height: 8,
|
|
147
|
+
radius: "full",
|
|
148
|
+
background: "var(--primary)",
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
expect(html).toBe(
|
|
152
|
+
'<div data-aui="box" data-aui-radius="full" style="width:100%;height:8px;background:var(--muted);border-radius:9999px">' +
|
|
153
|
+
'<div data-aui="box" data-aui-radius="full" style="width:60%;height:8px;background:var(--primary);border-radius:9999px"></div>' +
|
|
154
|
+
"</div>",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("renders nested children alongside style props", () => {
|
|
159
|
+
const html = render({
|
|
160
|
+
$type: "Box",
|
|
161
|
+
background: "red",
|
|
162
|
+
children: "content",
|
|
163
|
+
});
|
|
164
|
+
expect(html).toBe(
|
|
165
|
+
'<div data-aui="box" style="background:red">content</div>',
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("layoutVocabulary Card asForm/confirm/cancel", () => {
|
|
171
|
+
it("Card with asForm renders a form element with the data-aui-asform hook", () => {
|
|
172
|
+
const html = render({ $type: "Card", asForm: true, title: "T" });
|
|
173
|
+
expect(html).toContain("<form ");
|
|
174
|
+
expect(html).toContain('data-aui-asform="true"');
|
|
175
|
+
expect(html).not.toContain("<section");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("Card without asForm renders a section with no data-aui-asform hook", () => {
|
|
179
|
+
const html = render({ $type: "Card" });
|
|
180
|
+
expect(html).toContain("<section");
|
|
181
|
+
expect(html).not.toContain("data-aui-asform");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("Card omits the footer when neither confirm nor cancel is present", () => {
|
|
185
|
+
expect(render({ $type: "Card" })).not.toContain("card-footer");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("Card renders a footer when only confirm is present", () => {
|
|
189
|
+
expect(render({ $type: "Card", confirm: { label: "OK" } })).toContain(
|
|
190
|
+
'data-aui="card-footer"',
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("Card renders a footer when only cancel is present", () => {
|
|
195
|
+
expect(render({ $type: "Card", cancel: { label: "Cancel" } })).toContain(
|
|
196
|
+
'data-aui="card-footer"',
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("Card confirm renders a submit button when asForm is set", () => {
|
|
201
|
+
const html = render({
|
|
202
|
+
$type: "Card",
|
|
203
|
+
asForm: true,
|
|
204
|
+
confirm: { label: "Save" },
|
|
205
|
+
});
|
|
206
|
+
expect(html).toContain('type="submit" data-aui="card-confirm"');
|
|
207
|
+
expect(html).toContain("Save");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("Card confirm renders a click button when asForm is not set", () => {
|
|
211
|
+
const html = render({ $type: "Card", confirm: { label: "Save" } });
|
|
212
|
+
expect(html).toContain('type="button" data-aui="card-confirm"');
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("Card cancel always renders a plain button regardless of asForm", () => {
|
|
216
|
+
const html = render({
|
|
217
|
+
$type: "Card",
|
|
218
|
+
asForm: true,
|
|
219
|
+
cancel: { label: "Dismiss" },
|
|
220
|
+
});
|
|
221
|
+
expect(html).toContain('type="button" data-aui="card-cancel"');
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("layoutVocabulary Card asForm/confirm/cancel dispatch", () => {
|
|
226
|
+
it("Card asForm submit collects named values and dispatches confirm.$action with $input", () => {
|
|
227
|
+
const handler = vi.fn();
|
|
228
|
+
const registry = createActionRegistry({ save: handler });
|
|
229
|
+
const out = layoutVocabulary.Card.render({
|
|
230
|
+
asForm: true,
|
|
231
|
+
confirm: { label: "Save", $action: { type: "save" } },
|
|
232
|
+
$status: "done",
|
|
233
|
+
$dispatch: registry.dispatch,
|
|
234
|
+
}) as ReactElement;
|
|
235
|
+
const onSubmit = (out.props as { onSubmit: (e: unknown) => void }).onSubmit;
|
|
236
|
+
const event = {
|
|
237
|
+
preventDefault: vi.fn(),
|
|
238
|
+
currentTarget: {
|
|
239
|
+
elements: [el({ name: "email", type: "email", value: "a@x.com" })],
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
onSubmit(event);
|
|
243
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
244
|
+
expect(handler).toHaveBeenCalledWith({
|
|
245
|
+
payload: { type: "save", $input: { email: "a@x.com" } },
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("Card confirm click (non-asForm) fires confirm.$action without $input", () => {
|
|
250
|
+
const handler = vi.fn();
|
|
251
|
+
const registry = createActionRegistry({ save: handler });
|
|
252
|
+
const out = layoutVocabulary.Card.render({
|
|
253
|
+
confirm: { label: "Save", $action: { type: "save" } },
|
|
254
|
+
$status: "done",
|
|
255
|
+
$dispatch: registry.dispatch,
|
|
256
|
+
}) as ReactElement;
|
|
257
|
+
const { confirmBtn } = getFooterButtons(out);
|
|
258
|
+
const onClick = (confirmBtn!.props as { onClick: () => void }).onClick;
|
|
259
|
+
onClick();
|
|
260
|
+
expect(handler).toHaveBeenCalledWith({ payload: { type: "save" } });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("Card confirm button has no onClick when asForm is set (the form submit handles it)", () => {
|
|
264
|
+
const out = layoutVocabulary.Card.render({
|
|
265
|
+
asForm: true,
|
|
266
|
+
confirm: { label: "Save", $action: { type: "save" } },
|
|
267
|
+
$status: "done",
|
|
268
|
+
}) as ReactElement;
|
|
269
|
+
const { confirmBtn } = getFooterButtons(out);
|
|
270
|
+
expect(
|
|
271
|
+
(confirmBtn!.props as { onClick?: unknown }).onClick,
|
|
272
|
+
).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("Card cancel click fires cancel.$action without $input regardless of asForm", () => {
|
|
276
|
+
const handler = vi.fn();
|
|
277
|
+
const registry = createActionRegistry({ dismiss: handler });
|
|
278
|
+
const out = layoutVocabulary.Card.render({
|
|
279
|
+
asForm: true,
|
|
280
|
+
cancel: { label: "Dismiss", $action: { type: "dismiss" } },
|
|
281
|
+
$status: "done",
|
|
282
|
+
$dispatch: registry.dispatch,
|
|
283
|
+
}) as ReactElement;
|
|
284
|
+
const { cancelBtn } = getFooterButtons(out);
|
|
285
|
+
const onClick = (cancelBtn!.props as { onClick: () => void }).onClick;
|
|
286
|
+
onClick();
|
|
287
|
+
expect(handler).toHaveBeenCalledWith({ payload: { type: "dismiss" } });
|
|
288
|
+
});
|
|
289
|
+
});
|