@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,762 @@
1
+ import {
2
+ normalizeSpec,
3
+ type NormalizedUIElement,
4
+ type NormalizedUINode,
5
+ } from "../ir";
6
+ import { boundSpec } from "./boundSpec";
7
+ import {
8
+ CHOICE_OPTION_CAP,
9
+ MAX_TRAVERSAL_DEPTH,
10
+ PAYLOAD_SOFT_CAP,
11
+ PRIMARY_ACTION_CAP,
12
+ TABLE_COLUMN_CAP,
13
+ TABLE_ROW_CAP,
14
+ buildCard,
15
+ buildSubmitAction,
16
+ clampReasonDetail,
17
+ utf8ByteLength,
18
+ } from "./constants";
19
+ import type {
20
+ AdaptiveCardResult,
21
+ TeamsActionSet,
22
+ TeamsAdaptiveCard,
23
+ TeamsCardElement,
24
+ TeamsColumn,
25
+ TeamsContainer,
26
+ TeamsContainerStyle,
27
+ TeamsConversionWarning,
28
+ TeamsFact,
29
+ TeamsInputChoice,
30
+ TeamsTable,
31
+ TeamsTableColumnDefinition,
32
+ TeamsTableRow,
33
+ TeamsTextBlock,
34
+ TeamsTextSize,
35
+ ToAdaptiveCardOptions,
36
+ } from "./types";
37
+
38
+ /** Shared mutable conversion state threaded through one top-level conversion. */
39
+ export interface ConversionContext {
40
+ readonly warnings: TeamsConversionWarning[];
41
+ usedInputIds: Set<string>;
42
+ }
43
+
44
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
45
+ typeof value === "object" && value !== null && !Array.isArray(value);
46
+
47
+ const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
48
+ isRecord(node);
49
+
50
+ const asString = (value: unknown): string =>
51
+ typeof value === "string" ? value : "";
52
+
53
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
54
+
55
+ const SUBTLE_COLORS = new Set([
56
+ "secondary",
57
+ "alpha-70",
58
+ "white",
59
+ "white-70",
60
+ "white-50",
61
+ ]);
62
+ const isSubtleColor = (color: unknown): boolean =>
63
+ typeof color === "string" && SUBTLE_COLORS.has(color);
64
+
65
+ const BOLDER_WEIGHTS = new Set(["medium", "semibold", "bold"]);
66
+ const isBolderWeight = (weight: unknown): boolean =>
67
+ typeof weight === "string" && BOLDER_WEIGHTS.has(weight);
68
+
69
+ const TEXT_SIZE_MAP: Record<string, TeamsTextSize> = {
70
+ sm: "small",
71
+ lg: "medium",
72
+ xl: "large",
73
+ "2xl": "extraLarge",
74
+ "3xl": "extraLarge",
75
+ };
76
+
77
+ const warn = (
78
+ context: ConversionContext,
79
+ code: TeamsConversionWarning["code"],
80
+ component: string,
81
+ detail: string,
82
+ ) => {
83
+ context.warnings.push({ code, component, detail });
84
+ };
85
+
86
+ /**
87
+ * The `aui` key is reserved for the submit envelope (see
88
+ * {@link buildSubmitData} and `decodeSubmitData`): Adaptive Cards merge
89
+ * every input's current value into the same submit object keyed by its
90
+ * `id`, so an input id of `aui` would clobber the envelope. Renamed to an
91
+ * unused id derived from `aui_` instead of being emitted as-is.
92
+ */
93
+ const RESERVED_INPUT_ID = "aui";
94
+
95
+ function reservedSafeId(
96
+ id: string,
97
+ component: string,
98
+ context: ConversionContext,
99
+ ): string {
100
+ const reserved = id === RESERVED_INPUT_ID;
101
+ const base = reserved ? `${RESERVED_INPUT_ID}_` : id;
102
+ let candidate = base;
103
+ let n = 2;
104
+ while (context.usedInputIds.has(candidate)) {
105
+ candidate = `${base}_${n}`;
106
+ n += 1;
107
+ }
108
+ if (reserved) {
109
+ warn(
110
+ context,
111
+ "clamped",
112
+ component,
113
+ `the input id "${RESERVED_INPUT_ID}" collides with the submit envelope's reserved key and was renamed to "${candidate}".`,
114
+ );
115
+ } else if (candidate !== base) {
116
+ warn(
117
+ context,
118
+ "clamped",
119
+ component,
120
+ `the input id "${base}" was already used on this card and was renamed to "${candidate}".`,
121
+ );
122
+ }
123
+ context.usedInputIds.add(candidate);
124
+ return candidate;
125
+ }
126
+
127
+ /**
128
+ * The shared bounded-iteration primitive: slices `value` to `cap` entries
129
+ * without ever reading past that many indices, so a hostile array (sparse or
130
+ * proxied with a fabricated `length`) cannot stall the event loop.
131
+ */
132
+ const clampArray = (
133
+ value: unknown,
134
+ cap: number,
135
+ ): { readonly items: unknown[]; readonly truncated: boolean } => {
136
+ if (!Array.isArray(value)) return { items: [], truncated: false };
137
+ return { items: value.slice(0, cap), truncated: value.length > cap };
138
+ };
139
+
140
+ const normalizedList = (
141
+ node: NormalizedUINode | undefined,
142
+ ): NormalizedUINode[] => {
143
+ if (node === undefined || node === null) return [];
144
+ if (!Array.isArray(node)) return [node];
145
+ return node.flatMap((child) => normalizedList(child));
146
+ };
147
+
148
+ function textBlock(
149
+ text: string,
150
+ extra?: Partial<Omit<TeamsTextBlock, "type" | "text" | "wrap">>,
151
+ ): TeamsTextBlock {
152
+ return { type: "TextBlock", text, wrap: true, ...extra };
153
+ }
154
+
155
+ function applyPending(
156
+ element: TeamsCardElement,
157
+ separator: boolean,
158
+ spacing: boolean,
159
+ ): TeamsCardElement {
160
+ if (!separator && !spacing) return element;
161
+ return {
162
+ ...element,
163
+ ...(separator ? { separator: true as const } : {}),
164
+ ...(spacing ? { spacing: "large" as const } : {}),
165
+ };
166
+ }
167
+
168
+ const toChoice = (option: unknown): TeamsInputChoice | undefined => {
169
+ if (!isRecord(option) || typeof option["value"] !== "string") {
170
+ return undefined;
171
+ }
172
+ return { title: asString(option["label"]), value: option["value"] };
173
+ };
174
+
175
+ const choicesFrom = (
176
+ optionsProp: unknown,
177
+ component: string,
178
+ context: ConversionContext,
179
+ ): TeamsInputChoice[] => {
180
+ const { items, truncated } = clampArray(optionsProp, CHOICE_OPTION_CAP);
181
+ if (truncated) {
182
+ warn(
183
+ context,
184
+ "clamped",
185
+ component,
186
+ `options were clamped to ${CHOICE_OPTION_CAP} entries.`,
187
+ );
188
+ }
189
+ const choices: TeamsInputChoice[] = [];
190
+ for (const item of items) {
191
+ const choice = toChoice(item);
192
+ if (choice !== undefined) choices.push(choice);
193
+ }
194
+ return choices;
195
+ };
196
+
197
+ function convertFacts(facts: readonly NormalizedUIElement[]): TeamsCardElement {
198
+ const set: TeamsFact[] = facts.map((fact) => ({
199
+ title: asString(fact.props["label"]),
200
+ value: asString(fact.props["value"]),
201
+ }));
202
+ return { type: "FactSet", facts: set };
203
+ }
204
+
205
+ // Action.Submit has no visual style concept, so a Button's `buttonStyle`
206
+ // (positive/destructive or otherwise) has nothing to map onto and is dropped.
207
+ function convertButtons(
208
+ buttons: readonly NormalizedUIElement[],
209
+ context: ConversionContext,
210
+ ): TeamsActionSet {
211
+ if (buttons.length > PRIMARY_ACTION_CAP) {
212
+ warn(
213
+ context,
214
+ "clamped",
215
+ "Button",
216
+ `actions beyond ${PRIMARY_ACTION_CAP} were set to secondary mode.`,
217
+ );
218
+ }
219
+ const actions = buttons.map((button, index) =>
220
+ buildSubmitAction(
221
+ asString(button.props["label"]),
222
+ button.action,
223
+ index >= PRIMARY_ACTION_CAP ? "secondary" : undefined,
224
+ ),
225
+ );
226
+ return { type: "ActionSet", actions };
227
+ }
228
+
229
+ function withCompanionSubmit(
230
+ element: NormalizedUIElement,
231
+ input: TeamsCardElement,
232
+ context: ConversionContext,
233
+ ): TeamsCardElement[] {
234
+ if (element.action === undefined) return [input];
235
+ warn(
236
+ context,
237
+ "fallback",
238
+ element.type,
239
+ "Teams inputs cannot dispatch on change; a companion submit action was appended.",
240
+ );
241
+ return [
242
+ input,
243
+ {
244
+ type: "ActionSet",
245
+ actions: [buildSubmitAction("Submit", element.action)],
246
+ },
247
+ ];
248
+ }
249
+
250
+ function cardFooterActionSet(
251
+ props: Readonly<Record<string, unknown>>,
252
+ ): TeamsActionSet | undefined {
253
+ const actions = (["confirm", "cancel"] as const)
254
+ .map((key) => props[key])
255
+ .filter(isRecord)
256
+ .map((footer) =>
257
+ buildSubmitAction(asString(footer["label"]), footer["$action"]),
258
+ );
259
+ return actions.length > 0 ? { type: "ActionSet", actions } : undefined;
260
+ }
261
+
262
+ const ALERT_STYLE_MAP: Record<string, TeamsContainerStyle> = {
263
+ info: "accent",
264
+ success: "good",
265
+ warning: "warning",
266
+ danger: "attention",
267
+ };
268
+
269
+ const stringifyCell = (value: unknown): string => {
270
+ if (typeof value === "string") return value;
271
+ if (typeof value === "number" || typeof value === "boolean")
272
+ return String(value);
273
+ return "";
274
+ };
275
+
276
+ function convertTable(
277
+ props: Readonly<Record<string, unknown>>,
278
+ context: ConversionContext,
279
+ ): TeamsTable {
280
+ const { items: rawColumns, truncated: columnsTruncated } = clampArray(
281
+ props["columns"],
282
+ TABLE_COLUMN_CAP,
283
+ );
284
+ const { items: rawRows, truncated: rowsTruncated } = clampArray(
285
+ props["rows"],
286
+ TABLE_ROW_CAP,
287
+ );
288
+ if (columnsTruncated) {
289
+ warn(
290
+ context,
291
+ "clamped",
292
+ "Table",
293
+ `columns were clamped to ${TABLE_COLUMN_CAP} entries.`,
294
+ );
295
+ }
296
+ if (rowsTruncated) {
297
+ warn(
298
+ context,
299
+ "clamped",
300
+ "Table",
301
+ `rows were clamped to ${TABLE_ROW_CAP} entries.`,
302
+ );
303
+ }
304
+
305
+ const hasColumns = rawColumns.length > 0;
306
+ const columns: TeamsTableColumnDefinition[] = rawColumns.map(() => ({
307
+ width: 1 as const,
308
+ }));
309
+ const headerRow: TeamsTableRow | undefined = hasColumns
310
+ ? {
311
+ type: "TableRow",
312
+ cells: rawColumns.map((column) => ({
313
+ type: "TableCell" as const,
314
+ items: [textBlock(isRecord(column) ? asString(column["label"]) : "")],
315
+ })),
316
+ }
317
+ : undefined;
318
+ const dataRows: TeamsTableRow[] = rawRows.map((row) => ({
319
+ type: "TableRow",
320
+ cells: (Array.isArray(row) ? row.slice(0, TABLE_COLUMN_CAP) : []).map(
321
+ (cell) => ({
322
+ type: "TableCell" as const,
323
+ items: [textBlock(stringifyCell(cell))],
324
+ }),
325
+ ),
326
+ }));
327
+
328
+ return {
329
+ type: "Table",
330
+ firstRowAsHeaders: hasColumns,
331
+ columns,
332
+ rows: headerRow !== undefined ? [headerRow, ...dataRows] : dataRows,
333
+ };
334
+ }
335
+
336
+ function convertListViewItem(
337
+ element: NormalizedUIElement,
338
+ context: ConversionContext,
339
+ depth: number,
340
+ ): TeamsContainer {
341
+ const items = convertSequence(element.children, context, depth + 1);
342
+ const selectAction = isRecord(element.action)
343
+ ? buildSubmitAction("Open", element.action)
344
+ : undefined;
345
+ return {
346
+ type: "Container",
347
+ items,
348
+ ...(selectAction !== undefined ? { selectAction } : {}),
349
+ };
350
+ }
351
+
352
+ export function convertElement(
353
+ element: NormalizedUIElement,
354
+ context: ConversionContext,
355
+ depth: number,
356
+ ): TeamsCardElement[] {
357
+ const { props } = element;
358
+ switch (element.type) {
359
+ case "Header":
360
+ return [
361
+ textBlock(asString(props["text"]), {
362
+ size: "large",
363
+ weight: "bolder",
364
+ style: "heading",
365
+ }),
366
+ ];
367
+ case "Text": {
368
+ const mappedSize =
369
+ typeof props["size"] === "string"
370
+ ? TEXT_SIZE_MAP[props["size"]]
371
+ : undefined;
372
+ return [
373
+ textBlock(asString(props["value"]), {
374
+ ...(mappedSize !== undefined ? { size: mappedSize } : {}),
375
+ ...(isBolderWeight(props["weight"]) ? { weight: "bolder" } : {}),
376
+ ...(isSubtleColor(props["color"]) ? { isSubtle: true } : {}),
377
+ }),
378
+ ];
379
+ }
380
+ case "Caption":
381
+ return [
382
+ textBlock(asString(props["value"]), { size: "small", isSubtle: true }),
383
+ ];
384
+ case "Badge":
385
+ return [
386
+ textBlock(asString(props["value"]), { size: "small", isSubtle: true }),
387
+ ];
388
+ case "Markdown":
389
+ // Teams renders a strict subset of markdown (no headings, tables, images,
390
+ // preformatted blocks, or blockquotes); unsupported syntax renders literally.
391
+ return [textBlock(asString(props["value"]))];
392
+ case "Fact":
393
+ return [convertFacts([element])];
394
+ case "Image": {
395
+ const size = props["size"];
396
+ let mappedSize: "small" | "medium" | "large" | undefined;
397
+ if (size === "sm") mappedSize = "small";
398
+ else if (size === "md") mappedSize = "medium";
399
+ else if (size === "lg") mappedSize = "large";
400
+ else if (typeof size === "number") {
401
+ warn(context, "dropped", "Image", "A numeric size was dropped.");
402
+ }
403
+ return [
404
+ {
405
+ type: "Image",
406
+ url: asString(props["src"]),
407
+ altText: asString(props["alt"]),
408
+ ...(mappedSize !== undefined ? { size: mappedSize } : {}),
409
+ },
410
+ ];
411
+ }
412
+ case "Divider":
413
+ case "Spacer":
414
+ case "Icon":
415
+ return [];
416
+ case "Button":
417
+ return [convertButtons([element], context)];
418
+ case "Select": {
419
+ const name = asString(props["name"]);
420
+ const placeholder = asString(props["placeholder"]);
421
+ const label = asString(props["label"]);
422
+ const input: TeamsCardElement = {
423
+ type: "Input.ChoiceSet",
424
+ id: reservedSafeId(name || "select", "Select", context),
425
+ style: "compact",
426
+ choices: choicesFrom(props["options"], "Select", context),
427
+ ...(placeholder ? { placeholder } : {}),
428
+ ...(label ? { label } : {}),
429
+ };
430
+ return withCompanionSubmit(element, input, context);
431
+ }
432
+ case "RadioGroup": {
433
+ const name = asString(props["name"]);
434
+ const label = asString(props["label"]);
435
+ const defaultValue = props["defaultValue"];
436
+ const input: TeamsCardElement = {
437
+ type: "Input.ChoiceSet",
438
+ id: reservedSafeId(name || "radiogroup", "RadioGroup", context),
439
+ style: "expanded",
440
+ choices: choicesFrom(props["options"], "RadioGroup", context),
441
+ ...(typeof defaultValue === "string" && defaultValue
442
+ ? { value: defaultValue }
443
+ : {}),
444
+ ...(label ? { label } : {}),
445
+ };
446
+ return withCompanionSubmit(element, input, context);
447
+ }
448
+ case "Checkbox": {
449
+ const name = asString(props["name"]);
450
+ const label = asString(props["label"]);
451
+ const input: TeamsCardElement = {
452
+ type: "Input.Toggle",
453
+ id: reservedSafeId(name || label || "checkbox", "Checkbox", context),
454
+ title: label,
455
+ value: props["defaultChecked"] === true ? "true" : "false",
456
+ valueOn: "true",
457
+ valueOff: "false",
458
+ };
459
+ return withCompanionSubmit(element, input, context);
460
+ }
461
+ case "Input": {
462
+ const name = asString(props["name"]);
463
+ const label = asString(props["label"]);
464
+ const placeholder = asString(props["placeholder"]);
465
+ const input: TeamsCardElement = {
466
+ type: "Input.Text",
467
+ id: reservedSafeId(name || "input", "Input", context),
468
+ ...(label ? { label } : {}),
469
+ ...(placeholder ? { placeholder } : {}),
470
+ ...(props["multiline"] === true ? { isMultiline: true } : {}),
471
+ };
472
+ return withCompanionSubmit(element, input, context);
473
+ }
474
+ case "DatePicker": {
475
+ const name = asString(props["name"]);
476
+ const label = asString(props["label"]);
477
+ const rawValue = props["value"];
478
+ const value =
479
+ typeof rawValue === "string" && DATE_PATTERN.test(rawValue)
480
+ ? rawValue
481
+ : undefined;
482
+ const rawMin = props["min"];
483
+ const min =
484
+ typeof rawMin === "string" && DATE_PATTERN.test(rawMin)
485
+ ? rawMin
486
+ : undefined;
487
+ const rawMax = props["max"];
488
+ const max =
489
+ typeof rawMax === "string" && DATE_PATTERN.test(rawMax)
490
+ ? rawMax
491
+ : undefined;
492
+ const input: TeamsCardElement = {
493
+ type: "Input.Date",
494
+ id: reservedSafeId(name || "datepicker", "DatePicker", context),
495
+ ...(label ? { label } : {}),
496
+ ...(value !== undefined ? { value } : {}),
497
+ ...(min !== undefined ? { min } : {}),
498
+ ...(max !== undefined ? { max } : {}),
499
+ };
500
+ return withCompanionSubmit(element, input, context);
501
+ }
502
+ case "Form":
503
+ return [
504
+ ...convertSequence(element.children, context, depth + 1),
505
+ {
506
+ type: "ActionSet",
507
+ actions: [buildSubmitAction("Submit", element.action)],
508
+ },
509
+ ];
510
+ case "Card": {
511
+ const titleText = asString(props["title"]);
512
+ const items: TeamsCardElement[] = [];
513
+ if (titleText) {
514
+ items.push(
515
+ textBlock(titleText, {
516
+ size: "large",
517
+ weight: "bolder",
518
+ style: "heading",
519
+ }),
520
+ );
521
+ }
522
+ items.push(...convertSequence(element.children, context, depth + 1));
523
+ const container: TeamsContainer = {
524
+ type: "Container",
525
+ style: "default",
526
+ items,
527
+ };
528
+ const actionSet = cardFooterActionSet(props);
529
+ return actionSet !== undefined ? [container, actionSet] : [container];
530
+ }
531
+ case "Col":
532
+ case "Box":
533
+ return [
534
+ {
535
+ type: "Container",
536
+ items: convertSequence(element.children, context, depth + 1),
537
+ },
538
+ ];
539
+ case "Row": {
540
+ const children = normalizedList(element.children);
541
+ if (children.length > 3) {
542
+ warn(context, "clamped", "Row", "Teams recommends at most 3 columns.");
543
+ }
544
+ const columns: TeamsColumn[] = children.map((child) => ({
545
+ type: "Column",
546
+ width: "auto",
547
+ items: convertSequence(child, context, depth + 1),
548
+ }));
549
+ return [{ type: "ColumnSet", columns }];
550
+ }
551
+ case "ListView": {
552
+ const containers: TeamsContainer[] = [];
553
+ for (const child of normalizedList(element.children)) {
554
+ if (!isElement(child)) continue;
555
+ if (child.type === "ListViewItem") {
556
+ containers.push(convertListViewItem(child, context, depth + 1));
557
+ } else {
558
+ convertElement(child, context, depth + 1);
559
+ }
560
+ }
561
+ return containers.map((container, index) =>
562
+ index > 0 ? { ...container, separator: true } : container,
563
+ );
564
+ }
565
+ case "ListViewItem":
566
+ return [convertListViewItem(element, context, depth)];
567
+ case "Table":
568
+ return [convertTable(props, context)];
569
+ case "Alert": {
570
+ const tone = props["tone"];
571
+ const style =
572
+ typeof tone === "string"
573
+ ? (ALERT_STYLE_MAP[tone] ?? "default")
574
+ : "default";
575
+ const titleText = asString(props["title"]);
576
+ const description = asString(props["description"]);
577
+ const items: TeamsCardElement[] = [];
578
+ if (titleText) items.push(textBlock(titleText, { weight: "bolder" }));
579
+ if (description) items.push(textBlock(description));
580
+ return [{ type: "Container", style, items }];
581
+ }
582
+ case "Carousel": {
583
+ warn(
584
+ context,
585
+ "fallback",
586
+ "Carousel",
587
+ "A carousel was rendered as sequential cards because it is not at the root.",
588
+ );
589
+ const cards = normalizedList(element.children).filter(
590
+ (child): child is NormalizedUIElement =>
591
+ isElement(child) && child.type === "Card",
592
+ );
593
+ return cards.flatMap((card) => convertElement(card, context, depth + 1));
594
+ }
595
+ case "Chart":
596
+ warn(
597
+ context,
598
+ "dropped",
599
+ "Chart",
600
+ "Chart was replaced by a Teams omission note.",
601
+ );
602
+ return [
603
+ textBlock("Chart omitted on Teams.", { isSubtle: true, size: "small" }),
604
+ ];
605
+ default:
606
+ warn(
607
+ context,
608
+ "dropped",
609
+ element.type,
610
+ "Unknown component type was dropped.",
611
+ );
612
+ return [];
613
+ }
614
+ }
615
+
616
+ /**
617
+ * Walks a normalized sequence into card body elements. Nested arrays are
618
+ * flattened into one flat sibling sequence first (via {@link normalizedList}),
619
+ * so a `Divider`/`Fact`/`Button` run merges correctly even when the model
620
+ * wrapped some of its siblings in their own array. Consecutive `Fact`
621
+ * siblings merge into one FactSet and consecutive `Button` siblings merge
622
+ * into one ActionSet; a `Divider`/`Spacer` emits nothing itself but sets
623
+ * `separator`/`spacing` on the next emitted sibling (a trailing or orphan
624
+ * one is dropped silently, since there is no sibling left to carry it).
625
+ * `depth` advances by exactly one per element-nesting level: this call does
626
+ * not itself add a level, since flattening a sibling array or converting a
627
+ * sibling element is not a descent; only a container's own recursive
628
+ * `convertSequence(children, depth + 1)` call is.
629
+ */
630
+ export function convertSequence(
631
+ node: NormalizedUINode | undefined,
632
+ context: ConversionContext,
633
+ depth: number,
634
+ ): TeamsCardElement[] {
635
+ if (node === undefined || node === null || depth > MAX_TRAVERSAL_DEPTH) {
636
+ return [];
637
+ }
638
+ const nodes = normalizedList(node);
639
+ const items: TeamsCardElement[] = [];
640
+ let pendingSeparator = false;
641
+ let pendingSpacing = false;
642
+
643
+ const emit = (converted: TeamsCardElement[]) => {
644
+ const [first, ...rest] = converted;
645
+ if (first === undefined) return;
646
+ items.push(applyPending(first, pendingSeparator, pendingSpacing), ...rest);
647
+ pendingSeparator = false;
648
+ pendingSpacing = false;
649
+ };
650
+
651
+ for (let index = 0; index < nodes.length;) {
652
+ const current = nodes[index];
653
+ if (current === undefined || current === null) {
654
+ index += 1;
655
+ continue;
656
+ }
657
+ if (isElement(current) && current.type === "Divider") {
658
+ pendingSeparator = true;
659
+ index += 1;
660
+ continue;
661
+ }
662
+ if (isElement(current) && current.type === "Spacer") {
663
+ pendingSpacing = true;
664
+ index += 1;
665
+ continue;
666
+ }
667
+ if (isElement(current) && current.type === "Fact") {
668
+ const facts: NormalizedUIElement[] = [];
669
+ while (index < nodes.length) {
670
+ const candidate = nodes[index];
671
+ if (!candidate || !isElement(candidate) || candidate.type !== "Fact") {
672
+ break;
673
+ }
674
+ facts.push(candidate);
675
+ index += 1;
676
+ }
677
+ emit([convertFacts(facts)]);
678
+ continue;
679
+ }
680
+ if (isElement(current) && current.type === "Button") {
681
+ const buttons: NormalizedUIElement[] = [];
682
+ while (index < nodes.length) {
683
+ const candidate = nodes[index];
684
+ if (
685
+ !candidate ||
686
+ !isElement(candidate) ||
687
+ candidate.type !== "Button"
688
+ ) {
689
+ break;
690
+ }
691
+ buttons.push(candidate);
692
+ index += 1;
693
+ }
694
+ emit([convertButtons(buttons, context)]);
695
+ continue;
696
+ }
697
+ if (typeof current === "string" || typeof current === "number") {
698
+ emit([textBlock(String(current))]);
699
+ } else {
700
+ emit(convertElement(current as NormalizedUIElement, context, depth));
701
+ }
702
+ index += 1;
703
+ }
704
+ return items;
705
+ }
706
+
707
+ /** Builds a full card (schema envelope plus body) from an already-normalized root, checking the soft payload budget. */
708
+ export function convertRootToCard(
709
+ root: NormalizedUINode | readonly NormalizedUINode[],
710
+ context: ConversionContext,
711
+ ): TeamsAdaptiveCard {
712
+ context.usedInputIds = new Set();
713
+ const body = convertSequence(root, context, 0);
714
+ const card = buildCard(body);
715
+ const size = utf8ByteLength(JSON.stringify(card));
716
+ if (size > PAYLOAD_SOFT_CAP) {
717
+ warn(
718
+ context,
719
+ "clamped",
720
+ "Root",
721
+ `the card is ${size} bytes, exceeding Teams' 100 KB bot message limit.`,
722
+ );
723
+ }
724
+ return card;
725
+ }
726
+
727
+ /**
728
+ * Converts a generative-ui tree into a Microsoft Teams Adaptive Card and
729
+ * non-fatal downgrade warnings. Sizes, weights, and colors map to Adaptive
730
+ * Card's semantic enums rather than raw values. An Input/Select/RadioGroup/
731
+ * Checkbox/DatePicker whose id would be the reserved {@link RESERVED_INPUT_ID}
732
+ * is renamed with a warning (see `decodeSubmitData`). Never throws: an
733
+ * unknown `$type` is skipped with a "dropped" warning, and a malformed input
734
+ * resolves to an empty card plus a "dropped" warning instead of throwing.
735
+ */
736
+ export function toAdaptiveCard(
737
+ node: unknown,
738
+ _options?: ToAdaptiveCardOptions,
739
+ ): AdaptiveCardResult {
740
+ const context: ConversionContext = { warnings: [], usedInputIds: new Set() };
741
+ try {
742
+ const bounded = boundSpec(node, (reason) =>
743
+ warn(context, "clamped", "Root", clampReasonDetail(reason)),
744
+ );
745
+ const { root } = normalizeSpec(bounded as never);
746
+ return {
747
+ card: convertRootToCard(root, context),
748
+ warnings: context.warnings,
749
+ };
750
+ } catch {
751
+ return {
752
+ card: buildCard([]),
753
+ warnings: [
754
+ {
755
+ code: "dropped",
756
+ component: "Root",
757
+ detail: "Input could not be converted.",
758
+ },
759
+ ],
760
+ };
761
+ }
762
+ }