@braincrew-lab/langchain-canvas 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2570 @@
1
+ "use client";
2
+ import { loadOptional } from './chunk-YZZSJJMQ.js';
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';
7
+ import { createContext, lazy, useRef, useCallback, useEffect, useContext, useState, useMemo, Suspense, Component } from 'react';
8
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
+
10
+ // src/client/sse-client.ts
11
+ async function* streamChat(endpoint, request, options = {}) {
12
+ const response = await fetch(endpoint, {
13
+ method: "POST",
14
+ headers: { "content-type": "application/json", ...options.headers },
15
+ body: JSON.stringify({
16
+ thread_id: request.threadId,
17
+ message: request.message,
18
+ selections: request.selections ?? []
19
+ }),
20
+ signal: options.signal
21
+ });
22
+ if (!response.ok || !response.body) {
23
+ throw new Error(`chat stream failed: ${response.status} ${response.statusText}`);
24
+ }
25
+ yield* parseSSE(response.body, options.signal);
26
+ }
27
+ async function* parseSSE(body, signal) {
28
+ const reader = body.getReader();
29
+ const decoder = new TextDecoder();
30
+ let buffer = "";
31
+ try {
32
+ while (true) {
33
+ if (signal?.aborted) break;
34
+ const { done, value } = await reader.read();
35
+ if (done) break;
36
+ buffer += decoder.decode(value, { stream: true });
37
+ let boundary;
38
+ while ((boundary = buffer.indexOf("\n\n")) !== -1) {
39
+ const frame = buffer.slice(0, boundary);
40
+ buffer = buffer.slice(boundary + 2);
41
+ const event = parseFrame(frame);
42
+ if (event) {
43
+ yield event;
44
+ if (event.type === "done") return;
45
+ }
46
+ }
47
+ }
48
+ } finally {
49
+ reader.releaseLock();
50
+ }
51
+ }
52
+ function parseFrame(frame) {
53
+ const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
54
+ if (!data) return null;
55
+ try {
56
+ return JSON.parse(data);
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // src/client/inspector.ts
63
+ var INSPECTOR_MARK = "langchain-canvas";
64
+ function withInspector(html) {
65
+ let out = withViewport(html);
66
+ const injection = `<style data-lcx>${INSPECTOR_CSS}</style><script data-lcx>${INSPECTOR_SCRIPT}</script>`;
67
+ const marker = "</body>";
68
+ const at = out.lastIndexOf(marker);
69
+ out = at === -1 ? out + injection : out.slice(0, at) + injection + out.slice(at);
70
+ return out;
71
+ }
72
+ function withViewport(html) {
73
+ if (/name=["']?viewport/i.test(html)) return html;
74
+ const meta = '<meta name="viewport" content="width=device-width, initial-scale=1">';
75
+ const head = html.match(/<head[^>]*>/i);
76
+ if (head) return html.replace(head[0], head[0] + meta);
77
+ const htmlTag = html.match(/<html[^>]*>/i);
78
+ if (htmlTag) return html.replace(htmlTag[0], htmlTag[0] + "<head>" + meta + "</head>");
79
+ return meta + html;
80
+ }
81
+ var INSPECTOR_CSS = `
82
+ [data-cid].lcx-hover { outline: 2px solid #6366f1 !important; outline-offset: 1px; cursor: pointer; }
83
+ [data-cid].lcx-selected { outline: 2px solid #10b981 !important; outline-offset: 1px; cursor: move; }
84
+ [data-group-id] { outline: 1px dashed rgba(99,102,241,0.45); outline-offset: 2px; }
85
+ .lcx-marquee { position: fixed; border: 1.5px solid #6366f1; background: rgba(99,102,241,0.12); z-index: 999999; pointer-events: none; }
86
+ .lcx-fmt { position: fixed; z-index: 1000000; display: flex; gap: 2px; padding: 4px; border-radius: 8px;
87
+ background: #1f2328; box-shadow: 0 6px 20px rgba(0,0,0,0.25); font-family: -apple-system, system-ui, sans-serif; }
88
+ .lcx-fmt button { min-width: 28px; height: 28px; padding: 0 6px; border: 0; border-radius: 6px; background: transparent;
89
+ color: #fff; font-size: 14px; cursor: pointer; }
90
+ .lcx-fmt button:hover { background: rgba(255,255,255,0.15); }
91
+ .lcx-fmt button b { font-weight: 800; } .lcx-fmt button i { font-style: italic; } .lcx-fmt button u { text-decoration: underline; }
92
+ .lcx-resize { position: fixed; width: 14px; height: 14px; z-index: 1000001; background: #10b981;
93
+ border: 2px solid #fff; border-radius: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.3); cursor: nwse-resize; touch-action: none; }
94
+ `.trim();
95
+ var STYLE_PROPS = [
96
+ "color",
97
+ "backgroundColor",
98
+ "fontSize",
99
+ "fontWeight",
100
+ "textAlign",
101
+ "lineHeight",
102
+ "letterSpacing",
103
+ "padding",
104
+ "borderRadius",
105
+ "width"
106
+ ];
107
+ var INSPECTOR_SCRIPT = `
108
+ (function () {
109
+ var MARK = ${JSON.stringify(INSPECTOR_MARK)};
110
+ var STYLE_PROPS = ${JSON.stringify(STYLE_PROPS)};
111
+ function assign(el, path) {
112
+ if (el.hasAttribute && el.hasAttribute("data-lcx")) return; // skip injected style/script
113
+ el.setAttribute("data-cid", path);
114
+ for (var i = 0; i < el.children.length; i++) assign(el.children[i], path + "-" + i);
115
+ }
116
+ function byCid(cid) { return document.querySelector('[data-cid="' + cid + '"]'); }
117
+ function isEditable(el) { return el && el.hasAttribute && !el.hasAttribute("data-lcx") && !el.closest("[data-lcx]"); }
118
+ function selectorFor(el) {
119
+ var tag = el.tagName.toLowerCase();
120
+ var classes = typeof el.className === "string" ? el.className.trim().split(/\\s+/) : [];
121
+ var real = classes.filter(function (c) { return c && c.indexOf("lcx-") !== 0; }); // skip inspector classes
122
+ return tag + (real.length ? "." + real[0] : "");
123
+ }
124
+ function stylesOf(el) {
125
+ var cs = getComputedStyle(el), out = {};
126
+ for (var i = 0; i < STYLE_PROPS.length; i++) out[STYLE_PROPS[i]] = cs[STYLE_PROPS[i]];
127
+ return out;
128
+ }
129
+ function scrub(el) {
130
+ el.removeAttribute("data-cid");
131
+ el.removeAttribute("contenteditable");
132
+ if (el.classList) { el.classList.remove("lcx-hover"); el.classList.remove("lcx-selected"); }
133
+ if (el.getAttribute && el.getAttribute("class") === "") el.removeAttribute("class");
134
+ }
135
+ function emitEdit(el) {
136
+ // Serialize the *canonical* HTML \u2014 strip the inspector's own injected
137
+ // attributes/classes so they never persist into the stored source or exports.
138
+ var cid = el.getAttribute("data-cid");
139
+ var clone = el.cloneNode(true);
140
+ scrub(clone);
141
+ var inner = clone.querySelectorAll ? clone.querySelectorAll("[data-cid],[contenteditable],.lcx-hover,.lcx-selected") : [];
142
+ for (var i = 0; i < inner.length; i++) scrub(inner[i]);
143
+ parent.postMessage({ source: MARK, type: "node_edit", cid: cid, html: clone.outerHTML }, "*");
144
+ }
145
+ // A structural change (insert/delete/move) shifts every cid, so we save the
146
+ // whole document: clone <html>, drop the injected inspector nodes, scrub the
147
+ // inspector's attributes, and post it for the host to store wholesale.
148
+ // selfApplied = the iframe already reflects this change and every element kept
149
+ // its data-cid (a pure reorder/move), so the host can persist without reloading
150
+ // the iframe (no flicker). Edits that mint new nodes (insert) leave it false so a
151
+ // reload re-tags fresh cids.
152
+ function emitDoc(selfApplied) {
153
+ var clone = document.documentElement.cloneNode(true);
154
+ var injected = clone.querySelectorAll("[data-lcx]");
155
+ for (var i = 0; i < injected.length; i++) injected[i].parentNode && injected[i].parentNode.removeChild(injected[i]);
156
+ var marked = clone.querySelectorAll("[data-cid],[contenteditable],.lcx-hover,.lcx-selected");
157
+ for (var j = 0; j < marked.length; j++) scrub(marked[j]);
158
+ parent.postMessage({ source: MARK, type: "doc_edit", self: !!selfApplied, html: "<!doctype html>\\n" + clone.outerHTML }, "*");
159
+ }
160
+ function newBlock(tag) {
161
+ var el = document.createElement(tag);
162
+ if (tag === "img") {
163
+ el.src = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='270'%3E%3Crect width='100%25' height='100%25' fill='%23e5e7eb'/%3E%3Cpath d='M190 155l40-45 35 40 25-25 40 45z' fill='%23c3c8d0'/%3E%3Ccircle cx='300' cy='105' r='16' fill='%23c3c8d0'/%3E%3C/svg%3E";
164
+ el.alt = "image"; el.style.maxWidth = "100%";
165
+ }
166
+ else if (tag === "button") el.textContent = "Button";
167
+ else if (tag === "hr") { /* no content */ }
168
+ else if (tag === "section") { var p = document.createElement("p"); p.textContent = "New section"; el.appendChild(p); }
169
+ else el.textContent = tag === "h1" || tag === "h2" ? "New heading" : "New text";
170
+ return el;
171
+ }
172
+ // Floating rich-text toolbar shown while a text element is being edited.
173
+ var fmtBar = null;
174
+ function removeFmtBar() { if (fmtBar && fmtBar.parentNode) fmtBar.parentNode.removeChild(fmtBar); fmtBar = null; }
175
+ function showFmtBar(el) {
176
+ removeFmtBar();
177
+ var r = el.getBoundingClientRect();
178
+ fmtBar = document.createElement("div");
179
+ fmtBar.setAttribute("data-lcx", "");
180
+ fmtBar.className = "lcx-fmt";
181
+ fmtBar.style.left = Math.max(4, r.left) + "px";
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"]];
184
+ for (var i = 0; i < specs.length; i++) {
185
+ (function (spec) {
186
+ var btn = document.createElement("button");
187
+ btn.innerHTML = spec[0];
188
+ btn.title = spec[1];
189
+ // mousedown+preventDefault keeps the contenteditable focused and its selection intact.
190
+ btn.addEventListener("mousedown", function (ev) {
191
+ ev.preventDefault();
192
+ if (spec[1] === "createLink") {
193
+ var url = prompt("Link URL", "https://");
194
+ if (url) document.execCommand("createLink", false, url);
195
+ } else {
196
+ document.execCommand(spec[1], false, null);
197
+ }
198
+ });
199
+ fmtBar.appendChild(btn);
200
+ })(specs[i]);
201
+ }
202
+ document.body.appendChild(fmtBar);
203
+ }
204
+ // Drag-to-resize handle for the selected element (images keep their aspect).
205
+ var resizeEl = null, resizeHandle = null, resizing = null;
206
+ function positionResize() {
207
+ if (!resizeEl || !resizeHandle) return;
208
+ var r = resizeEl.getBoundingClientRect();
209
+ // Anchor to the element's bottom-right, but keep the whole handle on-screen so
210
+ // a section taller/wider than the viewport still exposes a reachable grip
211
+ // (otherwise it sits below the fold and "there's no way to adjust it").
212
+ var M = 8;
213
+ var left = Math.max(r.left - 7, Math.min(r.right - 7, window.innerWidth - M - 14));
214
+ var top = Math.max(r.top - 7, Math.min(r.bottom - 7, window.innerHeight - M - 14));
215
+ resizeHandle.style.left = left + "px";
216
+ resizeHandle.style.top = top + "px";
217
+ }
218
+ function hideResize() { resizeEl = null; if (resizeHandle) resizeHandle.style.display = "none"; }
219
+ function showResize(el) {
220
+ resizeEl = el;
221
+ if (!resizeHandle) {
222
+ resizeHandle = document.createElement("div");
223
+ resizeHandle.setAttribute("data-lcx", "");
224
+ resizeHandle.className = "lcx-resize";
225
+ resizeHandle.addEventListener("pointerdown", function (e) {
226
+ if (!resizeEl) return;
227
+ e.preventDefault(); e.stopPropagation();
228
+ var r = resizeEl.getBoundingClientRect();
229
+ resizing = { sx: e.clientX, sy: e.clientY, w: r.width, h: r.height, img: resizeEl.tagName === "IMG" };
230
+ resizeHandle.setPointerCapture(e.pointerId);
231
+ });
232
+ resizeHandle.addEventListener("pointermove", function (e) {
233
+ if (!resizing || !resizeEl) return;
234
+ var w = Math.max(20, Math.round(resizing.w + (e.clientX - resizing.sx)));
235
+ if (resizing.img) {
236
+ // Store image width as a % of its container so it scales with the layout
237
+ // (responsive): shrinks on mobile, grows on desktop. Aspect ratio kept.
238
+ var parentW = resizeEl.parentElement ? resizeEl.parentElement.getBoundingClientRect().width : window.innerWidth;
239
+ resizeEl.style.width = Math.max(5, Math.min(100, Math.round((w / parentW) * 100))) + "%";
240
+ resizeEl.style.height = "auto";
241
+ } else {
242
+ resizeEl.style.width = w + "px";
243
+ resizeEl.style.height = Math.max(20, Math.round(resizing.h + (e.clientY - resizing.sy))) + "px";
244
+ }
245
+ positionResize();
246
+ });
247
+ resizeHandle.addEventListener("pointerup", function () {
248
+ if (!resizing) return;
249
+ resizing = null;
250
+ emitEdit(resizeEl); // commit the new size as a node edit
251
+ });
252
+ document.body.appendChild(resizeHandle);
253
+ }
254
+ resizeHandle.style.display = "block";
255
+ positionResize();
256
+ }
257
+ function start() {
258
+ assign(document.body, "e");
259
+ var hovered = null;
260
+ var selected = []; // currently highlighted elements
261
+ var marquee = null, sx = 0, sy = 0, dragging = false, moved = false, suppressClick = false;
262
+ // Free positioning: dragging places an element anywhere. The element is pulled
263
+ // into absolute positioning inside its own parent, and its final spot + size are
264
+ // stored as percentages of that parent \u2014 so it stays put proportionally across
265
+ // responsive breakpoints, instead of a fixed pixel offset that drifts off.
266
+ var dragEls = null, dragStart = null, dragBases = null, groupSeq = 0;
267
+
268
+ function ensurePositioned(parent) {
269
+ if (!parent || parent === document.body || parent === document.documentElement) return;
270
+ if (window.getComputedStyle(parent).position === "static") parent.style.position = "relative";
271
+ }
272
+ // Pull each element out into absolute positioning at its current spot (no visual
273
+ // jump), so it can then be moved freely.
274
+ function beginFreeDrag(els) {
275
+ dragBases = [];
276
+ for (var i = 0; i < els.length; i++) {
277
+ var el = els[i], parent = el.parentElement || document.body;
278
+ ensurePositioned(parent);
279
+ var pr = parent.getBoundingClientRect(), r = el.getBoundingClientRect();
280
+ var base = { el: el, parent: parent, left: r.left - pr.left, top: r.top - pr.top, w: el.offsetWidth, h: el.offsetHeight };
281
+ el.style.position = "absolute";
282
+ el.style.margin = "0";
283
+ el.style.width = base.w + "px"; // freeze size while dragging (absolute can't inherit flow width)
284
+ el.style.left = base.left + "px";
285
+ el.style.top = base.top + "px";
286
+ dragBases.push(base);
287
+ }
288
+ }
289
+ function moveFree(dx, dy) {
290
+ for (var i = 0; i < dragBases.length; i++) {
291
+ var b = dragBases[i];
292
+ b.el.style.left = (b.left + dx) + "px";
293
+ b.el.style.top = (b.top + dy) + "px";
294
+ }
295
+ }
296
+ // Commit the current position (in px, as set live by moveFree) as % of the
297
+ // parent \u2014 position and width \u2014 so it scales with the layout. Falls back to px
298
+ // only if the parent has collapsed to zero on that axis.
299
+ function commitFree() {
300
+ for (var i = 0; i < dragBases.length; i++) {
301
+ var b = dragBases[i], pr = b.parent.getBoundingClientRect();
302
+ var curLeft = parseFloat(b.el.style.left) || 0, curTop = parseFloat(b.el.style.top) || 0;
303
+ b.el.style.left = pr.width ? ((curLeft / pr.width) * 100).toFixed(3) + "%" : curLeft + "px";
304
+ b.el.style.top = pr.height ? ((curTop / pr.height) * 100).toFixed(3) + "%" : curTop + "px";
305
+ if (pr.width) b.el.style.width = ((b.w / pr.width) * 100).toFixed(3) + "%";
306
+ }
307
+ }
308
+
309
+ function clearSelected() {
310
+ for (var i = 0; i < selected.length; i++) selected[i].classList.remove("lcx-selected");
311
+ selected = [];
312
+ hideResize();
313
+ }
314
+ window.addEventListener("scroll", positionResize, true);
315
+ function overlapRatio(r, box) {
316
+ var ix = Math.max(0, Math.min(r.right, box.right) - Math.max(r.left, box.left));
317
+ var iy = Math.max(0, Math.min(r.bottom, box.bottom) - Math.max(r.top, box.top));
318
+ var area = r.width * r.height;
319
+ return area > 0 ? (ix * iy) / area : 0;
320
+ }
321
+ function hasAncestorIn(el, list) {
322
+ var p = el.parentElement;
323
+ while (p) { if (list.indexOf(p) !== -1) return true; p = p.parentElement; }
324
+ return false;
325
+ }
326
+ function selectSummary(el) {
327
+ return { cid: el.getAttribute("data-cid"), selector: selectorFor(el), tag: el.tagName.toLowerCase(),
328
+ text: (el.textContent || "").trim().slice(0, 60) };
329
+ }
330
+
331
+ document.addEventListener("mouseover", function (e) {
332
+ var t = e.target;
333
+ if (!(t instanceof Element) || !isEditable(t)) return;
334
+ if (hovered) hovered.classList.remove("lcx-hover");
335
+ hovered = t; t.classList.add("lcx-hover");
336
+ }, true);
337
+ document.addEventListener("mouseout", function () {
338
+ if (hovered) { hovered.classList.remove("lcx-hover"); hovered = null; }
339
+ }, true);
340
+
341
+ // --- click = single select --------------------------------------------------
342
+ document.addEventListener("click", function (e) {
343
+ if (suppressClick) { suppressClick = false; return; }
344
+ var t = e.target;
345
+ if (!(t instanceof Element) || t.isContentEditable || !isEditable(t)) return;
346
+ e.preventDefault(); e.stopPropagation();
347
+ // Clicking the page background (body / near-full-page element) deselects.
348
+ var rr = t.getBoundingClientRect();
349
+ if (t === document.body || t === document.documentElement ||
350
+ rr.width * rr.height > window.innerWidth * window.innerHeight * 0.9) {
351
+ clearSelected();
352
+ parent.postMessage({ source: MARK, type: "multi_select", items: [] }, "*");
353
+ return;
354
+ }
355
+ if (e.shiftKey) {
356
+ // Shift-click toggles an element in a multi-selection.
357
+ var i = selected.indexOf(t);
358
+ if (i !== -1) { t.classList.remove("lcx-selected"); selected.splice(i, 1); }
359
+ else {
360
+ // Keep the selection flat \u2014 a nested pair can't be grouped, so drop any
361
+ // already-selected ancestor/descendant of the newly-clicked element.
362
+ for (var j = selected.length - 1; j >= 0; j--) {
363
+ if (selected[j].contains(t) || t.contains(selected[j])) {
364
+ selected[j].classList.remove("lcx-selected"); selected.splice(j, 1);
365
+ }
366
+ }
367
+ t.classList.add("lcx-selected"); selected.push(t);
368
+ }
369
+ parent.postMessage({ source: MARK, type: "multi_select", items: selected.map(selectSummary) }, "*");
370
+ return;
371
+ }
372
+ clearSelected();
373
+ var gid = t.getAttribute("data-group-id");
374
+ if (gid) {
375
+ // Clicking a grouped element highlights the whole group; it moves together
376
+ // and offers Ungroup. (Double-click still edits the deep text element.)
377
+ var mates = document.querySelectorAll('[data-group-id="' + gid + '"]');
378
+ for (var mi = 0; mi < mates.length; mi++) { mates[mi].classList.add("lcx-selected"); selected.push(mates[mi]); }
379
+ } else {
380
+ t.classList.add("lcx-selected"); selected = [t];
381
+ }
382
+ showResize(t);
383
+ parent.postMessage({
384
+ source: MARK, type: "select",
385
+ cid: t.getAttribute("data-cid"),
386
+ selector: selectorFor(t),
387
+ tag: t.tagName.toLowerCase(),
388
+ text: (t.textContent || "").trim().slice(0, 80),
389
+ outerHtml: t.outerHTML.slice(0, 4000),
390
+ styles: stylesOf(t),
391
+ isGroup: !!gid
392
+ }, "*");
393
+ }, true);
394
+
395
+ // --- drag: freely move an element anywhere, or marquee-select empty space ----
396
+ document.addEventListener("mousedown", function (e) {
397
+ suppressClick = false; // a fresh press: never carry a stale suppress
398
+ if (e.button !== 0) return;
399
+ var t = e.target;
400
+ if (t instanceof Element && t.isContentEditable) return;
401
+ if (t === resizeHandle) return; // the resize handle drives its own drag
402
+ // Pressing on a selected element drags the whole selection; pressing on any
403
+ // other element drags just that one (a single gesture both selects and moves,
404
+ // so you never have to click first). Empty space starts a marquee.
405
+ var onSel = false;
406
+ for (var si = 0; si < selected.length; si++) { if (selected[si] === t || selected[si].contains(t)) { onSel = true; break; } }
407
+ if (onSel && selected.length >= 1) {
408
+ dragEls = selected.slice();
409
+ } else {
410
+ var el = t instanceof Element ? t.closest("[data-cid]") : null;
411
+ if (el && el !== document.body && el.parentElement) dragEls = [el];
412
+ }
413
+ if (dragEls) {
414
+ dragStart = { x: e.clientX, y: e.clientY }; moved = false;
415
+ document.body.style.userSelect = "none";
416
+ return;
417
+ }
418
+ dragging = true; moved = false; sx = e.clientX; sy = e.clientY;
419
+ document.body.style.userSelect = "none";
420
+ marquee = document.createElement("div");
421
+ marquee.className = "lcx-marquee";
422
+ document.body.appendChild(marquee);
423
+ }, true);
424
+ document.addEventListener("mousemove", function (e) {
425
+ if (dragEls) {
426
+ if (!moved && (Math.abs(e.clientX - dragStart.x) > 3 || Math.abs(e.clientY - dragStart.y) > 3)) {
427
+ moved = true;
428
+ beginFreeDrag(dragEls); // pull into absolute positioning at the current spot
429
+ }
430
+ if (!moved) return;
431
+ moveFree(e.clientX - dragStart.x, e.clientY - dragStart.y);
432
+ positionResize();
433
+ return;
434
+ }
435
+ if (!dragging || !marquee) return;
436
+ if (Math.abs(e.clientX - sx) > 4 || Math.abs(e.clientY - sy) > 4) moved = true;
437
+ var l = Math.min(sx, e.clientX), tp = Math.min(sy, e.clientY);
438
+ marquee.style.left = l + "px"; marquee.style.top = tp + "px";
439
+ marquee.style.width = Math.abs(e.clientX - sx) + "px";
440
+ marquee.style.height = Math.abs(e.clientY - sy) + "px";
441
+ }, true);
442
+ document.addEventListener("mouseup", function (e) {
443
+ if (dragEls) {
444
+ var els = dragEls; dragEls = null;
445
+ document.body.style.userSelect = "";
446
+ if (moved && dragBases) {
447
+ suppressClick = true;
448
+ moveFree(e.clientX - dragStart.x, e.clientY - dragStart.y); // final px position
449
+ commitFree(); // px \u2192 % of parent
450
+ positionResize();
451
+ // The new position is already shown in the iframe with every cid intact,
452
+ // so persist without a reload (no flicker): one element \u2192 node_edit,
453
+ // several \u2192 a self-applied doc_edit.
454
+ if (els.length === 1) emitEdit(els[0]); else emitDoc(true);
455
+ }
456
+ dragBases = null;
457
+ return;
458
+ }
459
+ if (!dragging) return;
460
+ dragging = false;
461
+ document.body.style.userSelect = "";
462
+ var box = marquee ? marquee.getBoundingClientRect() : null;
463
+ if (marquee && marquee.parentNode) marquee.parentNode.removeChild(marquee);
464
+ marquee = null;
465
+ if (!moved || !box) return; // a click, not a drag
466
+ suppressClick = true;
467
+ // Predictable rubber-band: select the outermost elements *fully enclosed*
468
+ // by the box (standard marquee behavior \u2014 no partial-overlap guessing).
469
+ var vpArea = window.innerWidth * window.innerHeight;
470
+ var all = document.body.querySelectorAll("[data-cid]");
471
+ var hits = [];
472
+ all.forEach(function (el) {
473
+ if (el.hasAttribute("data-lcx")) return;
474
+ var r = el.getBoundingClientRect();
475
+ if (r.width <= 0 || r.height <= 0) return;
476
+ if (r.width * r.height > vpArea * 0.9) return; // never select the page/body wrapper
477
+ if (r.left >= box.left - 1 && r.top >= box.top - 1 && r.right <= box.right + 1 && r.bottom <= box.bottom + 1) hits.push(el);
478
+ });
479
+ var top = hits.filter(function (el) { return !hasAncestorIn(el, hits); }); // outermost only
480
+ // If the enclosure is a single wrapper of several items, select the items
481
+ // (so dragging a box around 3 cards selects the cards, not their container).
482
+ var guard = 0;
483
+ while (top.length === 1 && guard++ < 4) {
484
+ var kids = [];
485
+ for (var k = 0; k < top[0].children.length; k++) {
486
+ if (hits.indexOf(top[0].children[k]) !== -1) kids.push(top[0].children[k]);
487
+ }
488
+ if (kids.length >= 2) top = kids; else break;
489
+ }
490
+ if (!e.shiftKey) clearSelected();
491
+ top.forEach(function (el) {
492
+ if (selected.indexOf(el) === -1) { el.classList.add("lcx-selected"); selected.push(el); }
493
+ });
494
+ parent.postMessage({ source: MARK, type: "multi_select", items: selected.map(selectSummary) }, "*");
495
+ }, true);
496
+
497
+ // Safety net: if the pointer leaves the frame mid-drag the mouseup can be lost,
498
+ // which would leave move/marquee state stuck. Finalize cleanly on exit.
499
+ function endDrag() {
500
+ document.body.style.userSelect = "";
501
+ if (dragEls) {
502
+ // Pointer left the frame mid-drag: commit the move at its last position so
503
+ // it isn't lost (the element is already placed absolutely in the iframe).
504
+ var els = dragEls; dragEls = null;
505
+ if (moved && dragBases) { commitFree(); if (els.length === 1) emitEdit(els[0]); else emitDoc(true); }
506
+ dragBases = null;
507
+ }
508
+ if (dragging) {
509
+ dragging = false;
510
+ if (marquee && marquee.parentNode) marquee.parentNode.removeChild(marquee);
511
+ marquee = null;
512
+ }
513
+ }
514
+ document.addEventListener("mouseleave", endDrag);
515
+ window.addEventListener("blur", endDrag);
516
+
517
+ // --- double-click = edit text inline; commit on blur ------------------------
518
+ document.addEventListener("dblclick", function (e) {
519
+ var t = e.target;
520
+ if (!(t instanceof Element) || !isEditable(t)) return;
521
+ e.preventDefault();
522
+ t.setAttribute("contenteditable", "true");
523
+ t.focus();
524
+ showFmtBar(t);
525
+ }, true);
526
+ document.addEventListener("blur", function (e) {
527
+ var t = e.target;
528
+ if (!(t instanceof Element) || !t.hasAttribute("contenteditable")) return;
529
+ removeFmtBar();
530
+ t.removeAttribute("contenteditable");
531
+ emitEdit(t);
532
+ }, true);
533
+ // While editing text, Enter inserts a real line break. The browser's default
534
+ // for Enter is to split the block (which nests <div>s, or does nothing at all
535
+ // on inline/button-like elements), so line breaks appear not to work \u2014 force a
536
+ // clean <br> instead. Escape commits by blurring.
537
+ document.addEventListener("keydown", function (e) {
538
+ var t = e.target;
539
+ var editing = t instanceof Element && t.hasAttribute("contenteditable");
540
+ if (editing) {
541
+ if (e.key === "Enter") {
542
+ e.preventDefault();
543
+ if (!document.execCommand("insertLineBreak")) document.execCommand("insertHTML", false, "<br>");
544
+ } else if (e.key === "Escape") {
545
+ e.preventDefault();
546
+ t.blur();
547
+ }
548
+ return;
549
+ }
550
+ // Escape with a live selection clears it here and tells the host to close its
551
+ // selection UI (an empty multi-select).
552
+ if (e.key === "Escape" && selected.length) {
553
+ clearSelected();
554
+ parent.postMessage({ source: MARK, type: "multi_select", items: [] }, "*");
555
+ }
556
+ }, true);
557
+
558
+ // --- parent commands --------------------------------------------------------
559
+ window.addEventListener("message", function (e) {
560
+ var d = e.data;
561
+ if (!d || d.source !== MARK) return;
562
+ if (d.type === "clear") { clearSelected(); return; }
563
+ if (d.type === "set_style") { var el = byCid(d.cid); if (el) el.style[d.prop] = d.value; return; }
564
+ if (d.type === "set_src") { var ei = byCid(d.cid); if (ei) { ei.setAttribute("src", d.value); emitEdit(ei); } return; }
565
+ if (d.type === "commit") { var el2 = byCid(d.cid); if (el2) emitEdit(el2); return; }
566
+
567
+ // Structural edits \u2014 mutate the tree, then persist the whole document.
568
+ if (d.type === "insert") {
569
+ var block = newBlock(d.block || "p");
570
+ var anchor = d.cid ? byCid(d.cid) : null;
571
+ if (anchor && anchor.parentNode && anchor.parentNode !== document.documentElement) {
572
+ anchor.parentNode.insertBefore(block, anchor.nextSibling);
573
+ } else {
574
+ document.body.appendChild(block);
575
+ }
576
+ emitDoc();
577
+ return;
578
+ }
579
+ if (d.type === "insert_html") {
580
+ // A built-in section template (trusted markup from the toolbar).
581
+ var anc = d.cid ? byCid(d.cid) : null;
582
+ var container = (anc && anc.parentNode && anc.parentNode !== document.documentElement) ? anc.parentNode : document.body;
583
+ var ref = (anc && anc.parentNode === container) ? anc.nextSibling : null;
584
+ var frag = document.createElement("div");
585
+ frag.innerHTML = d.html || "";
586
+ while (frag.firstChild) container.insertBefore(frag.firstChild, ref);
587
+ emitDoc();
588
+ return;
589
+ }
590
+ if (d.type === "group") {
591
+ // Tag members with a shared id \u2014 no wrapper, so the page layout (grid /
592
+ // flex flow) is untouched; the members simply move together.
593
+ var members = [];
594
+ var cids = d.cids || [];
595
+ for (var g = 0; g < cids.length; g++) { var m = byCid(cids[g]); if (m) members.push(m); }
596
+ if (members.length < 2) return;
597
+ var gid = "g" + (groupSeq++);
598
+ for (var w = 0; w < members.length; w++) members[w].setAttribute("data-group-id", gid);
599
+ clearSelected();
600
+ emitDoc();
601
+ return;
602
+ }
603
+ if (d.type === "ungroup") {
604
+ var el0 = d.cid ? byCid(d.cid) : null;
605
+ if (!el0) return;
606
+ var gid2 = el0.getAttribute("data-group-id");
607
+ if (gid2) {
608
+ var mates = document.querySelectorAll('[data-group-id="' + gid2 + '"]');
609
+ for (var u = 0; u < mates.length; u++) mates[u].removeAttribute("data-group-id");
610
+ clearSelected();
611
+ emitDoc();
612
+ }
613
+ return;
614
+ }
615
+ var target = d.cid ? byCid(d.cid) : null;
616
+ if (!target || !target.parentNode) return;
617
+ if (d.type === "duplicate") {
618
+ var copy = target.cloneNode(true);
619
+ scrub(copy);
620
+ target.parentNode.insertBefore(copy, target.nextSibling);
621
+ emitDoc();
622
+ } else if (d.type === "delete") {
623
+ target.parentNode.removeChild(target);
624
+ clearSelected();
625
+ emitDoc();
626
+ } else if (d.type === "move_up") {
627
+ var prev = target.previousElementSibling;
628
+ while (prev && prev.hasAttribute("data-lcx")) prev = prev.previousElementSibling;
629
+ if (prev) { target.parentNode.insertBefore(target, prev); emitDoc(); }
630
+ } else if (d.type === "move_down") {
631
+ var next = target.nextElementSibling;
632
+ while (next && next.hasAttribute("data-lcx")) next = next.nextElementSibling;
633
+ if (next) { target.parentNode.insertBefore(next, target); emitDoc(); }
634
+ }
635
+ });
636
+ }
637
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", start);
638
+ else start();
639
+ })();
640
+ `.trim();
641
+
642
+ // src/client/mock.ts
643
+ async function* mockStream(events, options = {}) {
644
+ const { delayMs = 120, signal } = options;
645
+ for (const event of events) {
646
+ if (signal?.aborted) return;
647
+ yield event;
648
+ if (delayMs > 0) await sleep(delayMs);
649
+ }
650
+ }
651
+ function sleep(ms) {
652
+ return new Promise((resolve) => setTimeout(resolve, ms));
653
+ }
654
+ function useCanvasStream(options = {}) {
655
+ const endpoint = options.endpoint ?? "/api/chat";
656
+ const threadIdRef = useRef(options.threadId ?? crypto.randomUUID());
657
+ const abortRef = useRef(null);
658
+ const api = useCanvasStoreApi();
659
+ const queueRef = useRef([]);
660
+ const frameRef = useRef(null);
661
+ const flush = useCallback(() => {
662
+ frameRef.current = null;
663
+ if (queueRef.current.length === 0) return;
664
+ const batch = queueRef.current;
665
+ queueRef.current = [];
666
+ api.getState().applyEvents(batch);
667
+ }, [api]);
668
+ const enqueue = useCallback(
669
+ (event) => {
670
+ queueRef.current.push(event);
671
+ if (frameRef.current === null) {
672
+ frameRef.current = requestAnimationFrame(flush);
673
+ }
674
+ },
675
+ [flush]
676
+ );
677
+ useEffect(() => () => {
678
+ if (frameRef.current !== null) cancelAnimationFrame(frameRef.current);
679
+ }, []);
680
+ const messages = useCanvasStore((s) => s.messages);
681
+ const canvas = useCanvasStore((s) => s.canvas);
682
+ const isStreaming = useCanvasStore((s) => s.isStreaming);
683
+ const error = useCanvasStore((s) => s.error);
684
+ const selections = useCanvasStore((s) => s.selections);
685
+ const sendMessage = useCallback(
686
+ async (text, withSelections) => {
687
+ const store = api.getState();
688
+ if (store.isStreaming || !text.trim()) return;
689
+ store.addUserMessage(text);
690
+ store.setStreaming(true);
691
+ const controller = new AbortController();
692
+ abortRef.current = controller;
693
+ try {
694
+ const mockEvents = options.mock?.(text) ?? null;
695
+ const stream = mockEvents ? mockStream(mockEvents, { delayMs: 60, signal: controller.signal }) : streamChat(
696
+ endpoint,
697
+ { threadId: threadIdRef.current, message: text, selections: withSelections },
698
+ { signal: controller.signal }
699
+ );
700
+ for await (const event of stream) {
701
+ enqueue(event);
702
+ }
703
+ } catch (err) {
704
+ if (!controller.signal.aborted) {
705
+ enqueue({ type: "error", message: err instanceof Error ? err.message : "stream failed" });
706
+ }
707
+ } finally {
708
+ flush();
709
+ api.getState().setStreaming(false);
710
+ }
711
+ },
712
+ [api, endpoint, enqueue, flush, options.mock]
713
+ );
714
+ const stop = useCallback(() => abortRef.current?.abort(), []);
715
+ const reset = useCallback(() => api.getState().reset(), [api]);
716
+ const setActiveArtifact = useCallback((id) => api.getState().setActiveArtifact(id), [api]);
717
+ const clearSelection = useCallback(() => api.getState().setSelections([]), [api]);
718
+ const editSelection = useCallback(
719
+ (instruction) => {
720
+ const current = api.getState().selections;
721
+ if (current.length === 0) return;
722
+ void sendMessage(instruction, current);
723
+ api.getState().setSelections([]);
724
+ },
725
+ [api, sendMessage]
726
+ );
727
+ return {
728
+ sendMessage,
729
+ stop,
730
+ reset,
731
+ setActiveArtifact,
732
+ selections,
733
+ editSelection,
734
+ clearSelection,
735
+ messages,
736
+ canvas,
737
+ isStreaming,
738
+ error,
739
+ threadId: threadIdRef.current
740
+ };
741
+ }
742
+ function useCanvasReplay() {
743
+ const abortRef = useRef(null);
744
+ const api = useCanvasStoreApi();
745
+ const canvas = useCanvasStore((s) => s.canvas);
746
+ const isPlaying = useCanvasStore((s) => s.isStreaming);
747
+ const play = useCallback(
748
+ async (events, options = {}) => {
749
+ abortRef.current?.abort();
750
+ const controller = new AbortController();
751
+ abortRef.current = controller;
752
+ const store = api.getState();
753
+ store.reset();
754
+ store.setStreaming(true);
755
+ try {
756
+ for await (const event of mockStream(events, { ...options, signal: controller.signal })) {
757
+ api.getState().applyEvent(event);
758
+ }
759
+ } finally {
760
+ api.getState().setStreaming(false);
761
+ }
762
+ },
763
+ [api]
764
+ );
765
+ const stop = useCallback(() => abortRef.current?.abort(), []);
766
+ const reset = useCallback(() => api.getState().reset(), [api]);
767
+ return { play, stop, reset, canvas, isPlaying };
768
+ }
769
+
770
+ // src/fixtures/scenarios.ts
771
+ var PRICING_HTML = `<!doctype html>
772
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style>
773
+ body{font-family:system-ui;margin:0;background:#0b1020;color:#e6e8ef}
774
+ .wrap{max-width:820px;margin:48px auto;padding:0 20px;text-align:center}
775
+ h1{font-size:34px;margin:0 0 8px}
776
+ .sub{color:#9aa4b2;margin-bottom:32px}
777
+ .tiers{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}
778
+ .card{background:#151a2e;border:1px solid #232a44;border-radius:14px;padding:24px}
779
+ .price{font-size:28px;font-weight:700;margin:8px 0}
780
+ .cta{margin-top:16px;padding:10px 16px;border:0;border-radius:9px;background:#6366f1;color:#fff;font-weight:600;cursor:pointer}
781
+ @media (max-width:640px){
782
+ .wrap{margin:28px auto}
783
+ h1{font-size:26px}
784
+ .tiers{grid-template-columns:1fr}
785
+ }
786
+ </style></head>
787
+ <body><div class="wrap">
788
+ <h1>Simple, honest pricing</h1>
789
+ <p class="sub">Start free. Upgrade when you grow.</p>
790
+ <div class="tiers">
791
+ <div class="card"><div>Starter</div><div class="price">$0</div><button class="cta">Get started</button></div>
792
+ <div class="card"><div>Pro</div><div class="price">$20</div><button class="cta">Start trial</button></div>
793
+ <div class="card"><div>Enterprise</div><div class="price">Custom</div><button class="cta">Contact us</button></div>
794
+ </div>
795
+ </div></body></html>`;
796
+ var htmlPage = {
797
+ id: "html-page",
798
+ title: "HTML page + edit",
799
+ description: "An agent builds a pricing page, then surgically edits one heading via node_patch.",
800
+ events: [
801
+ { type: "message.delta", messageId: "m1", text: "Here's a pricing page \u2014 click any element to edit it." },
802
+ { type: "message.end", messageId: "m1" },
803
+ {
804
+ type: "canvas.create",
805
+ artifact: { id: "page", type: "html", title: "Pricing", version: 1, status: "streaming", data: { html: PRICING_HTML } }
806
+ },
807
+ { type: "canvas.status", id: "page", status: "complete" },
808
+ // A targeted edit: replace the <h1> (cid "e-0-0": wrap → child 0) in place.
809
+ { type: "canvas.node_patch", id: "page", cid: "e-0-0", html: '<h1 data-cid="e-0-0">Pricing that scales with you</h1>' },
810
+ { type: "done" }
811
+ ]
812
+ };
813
+ var REPORT_CHUNKS = [
814
+ "# EV market, 2026\n\n",
815
+ "The electric-vehicle market continued its shift ",
816
+ "from early adopters to the mainstream.\n\n",
817
+ "## Highlights\n\n",
818
+ "- Global BEV share crossed **20%** of new sales\n",
819
+ "- Battery pack prices fell below **$80/kWh**\n",
820
+ "- Charging networks doubled in dense metros\n\n",
821
+ "## Outlook\n\nExpect continued margin pressure as ",
822
+ "legacy OEMs scale volume."
823
+ ];
824
+ var document2 = {
825
+ id: "document",
826
+ title: "Streaming document",
827
+ description: "A markdown report streamed token-by-token via canvas.append.",
828
+ events: [
829
+ { type: "canvas.create", artifact: { id: "doc", type: "document", title: "EV market report", version: 1, status: "streaming", data: { format: "markdown", content: "" } } },
830
+ ...REPORT_CHUNKS.map((text) => ({ type: "canvas.append", id: "doc", path: "content", text })),
831
+ { type: "canvas.status", id: "doc", status: "complete" },
832
+ { type: "done" }
833
+ ]
834
+ };
835
+ var chart = {
836
+ id: "chart",
837
+ title: "Chart",
838
+ description: "A bar chart whose rows arrive via canvas.patch.",
839
+ events: [
840
+ {
841
+ type: "canvas.create",
842
+ artifact: {
843
+ id: "rev",
844
+ type: "chart",
845
+ title: "Quarterly revenue",
846
+ version: 1,
847
+ status: "streaming",
848
+ data: { chart: "bar", xKey: "quarter", series: [{ key: "amount", label: "Revenue ($M)" }], rows: [] }
849
+ }
850
+ },
851
+ {
852
+ type: "canvas.patch",
853
+ id: "rev",
854
+ patch: {
855
+ rows: [
856
+ { quarter: "Q1", amount: 12 },
857
+ { quarter: "Q2", amount: 18 },
858
+ { quarter: "Q3", amount: 24 },
859
+ { quarter: "Q4", amount: 30 }
860
+ ]
861
+ }
862
+ },
863
+ { type: "canvas.status", id: "rev", status: "complete" },
864
+ { type: "done" }
865
+ ]
866
+ };
867
+ var table = {
868
+ id: "table",
869
+ title: "Table",
870
+ description: "A data grid with columns and rows.",
871
+ events: [
872
+ {
873
+ type: "canvas.create",
874
+ artifact: {
875
+ id: "tbl",
876
+ type: "table",
877
+ title: "Model comparison",
878
+ version: 1,
879
+ status: "streaming",
880
+ data: {
881
+ columns: [
882
+ { key: "model", label: "Model" },
883
+ { key: "context", label: "Context", align: "right" },
884
+ { key: "price", label: "$/Mtok", align: "right" }
885
+ ],
886
+ rows: []
887
+ }
888
+ }
889
+ },
890
+ {
891
+ type: "canvas.patch",
892
+ id: "tbl",
893
+ patch: {
894
+ rows: [
895
+ { model: "Opus 4.8", context: "1M", price: 15 },
896
+ { model: "Sonnet 5", context: "400K", price: 3 },
897
+ { model: "Haiku 4.5", context: "200K", price: 1 },
898
+ { model: "Average", context: "", price: "=ROUND(AVERAGE(C2:C4),2)" }
899
+ ]
900
+ }
901
+ },
902
+ { type: "canvas.status", id: "tbl", status: "complete" },
903
+ { type: "done" }
904
+ ]
905
+ };
906
+ var slides = {
907
+ id: "slides",
908
+ title: "Slide deck",
909
+ description: "A slide deck (title + bullets), navigable and exportable to .pptx.",
910
+ events: [
911
+ { type: "canvas.create", artifact: { id: "deck", type: "slides", title: "Q4 Review", version: 1, status: "streaming", data: { slides: [] } } },
912
+ {
913
+ type: "canvas.patch",
914
+ id: "deck",
915
+ patch: {
916
+ slides: [
917
+ { layout: "title", title: "Q4 Business Review", subtitle: "Prepared for the board \xB7 2026", background: "#0b1020", textColor: "#e6e8ef", notes: "Welcome the room; set the tone for the quarter." },
918
+ { layout: "content", title: "Q4 in review", bullets: ["Revenue up 24% QoQ", "Two new enterprise logos", "Churn down to 1.2%"], notes: "Lead with the revenue number." },
919
+ {
920
+ layout: "two-column",
921
+ title: "Wins & watch-items",
922
+ bullets: ["Self-serve onboarding", "Usage-based pricing", "Faster support SLAs"],
923
+ bullets2: ["Enterprise security review", "EU data residency", "On-call load"]
924
+ },
925
+ { layout: "section", title: "What's next", subtitle: "Roadmap for Q1" },
926
+ {
927
+ layout: "blank",
928
+ elements: [
929
+ { id: "e1", type: "text", x: 8, y: 12, w: 60, h: 16, text: "Thank you", fontSize: 54, bold: true },
930
+ { id: "e2", type: "text", x: 8, y: 34, w: 70, h: 12, text: "Questions?", fontSize: 28, color: "#6b7280" }
931
+ ]
932
+ }
933
+ ]
934
+ }
935
+ },
936
+ { type: "canvas.status", id: "deck", status: "complete" },
937
+ { type: "done" }
938
+ ]
939
+ };
940
+ var scenarios = [htmlPage, document2, chart, table, slides];
941
+ var RegistryContext = createContext({});
942
+ function CanvasRegistryProvider({ registry, children }) {
943
+ return /* @__PURE__ */ jsx(RegistryContext.Provider, { value: registry, children });
944
+ }
945
+ function useRenderer(type) {
946
+ return useContext(RegistryContext)[type];
947
+ }
948
+ function mergeRegistries(...registries) {
949
+ return Object.assign({}, ...registries);
950
+ }
951
+
952
+ // src/io/xlsx.ts
953
+ var THEME_PALETTE = [
954
+ "FFFFFF",
955
+ "000000",
956
+ "E7E6E6",
957
+ "44546A",
958
+ // lt1, dk1, lt2, dk2
959
+ "4472C4",
960
+ "ED7D31",
961
+ "A5A5A5",
962
+ "FFC000",
963
+ // accent 1–4
964
+ "5B9BD5",
965
+ "70AD47",
966
+ "0563C1",
967
+ "954F72"
968
+ // accent 5–6, hlink, followed-hlink
969
+ ];
970
+ function tintChannel(channel, tint) {
971
+ const t = tint < 0 ? channel * (1 + tint) : channel * (1 - tint) + 255 * tint;
972
+ return Math.max(0, Math.min(255, Math.round(t)));
973
+ }
974
+ var INDEXED_PALETTE = {
975
+ 0: "000000",
976
+ 1: "FFFFFF",
977
+ 2: "FF0000",
978
+ 3: "00FF00",
979
+ 4: "0000FF",
980
+ 5: "FFFF00",
981
+ 6: "FF00FF",
982
+ 7: "00FFFF",
983
+ 8: "000000",
984
+ 9: "FFFFFF",
985
+ 10: "FF0000",
986
+ 11: "00FF00",
987
+ 12: "0000FF",
988
+ 13: "FFFF00",
989
+ 14: "FF00FF",
990
+ 15: "00FFFF",
991
+ 16: "800000",
992
+ 17: "008000",
993
+ 18: "000080",
994
+ 19: "808000",
995
+ 20: "800080",
996
+ 21: "008080",
997
+ 22: "C0C0C0",
998
+ 23: "808080",
999
+ 24: "9999FF",
1000
+ 25: "993366",
1001
+ 26: "FFFFCC",
1002
+ 27: "CCFFFF",
1003
+ 28: "660066",
1004
+ 29: "FF8080",
1005
+ 30: "0066CC",
1006
+ 31: "CCCCFF",
1007
+ 32: "000080",
1008
+ 33: "FF00FF",
1009
+ 34: "FFFF00",
1010
+ 35: "00FFFF",
1011
+ 36: "800080",
1012
+ 37: "800000",
1013
+ 38: "008080",
1014
+ 39: "0000FF",
1015
+ 40: "00CCFF",
1016
+ 41: "CCFFFF",
1017
+ 42: "CCFFCC",
1018
+ 43: "FFFF99",
1019
+ 44: "99CCFF",
1020
+ 45: "FF99CC",
1021
+ 46: "CC99FF",
1022
+ 47: "FFCC99",
1023
+ 48: "3366FF",
1024
+ 49: "33CCCC",
1025
+ 50: "99CC00",
1026
+ 51: "FFCC00",
1027
+ 52: "FF9900",
1028
+ 53: "FF6600",
1029
+ 54: "666699",
1030
+ 55: "969696",
1031
+ 56: "003366",
1032
+ 57: "339966",
1033
+ 58: "003300",
1034
+ 59: "333300",
1035
+ 60: "993300",
1036
+ 61: "993366",
1037
+ 62: "333399",
1038
+ 63: "333333"
1039
+ };
1040
+ function toHex(color) {
1041
+ if (!color || typeof color !== "object") return void 0;
1042
+ let hex;
1043
+ if (typeof color.argb === "string") {
1044
+ hex = color.argb.length === 8 ? color.argb.slice(2) : color.argb;
1045
+ } else if (typeof color.theme === "number") {
1046
+ hex = THEME_PALETTE[color.theme];
1047
+ } else if (typeof color.indexed === "number") {
1048
+ hex = INDEXED_PALETTE[color.indexed];
1049
+ }
1050
+ if (!hex || !/^[0-9a-fA-F]{6}$/.test(hex)) return void 0;
1051
+ const tint = typeof color.tint === "number" ? color.tint : 0;
1052
+ if (tint) {
1053
+ const n = parseInt(hex, 16);
1054
+ const r = tintChannel(n >> 16 & 255, tint);
1055
+ const g = tintChannel(n >> 8 & 255, tint);
1056
+ const b = tintChannel(n & 255, tint);
1057
+ hex = (r << 16 | g << 8 | b).toString(16).padStart(6, "0");
1058
+ }
1059
+ return `#${hex}`;
1060
+ }
1061
+ var H_ALIGN = { center: 0, left: 1, right: 2 };
1062
+ var V_ALIGN = { middle: 0, top: 1, bottom: 2 };
1063
+ var BORDER_STYLE = {
1064
+ hair: 2,
1065
+ thin: 1,
1066
+ dotted: 3,
1067
+ dashDot: 5,
1068
+ dashDotDot: 6,
1069
+ dashed: 4,
1070
+ mediumDashed: 9,
1071
+ mediumDashDot: 10,
1072
+ mediumDashDotDot: 11,
1073
+ slantDashDot: 12,
1074
+ medium: 8,
1075
+ double: 7,
1076
+ thick: 13
1077
+ };
1078
+ function borderSide(side) {
1079
+ if (!side || !side.style) return void 0;
1080
+ return { style: BORDER_STYLE[side.style] ?? 1, color: toHex(side.color) ?? "#000000" };
1081
+ }
1082
+ function display(value) {
1083
+ if (value == null) return "";
1084
+ if (typeof value === "object") {
1085
+ if (value.result != null) return display(value.result);
1086
+ if (typeof value.text === "string") return value.text;
1087
+ if (value instanceof Date) return formatDate(value, "yyyy-mm-dd");
1088
+ return "";
1089
+ }
1090
+ return String(value);
1091
+ }
1092
+ function formatNumber(value, numFmt) {
1093
+ const fmt = numFmt && numFmt !== "General" ? numFmt : "";
1094
+ if (!fmt) return String(value);
1095
+ const decimals = (fmt.match(/\.([0#]+)/)?.[1] ?? "").length;
1096
+ if (fmt.includes("%")) return `${(value * 100).toFixed(decimals)}%`;
1097
+ const thousands = /[#0],[#0]/.test(fmt);
1098
+ let out = thousands ? value.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : value.toFixed(decimals);
1099
+ const currency = fmt.match(/[$₩€£¥]/);
1100
+ if (currency) out = value < 0 ? `-${currency[0]}${out.slice(1)}` : `${currency[0]}${out}`;
1101
+ return out;
1102
+ }
1103
+ function formatDate(d, numFmt) {
1104
+ const fmt = numFmt && numFmt !== "General" && /[ymdhs]/i.test(numFmt) ? numFmt : "yyyy-mm-dd";
1105
+ const p = (n, w = 2) => String(n).padStart(w, "0");
1106
+ const map = {
1107
+ yyyy: String(d.getFullYear()),
1108
+ yy: p(d.getFullYear() % 100),
1109
+ mmmm: d.toLocaleString("en-US", { month: "long" }),
1110
+ mmm: d.toLocaleString("en-US", { month: "short" }),
1111
+ mm: p(d.getMonth() + 1),
1112
+ m: String(d.getMonth() + 1),
1113
+ dd: p(d.getDate()),
1114
+ d: String(d.getDate()),
1115
+ hh: p(d.getHours()),
1116
+ h: String(d.getHours()),
1117
+ ss: p(d.getSeconds())
1118
+ };
1119
+ return fmt.replace(/yyyy|yy|mmmm|mmm|mm|m|dd|d|hh|h|ss/g, (t) => map[t] ?? t);
1120
+ }
1121
+ function cellValue(cell) {
1122
+ const raw = cell.value;
1123
+ const effective = raw;
1124
+ const m = display(effective);
1125
+ const isFormula = raw && typeof raw === "object" && "formula" in raw;
1126
+ const hasStyle = cell.font || cell.fill?.type === "pattern" || cell.alignment;
1127
+ if (m === "" && !isFormula && !hasStyle) return null;
1128
+ const v = { m };
1129
+ if (isFormula) {
1130
+ v.f = `=${raw.formula}`;
1131
+ v.v = display(raw.result);
1132
+ if (typeof raw.result === "number") v.m = formatNumber(raw.result, cell.numFmt);
1133
+ } else if (typeof effective === "number") {
1134
+ v.v = effective;
1135
+ v.m = formatNumber(effective, cell.numFmt);
1136
+ v.ct = { fa: cell.numFmt || "General", t: "n" };
1137
+ } else if (effective instanceof Date) {
1138
+ v.m = formatDate(effective, cell.numFmt);
1139
+ v.v = v.m;
1140
+ v.ct = { fa: cell.numFmt || "yyyy-mm-dd", t: "d" };
1141
+ } else {
1142
+ v.v = m;
1143
+ }
1144
+ const font = cell.font;
1145
+ if (font?.bold) v.bl = 1;
1146
+ if (font?.italic) v.it = 1;
1147
+ if (font?.underline) v.un = 1;
1148
+ if (font?.size) v.fs = font.size;
1149
+ if (font?.name) v.ff = font.name;
1150
+ const fc = toHex(font?.color);
1151
+ if (fc) v.fc = fc;
1152
+ if (cell.fill?.type === "pattern" && cell.fill.pattern === "solid") {
1153
+ const bg = toHex(cell.fill.fgColor) ?? toHex(cell.fill.bgColor);
1154
+ if (bg) v.bg = bg;
1155
+ }
1156
+ const ha = cell.alignment?.horizontal;
1157
+ if (ha && ha in H_ALIGN) v.ht = H_ALIGN[ha];
1158
+ const va = cell.alignment?.vertical;
1159
+ if (va && va in V_ALIGN) v.vt = V_ALIGN[va];
1160
+ if (cell.alignment?.wrapText) v.tb = "2";
1161
+ return v;
1162
+ }
1163
+ function bytesToBase64(buf) {
1164
+ const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
1165
+ let bin = "";
1166
+ for (let i = 0; i < bytes.length; i += 32768) {
1167
+ bin += String.fromCharCode.apply(null, Array.from(bytes.subarray(i, i + 32768)));
1168
+ }
1169
+ return btoa(bin);
1170
+ }
1171
+ var EMU_PER_PX = 9525;
1172
+ var DEFAULT_COL_PX = 73;
1173
+ var DEFAULT_ROW_PX = 19;
1174
+ function sheetImages(ws, media, columnlen, rowlen) {
1175
+ const placed = typeof ws.getImages === "function" ? ws.getImages() : [];
1176
+ if (!placed.length || !media?.length) return [];
1177
+ const colLeft = (c) => {
1178
+ let x = 0;
1179
+ for (let i = 0; i < c; i++) x += columnlen[i] ?? DEFAULT_COL_PX;
1180
+ return x;
1181
+ };
1182
+ const rowTop = (r) => {
1183
+ let y = 0;
1184
+ for (let i = 0; i < r; i++) y += rowlen[i] ?? DEFAULT_ROW_PX;
1185
+ return y;
1186
+ };
1187
+ const images = [];
1188
+ placed.forEach((im, idx) => {
1189
+ const entry = media[Number(im.imageId)];
1190
+ if (!entry?.buffer) return;
1191
+ const src = `data:image/${entry.extension || "png"};base64,${bytesToBase64(entry.buffer)}`;
1192
+ const tl = im.range?.tl;
1193
+ if (!tl) return;
1194
+ const left = colLeft(tl.nativeCol ?? tl.col ?? 0) + Math.round((tl.nativeColOff ?? 0) / EMU_PER_PX);
1195
+ const top = rowTop(tl.nativeRow ?? tl.row ?? 0) + Math.round((tl.nativeRowOff ?? 0) / EMU_PER_PX);
1196
+ let width = im.range?.ext?.width;
1197
+ let height = im.range?.ext?.height;
1198
+ if ((!width || !height) && im.range?.br) {
1199
+ const br = im.range.br;
1200
+ width = colLeft(br.nativeCol ?? br.col ?? (tl.col ?? 0) + 2) - left;
1201
+ height = rowTop(br.nativeRow ?? br.row ?? (tl.row ?? 0) + 2) - top;
1202
+ }
1203
+ images.push({
1204
+ id: `img_${idx}`,
1205
+ src,
1206
+ left: Math.max(0, Math.round(left)),
1207
+ top: Math.max(0, Math.round(top)),
1208
+ width: Math.max(16, Math.round(width || 100)),
1209
+ height: Math.max(16, Math.round(height || 100))
1210
+ });
1211
+ });
1212
+ return images;
1213
+ }
1214
+ function parseMerge(range) {
1215
+ const m = /^([A-Z]+)(\d+):([A-Z]+)(\d+)$/.exec(range);
1216
+ if (!m) return null;
1217
+ const col = (s) => [...s].reduce((n, ch) => n * 26 + (ch.charCodeAt(0) - 64), 0) - 1;
1218
+ const r1 = Number(m[2]) - 1, c1 = col(m[1]);
1219
+ const r2 = Number(m[4]) - 1, c2 = col(m[3]);
1220
+ const r = Math.min(r1, r2), c = Math.min(c1, c2);
1221
+ return { key: `${r}_${c}`, entry: { r, c, rs: Math.abs(r2 - r1) + 1, cs: Math.abs(c2 - c1) + 1 } };
1222
+ }
1223
+ async function xlsxToSheets(buffer, load) {
1224
+ const ExcelJS = await load();
1225
+ const wb = new ExcelJS.Workbook();
1226
+ await wb.xlsx.load(buffer);
1227
+ const media = wb.model?.media ?? [];
1228
+ const sheets = [];
1229
+ wb.worksheets.forEach((ws, index) => {
1230
+ const colCount = Math.max(ws.columnCount || 0, ws.actualColumnCount || 0);
1231
+ const rowCount = Math.max(ws.rowCount || 0, ws.actualRowCount || 0);
1232
+ const merge = {};
1233
+ const master = /* @__PURE__ */ new Map();
1234
+ for (const range of ws.model?.merges ?? []) {
1235
+ const parsed = parseMerge(range);
1236
+ if (!parsed) continue;
1237
+ const { r, c, rs, cs } = parsed.entry;
1238
+ merge[parsed.key] = parsed.entry;
1239
+ for (let rr = r; rr < r + rs; rr++) {
1240
+ for (let cc = c; cc < c + cs; cc++) {
1241
+ if (rr !== r || cc !== c) master.set(`${rr}_${cc}`, { r, c });
1242
+ }
1243
+ }
1244
+ }
1245
+ const celldata = [];
1246
+ const borderInfo = [];
1247
+ for (let r = 1; r <= rowCount; r++) {
1248
+ const row = ws.getRow(r);
1249
+ for (let c = 1; c <= colCount; c++) {
1250
+ const cell = row.getCell(c);
1251
+ const b = cell.border;
1252
+ if (b) {
1253
+ const l = borderSide(b.left), rt = borderSide(b.right), t = borderSide(b.top), bt = borderSide(b.bottom);
1254
+ if (l || rt || t || bt) {
1255
+ const value = { row_index: r - 1, col_index: c - 1 };
1256
+ if (l) value.l = l;
1257
+ if (rt) value.r = rt;
1258
+ if (t) value.t = t;
1259
+ if (bt) value.b = bt;
1260
+ borderInfo.push({ rangeType: "cell", value });
1261
+ }
1262
+ }
1263
+ const key = `${r - 1}_${c - 1}`;
1264
+ const cover = master.get(key);
1265
+ if (cover) {
1266
+ celldata.push({ r: r - 1, c: c - 1, v: { mc: { r: cover.r, c: cover.c } } });
1267
+ continue;
1268
+ }
1269
+ const v = cellValue(cell);
1270
+ if (v) {
1271
+ if (merge[key]) v.mc = merge[key];
1272
+ celldata.push({ r: r - 1, c: c - 1, v });
1273
+ }
1274
+ }
1275
+ }
1276
+ const colChars = {};
1277
+ for (const cell of celldata) {
1278
+ const v = cell.v;
1279
+ if (v.mc) continue;
1280
+ const len = typeof v.m === "string" ? v.m.length : 0;
1281
+ const c = cell.c;
1282
+ if (len > (colChars[c] ?? 0)) colChars[c] = len;
1283
+ }
1284
+ const columnlen = {};
1285
+ for (let c = 0; c < colCount; c++) {
1286
+ const stored = ws.getColumn(c + 1)?.width;
1287
+ const px2 = Math.max(colChars[c] ? colChars[c] * 8 + 20 : 0, stored ? stored * 7 + 5 : 0);
1288
+ if (px2) columnlen[c] = Math.max(56, Math.min(320, Math.round(px2)));
1289
+ }
1290
+ const rowlen = {};
1291
+ for (let r = 1; r <= rowCount; r++) {
1292
+ const h = ws.getRow(r)?.height;
1293
+ if (h) rowlen[r - 1] = Math.round(h * 1.33);
1294
+ }
1295
+ sheets.push({
1296
+ name: ws.name || `Sheet${index + 1}`,
1297
+ id: `sheet_${index}`,
1298
+ order: index,
1299
+ status: index === 0 ? 1 : 0,
1300
+ // Size the grid to the data plus a small buffer — enough to feel like a real
1301
+ // sheet, tight enough that the scrollbars stay proportional and there aren't
1302
+ // rows/columns of empty grid to scroll past.
1303
+ row: Math.max(rowCount + 8, 24),
1304
+ column: Math.max(colCount + 2, 10),
1305
+ celldata,
1306
+ config: { merge, columnlen, rowlen, borderInfo },
1307
+ images: sheetImages(ws, media, columnlen, rowlen)
1308
+ });
1309
+ });
1310
+ const { columns, rows } = flatten(wb.worksheets[0]);
1311
+ return { sheets, columns, rows };
1312
+ }
1313
+ function cellVal(v) {
1314
+ if (v == null) return "";
1315
+ if (typeof v === "number" || typeof v === "string") return v;
1316
+ if (typeof v === "object") {
1317
+ if (v.result != null) return cellVal(v.result);
1318
+ if (typeof v.text === "string") return v.text;
1319
+ if (v instanceof Date) return v.toISOString().slice(0, 10);
1320
+ }
1321
+ return String(v);
1322
+ }
1323
+ function flatten(ws) {
1324
+ if (!ws) return { columns: [], rows: [] };
1325
+ const colCount = Math.max(ws.columnCount || 0, ws.actualColumnCount || 0);
1326
+ const rowCount = Math.max(ws.rowCount || 0, ws.actualRowCount || 0);
1327
+ const letter = (n) => {
1328
+ let s = "";
1329
+ while (n > 0) {
1330
+ s = String.fromCharCode(65 + (n - 1) % 26) + s;
1331
+ n = Math.floor((n - 1) / 26);
1332
+ }
1333
+ return s;
1334
+ };
1335
+ const read = (cell) => cellVal(cell.value != null ? cell.value : cell.master?.value ?? null);
1336
+ const header = ws.getRow(1);
1337
+ const seen = /* @__PURE__ */ new Map();
1338
+ const columns = [];
1339
+ for (let c = 1; c <= colCount; c++) {
1340
+ let label = String(read(header.getCell(c))).trim() || letter(c);
1341
+ const count = seen.get(label) ?? 0;
1342
+ seen.set(label, count + 1);
1343
+ columns.push({ key: count ? `${label} (${count + 1})` : label, label });
1344
+ }
1345
+ const rows = [];
1346
+ for (let r = 2; r <= rowCount; r++) {
1347
+ const row = ws.getRow(r);
1348
+ const obj = {};
1349
+ let has = false;
1350
+ for (let c = 1; c <= colCount; c++) {
1351
+ const v = read(row.getCell(c));
1352
+ obj[columns[c - 1].key] = v;
1353
+ if (v !== "") has = true;
1354
+ }
1355
+ if (has) rows.push(obj);
1356
+ }
1357
+ return { columns, rows };
1358
+ }
1359
+
1360
+ // src/io/importers.ts
1361
+ var IMPORTABLE_EXTENSIONS = [".csv", ".md", ".markdown", ".txt", ".html", ".htm", ".json", ".xlsx"];
1362
+ var extensionOf = (name) => {
1363
+ const dot = name.lastIndexOf(".");
1364
+ return dot === -1 ? "" : name.slice(dot).toLowerCase();
1365
+ };
1366
+ var canImport = (file) => IMPORTABLE_EXTENSIONS.includes(extensionOf(file.name));
1367
+ var baseName = (name) => name.replace(/\.[^.]+$/, "");
1368
+ var slug = (name) => baseName(name).replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "file";
1369
+ var importSeq = 0;
1370
+ var newId = (name) => `imp_${slug(name)}_${Date.now().toString(36)}${importSeq++}`;
1371
+ function toEvents(artifact2) {
1372
+ const create = { type: "canvas.create", artifact: artifact2 };
1373
+ const complete = { type: "canvas.status", id: artifact2.id, status: "complete" };
1374
+ return [create, complete];
1375
+ }
1376
+ async function importFile(file) {
1377
+ const ext = extensionOf(file.name);
1378
+ const id = newId(file.name);
1379
+ const title = baseName(file.name);
1380
+ switch (ext) {
1381
+ case ".csv": {
1382
+ const data = parseCsv(await file.text());
1383
+ return toEvents(artifact(id, "table", title, data));
1384
+ }
1385
+ case ".md":
1386
+ case ".markdown":
1387
+ case ".txt": {
1388
+ const data = { format: "markdown", content: await file.text() };
1389
+ return toEvents(artifact(id, "document", title, data));
1390
+ }
1391
+ case ".html":
1392
+ case ".htm": {
1393
+ const data = { html: await file.text() };
1394
+ return toEvents(artifact(id, "html", title, data));
1395
+ }
1396
+ case ".json":
1397
+ return importJson(await file.text(), id, title);
1398
+ case ".xlsx": {
1399
+ const { sheets, columns, rows } = await xlsxToSheets(await file.arrayBuffer(), () => loadOptional("exceljs", () => import('exceljs')));
1400
+ return toEvents(artifact(id, "table", title, { columns, rows, sheet: sheets }));
1401
+ }
1402
+ default:
1403
+ throw new Error(`Unsupported file type "${ext || file.name}". Supported: ${IMPORTABLE_EXTENSIONS.join(", ")}`);
1404
+ }
1405
+ }
1406
+ function artifact(id, type, title, data) {
1407
+ return { id, type, title, version: 1, status: "complete", data };
1408
+ }
1409
+ function importJson(text, id, title) {
1410
+ let parsed;
1411
+ try {
1412
+ parsed = JSON.parse(text);
1413
+ } catch {
1414
+ return toEvents(artifact(id, "document", title, { format: "markdown", content: text }));
1415
+ }
1416
+ if (parsed && typeof parsed === "object" && "type" in parsed && "data" in parsed) {
1417
+ const a = parsed;
1418
+ return toEvents({ ...a, id, version: 1, status: "complete" });
1419
+ }
1420
+ const content = "```json\n" + JSON.stringify(parsed, null, 2) + "\n```";
1421
+ return toEvents(artifact(id, "document", title, { format: "markdown", content }));
1422
+ }
1423
+ function parseCsv(text) {
1424
+ const rows = [];
1425
+ let field = "";
1426
+ let record = [];
1427
+ let inQuotes = false;
1428
+ const src = text.replace(/\r\n?/g, "\n");
1429
+ for (let i = 0; i < src.length; i++) {
1430
+ const ch = src[i];
1431
+ if (inQuotes) {
1432
+ if (ch === '"') {
1433
+ if (src[i + 1] === '"') {
1434
+ field += '"';
1435
+ i++;
1436
+ } else inQuotes = false;
1437
+ } else field += ch;
1438
+ } else if (ch === '"') inQuotes = true;
1439
+ else if (ch === ",") {
1440
+ record.push(field);
1441
+ field = "";
1442
+ } else if (ch === "\n") {
1443
+ record.push(field);
1444
+ rows.push(record);
1445
+ field = "";
1446
+ record = [];
1447
+ } else field += ch;
1448
+ }
1449
+ if (field !== "" || record.length) {
1450
+ record.push(field);
1451
+ rows.push(record);
1452
+ }
1453
+ const [header = [], ...body] = rows.filter((r) => r.length > 1 || r[0] !== "");
1454
+ const columns = uniqueColumns(header.map((label, i) => label.trim() || `Column ${i + 1}`));
1455
+ const dataRows = body.map((r) => {
1456
+ const obj = {};
1457
+ columns.forEach((c, i) => obj[c.key] = coerce(r[i] ?? ""));
1458
+ return obj;
1459
+ });
1460
+ return { columns, rows: dataRows };
1461
+ }
1462
+ function uniqueColumns(labels) {
1463
+ const seen = /* @__PURE__ */ new Map();
1464
+ return labels.map((label) => {
1465
+ const count = seen.get(label) ?? 0;
1466
+ seen.set(label, count + 1);
1467
+ return { key: count ? `${label} (${count + 1})` : label, label };
1468
+ });
1469
+ }
1470
+ function coerce(raw) {
1471
+ const t = raw.trim();
1472
+ if (t === "") return raw;
1473
+ const n = Number(t);
1474
+ return Number.isFinite(n) && String(n) === t ? n : raw;
1475
+ }
1476
+ var DEVICES = [
1477
+ { id: "desktop", label: "Desktop", width: "100%" },
1478
+ { id: "tablet", label: "Tablet", width: "768px" },
1479
+ { id: "mobile", label: "Mobile", width: "390px" }
1480
+ ];
1481
+ var BLOCKS = [
1482
+ { tag: "h2", label: "Heading" },
1483
+ { tag: "p", label: "Text" },
1484
+ { tag: "button", label: "Button" },
1485
+ { tag: "img", label: "Image" },
1486
+ { tag: "hr", label: "Divider" }
1487
+ ];
1488
+ var TEMPLATES = {
1489
+ hero: {
1490
+ label: "Hero",
1491
+ html: `<section style="padding:72px 24px;text-align:center;background:linear-gradient(180deg,#0b1020,#151a2e);color:#e6e8ef">
1492
+ <h1 style="font-size:clamp(30px,5vw,52px);margin:0 0 14px;font-weight:800">Ship faster with confidence</h1>
1493
+ <p style="font-size:18px;line-height:1.6;color:#9aa4b2;max-width:560px;margin:0 auto 28px">One clear sentence about the value you deliver to customers.</p>
1494
+ <a href="#" style="display:inline-block;padding:13px 26px;background:#6366f1;color:#fff;border-radius:10px;text-decoration:none;font-weight:700">Get started free</a>
1495
+ </section>`
1496
+ },
1497
+ features: {
1498
+ label: "Features",
1499
+ html: `<section style="padding:56px 24px;background:#0b1020;color:#e6e8ef">
1500
+ <div style="max-width:960px;margin:0 auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:20px">
1501
+ <div style="background:#151a2e;border:1px solid #232a44;border-radius:14px;padding:24px"><h3 style="margin:0 0 8px;font-size:18px">Fast</h3><p style="margin:0;color:#9aa4b2;line-height:1.6">Explain this benefit in a sentence or two.</p></div>
1502
+ <div style="background:#151a2e;border:1px solid #232a44;border-radius:14px;padding:24px"><h3 style="margin:0 0 8px;font-size:18px">Reliable</h3><p style="margin:0;color:#9aa4b2;line-height:1.6">Explain this benefit in a sentence or two.</p></div>
1503
+ <div style="background:#151a2e;border:1px solid #232a44;border-radius:14px;padding:24px"><h3 style="margin:0 0 8px;font-size:18px">Secure</h3><p style="margin:0;color:#9aa4b2;line-height:1.6">Explain this benefit in a sentence or two.</p></div>
1504
+ </div>
1505
+ </section>`
1506
+ },
1507
+ cta: {
1508
+ label: "Call to action",
1509
+ html: `<section style="margin:24px;padding:48px 24px;text-align:center;background:#6366f1;color:#fff;border-radius:16px">
1510
+ <h2 style="margin:0 0 10px;font-size:26px">Ready to get started?</h2>
1511
+ <p style="margin:0 0 22px;opacity:.9">Join thousands of teams already building with us.</p>
1512
+ <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>
1513
+ </section>`
1514
+ }
1515
+ };
1516
+ function HtmlRenderer({ artifact: artifact2 }) {
1517
+ const iframeRef = useRef(null);
1518
+ const imgFileRef = useRef(null);
1519
+ const api = useCanvasStoreApi();
1520
+ const setSelections = useCanvasStore((s) => s.setSelections);
1521
+ const applyEvent = useCanvasStore((s) => s.applyUserEvent);
1522
+ const sendIframeCommand = useCanvasStore((s) => s.sendIframeCommand);
1523
+ const selections = useCanvasStore((s) => s.selections);
1524
+ const iframeCommand = useCanvasStore((s) => s.iframeCommand);
1525
+ const [device, setDevice] = useState("desktop");
1526
+ const [mode, setMode] = useState("design");
1527
+ const lastSelfHtml = useRef(null);
1528
+ const srcDocRef = useRef("");
1529
+ const srcDoc = useMemo(() => {
1530
+ if (artifact2.data.html === lastSelfHtml.current) return srcDocRef.current;
1531
+ srcDocRef.current = withInspector(artifact2.data.html);
1532
+ return srcDocRef.current;
1533
+ }, [artifact2.data.html]);
1534
+ const selected = selections.filter((s) => s.artifactId === artifact2.id);
1535
+ const single = selected.length === 1 ? selected[0] : null;
1536
+ useEffect(() => {
1537
+ function onMessage(event) {
1538
+ if (event.source !== iframeRef.current?.contentWindow) return;
1539
+ const data = event.data;
1540
+ if (data?.source !== INSPECTOR_MARK) return;
1541
+ if (data.type === "select") {
1542
+ setSelections([
1543
+ { artifactId: artifact2.id, cid: data.cid, selector: data.selector, tag: data.tag, text: data.text, outerHtml: data.outerHtml, styles: data.styles, isGroup: data.isGroup }
1544
+ ]);
1545
+ } else if (data.type === "multi_select") {
1546
+ setSelections(
1547
+ (data.items ?? []).map((it) => ({
1548
+ artifactId: artifact2.id,
1549
+ cid: it.cid,
1550
+ selector: it.selector,
1551
+ tag: it.tag,
1552
+ text: it.text,
1553
+ outerHtml: it.outerHtml
1554
+ }))
1555
+ );
1556
+ } else if (data.type === "node_edit") {
1557
+ applyEvent({ type: "canvas.node_patch", id: artifact2.id, cid: data.cid, html: data.html });
1558
+ lastSelfHtml.current = api.getState().canvas.artifacts[artifact2.id]?.data?.html ?? null;
1559
+ } else if (data.type === "doc_edit") {
1560
+ applyEvent({ type: "canvas.patch", id: artifact2.id, patch: { html: data.html } });
1561
+ if (data.self) {
1562
+ lastSelfHtml.current = api.getState().canvas.artifacts[artifact2.id]?.data?.html ?? null;
1563
+ } else {
1564
+ setSelections([]);
1565
+ }
1566
+ }
1567
+ }
1568
+ window.addEventListener("message", onMessage);
1569
+ return () => window.removeEventListener("message", onMessage);
1570
+ }, [artifact2.id, setSelections, applyEvent, api]);
1571
+ useEffect(() => {
1572
+ if (!selected.length) {
1573
+ iframeRef.current?.contentWindow?.postMessage({ source: INSPECTOR_MARK, type: "clear" }, "*");
1574
+ }
1575
+ }, [selected.length]);
1576
+ useEffect(() => {
1577
+ if (!iframeCommand || iframeCommand.artifactId !== artifact2.id) return;
1578
+ iframeRef.current?.contentWindow?.postMessage({ source: INSPECTOR_MARK, ...iframeCommand }, "*");
1579
+ }, [iframeCommand, artifact2.id]);
1580
+ const command = (type, extra = {}) => sendIframeCommand({ artifactId: artifact2.id, type, cid: single?.cid, ...extra });
1581
+ const commitCode = (html) => {
1582
+ if (html !== artifact2.data.html) applyEvent({ type: "canvas.patch", id: artifact2.id, patch: { html } });
1583
+ };
1584
+ const onImgFile = (file) => {
1585
+ if (!file || !single) return;
1586
+ const reader = new FileReader();
1587
+ reader.onload = () => sendIframeCommand({ artifactId: artifact2.id, type: "set_src", cid: single.cid, value: String(reader.result) });
1588
+ reader.readAsDataURL(file);
1589
+ };
1590
+ return /* @__PURE__ */ jsxs("div", { className: "cv-html-wrap", children: [
1591
+ /* @__PURE__ */ jsx("input", { ref: imgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
1592
+ onImgFile(e.target.files?.[0]);
1593
+ e.target.value = "";
1594
+ } }),
1595
+ /* @__PURE__ */ jsxs("div", { className: "cv-html-bar cv-chrome", children: [
1596
+ mode === "design" && /* @__PURE__ */ jsxs(Fragment, { children: [
1597
+ /* @__PURE__ */ jsx("div", { className: "cv-html-seg", role: "group", "aria-label": "Preview width", children: DEVICES.map((d) => /* @__PURE__ */ jsx("button", { className: device === d.id ? "is-on" : "", onClick: () => setDevice(d.id), children: d.label }, d.id)) }),
1598
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" }),
1599
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Add" }),
1600
+ BLOCKS.map((b) => /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => command("insert", { block: b.tag }), children: b.label }, b.tag)),
1601
+ /* @__PURE__ */ jsxs(
1602
+ "select",
1603
+ {
1604
+ className: "cv-html-tpl",
1605
+ value: "",
1606
+ title: "Insert a section template",
1607
+ onChange: (e) => {
1608
+ const t = TEMPLATES[e.target.value];
1609
+ if (t) sendIframeCommand({ artifactId: artifact2.id, type: "insert_html", cid: single?.cid, html: t.html });
1610
+ e.currentTarget.value = "";
1611
+ },
1612
+ children: [
1613
+ /* @__PURE__ */ jsx("option", { value: "", children: "Section\u2026" }),
1614
+ Object.entries(TEMPLATES).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
1615
+ ]
1616
+ }
1617
+ ),
1618
+ selected.length >= 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
1619
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__sep" }),
1620
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__label", children: "Selection" }),
1621
+ single?.isGroup ? /* @__PURE__ */ jsx("button", { className: "cv-html-actbtn", onClick: () => command("ungroup"), children: "\u229F Ungroup" }) : /* @__PURE__ */ jsx(
1622
+ "button",
1623
+ {
1624
+ className: "cv-html-actbtn",
1625
+ disabled: selected.length < 2,
1626
+ title: selected.length < 2 ? "Select 2+ elements (Shift-click, or drag a box) to group" : "Group \u2014 they'll move together",
1627
+ onClick: () => sendIframeCommand({ artifactId: artifact2.id, type: "group", cids: selected.map((s) => s.cid) }),
1628
+ children: "\u229E Group"
1629
+ }
1630
+ ),
1631
+ single?.tag === "img" && /* @__PURE__ */ jsxs(Fragment, { children: [
1632
+ /* @__PURE__ */ jsx("button", { className: "cv-html-add", onClick: () => imgFileRef.current?.click(), children: "\u{1F5BC} Upload" }),
1633
+ /* @__PURE__ */ jsx(
1634
+ "button",
1635
+ {
1636
+ className: "cv-html-add",
1637
+ onClick: () => {
1638
+ const url = window.prompt("Image URL");
1639
+ if (url && single) sendIframeCommand({ artifactId: artifact2.id, type: "set_src", cid: single.cid, value: url });
1640
+ },
1641
+ children: "\u{1F517} URL"
1642
+ }
1643
+ )
1644
+ ] }),
1645
+ single && /* @__PURE__ */ jsxs(Fragment, { children: [
1646
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Duplicate", onClick: () => command("duplicate"), children: "\u29C9" }),
1647
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move up", onClick: () => command("move_up"), children: "\u2191" }),
1648
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act", title: "Move down", onClick: () => command("move_down"), children: "\u2193" }),
1649
+ /* @__PURE__ */ jsx("button", { className: "cv-html-act cv-html-act--del", title: "Delete", onClick: () => command("delete"), children: "\u{1F5D1}" })
1650
+ ] })
1651
+ ] })
1652
+ ] }),
1653
+ /* @__PURE__ */ jsx("span", { className: "cv-html-bar__spacer" }),
1654
+ /* @__PURE__ */ jsxs("div", { className: "cv-html-seg", role: "group", "aria-label": "View mode", children: [
1655
+ /* @__PURE__ */ jsx("button", { className: mode === "design" ? "is-on" : "", onClick: () => setMode("design"), children: "Design" }),
1656
+ /* @__PURE__ */ jsx("button", { className: mode === "code" ? "is-on" : "", onClick: () => setMode("code"), children: "Code" })
1657
+ ] })
1658
+ ] }),
1659
+ mode === "design" ? /* @__PURE__ */ jsx("div", { className: "cv-html-stage", children: /* @__PURE__ */ jsx(
1660
+ "iframe",
1661
+ {
1662
+ ref: iframeRef,
1663
+ className: "cv-html",
1664
+ title: artifact2.title,
1665
+ srcDoc,
1666
+ sandbox: "allow-scripts allow-popups allow-modals",
1667
+ style: { width: DEVICES.find((d) => d.id === device).width }
1668
+ }
1669
+ ) }) : /* @__PURE__ */ jsx(
1670
+ "textarea",
1671
+ {
1672
+ className: "cv-html-code",
1673
+ defaultValue: artifact2.data.html,
1674
+ spellCheck: false,
1675
+ onBlur: (e) => commitCode(e.target.value),
1676
+ "aria-label": "HTML source"
1677
+ },
1678
+ artifact2.data.html
1679
+ )
1680
+ ] });
1681
+ }
1682
+
1683
+ // src/components/renderers/index.ts
1684
+ var ChartRenderer = lazy(() => import('./ChartRenderer-CKCJ2LQX.js').then((m) => ({ default: m.ChartRenderer })));
1685
+ var DocumentRenderer = lazy(() => import('./DocumentRenderer-7HP3YQUK.js').then((m) => ({ default: m.DocumentRenderer })));
1686
+ var TableRenderer = lazy(() => import('./TableRenderer-TRYSMQA7.js').then((m) => ({ default: m.TableRenderer })));
1687
+ var SlidesRenderer = lazy(() => import('./SlidesRenderer-KFSPD63I.js').then((m) => ({ default: m.SlidesRenderer })));
1688
+ var builtinRenderers = {
1689
+ html: HtmlRenderer,
1690
+ document: DocumentRenderer,
1691
+ chart: ChartRenderer,
1692
+ table: TableRenderer,
1693
+ slides: SlidesRenderer
1694
+ };
1695
+
1696
+ // src/export/download.ts
1697
+ function downloadBlob(filename, mime, content) {
1698
+ const blob = new Blob([content], { type: `${mime};charset=utf-8` });
1699
+ const url = URL.createObjectURL(blob);
1700
+ const anchor = document.createElement("a");
1701
+ anchor.href = url;
1702
+ anchor.download = filename;
1703
+ document.body.appendChild(anchor);
1704
+ anchor.click();
1705
+ anchor.remove();
1706
+ URL.revokeObjectURL(url);
1707
+ }
1708
+ function slugify(text) {
1709
+ return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
1710
+ }
1711
+
1712
+ // src/export/exporters.ts
1713
+ var MIME = {
1714
+ md: "text/markdown",
1715
+ csv: "text/csv",
1716
+ json: "application/json",
1717
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1718
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1719
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1720
+ };
1721
+ var dataExporters = {
1722
+ document: [
1723
+ { label: "Markdown", extension: "md", mime: MIME.md, build: (a) => a.data.content },
1724
+ { label: "Word", extension: "docx", mime: MIME.docx, build: (a) => documentToDocx(a.data) }
1725
+ ],
1726
+ table: [
1727
+ { label: "CSV", extension: "csv", mime: MIME.csv, build: (a) => tableToCsv(a.data) },
1728
+ { label: "Excel", extension: "xlsx", mime: MIME.xlsx, build: (a) => tableToXlsx(a.data) }
1729
+ ],
1730
+ chart: [
1731
+ { label: "JSON", extension: "json", mime: MIME.json, build: (a) => JSON.stringify(a.data, null, 2) }
1732
+ ],
1733
+ slides: [
1734
+ { label: "PowerPoint", extension: "pptx", mime: MIME.pptx, build: (a) => slidesToPptx(a.data, a.title) },
1735
+ { label: "Figma (JSON)", extension: "json", mime: MIME.json, build: (a) => slidesToFigmaJson(a.data) }
1736
+ ]
1737
+ };
1738
+ function toStandaloneHtml(title, renderedHtml) {
1739
+ return `<!doctype html>
1740
+ <html lang="en">
1741
+ <head>
1742
+ <meta charset="utf-8" />
1743
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1744
+ <title>${escapeHtml(title)}</title>
1745
+ <style>${EXPORT_CSS}</style>
1746
+ </head>
1747
+ <body>
1748
+ <main class="export">
1749
+ ${renderedHtml}
1750
+ </main>
1751
+ </body>
1752
+ </html>`;
1753
+ }
1754
+ function tableToCsv(data) {
1755
+ const header = data.columns.map((c) => csvCell(c.label ?? c.key)).join(",");
1756
+ const body = data.rows.map((row) => data.columns.map((c) => csvCell(String(row[c.key] ?? ""))).join(",")).join("\n");
1757
+ return `${header}
1758
+ ${body}`;
1759
+ }
1760
+ async function tableToXlsx(data) {
1761
+ const { Workbook } = await loadOptional("exceljs", () => import('exceljs'));
1762
+ const workbook = new Workbook();
1763
+ if (data.sheet?.length) {
1764
+ fortuneToWorkbook(workbook, data.sheet);
1765
+ } else {
1766
+ const sheet = workbook.addWorksheet("Sheet1");
1767
+ sheet.addRow(data.columns.map((c) => c.label ?? c.key));
1768
+ for (const row of data.rows) sheet.addRow(data.columns.map((c) => row[c.key] ?? ""));
1769
+ sheet.getRow(1).font = { bold: true };
1770
+ }
1771
+ return workbook.xlsx.writeBuffer();
1772
+ }
1773
+ function fortuneToWorkbook(workbook, sheets) {
1774
+ const align = ["center", "left", "right"];
1775
+ for (const s of sheets) {
1776
+ const ws = workbook.addWorksheet(String(s.name ?? "Sheet1"));
1777
+ for (const cell of s.celldata ?? []) {
1778
+ const v = cell.v;
1779
+ const value = v && typeof v === "object" ? v.v ?? v.m ?? null : v;
1780
+ const xc = ws.getCell(cell.r + 1, cell.c + 1);
1781
+ xc.value = value;
1782
+ if (v && typeof v === "object") {
1783
+ if (v.bl) xc.font = { ...xc.font, bold: true };
1784
+ if (v.it) xc.font = { ...xc.font, italic: true };
1785
+ if (v.fc) xc.font = { ...xc.font, color: { argb: hexToArgb(v.fc) } };
1786
+ if (v.bg) xc.fill = { type: "pattern", pattern: "solid", fgColor: { argb: hexToArgb(v.bg) } };
1787
+ if (typeof v.ht === "number" && align[v.ht]) xc.alignment = { ...xc.alignment, horizontal: align[v.ht] };
1788
+ }
1789
+ }
1790
+ const merge = s.config?.merge ?? {};
1791
+ for (const key of Object.keys(merge)) {
1792
+ const m = merge[key];
1793
+ try {
1794
+ ws.mergeCells(m.r + 1, m.c + 1, m.r + m.rs, m.c + m.cs);
1795
+ } catch {
1796
+ }
1797
+ }
1798
+ }
1799
+ }
1800
+ function hexToArgb(hex) {
1801
+ const h = String(hex).replace("#", "");
1802
+ const full = h.length === 3 ? h.split("").map((x) => x + x).join("") : h;
1803
+ return ("FF" + full).toUpperCase();
1804
+ }
1805
+ async function documentToDocx(data) {
1806
+ const { Document, Packer, Paragraph, HeadingLevel } = await loadOptional("docx", () => import('docx'));
1807
+ const paragraphs = data.content.split("\n").map((line) => {
1808
+ const heading = line.match(/^(#{1,3})\s+(.*)$/);
1809
+ if (heading) {
1810
+ const level = [HeadingLevel.HEADING_1, HeadingLevel.HEADING_2, HeadingLevel.HEADING_3][heading[1].length - 1];
1811
+ return new Paragraph({ text: heading[2], heading: level });
1812
+ }
1813
+ const bullet = line.match(/^[-*]\s+(.*)$/);
1814
+ if (bullet) return new Paragraph({ text: bullet[1], bullet: { level: 0 } });
1815
+ return new Paragraph({ text: line });
1816
+ });
1817
+ const doc = new Document({ sections: [{ children: paragraphs }] });
1818
+ return Packer.toBlob(doc);
1819
+ }
1820
+ async function slidesToPptx(data, _title) {
1821
+ const PptxGenJS = (await loadOptional("pptxgenjs", () => import('pptxgenjs'))).default;
1822
+ const pptx = new PptxGenJS();
1823
+ const W = 10;
1824
+ const H = 5.625;
1825
+ for (const slide of data.slides) {
1826
+ const s = pptx.addSlide();
1827
+ if (slide.background) s.background = { color: slide.background.replace("#", "") };
1828
+ const tc = slide.textColor ? slide.textColor.replace("#", "") : void 0;
1829
+ for (const el of resolveElements(slide)) {
1830
+ const box = { x: el.x / 100 * W, y: el.y / 100 * H, w: el.w / 100 * W, h: el.h / 100 * H };
1831
+ if (el.type === "text") {
1832
+ const color = el.color ? el.color.replace("#", "") : tc;
1833
+ s.addText(el.text ?? "", { ...box, fontSize: (el.fontSize ?? 24) * 0.75, bold: !!el.bold, align: el.align ?? "left", ...color ? { color } : {} });
1834
+ } else if (el.src) {
1835
+ s.addImage({ data: el.src, ...box, sizing: { type: "contain", w: box.w, h: box.h } });
1836
+ }
1837
+ }
1838
+ if (slide.notes) s.addNotes(slide.notes);
1839
+ }
1840
+ return await pptx.write({ outputType: "blob" });
1841
+ }
1842
+ function slidesToFigmaJson(data) {
1843
+ const W = 1280;
1844
+ const H = 720;
1845
+ const GAP = 64;
1846
+ const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
1847
+ const textNode = (x, y, width, characters, fontSize, fontWeight, align, color = "#1F2328") => ({ type: "text", x, y, width, characters, fontSize, fontWeight, align, color });
1848
+ const frames = slides2.map((slide, i) => {
1849
+ const nodes = [];
1850
+ for (const el of resolveElements(slide)) {
1851
+ if (el.type !== "text") continue;
1852
+ nodes.push(
1853
+ textNode(el.x / 100 * W, el.y / 100 * H, el.w / 100 * W, el.text ?? "", el.fontSize ?? 24, el.bold ? 700 : 400, el.align === "center" ? "CENTER" : "LEFT", el.color ?? slide.textColor ?? "#1F2328")
1854
+ );
1855
+ }
1856
+ return { name: `Slide ${i + 1}`, x: 0, y: i * (H + GAP), width: W, height: H, fill: slide.background ?? "#FFFFFF", nodes };
1857
+ });
1858
+ return JSON.stringify({ type: "langchain-canvas/figma-deck", version: 1, frames }, null, 2);
1859
+ }
1860
+ function slidesToSvg(data) {
1861
+ const W = 1280;
1862
+ const H = 720;
1863
+ const GAP = 64;
1864
+ const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
1865
+ const totalH = slides2.length * H + (slides2.length - 1) * GAP;
1866
+ const text = (x, y, value, size, anchor, weight, fill = "#1f2328") => `<text x="${x}" y="${y}" font-family="Inter, Arial, sans-serif" font-size="${size}" font-weight="${weight}" text-anchor="${anchor}" fill="${fill}">${escapeXml(value)}</text>`;
1867
+ const frames = slides2.map((slide, i) => {
1868
+ const y = i * (H + GAP);
1869
+ const bg = slide.background ?? "#ffffff";
1870
+ let body = `<rect width="${W}" height="${H}" rx="8" fill="${bg}" stroke="#e5e7eb"/>`;
1871
+ for (const el of resolveElements(slide)) {
1872
+ const ex = el.x / 100 * W;
1873
+ const ey = el.y / 100 * H;
1874
+ const ew = el.w / 100 * W;
1875
+ if (el.type === "text") {
1876
+ const anchor = el.align === "center" ? "middle" : el.align === "right" ? "end" : "start";
1877
+ const ax = el.align === "center" ? ex + ew / 2 : el.align === "right" ? ex + ew : ex;
1878
+ const fs = el.fontSize ?? 24;
1879
+ body += text(ax, ey + fs, el.text ?? "", fs, anchor, el.bold ? 700 : 400, el.color ?? slide.textColor ?? "#1f2328");
1880
+ } else if (el.src) {
1881
+ body += `<image href="${el.src}" x="${ex}" y="${ey}" width="${ew}" height="${el.h / 100 * H}" preserveAspectRatio="xMidYMid meet"/>`;
1882
+ }
1883
+ }
1884
+ return `<g transform="translate(0 ${y})">${body}</g>`;
1885
+ }).join("");
1886
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${totalH}" viewBox="0 0 ${W} ${totalH}">${frames}</svg>`;
1887
+ }
1888
+ function slidesToPrintHtml(data, title) {
1889
+ const slides2 = data.slides.length ? data.slides : [{ title: "Empty deck" }];
1890
+ const pages = slides2.map((slide) => {
1891
+ const bg = slide.background ?? "#ffffff";
1892
+ const fg = slide.textColor ?? "#1f2328";
1893
+ const els = resolveElements(slide).map((el) => {
1894
+ const box = `left:${el.x}%;top:${el.y}%;width:${el.w}%;height:${el.h}%`;
1895
+ if (el.type === "text") {
1896
+ 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")}`;
1897
+ return `<div class="el" style="${style}">${escapeXml(el.text ?? "")}</div>`;
1898
+ }
1899
+ const src = safeSrc(el.src);
1900
+ return src ? `<img class="el" style="${box}" src="${escapeAttr(src)}"/>` : "";
1901
+ }).join("");
1902
+ return `<section class="slide" style="background:${escapeAttr(bg)}">${els}</section>`;
1903
+ }).join("");
1904
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeXml(title)}</title><style>
1905
+ @page { size: 1280px 720px; margin: 0; }
1906
+ * { margin: 0; box-sizing: border-box; }
1907
+ body { font-family: Inter, Arial, sans-serif; }
1908
+ .slide { position: relative; width: 1280px; height: 720px; overflow: hidden; page-break-after: always; }
1909
+ .el { position: absolute; overflow: hidden; line-height: 1.25; }
1910
+ img.el { object-fit: contain; }
1911
+ </style></head><body>${pages}</body></html>`;
1912
+ }
1913
+ function escapeXml(value) {
1914
+ return String(value ?? "").replace(/[&<>]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[c]);
1915
+ }
1916
+ function escapeAttr(value) {
1917
+ return String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
1918
+ }
1919
+ function safeSrc(src) {
1920
+ const s = (src ?? "").trim();
1921
+ return /^(data:image\/|https?:\/\/|\/)/i.test(s) ? s : "";
1922
+ }
1923
+ function csvCell(value) {
1924
+ return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
1925
+ }
1926
+ function escapeHtml(text) {
1927
+ return text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
1928
+ }
1929
+ var EXPORT_CSS = `
1930
+ :root { color-scheme: light dark; }
1931
+ body { margin: 0; background: #fff; color: #1f2328;
1932
+ font: 16px/1.7 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
1933
+ .export { max-width: 760px; margin: 48px auto; padding: 0 24px; }
1934
+ h1,h2,h3 { line-height: 1.25; }
1935
+ pre { background: #f6f8fa; padding: 14px 16px; border-radius: 10px; overflow-x: auto; }
1936
+ code { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.9em; }
1937
+ table { border-collapse: collapse; width: 100%; }
1938
+ th, td { border: 1px solid #e5e7eb; padding: 8px 12px; text-align: left; }
1939
+ th { background: #f6f8fa; }
1940
+ svg { max-width: 100%; height: auto; }
1941
+ `.trim();
1942
+
1943
+ // src/export/pdf.ts
1944
+ function printToPdf(html) {
1945
+ const iframe = document.createElement("iframe");
1946
+ iframe.setAttribute("aria-hidden", "true");
1947
+ iframe.setAttribute("sandbox", "allow-same-origin allow-modals");
1948
+ Object.assign(iframe.style, { position: "fixed", right: "0", bottom: "0", width: "0", height: "0", border: "0" });
1949
+ const cleanup = () => setTimeout(() => iframe.remove(), 1e3);
1950
+ iframe.onload = () => {
1951
+ const win = iframe.contentWindow;
1952
+ if (!win) {
1953
+ iframe.remove();
1954
+ return;
1955
+ }
1956
+ win.addEventListener("afterprint", cleanup);
1957
+ setTimeout(() => {
1958
+ try {
1959
+ win.focus();
1960
+ win.print();
1961
+ } catch {
1962
+ iframe.remove();
1963
+ }
1964
+ }, 200);
1965
+ };
1966
+ iframe.srcdoc = html;
1967
+ document.body.appendChild(iframe);
1968
+ }
1969
+ var PDF_TYPES = /* @__PURE__ */ new Set(["html", "document", "chart", "slides"]);
1970
+ function ExportMenu({ artifact: artifact2, getRenderedHtml }) {
1971
+ const [open, setOpen] = useState(false);
1972
+ const stem = slugify(artifact2.title);
1973
+ const dataOptions = dataExporters[artifact2.type] ?? [];
1974
+ const exportHtml = () => {
1975
+ if (artifact2.type === "html") {
1976
+ downloadBlob(`${stem}.html`, "text/html", artifact2.data.html);
1977
+ } else {
1978
+ const html = getRenderedHtml();
1979
+ if (html == null) return;
1980
+ downloadBlob(`${stem}.html`, "text/html", toStandaloneHtml(artifact2.title, html));
1981
+ }
1982
+ setOpen(false);
1983
+ };
1984
+ const exportData = async (option) => {
1985
+ const content = await option.build(artifact2);
1986
+ downloadBlob(`${stem}.${option.extension}`, option.mime, content);
1987
+ setOpen(false);
1988
+ };
1989
+ const exportPdf = () => {
1990
+ if (artifact2.type === "slides") {
1991
+ printToPdf(slidesToPrintHtml(artifact2.data, artifact2.title));
1992
+ } else if (artifact2.type === "html") {
1993
+ printToPdf(artifact2.data.html);
1994
+ } else {
1995
+ const html = getRenderedHtml();
1996
+ if (html == null) return;
1997
+ printToPdf(toStandaloneHtml(artifact2.title, html));
1998
+ }
1999
+ setOpen(false);
2000
+ };
2001
+ const copyToFigma = async () => {
2002
+ const svg = slidesToSvg(artifact2.data);
2003
+ try {
2004
+ await navigator.clipboard.writeText(svg);
2005
+ } catch {
2006
+ }
2007
+ setOpen(false);
2008
+ };
2009
+ return /* @__PURE__ */ jsxs("div", { className: "cv-export", children: [
2010
+ /* @__PURE__ */ jsx(
2011
+ "button",
2012
+ {
2013
+ className: "cv-export__btn",
2014
+ onClick: () => setOpen((o) => !o),
2015
+ "aria-haspopup": "menu",
2016
+ "aria-expanded": open,
2017
+ children: "Export \u25BE"
2018
+ }
2019
+ ),
2020
+ open && /* @__PURE__ */ jsxs(Fragment, { children: [
2021
+ /* @__PURE__ */ jsx("div", { className: "cv-export__scrim", onClick: () => setOpen(false) }),
2022
+ /* @__PURE__ */ jsxs("div", { className: "cv-export__menu", role: "menu", children: [
2023
+ /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: exportHtml, children: [
2024
+ "HTML ",
2025
+ /* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: ".html" })
2026
+ ] }),
2027
+ PDF_TYPES.has(artifact2.type) && /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: exportPdf, children: [
2028
+ "PDF ",
2029
+ /* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: ".pdf" })
2030
+ ] }),
2031
+ artifact2.type === "slides" && /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: copyToFigma, children: [
2032
+ "Copy to Figma ",
2033
+ /* @__PURE__ */ jsx("span", { className: "cv-export__ext", children: "paste \u2318V" })
2034
+ ] }),
2035
+ dataOptions.map((option) => /* @__PURE__ */ jsxs("button", { role: "menuitem", onClick: () => exportData(option), children: [
2036
+ option.label,
2037
+ " ",
2038
+ /* @__PURE__ */ jsxs("span", { className: "cv-export__ext", children: [
2039
+ ".",
2040
+ option.extension
2041
+ ] })
2042
+ ] }, option.extension))
2043
+ ] })
2044
+ ] })
2045
+ ] });
2046
+ }
2047
+ var RendererBoundary = class extends Component {
2048
+ constructor() {
2049
+ super(...arguments);
2050
+ this.state = { error: null };
2051
+ }
2052
+ static getDerivedStateFromError(error) {
2053
+ return { error };
2054
+ }
2055
+ componentDidUpdate(prev) {
2056
+ if (prev.resetKey !== this.props.resetKey && this.state.error) {
2057
+ this.setState({ error: null });
2058
+ }
2059
+ }
2060
+ componentDidCatch(error, info) {
2061
+ console.error("[langchain-canvas] renderer error:", error, info.componentStack);
2062
+ }
2063
+ render() {
2064
+ if (this.state.error) {
2065
+ return /* @__PURE__ */ jsxs("div", { className: "cv-renderer-error", role: "alert", children: [
2066
+ /* @__PURE__ */ jsx("p", { className: "cv-renderer-error__title", children: "This artifact couldn\u2019t be displayed" }),
2067
+ /* @__PURE__ */ jsx("p", { className: "cv-renderer-error__detail", children: this.state.error.message })
2068
+ ] });
2069
+ }
2070
+ return this.props.children;
2071
+ }
2072
+ };
2073
+ function SelectionBar({ selections, onEdit, onClear }) {
2074
+ const [instruction, setInstruction] = useState("");
2075
+ const primary = selections[0];
2076
+ const label = selections.length === 1 ? primary.selector : `${selections.length} elements`;
2077
+ const submit = () => {
2078
+ const text = instruction.trim();
2079
+ if (!text) return;
2080
+ onEdit(text);
2081
+ setInstruction("");
2082
+ };
2083
+ return /* @__PURE__ */ jsxs("div", { className: "cv-selection", children: [
2084
+ /* @__PURE__ */ jsx("span", { className: "cv-selection__chip", title: primary?.text, children: label }),
2085
+ /* @__PURE__ */ jsx(
2086
+ "input",
2087
+ {
2088
+ className: "cv-selection__input",
2089
+ value: instruction,
2090
+ placeholder: selections.length === 1 ? `Edit this ${primary.tag}\u2026` : `Edit ${selections.length} elements\u2026`,
2091
+ autoFocus: true,
2092
+ onChange: (e) => setInstruction(e.target.value),
2093
+ onKeyDown: (e) => {
2094
+ if (e.key === "Enter") submit();
2095
+ if (e.key === "Escape") onClear();
2096
+ }
2097
+ }
2098
+ ),
2099
+ /* @__PURE__ */ jsx("button", { className: "cv-selection__apply", onClick: submit, disabled: !instruction.trim(), children: "Apply" }),
2100
+ /* @__PURE__ */ jsx("button", { className: "cv-selection__clear", onClick: onClear, "aria-label": "Clear selection", children: "\u2715" })
2101
+ ] });
2102
+ }
2103
+ var WEIGHTS = ["400", "500", "600", "700"];
2104
+ var ALIGNMENTS = ["left", "center", "right", "justify"];
2105
+ var GRADIENTS = [
2106
+ "linear-gradient(135deg,#667eea,#764ba2)",
2107
+ "linear-gradient(135deg,#f093fb,#f5576c)",
2108
+ "linear-gradient(135deg,#4facfe,#00f2fe)",
2109
+ "linear-gradient(135deg,#43e97b,#38f9d7)",
2110
+ "linear-gradient(135deg,#fa709a,#fee140)"
2111
+ ];
2112
+ function StylePanel({ selection }) {
2113
+ const send = useCanvasStore((s) => s.sendIframeCommand);
2114
+ const setSelections = useCanvasStore((s) => s.setSelections);
2115
+ const styles = selection.styles ?? {};
2116
+ const [color, setColor] = useState(toHex2(styles.color));
2117
+ const [background, setBackground] = useState(toHex2(styles.backgroundColor));
2118
+ const [fontSize, setFontSize] = useState(px(styles.fontSize, 16));
2119
+ const [fontWeight, setFontWeight] = useState(String(styles.fontWeight ?? "400"));
2120
+ const [textAlign, setTextAlign] = useState(styles.textAlign ?? "left");
2121
+ const [lineHeight, setLineHeight] = useState(px(styles.lineHeight, 0));
2122
+ const [letterSpacing, setLetterSpacing] = useState(px(styles.letterSpacing, 0));
2123
+ const [padding, setPadding] = useState(px(styles.padding, 0));
2124
+ const [radius, setRadius] = useState(px(styles.borderRadius, 0));
2125
+ const [width, setWidth] = useState(px(styles.width, 0));
2126
+ const bgFileRef = useRef(null);
2127
+ const dirty = useRef(false);
2128
+ const setStyle = (prop, value) => {
2129
+ dirty.current = true;
2130
+ send({ artifactId: selection.artifactId, type: "set_style", cid: selection.cid, prop, value });
2131
+ };
2132
+ const setBg = (value) => setStyle("background", value);
2133
+ const onBgFile = (file) => {
2134
+ if (!file) return;
2135
+ const reader = new FileReader();
2136
+ reader.onload = () => setBg(`url(${String(reader.result)}) center/cover no-repeat`);
2137
+ reader.readAsDataURL(file);
2138
+ };
2139
+ const commit = () => {
2140
+ if (!dirty.current) return;
2141
+ dirty.current = false;
2142
+ send({ artifactId: selection.artifactId, type: "commit", cid: selection.cid });
2143
+ };
2144
+ const close = () => {
2145
+ commit();
2146
+ setSelections([]);
2147
+ };
2148
+ useEffect(() => commit, []);
2149
+ return /* @__PURE__ */ jsxs("div", { className: "cv-style", onBlur: commit, children: [
2150
+ /* @__PURE__ */ jsx("div", { className: "cv-style__title", children: "Style" }),
2151
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2152
+ /* @__PURE__ */ jsx("span", { children: "Text" }),
2153
+ /* @__PURE__ */ jsx(
2154
+ "input",
2155
+ {
2156
+ type: "color",
2157
+ value: color,
2158
+ onChange: (e) => {
2159
+ setColor(e.target.value);
2160
+ setStyle("color", e.target.value);
2161
+ }
2162
+ }
2163
+ )
2164
+ ] }),
2165
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2166
+ /* @__PURE__ */ jsx("span", { children: "Background" }),
2167
+ /* @__PURE__ */ jsx(
2168
+ "input",
2169
+ {
2170
+ type: "color",
2171
+ value: background,
2172
+ onChange: (e) => {
2173
+ setBackground(e.target.value);
2174
+ setStyle("backgroundColor", e.target.value);
2175
+ }
2176
+ }
2177
+ )
2178
+ ] }),
2179
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2180
+ /* @__PURE__ */ jsx("span", { children: "Size" }),
2181
+ /* @__PURE__ */ jsx(
2182
+ "input",
2183
+ {
2184
+ type: "number",
2185
+ min: 8,
2186
+ max: 96,
2187
+ value: fontSize,
2188
+ onChange: (e) => {
2189
+ const next = Number(e.target.value);
2190
+ setFontSize(next);
2191
+ setStyle("fontSize", `${next}px`);
2192
+ }
2193
+ }
2194
+ )
2195
+ ] }),
2196
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2197
+ /* @__PURE__ */ jsx("span", { children: "Weight" }),
2198
+ /* @__PURE__ */ jsx(
2199
+ "select",
2200
+ {
2201
+ value: fontWeight,
2202
+ onChange: (e) => {
2203
+ setFontWeight(e.target.value);
2204
+ setStyle("fontWeight", e.target.value);
2205
+ },
2206
+ children: WEIGHTS.map((w) => /* @__PURE__ */ jsx("option", { value: w, children: w }, w))
2207
+ }
2208
+ )
2209
+ ] }),
2210
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2211
+ /* @__PURE__ */ jsx("span", { children: "Align" }),
2212
+ /* @__PURE__ */ jsx(
2213
+ "select",
2214
+ {
2215
+ value: textAlign,
2216
+ onChange: (e) => {
2217
+ setTextAlign(e.target.value);
2218
+ setStyle("textAlign", e.target.value);
2219
+ },
2220
+ children: ALIGNMENTS.map((a) => /* @__PURE__ */ jsx("option", { value: a, children: a }, a))
2221
+ }
2222
+ )
2223
+ ] }),
2224
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2225
+ /* @__PURE__ */ jsx("span", { children: "Line height" }),
2226
+ /* @__PURE__ */ jsx(
2227
+ "input",
2228
+ {
2229
+ type: "number",
2230
+ min: 0,
2231
+ max: 4,
2232
+ step: 0.1,
2233
+ value: lineHeight,
2234
+ onChange: (e) => {
2235
+ const n = Number(e.target.value);
2236
+ setLineHeight(n);
2237
+ setStyle("lineHeight", n ? `${n}` : "normal");
2238
+ }
2239
+ }
2240
+ )
2241
+ ] }),
2242
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2243
+ /* @__PURE__ */ jsx("span", { children: "Letter spacing" }),
2244
+ /* @__PURE__ */ jsx(
2245
+ "input",
2246
+ {
2247
+ type: "number",
2248
+ min: -5,
2249
+ max: 20,
2250
+ step: 0.5,
2251
+ value: letterSpacing,
2252
+ onChange: (e) => {
2253
+ const n = Number(e.target.value);
2254
+ setLetterSpacing(n);
2255
+ setStyle("letterSpacing", `${n}px`);
2256
+ }
2257
+ }
2258
+ )
2259
+ ] }),
2260
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2261
+ /* @__PURE__ */ jsx("span", { children: "Padding" }),
2262
+ /* @__PURE__ */ jsx(
2263
+ "input",
2264
+ {
2265
+ type: "number",
2266
+ min: 0,
2267
+ max: 120,
2268
+ value: padding,
2269
+ onChange: (e) => {
2270
+ const n = Number(e.target.value);
2271
+ setPadding(n);
2272
+ setStyle("padding", `${n}px`);
2273
+ }
2274
+ }
2275
+ )
2276
+ ] }),
2277
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2278
+ /* @__PURE__ */ jsx("span", { children: "Radius" }),
2279
+ /* @__PURE__ */ jsx(
2280
+ "input",
2281
+ {
2282
+ type: "number",
2283
+ min: 0,
2284
+ max: 80,
2285
+ value: radius,
2286
+ onChange: (e) => {
2287
+ const n = Number(e.target.value);
2288
+ setRadius(n);
2289
+ setStyle("borderRadius", `${n}px`);
2290
+ }
2291
+ }
2292
+ )
2293
+ ] }),
2294
+ /* @__PURE__ */ jsxs("label", { className: "cv-style__row", children: [
2295
+ /* @__PURE__ */ jsx("span", { children: "Width" }),
2296
+ /* @__PURE__ */ jsx(
2297
+ "input",
2298
+ {
2299
+ type: "number",
2300
+ min: 0,
2301
+ max: 2e3,
2302
+ value: width,
2303
+ onChange: (e) => {
2304
+ const n = Number(e.target.value);
2305
+ setWidth(n);
2306
+ setStyle("width", n ? `${n}px` : "auto");
2307
+ }
2308
+ }
2309
+ )
2310
+ ] }),
2311
+ /* @__PURE__ */ jsxs("div", { className: "cv-style__bg", children: [
2312
+ /* @__PURE__ */ jsx("span", { children: "Background" }),
2313
+ /* @__PURE__ */ jsxs("div", { className: "cv-style__grads", children: [
2314
+ GRADIENTS.map((g) => /* @__PURE__ */ jsx("button", { type: "button", className: "cv-style__grad", style: { backgroundImage: g }, title: "Gradient", onClick: () => setBg(g) }, g)),
2315
+ /* @__PURE__ */ jsx("button", { type: "button", className: "cv-style__grad cv-style__grad--img", title: "Background image", onClick: () => bgFileRef.current?.click(), children: "\u{1F5BC}" }),
2316
+ /* @__PURE__ */ jsx("button", { type: "button", className: "cv-style__grad cv-style__grad--none", title: "Solid color (clear image)", onClick: () => {
2317
+ setBackground(background);
2318
+ setBg(background);
2319
+ }, children: "\u2300" })
2320
+ ] }),
2321
+ /* @__PURE__ */ jsx("input", { ref: bgFileRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => {
2322
+ onBgFile(e.target.files?.[0]);
2323
+ e.target.value = "";
2324
+ } })
2325
+ ] }),
2326
+ /* @__PURE__ */ jsx("button", { className: "cv-style__done", onClick: close, children: "Done" })
2327
+ ] });
2328
+ }
2329
+ function px(value, fallback) {
2330
+ if (!value) return fallback;
2331
+ const n = parseFloat(value);
2332
+ return Number.isFinite(n) ? n : fallback;
2333
+ }
2334
+ function toHex2(value) {
2335
+ if (!value) return "#000000";
2336
+ if (value.startsWith("#")) return value;
2337
+ const parts = value.match(/\d+/g);
2338
+ if (!parts || parts.length < 3) return "#000000";
2339
+ return "#" + parts.slice(0, 3).map((n) => Number(n).toString(16).padStart(2, "0")).join("");
2340
+ }
2341
+ function useCanvasImport() {
2342
+ const api = useCanvasStoreApi();
2343
+ const importFiles = useCallback(
2344
+ async (files) => {
2345
+ let lastId = null;
2346
+ for (const file of files) {
2347
+ if (!canImport(file)) continue;
2348
+ try {
2349
+ const events = await importFile(file);
2350
+ api.getState().applyEvents(events);
2351
+ const created = events.find((e) => e.type === "canvas.create");
2352
+ if (created && created.type === "canvas.create") {
2353
+ lastId = created.artifact.id;
2354
+ api.getState().setActiveArtifact(lastId);
2355
+ }
2356
+ } catch (err) {
2357
+ console.error("[langchain-canvas] import failed:", file.name, err);
2358
+ }
2359
+ }
2360
+ return lastId;
2361
+ },
2362
+ [api]
2363
+ );
2364
+ return { importFiles, canImport };
2365
+ }
2366
+ var ACCEPT = IMPORTABLE_EXTENSIONS.join(",");
2367
+ function Canvas({ registry = builtinRenderers, emptyState, onEditElement }) {
2368
+ return /* @__PURE__ */ jsx(CanvasRegistryProvider, { registry, children: /* @__PURE__ */ jsx(CanvasPanel, { emptyState, onEditElement }) });
2369
+ }
2370
+ function CanvasPanel({ emptyState, onEditElement }) {
2371
+ const { artifacts, order, activeId } = useCanvasStore((s) => s.canvas);
2372
+ const history = useCanvasStore((s) => s.canvas.history);
2373
+ const setActive = useCanvasStore((s) => s.setActiveArtifact);
2374
+ const selections = useCanvasStore((s) => s.selections);
2375
+ const setSelections = useCanvasStore((s) => s.setSelections);
2376
+ const { importFiles } = useCanvasImport();
2377
+ const [dropping, setDropping] = useState(false);
2378
+ useEffect(() => {
2379
+ if (!selections.length) return;
2380
+ const onKey = (e) => {
2381
+ if (e.key === "Escape") setSelections([]);
2382
+ };
2383
+ window.addEventListener("keydown", onKey);
2384
+ return () => window.removeEventListener("keydown", onKey);
2385
+ }, [selections.length, setSelections]);
2386
+ const active = activeId ? artifacts[activeId] : void 0;
2387
+ const dropProps = {
2388
+ onDragOver: (e) => {
2389
+ if (Array.from(e.dataTransfer.types).includes("Files")) {
2390
+ e.preventDefault();
2391
+ setDropping(true);
2392
+ }
2393
+ },
2394
+ onDragLeave: (e) => {
2395
+ if (e.currentTarget === e.target) setDropping(false);
2396
+ },
2397
+ onDrop: (e) => {
2398
+ e.preventDefault();
2399
+ setDropping(false);
2400
+ if (e.dataTransfer.files.length) void importFiles(e.dataTransfer.files);
2401
+ }
2402
+ };
2403
+ const dropOverlay = dropping ? /* @__PURE__ */ jsx("div", { className: "cv-canvas__drop", children: "Drop to open on the canvas" }) : null;
2404
+ if (!active) {
2405
+ return /* @__PURE__ */ jsxs("aside", { className: "cv-canvas cv-canvas--empty", ...dropProps, children: [
2406
+ emptyState ?? /* @__PURE__ */ jsx(EmptyState, { onOpenFiles: importFiles }),
2407
+ dropOverlay
2408
+ ] });
2409
+ }
2410
+ const versions = history[active.id] ?? [active];
2411
+ const showSelection = Boolean(onEditElement) && selections.length > 0 && selections[0].artifactId === active.id;
2412
+ return /* @__PURE__ */ jsxs("aside", { className: "cv-canvas", ...dropProps, children: [
2413
+ dropOverlay,
2414
+ order.length > 1 && /* @__PURE__ */ jsx("nav", { className: "cv-tabs", role: "tablist", children: order.map((id) => /* @__PURE__ */ jsx(
2415
+ "button",
2416
+ {
2417
+ role: "tab",
2418
+ "aria-selected": id === activeId,
2419
+ className: `cv-tab ${id === activeId ? "is-active" : ""}`,
2420
+ onClick: () => setActive(id),
2421
+ children: artifacts[id].title
2422
+ },
2423
+ id
2424
+ )) }),
2425
+ /* @__PURE__ */ jsx(ArtifactView, { artifact: active, versions }, active.id),
2426
+ showSelection && onEditElement && /* @__PURE__ */ jsxs(Fragment, { children: [
2427
+ active.type === "html" && selections.length === 1 && /* @__PURE__ */ jsx(StylePanel, { selection: selections[0] }, selections[0].cid),
2428
+ /* @__PURE__ */ jsx(SelectionBar, { selections, onEdit: onEditElement, onClear: () => setSelections([]) })
2429
+ ] })
2430
+ ] });
2431
+ }
2432
+ function ArtifactView({ artifact: artifact2, versions }) {
2433
+ const [viewIndex, setViewIndex] = useState(null);
2434
+ const bodyRef = useRef(null);
2435
+ const shown = viewIndex === null ? artifact2 : versions[viewIndex];
2436
+ const Renderer = useRenderer(shown.type);
2437
+ const getRenderedHtml = () => {
2438
+ const node = bodyRef.current;
2439
+ if (!node) return null;
2440
+ const clone = node.cloneNode(true);
2441
+ clone.querySelectorAll(".cv-edit-toolbar, .cv-slides__nav, .cv-selection, .cv-style, .cv-chrome").forEach((el) => el.remove());
2442
+ clone.querySelectorAll("[contenteditable]").forEach((el) => el.removeAttribute("contenteditable"));
2443
+ return clone.innerHTML;
2444
+ };
2445
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2446
+ /* @__PURE__ */ jsxs("header", { className: "cv-header", children: [
2447
+ /* @__PURE__ */ jsxs("div", { className: "cv-header__title", children: [
2448
+ /* @__PURE__ */ jsx("h2", { children: shown.title }),
2449
+ /* @__PURE__ */ jsx(StatusBadge, { status: shown.status })
2450
+ ] }),
2451
+ /* @__PURE__ */ jsxs("div", { className: "cv-header__actions", children: [
2452
+ /* @__PURE__ */ jsx(UndoRedo, {}),
2453
+ versions.length > 1 && /* @__PURE__ */ jsx(
2454
+ VersionRail,
2455
+ {
2456
+ total: versions.length,
2457
+ index: viewIndex ?? versions.length - 1,
2458
+ onSelect: (i) => setViewIndex(i === versions.length - 1 ? null : i)
2459
+ }
2460
+ ),
2461
+ /* @__PURE__ */ jsx(ExportMenu, { artifact: shown, getRenderedHtml })
2462
+ ] })
2463
+ ] }),
2464
+ /* @__PURE__ */ jsx("div", { className: `cv-body${shown.type === "table" ? " cv-body--flush" : ""}`, ref: bodyRef, children: Renderer ? /* @__PURE__ */ jsx(RendererBoundary, { resetKey: `${shown.id}:${shown.version}`, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-fallback", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Renderer, { artifact: shown }) }) }) : /* @__PURE__ */ jsxs("div", { className: "cv-fallback", children: [
2465
+ "No renderer registered for type \u201C",
2466
+ shown.type,
2467
+ "\u201D."
2468
+ ] }) })
2469
+ ] });
2470
+ }
2471
+ function UndoRedo() {
2472
+ const undo = useCanvasStore((s) => s.undo);
2473
+ const redo = useCanvasStore((s) => s.redo);
2474
+ const canUndo = useCanvasStore((s) => s.undoStack.length > 0);
2475
+ const canRedo = useCanvasStore((s) => s.redoStack.length > 0);
2476
+ useEffect(() => {
2477
+ const onKey = (e) => {
2478
+ if (!(e.metaKey || e.ctrlKey)) return;
2479
+ const key = e.key.toLowerCase();
2480
+ if (key !== "z" && key !== "y") return;
2481
+ const ae = document.activeElement;
2482
+ if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return;
2483
+ e.preventDefault();
2484
+ if (key === "y" || key === "z" && e.shiftKey) redo();
2485
+ else undo();
2486
+ };
2487
+ window.addEventListener("keydown", onKey);
2488
+ return () => window.removeEventListener("keydown", onKey);
2489
+ }, [undo, redo]);
2490
+ return /* @__PURE__ */ jsxs("div", { className: "cv-undo", role: "group", "aria-label": "Undo and redo", children: [
2491
+ /* @__PURE__ */ jsx("button", { onClick: undo, disabled: !canUndo, title: "Undo (\u2318Z)", "aria-label": "Undo", children: "\u21B6" }),
2492
+ /* @__PURE__ */ jsx("button", { onClick: redo, disabled: !canRedo, title: "Redo (\u2318\u21E7Z)", "aria-label": "Redo", children: "\u21B7" })
2493
+ ] });
2494
+ }
2495
+ function VersionRail({ total, index, onSelect }) {
2496
+ return /* @__PURE__ */ jsxs("div", { className: "cv-versions", role: "group", "aria-label": "Version history", children: [
2497
+ /* @__PURE__ */ jsx("button", { className: "cv-versions__nav", disabled: index === 0, onClick: () => onSelect(index - 1), "aria-label": "Previous version", children: "\u2039" }),
2498
+ /* @__PURE__ */ jsxs("span", { className: "cv-versions__label", children: [
2499
+ "v",
2500
+ index + 1,
2501
+ " / ",
2502
+ total
2503
+ ] }),
2504
+ /* @__PURE__ */ jsx(
2505
+ "button",
2506
+ {
2507
+ className: "cv-versions__nav",
2508
+ disabled: index === total - 1,
2509
+ onClick: () => onSelect(index + 1),
2510
+ "aria-label": "Next version",
2511
+ children: "\u203A"
2512
+ }
2513
+ )
2514
+ ] });
2515
+ }
2516
+ function StatusBadge({ status }) {
2517
+ const label = status === "streaming" ? "Writing\u2026" : status === "error" ? "Error" : "Ready";
2518
+ return /* @__PURE__ */ jsx("span", { className: `cv-badge cv-badge--${status}`, children: label });
2519
+ }
2520
+ function EmptyState({ onOpenFiles }) {
2521
+ const inputRef = useRef(null);
2522
+ return /* @__PURE__ */ jsxs("div", { className: "cv-empty", children: [
2523
+ /* @__PURE__ */ jsx("p", { className: "cv-empty__title", children: "Nothing on the canvas yet" }),
2524
+ /* @__PURE__ */ jsx("p", { className: "cv-empty__hint", children: "Ask for a report or a chart \u2014 or open a file to edit it here." }),
2525
+ onOpenFiles && /* @__PURE__ */ jsxs(Fragment, { children: [
2526
+ /* @__PURE__ */ jsx("button", { className: "cv-empty__open", onClick: () => inputRef.current?.click(), children: "Open file" }),
2527
+ /* @__PURE__ */ jsx(
2528
+ "input",
2529
+ {
2530
+ ref: inputRef,
2531
+ type: "file",
2532
+ accept: ACCEPT,
2533
+ multiple: true,
2534
+ hidden: true,
2535
+ onChange: (e) => {
2536
+ if (e.target.files?.length) onOpenFiles(e.target.files);
2537
+ e.target.value = "";
2538
+ }
2539
+ }
2540
+ ),
2541
+ /* @__PURE__ */ jsx("p", { className: "cv-empty__formats", children: "CSV \xB7 Excel \xB7 Markdown \xB7 HTML \xB7 JSON" })
2542
+ ] })
2543
+ ] });
2544
+ }
2545
+ var META = {
2546
+ html: { icon: "\u{1F310}", label: "Web page" },
2547
+ document: { icon: "\u{1F4C4}", label: "Word document" },
2548
+ chart: { icon: "\u{1F4CA}", label: "Chart" },
2549
+ table: { icon: "\u{1F522}", label: "Excel sheet" },
2550
+ slides: { icon: "\u{1F4FD}\uFE0F", label: "PowerPoint deck" }
2551
+ };
2552
+ function ArtifactCard({ artifactId }) {
2553
+ const artifact2 = useCanvasStore((s) => s.canvas.artifacts[artifactId]);
2554
+ const setActive = useCanvasStore((s) => s.setActiveArtifact);
2555
+ if (!artifact2) return null;
2556
+ const meta = META[artifact2.type] ?? { icon: "\u{1F4CE}", label: artifact2.type };
2557
+ return /* @__PURE__ */ jsxs("button", { className: "cv-card", onClick: () => setActive(artifact2.id), title: "Open on the canvas", children: [
2558
+ /* @__PURE__ */ jsx("span", { className: "cv-card__icon", children: meta.icon }),
2559
+ /* @__PURE__ */ jsxs("span", { className: "cv-card__meta", children: [
2560
+ /* @__PURE__ */ jsx("b", { children: artifact2.title }),
2561
+ /* @__PURE__ */ jsxs("span", { children: [
2562
+ meta.label,
2563
+ " \xB7 ",
2564
+ artifact2.status === "streaming" ? "writing\u2026" : "open \u2192"
2565
+ ] })
2566
+ ] })
2567
+ ] });
2568
+ }
2569
+
2570
+ export { ArtifactCard, Canvas, CanvasRegistryProvider, ChartRenderer, DocumentRenderer, ExportMenu, HtmlRenderer, IMPORTABLE_EXTENSIONS, INSPECTOR_MARK, STYLE_PROPS, SelectionBar, SlidesRenderer, StylePanel, TableRenderer, builtinRenderers, canImport, dataExporters, downloadBlob, importFile, mergeRegistries, mockStream, parseCsv, parseSSE, printToPdf, scenarios, slidesToPrintHtml, slugify, streamChat, toStandaloneHtml, useCanvasImport, useCanvasReplay, useCanvasStream, useRenderer, withInspector };