@dbx-tools/ui-mastra 0.1.23 → 0.3.1
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/index.ts +1 -1
- package/package.json +7 -6
- package/src/react/export-menu.tsx +10 -5
- package/src/react/mastra-chat.tsx +28 -2
- package/src/support/export.ts +115 -43
package/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type { UseMastraChatOptions, MastraChatProps } from "./src/react/mastra-c
|
|
|
26
26
|
export type { SuggestionPillsProps } from "./src/react/suggestion-pills";
|
|
27
27
|
export type { ThreadSidebarProps } from "./src/react/thread-sidebar";
|
|
28
28
|
export type { ChatStatus, ToolEvent, ToolProgress, ChatModelOption, FeedbackValue, FeedbackSubmission, MessageFeedback, ThreadSummary, ChatViewProps, ApprovalDecision, PendingApproval } from "./src/react/types";
|
|
29
|
-
export type { ExportFormat, EmbedResolver, ExportChatOptions } from "./src/support/export";
|
|
29
|
+
export type { ExportFormat, ExportBrand, EmbedResolver, ExportChatOptions } from "./src/support/export";
|
|
30
30
|
export type { ByIdFetchState } from "./src/support/mastra-client";
|
|
31
31
|
export type { MastraStreamChunk, MastraStreamResponse } from "./src/support/mastra-stream";
|
|
32
32
|
export type { ThreadSession } from "./src/support/thread-sessions";
|
package/package.json
CHANGED
|
@@ -26,18 +26,19 @@
|
|
|
26
26
|
"shiki": "^3.0.0",
|
|
27
27
|
"sql-formatter": "^15.6.9",
|
|
28
28
|
"streamdown": "^2.5.0",
|
|
29
|
-
"@dbx-tools/shared-core": "0.1
|
|
30
|
-
"@dbx-tools/shared-
|
|
31
|
-
"@dbx-tools/
|
|
32
|
-
"@dbx-tools/shared-
|
|
33
|
-
"@dbx-tools/
|
|
29
|
+
"@dbx-tools/shared-core": "0.3.1",
|
|
30
|
+
"@dbx-tools/shared-genie": "0.3.1",
|
|
31
|
+
"@dbx-tools/shared-model": "0.3.1",
|
|
32
|
+
"@dbx-tools/shared-mastra": "0.3.1",
|
|
33
|
+
"@dbx-tools/ui-appkit": "0.3.1",
|
|
34
|
+
"@dbx-tools/ui-branding": "0.3.1"
|
|
34
35
|
},
|
|
35
36
|
"main": "index.ts",
|
|
36
37
|
"license": "UNLICENSED",
|
|
37
38
|
"publishConfig": {
|
|
38
39
|
"access": "public"
|
|
39
40
|
},
|
|
40
|
-
"version": "0.1
|
|
41
|
+
"version": "0.3.1",
|
|
41
42
|
"types": "index.ts",
|
|
42
43
|
"type": "module",
|
|
43
44
|
"exports": {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
TooltipContent,
|
|
9
9
|
TooltipTrigger,
|
|
10
10
|
} from "@dbx-tools/ui-appkit/react";
|
|
11
|
-
import { DownloadIcon } from "lucide-react";
|
|
11
|
+
import { DownloadIcon, FileTextIcon } from "lucide-react";
|
|
12
12
|
import type { ExportFormat } from "../support/export";
|
|
13
13
|
|
|
14
14
|
// Shared export affordance: a download button that opens a small menu of
|
|
@@ -16,9 +16,13 @@ import type { ExportFormat } from "../support/export";
|
|
|
16
16
|
// labelled) and per-message export (bubble action row, icon-only).
|
|
17
17
|
|
|
18
18
|
/** Menu entries, in display order. */
|
|
19
|
-
const FORMATS: ReadonlyArray<{
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const FORMATS: ReadonlyArray<{
|
|
20
|
+
format: ExportFormat;
|
|
21
|
+
label: string;
|
|
22
|
+
Icon: typeof DownloadIcon;
|
|
23
|
+
}> = [
|
|
24
|
+
{ format: "pdf", label: "PDF", Icon: DownloadIcon },
|
|
25
|
+
{ format: "markdown", label: "Markdown", Icon: FileTextIcon },
|
|
22
26
|
];
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -65,8 +69,9 @@ export const ExportMenu = ({
|
|
|
65
69
|
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
|
|
66
70
|
)}
|
|
67
71
|
<DropdownMenuContent align="end">
|
|
68
|
-
{FORMATS.map(({ format, label }) => (
|
|
72
|
+
{FORMATS.map(({ format, label, Icon }) => (
|
|
69
73
|
<DropdownMenuItem key={format} onClick={() => onExport(format)}>
|
|
74
|
+
<Icon className="size-3.5" />
|
|
70
75
|
{label}
|
|
71
76
|
</DropdownMenuItem>
|
|
72
77
|
))}
|
|
@@ -7,6 +7,7 @@ import type { UIMessage } from "ai";
|
|
|
7
7
|
import { nanoid } from "nanoid";
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { exportChat, type EmbedResolver, type ExportFormat } from "../support/export";
|
|
10
|
+
import { useBrand } from "@dbx-tools/ui-branding/react";
|
|
10
11
|
import {
|
|
11
12
|
useMastraClient,
|
|
12
13
|
useMastraModels,
|
|
@@ -1139,6 +1140,21 @@ export const useMastraChat = (
|
|
|
1139
1140
|
const resourceId = threads.find((t) => t.resourceId)?.resourceId;
|
|
1140
1141
|
return resourceId ? `User (${resourceId})` : "User";
|
|
1141
1142
|
}, [threads]);
|
|
1143
|
+
// Brand styling for the exported document, resolved from the active
|
|
1144
|
+
// BrandProvider (or the built-in dbx-tools default when the host wired
|
|
1145
|
+
// none). The logo asset id is resolved to a portable data URL so the
|
|
1146
|
+
// export is self-contained; colors/font come straight from the context.
|
|
1147
|
+
const { context: brandContext, resolveAsset: resolveBrandAsset } = useBrand();
|
|
1148
|
+
const exportBrand = useMemo(
|
|
1149
|
+
() => ({
|
|
1150
|
+
logoDataUrl: resolveBrandAsset(brandContext.assets.logo.light),
|
|
1151
|
+
primary: brandContext.colors.primary,
|
|
1152
|
+
accent: brandContext.colors.accent,
|
|
1153
|
+
foreground: brandContext.colors.foreground,
|
|
1154
|
+
fontSans: brandContext.typography.sans,
|
|
1155
|
+
}),
|
|
1156
|
+
[brandContext, resolveBrandAsset],
|
|
1157
|
+
);
|
|
1142
1158
|
const exportConversation = useCallback(
|
|
1143
1159
|
async (format: ExportFormat) => {
|
|
1144
1160
|
const title =
|
|
@@ -1151,6 +1167,7 @@ export const useMastraChat = (
|
|
|
1151
1167
|
resolver: exportResolver,
|
|
1152
1168
|
title,
|
|
1153
1169
|
userLabel: exportUserLabel,
|
|
1170
|
+
brand: exportBrand,
|
|
1154
1171
|
});
|
|
1155
1172
|
} catch (error) {
|
|
1156
1173
|
logger.error("conversation export error", {
|
|
@@ -1159,7 +1176,15 @@ export const useMastraChat = (
|
|
|
1159
1176
|
});
|
|
1160
1177
|
}
|
|
1161
1178
|
},
|
|
1162
|
-
[
|
|
1179
|
+
[
|
|
1180
|
+
exportResolver,
|
|
1181
|
+
activeThreadId,
|
|
1182
|
+
activeKey,
|
|
1183
|
+
getSession,
|
|
1184
|
+
threads,
|
|
1185
|
+
exportUserLabel,
|
|
1186
|
+
exportBrand,
|
|
1187
|
+
],
|
|
1163
1188
|
);
|
|
1164
1189
|
const exportMessage = useCallback(
|
|
1165
1190
|
async (message: UIMessage, format: ExportFormat) => {
|
|
@@ -1171,6 +1196,7 @@ export const useMastraChat = (
|
|
|
1171
1196
|
title: "Message",
|
|
1172
1197
|
filename: "message",
|
|
1173
1198
|
userLabel: exportUserLabel,
|
|
1199
|
+
brand: exportBrand,
|
|
1174
1200
|
});
|
|
1175
1201
|
} catch (error) {
|
|
1176
1202
|
logger.error("message export error", {
|
|
@@ -1179,7 +1205,7 @@ export const useMastraChat = (
|
|
|
1179
1205
|
});
|
|
1180
1206
|
}
|
|
1181
1207
|
},
|
|
1182
|
-
[exportResolver, exportUserLabel],
|
|
1208
|
+
[exportResolver, exportUserLabel, exportBrand],
|
|
1183
1209
|
);
|
|
1184
1210
|
|
|
1185
1211
|
// Submit thumbs / comment feedback for an assistant message to MLflow
|
package/src/support/export.ts
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
* the single-message or the whole-conversation level, in one of two
|
|
6
6
|
* formats:
|
|
7
7
|
*
|
|
8
|
-
* - `"pdf"` - a print-ready HTML document
|
|
9
|
-
* the browser's print dialog triggered, so the user
|
|
10
|
-
* a real PDF ("Save as PDF")
|
|
8
|
+
* - `"pdf"` - a print-ready HTML document rendered in a hidden iframe
|
|
9
|
+
* with the browser's print dialog triggered, so the user
|
|
10
|
+
* saves a real PDF ("Save as PDF") with no popup tab.
|
|
11
11
|
* - `"markdown"` - a `.md` file download.
|
|
12
12
|
*
|
|
13
|
+
* The document can be brand-styled (logo, colors, font) via the optional
|
|
14
|
+
* `brand` option; the driver resolves it from `@dbx-tools/ui-branding`.
|
|
15
|
+
*
|
|
13
16
|
* Charts are included, not dropped: each `[chart:<id>]` marker is
|
|
14
17
|
* resolved against the plugin's chart cache and rendered to an inline
|
|
15
18
|
* SVG via Echarts' server-side renderer (no DOM needed), so a PDF/HTML
|
|
@@ -29,9 +32,33 @@ import * as echarts from "echarts";
|
|
|
29
32
|
import { marked } from "marked";
|
|
30
33
|
import { normalizeChartOption } from "./chart-option";
|
|
31
34
|
|
|
32
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Output formats {@link exportChat} can produce.
|
|
37
|
+
* - `"pdf"` - Save-as-PDF via a hidden print iframe (dialog defaults to
|
|
38
|
+
* "Save as PDF"); no new tab.
|
|
39
|
+
* - `"markdown"` - a `.md` file download.
|
|
40
|
+
*/
|
|
33
41
|
export type ExportFormat = "pdf" | "markdown";
|
|
34
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Optional brand styling for the exported document. Plain data (no React /
|
|
45
|
+
* DOM), so this module stays framework-free; the driver resolves it from
|
|
46
|
+
* `@dbx-tools/ui-branding` (`useBrand()` + `resolveBrandAsset`) and passes it
|
|
47
|
+
* through. Any field omitted falls back to the neutral default styling.
|
|
48
|
+
*/
|
|
49
|
+
export interface ExportBrand {
|
|
50
|
+
/** Logo image src (a data URL) rendered in the document header. */
|
|
51
|
+
logoDataUrl?: string;
|
|
52
|
+
/** Primary brand color for the header rule, headings, and role labels. */
|
|
53
|
+
primary?: string;
|
|
54
|
+
/** Accent brand color (links). */
|
|
55
|
+
accent?: string;
|
|
56
|
+
/** Body text color. */
|
|
57
|
+
foreground?: string;
|
|
58
|
+
/** Sans-serif font stack for the document body. */
|
|
59
|
+
fontSans?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/**
|
|
36
63
|
* Resolves the embeds referenced by `[chart:<id>]` / `[data:<id>]`
|
|
37
64
|
* markers in message prose. Satisfied by `MastraPluginClient` (its
|
|
@@ -61,6 +88,8 @@ export interface ExportChatOptions {
|
|
|
61
88
|
* id / email form like `"User (someone@example.com)"`.
|
|
62
89
|
*/
|
|
63
90
|
userLabel?: string;
|
|
91
|
+
/** Optional brand styling for the exported document. */
|
|
92
|
+
brand?: ExportBrand;
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
/** Fixed Echarts SSR canvas; the SVG scales to the print column via CSS. */
|
|
@@ -71,14 +100,13 @@ const CHART_HEIGHT_PX = 380;
|
|
|
71
100
|
const PRINT_SETTLE_MS = 300;
|
|
72
101
|
|
|
73
102
|
/**
|
|
74
|
-
* Export `messages` as a PDF
|
|
103
|
+
* Export `messages` as a PDF or a Markdown file.
|
|
75
104
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* still succeeds.
|
|
105
|
+
* `"pdf"` renders a branded, self-contained HTML document and drives it
|
|
106
|
+
* through a hidden `<iframe>` + `print()` - so the browser's print dialog
|
|
107
|
+
* (defaulting to "Save as PDF") opens directly, with no popup tab. If the
|
|
108
|
+
* iframe path can't run (e.g. no DOM body), the document is downloaded as a
|
|
109
|
+
* self-contained `.html` file so the export - charts included - still lands.
|
|
82
110
|
*/
|
|
83
111
|
export async function exportChat(options: ExportChatOptions): Promise<void> {
|
|
84
112
|
const { messages, format, resolver } = options;
|
|
@@ -92,22 +120,59 @@ export async function exportChat(options: ExportChatOptions): Promise<void> {
|
|
|
92
120
|
return;
|
|
93
121
|
}
|
|
94
122
|
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
// pdf: build the branded document, then print it from a hidden iframe so
|
|
124
|
+
// the Save-as-PDF dialog opens without a stray tab.
|
|
125
|
+
const html = await buildHtmlDocument(messages, resolver, title, userLabel, options.brand);
|
|
126
|
+
printViaHiddenIframe(html, `${stem}.html`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render `html` in an off-screen iframe and trigger its print dialog. The
|
|
131
|
+
* iframe is same-origin via `srcdoc`, so `contentWindow.print()` is allowed;
|
|
132
|
+
* it's removed after printing. Falls back to a file download when there's no
|
|
133
|
+
* document body to attach to.
|
|
134
|
+
*/
|
|
135
|
+
function printViaHiddenIframe(html: string, downloadName: string): void {
|
|
136
|
+
if (typeof document === "undefined" || !document.body) {
|
|
137
|
+
downloadTextFile(downloadName, html, "text/html;charset=utf-8");
|
|
104
138
|
return;
|
|
105
139
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
140
|
+
const iframe = document.createElement("iframe");
|
|
141
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
142
|
+
iframe.style.position = "fixed";
|
|
143
|
+
iframe.style.right = "0";
|
|
144
|
+
iframe.style.bottom = "0";
|
|
145
|
+
iframe.style.width = "0";
|
|
146
|
+
iframe.style.height = "0";
|
|
147
|
+
iframe.style.border = "0";
|
|
148
|
+
iframe.srcdoc = html;
|
|
149
|
+
|
|
150
|
+
let cleaned = false;
|
|
151
|
+
const cleanup = () => {
|
|
152
|
+
if (cleaned) return;
|
|
153
|
+
cleaned = true;
|
|
154
|
+
window.setTimeout(() => iframe.remove(), 1000);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
iframe.onload = () => {
|
|
158
|
+
const win = iframe.contentWindow;
|
|
159
|
+
if (!win) {
|
|
160
|
+
iframe.remove();
|
|
161
|
+
downloadTextFile(downloadName, html, "text/html;charset=utf-8");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Settle so charts / fonts lay out, then print. `afterprint` removes the
|
|
165
|
+
// iframe once the dialog closes (a timeout backstops browsers that don't
|
|
166
|
+
// fire it).
|
|
167
|
+
win.addEventListener("afterprint", cleanup);
|
|
168
|
+
win.setTimeout(() => {
|
|
169
|
+
win.focus();
|
|
170
|
+
win.print();
|
|
171
|
+
window.setTimeout(cleanup, 60000);
|
|
172
|
+
}, PRINT_SETTLE_MS);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
document.body.appendChild(iframe);
|
|
111
176
|
}
|
|
112
177
|
|
|
113
178
|
/* ------------------------------- segments -------------------------------- */
|
|
@@ -168,6 +233,7 @@ async function buildHtmlDocument(
|
|
|
168
233
|
resolver: EmbedResolver,
|
|
169
234
|
title: string,
|
|
170
235
|
userLabel: string,
|
|
236
|
+
brand?: ExportBrand,
|
|
171
237
|
): Promise<string> {
|
|
172
238
|
const articles: string[] = [];
|
|
173
239
|
for (const message of messages) {
|
|
@@ -179,11 +245,14 @@ async function buildHtmlDocument(
|
|
|
179
245
|
`<div class="content">${body}</div></article>`,
|
|
180
246
|
);
|
|
181
247
|
}
|
|
248
|
+
const logo = brand?.logoDataUrl
|
|
249
|
+
? `<img class="doc-logo" src="${escapeHtml(brand.logoDataUrl)}" alt="">`
|
|
250
|
+
: "";
|
|
182
251
|
return (
|
|
183
252
|
`<!doctype html><html lang="en"><head><meta charset="utf-8">` +
|
|
184
253
|
`<meta name="viewport" content="width=device-width, initial-scale=1">` +
|
|
185
|
-
`<title>${escapeHtml(title)}</title><style>${
|
|
186
|
-
`<body><header class="doc-header"
|
|
254
|
+
`<title>${escapeHtml(title)}</title><style>${buildDocumentCss(brand)}</style></head>` +
|
|
255
|
+
`<body><header class="doc-header">${logo}<h1>${escapeHtml(title)}</h1>` +
|
|
187
256
|
`<div class="doc-meta">Exported ${escapeHtml(new Date().toLocaleString())}</div>` +
|
|
188
257
|
`</header><main>${articles.join("")}</main></body></html>`
|
|
189
258
|
);
|
|
@@ -417,15 +486,10 @@ function downloadTextFile(filename: string, content: string, mime: string): void
|
|
|
417
486
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
418
487
|
}
|
|
419
488
|
|
|
420
|
-
/** Placeholder shown in the print tab while embeds resolve. */
|
|
421
|
-
const PREPARING_HTML =
|
|
422
|
-
'<!doctype html><html><head><meta charset="utf-8"><title>Preparing export...</title>' +
|
|
423
|
-
"<style>body{font:14px system-ui,sans-serif;color:#475569;display:flex;" +
|
|
424
|
-
"height:100vh;margin:0;align-items:center;justify-content:center}</style></head>" +
|
|
425
|
-
"<body>Preparing export...</body></html>";
|
|
426
|
-
|
|
427
489
|
/**
|
|
428
|
-
*
|
|
490
|
+
* Build the inline stylesheet for the exported document, folding in optional
|
|
491
|
+
* brand color / font overrides (falling back to the neutral slate/blue theme
|
|
492
|
+
* and a system font stack). Tuned for print:
|
|
429
493
|
*
|
|
430
494
|
* - A readable measure with role-labelled message blocks.
|
|
431
495
|
* - Content flows naturally across pages: whole messages are NOT
|
|
@@ -436,23 +500,30 @@ const PREPARING_HTML =
|
|
|
436
500
|
* rows - while long tables split across pages and repeat their
|
|
437
501
|
* header (`thead { display: table-header-group }`).
|
|
438
502
|
* - `@page { margin: 0 }` collapses the page margin box so the
|
|
439
|
-
* browser's own print header/footer (the
|
|
503
|
+
* browser's own print header/footer (the source URL, the
|
|
440
504
|
* date, and `n/N` page numbers) have nowhere to render and drop
|
|
441
505
|
* out; the visible page margin is re-supplied as body padding.
|
|
442
506
|
*/
|
|
443
|
-
|
|
507
|
+
function buildDocumentCss(brand?: ExportBrand): string {
|
|
508
|
+
const fg = brand?.foreground || "#0f172a";
|
|
509
|
+
const primary = brand?.primary || "#0f172a";
|
|
510
|
+
const link = brand?.accent || "#2563eb";
|
|
511
|
+
const font =
|
|
512
|
+
brand?.fontSans || 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
|
|
513
|
+
return `
|
|
444
514
|
:root { color-scheme: light; }
|
|
445
515
|
* { box-sizing: border-box; }
|
|
446
516
|
body {
|
|
447
|
-
font: 15px/1.6
|
|
448
|
-
color:
|
|
517
|
+
font: 15px/1.6 ${font};
|
|
518
|
+
color: ${fg}; margin: 0; padding: 32px; background: #fff;
|
|
449
519
|
}
|
|
450
520
|
main { max-width: 820px; margin: 0 auto; }
|
|
451
|
-
.doc-header { max-width: 820px; margin: 0 auto 24px; border-bottom:
|
|
452
|
-
.doc-
|
|
521
|
+
.doc-header { max-width: 820px; margin: 0 auto 24px; border-bottom: 2px solid ${primary}; padding-bottom: 12px; }
|
|
522
|
+
.doc-logo { height: 28px; width: auto; display: block; margin: 0 0 10px; }
|
|
523
|
+
.doc-header h1 { font-size: 22px; margin: 0 0 4px; color: ${primary}; }
|
|
453
524
|
.doc-meta { font-size: 12px; color: #64748b; }
|
|
454
525
|
.msg { margin: 0 0 20px; }
|
|
455
|
-
.msg .role { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color:
|
|
526
|
+
.msg .role { font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: ${primary}; margin-bottom: 4px; break-after: avoid; }
|
|
456
527
|
.msg-user .content { background: #f1f5f9; border-radius: 8px; padding: 10px 14px; }
|
|
457
528
|
.msg .content > *:first-child { margin-top: 0; }
|
|
458
529
|
.msg .content > *:last-child { margin-bottom: 0; }
|
|
@@ -462,7 +533,7 @@ const PRINT_CSS = `
|
|
|
462
533
|
code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .9em; background: #f1f5f9; padding: .1em .3em; border-radius: 4px; }
|
|
463
534
|
pre { background: #0f172a; color: #e2e8f0; padding: 12px 14px; border-radius: 8px; overflow-x: auto; break-inside: avoid; }
|
|
464
535
|
pre code { background: none; padding: 0; color: inherit; }
|
|
465
|
-
a { color:
|
|
536
|
+
a { color: ${link}; }
|
|
466
537
|
.embed { margin: 12px 0; }
|
|
467
538
|
.embed-chart { border: 1px solid #e2e8f0; border-radius: 8px; padding: 8px; break-inside: avoid; }
|
|
468
539
|
.embed-chart svg { max-width: 100%; height: auto; display: block; margin: 0 auto; }
|
|
@@ -478,3 +549,4 @@ const PRINT_CSS = `
|
|
|
478
549
|
a { color: inherit; text-decoration: none; }
|
|
479
550
|
}
|
|
480
551
|
`;
|
|
552
|
+
}
|