@braincrew-lab/langchain-canvas 0.1.7 → 0.1.9

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.
@@ -1,5 +1,5 @@
1
- import { useArtifactPatch } from './chunk-HHJWIO2C.js';
2
- import './chunk-PSIT32I5.js';
1
+ import { useArtifactPatch } from './chunk-TERWLUW3.js';
2
+ import './chunk-KKLWKR5G.js';
3
3
  import { useState, useMemo, useRef, useEffect } from 'react';
4
4
  import * as echarts from 'echarts/core';
5
5
  import { BarChart, LineChart, PieChart } from 'echarts/charts';
@@ -1,5 +1,5 @@
1
- import { useArtifactPatch } from './chunk-HHJWIO2C.js';
2
- import './chunk-PSIT32I5.js';
1
+ import { useArtifactPatch } from './chunk-TERWLUW3.js';
2
+ import './chunk-KKLWKR5G.js';
3
3
  import { useState } from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
5
5
  import remarkGfm from 'remark-gfm';
@@ -1,9 +1,15 @@
1
1
  import { resolveElements } from './chunk-6L3AL6W4.js';
2
- import { useArtifactPatch } from './chunk-HHJWIO2C.js';
3
- import './chunk-PSIT32I5.js';
2
+ import { useArtifactPatch } from './chunk-TERWLUW3.js';
3
+ 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
  " / ",
@@ -1,5 +1,5 @@
1
1
  import { loadOptional } from './chunk-YZZSJJMQ.js';
2
- import { useCanvasStore } from './chunk-PSIT32I5.js';
2
+ import { useCanvasStore } from './chunk-KKLWKR5G.js';
3
3
  import { lazy, useMemo, useState, useRef, useEffect, useCallback, Suspense } from 'react';
4
4
  import '@fortune-sheet/react/dist/index.css';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -208,7 +208,7 @@ function TableRenderer({ artifact }) {
208
208
  // eslint-disable-next-line react-hooks/exhaustive-deps
209
209
  [dataKey, formulasReady]
210
210
  );
211
- const applyEvent = useCanvasStore((s) => s.applyEvent);
211
+ const applyEvent = useCanvasStore((s) => s.applyUserEvent);
212
212
  const persistTimer = useRef(null);
213
213
  const handleChange = useCallback(
214
214
  (sheets) => {
@@ -129,6 +129,19 @@ function lastOf(order, excludeId) {
129
129
  const remaining = order.filter((id) => id !== excludeId);
130
130
  return remaining.length ? remaining[remaining.length - 1] : null;
131
131
  }
132
+ function editedArtifactId(event) {
133
+ switch (event.type) {
134
+ case "canvas.create":
135
+ case "canvas.replace":
136
+ return event.artifact.id;
137
+ case "canvas.append":
138
+ case "canvas.patch":
139
+ case "canvas.node_patch":
140
+ return event.id;
141
+ default:
142
+ return null;
143
+ }
144
+ }
132
145
  var initialState = () => ({
133
146
  canvas: emptyCanvasState(),
134
147
  messages: [],
@@ -137,17 +150,24 @@ var initialState = () => ({
137
150
  selections: [],
138
151
  iframeCommand: null,
139
152
  undoStack: [],
140
- redoStack: []
153
+ redoStack: [],
154
+ onUserEdit: null
141
155
  });
142
156
  function createCanvasStore() {
143
- return createStore((set) => ({
157
+ return createStore((set, get) => ({
144
158
  ...initialState(),
145
159
  applyEvent: (event) => set((state) => foldEvent(state, event)),
146
160
  applyEvents: (events) => set((state) => events.reduce(foldEvent, state)),
147
- applyUserEvent: (event) => set((state) => {
148
- const undoStack = [...state.undoStack, state.canvas].slice(-50);
149
- return { ...foldEvent(state, event), undoStack, redoStack: [] };
150
- }),
161
+ applyUserEvent: (event) => {
162
+ set((state2) => {
163
+ const undoStack = [...state2.undoStack, state2.canvas].slice(-50);
164
+ return { ...foldEvent(state2, event), undoStack, redoStack: [] };
165
+ });
166
+ const state = get();
167
+ const id = editedArtifactId(event);
168
+ const artifact = id ? state.canvas.artifacts[id] : void 0;
169
+ if (artifact) state.onUserEdit?.(artifact);
170
+ },
151
171
  undo: () => set((state) => {
152
172
  if (!state.undoStack.length) return state;
153
173
  const previous = state.undoStack[state.undoStack.length - 1];
@@ -176,6 +196,7 @@ function createCanvasStore() {
176
196
  setActiveArtifact: (id) => set((state) => ({ canvas: { ...state.canvas, activeId: id } })),
177
197
  setSelections: (selections) => set({ selections }),
178
198
  sendIframeCommand: (command) => set((state) => ({ iframeCommand: { ...command, seq: (state.iframeCommand?.seq ?? 0) + 1 } })),
199
+ setOnUserEdit: (handler) => set({ onUserEdit: handler }),
179
200
  reset: () => set(initialState())
180
201
  }));
181
202
  }
@@ -1,4 +1,4 @@
1
- import { useCanvasStore } from './chunk-PSIT32I5.js';
1
+ import { useCanvasStore } from './chunk-KKLWKR5G.js';
2
2
  import { useCallback } from 'react';
3
3
 
4
4
  function useArtifactPatch(id) {
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). */
@@ -355,6 +359,10 @@ declare function mockStream(events: StreamEvent[], options?: MockStreamOptions):
355
359
  * patching, and versioning stay in one auditable place.
356
360
  */
357
361
 
362
+ /** Fired when the *user* edits an artifact in the canvas (a table cell, a chart
363
+ * value, document text, a slide/HTML element). The host wires this to sync the
364
+ * edit back to the agent/backend so the next turn sees it. */
365
+ type UserEditHandler = (artifact: Artifact) => void;
358
366
  interface ChatMessage {
359
367
  id: string;
360
368
  role: "user" | "assistant";
@@ -365,14 +373,16 @@ interface ChatMessage {
365
373
  /** A command the editing UI forwards to the active html artifact's iframe. */
366
374
  interface IframeCommand {
367
375
  artifactId: string;
368
- /** style · structure (duplicate/delete/move/insert/insert_html) · group/ungroup · set_src · clear. */
369
- 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";
370
378
  /** Target element (omitted for document-level inserts with no selection). */
371
379
  cid?: string;
372
380
  /** Members to wrap for `group`. */
373
381
  cids?: string[];
374
382
  prop?: string;
375
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>;
376
386
  /** Tag/block to insert for `insert` (e.g. "h2", "p", "button", "img", "hr", "section"). */
377
387
  block?: string;
378
388
  /** HTML fragment to insert for `insert_html` (a built-in section template). */
@@ -392,6 +402,8 @@ interface CanvasStore {
392
402
  /** Snapshots for undo/redo — only user edits are recorded (not agent streaming). */
393
403
  undoStack: CanvasState[];
394
404
  redoStack: CanvasState[];
405
+ /** Host callback fired after a user edit reconciles — the write-back hook. */
406
+ onUserEdit: UserEditHandler | null;
395
407
  applyEvent: (event: StreamEvent) => void;
396
408
  /** Apply a batch of events in a single store write (one re-render per frame). */
397
409
  applyEvents: (events: StreamEvent[]) => void;
@@ -404,6 +416,8 @@ interface CanvasStore {
404
416
  setActiveArtifact: (id: string) => void;
405
417
  setSelections: (selections: ElementSelection[]) => void;
406
418
  sendIframeCommand: (command: Omit<IframeCommand, "seq">) => void;
419
+ /** Register (or clear) the user-edit write-back handler. */
420
+ setOnUserEdit: (handler: UserEditHandler | null) => void;
407
421
  reset: () => void;
408
422
  }
409
423
  /** Create an isolated canvas store. */
@@ -508,8 +522,16 @@ interface CanvasProps {
508
522
  * reveals a quick-edit bar.
509
523
  */
510
524
  onEditElement?: (instruction: string) => void;
525
+ /**
526
+ * Fired after the *user* edits an artifact directly in the canvas — a table
527
+ * cell, a chart value, document text, a slide/HTML element — with the
528
+ * reconciled artifact. Wire this to sync the edit back to the agent/backend so
529
+ * the next turn sees it. Fires per committed edit (table edits are debounced);
530
+ * debounce further on the host before hitting the network.
531
+ */
532
+ onUserEdit?: (artifact: Artifact) => void;
511
533
  }
512
- declare function Canvas({ registry, emptyState, onEditElement }: CanvasProps): react.JSX.Element;
534
+ declare function Canvas({ registry, emptyState, onEditElement, onUserEdit }: CanvasProps): react.JSX.Element;
513
535
 
514
536
  interface ExportMenuProps {
515
537
  artifact: Artifact;
@@ -666,4 +688,4 @@ declare function useCanvasImport(): {
666
688
  canImport: (file: File) => boolean;
667
689
  };
668
690
 
669
- export { type Artifact, ArtifactCard, type ArtifactRegistry, type ArtifactRenderer, type ArtifactStatus, Canvas, type CanvasAppend, type CanvasClose, type CanvasCreate, type CanvasEvent, type CanvasNodePatch, type CanvasPatch, type CanvasProps, CanvasProvider, type CanvasProviderProps, CanvasRegistryProvider, type CanvasReplace, type CanvasState, type CanvasStatus, type CanvasStore, type ChartArtifact, type ChartData, type ChartOptions, ChartRenderer, type ChartSeries, type ChatEvent, type ChatMessage, type ChatRequest, type DocumentArtifact, type DocumentData, DocumentRenderer, type DoneEvent, type ElementSelection, type ErrorEvent, ExportMenu, type FileExport, type HtmlArtifact, type HtmlData, HtmlRenderer, IMPORTABLE_EXTENSIONS, INSPECTOR_MARK, type IframeCommand, type KnownArtifact, type MessageDelta, type MessageEnd, type MockStreamOptions, type RendererProps, STYLE_PROPS, type Scenario, SelectionBar, type Slide, type SlideElement, type SlidesArtifact, type SlidesData, SlidesRenderer, type StreamEvent, type StreamOptions, StylePanel, type TableArtifact, type TableColumn, type TableData, TableRenderer, type ToolEnd, type ToolStart, type UseCanvasStreamOptions, builtinRenderers, canImport, createCanvasStore, dataExporters, downloadBlob, emptyCanvasState, importFile, isCanvasEvent, isChatEvent, mergePatch, mergeRegistries, mockStream, parseCsv, parseSSE, printToPdf, reduceCanvas, scenarios, slidesToPrintHtml, slugify, streamChat, toStandaloneHtml, useArtifactPatch, useCanvasImport, useCanvasReplay, useCanvasStore, useCanvasStoreApi, useCanvasStream, useRenderer, withInspector };
691
+ export { type Artifact, ArtifactCard, type ArtifactRegistry, type ArtifactRenderer, type ArtifactStatus, Canvas, type CanvasAppend, type CanvasClose, type CanvasCreate, type CanvasEvent, type CanvasNodePatch, type CanvasPatch, type CanvasProps, CanvasProvider, type CanvasProviderProps, CanvasRegistryProvider, type CanvasReplace, type CanvasState, type CanvasStatus, type CanvasStore, type ChartArtifact, type ChartData, type ChartOptions, ChartRenderer, type ChartSeries, type ChatEvent, type ChatMessage, type ChatRequest, type DocumentArtifact, type DocumentData, DocumentRenderer, type DoneEvent, type ElementSelection, type ErrorEvent, ExportMenu, type FileExport, type HtmlArtifact, type HtmlData, HtmlRenderer, IMPORTABLE_EXTENSIONS, INSPECTOR_MARK, type IframeCommand, type KnownArtifact, type MessageDelta, type MessageEnd, type MockStreamOptions, type RendererProps, STYLE_PROPS, type Scenario, SelectionBar, type Slide, type SlideElement, type SlidesArtifact, type SlidesData, SlidesRenderer, type StreamEvent, type StreamOptions, StylePanel, type TableArtifact, type TableColumn, type TableData, TableRenderer, type ToolEnd, type ToolStart, type UseCanvasStreamOptions, type UserEditHandler, builtinRenderers, canImport, createCanvasStore, dataExporters, downloadBlob, emptyCanvasState, importFile, isCanvasEvent, isChatEvent, mergePatch, mergeRegistries, mockStream, parseCsv, parseSSE, printToPdf, reduceCanvas, scenarios, slidesToPrintHtml, slugify, streamChat, toStandaloneHtml, useArtifactPatch, useCanvasImport, useCanvasReplay, useCanvasStore, useCanvasStoreApi, useCanvasStream, useRenderer, withInspector };
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
  import { loadOptional } from './chunk-YZZSJJMQ.js';
3
3
  import { resolveElements } from './chunk-6L3AL6W4.js';
4
- export { useArtifactPatch } from './chunk-HHJWIO2C.js';
5
- import { useCanvasStoreApi, useCanvasStore } from './chunk-PSIT32I5.js';
6
- export { CanvasProvider, createCanvasStore, emptyCanvasState, isCanvasEvent, isChatEvent, mergePatch, reduceCanvas, useCanvasStore, useCanvasStoreApi } from './chunk-PSIT32I5.js';
4
+ export { useArtifactPatch } from './chunk-TERWLUW3.js';
5
+ import { useCanvasStoreApi, useCanvasStore } from './chunk-KKLWKR5G.js';
6
+ export { CanvasProvider, createCanvasStore, emptyCanvasState, isCanvasEvent, isChatEvent, mergePatch, reduceCanvas, useCanvasStore, useCanvasStoreApi } from './chunk-KKLWKR5G.js';
7
7
  import { createContext, lazy, useRef, useCallback, useEffect, useContext, useState, useMemo, Suspense, Component } from 'react';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
@@ -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",
@@ -1521,6 +1529,105 @@ var TEMPLATES = {
1521
1529
  </section>`
1522
1530
  }
1523
1531
  };
1532
+ var SLIDE_TEMPLATES = {
1533
+ title: {
1534
+ label: "Title",
1535
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1536
+ <h1 style="font-size:64px;font-weight:800;letter-spacing:-.02em;margin:0 0 20px;line-height:1.1">Presentation title</h1>
1537
+ <p style="font-size:26px;opacity:.7;margin:0">Subtitle or one-line summary</p>
1538
+ </div>`
1539
+ },
1540
+ section: {
1541
+ label: "Section",
1542
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1543
+ <div style="width:72px;height:6px;border-radius:3px;background:currentColor;opacity:.85;margin-bottom:28px"></div>
1544
+ <h2 style="font-size:52px;font-weight:800;margin:0;line-height:1.1">Section title</h2>
1545
+ </div>`
1546
+ },
1547
+ bullets: {
1548
+ label: "Bullets",
1549
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1550
+ <h2 style="font-size:40px;font-weight:750;margin:0 0 28px">Heading</h2>
1551
+ <ul style="font-size:26px;line-height:1.9;margin:0;padding-left:1.1em">
1552
+ <li>First key point</li><li>Second key point</li><li>Third key point</li>
1553
+ </ul>
1554
+ </div>`
1555
+ },
1556
+ "two-column": {
1557
+ label: "Two column",
1558
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:56px;align-content:center;padding:0 9%">
1559
+ <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>
1560
+ <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>
1561
+ </div>`
1562
+ },
1563
+ "image-text": {
1564
+ label: "Image + text",
1565
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:56px;align-items:center;padding:0 9%">
1566
+ <img src="https://placehold.co/720x480/eef/335?text=Image" alt="image" style="width:100%;border-radius:16px;display:block" />
1567
+ <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>
1568
+ </div>`
1569
+ },
1570
+ quote: {
1571
+ label: "Quote",
1572
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 12%">
1573
+ <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>
1574
+ <p style="font-size:22px;opacity:.65;margin:0">\u2014 Attribution</p>
1575
+ </div>`
1576
+ },
1577
+ agenda: {
1578
+ label: "Agenda",
1579
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;padding:0 9%">
1580
+ <h2 style="font-size:40px;font-weight:750;margin:0 0 28px">Agenda</h2>
1581
+ <ol style="font-size:25px;line-height:2;margin:0;padding-left:1.2em">
1582
+ <li>Background &amp; goals</li><li>Approach</li><li>Results</li><li>Next steps</li>
1583
+ </ol>
1584
+ </div>`
1585
+ },
1586
+ stat: {
1587
+ label: "Big stat",
1588
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1589
+ <div style="font-size:140px;font-weight:800;letter-spacing:-.03em;line-height:1">12\xD7</div>
1590
+ <p style="font-size:28px;opacity:.7;margin:16px 0 0">faster than the previous approach</p>
1591
+ </div>`
1592
+ },
1593
+ comparison: {
1594
+ label: "Comparison",
1595
+ html: `<div style="height:100%;display:grid;grid-template-columns:1fr 1fr;gap:40px;align-content:center;padding:0 9%">
1596
+ <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>
1597
+ <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>
1598
+ </div>`
1599
+ },
1600
+ closing: {
1601
+ label: "Closing",
1602
+ html: `<div style="height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;padding:0 8%">
1603
+ <h1 style="font-size:56px;font-weight:800;margin:0 0 16px">Thank you</h1>
1604
+ <p style="font-size:24px;opacity:.7;margin:0">Questions? \xB7 contact@example.com</p>
1605
+ </div>`
1606
+ }
1607
+ };
1608
+ var SLIDE_THEMES = {
1609
+ light: { label: "Light", style: { background: "#ffffff", color: "#141821" } },
1610
+ paper: { label: "Paper", style: { background: "#f7f5ef", color: "#2b2a26" } },
1611
+ ink: { label: "Ink", style: { background: "#14161d", color: "#f0f1f5" } },
1612
+ navy: { label: "Navy", style: { background: "#0d1b3e", color: "#eef2ff" } },
1613
+ forest: { label: "Forest", style: { background: "#0f2a22", color: "#e6f2ec" } },
1614
+ sunset: { label: "Sunset", style: { background: "#2a1533", color: "#ffe8d6" } },
1615
+ brand: { label: "Brand", style: { background: "#ffffff", color: "#b01722" } }
1616
+ };
1617
+ var SLIDE_SHAPES = {
1618
+ rect: { label: "Rectangle", html: `<div style="width:280px;height:160px;background:currentColor;opacity:.9;border-radius:10px;margin:24px"></div>` },
1619
+ circle: { label: "Circle", html: `<div style="width:200px;height:200px;background:currentColor;opacity:.9;border-radius:50%;margin:24px"></div>` },
1620
+ line: { label: "Line", html: `<div style="width:360px;height:4px;background:currentColor;opacity:.85;margin:24px"></div>` },
1621
+ 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>` },
1622
+ 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>` }
1623
+ };
1624
+ var SLIDE_FONTS = {
1625
+ sans: { label: "Sans", stack: "'Segoe UI', system-ui, 'Apple SD Gothic Neo', 'Noto Sans KR', sans-serif" },
1626
+ serif: { label: "Serif", stack: "Georgia, 'Times New Roman', 'Apple SD Gothic Neo', serif" },
1627
+ mono: { label: "Mono", stack: "ui-monospace, 'SF Mono', Menlo, Consolas, monospace" },
1628
+ rounded: { label: "Rounded", stack: "'Trebuchet MS', 'Segoe UI', 'Apple SD Gothic Neo', sans-serif" },
1629
+ condensed: { label: "Condensed", stack: "'Arial Narrow', 'Helvetica Neue', 'Apple SD Gothic Neo', sans-serif" }
1630
+ };
1524
1631
  var SLIDE_W = 1280;
1525
1632
  var SCROLL_FIX = "<style>html,body{overflow:auto!important;height:auto!important;min-height:100%!important}</style>";
1526
1633
  function withScrollableBody(html) {
@@ -1550,6 +1657,7 @@ function useSlideFit(ratio, boxRef) {
1550
1657
  function HtmlRenderer({ artifact: artifact2 }) {
1551
1658
  const iframeRef = useRef(null);
1552
1659
  const imgFileRef = useRef(null);
1660
+ const bgFileRef = useRef(null);
1553
1661
  const stageRef = useRef(null);
1554
1662
  const api = useCanvasStoreApi();
1555
1663
  const setSelections = useCanvasStore((s) => s.setSelections);
@@ -1625,6 +1733,17 @@ function HtmlRenderer({ artifact: artifact2 }) {
1625
1733
  reader.onload = () => sendIframeCommand({ artifactId: artifact2.id, type: "set_src", cid: single.cid, value: String(reader.result) });
1626
1734
  reader.readAsDataURL(file);
1627
1735
  };
1736
+ const onSlideBg = (file) => {
1737
+ if (!file) return;
1738
+ const reader = new FileReader();
1739
+ reader.onload = () => sendIframeCommand({
1740
+ artifactId: artifact2.id,
1741
+ type: "set_slide_style",
1742
+ style: { backgroundImage: `url("${String(reader.result)}")`, backgroundSize: "cover", backgroundPosition: "center" }
1743
+ });
1744
+ reader.readAsDataURL(file);
1745
+ };
1746
+ const setSlideStyle = (style) => sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style });
1628
1747
  const ratio = artifact2.meta?.ratio;
1629
1748
  const slide = useSlideFit(ratio, stageRef);
1630
1749
  return /* @__PURE__ */ jsxs("div", { className: "cv-html-wrap", children: [
@@ -1632,6 +1751,10 @@ function HtmlRenderer({ artifact: artifact2 }) {
1632
1751
  onImgFile(e.target.files?.[0]);
1633
1752
  e.target.value = "";
1634
1753
  } }),
1754
+ /* @__PURE__ */ jsx("input", { ref: bgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
1755
+ onSlideBg(e.target.files?.[0]);
1756
+ e.target.value = "";
1757
+ } }),
1635
1758
  /* @__PURE__ */ jsxs("div", { className: "cv-html-bar cv-chrome", children: [
1636
1759
  mode === "design" && /* @__PURE__ */ jsxs(Fragment, { children: [
1637
1760
  !ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1639,8 +1762,8 @@ function HtmlRenderer({ artifact: artifact2 }) {
1639
1762
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" })
1640
1763
  ] }),
1641
1764
  /* @__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(
1765
+ (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)),
1766
+ !ratio && /* @__PURE__ */ jsxs(
1644
1767
  "select",
1645
1768
  {
1646
1769
  className: "cv-html-tpl",
@@ -1657,6 +1780,77 @@ function HtmlRenderer({ artifact: artifact2 }) {
1657
1780
  ]
1658
1781
  }
1659
1782
  ),
1783
+ ratio && /* @__PURE__ */ jsxs(Fragment, { children: [
1784
+ /* @__PURE__ */ jsxs(
1785
+ "select",
1786
+ {
1787
+ className: "cv-html-tpl",
1788
+ value: "",
1789
+ title: "Insert a slide layout",
1790
+ onChange: (e) => {
1791
+ const t = SLIDE_TEMPLATES[e.target.value];
1792
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1793
+ e.currentTarget.value = "";
1794
+ },
1795
+ children: [
1796
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
1797
+ Object.entries(SLIDE_TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1798
+ ]
1799
+ }
1800
+ ),
1801
+ /* @__PURE__ */ jsxs(
1802
+ "select",
1803
+ {
1804
+ className: "cv-html-tpl",
1805
+ value: "",
1806
+ title: "Apply a slide theme",
1807
+ onChange: (e) => {
1808
+ const t = SLIDE_THEMES[e.target.value];
1809
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "set_slide_style", style: t.style });
1810
+ e.currentTarget.value = "";
1811
+ },
1812
+ children: [
1813
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
1814
+ Object.entries(SLIDE_THEMES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1815
+ ]
1816
+ }
1817
+ ),
1818
+ /* @__PURE__ */ jsxs(
1819
+ "select",
1820
+ {
1821
+ className: "cv-html-tpl",
1822
+ value: "",
1823
+ title: "Insert a shape",
1824
+ onChange: (e) => {
1825
+ const t = SLIDE_SHAPES[e.target.value];
1826
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1827
+ e.currentTarget.value = "";
1828
+ },
1829
+ children: [
1830
+ /* @__PURE__ */ jsx("option", { value: "", children: "Shape\u2026" }),
1831
+ Object.entries(SLIDE_SHAPES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1832
+ ]
1833
+ }
1834
+ ),
1835
+ /* @__PURE__ */ jsxs(
1836
+ "select",
1837
+ {
1838
+ className: "cv-html-tpl",
1839
+ value: "",
1840
+ title: "Slide font",
1841
+ onChange: (e) => {
1842
+ const f = SLIDE_FONTS[e.target.value];
1843
+ if (f) setSlideStyle({ fontFamily: f.stack });
1844
+ e.currentTarget.value = "";
1845
+ },
1846
+ children: [
1847
+ /* @__PURE__ */ jsx("option", { value: "", children: "Font\u2026" }),
1848
+ Object.entries(SLIDE_FONTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1849
+ ]
1850
+ }
1851
+ ),
1852
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", title: "Slide background image", onClick: () => bgFileRef.current?.click(), children: "\u{1F5BC} BG" })
1853
+ ] }),
1660
1854
  selected.length >= 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
1661
1855
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" }),
1662
1856
  /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Selection" }),
@@ -1684,6 +1878,12 @@ function HtmlRenderer({ artifact: artifact2 }) {
1684
1878
  }
1685
1879
  )
1686
1880
  ] }),
1881
+ single && single.tag !== "img" && /* @__PURE__ */ jsxs(Fragment, { children: [
1882
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align left", onClick: () => command("style_persist", { prop: "textAlign", value: "left" }), children: "\u2B05" }),
1883
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align center", onClick: () => command("style_persist", { prop: "textAlign", value: "center" }), children: "\u2B0C" }),
1884
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Align right", onClick: () => command("style_persist", { prop: "textAlign", value: "right" }), children: "\u27A1" }),
1885
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Bold", onClick: () => command("style_persist", { prop: "fontWeight", value: "800" }), children: "B" })
1886
+ ] }),
1687
1887
  single && /* @__PURE__ */ jsxs(Fragment, { children: [
1688
1888
  /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Duplicate", onClick: () => command("duplicate"), children: "\u29C9" }),
1689
1889
  /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move up", onClick: () => command("move_up"), children: "\u2191" }),
@@ -1738,10 +1938,10 @@ function HtmlRenderer({ artifact: artifact2 }) {
1738
1938
  }
1739
1939
 
1740
1940
  // src/components/renderers/index.ts
1741
- var ChartRenderer = lazy(() => import('./ChartRenderer-CKCJ2LQX.js').then((m) => ({ default: m.ChartRenderer })));
1742
- var DocumentRenderer = lazy(() => import('./DocumentRenderer-7HP3YQUK.js').then((m) => ({ default: m.DocumentRenderer })));
1743
- var TableRenderer = lazy(() => import('./TableRenderer-TRYSMQA7.js').then((m) => ({ default: m.TableRenderer })));
1744
- var SlidesRenderer = lazy(() => import('./SlidesRenderer-KFSPD63I.js').then((m) => ({ default: m.SlidesRenderer })));
1941
+ var ChartRenderer = lazy(() => import('./ChartRenderer-4MWKP2J4.js').then((m) => ({ default: m.ChartRenderer })));
1942
+ var DocumentRenderer = lazy(() => import('./DocumentRenderer-LJGP6N7E.js').then((m) => ({ default: m.DocumentRenderer })));
1943
+ var TableRenderer = lazy(() => import('./TableRenderer-ZF7MVA7I.js').then((m) => ({ default: m.TableRenderer })));
1944
+ var SlidesRenderer = lazy(() => import('./SlidesRenderer-7ICJQHYT.js').then((m) => ({ default: m.SlidesRenderer })));
1745
1945
  var builtinRenderers = {
1746
1946
  html: HtmlRenderer,
1747
1947
  document: DocumentRenderer,
@@ -1880,13 +2080,20 @@ async function slidesToPptx(data, _title) {
1880
2080
  const H = 5.625;
1881
2081
  for (const slide of data.slides) {
1882
2082
  const s = pptx.addSlide();
1883
- if (slide.background) s.background = { color: slide.background.replace("#", "") };
2083
+ if (slide.background && /^#[0-9a-f]{3,8}$/i.test(slide.background)) s.background = { color: slide.background.replace("#", "") };
1884
2084
  const tc = slide.textColor ? slide.textColor.replace("#", "") : void 0;
1885
2085
  for (const el of resolveElements(slide)) {
1886
2086
  const box = { x: el.x / 100 * W, y: el.y / 100 * H, w: el.w / 100 * W, h: el.h / 100 * H };
1887
2087
  if (el.type === "text") {
1888
2088
  const color = el.color ? el.color.replace("#", "") : tc;
1889
2089
  s.addText(el.text ?? "", { ...box, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
2090
+ } else if (el.type === "shape") {
2091
+ const fill = (el.fill ?? "#5b5bd6").replace("#", "");
2092
+ if (el.shape === "line") {
2093
+ s.addShape(pptx.ShapeType.line, { ...box, line: { color: fill, width: 2 } });
2094
+ } else {
2095
+ s.addShape(el.shape === "ellipse" ? pptx.ShapeType.ellipse : pptx.ShapeType.rect, { ...box, fill: { color: fill } });
2096
+ }
1890
2097
  } else if (el.src) {
1891
2098
  s.addImage({ data: el.src, ...box, sizing: { type: "contain", w: box.w, h: box.h } });
1892
2099
  }
@@ -1903,9 +2110,14 @@ function slidesToPrintHtml(data, title) {
1903
2110
  const els = resolveElements(slide).map((el) => {
1904
2111
  const box = `left:${el.x}%;top:${el.y}%;width:${el.w}%;height:${el.h}%`;
1905
2112
  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")}`;
2113
+ 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
2114
  return `<div class="el" style="${style}">${escapeXml(el.text ?? "")}</div>`;
1908
2115
  }
2116
+ if (el.type === "shape") {
2117
+ const fill = escapeAttr(el.fill ?? fg);
2118
+ const radius = el.shape === "ellipse" ? "50%" : el.shape === "line" ? "2px" : "8px";
2119
+ return `<div class="el" style="${box};background:${fill};border-radius:${radius}"></div>`;
2120
+ }
1909
2121
  const src = safeSrc(el.src);
1910
2122
  return src ? `<img class="el" style="${box}" src="${escapeAttr(src)}"/>` : "";
1911
2123
  }).join("");
@@ -1920,6 +2132,13 @@ function slidesToPrintHtml(data, title) {
1920
2132
  img.el { object-fit: contain; }
1921
2133
  </style></head><body>${pages}</body></html>`;
1922
2134
  }
2135
+ function htmlSlideToPrintHtml(html, ratio) {
2136
+ const w = ratio === "4:3" ? 960 : 1280;
2137
+ const h = 720;
2138
+ 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>`;
2139
+ const i = html.toLowerCase().lastIndexOf("</head>");
2140
+ return i === -1 ? style + html : html.slice(0, i) + style + html.slice(i);
2141
+ }
1923
2142
  function escapeXml(value) {
1924
2143
  return String(value ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
1925
2144
  }
@@ -1983,7 +2202,9 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
1983
2202
  const dataOptions = dataExporters[artifact2.type] ?? [];
1984
2203
  const exportHtml = () => {
1985
2204
  if (artifact2.type === "html") {
1986
- downloadBlob(`${stem}.html`, "text/html", artifact2.data.html);
2205
+ const ratio = artifact2.meta?.ratio;
2206
+ const html = artifact2.data.html;
2207
+ downloadBlob(`${stem}.html`, "text/html", ratio ? htmlSlideToPrintHtml(html, ratio) : html);
1987
2208
  } else {
1988
2209
  const html = getRenderedHtml();
1989
2210
  if (html == null) return;
@@ -2000,7 +2221,9 @@ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
2000
2221
  if (artifact2.type === "slides") {
2001
2222
  printToPdf(slidesToPrintHtml(artifact2.data, artifact2.title));
2002
2223
  } else if (artifact2.type === "html") {
2003
- printToPdf(artifact2.data.html);
2224
+ const ratio = artifact2.meta?.ratio;
2225
+ const html = artifact2.data.html;
2226
+ printToPdf(ratio ? htmlSlideToPrintHtml(html, ratio) : html);
2004
2227
  } else {
2005
2228
  const html = getRenderedHtml();
2006
2229
  if (html == null) return;
@@ -2362,17 +2585,22 @@ function useCanvasImport() {
2362
2585
  return { importFiles, canImport };
2363
2586
  }
2364
2587
  var ACCEPT = IMPORTABLE_EXTENSIONS.join(",");
2365
- function Canvas({ registry = builtinRenderers, emptyState, onEditElement }) {
2366
- return /* @__PURE__ */ jsx(CanvasRegistryProvider, { registry, children: /* @__PURE__ */ jsx(CanvasPanel, { emptyState, onEditElement }) });
2588
+ function Canvas({ registry = builtinRenderers, emptyState, onEditElement, onUserEdit }) {
2589
+ return /* @__PURE__ */ jsx(CanvasRegistryProvider, { registry, children: /* @__PURE__ */ jsx(CanvasPanel, { emptyState, onEditElement, onUserEdit }) });
2367
2590
  }
2368
- function CanvasPanel({ emptyState, onEditElement }) {
2591
+ function CanvasPanel({ emptyState, onEditElement, onUserEdit }) {
2369
2592
  const { artifacts, order, activeId } = useCanvasStore((s) => s.canvas);
2370
2593
  const history = useCanvasStore((s) => s.canvas.history);
2371
2594
  const setActive = useCanvasStore((s) => s.setActiveArtifact);
2372
2595
  const selections = useCanvasStore((s) => s.selections);
2373
2596
  const setSelections = useCanvasStore((s) => s.setSelections);
2597
+ const setOnUserEdit = useCanvasStore((s) => s.setOnUserEdit);
2374
2598
  const { importFiles } = useCanvasImport();
2375
2599
  const [dropping, setDropping] = useState(false);
2600
+ useEffect(() => {
2601
+ setOnUserEdit(onUserEdit ?? null);
2602
+ return () => setOnUserEdit(null);
2603
+ }, [onUserEdit, setOnUserEdit]);
2376
2604
  useEffect(() => {
2377
2605
  if (!selections.length) return;
2378
2606
  const onKey = (e) => {
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.7",
3
+ "version": "0.1.9",
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",