@braincrew-lab/langchain-canvas 0.1.10 → 0.1.11

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.d.ts CHANGED
@@ -374,7 +374,7 @@ interface ChatMessage {
374
374
  interface IframeCommand {
375
375
  artifactId: string;
376
376
  /** style · structure (duplicate/delete/move/insert/insert_html) · group/ungroup · set_src · set_slide_style · clear. */
377
- type: "set_style" | "style_persist" | "commit" | "clear" | "set_src" | "set_slide_style" | "duplicate" | "delete" | "move_up" | "move_down" | "insert" | "insert_html" | "group" | "ungroup";
377
+ type: "set_style" | "style_persist" | "commit" | "clear" | "set_src" | "set_slide_style" | "scroll_to" | "duplicate" | "delete" | "move_up" | "move_down" | "insert" | "insert_html" | "group" | "ungroup";
378
378
  /** Target element (omitted for document-level inserts with no selection). */
379
379
  cid?: string;
380
380
  /** Members to wrap for `group`. */
@@ -383,6 +383,8 @@ interface IframeCommand {
383
383
  value?: string;
384
384
  /** Style map to apply to the slide root for `set_slide_style` (e.g. background, color). */
385
385
  style?: Record<string, string>;
386
+ /** Heading index to scroll into view for `scroll_to`. */
387
+ index?: number;
386
388
  /** Tag/block to insert for `insert` (e.g. "h2", "p", "button", "img", "hr", "section"). */
387
389
  block?: string;
388
390
  /** HTML fragment to insert for `insert_html` (a built-in section template). */
package/dist/index.js CHANGED
@@ -568,6 +568,11 @@ var INSPECTOR_SCRIPT = `
568
568
  var d = e.data;
569
569
  if (!d || d.source !== MARK) return;
570
570
  if (d.type === "clear") { clearSelected(); return; }
571
+ if (d.type === "scroll_to") {
572
+ var hs = document.querySelectorAll("h1,h2,h3,h4,h5,h6");
573
+ if (hs[d.index]) hs[d.index].scrollIntoView({ behavior: "smooth", block: "start" });
574
+ return;
575
+ }
571
576
  if (d.type === "set_style") { var el = byCid(d.cid); if (el) el.style[d.prop] = d.value; return; }
572
577
  if (d.type === "style_persist") { var elp = byCid(d.cid); if (elp) { elp.style[d.prop] = d.value; emitEdit(elp); } return; }
573
578
  if (d.type === "set_slide_style") {
@@ -1610,6 +1615,14 @@ var TEMPLATES = {
1610
1615
  </footer>`
1611
1616
  }
1612
1617
  };
1618
+ function pageDoc(title, sections) {
1619
+ return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="margin:0;font-family:'Segoe UI',system-ui,'Apple SD Gothic Neo','Noto Sans KR',sans-serif;background:#0b1020;color:#e6e8ef">${sections.join("\n")}</body></html>`;
1620
+ }
1621
+ var STARTERS = {
1622
+ landing: { label: "Landing page", build: () => pageDoc("Landing", [TEMPLATES.navbar.html, TEMPLATES.hero.html, TEMPLATES.features.html, TEMPLATES.pricing.html, TEMPLATES.cta.html, TEMPLATES.footer.html]) },
1623
+ saas: { label: "SaaS", build: () => pageDoc("SaaS", [TEMPLATES.navbar.html, TEMPLATES.hero.html, TEMPLATES.stats.html, TEMPLATES.features.html, TEMPLATES.testimonial.html, TEMPLATES.faq.html, TEMPLATES.cta.html, TEMPLATES.footer.html]) },
1624
+ portfolio: { label: "Portfolio", build: () => pageDoc("Portfolio", [TEMPLATES.navbar.html, TEMPLATES.hero.html, TEMPLATES.gallery.html, TEMPLATES.contact.html, TEMPLATES.footer.html]) }
1625
+ };
1613
1626
  var SLIDE_TEMPLATES = {
1614
1627
  title: {
1615
1628
  label: "Title",
@@ -1709,6 +1722,27 @@ var SLIDE_FONTS = {
1709
1722
  rounded: { label: "Rounded", stack: "'Trebuchet MS', 'Segoe UI', 'Apple SD Gothic Neo', sans-serif" },
1710
1723
  condensed: { label: "Condensed", stack: "'Arial Narrow', 'Helvetica Neue', 'Apple SD Gothic Neo', sans-serif" }
1711
1724
  };
1725
+ function checkA11y(html) {
1726
+ if (typeof DOMParser === "undefined") return [];
1727
+ const doc = new DOMParser().parseFromString(html, "text/html");
1728
+ const issues = [];
1729
+ const count = (sel, keep) => Array.from(doc.querySelectorAll(sel)).filter(keep).length;
1730
+ const imgs = count("img", (el) => !el.getAttribute("alt"));
1731
+ if (imgs) issues.push(`${imgs} image${imgs > 1 ? "s" : ""} missing alt text`);
1732
+ const links = count("a", (el) => !el.textContent?.trim() && !el.getAttribute("aria-label"));
1733
+ if (links) issues.push(`${links} link${links > 1 ? "s" : ""} with no text or aria-label`);
1734
+ const btns = count("button", (el) => !el.textContent?.trim() && !el.getAttribute("aria-label"));
1735
+ if (btns) issues.push(`${btns} button${btns > 1 ? "s" : ""} with no accessible label`);
1736
+ const fields = count("input, textarea, select", (el) => {
1737
+ const id = el.getAttribute("id");
1738
+ const labelled = id && doc.querySelector(`label[for="${CSS.escape(id)}"]`);
1739
+ return !labelled && !el.getAttribute("aria-label") && !el.getAttribute("placeholder");
1740
+ });
1741
+ if (fields) issues.push(`${fields} form field${fields > 1 ? "s" : ""} with no label`);
1742
+ if (!doc.documentElement.getAttribute("lang")) issues.push("Missing a <html lang> attribute");
1743
+ if (!doc.querySelector("h1")) issues.push("No <h1> \u2014 every page needs one top-level heading");
1744
+ return issues;
1745
+ }
1712
1746
  var SLIDE_W = 1280;
1713
1747
  var SCROLL_FIX = "<style>html,body{overflow:auto!important;height:auto!important;min-height:100%!important}</style>";
1714
1748
  function withScrollableBody(html) {
@@ -1748,6 +1782,7 @@ function HtmlRenderer({ artifact: artifact2 }) {
1748
1782
  const iframeCommand = useCanvasStore((s) => s.iframeCommand);
1749
1783
  const [device, setDevice] = useState("desktop");
1750
1784
  const [mode, setMode] = useState("design");
1785
+ const [a11y, setA11y] = useState(null);
1751
1786
  const lastSelfHtml = useRef(null);
1752
1787
  const srcDocRef = useRef("");
1753
1788
  const isFixedSlide = Boolean(artifact2.meta?.ratio);
@@ -1760,6 +1795,14 @@ function HtmlRenderer({ artifact: artifact2 }) {
1760
1795
  }, [artifact2.data.html, mode, isFixedSlide]);
1761
1796
  const selected = selections.filter((s) => s.artifactId === artifact2.id);
1762
1797
  const single = selected.length === 1 ? selected[0] : null;
1798
+ const outline = useMemo(() => {
1799
+ if (typeof DOMParser === "undefined") return [];
1800
+ const doc = new DOMParser().parseFromString(artifact2.data.html, "text/html");
1801
+ return Array.from(doc.querySelectorAll("h1,h2,h3,h4,h5,h6")).map((h) => ({
1802
+ level: Number(h.tagName[1]),
1803
+ text: (h.textContent ?? "").trim().slice(0, 48) || "(untitled)"
1804
+ }));
1805
+ }, [artifact2.data.html]);
1763
1806
  useEffect(() => {
1764
1807
  function onMessage(event) {
1765
1808
  if (event.source !== iframeRef.current?.contentWindow) return;
@@ -1844,23 +1887,60 @@ function HtmlRenderer({ artifact: artifact2 }) {
1844
1887
  ] }),
1845
1888
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Add" }),
1846
1889
  (ratio ? BLOCKS.filter((b) => SLIDE_BLOCK_TAGS.has(b.tag)) : BLOCKS).map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: b.label }, b.tag)),
1847
- !ratio && /* @__PURE__ */ jsxs(
1848
- "select",
1849
- {
1850
- className: "cv-html-tpl",
1851
- value: "",
1852
- title: "Insert a section template",
1853
- onChange: (e) => {
1854
- const t = TEMPLATES[e.target.value];
1855
- if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1856
- e.currentTarget.value = "";
1857
- },
1858
- children: [
1859
- /* @__PURE__ */ jsx("option", { value: "", children: "Section\u2026" }),
1860
- Object.entries(TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1861
- ]
1862
- }
1863
- ),
1890
+ !ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1891
+ /* @__PURE__ */ jsxs(
1892
+ "select",
1893
+ {
1894
+ className: "cv-html-tpl",
1895
+ value: "",
1896
+ title: "Start from a full page template (replaces the page)",
1897
+ onChange: (e) => {
1898
+ const s = STARTERS[e.target.value];
1899
+ if (s) applyEvent({ type: "canvas.patch", id: artifact2.id, patch: { html: s.build() } });
1900
+ e.currentTarget.value = "";
1901
+ },
1902
+ children: [
1903
+ /* @__PURE__ */ jsx("option", { value: "", children: "Page\u2026" }),
1904
+ Object.entries(STARTERS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1905
+ ]
1906
+ }
1907
+ ),
1908
+ /* @__PURE__ */ jsxs(
1909
+ "select",
1910
+ {
1911
+ className: "cv-html-tpl",
1912
+ value: "",
1913
+ title: "Insert a section template",
1914
+ onChange: (e) => {
1915
+ const t = TEMPLATES[e.target.value];
1916
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1917
+ e.currentTarget.value = "";
1918
+ },
1919
+ children: [
1920
+ /* @__PURE__ */ jsx("option", { value: "", children: "Section\u2026" }),
1921
+ Object.entries(TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1922
+ ]
1923
+ }
1924
+ ),
1925
+ outline.length > 0 && /* @__PURE__ */ jsxs(
1926
+ "select",
1927
+ {
1928
+ className: "cv-html-tpl",
1929
+ value: "",
1930
+ title: "Jump to a heading",
1931
+ onChange: (e) => {
1932
+ const idx = Number(e.target.value);
1933
+ if (!Number.isNaN(idx)) sendIframeCommand({ artifactId: artifact2.id, type: "scroll_to", index: idx });
1934
+ e.currentTarget.value = "";
1935
+ },
1936
+ children: [
1937
+ /* @__PURE__ */ jsx("option", { value: "", children: "Outline\u2026" }),
1938
+ outline.map((h, i) => /* @__PURE__ */ jsx("option", { value: i, children: "\xA0".repeat((h.level - 1) * 2) + h.text }, i))
1939
+ ]
1940
+ }
1941
+ ),
1942
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: "Accessibility check", onClick: () => setA11y(checkA11y(artifact2.data.html)), children: "\u267F Check" })
1943
+ ] }),
1864
1944
  ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1865
1945
  /* @__PURE__ */ jsxs(
1866
1946
  "select",
@@ -1979,6 +2059,18 @@ function HtmlRenderer({ artifact: artifact2 }) {
1979
2059
  /* @__PURE__ */ jsx("button", { className: mode === "code" ? "is-on" : "", onClick: () => setMode("code"), children: "Code" })
1980
2060
  ] })
1981
2061
  ] }),
2062
+ a11y !== null && /* @__PURE__ */ jsxs("div", { className: "cv-a11y", role: "status", children: [
2063
+ /* @__PURE__ */ jsx("button", { className: "cv-a11y__close", onClick: () => setA11y(null), "aria-label": "Dismiss", children: "\xD7" }),
2064
+ a11y.length === 0 ? /* @__PURE__ */ jsx("span", { className: "cv-a11y__ok", children: "\u267F No accessibility issues found" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2065
+ /* @__PURE__ */ jsxs("b", { children: [
2066
+ "\u267F ",
2067
+ a11y.length,
2068
+ " accessibility issue",
2069
+ a11y.length > 1 ? "s" : ""
2070
+ ] }),
2071
+ /* @__PURE__ */ jsx("ul", { children: a11y.map((m, i) => /* @__PURE__ */ jsx("li", { children: m }, i)) })
2072
+ ] })
2073
+ ] }),
1982
2074
  mode === "design" ? /* @__PURE__ */ jsx("div", { className: `cv-html-stage${ratio ? " cv-html-stage--slide" : ""}`, ref: stageRef, children: ratio ? (
1983
2075
  // Scaled slide: the iframe lays out at its full design size, then a
1984
2076
  // transform shrinks it to fit; a sized wrapper reserves the scaled box
package/dist/styles.css CHANGED
@@ -761,6 +761,17 @@
761
761
  /* --- html renderer (base substrate) ------------------------------------------- */
762
762
 
763
763
  .cv-html-wrap { display: flex; flex-direction: column; height: 100%; min-height: 0; gap: 10px; }
764
+ .cv-a11y {
765
+ position: relative; flex: none; padding: 12px 34px 12px 16px;
766
+ border: 1px solid var(--cv-border); border-radius: 10px; background: var(--cv-surface);
767
+ font-size: 13px; color: var(--cv-text);
768
+ }
769
+ .cv-a11y ul { margin: 8px 0 0; padding-left: 18px; color: var(--cv-muted); line-height: 1.7; }
770
+ .cv-a11y__ok { color: var(--cv-good, #1f9d6b); font-weight: 600; }
771
+ .cv-a11y__close {
772
+ position: absolute; top: 6px; right: 8px; border: 0; background: transparent;
773
+ color: var(--cv-muted); font-size: 18px; cursor: pointer; line-height: 1;
774
+ }
764
775
  .cv-html-bar {
765
776
  display: flex; align-items: center; flex-wrap: wrap; gap: 5px;
766
777
  padding: 7px 9px; border: 1px solid var(--cv-border); border-radius: 9px; background: var(--cv-surface);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braincrew-lab/langchain-canvas",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
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",