@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,649 @@
1
+ import { normalizeSpec } from "../ir.js";
2
+ import { CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, FACT_FIELD_TEXT_CAP, INPUT_LABEL_CAP, MARKDOWN_TEXT_BUDGET, NODE_BUDGET, SECTION_TEXT_CAP, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock } from "./constants.js";
3
+ //#region src/slack/toSlackBlocks.ts
4
+ const INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
5
+ "Button",
6
+ "Select",
7
+ "DatePicker",
8
+ "Checkbox",
9
+ "RadioGroup"
10
+ ]);
11
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
12
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
+ const isElement = (node) => isRecord(node);
14
+ const asString = (value) => typeof value === "string" ? value : "";
15
+ const warn = (context, code, component, detail) => {
16
+ context.warnings.push({
17
+ code,
18
+ component,
19
+ detail
20
+ });
21
+ };
22
+ const clampText = (value, cap, component, field, context) => {
23
+ if (value.length <= cap) return value;
24
+ warn(context, "clamped", component, `${field} was clamped to ${cap} characters.`);
25
+ return value.slice(0, cap);
26
+ };
27
+ const asActionId = (action, component, context) => clampText(isRecord(action) && typeof action["type"] === "string" ? action["type"] : "action", 255, component, "action_id", context);
28
+ const plainText = (text) => ({
29
+ type: "plain_text",
30
+ text
31
+ });
32
+ const actionValue = (action) => {
33
+ if (!isRecord(action)) return void 0;
34
+ const { type: _type, ...payload } = action;
35
+ if (Object.keys(payload).length === 0) return void 0;
36
+ try {
37
+ const serialized = JSON.stringify(payload);
38
+ return serialized === "{}" ? void 0 : serialized;
39
+ } catch {
40
+ return;
41
+ }
42
+ };
43
+ const buttonElement = (label, buttonStyle, action, component, context) => {
44
+ let value = actionValue(action);
45
+ if (value !== void 0 && value.length > 2e3) {
46
+ warn(context, "dropped", component, "value was dropped because the action payload was too large to round-trip.");
47
+ value = void 0;
48
+ }
49
+ const style = buttonStyle === "primary" || buttonStyle === "danger" ? buttonStyle : void 0;
50
+ return {
51
+ type: "button",
52
+ text: plainText(clampText(label, 75, component, "label", context)),
53
+ action_id: asActionId(action, component, context),
54
+ ...value !== void 0 ? { value } : {},
55
+ ...style !== void 0 ? { style } : {}
56
+ };
57
+ };
58
+ const optionFrom = (value, component, context) => {
59
+ if (!isRecord(value) || typeof value["label"] !== "string" || typeof value["value"] !== "string") return;
60
+ return {
61
+ text: plainText(clampText(value["label"], 75, component, "option label", context)),
62
+ value: value["value"]
63
+ };
64
+ };
65
+ const toActionElement = (element, context) => {
66
+ const { props, action } = element;
67
+ switch (element.type) {
68
+ case "Button": return buttonElement(asString(props["label"]), props["buttonStyle"], action, "Button", context);
69
+ case "Select": {
70
+ const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
71
+ if (rawOptions.length > 100) warn(context, "clamped", "Select", `options were clamped to 100 entries.`);
72
+ const options = rawOptions.slice(0, 100).map((option) => optionFrom(option, "Select", context)).filter((option) => option !== void 0);
73
+ const placeholder = clampText(asString(props["placeholder"]), 150, "Select", "placeholder", context);
74
+ return {
75
+ type: "static_select",
76
+ action_id: asActionId(action, "Select", context),
77
+ options,
78
+ ...placeholder ? { placeholder: plainText(placeholder) } : {}
79
+ };
80
+ }
81
+ case "DatePicker": {
82
+ const rawDate = asString(props["value"]);
83
+ const initialDate = DATE_PATTERN.test(rawDate) ? rawDate : void 0;
84
+ if (rawDate && initialDate === void 0) warn(context, "dropped", "DatePicker", "value did not match the YYYY-MM-DD format and was dropped.");
85
+ return {
86
+ type: "datepicker",
87
+ action_id: asActionId(action, "DatePicker", context),
88
+ ...initialDate !== void 0 ? { initial_date: initialDate } : {}
89
+ };
90
+ }
91
+ case "Checkbox": {
92
+ const label = clampText(asString(props["label"]), 75, "Checkbox", "label", context);
93
+ const name = asString(props["name"]);
94
+ const option = {
95
+ text: plainText(label),
96
+ value: name || label
97
+ };
98
+ return {
99
+ type: "checkboxes",
100
+ action_id: asActionId(action, "Checkbox", context),
101
+ options: [option],
102
+ ...props["defaultChecked"] === true ? { initial_options: [option] } : {}
103
+ };
104
+ }
105
+ case "RadioGroup": {
106
+ const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
107
+ if (rawOptions.length > 10) warn(context, "clamped", "RadioGroup", `options were clamped to 10 entries.`);
108
+ const options = rawOptions.slice(0, 10).map((option) => optionFrom(option, "RadioGroup", context)).filter((option) => option !== void 0);
109
+ const selectedValue = typeof props["value"] === "string" ? props["value"] : asString(props["defaultValue"]);
110
+ const initialOption = options.find((option) => option.value === selectedValue);
111
+ return {
112
+ type: "radio_buttons",
113
+ action_id: asActionId(action, "RadioGroup", context),
114
+ options,
115
+ ...initialOption !== void 0 ? { initial_option: initialOption } : {}
116
+ };
117
+ }
118
+ default: return;
119
+ }
120
+ };
121
+ const normalizedList = (node) => {
122
+ if (node === void 0 || node === null) return [];
123
+ if (!Array.isArray(node)) return [node];
124
+ return node.flatMap((child) => normalizedList(child));
125
+ };
126
+ const convertFacts = (facts, context) => {
127
+ const fields = facts.map((fact) => {
128
+ const label = asString(fact.props["label"]);
129
+ const value = asString(fact.props["value"]);
130
+ return {
131
+ type: "mrkdwn",
132
+ text: clampText(`*${label}*\n${value}`, FACT_FIELD_TEXT_CAP, "Fact", "field", context)
133
+ };
134
+ });
135
+ const blocks = [];
136
+ for (let index = 0; index < fields.length; index += 10) blocks.push({
137
+ type: "section",
138
+ fields: fields.slice(index, index + 10)
139
+ });
140
+ return blocks;
141
+ };
142
+ const convertActions = (elements, context) => {
143
+ const converted = elements.map((element) => toActionElement(element, context)).filter((element) => element !== void 0);
144
+ const blocks = [];
145
+ for (let index = 0; index < converted.length; index += 25) blocks.push({
146
+ type: "actions",
147
+ elements: converted.slice(index, index + 25)
148
+ });
149
+ return blocks;
150
+ };
151
+ const cardActionButtons = (props, context) => {
152
+ const buttons = [];
153
+ for (const key of ["confirm", "cancel"]) {
154
+ const footer = props[key];
155
+ if (!isRecord(footer)) continue;
156
+ buttons.push(buttonElement(asString(footer["label"]), key === "confirm" ? "primary" : void 0, footer["$action"], "Card", context));
157
+ }
158
+ return buttons;
159
+ };
160
+ const cardTitleText = (props, context) => {
161
+ const rawTitle = asString(props["title"]);
162
+ return rawTitle ? clampText(rawTitle, 150, "Card", "title", context) : void 0;
163
+ };
164
+ const isCardNoOp = (node) => isElement(node) && (node.type === "Spacer" || node.type === "Icon");
165
+ const collectText = (node, depth) => {
166
+ if (node === void 0 || node === null || depth > 64) return [];
167
+ if (typeof node === "string" || typeof node === "number") return [String(node)];
168
+ if (Array.isArray(node)) return node.flatMap((child) => collectText(child, depth + 1));
169
+ const element = node;
170
+ return [...[
171
+ "text",
172
+ "value",
173
+ "label",
174
+ "title",
175
+ "description"
176
+ ].map((key) => element.props[key]).filter((value) => typeof value === "string"), ...collectText(element.children, depth + 1)];
177
+ };
178
+ /**
179
+ * Scans a card's children for the pieces the native card block shape can
180
+ * carry (a hero image, body text, and subtext), each taken from the first
181
+ * matching child. Anything left over means the card cannot map cleanly onto
182
+ * the native shape.
183
+ */
184
+ const extractCardFields = (element) => {
185
+ const children = normalizedList(element.children);
186
+ let heroImage;
187
+ let bodyRaw;
188
+ let subtextRaw;
189
+ const leftover = [];
190
+ for (const child of children) {
191
+ if (heroImage === void 0 && isElement(child) && child.type === "Image") {
192
+ heroImage = {
193
+ type: "image",
194
+ image_url: asString(child.props["src"]),
195
+ alt_text: asString(child.props["alt"])
196
+ };
197
+ continue;
198
+ }
199
+ if (bodyRaw === void 0 && isElement(child) && (child.type === "Text" || child.type === "Markdown")) {
200
+ bodyRaw = asString(child.props["value"]);
201
+ continue;
202
+ }
203
+ if (subtextRaw === void 0 && isElement(child) && child.type === "Caption") {
204
+ subtextRaw = asString(child.props["value"]);
205
+ continue;
206
+ }
207
+ if (isCardNoOp(child)) continue;
208
+ leftover.push(child);
209
+ }
210
+ return {
211
+ heroImage,
212
+ bodyRaw,
213
+ subtextRaw,
214
+ leftover
215
+ };
216
+ };
217
+ /** Assembles a native card block, clamping its body and subtext text. */
218
+ const assembleCleanCard = (titleText, fields, buttons, context) => {
219
+ const body = fields.bodyRaw !== void 0 ? {
220
+ type: "mrkdwn",
221
+ text: clampText(fields.bodyRaw, 200, "Card", "body", context)
222
+ } : void 0;
223
+ const subtext = fields.subtextRaw !== void 0 ? {
224
+ type: "mrkdwn",
225
+ text: clampText(fields.subtextRaw, 200, "Card", "subtext", context)
226
+ } : void 0;
227
+ if (buttons.length > 3) warn(context, "clamped", "Card", `actions were clamped to 3 entries.`);
228
+ return buildCardBlock({
229
+ ...fields.heroImage !== void 0 ? { heroImage: fields.heroImage } : {},
230
+ ...titleText !== void 0 ? { title: {
231
+ type: "mrkdwn",
232
+ text: titleText
233
+ } } : {},
234
+ ...body !== void 0 ? { body } : {},
235
+ ...subtext !== void 0 ? { subtext } : {},
236
+ ...buttons.length > 0 ? { actions: buttons.slice(0, 3) } : {}
237
+ });
238
+ };
239
+ /**
240
+ * A card requires at least one of `hero_image`, `title`, `body`, or `actions`
241
+ * (see {@link SlackCardBlock}); a card with none of them is not a valid
242
+ * native block and must be dropped instead of emitted.
243
+ */
244
+ const isEmptyCard = (card) => card.hero_image === void 0 && card.title === void 0 && card.body === void 0 && (card.actions === void 0 || card.actions.length === 0);
245
+ const convertCard = (element, context, depth) => {
246
+ const titleText = cardTitleText(element.props, context);
247
+ const fields = extractCardFields(element);
248
+ const buttons = cardActionButtons(element.props, context);
249
+ if (fields.leftover.length > 0) {
250
+ warn(context, "fallback", "Card", "A card with content beyond hero_image, body, subtext, and actions was rendered inline.");
251
+ return [
252
+ ...titleText ? [{
253
+ type: "header",
254
+ text: plainText(titleText)
255
+ }] : [],
256
+ ...convertSequence(element.children, context, depth + 1),
257
+ ...buttons.length > 0 ? [{
258
+ type: "actions",
259
+ elements: buttons
260
+ }] : []
261
+ ];
262
+ }
263
+ const card = assembleCleanCard(titleText, fields, buttons, context);
264
+ if (isEmptyCard(card)) {
265
+ warn(context, "dropped", "Card", "A card with none of hero_image, title, body, or actions was dropped.");
266
+ return [];
267
+ }
268
+ return [card];
269
+ };
270
+ /**
271
+ * Degrades a card that cannot map cleanly into a title-and-body card block.
272
+ * A carousel cannot fall back to a block sequence like a standalone card
273
+ * can, so its content is clamped down to fit the card shape instead.
274
+ */
275
+ const degradeCard = (element, context, depth) => {
276
+ const rawTitle = asString(element.props["title"]);
277
+ const textChunks = collectText(element.children, depth + 1);
278
+ const titleSource = rawTitle || textChunks[0] || "";
279
+ const bodyChunks = rawTitle ? textChunks : textChunks.slice(1);
280
+ const titleText = titleSource.slice(0, 150);
281
+ const bodyText = bodyChunks.join("\n").slice(0, 200);
282
+ warn(context, "clamped", "Card", "A card inside a carousel was degraded to title and body.");
283
+ return buildCardBlock({
284
+ ...titleText ? { title: {
285
+ type: "mrkdwn",
286
+ text: titleText
287
+ } } : {},
288
+ ...bodyText ? { body: {
289
+ type: "mrkdwn",
290
+ text: bodyText
291
+ } } : {}
292
+ });
293
+ };
294
+ const convertCarouselCard = (element, context, depth) => {
295
+ const fields = extractCardFields(element);
296
+ const card = fields.leftover.length === 0 ? assembleCleanCard(cardTitleText(element.props, context), fields, cardActionButtons(element.props, context), context) : degradeCard(element, context, depth);
297
+ if (isEmptyCard(card)) {
298
+ warn(context, "dropped", "Card", "A card with none of hero_image, title, body, or actions was dropped.");
299
+ return;
300
+ }
301
+ return card;
302
+ };
303
+ const isBadgeOrCaption = (node) => isElement(node) && (node.type === "Badge" || node.type === "Caption");
304
+ const contextRow = (element, context) => {
305
+ const allChildren = normalizedList(element.children);
306
+ const children = allChildren.filter(isBadgeOrCaption);
307
+ if (allChildren.length === 0 || children.length !== allChildren.length) return;
308
+ if (children.length > 10) warn(context, "clamped", "Row", `context elements were clamped to 10 entries.`);
309
+ return [{
310
+ type: "context",
311
+ elements: children.slice(0, 10).map((child) => ({
312
+ type: "mrkdwn",
313
+ text: clampText(asString(child.props["value"]), CONTEXT_TEXT_CAP, child.type, "value", context)
314
+ }))
315
+ }];
316
+ };
317
+ const convertListItem = (element, context, depth) => {
318
+ const text = clampText(collectText(element.children, depth + 1).join("\n"), SECTION_TEXT_CAP, "ListViewItem", "text", context);
319
+ const accessory = isRecord(element.action) ? buttonElement("Open", void 0, element.action, "ListViewItem", context) : void 0;
320
+ return {
321
+ type: "section",
322
+ text: {
323
+ type: "mrkdwn",
324
+ text
325
+ },
326
+ ...accessory !== void 0 ? { accessory } : {}
327
+ };
328
+ };
329
+ const convertListView = (element, context, depth) => {
330
+ return normalizedList(element.children).filter((child) => isElement(child) && child.type === "ListViewItem").flatMap((item, index) => [...index > 0 ? [{ type: "divider" }] : [], convertListItem(item, context, depth + 1)]);
331
+ };
332
+ const convertCarousel = (element, context, depth) => {
333
+ const cardChildren = [];
334
+ for (const child of normalizedList(element.children)) if (isElement(child) && child.type === "Card") cardChildren.push(child);
335
+ else warn(context, "dropped", "Carousel", "A non-card child was dropped.");
336
+ if (cardChildren.length > 10) warn(context, "clamped", "Carousel", `cards were clamped to 10 entries.`);
337
+ const cards = cardChildren.slice(0, 10).map((card) => convertCarouselCard(card, context, depth + 1)).filter((card) => card !== void 0);
338
+ if (cards.length < 1) {
339
+ warn(context, "dropped", "Carousel", "A carousel without renderable cards was dropped.");
340
+ return [];
341
+ }
342
+ return [buildCarouselBlock(cards)];
343
+ };
344
+ const toDataTableCell = (value) => {
345
+ if (typeof value === "number") return {
346
+ type: "raw_number",
347
+ text: String(value)
348
+ };
349
+ if (typeof value === "string" || typeof value === "boolean") return {
350
+ type: "raw_text",
351
+ text: String(value)
352
+ };
353
+ };
354
+ const rowCharacters = (row) => row.reduce((sum, cell) => sum + cell.text.length, 0);
355
+ const convertTable = (element, context) => {
356
+ const rawColumns = Array.isArray(element.props["columns"]) ? element.props["columns"] : [];
357
+ const rawRows = Array.isArray(element.props["rows"]) ? element.props["rows"] : [];
358
+ if (rawColumns.length > 20) warn(context, "clamped", "Table", `columns were clamped to 20 entries.`);
359
+ if (rawRows.length > 100) warn(context, "clamped", "Table", `rows were clamped to 100 entries.`);
360
+ const columnHeaderRow = rawColumns.slice(0, 20).filter(isRecord).map((column) => ({
361
+ type: "raw_text",
362
+ text: asString(column["label"])
363
+ }));
364
+ const dataRows = rawRows.slice(0, 100).map((row) => (Array.isArray(row) ? row : []).slice(0, 20).map((value) => toDataTableCell(value) ?? {
365
+ type: "raw_text",
366
+ text: ""
367
+ }));
368
+ const width = Math.max(columnHeaderRow.length, 0, ...dataRows.map((row) => row.length));
369
+ const padRow = (row) => row.length >= width ? row : [...row, ...Array.from({ length: width - row.length }, () => ({
370
+ type: "raw_text",
371
+ text: ""
372
+ }))];
373
+ const headerRow = padRow(columnHeaderRow);
374
+ const headerCharacters = rowCharacters(headerRow);
375
+ if (context.dataTableCharacters + headerCharacters > 1e4) {
376
+ warn(context, "dropped", "Table", `the table was dropped because its header row exceeds the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
377
+ return [];
378
+ }
379
+ const kept = [headerRow];
380
+ let tableCharacters = headerCharacters;
381
+ let budgetClamped = false;
382
+ for (const row of dataRows.map(padRow)) {
383
+ const chars = rowCharacters(row);
384
+ if (context.dataTableCharacters + tableCharacters + chars > 1e4) {
385
+ budgetClamped = true;
386
+ break;
387
+ }
388
+ kept.push(row);
389
+ tableCharacters += chars;
390
+ }
391
+ context.dataTableCharacters += tableCharacters;
392
+ if (budgetClamped) warn(context, "clamped", "Table", `rows were clamped to fit the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
393
+ return [buildDataTableBlock(kept)];
394
+ };
395
+ const convertElement = (element, context, depth) => {
396
+ const { props } = element;
397
+ switch (element.type) {
398
+ case "Header": return [{
399
+ type: "header",
400
+ text: plainText(clampText(asString(props["text"]), 150, "Header", "text", context))
401
+ }];
402
+ case "Text": return [{
403
+ type: "section",
404
+ text: {
405
+ type: "mrkdwn",
406
+ text: clampText(asString(props["value"]), SECTION_TEXT_CAP, "Text", "value", context)
407
+ }
408
+ }];
409
+ case "Caption":
410
+ case "Badge": return [{
411
+ type: "context",
412
+ elements: [{
413
+ type: "mrkdwn",
414
+ text: clampText(asString(props["value"]), CONTEXT_TEXT_CAP, element.type, "value", context)
415
+ }]
416
+ }];
417
+ case "Fact": return convertFacts([element], context);
418
+ case "Image": return [{
419
+ type: "image",
420
+ image_url: asString(props["src"]),
421
+ alt_text: asString(props["alt"])
422
+ }];
423
+ case "Divider": return [{ type: "divider" }];
424
+ case "Button":
425
+ case "Select":
426
+ case "DatePicker":
427
+ case "Checkbox":
428
+ case "RadioGroup": return convertActions([element], context);
429
+ case "Input": {
430
+ const label = clampText(asString(props["label"]), INPUT_LABEL_CAP, "Input", "label", context);
431
+ const placeholder = clampText(asString(props["placeholder"]), 150, "Input", "placeholder", context);
432
+ return [{
433
+ type: "input",
434
+ label: plainText(label),
435
+ element: {
436
+ type: "plain_text_input",
437
+ action_id: asActionId(element.action, "Input", context),
438
+ ...props["multiline"] === true ? { multiline: true } : {},
439
+ ...placeholder ? { placeholder: plainText(placeholder) } : {}
440
+ }
441
+ }];
442
+ }
443
+ case "Alert": {
444
+ const tone = props["tone"] === "success" || props["tone"] === "warning" || props["tone"] === "danger" ? props["tone"] : "info";
445
+ const titleText = asString(props["title"]);
446
+ const description = asString(props["description"]);
447
+ if (context.surface === "modal") {
448
+ const combined = [titleText, description].filter(Boolean).join("\n");
449
+ return [buildAlertBlock(clampText(combined, 200, "Alert", "text", context), tone)];
450
+ }
451
+ warn(context, "fallback", "Alert", "Alert was rendered with message-surface fallback blocks.");
452
+ return [...buildAlertFallback(titleText, clampText(description, SECTION_TEXT_CAP, "Alert", "description", context), tone)];
453
+ }
454
+ case "Card": return convertCard(element, context, depth);
455
+ case "Carousel": return convertCarousel(element, context, depth);
456
+ case "Col":
457
+ case "Box": return convertSequence(element.children, context, depth + 1);
458
+ case "Row": return contextRow(element, context) ?? convertSequence(element.children, context, depth + 1);
459
+ case "Spacer":
460
+ case "Icon": return [];
461
+ case "Table": return convertTable(element, context);
462
+ case "Markdown": {
463
+ const value = asString(props["value"]);
464
+ if (!context.markdownExhausted && context.markdownCharacters + value.length <= 12e3) {
465
+ context.markdownCharacters += value.length;
466
+ return [{
467
+ type: "markdown",
468
+ text: value
469
+ }];
470
+ }
471
+ context.markdownExhausted = true;
472
+ warn(context, "fallback", "Markdown", `The ${MARKDOWN_TEXT_BUDGET}-character markdown budget was exceeded.`);
473
+ return [{
474
+ type: "section",
475
+ text: {
476
+ type: "mrkdwn",
477
+ text: clampText(value, SECTION_TEXT_CAP, "Markdown", "fallback text", context)
478
+ }
479
+ }];
480
+ }
481
+ case "Chart":
482
+ warn(context, "dropped", "Chart", "Chart was replaced by a Slack omission note.");
483
+ return [{
484
+ type: "context",
485
+ elements: [{
486
+ type: "mrkdwn",
487
+ text: "Chart omitted on Slack."
488
+ }]
489
+ }];
490
+ case "ListView": return convertListView(element, context, depth);
491
+ case "ListViewItem": return [convertListItem(element, context, depth)];
492
+ case "Form": return [...convertSequence(element.children, context, depth + 1), {
493
+ type: "actions",
494
+ elements: [buttonElement("Submit", "primary", element.action, "Form", context)]
495
+ }];
496
+ default:
497
+ warn(context, "dropped", element.type, "Unknown component type was dropped.");
498
+ return [];
499
+ }
500
+ };
501
+ function convertSequence(node, context, depth) {
502
+ if (node === void 0 || node === null || depth > 64) return [];
503
+ const nodes = Array.isArray(node) ? node : [node];
504
+ const blocks = [];
505
+ for (let index = 0; index < nodes.length;) {
506
+ const current = nodes[index];
507
+ if (current === void 0 || current === null) {
508
+ index += 1;
509
+ continue;
510
+ }
511
+ if (isElement(current) && current.type === "Fact") {
512
+ const facts = [];
513
+ while (index < nodes.length) {
514
+ const candidate = nodes[index];
515
+ if (!candidate || !isElement(candidate) || candidate.type !== "Fact") break;
516
+ facts.push(candidate);
517
+ index += 1;
518
+ }
519
+ blocks.push(...convertFacts(facts, context));
520
+ continue;
521
+ }
522
+ if (isElement(current) && INTERACTIVE_TYPES.has(current.type)) {
523
+ const controls = [];
524
+ while (index < nodes.length) {
525
+ const candidate = nodes[index];
526
+ if (!candidate || !isElement(candidate) || !INTERACTIVE_TYPES.has(candidate.type)) break;
527
+ controls.push(candidate);
528
+ index += 1;
529
+ }
530
+ blocks.push(...convertActions(controls, context));
531
+ continue;
532
+ }
533
+ if (typeof current === "string" || typeof current === "number") blocks.push({
534
+ type: "section",
535
+ text: {
536
+ type: "mrkdwn",
537
+ text: clampText(String(current), SECTION_TEXT_CAP, "Text", "value", context)
538
+ }
539
+ });
540
+ else if (Array.isArray(current)) blocks.push(...convertSequence(current, context, depth + 1));
541
+ else blocks.push(...convertElement(current, context, depth + 1));
542
+ index += 1;
543
+ }
544
+ return blocks;
545
+ }
546
+ function boundNode(value, depth, onClamp, state, ancestors) {
547
+ if (state.remaining <= 0) {
548
+ if (!state.exhausted) {
549
+ state.exhausted = true;
550
+ onClamp("budget");
551
+ }
552
+ return null;
553
+ }
554
+ state.remaining -= 1;
555
+ if (depth > 64) {
556
+ onClamp("depth");
557
+ return null;
558
+ }
559
+ if (Array.isArray(value)) {
560
+ if (ancestors.has(value)) {
561
+ onClamp("cycle");
562
+ return null;
563
+ }
564
+ ancestors.add(value);
565
+ const bounded = Array.prototype.slice.call(value, 0, 200);
566
+ if (value.length > 200) onClamp("children");
567
+ const result = bounded.map((item) => boundNode(item, depth + 1, onClamp, state, ancestors));
568
+ ancestors.delete(value);
569
+ return result;
570
+ }
571
+ if (value !== null && typeof value === "object" && "children" in value) {
572
+ if (ancestors.has(value)) {
573
+ onClamp("cycle");
574
+ return null;
575
+ }
576
+ ancestors.add(value);
577
+ const record = value;
578
+ const result = {
579
+ ...record,
580
+ children: boundNode(record["children"], depth + 1, onClamp, state, ancestors)
581
+ };
582
+ ancestors.delete(value);
583
+ return result;
584
+ }
585
+ return value;
586
+ }
587
+ /**
588
+ * Produces a bounded plain copy of a raw generative-ui spec before it
589
+ * reaches `normalizeSpec`, whose own traversal of the root array or any
590
+ * `children` array walks the full reported length of a hostile proxied
591
+ * array before any per-field cap downstream ever applies. Every array
592
+ * (root, or `children` at any depth) is capped to {@link CHILDREN_CAP}
593
+ * entries via `Array.prototype.slice`, which bounds even a proxy with a
594
+ * fabricated `length`; recursion itself is capped at
595
+ * {@link MAX_TRAVERSAL_DEPTH}. `onClamp` fires once per level that was
596
+ * truncated, receiving the reason for that truncation: `"children"`,
597
+ * `"depth"`, `"budget"`, or `"cycle"`. The walk also spends a total budget of
598
+ * {@link NODE_BUDGET} nodes, so shared references cannot multiply work
599
+ * exponentially, and a node that is its own ancestor is cut to `null`.
600
+ */
601
+ function boundSpec(spec, onClamp) {
602
+ return boundNode(spec, 0, onClamp, {
603
+ remaining: NODE_BUDGET,
604
+ exhausted: false
605
+ }, /* @__PURE__ */ new WeakSet());
606
+ }
607
+ /** Converts a generative-UI tree into Slack Block Kit JSON and downgrade warnings. */
608
+ function toSlackBlocks(node, options) {
609
+ const surface = options?.surface === "modal" ? "modal" : "message";
610
+ const context = {
611
+ blockCap: surface === "modal" ? 100 : 50,
612
+ surface,
613
+ warnings: [],
614
+ markdownCharacters: 0,
615
+ markdownExhausted: false,
616
+ dataTableCharacters: 0
617
+ };
618
+ try {
619
+ const { root } = normalizeSpec(boundSpec(node, (reason) => {
620
+ const detail = reason === "budget" ? `the tree was truncated after ${NODE_BUDGET} nodes.` : reason === "cycle" ? "a self-referencing node was dropped." : reason === "depth" ? `nodes deeper than 64 levels were dropped.` : `children were clamped to 200 entries.`;
621
+ warn(context, "clamped", "Root", detail);
622
+ }));
623
+ const converted = convertSequence(root, context, 0);
624
+ if (converted.length <= context.blockCap) return {
625
+ blocks: converted,
626
+ warnings: context.warnings
627
+ };
628
+ const kept = converted.slice(0, context.blockCap - 1);
629
+ const omitted = converted.length - kept.length;
630
+ warn(context, "clamped", "Root", `${omitted} ${omitted === 1 ? "block was" : "blocks were"} omitted.`);
631
+ return {
632
+ blocks: [...kept, buildBlockBudgetNote(omitted)],
633
+ warnings: context.warnings
634
+ };
635
+ } catch {
636
+ return {
637
+ blocks: [],
638
+ warnings: [{
639
+ code: "dropped",
640
+ component: "Root",
641
+ detail: "Input could not be converted."
642
+ }]
643
+ };
644
+ }
645
+ }
646
+ //#endregion
647
+ export { toSlackBlocks };
648
+
649
+ //# sourceMappingURL=toSlackBlocks.js.map