@braincrew-lab/langchain-canvas 0.1.8 → 0.1.10

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.
@@ -4,6 +4,12 @@ import './chunk-KKLWKR5G.js';
4
4
  import { useState, useRef, useEffect } from 'react';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
+ function shapeStyle(el) {
8
+ const fill = el.fill ?? "currentColor";
9
+ if (el.shape === "ellipse") return { width: "100%", height: "100%", background: fill, borderRadius: "50%" };
10
+ if (el.shape === "line") return { width: "100%", height: "100%", background: fill, borderRadius: 2 };
11
+ return { width: "100%", height: "100%", background: fill, borderRadius: 8 };
12
+ }
7
13
  var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
8
14
  var dupSeq = 0;
9
15
  var dupId = (base) => `${base}_c${Date.now().toString(36)}${dupSeq++}`;
@@ -175,7 +181,7 @@ function FreeSlide({ elements, onChange }) {
175
181
  },
176
182
  children: el.text
177
183
  }
178
- ) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "", draggable: false }),
184
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "", draggable: false }),
179
185
  selected === el.id && el.type === "text" && /* @__PURE__ */ jsxs("div", { className: `cv-free__fmt ${el.y < 16 ? "cv-free__fmt--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: [
180
186
  /* @__PURE__ */ jsx("button", { className: el.bold ? "is-on" : "", onClick: () => updateEl(el.id, { bold: !el.bold }), title: "Bold", children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
181
187
  /* @__PURE__ */ jsx(
@@ -194,6 +200,7 @@ function FreeSlide({ elements, onChange }) {
194
200
  /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
195
201
  /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
196
202
  ] }),
203
+ selected === el.id && el.type === "shape" && /* @__PURE__ */ jsx("div", { className: `cv-free__fmt ${el.y < 16 ? "cv-free__fmt--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx("input", { type: "color", value: el.fill ?? "#5b5bd6", onChange: (e) => updateEl(el.id, { fill: e.target.value }), title: "Fill color" }) }),
197
204
  selected === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
198
205
  /* @__PURE__ */ jsx("span", { className: "cv-free__resize", onPointerDown: (e) => onDown(e, el, "resize") }),
199
206
  /* @__PURE__ */ jsxs("div", { className: `cv-free__ctl ${el.y < 16 ? "cv-free__ctl--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), children: [
@@ -225,11 +232,41 @@ function FreeSlide({ elements, onChange }) {
225
232
  }
226
233
  var THEMES = [
227
234
  { id: "light", label: "Light", bg: "#ffffff", text: "#1f2328" },
235
+ { id: "paper", label: "Paper", bg: "#f7f5ef", text: "#2b2a26" },
228
236
  { id: "dark", label: "Dark", bg: "#14171f", text: "#f0f2f5" },
229
237
  { id: "midnight", label: "Midnight", bg: "#0b1020", text: "#e6e8ef" },
238
+ { id: "navy", label: "Navy", bg: "#0d1b3e", text: "#eef2ff" },
239
+ { id: "forest", label: "Forest", bg: "#0f2a22", text: "#e6f2ec" },
230
240
  { id: "sunset", label: "Sunset", bg: "#2b1a2e", text: "#ffe8d6" },
231
241
  { id: "mint", label: "Mint", bg: "#0f2a24", text: "#d7f5ec" }
232
242
  ];
243
+ var SHAPES = [
244
+ { id: "rect", label: "\u25AD Rectangle", box: { w: 30, h: 20 } },
245
+ { id: "ellipse", label: "\u25EF Ellipse", box: { w: 24, h: 24 } },
246
+ { id: "line", label: "\u2014 Line", box: { w: 40, h: 2 } }
247
+ ];
248
+ var LAYOUTS = {
249
+ title: { label: "Title", els: [
250
+ { type: "text", x: 12, y: 34, w: 76, h: 18, text: "Presentation title", fontSize: 54, bold: true, align: "center" },
251
+ { type: "text", x: 20, y: 56, w: 60, h: 10, text: "Subtitle or one-line summary", fontSize: 24, align: "center" }
252
+ ] },
253
+ section: { label: "Section", els: [
254
+ { type: "shape", shape: "rect", x: 10, y: 34, w: 8, h: 3, fill: "#5b5bd6" },
255
+ { type: "text", x: 10, y: 40, w: 80, h: 16, text: "Section title", fontSize: 46, bold: true }
256
+ ] },
257
+ bullets: { label: "Bullets", els: [
258
+ { type: "text", x: 10, y: 12, w: 80, h: 12, text: "Heading", fontSize: 36, bold: true },
259
+ { type: "text", x: 10, y: 30, w: 80, h: 50, text: "\u2022 First key point\n\u2022 Second key point\n\u2022 Third key point", fontSize: 26 }
260
+ ] },
261
+ "two-column": { label: "Two column", els: [
262
+ { type: "text", x: 8, y: 16, w: 40, h: 60, text: "Left column\n\n\u2022 point\n\u2022 point", fontSize: 24 },
263
+ { type: "text", x: 52, y: 16, w: 40, h: 60, text: "Right column\n\n\u2022 point\n\u2022 point", fontSize: 24 }
264
+ ] },
265
+ quote: { label: "Quote", els: [
266
+ { type: "text", x: 12, y: 30, w: 76, h: 30, text: "\u201CA short, memorable quote.\u201D", fontSize: 40, bold: true },
267
+ { type: "text", x: 12, y: 62, w: 50, h: 8, text: "\u2014 Attribution", fontSize: 22 }
268
+ ] }
269
+ };
233
270
  var elementSeq = 0;
234
271
  var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
235
272
  function SlidesRenderer({ artifact }) {
@@ -239,6 +276,7 @@ function SlidesRenderer({ artifact }) {
239
276
  const [dragIndex, setDragIndex] = useState(null);
240
277
  const [presenting, setPresenting] = useState(false);
241
278
  const imgRef = useRef(null);
279
+ const bgRef = useRef(null);
242
280
  useEffect(() => {
243
281
  if (!presenting) return;
244
282
  const onKey = (e) => {
@@ -301,6 +339,20 @@ function SlidesRenderer({ artifact }) {
301
339
  reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
302
340
  reader.readAsDataURL(file);
303
341
  };
342
+ const addShapeEl = (shape) => {
343
+ const s = SHAPES.find((x) => x.id === shape);
344
+ if (s) addElement({ type: "shape", shape, x: 20, y: 20, w: s.box.w, h: s.box.h, fill: "#5b5bd6" });
345
+ };
346
+ const applyLayout = (key) => {
347
+ const l = LAYOUTS[key];
348
+ if (l) update({ elements: l.els.map((e) => ({ ...e, id: newElementId() })) });
349
+ };
350
+ const setSlideBgImage = (file) => {
351
+ if (!file) return;
352
+ const reader = new FileReader();
353
+ reader.onload = () => update({ background: `#000 url("${String(reader.result)}") center/cover no-repeat` });
354
+ reader.readAsDataURL(file);
355
+ };
304
356
  return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
305
357
  /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
306
358
  slides.map((s, i) => /* @__PURE__ */ jsx(
@@ -321,11 +373,11 @@ function SlidesRenderer({ artifact }) {
321
373
  (el) => el.type === "text" ? /* @__PURE__ */ jsx(
322
374
  "span",
323
375
  {
324
- style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * 0.12, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden" },
376
+ style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * 0.12, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden", whiteSpace: "pre-wrap" },
325
377
  children: el.text
326
378
  },
327
379
  el.id
328
- ) : /* @__PURE__ */ jsx("img", { src: el.src, alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
380
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: s.textColor, ...shapeStyle(el) } }, el.id) : /* @__PURE__ */ jsx("img", { src: el.src, alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
329
381
  ) })
330
382
  ] })
331
383
  },
@@ -338,6 +390,39 @@ function SlidesRenderer({ artifact }) {
338
390
  /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
339
391
  /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
340
392
  /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
393
+ /* @__PURE__ */ jsx("input", { ref: bgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => setSlideBgImage(e.target.files?.[0]) }),
394
+ /* @__PURE__ */ jsxs(
395
+ "select",
396
+ {
397
+ className: "cv-deck__theme",
398
+ value: "",
399
+ title: "Add a shape",
400
+ onChange: (e) => {
401
+ if (e.target.value) addShapeEl(e.target.value);
402
+ e.currentTarget.value = "";
403
+ },
404
+ children: [
405
+ /* @__PURE__ */ jsx("option", { value: "", children: "+ Shape" }),
406
+ SHAPES.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.label }, s.id))
407
+ ]
408
+ }
409
+ ),
410
+ /* @__PURE__ */ jsxs(
411
+ "select",
412
+ {
413
+ className: "cv-deck__theme",
414
+ value: "",
415
+ title: "Apply a layout",
416
+ onChange: (e) => {
417
+ if (e.target.value) applyLayout(e.target.value);
418
+ e.currentTarget.value = "";
419
+ },
420
+ children: [
421
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
422
+ Object.entries(LAYOUTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
423
+ ]
424
+ }
425
+ ),
341
426
  /* @__PURE__ */ jsxs(
342
427
  "select",
343
428
  {
@@ -347,14 +432,16 @@ function SlidesRenderer({ artifact }) {
347
432
  onChange: (e) => {
348
433
  const t = THEMES.find((x) => x.id === e.target.value);
349
434
  if (t) update({ background: t.bg, textColor: t.text });
435
+ e.currentTarget.value = "";
350
436
  },
351
437
  children: [
352
- /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Theme" }),
438
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
353
439
  THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
354
440
  ]
355
441
  }
356
442
  ),
357
- /* @__PURE__ */ jsx("label", { className: "cv-deck__bg", title: "Background color", children: /* @__PURE__ */ jsx("input", { type: "color", value: slide.background ?? "#ffffff", onChange: (e) => update({ background: e.target.value }) }) }),
443
+ /* @__PURE__ */ jsx("label", { className: "cv-deck__bg", title: "Background color", children: /* @__PURE__ */ jsx("input", { type: "color", value: /^#/.test(slide.background ?? "") ? slide.background : "#ffffff", onChange: (e) => update({ background: e.target.value }) }) }),
444
+ /* @__PURE__ */ jsx("button", { onClick: () => bgRef.current?.click(), title: "Background image", children: "\u{1F5BC} BG" }),
358
445
  /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
359
446
  /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
360
447
  /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
@@ -383,9 +470,10 @@ function SlidesRenderer({ artifact }) {
383
470
  )
384
471
  ] }),
385
472
  presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
386
- /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", children: resolveElements(slide).map(
387
- (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left" }, children: el.text }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "" }) }, el.id)
388
- ) }) }),
473
+ /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-present__fade cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", children: resolveElements(slide).map(
474
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left", whiteSpace: "pre-wrap" }, children: el.text }) }, el.id) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: slide.textColor }, children: /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "" }) }, el.id)
475
+ ) }) }, at),
476
+ slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
389
477
  /* @__PURE__ */ jsxs("div", { className: "cv-present__hint", children: [
390
478
  at + 1,
391
479
  " / ",
package/dist/index.d.ts CHANGED
@@ -83,7 +83,7 @@ interface TableData {
83
83
  /** A freely-positioned element on a "blank" slide (percent geometry, 0–100). */
84
84
  interface SlideElement {
85
85
  id: string;
86
- type: "text" | "image";
86
+ type: "text" | "image" | "shape";
87
87
  x: number;
88
88
  y: number;
89
89
  w: number;
@@ -94,6 +94,10 @@ interface SlideElement {
94
94
  bold?: boolean;
95
95
  color?: string;
96
96
  align?: "left" | "center" | "right";
97
+ /** Shape kind for `type: "shape"`. */
98
+ shape?: "rect" | "ellipse" | "line";
99
+ /** Fill (rect/ellipse) or stroke (line) color for a shape. */
100
+ fill?: string;
97
101
  }
98
102
  interface Slide {
99
103
  /** title · content (bullets) · section · image · two-column · blank (free canvas). */
@@ -369,14 +373,16 @@ interface ChatMessage {
369
373
  /** A command the editing UI forwards to the active html artifact's iframe. */
370
374
  interface IframeCommand {
371
375
  artifactId: string;
372
- /** style · structure (duplicate/delete/move/insert/insert_html) · group/ungroup · set_src · clear. */
373
- type: "set_style" | "commit" | "clear" | "set_src" | "duplicate" | "delete" | "move_up" | "move_down" | "insert" | "insert_html" | "group" | "ungroup";
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";
374
378
  /** Target element (omitted for document-level inserts with no selection). */
375
379
  cid?: string;
376
380
  /** Members to wrap for `group`. */
377
381
  cids?: string[];
378
382
  prop?: string;
379
383
  value?: string;
384
+ /** Style map to apply to the slide root for `set_slide_style` (e.g. background, color). */
385
+ style?: Record<string, string>;
380
386
  /** Tag/block to insert for `insert` (e.g. "h2", "p", "button", "img", "hr", "section"). */
381
387
  block?: string;
382
388
  /** HTML fragment to insert for `insert_html` (a built-in section template). */
package/dist/index.js CHANGED
@@ -180,7 +180,7 @@ var INSPECTOR_SCRIPT = `
180
180
  fmtBar.className = "lcx-fmt";
181
181
  fmtBar.style.left = Math.max(4, r.left) + "px";
182
182
  fmtBar.style.top = Math.max(4, r.top - 40) + "px";
183
- var specs = [["<b>B</b>", "bold"], ["<i>I</i>", "italic"], ["<u>U</u>", "underline"], ["\\uD83D\\uDD17", "createLink"]];
183
+ var specs = [["<b>B</b>", "bold"], ["<i>I</i>", "italic"], ["<u>U</u>", "underline"], ["&bull;", "insertUnorderedList"], ["1.", "insertOrderedList"], ["\\uD83D\\uDD17", "createLink"]];
184
184
  for (var i = 0; i < specs.length; i++) {
185
185
  (function (spec) {
186
186
  var btn = document.createElement("button");
@@ -569,6 +569,13 @@ var INSPECTOR_SCRIPT = `
569
569
  if (!d || d.source !== MARK) return;
570
570
  if (d.type === "clear") { clearSelected(); return; }
571
571
  if (d.type === "set_style") { var el = byCid(d.cid); if (el) el.style[d.prop] = d.value; return; }
572
+ if (d.type === "style_persist") { var elp = byCid(d.cid); if (elp) { elp.style[d.prop] = d.value; emitEdit(elp); } return; }
573
+ if (d.type === "set_slide_style") {
574
+ // Theme the whole slide: apply to the .slide-container (fallback body).
575
+ var root = document.querySelector(".slide-container") || document.body;
576
+ if (root && d.style) { for (var sk in d.style) { try { root.style[sk] = d.style[sk]; } catch (_e) {} } emitDoc(); }
577
+ return;
578
+ }
572
579
  if (d.type === "set_src") { var ei = byCid(d.cid); if (ei) { ei.setAttribute("src", d.value); emitEdit(ei); } return; }
573
580
  if (d.type === "commit") { var el2 = byCid(d.cid); if (el2) emitEdit(el2); return; }
574
581
 
@@ -1493,6 +1500,7 @@ var BLOCKS = [
1493
1500
  { tag: "img", label: "Image" },
1494
1501
  { tag: "hr", label: "Divider" }
1495
1502
  ];
1503
+ var SLIDE_BLOCK_TAGS = /* @__PURE__ */ new Set(["h2", "p", "img"]);
1496
1504
  var TEMPLATES = {
1497
1505
  hero: {
1498
1506
  label: "Hero",
@@ -1519,8 +1527,188 @@ var TEMPLATES = {
1519
1527
  <p style="margin:0 0 22px;opacity:.9">Join thousands of teams already building with us.</p>
1520
1528
  <a href="#" style="display:inline-block;padding:12px 24px;background:#fff;color:#4338ca;border-radius:10px;text-decoration:none;font-weight:700">Sign up</a>
1521
1529
  </section>`
1530
+ },
1531
+ navbar: {
1532
+ label: "Nav bar",
1533
+ html: `<nav style="display:flex;align-items:center;justify-content:space-between;padding:18px 32px;background:#0b1020;color:#e6e8ef;border-bottom:1px solid #232a44">
1534
+ <strong style="font-size:18px;letter-spacing:-.01em">Brand</strong>
1535
+ <div style="display:flex;gap:26px;align-items:center;font-size:15px">
1536
+ <a href="#" style="color:#c7cddb;text-decoration:none">Product</a>
1537
+ <a href="#" style="color:#c7cddb;text-decoration:none">Pricing</a>
1538
+ <a href="#" style="color:#c7cddb;text-decoration:none">Docs</a>
1539
+ <a href="#" style="padding:9px 18px;background:#6366f1;color:#fff;border-radius:9px;text-decoration:none;font-weight:600">Sign in</a>
1540
+ </div>
1541
+ </nav>`
1542
+ },
1543
+ pricing: {
1544
+ label: "Pricing",
1545
+ html: `<section style="padding:56px 24px;background:#0b1020;color:#e6e8ef">
1546
+ <div style="max-width:960px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:20px">
1547
+ <div style="background:#151a2e;border:1px solid #232a44;border-radius:16px;padding:28px"><h3 style="margin:0 0 6px">Starter</h3><p style="font-size:34px;font-weight:800;margin:0 0 4px">$0<span style="font-size:15px;font-weight:400;color:#9aa4b2">/mo</span></p><ul style="margin:16px 0 0;padding-left:1.1em;color:#9aa4b2;line-height:1.9"><li>1 project</li><li>Community support</li></ul></div>
1548
+ <div style="background:#1b2140;border:1px solid #3a44f1;border-radius:16px;padding:28px"><h3 style="margin:0 0 6px">Pro</h3><p style="font-size:34px;font-weight:800;margin:0 0 4px">$29<span style="font-size:15px;font-weight:400;color:#9aa4b2">/mo</span></p><ul style="margin:16px 0 0;padding-left:1.1em;color:#c7cddb;line-height:1.9"><li>Unlimited projects</li><li>Priority support</li></ul></div>
1549
+ <div style="background:#151a2e;border:1px solid #232a44;border-radius:16px;padding:28px"><h3 style="margin:0 0 6px">Team</h3><p style="font-size:34px;font-weight:800;margin:0 0 4px">$99<span style="font-size:15px;font-weight:400;color:#9aa4b2">/mo</span></p><ul style="margin:16px 0 0;padding-left:1.1em;color:#9aa4b2;line-height:1.9"><li>SSO &amp; roles</li><li>SLA</li></ul></div>
1550
+ </div>
1551
+ </section>`
1552
+ },
1553
+ stats: {
1554
+ label: "Stats",
1555
+ html: `<section style="padding:56px 24px;background:#0b1020;color:#e6e8ef">
1556
+ <div style="max-width:900px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:24px;text-align:center">
1557
+ <div><div style="font-size:44px;font-weight:800">12k+</div><div style="color:#9aa4b2">Customers</div></div>
1558
+ <div><div style="font-size:44px;font-weight:800">99.9%</div><div style="color:#9aa4b2">Uptime</div></div>
1559
+ <div><div style="font-size:44px;font-weight:800">4.9/5</div><div style="color:#9aa4b2">Rating</div></div>
1560
+ </div>
1561
+ </section>`
1562
+ },
1563
+ testimonial: {
1564
+ label: "Testimonial",
1565
+ html: `<section style="padding:64px 24px;background:#151a2e;color:#e6e8ef;text-align:center">
1566
+ <p style="max-width:640px;margin:0 auto 20px;font-size:24px;line-height:1.5;font-weight:600">\u201CThis is the tool our team didn't know it was missing. Shipped a landing page in an afternoon.\u201D</p>
1567
+ <p style="margin:0;color:#9aa4b2">Jordan Lee \xB7 Head of Product, Acme</p>
1568
+ </section>`
1569
+ },
1570
+ faq: {
1571
+ label: "FAQ",
1572
+ html: `<section style="padding:56px 24px;background:#0b1020;color:#e6e8ef">
1573
+ <div style="max-width:720px;margin:0 auto">
1574
+ <h2 style="margin:0 0 24px;font-size:26px">Frequently asked</h2>
1575
+ <div style="border-top:1px solid #232a44;padding:18px 0"><h4 style="margin:0 0 6px">Is there a free plan?</h4><p style="margin:0;color:#9aa4b2">Yes \u2014 the Starter plan is free forever.</p></div>
1576
+ <div style="border-top:1px solid #232a44;padding:18px 0"><h4 style="margin:0 0 6px">Can I cancel anytime?</h4><p style="margin:0;color:#9aa4b2">Absolutely, no questions asked.</p></div>
1577
+ </div>
1578
+ </section>`
1579
+ },
1580
+ gallery: {
1581
+ label: "Gallery",
1582
+ html: `<section style="padding:40px 24px;background:#0b1020">
1583
+ <div style="max-width:960px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px">
1584
+ <img src="https://placehold.co/400x300/1b2140/9aa4b2?text=1" alt="" style="width:100%;border-radius:12px;display:block" />
1585
+ <img src="https://placehold.co/400x300/1b2140/9aa4b2?text=2" alt="" style="width:100%;border-radius:12px;display:block" />
1586
+ <img src="https://placehold.co/400x300/1b2140/9aa4b2?text=3" alt="" style="width:100%;border-radius:12px;display:block" />
1587
+ </div>
1588
+ </section>`
1589
+ },
1590
+ contact: {
1591
+ label: "Contact form",
1592
+ html: `<section style="padding:56px 24px;background:#151a2e;color:#e6e8ef">
1593
+ <form style="max-width:480px;margin:0 auto;display:flex;flex-direction:column;gap:12px">
1594
+ <h2 style="margin:0 0 6px;font-size:24px">Get in touch</h2>
1595
+ <input placeholder="Your name" style="padding:12px 14px;border-radius:10px;border:1px solid #232a44;background:#0b1020;color:#e6e8ef" />
1596
+ <input placeholder="Email" style="padding:12px 14px;border-radius:10px;border:1px solid #232a44;background:#0b1020;color:#e6e8ef" />
1597
+ <textarea placeholder="Message" rows="4" style="padding:12px 14px;border-radius:10px;border:1px solid #232a44;background:#0b1020;color:#e6e8ef"></textarea>
1598
+ <button style="padding:12px;background:#6366f1;color:#fff;border:0;border-radius:10px;font-weight:700;cursor:pointer">Send</button>
1599
+ </form>
1600
+ </section>`
1601
+ },
1602
+ footer: {
1603
+ label: "Footer",
1604
+ html: `<footer style="padding:40px 32px;background:#080b16;color:#9aa4b2;display:flex;flex-wrap:wrap;gap:24px;justify-content:space-between;border-top:1px solid #232a44">
1605
+ <div><strong style="color:#e6e8ef;font-size:16px">Brand</strong><p style="margin:8px 0 0;max-width:260px;font-size:14px">Build faster with confidence.</p></div>
1606
+ <div style="display:flex;gap:48px;font-size:14px">
1607
+ <div style="display:flex;flex-direction:column;gap:8px"><a href="#" style="color:#9aa4b2;text-decoration:none">Product</a><a href="#" style="color:#9aa4b2;text-decoration:none">Pricing</a></div>
1608
+ <div style="display:flex;flex-direction:column;gap:8px"><a href="#" style="color:#9aa4b2;text-decoration:none">About</a><a href="#" style="color:#9aa4b2;text-decoration:none">Contact</a></div>
1609
+ </div>
1610
+ </footer>`
1522
1611
  }
1523
1612
  };
1613
+ var SLIDE_TEMPLATES = {
1614
+ title: {
1615
+ label: "Title",
1616
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1617
+ <h1 style="font-size:64px;font-weight:800;letter-spacing:-.02em;margin:0 0 20px;line-height:1.1">Presentation title</h1>
1618
+ <p style="font-size:26px;opacity:.7;margin:0">Subtitle or one-line summary</p>
1619
+ </div>`
1620
+ },
1621
+ section: {
1622
+ label: "Section",
1623
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1624
+ <div style="width:72px;height:6px;border-radius:3px;background:currentColor;opacity:.85;margin-bottom:28px"></div>
1625
+ <h2 style="font-size:52px;font-weight:800;margin:0;line-height:1.1">Section title</h2>
1626
+ </div>`
1627
+ },
1628
+ bullets: {
1629
+ label: "Bullets",
1630
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1631
+ <h2 style="font-size:40px;font-weight:750;margin:0 0 28px">Heading</h2>
1632
+ <ul style="font-size:26px;line-height:1.9;margin:0;padding-left:1.1em">
1633
+ <li>First key point</li><li>Second key point</li><li>Third key point</li>
1634
+ </ul>
1635
+ </div>`
1636
+ },
1637
+ "two-column": {
1638
+ label: "Two column",
1639
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:56px;align-content:center;padding:0 9%">
1640
+ <div><h3 style="font-size:28px;font-weight:700;margin:0 0 14px">Left heading</h3><p style="font-size:22px;line-height:1.6;margin:0;opacity:.85">Supporting copy for the left column.</p></div>
1641
+ <div><h3 style="font-size:28px;font-weight:700;margin:0 0 14px">Right heading</h3><p style="font-size:22px;line-height:1.6;margin:0;opacity:.85">Supporting copy for the right column.</p></div>
1642
+ </div>`
1643
+ },
1644
+ "image-text": {
1645
+ label: "Image + text",
1646
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;padding:0 9%">
1647
+ <img src="https://placehold.co/720x480/eef/335?text=Image" alt="image" style="width:100%;border-radius:16px;display:block" />
1648
+ <div><h2 style="font-size:36px;font-weight:750;margin:0 0 16px">Heading</h2><p style="font-size:24px;line-height:1.65;margin:0;opacity:.85">Explain the visual in a sentence or two.</p></div>
1649
+ </div>`
1650
+ },
1651
+ quote: {
1652
+ label: "Quote",
1653
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 12%">
1654
+ <p style="font-size:44px;font-weight:700;line-height:1.3;margin:0 0 24px">\u201CA short, memorable quote that anchors the point.\u201D</p>
1655
+ <p style="font-size:22px;opacity:.65;margin:0">\u2014 Attribution</p>
1656
+ </div>`
1657
+ },
1658
+ agenda: {
1659
+ label: "Agenda",
1660
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1661
+ <h2 style="font-size:40px;font-weight:750;margin:0 0 28px">Agenda</h2>
1662
+ <ol style="font-size:25px;line-height:2;margin:0;padding-left:1.2em">
1663
+ <li>Background &amp; goals</li><li>Approach</li><li>Results</li><li>Next steps</li>
1664
+ </ol>
1665
+ </div>`
1666
+ },
1667
+ stat: {
1668
+ label: "Big stat",
1669
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1670
+ <div style="font-size:140px;font-weight:800;letter-spacing:-.03em;line-height:1">12\xD7</div>
1671
+ <p style="font-size:28px;opacity:.7;margin:16px 0 0">faster than the previous approach</p>
1672
+ </div>`
1673
+ },
1674
+ comparison: {
1675
+ label: "Comparison",
1676
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:40px;align-content:center;padding:0 9%">
1677
+ <div style="border:2px solid currentColor;opacity:.9;border-radius:16px;padding:28px"><h3 style="font-size:26px;margin:0 0 14px">Before</h3><ul style="font-size:21px;line-height:1.8;margin:0;padding-left:1.1em"><li>Point one</li><li>Point two</li></ul></div>
1678
+ <div style="border-radius:16px;padding:28px;background:currentColor"><div style="color:#fff;mix-blend-mode:difference"><h3 style="font-size:26px;margin:0 0 14px">After</h3><ul style="font-size:21px;line-height:1.8;margin:0;padding-left:1.1em"><li>Point one</li><li>Point two</li></ul></div></div>
1679
+ </div>`
1680
+ },
1681
+ closing: {
1682
+ label: "Closing",
1683
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1684
+ <h1 style="font-size:56px;font-weight:800;margin:0 0 16px">Thank you</h1>
1685
+ <p style="font-size:24px;opacity:.7;margin:0">Questions? \xB7 contact@example.com</p>
1686
+ </div>`
1687
+ }
1688
+ };
1689
+ var SLIDE_THEMES = {
1690
+ light: { label: "Light", style: { background: "#ffffff", color: "#141821" } },
1691
+ paper: { label: "Paper", style: { background: "#f7f5ef", color: "#2b2a26" } },
1692
+ ink: { label: "Ink", style: { background: "#14161d", color: "#f0f1f5" } },
1693
+ navy: { label: "Navy", style: { background: "#0d1b3e", color: "#eef2ff" } },
1694
+ forest: { label: "Forest", style: { background: "#0f2a22", color: "#e6f2ec" } },
1695
+ sunset: { label: "Sunset", style: { background: "#2a1533", color: "#ffe8d6" } },
1696
+ brand: { label: "Brand", style: { background: "#ffffff", color: "#b01722" } }
1697
+ };
1698
+ var SLIDE_SHAPES = {
1699
+ rect: { label: "Rectangle", html: `<div style="width:280px;height:160px;background:currentColor;opacity:.9;border-radius:10px;margin:24px"></div>` },
1700
+ circle: { label: "Circle", html: `<div style="width:200px;height:200px;background:currentColor;opacity:.9;border-radius:50%;margin:24px"></div>` },
1701
+ line: { label: "Line", html: `<div style="width:360px;height:4px;background:currentColor;opacity:.85;margin:24px"></div>` },
1702
+ arrow: { label: "Arrow", html: `<svg width="240" height="60" viewBox="0 0 240 60" style="margin:24px;color:currentColor"><path d="M0 30 H210 M188 12 L212 30 L188 48" fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/></svg>` },
1703
+ badge: { label: "Pill", html: `<span style="display:inline-block;padding:10px 22px;border-radius:999px;background:currentColor;color:#fff;mix-blend-mode:normal;font-weight:700;margin:24px">Label</span>` }
1704
+ };
1705
+ var SLIDE_FONTS = {
1706
+ sans: { label: "Sans", stack: "'Segoe UI', system-ui, 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif" },
1707
+ serif: { label: "Serif", stack: "Georgia, 'Times New Roman', 'Apple SD Gothic Neo', serif" },
1708
+ mono: { label: "Mono", stack: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace" },
1709
+ rounded: { label: "Rounded", stack: "'Trebuchet MS', 'Segoe UI', 'Apple SD Gothic Neo', sans-serif" },
1710
+ condensed: { label: "Condensed", stack: "'Arial Narrow', 'Helvetica Neue', 'Apple SD Gothic Neo', sans-serif" }
1711
+ };
1524
1712
  var SLIDE_W = 1280;
1525
1713
  var SCROLL_FIX = "<style>html,body{overflow:auto!important;height:auto!important;min-height:100%!important}</style>";
1526
1714
  function withScrollableBody(html) {
@@ -1550,6 +1738,7 @@ function useSlideFit(ratio, boxRef) {
1550
1738
  function HtmlRenderer({ artifact: artifact2 }) {
1551
1739
  const iframeRef = useRef(null);
1552
1740
  const imgFileRef = useRef(null);
1741
+ const bgFileRef = useRef(null);
1553
1742
  const stageRef = useRef(null);
1554
1743
  const api = useCanvasStoreApi();
1555
1744
  const setSelections = useCanvasStore((s) => s.setSelections);
@@ -1625,6 +1814,17 @@ function HtmlRenderer({ artifact: artifact2 }) {
1625
1814
  reader.onload = () => sendIframeCommand({ artifactId: artifact2.id, type: "set_src", cid: single.cid, value: String(reader.result) });
1626
1815
  reader.readAsDataURL(file);
1627
1816
  };
1817
+ const onSlideBg = (file) => {
1818
+ if (!file) return;
1819
+ const reader = new FileReader();
1820
+ reader.onload = () => sendIframeCommand({
1821
+ artifactId: artifact2.id,
1822
+ type: "set_slide_style",
1823
+ style: { backgroundImage: `url("${String(reader.result)}")`, backgroundSize: "cover", backgroundPosition: "center" }
1824
+ });
1825
+ reader.readAsDataURL(file);
1826
+ };
1827
+ const setSlideStyle = (style) => sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style });
1628
1828
  const ratio = artifact2.meta?.ratio;
1629
1829
  const slide = useSlideFit(ratio, stageRef);
1630
1830
  return /* @__PURE__ */ jsxs("div", { className: "cv-html-wrap", children: [
@@ -1632,6 +1832,10 @@ function HtmlRenderer({ artifact: artifact2 }) {
1632
1832
  onImgFile(e.target.files?.[0]);
1633
1833
  e.target.value = "";
1634
1834
  } }),
1835
+ /* @__PURE__ */ jsx("input", { ref: bgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
1836
+ onSlideBg(e.target.files?.[0]);
1837
+ e.target.value = "";
1838
+ } }),
1635
1839
  /* @__PURE__ */ jsxs("div", { className: "cv-html-bar cv-chrome", children: [
1636
1840
  mode === "design" && /* @__PURE__ */ jsxs(Fragment, { children: [
1637
1841
  !ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1639,8 +1843,8 @@ function HtmlRenderer({ artifact: artifact2 }) {
1639
1843
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" })
1640
1844
  ] }),
1641
1845
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Add" }),
1642
- BLOCKS.map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: b.label }, b.tag)),
1643
- /* @__PURE__ */ jsxs(
1846
+ (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(
1644
1848
  "select",
1645
1849
  {
1646
1850
  className: "cv-html-tpl",
@@ -1657,6 +1861,77 @@ function HtmlRenderer({ artifact: artifact2 }) {
1657
1861
  ]
1658
1862
  }
1659
1863
  ),
1864
+ ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1865
+ /* @__PURE__ */ jsxs(
1866
+ "select",
1867
+ {
1868
+ className: "cv-html-tpl",
1869
+ value: "",
1870
+ title: "Insert a slide layout",
1871
+ onChange: (e) => {
1872
+ const t = SLIDE_TEMPLATES[e.target.value];
1873
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1874
+ e.currentTarget.value = "";
1875
+ },
1876
+ children: [
1877
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
1878
+ Object.entries(SLIDE_TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1879
+ ]
1880
+ }
1881
+ ),
1882
+ /* @__PURE__ */ jsxs(
1883
+ "select",
1884
+ {
1885
+ className: "cv-html-tpl",
1886
+ value: "",
1887
+ title: "Apply a slide theme",
1888
+ onChange: (e) => {
1889
+ const t = SLIDE_THEMES[e.target.value];
1890
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style: t.style });
1891
+ e.currentTarget.value = "";
1892
+ },
1893
+ children: [
1894
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
1895
+ Object.entries(SLIDE_THEMES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1896
+ ]
1897
+ }
1898
+ ),
1899
+ /* @__PURE__ */ jsxs(
1900
+ "select",
1901
+ {
1902
+ className: "cv-html-tpl",
1903
+ value: "",
1904
+ title: "Insert a shape",
1905
+ onChange: (e) => {
1906
+ const t = SLIDE_SHAPES[e.target.value];
1907
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1908
+ e.currentTarget.value = "";
1909
+ },
1910
+ children: [
1911
+ /* @__PURE__ */ jsx("option", { value: "", children: "Shape\u2026" }),
1912
+ Object.entries(SLIDE_SHAPES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1913
+ ]
1914
+ }
1915
+ ),
1916
+ /* @__PURE__ */ jsxs(
1917
+ "select",
1918
+ {
1919
+ className: "cv-html-tpl",
1920
+ value: "",
1921
+ title: "Slide font",
1922
+ onChange: (e) => {
1923
+ const f = SLIDE_FONTS[e.target.value];
1924
+ if (f) setSlideStyle({ fontFamily: f.stack });
1925
+ e.currentTarget.value = "";
1926
+ },
1927
+ children: [
1928
+ /* @__PURE__ */ jsx("option", { value: "", children: "Font\u2026" }),
1929
+ Object.entries(SLIDE_FONTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1930
+ ]
1931
+ }
1932
+ ),
1933
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: "Slide background image", onClick: () => bgFileRef.current?.click(), children: "\u{1F5BC} BG" })
1934
+ ] }),
1660
1935
  selected.length >= 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
1661
1936
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" }),
1662
1937
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Selection" }),
@@ -1684,6 +1959,12 @@ function HtmlRenderer({ artifact: artifact2 }) {
1684
1959
  }
1685
1960
  )
1686
1961
  ] }),
1962
+ single && single.tag !== "img" && /* @__PURE__ */ jsxs(Fragment, { children: [
1963
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align left", onClick: () => command("style_persist", { prop: "textAlign", value: "left" }), children: "\u2B05" }),
1964
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align center", onClick: () => command("style_persist", { prop: "textAlign", value: "center" }), children: "\u2B0C" }),
1965
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align right", onClick: () => command("style_persist", { prop: "textAlign", value: "right" }), children: "\u27A1" }),
1966
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Bold", onClick: () => command("style_persist", { prop: "fontWeight", value: "800" }), children: "B" })
1967
+ ] }),
1687
1968
  single && /* @__PURE__ */ jsxs(Fragment, { children: [
1688
1969
  /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Duplicate", onClick: () => command("duplicate"), children: "\u29C9" }),
1689
1970
  /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move up", onClick: () => command("move_up"), children: "\u2191" }),
@@ -1741,7 +2022,7 @@ function HtmlRenderer({ artifact: artifact2 }) {
1741
2022
  var ChartRenderer = lazy(() => import('./ChartRenderer-4MWKP2J4.js').then((m) => ({ default: m.ChartRenderer })));
1742
2023
  var DocumentRenderer = lazy(() => import('./DocumentRenderer-LJGP6N7E.js').then((m) => ({ default: m.DocumentRenderer })));
1743
2024
  var TableRenderer = lazy(() => import('./TableRenderer-ZF7MVA7I.js').then((m) => ({ default: m.TableRenderer })));
1744
- var SlidesRenderer = lazy(() => import('./SlidesRenderer-A5HX4MOA.js').then((m) => ({ default: m.SlidesRenderer })));
2025
+ var SlidesRenderer = lazy(() => import('./SlidesRenderer-7ICJQHYT.js').then((m) => ({ default: m.SlidesRenderer })));
1745
2026
  var builtinRenderers = {
1746
2027
  html: HtmlRenderer,
1747
2028
  document: DocumentRenderer,
@@ -1880,13 +2161,20 @@ async function slidesToPptx(data, _title) {
1880
2161
  const H = 5.625;
1881
2162
  for (const slide of data.slides) {
1882
2163
  const s = pptx.addSlide();
1883
- if (slide.background) s.background = { color: slide.background.replace("#", "") };
2164
+ if (slide.background && /^#[0-9a-f]{3,8}$/i.test(slide.background)) s.background = { color: slide.background.replace("#", "") };
1884
2165
  const tc = slide.textColor ? slide.textColor.replace("#", "") : void 0;
1885
2166
  for (const el of resolveElements(slide)) {
1886
2167
  const box = { x: el.x / 100 * W, y: el.y / 100 * H, w: el.w / 100 * W, h: el.h / 100 * H };
1887
2168
  if (el.type === "text") {
1888
2169
  const color = el.color ? el.color.replace("#", "") : tc;
1889
2170
  s.addText(el.text ?? "", { ...box, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
2171
+ } else if (el.type === "shape") {
2172
+ const fill = (el.fill ?? "#5b5bd6").replace("#", "");
2173
+ if (el.shape === "line") {
2174
+ s.addShape(pptx.ShapeType.line, { ...box, line: { color: fill, width: 2 } });
2175
+ } else {
2176
+ s.addShape(el.shape === "ellipse" ? pptx.ShapeType.ellipse : pptx.ShapeType.rect, { ...box, fill: { color: fill } });
2177
+ }
1890
2178
  } else if (el.src) {
1891
2179
  s.addImage({ data: el.src, ...box, sizing: { type: "contain", w: box.w, h: box.h } });
1892
2180
  }
@@ -1903,9 +2191,14 @@ function slidesToPrintHtml(data, title) {
1903
2191
  const els = resolveElements(slide).map((el) => {
1904
2192
  const box = `left:${el.x}%;top:${el.y}%;width:${el.w}%;height:${el.h}%`;
1905
2193
  if (el.type === "text") {
1906
- const style = `${box};font-size:${(el.fontSize ?? 24) / 7.2}vw;font-weight:${el.bold ? 700 : 400};color:${escapeAttr(el.color ?? fg)};text-align:${escapeAttr(el.align ?? "left")}`;
2194
+ const style = `${box};font-size:${(el.fontSize ?? 24) / 7.2}vw;font-weight:${el.bold ? 700 : 400};color:${escapeAttr(el.color ?? fg)};text-align:${escapeAttr(el.align ?? "left")};white-space:pre-wrap`;
1907
2195
  return `<div class="el" style="${style}">${escapeXml(el.text ?? "")}</div>`;
1908
2196
  }
2197
+ if (el.type === "shape") {
2198
+ const fill = escapeAttr(el.fill ?? fg);
2199
+ const radius = el.shape === "ellipse" ? "50%" : el.shape === "line" ? "2px" : "8px";
2200
+ return `<div class="el" style="${box};background:${fill};border-radius:${radius}"></div>`;
2201
+ }
1909
2202
  const src = safeSrc(el.src);
1910
2203
  return src ? `<img class="el" style="${box}" src="${escapeAttr(src)}"/>` : "";
1911
2204
  }).join("");
@@ -1920,6 +2213,13 @@ function slidesToPrintHtml(data, title) {
1920
2213
  img.el { object-fit: contain; }
1921
2214
  </style></head><body>${pages}</body></html>`;
1922
2215
  }
2216
+ function htmlSlideToPrintHtml(html, ratio) {
2217
+ const w = ratio === "4:3" ? 960 : 1280;
2218
+ const h = 720;
2219
+ 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>`;
2220
+ const i = html.toLowerCase().lastIndexOf("</head>");
2221
+ return i === -1 ? style + html : html.slice(0, i) + style + html.slice(i);
2222
+ }
1923
2223
  function escapeXml(value) {
1924
2224
  return String(value ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
1925
2225
  }
@@ -1983,7 +2283,9 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
1983
2283
  const dataOptions = dataExporters[artifact2.type] ?? [];
1984
2284
  const exportHtml = () => {
1985
2285
  if (artifact2.type === "html") {
1986
- downloadBlob(`${stem}.html`, "text/html", artifact2.data.html);
2286
+ const ratio = artifact2.meta?.ratio;
2287
+ const html = artifact2.data.html;
2288
+ downloadBlob(`${stem}.html`, "text/html", ratio ? htmlSlideToPrintHtml(html, ratio) : html);
1987
2289
  } else {
1988
2290
  const html = getRenderedHtml();
1989
2291
  if (html == null) return;
@@ -2000,7 +2302,9 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2000
2302
  if (artifact2.type === "slides") {
2001
2303
  printToPdf(slidesToPrintHtml(artifact2.data, artifact2.title));
2002
2304
  } else if (artifact2.type === "html") {
2003
- printToPdf(artifact2.data.html);
2305
+ const ratio = artifact2.meta?.ratio;
2306
+ const html = artifact2.data.html;
2307
+ printToPdf(ratio ? htmlSlideToPrintHtml(html, ratio) : html);
2004
2308
  } else {
2005
2309
  const html = getRenderedHtml();
2006
2310
  if (html == null) return;
@@ -2008,6 +2312,30 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2008
2312
  }
2009
2313
  setOpen(false);
2010
2314
  };
2315
+ const openInTab = () => {
2316
+ const html = artifact2.type === "html" ? artifact2.data.html : artifact2.type === "slides" ? slidesToPrintHtml(artifact2.data, artifact2.title) : (() => {
2317
+ const h = getRenderedHtml();
2318
+ return h == null ? null : toStandaloneHtml(artifact2.title, h);
2319
+ })();
2320
+ if (html == null) return;
2321
+ const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
2322
+ window.open(url, "_blank", "noopener");
2323
+ setTimeout(() => URL.revokeObjectURL(url), 1e4);
2324
+ setOpen(false);
2325
+ };
2326
+ const [copied, setCopied] = useState(false);
2327
+ const copyHtml = async () => {
2328
+ const html = artifact2.type === "html" ? artifact2.data.html : getRenderedHtml();
2329
+ if (html == null) return;
2330
+ try {
2331
+ await navigator.clipboard.writeText(
2332
+ artifact2.type === "html" ? html : toStandaloneHtml(artifact2.title, html)
2333
+ );
2334
+ setCopied(true);
2335
+ setTimeout(() => setCopied(false), 1400);
2336
+ } catch {
2337
+ }
2338
+ };
2011
2339
  return /* @__PURE__ */ jsxs("div", { className: "cv-export", children: [
2012
2340
  /* @__PURE__ */ jsx(
2013
2341
  "button",
@@ -2022,6 +2350,8 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2022
2350
  open && /* @__PURE__ */ jsxs(Fragment, { children: [
2023
2351
  /* @__PURE__ */ jsx("div", { className: "cv-export__scrim", onClick: () => setOpen(false) }),
2024
2352
  /* @__PURE__ */ jsxs("div", { className: "cv-export__menu", role: "menu", children: [
2353
+ /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: openInTab, children: "Open in new tab \u2197" }),
2354
+ /* @__PURE__ */ jsx("button", { role: "menuitem", onClick: copyHtml, children: copied ? "Copied \u2713" : "Copy HTML" }),
2025
2355
  /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: exportHtml, children: [
2026
2356
  "HTML ",
2027
2357
  /* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: ".html" })
package/dist/styles.css CHANGED
@@ -617,6 +617,16 @@
617
617
  .cv-present__slide .cv-slide__subtitle { font-size: clamp(16px, 2.6vw, 30px); }
618
618
  .cv-present__slide .cv-slide__bullets li { font-size: clamp(16px, 2.4vw, 30px); }
619
619
  .cv-present__hint { color: #9aa4b2; font-size: 13px; }
620
+ /* Fade each slide in as the presenter advances. */
621
+ .cv-present__fade { animation: cv-present-fade 0.32s ease; }
622
+ @keyframes cv-present-fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
623
+ @media (prefers-reduced-motion: reduce) { .cv-present__fade { animation: none; } }
624
+ /* Speaker notes shown to the presenter, below the slide. */
625
+ .cv-present__notes {
626
+ max-width: min(92vw, 1280px); color: #cdd3de; font-size: 15px; line-height: 1.5;
627
+ background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.08);
628
+ border-radius: 10px; padding: 12px 16px; cursor: default; white-space: pre-wrap;
629
+ }
620
630
 
621
631
  /* in-place editing cues */
622
632
  .cv-word .cv-doc { cursor: text; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@braincrew-lab/langchain-canvas",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
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",