@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.
@@ -0,0 +1,399 @@
1
+ import { resolveElements } from './chunk-6L3AL6W4.js';
2
+ import { useArtifactPatch } from './chunk-HHJWIO2C.js';
3
+ import './chunk-PSIT32I5.js';
4
+ import { useState, useRef, useEffect } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
8
+ var dupSeq = 0;
9
+ var dupId = (base) => `${base}_c${Date.now().toString(36)}${dupSeq++}`;
10
+ var SNAP = 1.2;
11
+ function snapAxis(pos, size, targets) {
12
+ const anchors = [pos, pos + size / 2, pos + size];
13
+ let best = null;
14
+ for (const anchor of anchors) {
15
+ for (const t of targets) {
16
+ const delta = t - anchor;
17
+ if (Math.abs(delta) <= SNAP && (!best || Math.abs(delta) < Math.abs(best.delta))) {
18
+ best = { delta, guide: t };
19
+ }
20
+ }
21
+ }
22
+ return best ? { pos: pos + best.delta, guide: best.guide } : { pos, guide: null };
23
+ }
24
+ function FreeSlide({ elements, onChange }) {
25
+ const slideRef = useRef(null);
26
+ const [els, setEls] = useState(elements);
27
+ const [selected, setSelected] = useState(null);
28
+ const [editingId, setEditingId] = useState(null);
29
+ const [guides, setGuides] = useState({ x: null, y: null });
30
+ const drag = useRef(null);
31
+ useEffect(() => {
32
+ if (!drag.current) setEls(elements);
33
+ }, [elements]);
34
+ const commit = (next) => {
35
+ setEls(next);
36
+ onChange(next);
37
+ };
38
+ const updateEl = (id, partial) => commit(els.map((el) => el.id === id ? { ...el, ...partial } : el));
39
+ const duplicate = (el) => {
40
+ const copy = {
41
+ ...el,
42
+ id: dupId(el.id),
43
+ x: Math.min(el.x + 4, 100 - el.w),
44
+ y: Math.min(el.y + 4, 100 - el.h)
45
+ };
46
+ commit([...els, copy]);
47
+ setSelected(copy.id);
48
+ };
49
+ const zorder = (id, dir) => {
50
+ const i = els.findIndex((e) => e.id === id);
51
+ const j = i + dir;
52
+ if (i < 0 || j < 0 || j >= els.length) return;
53
+ const next = [...els];
54
+ [next[i], next[j]] = [next[j], next[i]];
55
+ commit(next);
56
+ };
57
+ const onDown = (e, el, mode) => {
58
+ if (editingId === el.id && mode === "move") return;
59
+ e.preventDefault();
60
+ e.stopPropagation();
61
+ setSelected(el.id);
62
+ drag.current = { id: el.id, mode, sx: e.clientX, sy: e.clientY, orig: { ...el } };
63
+ e.currentTarget.setPointerCapture(e.pointerId);
64
+ };
65
+ const onMove = (e) => {
66
+ const d = drag.current;
67
+ const rect = slideRef.current?.getBoundingClientRect();
68
+ if (!d || !rect) return;
69
+ const dx = (e.clientX - d.sx) / rect.width * 100;
70
+ const dy = (e.clientY - d.sy) / rect.height * 100;
71
+ if (d.mode === "resize") {
72
+ setGuides({ x: null, y: null });
73
+ setEls((prev) => prev.map((el) => el.id === d.id ? { ...el, w: clamp(d.orig.w + dx, 6, 100 - el.x), h: clamp(d.orig.h + dy, 5, 100 - el.y) } : el));
74
+ return;
75
+ }
76
+ const others = els.filter((el) => el.id !== d.id);
77
+ const xTargets = [0, 50, 100, ...others.flatMap((o) => [o.x, o.x + o.w / 2, o.x + o.w])];
78
+ const yTargets = [0, 50, 100, ...others.flatMap((o) => [o.y, o.y + o.h / 2, o.y + o.h])];
79
+ const rawX = clamp(d.orig.x + dx, 0, 100 - d.orig.w);
80
+ const rawY = clamp(d.orig.y + dy, 0, 100 - d.orig.h);
81
+ const sx = snapAxis(rawX, d.orig.w, xTargets);
82
+ const sy = snapAxis(rawY, d.orig.h, yTargets);
83
+ setGuides({ x: sx.guide, y: sy.guide });
84
+ setEls(
85
+ (prev) => prev.map(
86
+ (el) => el.id === d.id ? { ...el, x: clamp(sx.pos, 0, 100 - el.w), y: clamp(sy.pos, 0, 100 - el.h) } : el
87
+ )
88
+ );
89
+ };
90
+ const onUp = () => {
91
+ if (drag.current) {
92
+ drag.current = null;
93
+ setGuides({ x: null, y: null });
94
+ onChange(els);
95
+ }
96
+ };
97
+ useEffect(() => {
98
+ if (!selected) return;
99
+ const onKey = (e) => {
100
+ if (editingId) return;
101
+ const ae = document.activeElement;
102
+ if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return;
103
+ const el = els.find((x) => x.id === selected);
104
+ if (!el) return;
105
+ const step = e.shiftKey ? 5 : 1;
106
+ const nudge = (ddx, ddy) => {
107
+ e.preventDefault();
108
+ commit(els.map((x) => x.id === selected ? { ...x, x: clamp(x.x + ddx, 0, 100 - x.w), y: clamp(x.y + ddy, 0, 100 - x.h) } : x));
109
+ };
110
+ if (e.key === "ArrowLeft") nudge(-step, 0);
111
+ else if (e.key === "ArrowRight") nudge(step, 0);
112
+ else if (e.key === "ArrowUp") nudge(0, -step);
113
+ else if (e.key === "ArrowDown") nudge(0, step);
114
+ else if (e.key === "Delete" || e.key === "Backspace") {
115
+ e.preventDefault();
116
+ commit(els.filter((x) => x.id !== selected));
117
+ setSelected(null);
118
+ } else if ((e.metaKey || e.ctrlKey) && (e.key === "d" || e.key === "D")) {
119
+ e.preventDefault();
120
+ duplicate(el);
121
+ } else if (e.key === "Escape") {
122
+ setSelected(null);
123
+ }
124
+ };
125
+ window.addEventListener("keydown", onKey);
126
+ return () => window.removeEventListener("keydown", onKey);
127
+ }, [selected, editingId, els]);
128
+ return /* @__PURE__ */ jsxs(
129
+ "div",
130
+ {
131
+ className: "cv-free",
132
+ ref: slideRef,
133
+ onPointerMove: onMove,
134
+ onPointerUp: onUp,
135
+ onPointerLeave: onUp,
136
+ onClick: () => {
137
+ setSelected(null);
138
+ setEditingId(null);
139
+ },
140
+ children: [
141
+ guides.x !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--v", style: { left: `${guides.x}%` } }),
142
+ guides.y !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--h", style: { top: `${guides.y}%` } }),
143
+ els.map((el) => /* @__PURE__ */ jsxs(
144
+ "div",
145
+ {
146
+ className: `cv-free__el ${selected === el.id ? "is-selected" : ""}`,
147
+ style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` },
148
+ onPointerDown: (e) => onDown(e, el, "move"),
149
+ onDoubleClick: (e) => {
150
+ if (el.type === "text") {
151
+ e.stopPropagation();
152
+ setEditingId(el.id);
153
+ }
154
+ },
155
+ onClick: (e) => {
156
+ e.stopPropagation();
157
+ setSelected(el.id);
158
+ },
159
+ children: [
160
+ el.type === "text" ? /* @__PURE__ */ jsx(
161
+ "div",
162
+ {
163
+ className: "cv-free__text",
164
+ contentEditable: editingId === el.id,
165
+ suppressContentEditableWarning: true,
166
+ style: {
167
+ fontSize: el.fontSize ?? 24,
168
+ fontWeight: el.bold ? 700 : 400,
169
+ color: el.color,
170
+ textAlign: el.align ?? "left"
171
+ },
172
+ onBlur: (e) => {
173
+ setEditingId(null);
174
+ updateEl(el.id, { text: e.currentTarget.textContent ?? "" });
175
+ },
176
+ children: el.text
177
+ }
178
+ ) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "", draggable: false }),
179
+ selected === el.id && el.type === "text" && /* @__PURE__ */ jsxs("div", { className: `cv-free__fmt ${el.y < 16 ? "cv-free__fmt--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(), children: [
180
+ /* @__PURE__ */ jsx("button", { className: el.bold ? "is-on" : "", onClick: () => updateEl(el.id, { bold: !el.bold }), title: "Bold", children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
181
+ /* @__PURE__ */ jsx(
182
+ "input",
183
+ {
184
+ type: "number",
185
+ min: 8,
186
+ max: 120,
187
+ value: el.fontSize ?? 24,
188
+ onChange: (e) => updateEl(el.id, { fontSize: Number(e.target.value) }),
189
+ title: "Font size"
190
+ }
191
+ ),
192
+ /* @__PURE__ */ jsx("input", { type: "color", value: el.color ?? "#1f2328", onChange: (e) => updateEl(el.id, { color: e.target.value }), title: "Text color" }),
193
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "left" }), title: "Align left", children: "\u27F8" }),
194
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
195
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
196
+ ] }),
197
+ selected === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
198
+ /* @__PURE__ */ jsx("span", { className: "cv-free__resize", onPointerDown: (e) => onDown(e, el, "resize") }),
199
+ /* @__PURE__ */ jsxs("div", { className: `cv-free__ctl ${el.y < 16 ? "cv-free__ctl--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), children: [
200
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
201
+ e.stopPropagation();
202
+ duplicate(el);
203
+ }, title: "Duplicate", children: "\u29C9" }),
204
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
205
+ e.stopPropagation();
206
+ zorder(el.id, 1);
207
+ }, title: "Bring forward", children: "\u2191" }),
208
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
209
+ e.stopPropagation();
210
+ zorder(el.id, -1);
211
+ }, title: "Send back", children: "\u2193" }),
212
+ /* @__PURE__ */ jsx("button", { className: "cv-free__ctl-del", onClick: (e) => {
213
+ e.stopPropagation();
214
+ commit(els.filter((x) => x.id !== el.id));
215
+ }, title: "Delete", children: "\xD7" })
216
+ ] })
217
+ ] })
218
+ ]
219
+ },
220
+ el.id
221
+ ))
222
+ ]
223
+ }
224
+ );
225
+ }
226
+ var THEMES = [
227
+ { id: "light", label: "Light", bg: "#ffffff", text: "#1f2328" },
228
+ { id: "dark", label: "Dark", bg: "#14171f", text: "#f0f2f5" },
229
+ { id: "midnight", label: "Midnight", bg: "#0b1020", text: "#e6e8ef" },
230
+ { id: "sunset", label: "Sunset", bg: "#2b1a2e", text: "#ffe8d6" },
231
+ { id: "mint", label: "Mint", bg: "#0f2a24", text: "#d7f5ec" }
232
+ ];
233
+ var elementSeq = 0;
234
+ var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
235
+ function SlidesRenderer({ artifact }) {
236
+ const slides = artifact.data.slides ?? [];
237
+ const patch = useArtifactPatch(artifact.id);
238
+ const [index, setIndex] = useState(0);
239
+ const [dragIndex, setDragIndex] = useState(null);
240
+ const [presenting, setPresenting] = useState(false);
241
+ const imgRef = useRef(null);
242
+ useEffect(() => {
243
+ if (!presenting) return;
244
+ const onKey = (e) => {
245
+ if (e.key === "ArrowRight" || e.key === " ") setIndex((i) => Math.min(i + 1, slides.length - 1));
246
+ else if (e.key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
247
+ else if (e.key === "Escape") setPresenting(false);
248
+ };
249
+ window.addEventListener("keydown", onKey);
250
+ return () => window.removeEventListener("keydown", onKey);
251
+ }, [presenting]);
252
+ if (slides.length === 0) {
253
+ return /* @__PURE__ */ jsx("div", { className: "cv-deck cv-deck--empty", children: "No slides yet\u2026" });
254
+ }
255
+ const at = Math.min(index, slides.length - 1);
256
+ const slide = slides[at];
257
+ const slideStyle = {
258
+ ...slide.background ? { background: slide.background } : {},
259
+ ...slide.textColor ? { color: slide.textColor } : {}
260
+ };
261
+ const setSlides = (next) => patch({ slides: next });
262
+ const update = (partial) => setSlides(slides.map((s, i) => i === at ? { ...s, ...partial } : s));
263
+ const addSlide = () => {
264
+ const next = [...slides];
265
+ next.splice(at + 1, 0, { elements: [{ id: newElementId(), type: "text", x: 8, y: 10, w: 80, h: 14, text: "New slide", fontSize: 36, bold: true }] });
266
+ setSlides(next);
267
+ setIndex(at + 1);
268
+ };
269
+ const duplicateSlide = () => {
270
+ const next = [...slides];
271
+ next.splice(at + 1, 0, { ...slide, elements: resolveElements(slide).map((e) => ({ ...e })) });
272
+ setSlides(next);
273
+ setIndex(at + 1);
274
+ };
275
+ const deleteSlide = () => {
276
+ if (slides.length === 1) return;
277
+ setSlides(slides.filter((_, i) => i !== at));
278
+ setIndex(Math.max(0, at - 1));
279
+ };
280
+ const moveSlide = (dir) => {
281
+ const j = at + dir;
282
+ if (j < 0 || j >= slides.length) return;
283
+ const next = [...slides];
284
+ [next[at], next[j]] = [next[j], next[at]];
285
+ setSlides(next);
286
+ setIndex(j);
287
+ };
288
+ const reorder = (from, to) => {
289
+ if (from === to) return;
290
+ const next = [...slides];
291
+ const [moved] = next.splice(from, 1);
292
+ next.splice(to, 0, moved);
293
+ setSlides(next);
294
+ setIndex(to);
295
+ };
296
+ const addElement = (el) => update({ elements: [...resolveElements(slide), { ...el, id: newElementId() }] });
297
+ const addTextEl = () => addElement({ type: "text", x: 12, y: 16, w: 45, h: 16, text: "Text", fontSize: 24 });
298
+ const addImageEl = (file) => {
299
+ if (!file) return;
300
+ const reader = new FileReader();
301
+ reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
302
+ reader.readAsDataURL(file);
303
+ };
304
+ return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
305
+ /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
306
+ slides.map((s, i) => /* @__PURE__ */ jsx(
307
+ "div",
308
+ {
309
+ className: `cv-deck__thumb-wrap ${i === at ? "is-active" : ""} ${dragIndex === i ? "is-dragging" : ""}`,
310
+ draggable: true,
311
+ onDragStart: () => setDragIndex(i),
312
+ onDragOver: (e) => e.preventDefault(),
313
+ onDrop: () => {
314
+ if (dragIndex !== null) reorder(dragIndex, i);
315
+ setDragIndex(null);
316
+ },
317
+ onDragEnd: () => setDragIndex(null),
318
+ children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
319
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
320
+ /* @__PURE__ */ jsx("div", { className: "cv-deck__thumb-slide", style: s.background ? { background: s.background } : void 0, children: resolveElements(s).map(
321
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx(
322
+ "span",
323
+ {
324
+ style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * 0.12, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden" },
325
+ children: el.text
326
+ },
327
+ el.id
328
+ ) : /* @__PURE__ */ jsx("img", { src: el.src, alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
329
+ ) })
330
+ ] })
331
+ },
332
+ i
333
+ )),
334
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__addslide", onClick: addSlide, children: "+ Add slide" })
335
+ ] }),
336
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__main", children: [
337
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__toolbar cv-chrome", children: [
338
+ /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
339
+ /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
340
+ /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
341
+ /* @__PURE__ */ jsxs(
342
+ "select",
343
+ {
344
+ className: "cv-deck__theme",
345
+ value: "",
346
+ title: "Theme",
347
+ onChange: (e) => {
348
+ const t = THEMES.find((x) => x.id === e.target.value);
349
+ if (t) update({ background: t.bg, textColor: t.text });
350
+ },
351
+ children: [
352
+ /* @__PURE__ */ jsx("option", { value: "", disabled: true, children: "Theme" }),
353
+ THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
354
+ ]
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsx("label", { className: "cv-deck__bg", title: "Background color", children: /* @__PURE__ */ jsx("input", { type: "color", value: slide.background ?? "#ffffff", onChange: (e) => update({ background: e.target.value }) }) }),
358
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
359
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
360
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
361
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(1), title: "Move down", disabled: at === slides.length - 1, children: "\u25BC" }),
362
+ /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
363
+ /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
364
+ ] }),
365
+ /* @__PURE__ */ jsx("div", { className: "cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx(FreeSlide, { elements: resolveElements(slide), onChange: (elements) => update({ elements }) }) }),
366
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
367
+ /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
368
+ /* @__PURE__ */ jsxs("span", { children: [
369
+ at + 1,
370
+ " / ",
371
+ slides.length
372
+ ] }),
373
+ /* @__PURE__ */ jsx("button", { disabled: at === slides.length - 1, onClick: () => setIndex(at + 1), "aria-label": "Next slide", children: "\u203A" })
374
+ ] }),
375
+ /* @__PURE__ */ jsx(
376
+ "textarea",
377
+ {
378
+ className: "cv-deck__notes cv-chrome",
379
+ value: slide.notes ?? "",
380
+ placeholder: "Speaker notes\u2026",
381
+ onChange: (e) => update({ notes: e.target.value })
382
+ }
383
+ )
384
+ ] }),
385
+ presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
386
+ /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", children: resolveElements(slide).map(
387
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left" }, children: el.text }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "" }) }, el.id)
388
+ ) }) }),
389
+ /* @__PURE__ */ jsxs("div", { className: "cv-present__hint", children: [
390
+ at + 1,
391
+ " / ",
392
+ slides.length,
393
+ " \xB7 \u2190 \u2192 to navigate \xB7 Esc to exit"
394
+ ] })
395
+ ] })
396
+ ] });
397
+ }
398
+
399
+ export { SlidesRenderer };
@@ -0,0 +1,250 @@
1
+ import { loadOptional } from './chunk-YZZSJJMQ.js';
2
+ import { useCanvasStore } from './chunk-PSIT32I5.js';
3
+ import { lazy, useMemo, useState, useRef, useEffect, useCallback, Suspense } from 'react';
4
+ import '@fortune-sheet/react/dist/index.css';
5
+ import { jsx, jsxs } from 'react/jsx-runtime';
6
+
7
+ // src/io/formula.ts
8
+ var EMPTY = /* @__PURE__ */ new Map();
9
+ async function computeFormulas(columns, rows) {
10
+ const formulaCells = [];
11
+ rows.forEach(
12
+ (row, dataIdx) => columns.forEach((col, c) => {
13
+ const v = row[col.key];
14
+ if (typeof v === "string" && v.startsWith("=")) formulaCells.push({ dataIdx, col: c, formula: v });
15
+ })
16
+ );
17
+ if (formulaCells.length === 0) return EMPTY;
18
+ const mod = await loadOptional("fast-formula-parser", () => import('fast-formula-parser'));
19
+ const FormulaParser = mod.default ?? mod;
20
+ const memo = /* @__PURE__ */ new Map();
21
+ const inProgress = /* @__PURE__ */ new Set();
22
+ const rawAt = (row, col) => {
23
+ const colIdx = col - 1;
24
+ if (row === 1) return columns[colIdx]?.label ?? columns[colIdx]?.key ?? null;
25
+ const dataRow = rows[row - 2];
26
+ const column = columns[colIdx];
27
+ if (!dataRow || !column) return null;
28
+ const v = dataRow[column.key];
29
+ return v ?? null;
30
+ };
31
+ const valueAt = (row, col) => {
32
+ const raw = rawAt(row, col);
33
+ if (typeof raw !== "string" || !raw.startsWith("=")) return raw ?? 0;
34
+ const key = `${row},${col}`;
35
+ const cached = memo.get(key);
36
+ if (cached !== void 0) return cached;
37
+ if (inProgress.has(key)) return 0;
38
+ inProgress.add(key);
39
+ const value = evaluate(raw, row, col);
40
+ inProgress.delete(key);
41
+ memo.set(key, value);
42
+ return value;
43
+ };
44
+ const parser = new FormulaParser({
45
+ onCell: ({ row, col }) => valueAt(row, col),
46
+ onRange: (ref) => {
47
+ const maxRow = Math.min(ref.to.row, rows.length + 1);
48
+ const maxCol = Math.min(ref.to.col, columns.length);
49
+ const grid = [];
50
+ for (let r = ref.from.row; r <= maxRow; r++) {
51
+ const line = [];
52
+ for (let c = ref.from.col; c <= maxCol; c++) line.push(valueAt(r, c));
53
+ grid.push(line);
54
+ }
55
+ return grid;
56
+ }
57
+ });
58
+ const evaluate = (formula, row, col) => {
59
+ try {
60
+ const result = parser.parse(formula.slice(1), { row, col });
61
+ if (result != null && typeof result === "object") return "#ERR";
62
+ return result ?? 0;
63
+ } catch {
64
+ return "#ERR";
65
+ }
66
+ };
67
+ const out = /* @__PURE__ */ new Map();
68
+ for (const { dataIdx, col, formula } of formulaCells) {
69
+ out.set(`${dataIdx + 1},${col}`, evaluate(formula, dataIdx + 2, col + 1));
70
+ }
71
+ return out;
72
+ }
73
+ var Workbook = lazy(() => import('@fortune-sheet/react').then((m) => ({ default: m.Workbook })));
74
+ var isFormula = (v) => typeof v === "string" && v.startsWith("=");
75
+ function toWorkbook(columns, rows, formulas) {
76
+ const celldata = [];
77
+ columns.forEach((col, c) => {
78
+ const label = col.label ?? col.key;
79
+ celldata.push({ r: 0, c, v: { v: label, m: String(label), bl: 1, bg: "#f3f4f6" } });
80
+ });
81
+ rows.forEach((row, r) => {
82
+ columns.forEach((col, c) => {
83
+ const val = row[col.key];
84
+ if (val === void 0 || val === null || val === "") return;
85
+ if (isFormula(val)) {
86
+ const computed = formulas.get(`${r + 1},${c}`);
87
+ const v = { f: val };
88
+ if (computed !== void 0) {
89
+ v.v = computed;
90
+ v.m = String(computed);
91
+ if (typeof computed === "number") v.ct = { fa: "General", t: "n" };
92
+ }
93
+ celldata.push({ r: r + 1, c, v });
94
+ return;
95
+ }
96
+ const numeric = typeof val === "number";
97
+ celldata.push({
98
+ r: r + 1,
99
+ c,
100
+ v: { v: val, m: String(val), ...numeric ? { ct: { fa: "General", t: "n" } } : {} }
101
+ });
102
+ });
103
+ });
104
+ const sample = Math.min(rows.length, 400);
105
+ const columnlen = {};
106
+ columns.forEach((col, c) => {
107
+ let widest = String(col.label ?? col.key).length;
108
+ for (let ri = 0; ri < sample; ri++) {
109
+ let v = rows[ri][col.key];
110
+ if (isFormula(v)) v = formulas.get(`${ri + 1},${c}`) ?? "";
111
+ if (v != null && v !== "") widest = Math.max(widest, String(v).length);
112
+ }
113
+ columnlen[c] = Math.min(360, Math.max(64, Math.round(widest * 8.5) + 18));
114
+ });
115
+ return [
116
+ {
117
+ name: "Sheet1",
118
+ id: "sheet1",
119
+ order: 0,
120
+ // Size the grid to the data plus a modest buffer — big enough to feel like a
121
+ // real sheet and to keep growing, small enough that the scrollbar stays
122
+ // proportional (a huge empty grid makes scrolling feel disconnected).
123
+ row: Math.max(rows.length + 40, 60),
124
+ column: Math.max(columns.length + 2, 8),
125
+ celldata,
126
+ // No frozen pane: a freeze split offsets the initial scroll and hides the
127
+ // first data rows behind the split line. A plain grid scrolls cleanly.
128
+ config: { rowlen: { 0: 28 }, columnlen }
129
+ }
130
+ ];
131
+ }
132
+ function deriveColumns(rows) {
133
+ const keys = /* @__PURE__ */ new Set();
134
+ for (let i = 0; i < Math.min(rows.length, 50); i++) Object.keys(rows[i] ?? {}).forEach((k) => keys.add(k));
135
+ return [...keys].map((key) => ({ key }));
136
+ }
137
+ var EMPTY_FORMULAS = /* @__PURE__ */ new Map();
138
+ function TableRenderer({ artifact }) {
139
+ const rows = artifact.data.rows;
140
+ const columns = useMemo(
141
+ () => artifact.data.columns.length ? artifact.data.columns : deriveColumns(rows),
142
+ // eslint-disable-next-line react-hooks/exhaustive-deps
143
+ [artifact.id, artifact.version, artifact.data.columns.length, rows.length]
144
+ );
145
+ const [mounted, setMounted] = useState(false);
146
+ const rootRef = useRef(null);
147
+ useEffect(() => setMounted(true), []);
148
+ useEffect(() => {
149
+ const root = rootRef.current;
150
+ if (!root) return;
151
+ const onWheel = (e) => {
152
+ const y = root.querySelector(".luckysheet-scrollbar-y");
153
+ const x = root.querySelector(".luckysheet-scrollbar-x");
154
+ let moved = false;
155
+ if (y && e.deltaY && y.scrollHeight > y.clientHeight) {
156
+ const max = y.scrollHeight - y.clientHeight;
157
+ const next = Math.max(0, Math.min(max, y.scrollTop + e.deltaY));
158
+ if (next !== y.scrollTop) {
159
+ y.scrollTop = next;
160
+ moved = true;
161
+ }
162
+ }
163
+ if (x && e.deltaX && x.scrollWidth > x.clientWidth) {
164
+ const max = x.scrollWidth - x.clientWidth;
165
+ const next = Math.max(0, Math.min(max, x.scrollLeft + e.deltaX));
166
+ if (next !== x.scrollLeft) {
167
+ x.scrollLeft = next;
168
+ moved = true;
169
+ }
170
+ }
171
+ const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
172
+ if (moved || horizontal && e.deltaX) {
173
+ e.preventDefault();
174
+ e.stopPropagation();
175
+ }
176
+ };
177
+ root.addEventListener("wheel", onWheel, { passive: false, capture: true });
178
+ return () => root.removeEventListener("wheel", onWheel, { capture: true });
179
+ }, [mounted]);
180
+ const dataKey = `${artifact.id}:v${artifact.version}:${columns.length}x${rows.length}`;
181
+ const hasFormulas = useMemo(
182
+ () => rows.slice(0, 400).some((row) => columns.some((col) => isFormula(row[col.key]))),
183
+ // eslint-disable-next-line react-hooks/exhaustive-deps
184
+ [dataKey]
185
+ );
186
+ const [formulas, setFormulas] = useState(EMPTY_FORMULAS);
187
+ const [formulasReady, setFormulasReady] = useState(!hasFormulas);
188
+ useEffect(() => {
189
+ if (!hasFormulas) {
190
+ setFormulas(EMPTY_FORMULAS);
191
+ setFormulasReady(true);
192
+ return;
193
+ }
194
+ let alive = true;
195
+ setFormulasReady(false);
196
+ computeFormulas(columns, rows).then((values) => {
197
+ if (!alive) return;
198
+ setFormulas(values);
199
+ setFormulasReady(true);
200
+ });
201
+ return () => {
202
+ alive = false;
203
+ };
204
+ }, [dataKey, hasFormulas]);
205
+ const hasSheet = !!artifact.data.sheet?.length;
206
+ const initialData = useMemo(
207
+ () => artifact.data.sheet?.length ? artifact.data.sheet : toWorkbook(columns, rows, formulas),
208
+ // eslint-disable-next-line react-hooks/exhaustive-deps
209
+ [dataKey, formulasReady]
210
+ );
211
+ const applyEvent = useCanvasStore((s) => s.applyEvent);
212
+ const persistTimer = useRef(null);
213
+ const handleChange = useCallback(
214
+ (sheets) => {
215
+ if (persistTimer.current) clearTimeout(persistTimer.current);
216
+ persistTimer.current = setTimeout(() => {
217
+ applyEvent({ type: "canvas.patch", id: artifact.id, patch: { sheet: sheets } });
218
+ }, 400);
219
+ },
220
+ [applyEvent, artifact.id]
221
+ );
222
+ useEffect(() => () => {
223
+ if (persistTimer.current) clearTimeout(persistTimer.current);
224
+ }, []);
225
+ const wbRef = useRef(null);
226
+ const insert = (type) => {
227
+ const sel = wbRef.current?.getSelection?.();
228
+ const range = sel?.[0]?.[type] ?? [0, 0];
229
+ wbRef.current?.insertRowOrColumn(type, Math.max(0, range[1]), 1, "rightbottom");
230
+ };
231
+ if (!mounted) {
232
+ return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Loading spreadsheet\u2026" });
233
+ }
234
+ if (!hasSheet && columns.length === 0) {
235
+ return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Waiting for data\u2026" });
236
+ }
237
+ if (!hasSheet && !formulasReady) {
238
+ return /* @__PURE__ */ jsx("div", { className: "cv-sheet cv-sheet--empty", children: "Calculating\u2026" });
239
+ }
240
+ return /* @__PURE__ */ jsxs("div", { className: "cv-sheet-panel", children: [
241
+ /* @__PURE__ */ jsxs("div", { className: "cv-sheet-tools", children: [
242
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("column"), children: "\uFF0B Column" }),
243
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: () => insert("row"), children: "\uFF0B Row" }),
244
+ /* @__PURE__ */ jsx("span", { className: "cv-sheet-tools__hint", children: "Right-click a header for more, or drag to edit" })
245
+ ] }),
246
+ /* @__PURE__ */ jsx("div", { className: "cv-sheet", ref: rootRef, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "cv-sheet--empty", children: "Loading\u2026" }), children: /* @__PURE__ */ jsx(Workbook, { ref: wbRef, data: initialData, onChange: handleChange }, dataKey) }) })
247
+ ] });
248
+ }
249
+
250
+ export { TableRenderer };
@@ -0,0 +1,24 @@
1
+ // src/client/slideElements.ts
2
+ function toElements(s) {
3
+ const layout = s.layout ?? "content";
4
+ const els = [];
5
+ const push = (id, e) => els.push({ color: s.textColor, ...e, id });
6
+ if (layout === "title" || layout === "section") {
7
+ if (s.title) push("title", { type: "text", x: 10, y: 34, w: 80, h: 18, text: s.title, fontSize: layout === "title" ? 54 : 40, bold: true, align: "center" });
8
+ if (s.subtitle) push("subtitle", { type: "text", x: 10, y: 58, w: 80, h: 8, text: s.subtitle, fontSize: 24, align: "center" });
9
+ } else if (layout === "image") {
10
+ if (s.title) push("title", { type: "text", x: 6, y: 6, w: 88, h: 10, text: s.title, fontSize: 28, bold: true });
11
+ if (s.image) els.push({ id: "img", type: "image", x: 14, y: 20, w: 72, h: 66, src: s.image });
12
+ } else if (layout === "two-column") {
13
+ if (s.title) push("title", { type: "text", x: 6, y: 6, w: 88, h: 10, text: s.title, fontSize: 28, bold: true });
14
+ (s.bullets ?? []).forEach((b, i) => push(`bul_${i}`, { type: "text", x: 6, y: 24 + i * 8, w: 42, h: 7, text: `\u2022 ${b}`, fontSize: 18 }));
15
+ (s.bullets2 ?? []).forEach((b, i) => push(`bul2_${i}`, { type: "text", x: 52, y: 24 + i * 8, w: 42, h: 7, text: `\u2022 ${b}`, fontSize: 18 }));
16
+ } else {
17
+ if (s.title) push("title", { type: "text", x: 6, y: 8, w: 88, h: 10, text: s.title, fontSize: 32, bold: true });
18
+ (s.bullets ?? []).forEach((b, i) => push(`bul_${i}`, { type: "text", x: 8, y: 28 + i * 9, w: 84, h: 8, text: `\u2022 ${b}`, fontSize: 20 }));
19
+ }
20
+ return els;
21
+ }
22
+ var resolveElements = (s) => s.elements?.length ? s.elements : toElements(s);
23
+
24
+ export { resolveElements };