@dbx-tools/ui-teams 0.6.63 → 0.6.64
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/README.md +6 -5
- package/package.json +4 -4
- package/src/react/_adaptive-card-renderer.tsx +58 -0
- package/src/react/adaptive-card.tsx +11 -72
package/README.md
CHANGED
|
@@ -25,7 +25,8 @@ contract from [`@dbx-tools/shared-teams`](../../shared/teams) and renders throug
|
|
|
25
25
|
- `AdaptiveCardView` - a thin React wrapper over the imperative `adaptivecards`
|
|
26
26
|
renderer: feed it a compiled Adaptive Card document and it mounts the rendered
|
|
27
27
|
node, re-rendering on change and wiring `Action.OpenUrl` clicks (open in a new
|
|
28
|
-
tab by default, or intercept with `onOpenUrl`).
|
|
28
|
+
tab by default, or intercept with `onOpenUrl`). The renderer is loaded only
|
|
29
|
+
when the first card mounts.
|
|
29
30
|
- `AdaptiveCardGallery` - a self-contained dev tool: edit a `CardSpec` (or pick a
|
|
30
31
|
sample), compile it through the server's `POST /api/teams/card` route, and see
|
|
31
32
|
the card render live. This is the drop-in "display them" surface for a dev
|
|
@@ -35,10 +36,10 @@ contract from [`@dbx-tools/shared-teams`](../../shared/teams) and renders throug
|
|
|
35
36
|
- Markdown in a `TextBlock` actually renders. A `TextBlock` is markdown per the
|
|
36
37
|
Adaptive Cards spec, but `adaptivecards` ships no parser - it leaves the
|
|
37
38
|
implementation (and its sanitization) to the host, so `**bold**` renders
|
|
38
|
-
literally until one is installed.
|
|
39
|
-
as the renderer's `onProcessMarkdown` processor, degrading to plain
|
|
40
|
-
string fails to parse, and the stylesheet re-applies list markers /
|
|
41
|
-
spacing that Tailwind's preflight resets away.
|
|
39
|
+
literally until one is installed. Mounting the first card loads and registers
|
|
40
|
+
`marked` as the renderer's `onProcessMarkdown` processor, degrading to plain
|
|
41
|
+
text if a string fails to parse, and the stylesheet re-applies list markers /
|
|
42
|
+
paragraph spacing that Tailwind's preflight resets away.
|
|
42
43
|
|
|
43
44
|
## Simulate A Teams Chat
|
|
44
45
|
|
package/package.json
CHANGED
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"typescript": "^5.9.3"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@dbx-tools/shared-core": "0.6.
|
|
28
|
-
"@dbx-tools/shared-teams": "0.6.
|
|
29
|
-
"@dbx-tools/ui-appkit": "0.6.
|
|
27
|
+
"@dbx-tools/shared-core": "0.6.64",
|
|
28
|
+
"@dbx-tools/shared-teams": "0.6.64",
|
|
29
|
+
"@dbx-tools/ui-appkit": "0.6.64",
|
|
30
30
|
"adaptivecards": "^3.0.5",
|
|
31
31
|
"marked": "^18.0.5",
|
|
32
32
|
"react": "^19.2.4",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
|
-
"version": "0.6.
|
|
39
|
+
"version": "0.6.64",
|
|
40
40
|
"type": "module",
|
|
41
41
|
"exports": {
|
|
42
42
|
"./react": "./src/react/index.ts",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imperative Adaptive Cards renderer loaded behind the public React wrapper.
|
|
3
|
+
*
|
|
4
|
+
* The `adaptivecards` package leaves TextBlock markdown processing to its host,
|
|
5
|
+
* so this module installs the shared `marked` processor when the renderer chunk
|
|
6
|
+
* is first requested.
|
|
7
|
+
*/
|
|
8
|
+
import * as AdaptiveCards from "adaptivecards";
|
|
9
|
+
import { marked } from "marked";
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
import type { AdaptiveCardViewProps } from "./adaptive-card.tsx";
|
|
12
|
+
|
|
13
|
+
/** Install the process-wide markdown callback required by Adaptive Cards. */
|
|
14
|
+
function _installMarkdownProcessor(): void {
|
|
15
|
+
AdaptiveCards.AdaptiveCard.onProcessMarkdown = (text, result) => {
|
|
16
|
+
try {
|
|
17
|
+
result.outputHtml = marked.parse(text, { async: false, breaks: true }) as string;
|
|
18
|
+
result.didProcess = true;
|
|
19
|
+
} catch {
|
|
20
|
+
result.didProcess = false;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_installMarkdownProcessor();
|
|
26
|
+
|
|
27
|
+
/** Parse and mount one Adaptive Card document into a host element. */
|
|
28
|
+
const AdaptiveCardRenderer = ({ card, onOpenUrl, className }: AdaptiveCardViewProps) => {
|
|
29
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const host = hostRef.current;
|
|
33
|
+
if (!host) return;
|
|
34
|
+
const rendered = new AdaptiveCards.AdaptiveCard();
|
|
35
|
+
rendered.onExecuteAction = (action) => {
|
|
36
|
+
if (action instanceof AdaptiveCards.OpenUrlAction && action.url) {
|
|
37
|
+
if (onOpenUrl) onOpenUrl(action.url);
|
|
38
|
+
else window.open(action.url, "_blank", "noopener,noreferrer");
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
rendered.parse(card as unknown as Record<string, unknown>);
|
|
43
|
+
const element = rendered.render();
|
|
44
|
+
host.replaceChildren(...(element ? [element] : []));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
host.replaceChildren(
|
|
47
|
+
Object.assign(document.createElement("pre"), {
|
|
48
|
+
textContent: `Failed to render card: ${(error as Error).message}`,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return () => host.replaceChildren();
|
|
53
|
+
}, [card, onOpenUrl]);
|
|
54
|
+
|
|
55
|
+
return <div ref={hostRef} className={className} data-testid="adaptive-card" />;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default AdaptiveCardRenderer;
|
|
@@ -1,46 +1,7 @@
|
|
|
1
|
-
// React wrapper over the Adaptive Cards JavaScript renderer (the
|
|
2
|
-
// `adaptivecards` npm package). The renderer is imperative - you feed it card
|
|
3
|
-
// JSON and it returns a rendered `HTMLElement` - so this component parses the
|
|
4
|
-
// document and mounts the rendered node into a ref on every change, and wires
|
|
5
|
-
// `Action.OpenUrl` clicks to an optional handler (defaulting to opening the URL
|
|
6
|
-
// in a new tab). This is the same renderer many internal Teams preview tools
|
|
7
|
-
// embed to show cards outside Teams.
|
|
8
|
-
//
|
|
9
|
-
// The renderer deliberately ships NO markdown parser: a `TextBlock` is markdown
|
|
10
|
-
// per the Adaptive Cards spec, but `adaptivecards` leaves the implementation to
|
|
11
|
-
// the host so each host can pick its own (and its own sanitization). Without a
|
|
12
|
-
// host processor Teams' own `**bold**` renders literally, so this module
|
|
13
|
-
// installs `marked` as the processor once at import - see
|
|
14
|
-
// {@link installMarkdownProcessor}.
|
|
15
|
-
|
|
16
1
|
import type { AdaptiveCard as AdaptiveCardDocument } from "@dbx-tools/shared-teams";
|
|
17
|
-
import
|
|
18
|
-
import { marked } from "marked";
|
|
19
|
-
import { useEffect, useRef } from "react";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Teach the renderer to process `TextBlock` markdown with `marked`.
|
|
23
|
-
*
|
|
24
|
-
* `onProcessMarkdown` is a STATIC hook on `AdaptiveCard`, so this runs once per
|
|
25
|
-
* module load rather than per component. `didProcess` must be set to `true` or
|
|
26
|
-
* the renderer discards the HTML and falls back to the raw text.
|
|
27
|
-
*
|
|
28
|
-
* `marked` is called in a try/catch and falls back to leaving the text alone:
|
|
29
|
-
* card text can come from a model, and a markdown edge case should degrade to
|
|
30
|
-
* plain text rather than blanking the card.
|
|
31
|
-
*/
|
|
32
|
-
const installMarkdownProcessor = () => {
|
|
33
|
-
AdaptiveCards.AdaptiveCard.onProcessMarkdown = (text, result) => {
|
|
34
|
-
try {
|
|
35
|
-
result.outputHtml = marked.parse(text, { async: false, breaks: true }) as string;
|
|
36
|
-
result.didProcess = true;
|
|
37
|
-
} catch {
|
|
38
|
-
result.didProcess = false;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
};
|
|
2
|
+
import { lazy, Suspense } from "react";
|
|
42
3
|
|
|
43
|
-
|
|
4
|
+
const AdaptiveCardRenderer = lazy(() => import("./_adaptive-card-renderer.tsx"));
|
|
44
5
|
|
|
45
6
|
/** Props for {@link AdaptiveCardView}. */
|
|
46
7
|
export interface AdaptiveCardViewProps {
|
|
@@ -56,35 +17,13 @@ export interface AdaptiveCardViewProps {
|
|
|
56
17
|
}
|
|
57
18
|
|
|
58
19
|
/**
|
|
59
|
-
* Render a single Adaptive Card document
|
|
60
|
-
*
|
|
20
|
+
* Render a single Adaptive Card document, loading the imperative renderer and
|
|
21
|
+
* markdown processor only when a card is actually mounted.
|
|
61
22
|
*/
|
|
62
|
-
export const AdaptiveCardView = (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
rendered.onExecuteAction = (action) => {
|
|
70
|
-
if (action instanceof AdaptiveCards.OpenUrlAction && action.url) {
|
|
71
|
-
if (onOpenUrl) onOpenUrl(action.url);
|
|
72
|
-
else window.open(action.url, "_blank", "noopener,noreferrer");
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
try {
|
|
76
|
-
rendered.parse(card as unknown as Record<string, unknown>);
|
|
77
|
-
const element = rendered.render();
|
|
78
|
-
host.replaceChildren(...(element ? [element] : []));
|
|
79
|
-
} catch (err) {
|
|
80
|
-
host.replaceChildren(
|
|
81
|
-
Object.assign(document.createElement("pre"), {
|
|
82
|
-
textContent: `Failed to render card: ${(err as Error).message}`,
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
return () => host.replaceChildren();
|
|
87
|
-
}, [card, onOpenUrl]);
|
|
88
|
-
|
|
89
|
-
return <div ref={hostRef} className={className} data-testid="adaptive-card" />;
|
|
90
|
-
};
|
|
23
|
+
export const AdaptiveCardView = (props: AdaptiveCardViewProps) => (
|
|
24
|
+
<Suspense
|
|
25
|
+
fallback={<div className={props.className} data-testid="adaptive-card" aria-busy="true" />}
|
|
26
|
+
>
|
|
27
|
+
<AdaptiveCardRenderer {...props} />
|
|
28
|
+
</Suspense>
|
|
29
|
+
);
|