@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.
Files changed (179) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +0 -1
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.server.d.ts +0 -1
  4. package/dist/JSONGenerativeUI.server.d.ts.map +1 -1
  5. package/dist/JSONGenerativeUI.shared.d.ts +0 -1
  6. package/dist/JSONGenerativeUI.shared.d.ts.map +1 -1
  7. package/dist/actionRegistry.d.ts +2 -11
  8. package/dist/actionRegistry.d.ts.map +1 -1
  9. package/dist/actionRegistry.js.map +1 -1
  10. package/dist/buildPresentParameters.d.ts +5 -5
  11. package/dist/buildPresentParameters.d.ts.map +1 -1
  12. package/dist/buildPresentParameters.js +10 -5
  13. package/dist/buildPresentParameters.js.map +1 -1
  14. package/dist/constants.d.ts.map +1 -1
  15. package/dist/defineGenerativeComponents.d.ts +0 -1
  16. package/dist/defineGenerativeComponents.d.ts.map +1 -1
  17. package/dist/generativeUIToJSX.d.ts +15 -10
  18. package/dist/generativeUIToJSX.d.ts.map +1 -1
  19. package/dist/generativeUIToJSX.js +47 -17
  20. package/dist/generativeUIToJSX.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +2 -2
  23. package/dist/index.server.d.ts +4 -3
  24. package/dist/index.server.js +2 -2
  25. package/dist/index.shared.d.ts +3 -3
  26. package/dist/index.shared.js +2 -2
  27. package/dist/ir.d.ts +6 -6
  28. package/dist/ir.d.ts.map +1 -1
  29. package/dist/ir.js +27 -1
  30. package/dist/ir.js.map +1 -1
  31. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
  32. package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
  33. package/dist/renderGenerativeUI.d.ts +0 -1
  34. package/dist/renderGenerativeUI.d.ts.map +1 -1
  35. package/dist/slack/constants.d.ts +111 -0
  36. package/dist/slack/constants.d.ts.map +1 -0
  37. package/dist/slack/constants.js +168 -0
  38. package/dist/slack/constants.js.map +1 -0
  39. package/dist/slack/decodeBlockAction.d.ts +11 -0
  40. package/dist/slack/decodeBlockAction.d.ts.map +1 -0
  41. package/dist/slack/decodeBlockAction.js +40 -0
  42. package/dist/slack/decodeBlockAction.js.map +1 -0
  43. package/dist/slack/fromSlackBlocks.d.ts +30 -0
  44. package/dist/slack/fromSlackBlocks.d.ts.map +1 -0
  45. package/dist/slack/fromSlackBlocks.js +447 -0
  46. package/dist/slack/fromSlackBlocks.js.map +1 -0
  47. package/dist/slack/toSlackBlocks.d.ts +7 -0
  48. package/dist/slack/toSlackBlocks.d.ts.map +1 -0
  49. package/dist/slack/toSlackBlocks.js +649 -0
  50. package/dist/slack/toSlackBlocks.js.map +1 -0
  51. package/dist/slack/types.d.ts +178 -0
  52. package/dist/slack/types.d.ts.map +1 -0
  53. package/dist/slack/types.js +0 -0
  54. package/dist/slack.d.ts +5 -0
  55. package/dist/slack.js +4 -0
  56. package/dist/teams/boundSpec.d.ts +20 -0
  57. package/dist/teams/boundSpec.d.ts.map +1 -0
  58. package/dist/teams/boundSpec.js +67 -0
  59. package/dist/teams/boundSpec.js.map +1 -0
  60. package/dist/teams/constants.d.ts +57 -0
  61. package/dist/teams/constants.d.ts.map +1 -0
  62. package/dist/teams/constants.js +92 -0
  63. package/dist/teams/constants.js.map +1 -0
  64. package/dist/teams/decodeSubmitData.d.ts +25 -0
  65. package/dist/teams/decodeSubmitData.d.ts.map +1 -0
  66. package/dist/teams/decodeSubmitData.js +44 -0
  67. package/dist/teams/decodeSubmitData.js.map +1 -0
  68. package/dist/teams/toAdaptiveCard.d.ts +39 -0
  69. package/dist/teams/toAdaptiveCard.d.ts.map +1 -0
  70. package/dist/teams/toAdaptiveCard.js +496 -0
  71. package/dist/teams/toAdaptiveCard.js.map +1 -0
  72. package/dist/teams/toTeamsAttachments.d.ts +13 -0
  73. package/dist/teams/toTeamsAttachments.d.ts.map +1 -0
  74. package/dist/teams/toTeamsAttachments.js +95 -0
  75. package/dist/teams/toTeamsAttachments.js.map +1 -0
  76. package/dist/teams/types.d.ts +196 -0
  77. package/dist/teams/types.d.ts.map +1 -0
  78. package/dist/teams/types.js +0 -0
  79. package/dist/teams.d.ts +5 -0
  80. package/dist/teams.js +4 -0
  81. package/dist/types.d.ts +7 -4
  82. package/dist/types.d.ts.map +1 -1
  83. package/dist/vocabulary/alert.d.ts +6 -12
  84. package/dist/vocabulary/alert.d.ts.map +1 -1
  85. package/dist/vocabulary/alert.js +13 -4
  86. package/dist/vocabulary/alert.js.map +1 -1
  87. package/dist/vocabulary/collectFormValues.d.ts +23 -0
  88. package/dist/vocabulary/collectFormValues.d.ts.map +1 -0
  89. package/dist/vocabulary/collectFormValues.js +32 -0
  90. package/dist/vocabulary/collectFormValues.js.map +1 -0
  91. package/dist/vocabulary/data.d.ts +16 -17
  92. package/dist/vocabulary/data.d.ts.map +1 -1
  93. package/dist/vocabulary/data.js +244 -13
  94. package/dist/vocabulary/data.js.map +1 -1
  95. package/dist/vocabulary/dispatch.d.ts +11 -0
  96. package/dist/vocabulary/dispatch.d.ts.map +1 -0
  97. package/dist/vocabulary/dispatch.js +21 -0
  98. package/dist/vocabulary/dispatch.js.map +1 -0
  99. package/dist/vocabulary/fact.d.ts +1 -6
  100. package/dist/vocabulary/fact.d.ts.map +1 -1
  101. package/dist/vocabulary/form.d.ts +14 -0
  102. package/dist/vocabulary/form.d.ts.map +1 -0
  103. package/dist/vocabulary/form.js +23 -0
  104. package/dist/vocabulary/form.js.map +1 -0
  105. package/dist/vocabulary/icon.d.ts +44 -0
  106. package/dist/vocabulary/icon.d.ts.map +1 -0
  107. package/dist/vocabulary/icon.js +272 -0
  108. package/dist/vocabulary/icon.js.map +1 -0
  109. package/dist/vocabulary/index.d.ts +0 -1
  110. package/dist/vocabulary/index.d.ts.map +1 -1
  111. package/dist/vocabulary/index.js +7 -1
  112. package/dist/vocabulary/index.js.map +1 -1
  113. package/dist/vocabulary/interactive.d.ts +45 -33
  114. package/dist/vocabulary/interactive.d.ts.map +1 -1
  115. package/dist/vocabulary/interactive.js +78 -29
  116. package/dist/vocabulary/interactive.js.map +1 -1
  117. package/dist/vocabulary/layout.d.ts +27 -23
  118. package/dist/vocabulary/layout.d.ts.map +1 -1
  119. package/dist/vocabulary/layout.js +78 -14
  120. package/dist/vocabulary/layout.js.map +1 -1
  121. package/dist/vocabulary/list.d.ts +17 -0
  122. package/dist/vocabulary/list.d.ts.map +1 -0
  123. package/dist/vocabulary/list.js +48 -0
  124. package/dist/vocabulary/list.js.map +1 -0
  125. package/dist/vocabulary/media.d.ts +3 -9
  126. package/dist/vocabulary/media.d.ts.map +1 -1
  127. package/dist/vocabulary/media.js +17 -7
  128. package/dist/vocabulary/media.js.map +1 -1
  129. package/dist/vocabulary/text.d.ts +3 -17
  130. package/dist/vocabulary/text.d.ts.map +1 -1
  131. package/package.json +13 -5
  132. package/src/actionRegistry.ts +2 -10
  133. package/src/buildPresentParameters.ts +15 -9
  134. package/src/generativeUIToJSX.test.ts +186 -0
  135. package/src/generativeUIToJSX.ts +112 -21
  136. package/src/index.shared.ts +3 -0
  137. package/src/ir.ts +31 -5
  138. package/src/renderGenerativeUI.test.tsx +16 -2
  139. package/src/slack/constants.ts +217 -0
  140. package/src/slack/decodeBlockAction.test.ts +135 -0
  141. package/src/slack/decodeBlockAction.ts +66 -0
  142. package/src/slack/fromSlackBlocks.test.ts +1145 -0
  143. package/src/slack/fromSlackBlocks.ts +709 -0
  144. package/src/slack/toSlackBlocks.test.ts +1936 -0
  145. package/src/slack/toSlackBlocks.ts +1319 -0
  146. package/src/slack/types.ts +230 -0
  147. package/src/slack.ts +37 -0
  148. package/src/teams/boundSpec.ts +102 -0
  149. package/src/teams/constants.ts +127 -0
  150. package/src/teams/decodeSubmitData.test.ts +148 -0
  151. package/src/teams/decodeSubmitData.ts +57 -0
  152. package/src/teams/toAdaptiveCard.test.ts +1410 -0
  153. package/src/teams/toAdaptiveCard.ts +762 -0
  154. package/src/teams/toTeamsAttachments.test.ts +29 -0
  155. package/src/teams/toTeamsAttachments.ts +137 -0
  156. package/src/teams/types.ts +237 -0
  157. package/src/teams.ts +34 -0
  158. package/src/vocabulary/alert.test.tsx +72 -2
  159. package/src/vocabulary/alert.tsx +24 -5
  160. package/src/vocabulary/collectFormValues.test.ts +127 -0
  161. package/src/vocabulary/collectFormValues.ts +58 -0
  162. package/src/vocabulary/data.test.tsx +503 -3
  163. package/src/vocabulary/data.tsx +426 -18
  164. package/src/vocabulary/dispatch.ts +22 -0
  165. package/src/vocabulary/form.test.tsx +112 -0
  166. package/src/vocabulary/form.tsx +34 -0
  167. package/src/vocabulary/icon.test.tsx +64 -0
  168. package/src/vocabulary/icon.tsx +174 -0
  169. package/src/vocabulary/index.test.ts +7 -0
  170. package/src/vocabulary/index.ts +6 -0
  171. package/src/vocabulary/interactive.dispatch.test.tsx +243 -1
  172. package/src/vocabulary/interactive.test.tsx +143 -3
  173. package/src/vocabulary/interactive.tsx +134 -30
  174. package/src/vocabulary/layout.test.tsx +231 -3
  175. package/src/vocabulary/layout.tsx +168 -15
  176. package/src/vocabulary/list.test.tsx +195 -0
  177. package/src/vocabulary/list.tsx +48 -0
  178. package/src/vocabulary/media.test.tsx +36 -4
  179. package/src/vocabulary/media.tsx +27 -8
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { toTeamsAttachments } from "./toTeamsAttachments";
3
+ import { PAYLOAD_SOFT_CAP } from "./constants";
4
+
5
+ describe("toTeamsAttachments payload budget", () => {
6
+ it("warns on the aggregate carousel size even when no single card exceeds the soft cap", () => {
7
+ const value = "x".repeat(45000);
8
+ const { attachments, warnings } = toTeamsAttachments({
9
+ $type: "Carousel",
10
+ children: [
11
+ { $type: "Card", title: "A", children: { $type: "Markdown", value } },
12
+ { $type: "Card", title: "B", children: { $type: "Markdown", value } },
13
+ ],
14
+ });
15
+ for (const attachment of attachments) {
16
+ expect(JSON.stringify(attachment.content).length).toBeLessThanOrEqual(
17
+ PAYLOAD_SOFT_CAP,
18
+ );
19
+ }
20
+ expect(
21
+ warnings.some(
22
+ (warning) => warning.code === "clamped" && warning.component === "Root",
23
+ ),
24
+ ).toBe(false);
25
+ expect(warnings).toContainEqual(
26
+ expect.objectContaining({ code: "clamped", component: "Carousel" }),
27
+ );
28
+ });
29
+ });
@@ -0,0 +1,137 @@
1
+ import {
2
+ normalizeSpec,
3
+ type NormalizedUIElement,
4
+ type NormalizedUINode,
5
+ } from "../ir";
6
+ import { boundSpec } from "./boundSpec";
7
+ import {
8
+ CAROUSEL_ATTACHMENT_CAP,
9
+ PAYLOAD_SOFT_CAP,
10
+ buildAttachment,
11
+ clampReasonDetail,
12
+ utf8ByteLength,
13
+ } from "./constants";
14
+ import {
15
+ convertElement,
16
+ convertRootToCard,
17
+ type ConversionContext,
18
+ } from "./toAdaptiveCard";
19
+ import type {
20
+ TeamsAttachmentsResult,
21
+ TeamsCardAttachment,
22
+ TeamsConversionWarning,
23
+ ToAdaptiveCardOptions,
24
+ } from "./types";
25
+
26
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
27
+ typeof value === "object" && value !== null && !Array.isArray(value);
28
+
29
+ const isElement = (node: NormalizedUINode): node is NormalizedUIElement =>
30
+ isRecord(node);
31
+
32
+ const normalizedList = (
33
+ node: NormalizedUINode | undefined,
34
+ ): NormalizedUINode[] => {
35
+ if (node === undefined || node === null) return [];
36
+ if (!Array.isArray(node)) return [node];
37
+ return node.flatMap((child) => normalizedList(child));
38
+ };
39
+
40
+ const clampArray = <T>(
41
+ value: readonly T[],
42
+ cap: number,
43
+ ): { readonly items: T[]; readonly truncated: boolean } => ({
44
+ items: value.slice(0, cap),
45
+ truncated: value.length > cap,
46
+ });
47
+
48
+ /**
49
+ * Recognizes a root that is a single `Carousel` element, returning its `Card`
50
+ * children. Every child is routed through {@link convertElement} first (its
51
+ * result is discarded) so an unknown or unsupported child still produces its
52
+ * usual "dropped" warning instead of being filtered out silently; non-card
53
+ * children are then dropped. `undefined` means the root is not exclusively a
54
+ * root-level carousel, so the caller falls back to converting the whole tree
55
+ * as one card.
56
+ */
57
+ function rootCarouselCards(
58
+ root: NormalizedUINode | readonly NormalizedUINode[],
59
+ context: ConversionContext,
60
+ ): NormalizedUIElement[] | undefined {
61
+ const element = Array.isArray(root) && root.length === 1 ? root[0] : root;
62
+ if (!isElement(element) || element.type !== "Carousel") return undefined;
63
+ const cards: NormalizedUIElement[] = [];
64
+ for (const child of normalizedList(element.children)) {
65
+ if (!isElement(child)) continue;
66
+ if (child.type === "Card") {
67
+ cards.push(child);
68
+ } else {
69
+ convertElement(child, context, 1);
70
+ }
71
+ }
72
+ return cards;
73
+ }
74
+
75
+ /**
76
+ * Converts a generative-ui tree into Teams bot-framework message attachments.
77
+ * A root-level `Carousel` becomes one attachment per `Card` child (clamped to
78
+ * {@link CAROUSEL_ATTACHMENT_CAP}) with `attachmentLayout: "carousel"`;
79
+ * anything else converts through {@link toAdaptiveCard}'s engine into a
80
+ * single attachment. Never throws.
81
+ */
82
+ export function toTeamsAttachments(
83
+ node: unknown,
84
+ _options?: ToAdaptiveCardOptions,
85
+ ): TeamsAttachmentsResult {
86
+ const warnings: TeamsConversionWarning[] = [];
87
+ const context: ConversionContext = { warnings, usedInputIds: new Set() };
88
+ try {
89
+ const bounded = boundSpec(node, (reason) =>
90
+ warnings.push({
91
+ code: "clamped",
92
+ component: "Root",
93
+ detail: clampReasonDetail(reason),
94
+ }),
95
+ );
96
+ const { root } = normalizeSpec(bounded as never);
97
+ const carouselCards = rootCarouselCards(root, context);
98
+ if (carouselCards !== undefined) {
99
+ const { items, truncated } = clampArray(
100
+ carouselCards,
101
+ CAROUSEL_ATTACHMENT_CAP,
102
+ );
103
+ if (truncated) {
104
+ warnings.push({
105
+ code: "clamped",
106
+ component: "Carousel",
107
+ detail: `cards were clamped to ${CAROUSEL_ATTACHMENT_CAP} entries.`,
108
+ });
109
+ }
110
+ const attachments: TeamsCardAttachment[] = items.map((child) =>
111
+ buildAttachment(convertRootToCard(child, context)),
112
+ );
113
+ const size = utf8ByteLength(JSON.stringify(attachments));
114
+ if (size > PAYLOAD_SOFT_CAP) {
115
+ warnings.push({
116
+ code: "clamped",
117
+ component: "Carousel",
118
+ detail: `the carousel attachments total ${size} bytes, exceeding Teams' 100 KB bot message limit.`,
119
+ });
120
+ }
121
+ return { attachments, attachmentLayout: "carousel", warnings };
122
+ }
123
+ const attachment = buildAttachment(convertRootToCard(root, context));
124
+ return { attachments: [attachment], warnings };
125
+ } catch {
126
+ return {
127
+ attachments: [],
128
+ warnings: [
129
+ {
130
+ code: "dropped",
131
+ component: "Root",
132
+ detail: "Input could not be converted.",
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ }
@@ -0,0 +1,237 @@
1
+ /** A TextBlock size, from Adaptive Card's closed size enum. */
2
+ export type TeamsTextSize = "small" | "medium" | "large" | "extraLarge";
3
+
4
+ /** A Container's style, driving its accent color in Teams' renderer. */
5
+ export type TeamsContainerStyle =
6
+ | "default"
7
+ | "emphasis"
8
+ | "good"
9
+ | "attention"
10
+ | "warning"
11
+ | "accent";
12
+
13
+ /** A run of wrapped text. Every TextBlock we emit sets `wrap: true`. */
14
+ export interface TeamsTextBlock {
15
+ readonly type: "TextBlock";
16
+ readonly text: string;
17
+ readonly wrap: true;
18
+ readonly size?: TeamsTextSize;
19
+ readonly weight?: "bolder";
20
+ readonly style?: "heading";
21
+ readonly isSubtle?: true;
22
+ readonly separator?: true;
23
+ readonly spacing?: "large";
24
+ }
25
+
26
+ /** An image element. */
27
+ export interface TeamsImage {
28
+ readonly type: "Image";
29
+ readonly url: string;
30
+ readonly altText?: string;
31
+ readonly size?: "small" | "medium" | "large";
32
+ readonly separator?: true;
33
+ readonly spacing?: "large";
34
+ }
35
+
36
+ /** One label/value entry inside a {@link TeamsFactSet}. */
37
+ export interface TeamsFact {
38
+ readonly title: string;
39
+ readonly value: string;
40
+ }
41
+
42
+ /** A set of label/value facts, rendered as an aligned two-column list. */
43
+ export interface TeamsFactSet {
44
+ readonly type: "FactSet";
45
+ readonly facts: readonly TeamsFact[];
46
+ readonly separator?: true;
47
+ readonly spacing?: "large";
48
+ }
49
+
50
+ /** The `aui` envelope nested inside every {@link TeamsSubmitAction}'s `data`. */
51
+ export interface TeamsSubmitData {
52
+ readonly aui: {
53
+ readonly type: string;
54
+ readonly payload?: Record<string, unknown>;
55
+ };
56
+ }
57
+
58
+ /**
59
+ * A submit action. `data` carries the resume payload under an `aui` key so
60
+ * {@link decodeSubmitData} can invert it out of the merged `activity.value`
61
+ * a Teams bot receives, alongside whatever other named inputs shared the card.
62
+ */
63
+ export interface TeamsSubmitAction {
64
+ readonly type: "Action.Submit";
65
+ readonly title: string;
66
+ readonly data: TeamsSubmitData;
67
+ readonly mode?: "secondary";
68
+ }
69
+
70
+ /** The action shape accepted at the card's top-level `actions` field. */
71
+ export type TeamsCardAction = TeamsSubmitAction;
72
+
73
+ /** A bar of submit actions, rendered inline in the body flow. */
74
+ export interface TeamsActionSet {
75
+ readonly type: "ActionSet";
76
+ readonly actions: readonly TeamsSubmitAction[];
77
+ readonly separator?: true;
78
+ readonly spacing?: "large";
79
+ }
80
+
81
+ /** A bordered grouping container. */
82
+ export interface TeamsContainer {
83
+ readonly type: "Container";
84
+ readonly style?: TeamsContainerStyle;
85
+ readonly items: readonly TeamsCardElement[];
86
+ readonly selectAction?: TeamsSubmitAction;
87
+ readonly separator?: true;
88
+ readonly spacing?: "large";
89
+ }
90
+
91
+ /** One column inside a {@link TeamsColumnSet}. */
92
+ export interface TeamsColumn {
93
+ readonly type: "Column";
94
+ readonly width: "auto";
95
+ readonly items: readonly TeamsCardElement[];
96
+ }
97
+
98
+ /** A horizontal row of columns. */
99
+ export interface TeamsColumnSet {
100
+ readonly type: "ColumnSet";
101
+ readonly columns: readonly TeamsColumn[];
102
+ readonly separator?: true;
103
+ readonly spacing?: "large";
104
+ }
105
+
106
+ /** One selectable choice inside a {@link TeamsInputChoiceSet}. */
107
+ export interface TeamsInputChoice {
108
+ readonly title: string;
109
+ readonly value: string;
110
+ }
111
+
112
+ /** A dropdown (`compact`) or radio group (`expanded`) input. */
113
+ export interface TeamsInputChoiceSet {
114
+ readonly type: "Input.ChoiceSet";
115
+ readonly id: string;
116
+ readonly style: "compact" | "expanded";
117
+ readonly choices: readonly TeamsInputChoice[];
118
+ readonly placeholder?: string;
119
+ readonly label?: string;
120
+ readonly value?: string;
121
+ readonly separator?: true;
122
+ readonly spacing?: "large";
123
+ }
124
+
125
+ /** A checkbox-shaped boolean input. */
126
+ export interface TeamsInputToggle {
127
+ readonly type: "Input.Toggle";
128
+ readonly id: string;
129
+ readonly title: string;
130
+ readonly value: "true" | "false";
131
+ readonly valueOn: "true";
132
+ readonly valueOff: "false";
133
+ readonly separator?: true;
134
+ readonly spacing?: "large";
135
+ }
136
+
137
+ /** A single or multi-line text input. */
138
+ export interface TeamsInputText {
139
+ readonly type: "Input.Text";
140
+ readonly id: string;
141
+ readonly label?: string;
142
+ readonly placeholder?: string;
143
+ readonly isMultiline?: true;
144
+ readonly separator?: true;
145
+ readonly spacing?: "large";
146
+ }
147
+
148
+ /** A date input, restricted to `YYYY-MM-DD` values. */
149
+ export interface TeamsInputDate {
150
+ readonly type: "Input.Date";
151
+ readonly id: string;
152
+ readonly label?: string;
153
+ readonly value?: string;
154
+ readonly min?: string;
155
+ readonly max?: string;
156
+ readonly separator?: true;
157
+ readonly spacing?: "large";
158
+ }
159
+
160
+ /** One cell inside a {@link TeamsTableRow}. */
161
+ export interface TeamsTableCell {
162
+ readonly type: "TableCell";
163
+ readonly items: readonly TeamsTextBlock[];
164
+ }
165
+
166
+ /** One row inside a {@link TeamsTable}. */
167
+ export interface TeamsTableRow {
168
+ readonly type: "TableRow";
169
+ readonly cells: readonly TeamsTableCell[];
170
+ }
171
+
172
+ /** A column width definition inside a {@link TeamsTable}. */
173
+ export interface TeamsTableColumnDefinition {
174
+ readonly width: 1;
175
+ }
176
+
177
+ /** A native table. `rows[0]` is the header row when `firstRowAsHeaders` is true. */
178
+ export interface TeamsTable {
179
+ readonly type: "Table";
180
+ readonly firstRowAsHeaders: boolean;
181
+ readonly columns: readonly TeamsTableColumnDefinition[];
182
+ readonly rows: readonly TeamsTableRow[];
183
+ readonly separator?: true;
184
+ readonly spacing?: "large";
185
+ }
186
+
187
+ /** A body element emitted by {@link toAdaptiveCard}. */
188
+ export type TeamsCardElement =
189
+ | TeamsTextBlock
190
+ | TeamsImage
191
+ | TeamsFactSet
192
+ | TeamsActionSet
193
+ | TeamsContainer
194
+ | TeamsColumnSet
195
+ | TeamsInputChoiceSet
196
+ | TeamsInputToggle
197
+ | TeamsInputText
198
+ | TeamsInputDate
199
+ | TeamsTable;
200
+
201
+ /** A Microsoft Teams Adaptive Card, pinned to schema version 1.5 (the Teams desktop/web cap; mobile clients cap at 1.2). */
202
+ export interface TeamsAdaptiveCard {
203
+ readonly $schema: "http://adaptivecards.io/schemas/adaptive-card.json";
204
+ readonly type: "AdaptiveCard";
205
+ readonly version: "1.5";
206
+ readonly body: readonly TeamsCardElement[];
207
+ readonly actions: readonly TeamsCardAction[];
208
+ }
209
+
210
+ /** An Adaptive Card wrapped as a bot-framework message attachment. */
211
+ export interface TeamsCardAttachment {
212
+ readonly contentType: "application/vnd.microsoft.card.adaptive";
213
+ readonly content: TeamsAdaptiveCard;
214
+ }
215
+
216
+ /** A non-fatal downgrade reported during Teams conversion. */
217
+ export interface TeamsConversionWarning {
218
+ readonly code: "clamped" | "dropped" | "fallback";
219
+ readonly component: string;
220
+ readonly detail: string;
221
+ }
222
+
223
+ /** Options accepted by {@link toAdaptiveCard} and {@link toTeamsAttachments}. Reserved for future extension; no options are currently defined. */
224
+ export interface ToAdaptiveCardOptions {}
225
+
226
+ /** The card and non-fatal warnings produced by {@link toAdaptiveCard}. */
227
+ export interface AdaptiveCardResult {
228
+ readonly card: TeamsAdaptiveCard;
229
+ readonly warnings: TeamsConversionWarning[];
230
+ }
231
+
232
+ /** The attachments and non-fatal warnings produced by {@link toTeamsAttachments}. */
233
+ export interface TeamsAttachmentsResult {
234
+ readonly attachments: TeamsCardAttachment[];
235
+ readonly attachmentLayout?: "carousel";
236
+ readonly warnings: TeamsConversionWarning[];
237
+ }
package/src/teams.ts ADDED
@@ -0,0 +1,34 @@
1
+ export { decodeSubmitData } from "./teams/decodeSubmitData";
2
+ export { toAdaptiveCard } from "./teams/toAdaptiveCard";
3
+ export { toTeamsAttachments } from "./teams/toTeamsAttachments";
4
+ export type {
5
+ AdaptiveCardResult,
6
+ TeamsAdaptiveCard,
7
+ TeamsActionSet,
8
+ TeamsAttachmentsResult,
9
+ TeamsCardAction,
10
+ TeamsCardAttachment,
11
+ TeamsCardElement,
12
+ TeamsColumn,
13
+ TeamsColumnSet,
14
+ TeamsContainer,
15
+ TeamsContainerStyle,
16
+ TeamsConversionWarning,
17
+ TeamsFact,
18
+ TeamsFactSet,
19
+ TeamsImage,
20
+ TeamsInputChoice,
21
+ TeamsInputChoiceSet,
22
+ TeamsInputDate,
23
+ TeamsInputText,
24
+ TeamsInputToggle,
25
+ TeamsSubmitAction,
26
+ TeamsSubmitData,
27
+ TeamsTable,
28
+ TeamsTableCell,
29
+ TeamsTableColumnDefinition,
30
+ TeamsTableRow,
31
+ TeamsTextBlock,
32
+ TeamsTextSize,
33
+ ToAdaptiveCardOptions,
34
+ } from "./teams/types";
@@ -58,9 +58,79 @@ describe("alertVocabulary", () => {
58
58
  expect((html.match(/data-aui="carousel-slide"/g) ?? []).length).toBe(1);
59
59
  });
60
60
 
61
- it("Carousel with no children renders an empty container", () => {
61
+ it("Carousel without a label renders role=group, no aria-label, and no double-name landmark", () => {
62
62
  expect(render({ $type: "Carousel" })).toBe(
63
- '<div data-aui="carousel"></div>',
63
+ '<div data-aui="carousel" role="group" aria-roledescription="carousel" tabindex="0"></div>',
64
64
  );
65
65
  });
66
+
67
+ it("Carousel with a label renders role=region and the given aria-label", () => {
68
+ expect(render({ $type: "Carousel", label: "Featured" })).toBe(
69
+ '<div data-aui="carousel" role="region" aria-roledescription="carousel" aria-label="Featured" tabindex="0"></div>',
70
+ );
71
+ });
72
+
73
+ it("Carousel container carries region semantics and an optional label", () => {
74
+ const html = renderToStaticMarkup(
75
+ <>
76
+ {renderGenerativeUI(
77
+ {
78
+ $type: "Carousel",
79
+ label: "Featured items",
80
+ children: [
81
+ { $type: "Card", title: "a" },
82
+ { $type: "Card", title: "b" },
83
+ ],
84
+ },
85
+ defaultGenerativeUILibrary,
86
+ )}
87
+ </>,
88
+ );
89
+ expect(html).toContain('role="region"');
90
+ expect(html).toContain('aria-roledescription="carousel"');
91
+ expect(html).toContain('aria-label="Featured items"');
92
+ expect(html).toContain('tabindex="0"');
93
+ });
94
+
95
+ it("Carousel slides carry group semantics and a positional label", () => {
96
+ const html = renderToStaticMarkup(
97
+ <>
98
+ {renderGenerativeUI(
99
+ {
100
+ $type: "Carousel",
101
+ label: "Featured items",
102
+ children: [
103
+ { $type: "Card", title: "a" },
104
+ { $type: "Card", title: "b" },
105
+ { $type: "Card", title: "c" },
106
+ ],
107
+ },
108
+ defaultGenerativeUILibrary,
109
+ )}
110
+ </>,
111
+ );
112
+ expect((html.match(/role="group"/g) ?? []).length).toBe(3);
113
+ expect((html.match(/aria-roledescription="slide"/g) ?? []).length).toBe(3);
114
+ expect(html).toContain('aria-label="1 of 3"');
115
+ expect(html).toContain('aria-label="2 of 3"');
116
+ expect(html).toContain('aria-label="3 of 3"');
117
+ });
118
+
119
+ it("Carousel slide labels reflect the 10-slide cap, not the raw child count", () => {
120
+ const children = Array.from({ length: 12 }, (_, i) => ({
121
+ $type: "Card",
122
+ title: `c${i}`,
123
+ }));
124
+ const html = renderToStaticMarkup(
125
+ <>
126
+ {renderGenerativeUI(
127
+ { $type: "Carousel", children },
128
+ defaultGenerativeUILibrary,
129
+ )}
130
+ </>,
131
+ );
132
+ expect(html).toContain('aria-label="1 of 10"');
133
+ expect(html).toContain('aria-label="10 of 10"');
134
+ expect(html).not.toContain("11 of 10");
135
+ });
66
136
  });
@@ -29,14 +29,33 @@ export const alertVocabulary = {
29
29
  ),
30
30
  },
31
31
  Carousel: {
32
- description: "A horizontally scrollable group of `Card` children (max 10).",
33
- properties: z.object({}),
34
- render: ({ children }) => {
32
+ description:
33
+ "A horizontally scrollable group of `Card` children (max 10). Set `label` for an accessible name.",
34
+ properties: z.object({
35
+ label: z
36
+ .string()
37
+ .optional()
38
+ .describe("Accessible label for the carousel."),
39
+ }),
40
+ render: ({ label, children }) => {
35
41
  const cards = Children.toArray(children).slice(0, MAX_CARDS);
42
+ const n = cards.length;
36
43
  return (
37
- <div data-aui="carousel">
44
+ <div
45
+ data-aui="carousel"
46
+ role={label ? "region" : "group"}
47
+ aria-roledescription="carousel"
48
+ aria-label={label}
49
+ tabIndex={0}
50
+ >
38
51
  {cards.map((card, i) => (
39
- <div key={i} data-aui="carousel-slide">
52
+ <div
53
+ key={i}
54
+ data-aui="carousel-slide"
55
+ role="group"
56
+ aria-roledescription="slide"
57
+ aria-label={`${i + 1} of ${n}`}
58
+ >
40
59
  {card}
41
60
  </div>
42
61
  ))}
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ collectFormValues,
4
+ type FormControlElementLike,
5
+ } from "./collectFormValues";
6
+
7
+ const el = (
8
+ partial: Partial<FormControlElementLike>,
9
+ ): FormControlElementLike => ({
10
+ name: "",
11
+ type: "text",
12
+ value: "",
13
+ disabled: false,
14
+ ...partial,
15
+ });
16
+
17
+ describe("collectFormValues", () => {
18
+ it("resolves a checkbox to its checked boolean", () => {
19
+ expect(
20
+ collectFormValues([
21
+ el({ name: "agree", type: "checkbox", checked: true }),
22
+ ]),
23
+ ).toEqual({ agree: true });
24
+ expect(
25
+ collectFormValues([
26
+ el({ name: "agree", type: "checkbox", checked: false }),
27
+ ]),
28
+ ).toEqual({ agree: false });
29
+ });
30
+
31
+ it("resolves a radio set to the checked option's value", () => {
32
+ expect(
33
+ collectFormValues([
34
+ el({ name: "size", type: "radio", value: "sm", checked: false }),
35
+ el({ name: "size", type: "radio", value: "md", checked: true }),
36
+ el({ name: "size", type: "radio", value: "lg", checked: false }),
37
+ ]),
38
+ ).toEqual({ size: "md" });
39
+ });
40
+
41
+ it("resolves a radio set with no checked option to undefined", () => {
42
+ const values = collectFormValues([
43
+ el({ name: "size", type: "radio", value: "sm", checked: false }),
44
+ el({ name: "size", type: "radio", value: "md", checked: false }),
45
+ ]);
46
+ expect("size" in values).toBe(true);
47
+ expect(values.size).toBeUndefined();
48
+ });
49
+
50
+ it("resolves select/input/textarea/date controls to their string value", () => {
51
+ expect(
52
+ collectFormValues([
53
+ el({ name: "role", type: "select-one", value: "admin" }),
54
+ el({ name: "email", type: "email", value: "a@example.com" }),
55
+ el({ name: "bio", type: "textarea", value: "hi" }),
56
+ el({ name: "dob", type: "date", value: "2026-01-01" }),
57
+ ]),
58
+ ).toEqual({
59
+ role: "admin",
60
+ email: "a@example.com",
61
+ bio: "hi",
62
+ dob: "2026-01-01",
63
+ });
64
+ });
65
+
66
+ it("collects repeated non-radio names into an array in document order", () => {
67
+ expect(
68
+ collectFormValues([
69
+ el({ name: "tag", type: "text", value: "a" }),
70
+ el({ name: "tag", type: "text", value: "b" }),
71
+ el({ name: "tag", type: "text", value: "c" }),
72
+ ]),
73
+ ).toEqual({ tag: ["a", "b", "c"] });
74
+ });
75
+
76
+ it("skips controls without a name", () => {
77
+ expect(
78
+ collectFormValues([
79
+ el({ name: "", type: "text", value: "unnamed" }),
80
+ el({ name: "kept", type: "text", value: "yes" }),
81
+ ]),
82
+ ).toEqual({ kept: "yes" });
83
+ });
84
+
85
+ it("skips disabled controls", () => {
86
+ expect(
87
+ collectFormValues([
88
+ el({ name: "off", type: "text", value: "nope", disabled: true }),
89
+ el({ name: "on", type: "text", value: "yep" }),
90
+ ]),
91
+ ).toEqual({ on: "yep" });
92
+ });
93
+
94
+ it("skips a disabled control inside an otherwise-checked radio set", () => {
95
+ expect(
96
+ collectFormValues([
97
+ el({
98
+ name: "size",
99
+ type: "radio",
100
+ value: "sm",
101
+ checked: true,
102
+ disabled: true,
103
+ }),
104
+ el({ name: "size", type: "radio", value: "md", checked: false }),
105
+ ]),
106
+ ).toEqual({ size: undefined });
107
+ });
108
+
109
+ it("returns an empty object for no elements", () => {
110
+ expect(collectFormValues([])).toEqual({});
111
+ });
112
+
113
+ it("a field named __proto__ round-trips as an own data key", () => {
114
+ const values = collectFormValues([
115
+ el({ name: "__proto__", type: "text", value: "x" }),
116
+ ]);
117
+ expect(Object.hasOwn(values, "__proto__")).toBe(true);
118
+ expect(values["__proto__"]).toBe("x");
119
+ });
120
+
121
+ it("a field named constructor collects as its submitted string, not an array seeded with the inherited function", () => {
122
+ const values = collectFormValues([
123
+ el({ name: "constructor", type: "text", value: "hi" }),
124
+ ]);
125
+ expect(values["constructor"]).toBe("hi");
126
+ });
127
+ });