@braincrew-lab/langchain-canvas 0.7.1 → 0.7.4
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
|
@@ -18,13 +18,14 @@ const { sendMessage, messages, canvas } = useCanvasStream({ endpoint: "/api/chat
|
|
|
18
18
|
npm i @braincrew-lab/langchain-canvas
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Office formats use optional peers — install only what you need:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm i exceljs docx pptxgenjs fast-formula-parser
|
|
24
|
+
npm i exceljs docx pptxgenjs fast-formula-parser # export / import
|
|
25
|
+
npm i docx-preview # read an uploaded .docx on the canvas
|
|
25
26
|
```
|
|
26
27
|
|
|
27
|
-
If a format's package isn't installed, that one export
|
|
28
|
+
If a format's package isn't installed, that one feature says what to add and the rest keeps working: an export tells the user, and a Word upload falls back to the file card it showed before.
|
|
28
29
|
|
|
29
30
|
## Mount it
|
|
30
31
|
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { loadOptional } from './chunk-YZZSJJMQ.js';
|
|
2
|
+
import { useCanvasStore } from './chunk-RAPTQ6VH.js';
|
|
3
|
+
import { useRef, useState, useEffect } from 'react';
|
|
4
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/io/docxAddress.ts
|
|
7
|
+
var DOCX_ADDRESS_ATTRIBUTE = "data-canvas-docx";
|
|
8
|
+
var BODY_SELECTOR = "section.docx";
|
|
9
|
+
function bodyOf(section) {
|
|
10
|
+
return section.querySelector(":scope > article") ?? section;
|
|
11
|
+
}
|
|
12
|
+
function pages(root) {
|
|
13
|
+
const sections = Array.from(root.querySelectorAll(BODY_SELECTOR));
|
|
14
|
+
return sections.length > 0 ? sections : [root];
|
|
15
|
+
}
|
|
16
|
+
function stampDocxAddresses(root) {
|
|
17
|
+
const blocks = [];
|
|
18
|
+
let paragraphs = 0;
|
|
19
|
+
let tables = 0;
|
|
20
|
+
for (const page of pages(root)) {
|
|
21
|
+
for (const element of Array.from(bodyOf(page).children)) {
|
|
22
|
+
const tag = element.tagName.toLowerCase();
|
|
23
|
+
if (tag !== "p" && tag !== "table") continue;
|
|
24
|
+
const kind = tag === "table" ? "table" : "paragraph";
|
|
25
|
+
const index = kind === "table" ? tables++ : paragraphs++;
|
|
26
|
+
const address = `${kind === "table" ? "t" : "p"}${index}`;
|
|
27
|
+
element.setAttribute(DOCX_ADDRESS_ATTRIBUTE, address);
|
|
28
|
+
blocks.push({ kind, index, address, element });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return blocks;
|
|
32
|
+
}
|
|
33
|
+
function addressedAncestor(root, node) {
|
|
34
|
+
let current = node;
|
|
35
|
+
while (current && current !== root) {
|
|
36
|
+
if (current instanceof Element && current.hasAttribute(DOCX_ADDRESS_ATTRIBUTE)) {
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
current = current.parentNode;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function pickOf(element, literal) {
|
|
44
|
+
const address = element.getAttribute(DOCX_ADDRESS_ATTRIBUTE) ?? "";
|
|
45
|
+
const kind = address.startsWith("t") ? "table" : "paragraph";
|
|
46
|
+
const text = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
47
|
+
return {
|
|
48
|
+
address,
|
|
49
|
+
kind,
|
|
50
|
+
label: literal ? `\u201C${literal}\u201D` : `[${address}]`,
|
|
51
|
+
text,
|
|
52
|
+
...literal ? { literal } : {}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function pickFromNode(root, node) {
|
|
56
|
+
const element = addressedAncestor(root, node);
|
|
57
|
+
return element ? pickOf(element) : null;
|
|
58
|
+
}
|
|
59
|
+
function pickFromSelection(root, selection) {
|
|
60
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const literal = selection.toString().replace(/\s+/g, " ").trim();
|
|
63
|
+
if (!literal) return null;
|
|
64
|
+
const element = addressedAncestor(
|
|
65
|
+
root,
|
|
66
|
+
selection.getRangeAt(0).startContainer
|
|
67
|
+
);
|
|
68
|
+
return element ? pickOf(element, literal) : null;
|
|
69
|
+
}
|
|
70
|
+
var CJK = /[ㄱ-ㆎ가-힣-ヿ一-鿿]/gu;
|
|
71
|
+
var LATIN_WORD = /[A-Za-z0-9][A-Za-z0-9'’-]*/gu;
|
|
72
|
+
function countWords(text) {
|
|
73
|
+
return (text.match(LATIN_WORD)?.length ?? 0) + (text.match(CJK)?.length ?? 0);
|
|
74
|
+
}
|
|
75
|
+
function fitToWidth(root, available) {
|
|
76
|
+
const wrapper = root.querySelector(".docx-wrapper");
|
|
77
|
+
const page = root.querySelector(BODY_SELECTOR);
|
|
78
|
+
if (!wrapper || !page || available <= 0) return 1;
|
|
79
|
+
wrapper.style.zoom = "";
|
|
80
|
+
const width = Math.max(root.scrollWidth, page.getBoundingClientRect().width);
|
|
81
|
+
if (width <= 0) return 1;
|
|
82
|
+
const scale = Math.min(1, available / width);
|
|
83
|
+
wrapper.style.zoom = scale < 1 ? String(scale) : "";
|
|
84
|
+
return scale;
|
|
85
|
+
}
|
|
86
|
+
function declaredFontFamilies(root) {
|
|
87
|
+
const declarations = [];
|
|
88
|
+
for (const sheet of Array.from(root.querySelectorAll("style"))) {
|
|
89
|
+
for (const match of (sheet.textContent ?? "").matchAll(
|
|
90
|
+
/font-family:\s*([^;}]+)/g
|
|
91
|
+
)) {
|
|
92
|
+
declarations.push(match[1]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const element of Array.from(
|
|
96
|
+
root.querySelectorAll("[style]")
|
|
97
|
+
)) {
|
|
98
|
+
if (element.style?.fontFamily) declarations.push(element.style.fontFamily);
|
|
99
|
+
}
|
|
100
|
+
const families = /* @__PURE__ */ new Set();
|
|
101
|
+
for (const declaration of declarations) {
|
|
102
|
+
for (const part of declaration.split(",")) {
|
|
103
|
+
const family = part.trim().replace(/^['"]|['"]$/g, "");
|
|
104
|
+
if (!family || family.startsWith("var(")) continue;
|
|
105
|
+
if (/^(serif|sans-serif|monospace|cursive|fantasy|inherit|initial|unset)$/i.test(
|
|
106
|
+
family
|
|
107
|
+
))
|
|
108
|
+
continue;
|
|
109
|
+
families.add(family);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return Array.from(families).sort();
|
|
113
|
+
}
|
|
114
|
+
function substitutedFonts(root) {
|
|
115
|
+
const families = declaredFontFamilies(root);
|
|
116
|
+
if (families.length === 0) return [];
|
|
117
|
+
let context = null;
|
|
118
|
+
try {
|
|
119
|
+
context = document.createElement("canvas").getContext("2d");
|
|
120
|
+
} catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
if (!context) return [];
|
|
124
|
+
const measured = context;
|
|
125
|
+
const probe = "\uAC00\uB098\uB2E4 Handgloves 0123";
|
|
126
|
+
const width = (font) => {
|
|
127
|
+
measured.font = `24px ${font}`;
|
|
128
|
+
return measured.measureText(probe).width;
|
|
129
|
+
};
|
|
130
|
+
const control = width("monospace");
|
|
131
|
+
return families.filter(
|
|
132
|
+
(family) => width(`"${family}", monospace`) === control
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function docxStats(root) {
|
|
136
|
+
const bodies = pages(root).map((page) => bodyOf(page).textContent ?? "");
|
|
137
|
+
return {
|
|
138
|
+
words: countWords(bodies.join(" ")),
|
|
139
|
+
pagesDrawn: root.querySelectorAll(BODY_SELECTOR).length,
|
|
140
|
+
paginates: false,
|
|
141
|
+
substitutedFonts: substitutedFonts(root)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
var BANNER = "Preview only \u2014 to change it, ask in chat or select some text.";
|
|
145
|
+
function DocxPreview({
|
|
146
|
+
artifactId,
|
|
147
|
+
href,
|
|
148
|
+
name,
|
|
149
|
+
fallback
|
|
150
|
+
}) {
|
|
151
|
+
const hostRef = useRef(null);
|
|
152
|
+
const [status, setStatus] = useState("loading");
|
|
153
|
+
const [stats, setStats] = useState(null);
|
|
154
|
+
const [picked, setPicked] = useState(null);
|
|
155
|
+
const setSelections = useCanvasStore((s) => s.setSelections);
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
let live = true;
|
|
158
|
+
let observer = null;
|
|
159
|
+
const host = hostRef.current;
|
|
160
|
+
if (!host) return;
|
|
161
|
+
setStatus("loading");
|
|
162
|
+
setStats(null);
|
|
163
|
+
setPicked(null);
|
|
164
|
+
(async () => {
|
|
165
|
+
const { renderAsync } = await loadOptional(
|
|
166
|
+
"docx-preview",
|
|
167
|
+
() => import('docx-preview')
|
|
168
|
+
);
|
|
169
|
+
const response = await fetch(href);
|
|
170
|
+
if (!response.ok)
|
|
171
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
172
|
+
const data = await response.arrayBuffer();
|
|
173
|
+
if (!live) return;
|
|
174
|
+
host.replaceChildren();
|
|
175
|
+
await renderAsync(data, host, void 0, {
|
|
176
|
+
inWrapper: true,
|
|
177
|
+
breakPages: true,
|
|
178
|
+
renderHeaders: true,
|
|
179
|
+
renderFooters: true,
|
|
180
|
+
useBase64URL: true
|
|
181
|
+
});
|
|
182
|
+
if (!live) return;
|
|
183
|
+
stampDocxAddresses(host);
|
|
184
|
+
setStats(docxStats(host));
|
|
185
|
+
setStatus("ready");
|
|
186
|
+
let fittedFor = host.clientWidth;
|
|
187
|
+
fitToWidth(host, fittedFor);
|
|
188
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
189
|
+
observer = new ResizeObserver(() => {
|
|
190
|
+
if (Math.abs(host.clientWidth - fittedFor) < 2) return;
|
|
191
|
+
fittedFor = host.clientWidth;
|
|
192
|
+
fitToWidth(host, fittedFor);
|
|
193
|
+
});
|
|
194
|
+
observer.observe(host);
|
|
195
|
+
}
|
|
196
|
+
})().catch(() => {
|
|
197
|
+
if (live) setStatus("unavailable");
|
|
198
|
+
});
|
|
199
|
+
return () => {
|
|
200
|
+
live = false;
|
|
201
|
+
observer?.disconnect();
|
|
202
|
+
};
|
|
203
|
+
}, [href]);
|
|
204
|
+
const choose = (pick) => {
|
|
205
|
+
const host = hostRef.current;
|
|
206
|
+
if (!host) return;
|
|
207
|
+
for (const marked of Array.from(host.querySelectorAll(".is-picked"))) {
|
|
208
|
+
marked.classList.remove("is-picked");
|
|
209
|
+
}
|
|
210
|
+
if (!pick) {
|
|
211
|
+
setPicked(null);
|
|
212
|
+
setSelections([]);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
host.querySelector(`[${DOCX_ADDRESS_ATTRIBUTE}="${pick.address}"]`)?.classList.add("is-picked");
|
|
216
|
+
setPicked(pick.address);
|
|
217
|
+
setSelections([
|
|
218
|
+
{
|
|
219
|
+
artifactId,
|
|
220
|
+
cid: pick.address,
|
|
221
|
+
selector: pick.label,
|
|
222
|
+
tag: pick.kind === "table" ? "table" : "p",
|
|
223
|
+
text: pick.literal ?? pick.text
|
|
224
|
+
}
|
|
225
|
+
]);
|
|
226
|
+
};
|
|
227
|
+
const onMouseUp = (event) => {
|
|
228
|
+
const host = hostRef.current;
|
|
229
|
+
if (!host || status !== "ready") return;
|
|
230
|
+
const dragged = pickFromSelection(host, window.getSelection?.() ?? null);
|
|
231
|
+
choose(dragged ?? pickFromNode(host, event.target));
|
|
232
|
+
};
|
|
233
|
+
if (status === "unavailable") return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
234
|
+
return /* @__PURE__ */ jsxs("div", { className: "cv-docx", children: [
|
|
235
|
+
/* @__PURE__ */ jsx("div", { className: "cv-docx__banner", role: "note", children: BANNER }),
|
|
236
|
+
status === "loading" && /* @__PURE__ */ jsx("div", { className: "cv-docx__loading", children: fallback }),
|
|
237
|
+
/* @__PURE__ */ jsx(
|
|
238
|
+
"div",
|
|
239
|
+
{
|
|
240
|
+
ref: hostRef,
|
|
241
|
+
className: "cv-docx__page",
|
|
242
|
+
onMouseUp,
|
|
243
|
+
"aria-label": `${name} preview`,
|
|
244
|
+
hidden: status !== "ready"
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
stats && /* @__PURE__ */ jsxs(
|
|
248
|
+
"div",
|
|
249
|
+
{
|
|
250
|
+
className: "cv-docx__status",
|
|
251
|
+
title: "The preview keeps the document's own page breaks; it does not repaginate, so it states no page number.",
|
|
252
|
+
children: [
|
|
253
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
254
|
+
stats.words.toLocaleString(),
|
|
255
|
+
" words"
|
|
256
|
+
] }),
|
|
257
|
+
picked && /* @__PURE__ */ jsxs("span", { className: "cv-docx__picked", children: [
|
|
258
|
+
"pointing at [",
|
|
259
|
+
picked,
|
|
260
|
+
"]"
|
|
261
|
+
] }),
|
|
262
|
+
stats.substitutedFonts.length > 0 && /* @__PURE__ */ jsxs("span", { className: "cv-docx__fonts", children: [
|
|
263
|
+
"substituted: ",
|
|
264
|
+
stats.substitutedFonts.join(", ")
|
|
265
|
+
] })
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
] });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export { DocxPreview };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAssetReference, resolveAssetUrl } from './chunk-7T5DRR3F.js';
|
|
2
2
|
import { useCanvasStore } from './chunk-RAPTQ6VH.js';
|
|
3
|
+
import { lazy, Suspense } from 'react';
|
|
3
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
5
|
|
|
5
6
|
function formatSize(size) {
|
|
@@ -8,6 +9,9 @@ function formatSize(size) {
|
|
|
8
9
|
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
9
10
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
10
11
|
}
|
|
12
|
+
var DocxPreview = lazy(
|
|
13
|
+
() => import('./DocxPreview-QK4K5427.js').then((m) => ({ default: m.DocxPreview }))
|
|
14
|
+
);
|
|
11
15
|
function iconFor(mediaType, name) {
|
|
12
16
|
if (mediaType?.startsWith("image/")) return "\u{1F5BC}\uFE0F";
|
|
13
17
|
if (mediaType === "application/pdf") return "\u{1F4D5}";
|
|
@@ -22,9 +26,26 @@ function FileRenderer({ artifact }) {
|
|
|
22
26
|
const assetBaseUrl = useCanvasStore((s) => s.assetBaseUrl);
|
|
23
27
|
const href = assetBaseUrl && isAssetReference(path) ? resolveAssetUrl(path, assetBaseUrl) : null;
|
|
24
28
|
const isImage = Boolean(mediaType?.startsWith("image/"));
|
|
29
|
+
const isWord = `${path} ${name}`.toLowerCase().includes(".docx");
|
|
25
30
|
const facts = [mediaType, formatSize(size), detail].filter(Boolean).join(" \xB7 ");
|
|
31
|
+
const preview = isImage && href ? /* @__PURE__ */ jsx("img", { className: "cv-file__image", src: href, alt: name }) : cover ? /* @__PURE__ */ jsx(
|
|
32
|
+
"img",
|
|
33
|
+
{
|
|
34
|
+
className: "cv-file__cover",
|
|
35
|
+
src: cover,
|
|
36
|
+
alt: `${name} \u2014 first page`
|
|
37
|
+
}
|
|
38
|
+
) : excerpt ? /* @__PURE__ */ jsx("pre", { className: "cv-file__excerpt", children: excerpt }) : null;
|
|
26
39
|
return /* @__PURE__ */ jsxs("div", { className: "cv-file", children: [
|
|
27
|
-
|
|
40
|
+
isWord && href ? /* @__PURE__ */ jsx(Suspense, { fallback: preview, children: /* @__PURE__ */ jsx(
|
|
41
|
+
DocxPreview,
|
|
42
|
+
{
|
|
43
|
+
artifactId: artifact.id,
|
|
44
|
+
href,
|
|
45
|
+
name,
|
|
46
|
+
fallback: preview
|
|
47
|
+
}
|
|
48
|
+
) }) : preview,
|
|
28
49
|
/* @__PURE__ */ jsxs("div", { className: "cv-file__card", children: [
|
|
29
50
|
/* @__PURE__ */ jsx("span", { className: "cv-file__icon", "aria-hidden": true, children: iconFor(mediaType, name) }),
|
|
30
51
|
/* @__PURE__ */ jsxs("span", { className: "cv-file__meta", children: [
|
package/dist/index.js
CHANGED
|
@@ -2259,7 +2259,7 @@ var ChartRenderer = lazy(() => import('./ChartRenderer-TBTWNPV7.js').then((m) =>
|
|
|
2259
2259
|
var DocumentRenderer = lazy(() => import('./DocumentRenderer-ER5TSNA6.js').then((m) => ({ default: m.DocumentRenderer })));
|
|
2260
2260
|
var TableRenderer = lazy(() => import('./TableRenderer-YTOF2ZIC.js').then((m) => ({ default: m.TableRenderer })));
|
|
2261
2261
|
var SlidesRenderer = lazy(() => import('./SlidesRenderer-AHE5ANBZ.js').then((m) => ({ default: m.SlidesRenderer })));
|
|
2262
|
-
var FileRenderer = lazy(() => import('./FileRenderer-
|
|
2262
|
+
var FileRenderer = lazy(() => import('./FileRenderer-W2CR7BI2.js').then((m) => ({ default: m.FileRenderer })));
|
|
2263
2263
|
var builtinRenderers = {
|
|
2264
2264
|
html: HtmlRenderer,
|
|
2265
2265
|
document: DocumentRenderer,
|
|
@@ -2310,6 +2310,7 @@ var dataExporters = {
|
|
|
2310
2310
|
{ label: "PowerPoint", extension: "pptx", mime: MIME.pptx, build: (a) => slidesToPptx(a.data, a.title) }
|
|
2311
2311
|
]
|
|
2312
2312
|
};
|
|
2313
|
+
var PRINT_COLOR_CSS = "*{-webkit-print-color-adjust:exact;print-color-adjust:exact}";
|
|
2313
2314
|
function toStandaloneHtml(title, renderedHtml) {
|
|
2314
2315
|
return `<!doctype html>
|
|
2315
2316
|
<html lang="en">
|
|
@@ -2317,7 +2318,8 @@ function toStandaloneHtml(title, renderedHtml) {
|
|
|
2317
2318
|
<meta charset="utf-8" />
|
|
2318
2319
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
2319
2320
|
<title>${escapeHtml(title)}</title>
|
|
2320
|
-
<style>${EXPORT_CSS}
|
|
2321
|
+
<style>${EXPORT_CSS}
|
|
2322
|
+
${PRINT_COLOR_CSS}</style>
|
|
2321
2323
|
</head>
|
|
2322
2324
|
<body>
|
|
2323
2325
|
<main class="export">
|
|
@@ -2472,6 +2474,7 @@ function slidesToPrintHtml(data, title) {
|
|
|
2472
2474
|
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeXml(title)}</title><style>
|
|
2473
2475
|
@page { size: ${pw}px ${ph}px; margin: 0; }
|
|
2474
2476
|
* { margin: 0; box-sizing: border-box; }
|
|
2477
|
+
${PRINT_COLOR_CSS}
|
|
2475
2478
|
body { font-family: Inter, Arial, sans-serif; }
|
|
2476
2479
|
.slide { position: relative; width: ${pw}px; height: ${ph}px; overflow: hidden; page-break-after: always; }
|
|
2477
2480
|
.el { position: absolute; overflow: hidden; line-height: 1.25; }
|
|
@@ -2481,7 +2484,7 @@ function slidesToPrintHtml(data, title) {
|
|
|
2481
2484
|
function htmlSlideToPrintHtml(html, ratio) {
|
|
2482
2485
|
const w = ratio === "4:3" ? 960 : 1280;
|
|
2483
2486
|
const h = 720;
|
|
2484
|
-
const style = `<style>@page{size:${w}px ${h}px;margin:0}html,body{margin:0!important;padding:0!important;background:#fff}.slide-container{width:${w}px!important;height:${h}px!important;box-shadow:none!important;border-radius:0!important;overflow:hidden!important;page-break-after:avoid}</style>`;
|
|
2487
|
+
const style = `<style>@page{size:${w}px ${h}px;margin:0}${PRINT_COLOR_CSS}html,body{margin:0!important;padding:0!important;background:#fff}.slide-container{width:${w}px!important;height:${h}px!important;box-shadow:none!important;border-radius:0!important;overflow:hidden!important;page-break-after:avoid}</style>`;
|
|
2485
2488
|
const i = html.toLowerCase().lastIndexOf("</head>");
|
|
2486
2489
|
return i === -1 ? style + html : html.slice(0, i) + style + html.slice(i);
|
|
2487
2490
|
}
|
package/dist/styles.css
CHANGED
|
@@ -1268,6 +1268,48 @@
|
|
|
1268
1268
|
}
|
|
1269
1269
|
.cv-file__download:hover { border-color: var(--cv-accent); }
|
|
1270
1270
|
|
|
1271
|
+
/* Word preview — a page the user reads and points at, never types into.
|
|
1272
|
+
The renderer writes the document's own colours and fonts inline, so this
|
|
1273
|
+
only frames it: the banner that says it is read-only, the pointing
|
|
1274
|
+
highlight, and the status line under it. */
|
|
1275
|
+
.cv-docx {
|
|
1276
|
+
display: flex;
|
|
1277
|
+
flex-direction: column;
|
|
1278
|
+
gap: 8px;
|
|
1279
|
+
width: 100%;
|
|
1280
|
+
}
|
|
1281
|
+
.cv-docx__banner {
|
|
1282
|
+
padding: 7px 12px;
|
|
1283
|
+
border: 1px solid var(--cv-border);
|
|
1284
|
+
border-radius: 10px;
|
|
1285
|
+
background: var(--cv-bg);
|
|
1286
|
+
color: var(--cv-muted);
|
|
1287
|
+
font-size: 12px;
|
|
1288
|
+
}
|
|
1289
|
+
.cv-docx__page {
|
|
1290
|
+
max-height: 68vh;
|
|
1291
|
+
overflow: auto;
|
|
1292
|
+
border: 1px solid var(--cv-border);
|
|
1293
|
+
border-radius: 12px;
|
|
1294
|
+
background: var(--cv-bg);
|
|
1295
|
+
}
|
|
1296
|
+
.cv-docx__page [data-canvas-docx] { cursor: pointer; }
|
|
1297
|
+
.cv-docx__page [data-canvas-docx]:hover { outline: 1px dashed var(--cv-border); outline-offset: 2px; }
|
|
1298
|
+
.cv-docx__page .is-picked {
|
|
1299
|
+
outline: 2px solid var(--cv-accent);
|
|
1300
|
+
outline-offset: 2px;
|
|
1301
|
+
border-radius: 3px;
|
|
1302
|
+
}
|
|
1303
|
+
.cv-docx__status {
|
|
1304
|
+
display: flex;
|
|
1305
|
+
flex-wrap: wrap;
|
|
1306
|
+
gap: 10px;
|
|
1307
|
+
font-size: 11px;
|
|
1308
|
+
color: var(--cv-muted);
|
|
1309
|
+
}
|
|
1310
|
+
.cv-docx__picked { color: var(--cv-accent); }
|
|
1311
|
+
.cv-docx__fonts { color: var(--cv-warn, #b45309); }
|
|
1312
|
+
|
|
1271
1313
|
/* Tablet / narrow desktop: tighten paddings, let toolbars wrap. */
|
|
1272
1314
|
@media (max-width: 900px) {
|
|
1273
1315
|
.cv-body { padding: 16px; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@braincrew-lab/langchain-canvas",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "A live canvas for LangChain agents — stream documents, charts, and rich artifacts into a React panel.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Brain Crew (https://github.com/braincrew-lab)",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
71
|
"docx": "^9.0.2",
|
|
72
|
+
"docx-preview": "^0.4.0",
|
|
72
73
|
"exceljs": "^4.4.0",
|
|
73
74
|
"fast-formula-parser": "^1.0.19",
|
|
74
75
|
"pptxgenjs": "^3.12.0"
|