@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 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
- function SlideView({ html, ratio, title }) {
1517
- const boxRef = useRef(null);
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 DESIGN_W = 1280;
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 - 32) / DESIGN_W));
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
- }, [DESIGN_W]);
1531
- return /* @__PURE__ */ jsx("div", { ref: boxRef, className: "cv-slideview", children: /* @__PURE__ */ jsx("div", { className: "cv-slideview__frame", style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ jsx(
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
- if (ratio) {
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
- /* @__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" }),
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: /* @__PURE__ */ jsx(
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 preview (read-only, scaled to fit). */
792
- .cv-slideview {
793
- flex: 1; min-height: 0; overflow: auto; display: flex; justify-content: center; align-items: flex-start;
794
- padding: 16px; background: var(--cv-surface); border-radius: 10px;
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-slideview__frame iframe { display: block; }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braincrew-lab/langchain-canvas",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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
  "type": "module",