@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 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. Importing this package registers `marked`
39
- as the renderer's `onProcessMarkdown` processor, degrading to plain text if a
40
- string fails to parse, and the stylesheet re-applies list markers / paragraph
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.63",
28
- "@dbx-tools/shared-teams": "0.6.63",
29
- "@dbx-tools/ui-appkit": "0.6.63",
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.63",
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 * as AdaptiveCards from "adaptivecards";
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
- installMarkdownProcessor();
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 with the `adaptivecards` renderer.
60
- * Re-renders whenever the card JSON changes.
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 = ({ card, onOpenUrl, className }: AdaptiveCardViewProps) => {
63
- const hostRef = useRef<HTMLDivElement>(null);
64
-
65
- useEffect(() => {
66
- const host = hostRef.current;
67
- if (!host) return;
68
- const rendered = new AdaptiveCards.AdaptiveCard();
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
+ );