@braincrew-lab/langchain-canvas 0.1.1 → 0.1.3
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/index.js +39 -82
- package/dist/styles.css +6 -8
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -392,6 +392,14 @@ var INSPECTOR_SCRIPT = `
|
|
|
392
392
|
}, "*");
|
|
393
393
|
}, true);
|
|
394
394
|
|
|
395
|
+
// Block the browser's native drag (images and links start an HTML5 drag on
|
|
396
|
+
// mousemove by default, which hijacks our free-drag \u2014 the image "ghost" gets
|
|
397
|
+
// dragged instead of the element moving). Suppress it for any editable node.
|
|
398
|
+
document.addEventListener("dragstart", function (e) {
|
|
399
|
+
var t = e.target;
|
|
400
|
+
if (t instanceof Element && t.closest("[data-cid]") && !t.hasAttribute("data-lcx")) e.preventDefault();
|
|
401
|
+
}, true);
|
|
402
|
+
|
|
395
403
|
// --- drag: freely move an element anywhere, or marquee-select empty space ----
|
|
396
404
|
document.addEventListener("mousedown", function (e) {
|
|
397
405
|
suppressClick = false; // a fresh press: never carry a stale suppress
|
|
@@ -1513,34 +1521,27 @@ var TEMPLATES = {
|
|
|
1513
1521
|
</section>`
|
|
1514
1522
|
}
|
|
1515
1523
|
};
|
|
1516
|
-
|
|
1517
|
-
|
|
1524
|
+
var SLIDE_W = 1280;
|
|
1525
|
+
function useSlideFit(ratio, boxRef) {
|
|
1518
1526
|
const [scale, setScale] = useState(1);
|
|
1519
|
-
const [rw, rh] = ratio.split(/[:x/]/).map(Number);
|
|
1520
|
-
const
|
|
1521
|
-
const DESIGN_H = rw && rh ? Math.round(DESIGN_W * rh / rw) : 720;
|
|
1527
|
+
const [rw, rh] = (ratio ?? "16:9").split(/[:x/]/).map(Number);
|
|
1528
|
+
const height = rw && rh ? Math.round(SLIDE_W * rh / rw) : 720;
|
|
1522
1529
|
useEffect(() => {
|
|
1530
|
+
if (!ratio) return;
|
|
1523
1531
|
const el = boxRef.current;
|
|
1524
1532
|
if (!el) return;
|
|
1525
|
-
const fit = () => setScale(Math.min(1, (el.clientWidth -
|
|
1533
|
+
const fit = () => setScale(Math.min(1, (el.clientWidth - 40) / SLIDE_W));
|
|
1526
1534
|
fit();
|
|
1527
1535
|
const ro = new ResizeObserver(fit);
|
|
1528
1536
|
ro.observe(el);
|
|
1529
1537
|
return () => ro.disconnect();
|
|
1530
|
-
}, [
|
|
1531
|
-
return
|
|
1532
|
-
"iframe",
|
|
1533
|
-
{
|
|
1534
|
-
title,
|
|
1535
|
-
srcDoc: html,
|
|
1536
|
-
sandbox: "allow-scripts allow-popups allow-modals",
|
|
1537
|
-
style: { width: DESIGN_W, height: DESIGN_H, border: 0, transform: `scale(${scale})`, transformOrigin: "top left" }
|
|
1538
|
-
}
|
|
1539
|
-
) }) });
|
|
1538
|
+
}, [ratio, height]);
|
|
1539
|
+
return { scale, width: SLIDE_W, height };
|
|
1540
1540
|
}
|
|
1541
1541
|
function HtmlRenderer({ artifact: artifact2 }) {
|
|
1542
1542
|
const iframeRef = useRef(null);
|
|
1543
1543
|
const imgFileRef = useRef(null);
|
|
1544
|
+
const stageRef = useRef(null);
|
|
1544
1545
|
const api = useCanvasStoreApi();
|
|
1545
1546
|
const setSelections = useCanvasStore((s) => s.setSelections);
|
|
1546
1547
|
const applyEvent = useCanvasStore((s) => s.applyUserEvent);
|
|
@@ -1613,9 +1614,7 @@ function HtmlRenderer({ artifact: artifact2 }) {
|
|
|
1613
1614
|
reader.readAsDataURL(file);
|
|
1614
1615
|
};
|
|
1615
1616
|
const ratio = artifact2.meta?.ratio;
|
|
1616
|
-
|
|
1617
|
-
return /* @__PURE__ */ jsx("div", { className: "cv-html-wrap", children: /* @__PURE__ */ jsx(SlideView, { html: artifact2.data.html, ratio, title: artifact2.title }) });
|
|
1618
|
-
}
|
|
1617
|
+
const slide = useSlideFit(ratio, stageRef);
|
|
1619
1618
|
return /* @__PURE__ */ jsxs("div", { className: "cv-html-wrap", children: [
|
|
1620
1619
|
/* @__PURE__ */ jsx("input", { ref: imgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
|
|
1621
1620
|
onImgFile(e.target.files?.[0]);
|
|
@@ -1623,8 +1622,10 @@ function HtmlRenderer({ artifact: artifact2 }) {
|
|
|
1623
1622
|
} }),
|
|
1624
1623
|
/* @__PURE__ */ jsxs("div", { className: "cv-html-bar cv-chrome", children: [
|
|
1625
1624
|
mode === "design" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1626
|
-
|
|
1627
|
-
|
|
1625
|
+
!ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1626
|
+
/* @__PURE__ */ jsx("div", { className: "cv-html-seg", role: "group", "aria-label": "Preview width", children: DEVICES.map((d) => /* @__PURE__ */ jsx("button", { className: device === d.id ? "is-on" : "", onClick: () => setDevice(d.id), children: d.label }, d.id)) }),
|
|
1627
|
+
/* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" })
|
|
1628
|
+
] }),
|
|
1628
1629
|
/* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Add" }),
|
|
1629
1630
|
BLOCKS.map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: b.label }, b.tag)),
|
|
1630
1631
|
/* @__PURE__ */ jsxs(
|
|
@@ -1685,7 +1686,22 @@ function HtmlRenderer({ artifact: artifact2 }) {
|
|
|
1685
1686
|
/* @__PURE__ */ jsx("button", { className: mode === "code" ? "is-on" : "", onClick: () => setMode("code"), children: "Code" })
|
|
1686
1687
|
] })
|
|
1687
1688
|
] }),
|
|
1688
|
-
mode === "design" ? /* @__PURE__ */ jsx("div", { className: "cv-html-stage", children:
|
|
1689
|
+
mode === "design" ? /* @__PURE__ */ jsx("div", { className: `cv-html-stage${ratio ? " cv-html-stage--slide" : ""}`, ref: stageRef, children: ratio ? (
|
|
1690
|
+
// Scaled slide: the iframe lays out at its full design size, then a
|
|
1691
|
+
// transform shrinks it to fit; a sized wrapper reserves the scaled box
|
|
1692
|
+
// so the stage scrolls correctly. Clicks still hit the right elements.
|
|
1693
|
+
/* @__PURE__ */ jsx("div", { style: { width: slide.width * slide.scale, height: slide.height * slide.scale, flex: "0 0 auto" }, children: /* @__PURE__ */ jsx(
|
|
1694
|
+
"iframe",
|
|
1695
|
+
{
|
|
1696
|
+
ref: iframeRef,
|
|
1697
|
+
className: "cv-html",
|
|
1698
|
+
title: artifact2.title,
|
|
1699
|
+
srcDoc,
|
|
1700
|
+
sandbox: "allow-scripts allow-popups allow-modals",
|
|
1701
|
+
style: { width: slide.width, height: slide.height, transform: `scale(${slide.scale})`, transformOrigin: "top left" }
|
|
1702
|
+
}
|
|
1703
|
+
) })
|
|
1704
|
+
) : /* @__PURE__ */ jsx(
|
|
1689
1705
|
"iframe",
|
|
1690
1706
|
{
|
|
1691
1707
|
ref: iframeRef,
|
|
@@ -1760,8 +1776,7 @@ var dataExporters = {
|
|
|
1760
1776
|
{ label: "JSON", extension: "json", mime: MIME.json, build: (a) => JSON.stringify(a.data, null, 2) }
|
|
1761
1777
|
],
|
|
1762
1778
|
slides: [
|
|
1763
|
-
{ label: "PowerPoint", extension: "pptx", mime: MIME.pptx, build: (a) => slidesToPptx(a.data, a.title) }
|
|
1764
|
-
{ label: "Figma (JSON)", extension: "json", mime: MIME.json, build: (a) => slidesToFigmaJson(a.data) }
|
|
1779
|
+
{ label: "PowerPoint", extension: "pptx", mime: MIME.pptx, build: (a) => slidesToPptx(a.data, a.title) }
|
|
1765
1780
|
]
|
|
1766
1781
|
};
|
|
1767
1782
|
function toStandaloneHtml(title, renderedHtml) {
|
|
@@ -1868,52 +1883,6 @@ async function slidesToPptx(data, _title) {
|
|
|
1868
1883
|
}
|
|
1869
1884
|
return await pptx.write({ outputType: "blob" });
|
|
1870
1885
|
}
|
|
1871
|
-
function slidesToFigmaJson(data) {
|
|
1872
|
-
const W = 1280;
|
|
1873
|
-
const H = 720;
|
|
1874
|
-
const GAP = 64;
|
|
1875
|
-
const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
|
|
1876
|
-
const textNode = (x, y, width, characters, fontSize, fontWeight, align, color = "#1F2328") => ({ type: "text", x, y, width, characters, fontSize, fontWeight, align, color });
|
|
1877
|
-
const frames = slides2.map((slide, i) => {
|
|
1878
|
-
const nodes = [];
|
|
1879
|
-
for (const el of resolveElements(slide)) {
|
|
1880
|
-
if (el.type !== "text") continue;
|
|
1881
|
-
nodes.push(
|
|
1882
|
-
textNode(el.x / 100 * W, el.y / 100 * H, el.w / 100 * W, el.text ?? "", el.fontSize ?? 24, el.bold ? 700 : 400, el.align === "center" ? "CENTER" : "LEFT", el.color ?? slide.textColor ?? "#1F2328")
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
return { name: `Slide ${i + 1}`, x: 0, y: i * (H + GAP), width: W, height: H, fill: slide.background ?? "#FFFFFF", nodes };
|
|
1886
|
-
});
|
|
1887
|
-
return JSON.stringify({ type: "langchain-canvas/figma-deck", version: 1, frames }, null, 2);
|
|
1888
|
-
}
|
|
1889
|
-
function slidesToSvg(data) {
|
|
1890
|
-
const W = 1280;
|
|
1891
|
-
const H = 720;
|
|
1892
|
-
const GAP = 64;
|
|
1893
|
-
const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
|
|
1894
|
-
const totalH = slides2.length * H + (slides2.length - 1) * GAP;
|
|
1895
|
-
const text = (x, y, value, size, anchor, weight, fill = "#1f2328") => `<text x="${x}" y="${y}" font-family="Inter, Arial, sans-serif" font-size="${size}" font-weight="${weight}" text-anchor="${anchor}" fill="${fill}">${escapeXml(value)}</text>`;
|
|
1896
|
-
const frames = slides2.map((slide, i) => {
|
|
1897
|
-
const y = i * (H + GAP);
|
|
1898
|
-
const bg = slide.background ?? "#ffffff";
|
|
1899
|
-
let body = `<rect width="${W}" height="${H}" rx="8" fill="${bg}" stroke="#e5e7eb"/>`;
|
|
1900
|
-
for (const el of resolveElements(slide)) {
|
|
1901
|
-
const ex = el.x / 100 * W;
|
|
1902
|
-
const ey = el.y / 100 * H;
|
|
1903
|
-
const ew = el.w / 100 * W;
|
|
1904
|
-
if (el.type === "text") {
|
|
1905
|
-
const anchor = el.align === "center" ? "middle" : el.align === "right" ? "end" : "start";
|
|
1906
|
-
const ax = el.align === "center" ? ex + ew / 2 : el.align === "right" ? ex + ew : ex;
|
|
1907
|
-
const fs = el.fontSize ?? 24;
|
|
1908
|
-
body += text(ax, ey + fs, el.text ?? "", fs, anchor, el.bold ? 700 : 400, el.color ?? slide.textColor ?? "#1f2328");
|
|
1909
|
-
} else if (el.src) {
|
|
1910
|
-
body += `<image href="${el.src}" x="${ex}" y="${ey}" width="${ew}" height="${el.h / 100 * H}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
return `<g transform="translate(0 ${y})">${body}</g>`;
|
|
1914
|
-
}).join("");
|
|
1915
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${totalH}" viewBox="0 0 ${W} ${totalH}">${frames}</svg>`;
|
|
1916
|
-
}
|
|
1917
1886
|
function slidesToPrintHtml(data, title) {
|
|
1918
1887
|
const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
|
|
1919
1888
|
const pages = slides2.map((slide) => {
|
|
@@ -2027,14 +1996,6 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
|
|
|
2027
1996
|
}
|
|
2028
1997
|
setOpen(false);
|
|
2029
1998
|
};
|
|
2030
|
-
const copyToFigma = async () => {
|
|
2031
|
-
const svg = slidesToSvg(artifact2.data);
|
|
2032
|
-
try {
|
|
2033
|
-
await navigator.clipboard.writeText(svg);
|
|
2034
|
-
} catch {
|
|
2035
|
-
}
|
|
2036
|
-
setOpen(false);
|
|
2037
|
-
};
|
|
2038
1999
|
return /* @__PURE__ */ jsxs("div", { className: "cv-export", children: [
|
|
2039
2000
|
/* @__PURE__ */ jsx(
|
|
2040
2001
|
"button",
|
|
@@ -2057,10 +2018,6 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
|
|
|
2057
2018
|
"PDF ",
|
|
2058
2019
|
/* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: ".pdf" })
|
|
2059
2020
|
] }),
|
|
2060
|
-
artifact2.type === "slides" && /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: copyToFigma, children: [
|
|
2061
|
-
"Copy to Figma ",
|
|
2062
|
-
/* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: "paste \u2318V" })
|
|
2063
|
-
] }),
|
|
2064
2021
|
dataOptions.map((option) => /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: () => exportData(option), children: [
|
|
2065
2022
|
option.label,
|
|
2066
2023
|
" ",
|
package/dist/styles.css
CHANGED
|
@@ -788,16 +788,14 @@
|
|
|
788
788
|
flex: 1; min-height: 0; overflow: auto; display: flex; justify-content: center;
|
|
789
789
|
padding: 10px; background: var(--cv-surface); border-radius: 10px;
|
|
790
790
|
}
|
|
791
|
-
/* Fixed-aspect slide
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
.cv-slideview__frame {
|
|
797
|
-
position: relative; overflow: hidden; background: #fff; border-radius: 8px;
|
|
791
|
+
/* Fixed-aspect slide stage — the scaled iframe box sits centered at the top,
|
|
792
|
+
with a card-like frame around it. Still fully editable. */
|
|
793
|
+
.cv-html-stage--slide { align-items: flex-start; padding: 16px; }
|
|
794
|
+
.cv-html-stage--slide > div {
|
|
795
|
+
overflow: hidden; background: #fff; border-radius: 8px;
|
|
798
796
|
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
|
|
799
797
|
}
|
|
800
|
-
.cv-
|
|
798
|
+
.cv-html-stage--slide iframe { display: block; border: 0; }
|
|
801
799
|
.cv-html-code {
|
|
802
800
|
flex: 1; min-height: 0; width: 100%; resize: none;
|
|
803
801
|
padding: 14px 16px; border: 1px solid var(--cv-border); border-radius: 10px;
|
package/package.json
CHANGED