@assistant-ui/react-generative-ui 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/JSONGenerativeUI.client.d.ts +5 -0
- package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
- package/dist/JSONGenerativeUI.client.js +12 -3
- package/dist/JSONGenerativeUI.client.js.map +1 -1
- package/dist/renderGenerativeUI.js +2 -1
- package/dist/renderGenerativeUI.js.map +1 -1
- package/dist/slack/constants.d.ts +11 -4
- package/dist/slack/constants.d.ts.map +1 -1
- package/dist/slack/constants.js +11 -4
- package/dist/slack/constants.js.map +1 -1
- package/dist/slack/fromSlackBlocks.js +2 -2
- package/dist/slack/toSlackBlocks.d.ts.map +1 -1
- package/dist/slack/toSlackBlocks.js +114 -19
- package/dist/slack/toSlackBlocks.js.map +1 -1
- package/dist/slack/types.d.ts +7 -1
- package/dist/slack/types.d.ts.map +1 -1
- package/dist/teams/constants.d.ts +9 -2
- package/dist/teams/constants.d.ts.map +1 -1
- package/dist/teams/constants.js +10 -3
- package/dist/teams/constants.js.map +1 -1
- package/dist/teams/toAdaptiveCard.d.ts +10 -2
- package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
- package/dist/teams/toAdaptiveCard.js +54 -15
- package/dist/teams/toAdaptiveCard.js.map +1 -1
- package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
- package/dist/teams/toTeamsAttachments.js +21 -13
- package/dist/teams/toTeamsAttachments.js.map +1 -1
- package/dist/teams/types.d.ts +11 -2
- package/dist/teams/types.d.ts.map +1 -1
- package/dist/vocabulary/data.js +3 -2
- package/dist/vocabulary/data.js.map +1 -1
- package/dist/vocabulary/layout.js +2 -1
- package/dist/vocabulary/layout.js.map +1 -1
- package/package.json +5 -5
- package/src/JSONGenerativeUI.client.tsx +13 -5
- package/src/JSONGenerativeUI.test.tsx +21 -2
- package/src/slack/constants.ts +11 -3
- package/src/slack/toSlackBlocks.test.ts +374 -8
- package/src/slack/toSlackBlocks.ts +181 -24
- package/src/slack/types.ts +7 -1
- package/src/teams/constants.ts +10 -2
- package/src/teams/toAdaptiveCard.test.ts +204 -10
- package/src/teams/toAdaptiveCard.ts +79 -15
- package/src/teams/toTeamsAttachments.test.ts +47 -2
- package/src/teams/toTeamsAttachments.ts +17 -11
- package/src/teams/types.ts +11 -2
- package/src/vocabulary/layout.test.tsx +16 -1
- package/src/vocabulary/layout.tsx +4 -1
|
@@ -16,6 +16,11 @@ declare class JSONGenerativeUI {
|
|
|
16
16
|
private readonly parameters;
|
|
17
17
|
private readonly actions;
|
|
18
18
|
constructor(options: JSONGenerativeUIOptions);
|
|
19
|
+
/**
|
|
20
|
+
* The surface a `present` call paints into. It owns the vertical rhythm
|
|
21
|
+
* between top-level blocks, which the host's message container does not
|
|
22
|
+
* provide, and stays out of the way while it has nothing to show.
|
|
23
|
+
*/
|
|
19
24
|
private readonly render;
|
|
20
25
|
present(options?: PresentToolOptions): PresentTool;
|
|
21
26
|
promptUser(): PromptUserTool;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSONGenerativeUI.client.d.ts","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"mappings":";;;;;;;;;;;;;cAkCa;mBACM;mBACA;mBACA;EAEL,YAAA,SAAS;
|
|
1
|
+
{"version":3,"file":"JSONGenerativeUI.client.d.ts","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"mappings":";;;;;;;;;;;;;cAkCa;mBACM;mBACA;mBACA;EAEL,YAAA,SAAS;;;;;;mBAWJ;EAejB,QAAQ,UAAU,qBAAqB;EASvC,cAAc"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildPresentParameters } from "./buildPresentParameters.js";
|
|
2
2
|
import { presentToolBase, promptUserToolBase } from "./JSONGenerativeUI.shared.js";
|
|
3
3
|
import { renderGenerativeUI } from "./renderGenerativeUI.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
5
|
//#region src/JSONGenerativeUI.client.tsx
|
|
5
6
|
/** Maps a tool-call part status to the generative-UI streaming status. Only a
|
|
6
7
|
* `complete` call has fully-arrived args; `running` and `incomplete`
|
|
@@ -29,9 +30,17 @@ var JSONGenerativeUI = class {
|
|
|
29
30
|
this.parameters = buildPresentParameters(options.library);
|
|
30
31
|
this.actions = options.actions;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The surface a `present` call paints into. It owns the vertical rhythm
|
|
35
|
+
* between top-level blocks, which the host's message container does not
|
|
36
|
+
* provide, and stays out of the way while it has nothing to show.
|
|
37
|
+
*/
|
|
38
|
+
render = ({ args, status }) => /* @__PURE__ */ jsx("div", {
|
|
39
|
+
"data-aui": "root",
|
|
40
|
+
children: renderGenerativeUI(args, this.library, {
|
|
41
|
+
status: uiStatus(status),
|
|
42
|
+
...this.actions ? { dispatch: this.actions.dispatch } : {}
|
|
43
|
+
})
|
|
35
44
|
});
|
|
36
45
|
present(options) {
|
|
37
46
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JSONGenerativeUI.client.js","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { buildPresentParameters } from \"./buildPresentParameters\";\nimport {\n presentToolBase,\n promptUserToolBase,\n type JSONGenerativeUIOptions,\n type PresentParameters,\n type PresentTool,\n type PresentToolOptions,\n type PromptUserTool,\n} from \"./JSONGenerativeUI.shared\";\nimport { type ActionRegistry } from \"./actionRegistry\";\nimport { renderGenerativeUI } from \"./renderGenerativeUI\";\nimport type { GenerativeUILibrary, GenerativeUIStatus } from \"./types\";\n\n/** Maps a tool-call part status to the generative-UI streaming status. Only a\n * `complete` call has fully-arrived args; `running` and `incomplete`\n * (aborted/errored, so args may be partial) both render as `\"streaming\"` so a\n * non-streaming component is never handed partial props. */\nfunction uiStatus(status: { type: string }): GenerativeUIStatus {\n return status.type === \"complete\" ? \"done\" : \"streaming\";\n}\n\n/**\n * Client build of {@link JSONGenerativeUI}, resolved through the package's\n * `default` export condition (browser and SSR).\n *\n * `present` is a frontend tool that renders the model's `{ $type, ...props }`\n * tree against the library and resolves immediately. `prompt_user` is a\n * human-in-the-loop tool: the model pauses and the rendered UI supplies the\n * result. Both draw the tree the same way, so they share one `render`. The\n * `actions` registry (if provided) is threaded into the render context so\n * interactive components can fire `$action` through `$dispatch`.\n */\nexport class JSONGenerativeUI {\n private readonly library: GenerativeUILibrary;\n private readonly parameters: PresentParameters;\n private readonly actions: ActionRegistry | undefined;\n\n constructor(options: JSONGenerativeUIOptions) {\n this.library = options.library;\n this.parameters = buildPresentParameters(options.library);\n this.actions = options.actions;\n }\n\n private readonly render = ({\n args,\n status,\n }: {\n args: unknown;\n status: { type: string };\n }): ReactNode
|
|
1
|
+
{"version":3,"file":"JSONGenerativeUI.client.js","names":[],"sources":["../src/JSONGenerativeUI.client.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { buildPresentParameters } from \"./buildPresentParameters\";\nimport {\n presentToolBase,\n promptUserToolBase,\n type JSONGenerativeUIOptions,\n type PresentParameters,\n type PresentTool,\n type PresentToolOptions,\n type PromptUserTool,\n} from \"./JSONGenerativeUI.shared\";\nimport { type ActionRegistry } from \"./actionRegistry\";\nimport { renderGenerativeUI } from \"./renderGenerativeUI\";\nimport type { GenerativeUILibrary, GenerativeUIStatus } from \"./types\";\n\n/** Maps a tool-call part status to the generative-UI streaming status. Only a\n * `complete` call has fully-arrived args; `running` and `incomplete`\n * (aborted/errored, so args may be partial) both render as `\"streaming\"` so a\n * non-streaming component is never handed partial props. */\nfunction uiStatus(status: { type: string }): GenerativeUIStatus {\n return status.type === \"complete\" ? \"done\" : \"streaming\";\n}\n\n/**\n * Client build of {@link JSONGenerativeUI}, resolved through the package's\n * `default` export condition (browser and SSR).\n *\n * `present` is a frontend tool that renders the model's `{ $type, ...props }`\n * tree against the library and resolves immediately. `prompt_user` is a\n * human-in-the-loop tool: the model pauses and the rendered UI supplies the\n * result. Both draw the tree the same way, so they share one `render`. The\n * `actions` registry (if provided) is threaded into the render context so\n * interactive components can fire `$action` through `$dispatch`.\n */\nexport class JSONGenerativeUI {\n private readonly library: GenerativeUILibrary;\n private readonly parameters: PresentParameters;\n private readonly actions: ActionRegistry | undefined;\n\n constructor(options: JSONGenerativeUIOptions) {\n this.library = options.library;\n this.parameters = buildPresentParameters(options.library);\n this.actions = options.actions;\n }\n\n /**\n * The surface a `present` call paints into. It owns the vertical rhythm\n * between top-level blocks, which the host's message container does not\n * provide, and stays out of the way while it has nothing to show.\n */\n private readonly render = ({\n args,\n status,\n }: {\n args: unknown;\n status: { type: string };\n }): ReactNode => (\n <div data-aui=\"root\">\n {renderGenerativeUI(args, this.library, {\n status: uiStatus(status),\n ...(this.actions ? { dispatch: this.actions.dispatch } : {}),\n })}\n </div>\n );\n\n present(options?: PresentToolOptions): PresentTool {\n return {\n ...presentToolBase(this.parameters, options),\n unstable_backendDefault: { parameters: true },\n execute: async () => ({}),\n render: this.render,\n };\n }\n\n promptUser(): PromptUserTool {\n return {\n ...promptUserToolBase(this.parameters),\n unstable_backendDefault: { parameters: true },\n render: this.render,\n };\n }\n}\n"],"mappings":";;;;;;;;;AAmBA,SAAS,SAAS,QAA8C;CAC9D,OAAO,OAAO,SAAS,aAAa,SAAS;AAC/C;;;;;;;;;;;;AAaA,IAAa,mBAAb,MAA8B;CAC5B;CACA;CACA;CAEA,YAAY,SAAkC;EAC5C,KAAK,UAAU,QAAQ;EACvB,KAAK,aAAa,uBAAuB,QAAQ,OAAO;EACxD,KAAK,UAAU,QAAQ;CACzB;;;;;;CAOA,UAA2B,EACzB,MACA,aAKA,oBAAC,OAAD;EAAK,YAAS;EACX,UAAA,mBAAmB,MAAM,KAAK,SAAS;GACtC,QAAQ,SAAS,MAAM;GACvB,GAAI,KAAK,UAAU,EAAE,UAAU,KAAK,QAAQ,SAAS,IAAI,CAAC;EAC5D,CAAC;CACE,CAAA;CAGP,QAAQ,SAA2C;EACjD,OAAO;GACL,GAAG,gBAAgB,KAAK,YAAY,OAAO;GAC3C,yBAAyB,EAAE,YAAY,KAAK;GAC5C,SAAS,aAAa,CAAC;GACvB,QAAQ,KAAK;EACf;CACF;CAEA,aAA6B;EAC3B,OAAO;GACL,GAAG,mBAAmB,KAAK,UAAU;GACrC,yBAAyB,EAAE,YAAY,KAAK;GAC5C,QAAQ,KAAK;EACf;CACF;AACF"}
|
|
@@ -17,7 +17,8 @@ const isElement = (node) => typeof node === "object" && node !== null && !Array.
|
|
|
17
17
|
*/
|
|
18
18
|
function renderGenerativeUI(node, library, context = DEFAULT_CONTEXT) {
|
|
19
19
|
const meta = getPartialJsonObjectMeta(node);
|
|
20
|
-
|
|
20
|
+
const partialPath = meta?.state === "partial" ? meta.partialPath : void 0;
|
|
21
|
+
return renderNode(normalizeUINode(node, partialPath), library, context);
|
|
21
22
|
}
|
|
22
23
|
function renderNode(node, library, context) {
|
|
23
24
|
if (node == null || typeof node === "boolean") return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n normalizeUINode,\n type NormalizedUIElement,\n type NormalizedUINode,\n} from \"./ir\";\nimport type { GenerativeUILibrary, GenerativeUIRenderContext } from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\nconst isElement = (node: NormalizedUINode): node is NormalizedUIElement =>\n typeof node === \"object\" && node !== null && !Array.isArray(node);\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into the canonical {@link NormalizedUINode} (with\n * `children` lifted to a reserved top-level key), then render: each `type` is\n * looked up in the library and its `props` are passed to the component's\n * `render(props, context)`, with `children` rendered recursively so components\n * can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeUINode(node, partialPath), library, context);\n}\n\nfunction renderNode(\n node: NormalizedUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // Use a model-provided stable key when present. Otherwise, keep the\n // positional fallback: pairing the index with the node's kind means that\n // when the kind at an index changes, React remounts instead of handing a\n // streaming node's hook state to a different component.\n return node.map((child, index) => (\n <Fragment key={nodeKey(child, index)}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return isElement(node) ? renderElement(node, library, context) : null;\n}\n\nfunction renderElement(\n element: NormalizedUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // `children` is a reserved top-level key on the normalized element, not a\n // prop, so it is not in `props`. Inject the framework props last so the model\n // can never override them. `$action` is likewise reserved and stripped from\n // the prop bag during normalization, so it is re-injected here for components\n // that carry behavior (e.g. `Button`).\n const props: Record<string, unknown> = {\n ...element.props,\n $status: context.status,\n };\n if (context.dispatch !== undefined) {\n props[\"$dispatch\"] = context.dispatch;\n }\n if (element.action !== undefined) {\n props[\"$action\"] = element.action;\n }\n if (element.children !== undefined) {\n props[\"children\"] = renderNode(element.children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: NormalizedUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return isElement(node) ? node.type : \"\";\n}\n\nfunction nodeKey(node: NormalizedUINode, index: number): string {\n return isElement(node) &&\n (typeof node.key === \"string\" || typeof node.key === \"number\")\n ? `model:${node.key}`\n : `${index}:${nodeKind(node)}`;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AASA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;AAEpE,MAAM,aAAa,SACjB,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;;;;;;;;;;;AAYlE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;
|
|
1
|
+
{"version":3,"file":"renderGenerativeUI.js","names":[],"sources":["../src/renderGenerativeUI.tsx"],"sourcesContent":["import { getPartialJsonObjectMeta } from \"assistant-stream/utils\";\nimport { Fragment, type ReactNode } from \"react\";\nimport {\n normalizeUINode,\n type NormalizedUIElement,\n type NormalizedUINode,\n} from \"./ir\";\nimport type { GenerativeUILibrary, GenerativeUIRenderContext } from \"./types\";\n\nconst DEFAULT_CONTEXT: GenerativeUIRenderContext = { status: \"done\" };\n\nconst isElement = (node: NormalizedUINode): node is NormalizedUIElement =>\n typeof node === \"object\" && node !== null && !Array.isArray(node);\n\n/**\n * Renders a generative-ui tree against a {@link GenerativeUILibrary}.\n *\n * The model emits each node as a flat object `{ $type, ...props }`. We first\n * normalize that wire form into the canonical {@link NormalizedUINode} (with\n * `children` lifted to a reserved top-level key), then render: each `type` is\n * looked up in the library and its `props` are passed to the component's\n * `render(props, context)`, with `children` rendered recursively so components\n * can nest.\n */\nexport function renderGenerativeUI(\n node: unknown,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext = DEFAULT_CONTEXT,\n): ReactNode {\n // Tool args are parsed incrementally, and the parse meta records which path\n // is still mid-arrival, so normalization can hold back a node whose `$type`\n // string has not finished streaming.\n const meta = getPartialJsonObjectMeta(node as Record<symbol, unknown>);\n const partialPath = meta?.state === \"partial\" ? meta.partialPath : undefined;\n return renderNode(normalizeUINode(node, partialPath), library, context);\n}\n\nfunction renderNode(\n node: NormalizedUINode,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n if (node == null || typeof node === \"boolean\") return null;\n if (typeof node === \"string\" || typeof node === \"number\") return node;\n if (Array.isArray(node)) {\n // Use a model-provided stable key when present. Otherwise, keep the\n // positional fallback: pairing the index with the node's kind means that\n // when the kind at an index changes, React remounts instead of handing a\n // streaming node's hook state to a different component.\n return node.map((child, index) => (\n <Fragment key={nodeKey(child, index)}>\n {renderNode(child, library, context)}\n </Fragment>\n ));\n }\n return isElement(node) ? renderElement(node, library, context) : null;\n}\n\nfunction renderElement(\n element: NormalizedUIElement,\n library: GenerativeUILibrary,\n context: GenerativeUIRenderContext,\n): ReactNode {\n const entry = library[element.type];\n if (!entry) {\n reportUnknownComponent(element.type, Object.keys(library));\n return null;\n }\n\n // Components that opt out of prop streaming wait until their props are\n // complete rather than rendering from a partial parse.\n if (!entry.streamProperties && context.status === \"streaming\") return null;\n\n // `children` is a reserved top-level key on the normalized element, not a\n // prop, so it is not in `props`. Inject the framework props last so the model\n // can never override them. `$action` is likewise reserved and stripped from\n // the prop bag during normalization, so it is re-injected here for components\n // that carry behavior (e.g. `Button`).\n const props: Record<string, unknown> = {\n ...element.props,\n $status: context.status,\n };\n if (context.dispatch !== undefined) {\n props[\"$dispatch\"] = context.dispatch;\n }\n if (element.action !== undefined) {\n props[\"$action\"] = element.action;\n }\n if (element.children !== undefined) {\n props[\"children\"] = renderNode(element.children, library, context);\n }\n\n return <GenerativeUIComponentRenderer render={entry.render} props={props} />;\n}\n\n/**\n * Mounts a single node's `render` on its own fiber so the function may use\n * hooks and hold state independently of its siblings and parent.\n */\nfunction GenerativeUIComponentRenderer({\n render,\n props,\n}: {\n render: (props: any) => ReactNode;\n props: Record<string, unknown>;\n}): ReactNode {\n return render(props);\n}\n\n/** A coarse kind tag for a child, used in its list key so a node changing kind\n * at a given index forces a remount rather than a wrong-fiber reuse. */\nfunction nodeKind(node: NormalizedUINode): string {\n if (node == null || typeof node === \"boolean\") return \"\";\n if (typeof node === \"string\" || typeof node === \"number\") return \"#text\";\n if (Array.isArray(node)) return \"#array\";\n return isElement(node) ? node.type : \"\";\n}\n\nfunction nodeKey(node: NormalizedUINode, index: number): string {\n return isElement(node) &&\n (typeof node.key === \"string\" || typeof node.key === \"number\")\n ? `model:${node.key}`\n : `${index}:${nodeKind(node)}`;\n}\n\nfunction reportUnknownComponent(type: string, available: string[]): void {\n if (process.env[\"NODE_ENV\"] !== \"production\") {\n // eslint-disable-next-line no-console\n console.error(\n `[@assistant-ui/react-generative-ui] Unknown component \"${type}\". ` +\n `Available components: ${available.join(\", \") || \"(none)\"}.`,\n );\n }\n}\n"],"mappings":";;;;;AASA,MAAM,kBAA6C,EAAE,QAAQ,OAAO;AAEpE,MAAM,aAAa,SACjB,OAAO,SAAS,YAAY,SAAS,QAAQ,CAAC,MAAM,QAAQ,IAAI;;;;;;;;;;;AAYlE,SAAgB,mBACd,MACA,SACA,UAAqC,iBAC1B;CAIX,MAAM,OAAO,yBAAyB,IAA+B;CACrE,MAAM,cAAc,MAAM,UAAU,YAAY,KAAK,cAAc,KAAA;CACnE,OAAO,WAAW,gBAAgB,MAAM,WAAW,GAAG,SAAS,OAAO;AACxE;AAEA,SAAS,WACP,MACA,SACA,SACW;CACX,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAKpB,OAAO,KAAK,KAAK,OAAO,UACtB,oBAAC,UAAD,EAAA,UACG,WAAW,OAAO,SAAS,OAAO,EAC3B,GAFK,QAAQ,OAAO,KAAK,CAEzB,CACX;CAEH,OAAO,UAAU,IAAI,IAAI,cAAc,MAAM,SAAS,OAAO,IAAI;AACnE;AAEA,SAAS,cACP,SACA,SACA,SACW;CACX,MAAM,QAAQ,QAAQ,QAAQ;CAC9B,IAAI,CAAC,OAAO;EACV,uBAAuB,QAAQ,MAAM,OAAO,KAAK,OAAO,CAAC;EACzD,OAAO;CACT;CAIA,IAAI,CAAC,MAAM,oBAAoB,QAAQ,WAAW,aAAa,OAAO;CAOtE,MAAM,QAAiC;EACrC,GAAG,QAAQ;EACX,SAAS,QAAQ;CACnB;CACA,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,eAAe,QAAQ;CAE/B,IAAI,QAAQ,WAAW,KAAA,GACrB,MAAM,aAAa,QAAQ;CAE7B,IAAI,QAAQ,aAAa,KAAA,GACvB,MAAM,cAAc,WAAW,QAAQ,UAAU,SAAS,OAAO;CAGnE,OAAO,oBAAC,+BAAD;EAA+B,QAAQ,MAAM;EAAe;CAAQ,CAAA;AAC7E;;;;;AAMA,SAAS,8BAA8B,EACrC,QACA,SAIY;CACZ,OAAO,OAAO,KAAK;AACrB;;;AAIA,SAAS,SAAS,MAAgC;CAChD,IAAI,QAAQ,QAAQ,OAAO,SAAS,WAAW,OAAO;CACtD,IAAI,OAAO,SAAS,YAAY,OAAO,SAAS,UAAU,OAAO;CACjE,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO;CAChC,OAAO,UAAU,IAAI,IAAI,KAAK,OAAO;AACvC;AAEA,SAAS,QAAQ,MAAwB,OAAuB;CAC9D,OAAO,UAAU,IAAI,MAClB,OAAO,KAAK,QAAQ,YAAY,OAAO,KAAK,QAAQ,YACnD,SAAS,KAAK,QACd,GAAG,MAAM,GAAG,SAAS,IAAI;AAC/B;AAEA,SAAS,uBAAuB,MAAc,WAA2B;CACvE,IAAI,QAAQ,IAAI,gBAAgB,cAE9B,QAAQ,MACN,0DAA0D,KAAK,2BACpC,UAAU,KAAK,IAAI,KAAK,SAAS,EAC9D;AAEJ"}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { SlackAlertBlock, SlackButtonElement, SlackCardBlock, SlackCarouselBlock, SlackContextBlock, SlackDataTableBlock, SlackDataTableCell, SlackImageBlock, SlackSectionBlock, SlackTextObject } from "./types.js";
|
|
2
2
|
//#region src/slack/constants.d.ts
|
|
3
|
-
/** The maximum
|
|
3
|
+
/** The maximum traversal depth, counted in visited values, not tree levels. */
|
|
4
4
|
declare const MAX_TRAVERSAL_DEPTH = 64;
|
|
5
|
+
/**
|
|
6
|
+
* The element-nesting ceiling a caller observes. `boundNode` visits an element
|
|
7
|
+
* and its `children` array separately, so one level of nesting spends two of
|
|
8
|
+
* {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
|
|
9
|
+
* the looser of the two. A root array costs one more level than a root object.
|
|
10
|
+
*/
|
|
11
|
+
declare const MAX_ELEMENT_DEPTH: number;
|
|
5
12
|
/**
|
|
6
13
|
* The maximum number of entries kept at any single level (root, or a
|
|
7
14
|
* `children` array at any depth) of the raw spec, before it ever reaches
|
|
@@ -74,14 +81,14 @@ declare const ALERT_TEXT_CAP = 200;
|
|
|
74
81
|
* The maximum number of data rows in a data-table block; the emitted `rows`
|
|
75
82
|
* array additionally carries one header row.
|
|
76
83
|
*/
|
|
77
|
-
declare const DATA_TABLE_ROW_CAP =
|
|
84
|
+
declare const DATA_TABLE_ROW_CAP = 200;
|
|
78
85
|
/** The maximum number of columns in a data-table block. */
|
|
79
86
|
declare const DATA_TABLE_COLUMN_CAP = 20;
|
|
80
87
|
/**
|
|
81
88
|
* The cumulative character limit (summed across every cell's text) shared by
|
|
82
89
|
* every data-table block in one payload.
|
|
83
90
|
*/
|
|
84
|
-
declare const DATA_TABLE_CHAR_BUDGET =
|
|
91
|
+
declare const DATA_TABLE_CHAR_BUDGET = 20000;
|
|
85
92
|
/** The caption emitted on every native data-table block. */
|
|
86
93
|
declare const TABLE_CAPTION = "Table";
|
|
87
94
|
/** The cumulative character limit for native markdown blocks. */
|
|
@@ -107,5 +114,5 @@ declare function buildDataTableBlock(rows: readonly (readonly SlackDataTableCell
|
|
|
107
114
|
/** Builds the final note used when a surface block budget is exceeded. */
|
|
108
115
|
declare function buildBlockBudgetNote(omitted: number): SlackContextBlock;
|
|
109
116
|
//#endregion
|
|
110
|
-
export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
|
|
117
|
+
export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_ELEMENT_DEPTH, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
|
|
111
118
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","names":[],"sources":["../../src/slack/constants.ts"],"mappings":";;;cAca;;;;;;;;cASA;;;;;;;;cASA;;cAGA;;cAGA;;;;;;cAOA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;;;;cAMA;;cAGA;;;;;cAMA;;cAGA;;cAGA;;iBAGG,mBACd,eACA,qBACA,4DACW,mBAAmB;;iBAmBhB,gBACd,cACA,kDACC;KAKE;WACM,YAAY;WACZ,QAAQ;WACR,WAAW;WACX,OAAO;WACP,UAAU;WACV,mBAAmB;;;iBAId,eAAe,QAAQ,kBAAkB;;iBAczC,mBACd,mBAAmB,mBAClB;;iBAKa,oBACd,yBAAyB,0BACxB;;iBAKa,qBAAqB,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","names":[],"sources":["../../src/slack/constants.ts"],"mappings":";;;cAca;;;;;;;cAQA;;;;;;;;cASA;;;;;;;;cASA;;cAGA;;cAGA;;;;;;cAOA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;cAGA;;;;;cAMA;;cAGA;;;;;cAMA;;cAGA;;cAGA;;iBAGG,mBACd,eACA,qBACA,4DACW,mBAAmB;;iBAmBhB,gBACd,cACA,kDACC;KAKE;WACM,YAAY;WACZ,QAAQ;WACR,WAAW;WACX,OAAO;WACP,UAAU;WACV,mBAAmB;;;iBAId,eAAe,QAAQ,kBAAkB;;iBAczC,mBACd,mBAAmB,mBAClB;;iBAKa,oBACd,yBAAyB,0BACxB;;iBAKa,qBAAqB,kBAAkB"}
|
package/dist/slack/constants.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
//#region src/slack/constants.ts
|
|
2
|
-
/** The maximum
|
|
2
|
+
/** The maximum traversal depth, counted in visited values, not tree levels. */
|
|
3
3
|
const MAX_TRAVERSAL_DEPTH = 64;
|
|
4
4
|
/**
|
|
5
|
+
* The element-nesting ceiling a caller observes. `boundNode` visits an element
|
|
6
|
+
* and its `children` array separately, so one level of nesting spends two of
|
|
7
|
+
* {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
|
|
8
|
+
* the looser of the two. A root array costs one more level than a root object.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_ELEMENT_DEPTH = 32;
|
|
11
|
+
/**
|
|
5
12
|
* The maximum number of entries kept at any single level (root, or a
|
|
6
13
|
* `children` array at any depth) of the raw spec, before it ever reaches
|
|
7
14
|
* `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass
|
|
@@ -73,14 +80,14 @@ const ALERT_TEXT_CAP = 200;
|
|
|
73
80
|
* The maximum number of data rows in a data-table block; the emitted `rows`
|
|
74
81
|
* array additionally carries one header row.
|
|
75
82
|
*/
|
|
76
|
-
const DATA_TABLE_ROW_CAP =
|
|
83
|
+
const DATA_TABLE_ROW_CAP = 200;
|
|
77
84
|
/** The maximum number of columns in a data-table block. */
|
|
78
85
|
const DATA_TABLE_COLUMN_CAP = 20;
|
|
79
86
|
/**
|
|
80
87
|
* The cumulative character limit (summed across every cell's text) shared by
|
|
81
88
|
* every data-table block in one payload.
|
|
82
89
|
*/
|
|
83
|
-
const DATA_TABLE_CHAR_BUDGET =
|
|
90
|
+
const DATA_TABLE_CHAR_BUDGET = 2e4;
|
|
84
91
|
/** The caption emitted on every native data-table block. */
|
|
85
92
|
const TABLE_CAPTION = "Table";
|
|
86
93
|
/** The cumulative character limit for native markdown blocks. */
|
|
@@ -157,6 +164,6 @@ function buildBlockBudgetNote(omitted) {
|
|
|
157
164
|
};
|
|
158
165
|
}
|
|
159
166
|
//#endregion
|
|
160
|
-
export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
|
|
167
|
+
export { ACTIONS_ELEMENT_CAP, ACTION_ID_CAP, ALERT_TEXT_CAP, BUTTON_VALUE_CAP, CARD_ACTIONS_CAP, CARD_BODY_CAP, CARD_SUBTEXT_CAP, CARD_TITLE_CAP, CAROUSEL_CARD_CAP, CAROUSEL_CARD_MIN, CHILDREN_CAP, CONTEXT_ELEMENT_CAP, CONTEXT_TEXT_CAP, DATA_TABLE_CHAR_BUDGET, DATA_TABLE_COLUMN_CAP, DATA_TABLE_ROW_CAP, FACT_FIELD_CAP, FACT_FIELD_TEXT_CAP, HEADER_TEXT_CAP, INBOUND_BLOCK_CAP, INPUT_LABEL_CAP, INTERACTIVE_TEXT_CAP, MARKDOWN_TEXT_BUDGET, MAX_ELEMENT_DEPTH, MAX_TRAVERSAL_DEPTH, MESSAGE_BLOCK_CAP, MODAL_BLOCK_CAP, NODE_BUDGET, PLACEHOLDER_TEXT_CAP, RADIO_OPTION_CAP, SECTION_TEXT_CAP, SELECT_OPTION_CAP, TABLE_CAPTION, buildAlertBlock, buildAlertFallback, buildBlockBudgetNote, buildCardBlock, buildCarouselBlock, buildDataTableBlock };
|
|
161
168
|
|
|
162
169
|
//# sourceMappingURL=constants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../src/slack/constants.ts"],"sourcesContent":["import type {\n SlackAlertBlock,\n SlackButtonElement,\n SlackCardBlock,\n SlackCarouselBlock,\n SlackContextBlock,\n SlackDataTableBlock,\n SlackDataTableCell,\n SlackImageBlock,\n SlackSectionBlock,\n SlackTextObject,\n} from \"./types\";\n\n/** The maximum
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../src/slack/constants.ts"],"sourcesContent":["import type {\n SlackAlertBlock,\n SlackButtonElement,\n SlackCardBlock,\n SlackCarouselBlock,\n SlackContextBlock,\n SlackDataTableBlock,\n SlackDataTableCell,\n SlackImageBlock,\n SlackSectionBlock,\n SlackTextObject,\n} from \"./types\";\n\n/** The maximum traversal depth, counted in visited values, not tree levels. */\nexport const MAX_TRAVERSAL_DEPTH = 64;\n\n/**\n * The element-nesting ceiling a caller observes. `boundNode` visits an element\n * and its `children` array separately, so one level of nesting spends two of\n * {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never\n * the looser of the two. A root array costs one more level than a root object.\n */\nexport const MAX_ELEMENT_DEPTH = MAX_TRAVERSAL_DEPTH / 2;\n\n/**\n * The maximum number of entries kept at any single level (root, or a\n * `children` array at any depth) of the raw spec, before it ever reaches\n * `normalizeSpec`. Bounds a hostile array's reported `length` so the pre-pass\n * can never be made to walk further than this regardless of what the array\n * claims about itself.\n */\nexport const CHILDREN_CAP = 200;\n\n/**\n * The total number of nodes the pre-normalization bounding walk will visit\n * across one call, regardless of how many times a shared reference recurs.\n * Bounds the combinatorial work a DAG of shared or self-referential nodes\n * would otherwise force even though each individual array stays within\n * {@link CHILDREN_CAP}.\n */\nexport const NODE_BUDGET = 5000;\n\n/** The block limit for a Slack message. */\nexport const MESSAGE_BLOCK_CAP = 50;\n\n/** The block limit for a Slack modal. */\nexport const MODAL_BLOCK_CAP = 100;\n\n/**\n * The maximum number of top-level blocks accepted when parsing Slack Block\n * Kit JSON back into generative UI, guarding against a hostile or malformed\n * payload.\n */\nexport const INBOUND_BLOCK_CAP = 200;\n\n/** The character limit for header text. */\nexport const HEADER_TEXT_CAP = 150;\n\n/** The character limit for section text. */\nexport const SECTION_TEXT_CAP = 3000;\n\n/** The maximum number of fields in one section. */\nexport const FACT_FIELD_CAP = 10;\n\n/** The character limit for one section field. */\nexport const FACT_FIELD_TEXT_CAP = 2000;\n\n/** The character limit for button labels and option labels. */\nexport const INTERACTIVE_TEXT_CAP = 75;\n\n/** The character limit for an action_id. */\nexport const ACTION_ID_CAP = 255;\n\n/** The character limit for a button's serialized value payload. */\nexport const BUTTON_VALUE_CAP = 2000;\n\n/** The maximum number of elements in one actions block. */\nexport const ACTIONS_ELEMENT_CAP = 25;\n\n/** The maximum number of options in a static select. */\nexport const SELECT_OPTION_CAP = 100;\n\n/** The maximum number of options in a radio-button group. */\nexport const RADIO_OPTION_CAP = 10;\n\n/** The character limit for a select or input placeholder. */\nexport const PLACEHOLDER_TEXT_CAP = 150;\n\n/** The character limit for an input's label. */\nexport const INPUT_LABEL_CAP = 2000;\n\n/** The maximum number of elements in one context block. */\nexport const CONTEXT_ELEMENT_CAP = 10;\n\n/** The character limit for one context element's text. */\nexport const CONTEXT_TEXT_CAP = 3000;\n\n/** The character limit for card titles and subtitles. */\nexport const CARD_TITLE_CAP = 150;\n\n/** The character limit for a card's body text. */\nexport const CARD_BODY_CAP = 200;\n\n/** The character limit for a card's subtext. */\nexport const CARD_SUBTEXT_CAP = 200;\n\n/** The maximum number of buttons in a card's actions. */\nexport const CARD_ACTIONS_CAP = 3;\n\n/** The minimum number of elements in a renderable carousel. */\nexport const CAROUSEL_CARD_MIN = 1;\n\n/** The maximum number of elements in a carousel. */\nexport const CAROUSEL_CARD_CAP = 10;\n\n/** The character limit for an alert's text. */\nexport const ALERT_TEXT_CAP = 200;\n\n/**\n * The maximum number of data rows in a data-table block; the emitted `rows`\n * array additionally carries one header row.\n */\nexport const DATA_TABLE_ROW_CAP = 200;\n\n/** The maximum number of columns in a data-table block. */\nexport const DATA_TABLE_COLUMN_CAP = 20;\n\n/**\n * The cumulative character limit (summed across every cell's text) shared by\n * every data-table block in one payload.\n */\nexport const DATA_TABLE_CHAR_BUDGET = 20000;\n\n/** The caption emitted on every native data-table block. */\nexport const TABLE_CAPTION = \"Table\";\n\n/** The cumulative character limit for native markdown blocks. */\nexport const MARKDOWN_TEXT_BUDGET = 12000;\n\n/** Builds the message-surface fallback for an alert. */\nexport function buildAlertFallback(\n title: string,\n description: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): readonly [SlackContextBlock, SlackSectionBlock] {\n const emoji = {\n info: \"ℹ️\",\n success: \"✅\",\n warning: \"⚠️\",\n danger: \"🛑\",\n }[tone];\n return [\n {\n type: \"context\",\n elements: [\n { type: \"mrkdwn\", text: title ? `${emoji} *${title}*` : emoji },\n ],\n },\n { type: \"section\", text: { type: \"mrkdwn\", text: description } },\n ];\n}\n\n/** Builds a 2026 Slack modal-only alert block, mapping `tone` to its `level`. */\nexport function buildAlertBlock(\n text: string,\n tone: \"info\" | \"success\" | \"warning\" | \"danger\",\n): SlackAlertBlock {\n const level = tone === \"danger\" ? \"error\" : tone;\n return { type: \"alert\", text: { type: \"mrkdwn\", text }, level };\n}\n\ntype CardBlockFields = {\n readonly heroImage?: SlackImageBlock;\n readonly title?: SlackTextObject;\n readonly subtitle?: SlackTextObject;\n readonly body?: SlackTextObject;\n readonly subtext?: SlackTextObject;\n readonly actions?: readonly SlackButtonElement[];\n};\n\n/** Builds a 2026 Slack card block from its already-clamped fields. */\nexport function buildCardBlock(fields: CardBlockFields): SlackCardBlock {\n const { heroImage, title, subtitle, body, subtext, actions } = fields;\n return {\n type: \"card\",\n ...(heroImage !== undefined ? { hero_image: heroImage } : {}),\n ...(title !== undefined ? { title } : {}),\n ...(subtitle !== undefined ? { subtitle } : {}),\n ...(body !== undefined ? { body } : {}),\n ...(subtext !== undefined ? { subtext } : {}),\n ...(actions !== undefined ? { actions } : {}),\n };\n}\n\n/** Builds a 2026 Slack carousel block. */\nexport function buildCarouselBlock(\n elements: readonly SlackCardBlock[],\n): SlackCarouselBlock {\n return { type: \"carousel\", elements };\n}\n\n/** Builds a 2026 Slack data-table block; `rows[0]` must be the header row. */\nexport function buildDataTableBlock(\n rows: readonly (readonly SlackDataTableCell[])[],\n): SlackDataTableBlock {\n return { type: \"data_table\", caption: TABLE_CAPTION, rows };\n}\n\n/** Builds the final note used when a surface block budget is exceeded. */\nexport function buildBlockBudgetNote(omitted: number): SlackContextBlock {\n return {\n type: \"context\",\n elements: [\n {\n type: \"mrkdwn\",\n text: `${omitted} ${omitted === 1 ? \"block was\" : \"blocks were\"} omitted.`,\n },\n ],\n };\n}\n"],"mappings":";;AAcA,MAAa,sBAAsB;;;;;;;AAQnC,MAAa,oBAAA;;;;;;;;AASb,MAAa,eAAe;;;;;;;;AAS5B,MAAa,cAAc;;AAG3B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;;;;;AAO/B,MAAa,oBAAoB;;AAGjC,MAAa,kBAAkB;;AAG/B,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,sBAAsB;;AAGnC,MAAa,uBAAuB;;AAGpC,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,sBAAsB;;AAGnC,MAAa,oBAAoB;;AAGjC,MAAa,mBAAmB;;AAGhC,MAAa,uBAAuB;;AAGpC,MAAa,kBAAkB;;AAG/B,MAAa,sBAAsB;;AAGnC,MAAa,mBAAmB;;AAGhC,MAAa,iBAAiB;;AAG9B,MAAa,gBAAgB;;AAG7B,MAAa,mBAAmB;;AAGhC,MAAa,mBAAmB;;AAGhC,MAAa,oBAAoB;;AAGjC,MAAa,oBAAoB;;AAGjC,MAAa,iBAAiB;;;;;AAM9B,MAAa,qBAAqB;;AAGlC,MAAa,wBAAwB;;;;;AAMrC,MAAa,yBAAyB;;AAGtC,MAAa,gBAAgB;;AAG7B,MAAa,uBAAuB;;AAGpC,SAAgB,mBACd,OACA,aACA,MACiD;CACjD,MAAM,QAAQ;EACZ,MAAM;EACN,SAAS;EACT,SAAS;EACT,QAAQ;CACV,EAAE;CACF,OAAO,CACL;EACE,MAAM;EACN,UAAU,CACR;GAAE,MAAM;GAAU,MAAM,QAAQ,GAAG,MAAM,IAAI,MAAM,KAAK;EAAM,CAChE;CACF,GACA;EAAE,MAAM;EAAW,MAAM;GAAE,MAAM;GAAU,MAAM;EAAY;CAAE,CACjE;AACF;;AAGA,SAAgB,gBACd,MACA,MACiB;CAEjB,OAAO;EAAE,MAAM;EAAS,MAAM;GAAE,MAAM;GAAU;EAAK;EAAG,OAD1C,SAAS,WAAW,UAAU;CACkB;AAChE;;AAYA,SAAgB,eAAe,QAAyC;CACtE,MAAM,EAAE,WAAW,OAAO,UAAU,MAAM,SAAS,YAAY;CAC/D,OAAO;EACL,MAAM;EACN,GAAI,cAAc,KAAA,IAAY,EAAE,YAAY,UAAU,IAAI,CAAC;EAC3D,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACvC,GAAI,aAAa,KAAA,IAAY,EAAE,SAAS,IAAI,CAAC;EAC7C,GAAI,SAAS,KAAA,IAAY,EAAE,KAAK,IAAI,CAAC;EACrC,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;EAC3C,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;CAC7C;AACF;;AAGA,SAAgB,mBACd,UACoB;CACpB,OAAO;EAAE,MAAM;EAAY;CAAS;AACtC;;AAGA,SAAgB,oBACd,MACqB;CACrB,OAAO;EAAE,MAAM;EAAc,SAAS;EAAe;CAAK;AAC5D;;AAGA,SAAgB,qBAAqB,SAAoC;CACvE,OAAO;EACL,MAAM;EACN,UAAU,CACR;GACE,MAAM;GACN,MAAM,GAAG,QAAQ,GAAG,YAAY,IAAI,cAAc,cAAc;EAClE,CACF;CACF;AACF"}
|
|
@@ -308,8 +308,8 @@ const dataTableCellValue = (cell) => {
|
|
|
308
308
|
return text;
|
|
309
309
|
};
|
|
310
310
|
const dataTableFrom = (block, warnings) => {
|
|
311
|
-
const { items: rows, truncated: rowsTruncated } = clampArray(block["rows"],
|
|
312
|
-
if (rowsTruncated) warn(warnings, "clamped", "Table", `rows were clamped to
|
|
311
|
+
const { items: rows, truncated: rowsTruncated } = clampArray(block["rows"], 201);
|
|
312
|
+
if (rowsTruncated) warn(warnings, "clamped", "Table", `rows were clamped to 200 entries.`);
|
|
313
313
|
const [headerRow, ...dataRows] = rows;
|
|
314
314
|
const { items: headerCells, truncated: headerTruncated } = clampArray(headerRow, 20);
|
|
315
315
|
let columnsTruncated = headerTruncated;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toSlackBlocks.d.ts","names":[],"sources":["../../src/slack/toSlackBlocks.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"toSlackBlocks.d.ts","names":[],"sources":["../../src/slack/toSlackBlocks.ts"],"mappings":";;;iBA64CgB,cACd,eACA,UAAU,uBACT"}
|
|
@@ -8,6 +8,17 @@ const INTERACTIVE_TYPES = /* @__PURE__ */ new Set([
|
|
|
8
8
|
"Checkbox",
|
|
9
9
|
"RadioGroup"
|
|
10
10
|
]);
|
|
11
|
+
/**
|
|
12
|
+
* Types whose control disappears when a card is reshaped to text. `Input` and
|
|
13
|
+
* `Form` are not in {@link INTERACTIVE_TYPES} because they emit their own
|
|
14
|
+
* block rather than an actions element, but a reshape loses them the same way,
|
|
15
|
+
* and a `Form` gets a Submit button whether or not it carries an action.
|
|
16
|
+
*/
|
|
17
|
+
const CONTROL_TYPES = /* @__PURE__ */ new Set([
|
|
18
|
+
...INTERACTIVE_TYPES,
|
|
19
|
+
"Input",
|
|
20
|
+
"Form"
|
|
21
|
+
]);
|
|
11
22
|
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
23
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
24
|
const isElement = (node) => isRecord(node);
|
|
@@ -62,6 +73,11 @@ const optionFrom = (value, component, context) => {
|
|
|
62
73
|
value: value["value"]
|
|
63
74
|
};
|
|
64
75
|
};
|
|
76
|
+
const warnDroppedOptions = (kept, taken, component, context) => {
|
|
77
|
+
const dropped = taken - kept;
|
|
78
|
+
if (dropped === 0) return;
|
|
79
|
+
warn(context, "dropped", component, `${dropped} ${dropped === 1 ? "option was" : "options were"} dropped for want of a string label and value.`);
|
|
80
|
+
};
|
|
65
81
|
const toActionElement = (element, context) => {
|
|
66
82
|
const { props, action } = element;
|
|
67
83
|
switch (element.type) {
|
|
@@ -69,7 +85,9 @@ const toActionElement = (element, context) => {
|
|
|
69
85
|
case "Select": {
|
|
70
86
|
const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
|
|
71
87
|
if (rawOptions.length > 100) warn(context, "clamped", "Select", `options were clamped to 100 entries.`);
|
|
72
|
-
const
|
|
88
|
+
const takenOptions = rawOptions.slice(0, 100);
|
|
89
|
+
const options = takenOptions.map((option) => optionFrom(option, "Select", context)).filter((option) => option !== void 0);
|
|
90
|
+
warnDroppedOptions(options.length, takenOptions.length, "Select", context);
|
|
73
91
|
const placeholder = clampText(asString(props["placeholder"]), 150, "Select", "placeholder", context);
|
|
74
92
|
return {
|
|
75
93
|
type: "static_select",
|
|
@@ -105,7 +123,9 @@ const toActionElement = (element, context) => {
|
|
|
105
123
|
case "RadioGroup": {
|
|
106
124
|
const rawOptions = Array.isArray(props["options"]) ? props["options"] : [];
|
|
107
125
|
if (rawOptions.length > 10) warn(context, "clamped", "RadioGroup", `options were clamped to 10 entries.`);
|
|
108
|
-
const
|
|
126
|
+
const takenOptions = rawOptions.slice(0, 10);
|
|
127
|
+
const options = takenOptions.map((option) => optionFrom(option, "RadioGroup", context)).filter((option) => option !== void 0);
|
|
128
|
+
warnDroppedOptions(options.length, takenOptions.length, "RadioGroup", context);
|
|
109
129
|
const selectedValue = typeof props["value"] === "string" ? props["value"] : asString(props["defaultValue"]);
|
|
110
130
|
const initialOption = options.find((option) => option.value === selectedValue);
|
|
111
131
|
return {
|
|
@@ -267,19 +287,56 @@ const convertCard = (element, context, depth) => {
|
|
|
267
287
|
}
|
|
268
288
|
return [card];
|
|
269
289
|
};
|
|
290
|
+
/** The content kinds a reshaped carousel card cannot carry, in report order. */
|
|
291
|
+
const LOST_CONTENT_KINDS = [
|
|
292
|
+
"images",
|
|
293
|
+
"tables",
|
|
294
|
+
"charts",
|
|
295
|
+
"controls"
|
|
296
|
+
];
|
|
297
|
+
const listPhrase = (items) => items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
298
|
+
/**
|
|
299
|
+
* Collects the kinds of content a reshape drops, walking the same tree
|
|
300
|
+
* {@link collectText} does so a node nested below the top level counts too.
|
|
301
|
+
* These are exactly the pieces text cannot represent: an image has no text, a
|
|
302
|
+
* table and a chart carry theirs in array props, and a control is behavior,
|
|
303
|
+
* whether or not it carries an action of its own. An `$action` counts only on
|
|
304
|
+
* a node that renders a control from it, since a `Box` or a `Row` carrying one
|
|
305
|
+
* renders no control on the clean path either.
|
|
306
|
+
*/
|
|
307
|
+
const scanLostContent = (node, into, depth) => {
|
|
308
|
+
if (depth > 64) return;
|
|
309
|
+
if (Array.isArray(node)) {
|
|
310
|
+
for (const child of node) scanLostContent(child, into, depth + 1);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (node === void 0 || !isElement(node)) return;
|
|
314
|
+
if (CONTROL_TYPES.has(node.type) || node.type === "ListViewItem" && isRecord(node.action) || node.type === "Card" && (isRecord(node.props["confirm"]) || isRecord(node.props["cancel"]))) into.add("controls");
|
|
315
|
+
if (node.type === "Image") into.add("images");
|
|
316
|
+
if (node.type === "Table") into.add("tables");
|
|
317
|
+
if (node.type === "Chart") into.add("charts");
|
|
318
|
+
scanLostContent(node.children, into, depth + 1);
|
|
319
|
+
};
|
|
270
320
|
/**
|
|
271
321
|
* 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
|
-
*
|
|
322
|
+
* A carousel cannot fall back to a block sequence like a standalone card can,
|
|
323
|
+
* so the card is reshaped to the two text fields the block has. Text reachable
|
|
324
|
+
* by {@link collectText} survives at any depth; an image, a table, a chart,
|
|
325
|
+
* and a control do not, and are reported separately from the reshape itself.
|
|
274
326
|
*/
|
|
275
327
|
const degradeCard = (element, context, depth) => {
|
|
276
328
|
const rawTitle = asString(element.props["title"]);
|
|
277
329
|
const textChunks = collectText(element.children, depth + 1);
|
|
278
330
|
const titleSource = rawTitle || textChunks[0] || "";
|
|
279
331
|
const bodyChunks = rawTitle ? textChunks : textChunks.slice(1);
|
|
280
|
-
const titleText = titleSource
|
|
281
|
-
const bodyText = bodyChunks.join("\n")
|
|
282
|
-
warn(context, "
|
|
332
|
+
const titleText = clampText(titleSource, 150, "Card", "title", context);
|
|
333
|
+
const bodyText = clampText(bodyChunks.join("\n"), 200, "Card", "body", context);
|
|
334
|
+
warn(context, "fallback", "Card", "A card inside a carousel was reshaped to title and body.");
|
|
335
|
+
const lostKinds = /* @__PURE__ */ new Set();
|
|
336
|
+
if (isRecord(element.props["confirm"]) || isRecord(element.props["cancel"])) lostKinds.add("controls");
|
|
337
|
+
scanLostContent(element.children, lostKinds, depth + 1);
|
|
338
|
+
const lost = LOST_CONTENT_KINDS.filter((kind) => lostKinds.has(kind));
|
|
339
|
+
if (lost.length > 0) warn(context, "dropped", "Card", `A reshaped carousel card's ${listPhrase(lost)} were dropped.`);
|
|
283
340
|
return buildCardBlock({
|
|
284
341
|
...titleText ? { title: {
|
|
285
342
|
type: "mrkdwn",
|
|
@@ -326,13 +383,47 @@ const convertListItem = (element, context, depth) => {
|
|
|
326
383
|
...accessory !== void 0 ? { accessory } : {}
|
|
327
384
|
};
|
|
328
385
|
};
|
|
386
|
+
/**
|
|
387
|
+
* Converts a child whose output is thrown away, and reports whether anything
|
|
388
|
+
* was lost with it. A scratch context keeps the throwaway out of the shared
|
|
389
|
+
* markdown and data-table budgets that surviving blocks still need; only its
|
|
390
|
+
* `dropped` warnings are forwarded, because those describe the tree the caller
|
|
391
|
+
* wrote, while a clamp or a fallback would describe content never delivered.
|
|
392
|
+
*/
|
|
393
|
+
const discardedChild = (child, context, depth) => {
|
|
394
|
+
const scratch = {
|
|
395
|
+
...context,
|
|
396
|
+
warnings: []
|
|
397
|
+
};
|
|
398
|
+
const produced = convertSequence(child, scratch, depth).length > 0;
|
|
399
|
+
const lost = scratch.warnings.filter((warning) => warning.code === "dropped");
|
|
400
|
+
context.warnings.push(...lost);
|
|
401
|
+
return produced || lost.length > 0;
|
|
402
|
+
};
|
|
329
403
|
const convertListView = (element, context, depth) => {
|
|
330
|
-
|
|
404
|
+
const items = [];
|
|
405
|
+
let discarded = 0;
|
|
406
|
+
for (const child of normalizedList(element.children)) {
|
|
407
|
+
if (isElement(child) && child.type === "ListViewItem") {
|
|
408
|
+
items.push(child);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (discardedChild(child, context, depth + 1)) discarded += 1;
|
|
412
|
+
}
|
|
413
|
+
if (discarded > 0) warn(context, "dropped", "ListView", `${discarded} non-item ${discarded === 1 ? "child was" : "children were"} dropped.`);
|
|
414
|
+
return items.flatMap((item, index) => [...index > 0 ? [{ type: "divider" }] : [], convertListItem(item, context, depth + 1)]);
|
|
331
415
|
};
|
|
332
416
|
const convertCarousel = (element, context, depth) => {
|
|
333
417
|
const cardChildren = [];
|
|
334
|
-
|
|
335
|
-
|
|
418
|
+
let droppedCards = 0;
|
|
419
|
+
for (const child of normalizedList(element.children)) {
|
|
420
|
+
if (isElement(child) && child.type === "Card") {
|
|
421
|
+
cardChildren.push(child);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (discardedChild(child, context, depth + 1)) droppedCards += 1;
|
|
425
|
+
}
|
|
426
|
+
if (droppedCards > 0) warn(context, "dropped", "Carousel", `${droppedCards} non-card ${droppedCards === 1 ? "child was" : "children were"} dropped.`);
|
|
336
427
|
if (cardChildren.length > 10) warn(context, "clamped", "Carousel", `cards were clamped to 10 entries.`);
|
|
337
428
|
const cards = cardChildren.slice(0, 10).map((card) => convertCarouselCard(card, context, depth + 1)).filter((card) => card !== void 0);
|
|
338
429
|
if (cards.length < 1) {
|
|
@@ -356,12 +447,15 @@ const convertTable = (element, context) => {
|
|
|
356
447
|
const rawColumns = Array.isArray(element.props["columns"]) ? element.props["columns"] : [];
|
|
357
448
|
const rawRows = Array.isArray(element.props["rows"]) ? element.props["rows"] : [];
|
|
358
449
|
if (rawColumns.length > 20) warn(context, "clamped", "Table", `columns were clamped to 20 entries.`);
|
|
359
|
-
if (rawRows.length >
|
|
360
|
-
const
|
|
450
|
+
if (rawRows.length > 200) warn(context, "clamped", "Table", `rows were clamped to 200 entries.`);
|
|
451
|
+
const takenColumns = rawColumns.slice(0, 20);
|
|
452
|
+
const unlabeled = takenColumns.filter((column) => !isRecord(column) || typeof column["label"] !== "string").length;
|
|
453
|
+
if (unlabeled > 0) warn(context, "dropped", "Table", `${unlabeled} column ${unlabeled === 1 ? "header was" : "headers were"} left blank for want of a string label.`);
|
|
454
|
+
const columnHeaderRow = takenColumns.map((column) => ({
|
|
361
455
|
type: "raw_text",
|
|
362
|
-
text: asString(column["label"])
|
|
456
|
+
text: isRecord(column) ? asString(column["label"]) : ""
|
|
363
457
|
}));
|
|
364
|
-
const dataRows = rawRows.slice(0,
|
|
458
|
+
const dataRows = rawRows.slice(0, 200).map((row) => (Array.isArray(row) ? row : []).slice(0, 20).map((value) => toDataTableCell(value) ?? {
|
|
365
459
|
type: "raw_text",
|
|
366
460
|
text: ""
|
|
367
461
|
}));
|
|
@@ -372,7 +466,7 @@ const convertTable = (element, context) => {
|
|
|
372
466
|
}))];
|
|
373
467
|
const headerRow = padRow(columnHeaderRow);
|
|
374
468
|
const headerCharacters = rowCharacters(headerRow);
|
|
375
|
-
if (context.dataTableCharacters + headerCharacters >
|
|
469
|
+
if (context.dataTableCharacters + headerCharacters > 2e4) {
|
|
376
470
|
warn(context, "dropped", "Table", `the table was dropped because its header row exceeds the ${DATA_TABLE_CHAR_BUDGET}-character table budget.`);
|
|
377
471
|
return [];
|
|
378
472
|
}
|
|
@@ -381,7 +475,7 @@ const convertTable = (element, context) => {
|
|
|
381
475
|
let budgetClamped = false;
|
|
382
476
|
for (const row of dataRows.map(padRow)) {
|
|
383
477
|
const chars = rowCharacters(row);
|
|
384
|
-
if (context.dataTableCharacters + tableCharacters + chars >
|
|
478
|
+
if (context.dataTableCharacters + tableCharacters + chars > 2e4) {
|
|
385
479
|
budgetClamped = true;
|
|
386
480
|
break;
|
|
387
481
|
}
|
|
@@ -616,10 +710,11 @@ function toSlackBlocks(node, options) {
|
|
|
616
710
|
dataTableCharacters: 0
|
|
617
711
|
};
|
|
618
712
|
try {
|
|
619
|
-
const
|
|
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
|
|
713
|
+
const bounded = boundSpec(node, (reason) => {
|
|
714
|
+
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 32 levels were dropped.` : `children were clamped to 200 entries.`;
|
|
621
715
|
warn(context, "clamped", "Root", detail);
|
|
622
|
-
})
|
|
716
|
+
});
|
|
717
|
+
const { root } = normalizeSpec(bounded);
|
|
623
718
|
const converted = convertSequence(root, context, 0);
|
|
624
719
|
if (converted.length <= context.blockCap) return {
|
|
625
720
|
blocks: converted,
|