@braincrew-lab/langchain-canvas 0.2.0 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import { useArtifactPatch } from './chunk-UL5F66PN.js';
2
- import './chunk-S54GJDSJ.js';
1
+ import { useArtifactPatch } from './chunk-K2UZAYW2.js';
2
+ import './chunk-EHW446VF.js';
3
3
  import { useState, useRef, useMemo, useEffect } from 'react';
4
4
  import * as echarts from 'echarts/core';
5
5
  import { BarChart, LineChart, PieChart } from 'echarts/charts';
@@ -124,23 +124,6 @@ function ChartRenderer({ artifact }) {
124
124
  /* @__PURE__ */ jsx(EChart, { option, height: editing ? 260 : 340, onInst: (i) => instRef.current = i })
125
125
  ] });
126
126
  }
127
- function NumberCell({ value, onCommit }) {
128
- const [draft, setDraft] = useState(null);
129
- const shown = draft ?? String(Number(value ?? 0));
130
- return /* @__PURE__ */ jsx(
131
- "input",
132
- {
133
- type: "number",
134
- value: shown,
135
- onChange: (e) => {
136
- setDraft(e.target.value);
137
- const n = Number(e.target.value);
138
- if (e.target.value !== "" && Number.isFinite(n)) onCommit(n);
139
- },
140
- onBlur: () => setDraft(null)
141
- }
142
- );
143
- }
144
127
  function DataGrid({
145
128
  rows,
146
129
  xKey,
@@ -158,7 +141,14 @@ function DataGrid({
158
141
  ] }) }),
159
142
  /* @__PURE__ */ jsx("tbody", { children: rows.map((row, i) => /* @__PURE__ */ jsxs("tr", { children: [
160
143
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("input", { value: String(row[xKey] ?? ""), onChange: (e) => onCell(i, xKey, e.target.value) }) }),
161
- series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(NumberCell, { value: row[s.key], onCommit: (n) => onCell(i, s.key, n) }) }, s.key)),
144
+ series.map((s) => /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx(
145
+ "input",
146
+ {
147
+ type: "number",
148
+ value: Number(row[s.key] ?? 0),
149
+ onChange: (e) => onCell(i, s.key, e.target.value === "" ? 0 : Number(e.target.value))
150
+ }
151
+ ) }, s.key)),
162
152
  /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsx("button", { className: "cv-chart__row-del", onClick: () => onRemoveRow(i), disabled: rows.length <= 1, title: "Remove row", children: "\xD7" }) })
163
153
  ] }, i)) })
164
154
  ] }),
@@ -1,7 +1,9 @@
1
- import { useArtifactPatch } from './chunk-UL5F66PN.js';
2
- import './chunk-S54GJDSJ.js';
1
+ import { useAssetUrl } from './chunk-FTNRRJ3K.js';
2
+ import './chunk-7T5DRR3F.js';
3
+ import { useArtifactPatch } from './chunk-K2UZAYW2.js';
4
+ import './chunk-EHW446VF.js';
3
5
  import { useState, useRef } from 'react';
4
- import ReactMarkdown from 'react-markdown';
6
+ import ReactMarkdown, { defaultUrlTransform } from 'react-markdown';
5
7
  import remarkGfm from 'remark-gfm';
6
8
  import { jsxs, jsx } from 'react/jsx-runtime';
7
9
 
@@ -15,6 +17,8 @@ function DocumentRenderer({ artifact }) {
15
17
  const [draft, setDraft] = useState(null);
16
18
  const taRef = useRef(null);
17
19
  const editing = draft !== null;
20
+ const assetUrl = useAssetUrl();
21
+ const urlTransform = (url) => defaultUrlTransform(assetUrl(url) ?? url);
18
22
  const commit = () => {
19
23
  if (draft !== null && draft !== content) patch({ content: draft });
20
24
  setDraft(null);
@@ -112,7 +116,7 @@ function DocumentRenderer({ artifact }) {
112
116
  title: "Click to edit",
113
117
  onClick: () => artifact.status !== "streaming" && setDraft(content),
114
118
  children: [
115
- /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: content }),
119
+ /* @__PURE__ */ jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], urlTransform, children: content }),
116
120
  artifact.status === "streaming" && /* @__PURE__ */ jsx("span", { className: "cv-caret", "aria-hidden": true })
117
121
  ]
118
122
  }
@@ -0,0 +1,39 @@
1
+ import { isAssetReference, resolveAssetUrl } from './chunk-7T5DRR3F.js';
2
+ import { useCanvasStore } from './chunk-EHW446VF.js';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ function formatSize(size) {
6
+ if (typeof size !== "number") return null;
7
+ if (size < 1024) return `${size} B`;
8
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
9
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
10
+ }
11
+ function iconFor(mediaType, name) {
12
+ if (mediaType?.startsWith("image/")) return "\u{1F5BC}\uFE0F";
13
+ if (mediaType === "application/pdf") return "\u{1F4D5}";
14
+ const lower = name.toLowerCase();
15
+ if (lower.endsWith(".docx") || lower.endsWith(".doc")) return "\u{1F4C4}";
16
+ if (lower.endsWith(".xlsx") || lower.endsWith(".csv")) return "\u{1F522}";
17
+ if (lower.endsWith(".pptx") || lower.endsWith(".ppt")) return "\u{1F4FD}\uFE0F";
18
+ return "\u{1F4CE}";
19
+ }
20
+ function FileRenderer({ artifact }) {
21
+ const { path, name, mediaType, size, cover, excerpt, detail } = artifact.data;
22
+ const assetBaseUrl = useCanvasStore((s) => s.assetBaseUrl);
23
+ const href = assetBaseUrl && isAssetReference(path) ? resolveAssetUrl(path, assetBaseUrl) : null;
24
+ const isImage = Boolean(mediaType?.startsWith("image/"));
25
+ const facts = [mediaType, formatSize(size), detail].filter(Boolean).join(" \xB7 ");
26
+ return /* @__PURE__ */ jsxs("div", { className: "cv-file", children: [
27
+ isImage && href ? /* @__PURE__ */ jsx("img", { className: "cv-file__image", src: href, alt: name }) : cover ? /* @__PURE__ */ jsx("img", { className: "cv-file__cover", src: cover, alt: `${name} \u2014 first page` }) : excerpt ? /* @__PURE__ */ jsx("pre", { className: "cv-file__excerpt", children: excerpt }) : null,
28
+ /* @__PURE__ */ jsxs("div", { className: "cv-file__card", children: [
29
+ /* @__PURE__ */ jsx("span", { className: "cv-file__icon", "aria-hidden": true, children: iconFor(mediaType, name) }),
30
+ /* @__PURE__ */ jsxs("span", { className: "cv-file__meta", children: [
31
+ /* @__PURE__ */ jsx("b", { children: name }),
32
+ facts && /* @__PURE__ */ jsx("span", { className: "cv-file__facts", children: facts })
33
+ ] }),
34
+ href && /* @__PURE__ */ jsx("a", { className: "cv-file__download", href, download: name, children: "Download" })
35
+ ] })
36
+ ] });
37
+ }
38
+
39
+ export { FileRenderer };
@@ -0,0 +1,496 @@
1
+ import { resolveElements } from './chunk-6L3AL6W4.js';
2
+ import { useAssetUrl } from './chunk-FTNRRJ3K.js';
3
+ import './chunk-7T5DRR3F.js';
4
+ import { useArtifactPatch } from './chunk-K2UZAYW2.js';
5
+ import './chunk-EHW446VF.js';
6
+ import { useState, useRef, useEffect } from 'react';
7
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
8
+
9
+ function shapeStyle(el) {
10
+ const fill = el.fill ?? "currentColor";
11
+ if (el.shape === "ellipse") return { width: "100%", height: "100%", background: fill, borderRadius: "50%" };
12
+ if (el.shape === "line") return { width: "100%", height: "100%", background: fill, borderRadius: 2 };
13
+ return { width: "100%", height: "100%", background: fill, borderRadius: 8 };
14
+ }
15
+ var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
16
+ var dupSeq = 0;
17
+ var dupId = (base) => `${base}_c${Date.now().toString(36)}${dupSeq++}`;
18
+ var SNAP = 1.2;
19
+ function snapAxis(pos, size, targets) {
20
+ const anchors = [pos, pos + size / 2, pos + size];
21
+ let best = null;
22
+ for (const anchor of anchors) {
23
+ for (const t of targets) {
24
+ const delta = t - anchor;
25
+ if (Math.abs(delta) <= SNAP && (!best || Math.abs(delta) < Math.abs(best.delta))) {
26
+ best = { delta, guide: t };
27
+ }
28
+ }
29
+ }
30
+ return best ? { pos: pos + best.delta, guide: best.guide } : { pos, guide: null };
31
+ }
32
+ function FreeSlide({ elements, onChange, padding }) {
33
+ const slideRef = useRef(null);
34
+ const assetUrl = useAssetUrl();
35
+ const [els, setEls] = useState(elements);
36
+ const [selected, setSelected] = useState(null);
37
+ const [editingId, setEditingId] = useState(null);
38
+ const [guides, setGuides] = useState({ x: null, y: null });
39
+ const drag = useRef(null);
40
+ useEffect(() => {
41
+ if (!drag.current) setEls(elements);
42
+ }, [elements]);
43
+ const commit = (next) => {
44
+ setEls(next);
45
+ onChange(next);
46
+ };
47
+ const updateEl = (id, partial) => commit(els.map((el) => el.id === id ? { ...el, ...partial } : el));
48
+ const duplicate = (el) => {
49
+ const copy = {
50
+ ...el,
51
+ id: dupId(el.id),
52
+ x: Math.min(el.x + 4, 100 - el.w),
53
+ y: Math.min(el.y + 4, 100 - el.h)
54
+ };
55
+ commit([...els, copy]);
56
+ setSelected(copy.id);
57
+ };
58
+ const zorder = (id, dir) => {
59
+ const i = els.findIndex((e) => e.id === id);
60
+ const j = i + dir;
61
+ if (i < 0 || j < 0 || j >= els.length) return;
62
+ const next = [...els];
63
+ [next[i], next[j]] = [next[j], next[i]];
64
+ commit(next);
65
+ };
66
+ const onDown = (e, el, mode) => {
67
+ if (editingId === el.id && mode === "move") return;
68
+ e.preventDefault();
69
+ e.stopPropagation();
70
+ setSelected(el.id);
71
+ drag.current = { id: el.id, mode, sx: e.clientX, sy: e.clientY, orig: { ...el } };
72
+ e.currentTarget.setPointerCapture(e.pointerId);
73
+ };
74
+ const onMove = (e) => {
75
+ const d = drag.current;
76
+ const rect = slideRef.current?.getBoundingClientRect();
77
+ if (!d || !rect) return;
78
+ const dx = (e.clientX - d.sx) / rect.width * 100;
79
+ const dy = (e.clientY - d.sy) / rect.height * 100;
80
+ if (d.mode === "resize") {
81
+ setGuides({ x: null, y: null });
82
+ 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));
83
+ return;
84
+ }
85
+ const others = els.filter((el) => el.id !== d.id);
86
+ const xTargets = [0, 50, 100, ...others.flatMap((o) => [o.x, o.x + o.w / 2, o.x + o.w])];
87
+ const yTargets = [0, 50, 100, ...others.flatMap((o) => [o.y, o.y + o.h / 2, o.y + o.h])];
88
+ const rawX = clamp(d.orig.x + dx, 0, 100 - d.orig.w);
89
+ const rawY = clamp(d.orig.y + dy, 0, 100 - d.orig.h);
90
+ const sx = snapAxis(rawX, d.orig.w, xTargets);
91
+ const sy = snapAxis(rawY, d.orig.h, yTargets);
92
+ setGuides({ x: sx.guide, y: sy.guide });
93
+ setEls(
94
+ (prev) => prev.map(
95
+ (el) => el.id === d.id ? { ...el, x: clamp(sx.pos, 0, 100 - el.w), y: clamp(sy.pos, 0, 100 - el.h) } : el
96
+ )
97
+ );
98
+ };
99
+ const onUp = () => {
100
+ if (drag.current) {
101
+ drag.current = null;
102
+ setGuides({ x: null, y: null });
103
+ onChange(els);
104
+ }
105
+ };
106
+ useEffect(() => {
107
+ if (!selected) return;
108
+ const onKey = (e) => {
109
+ if (editingId) return;
110
+ const ae = document.activeElement;
111
+ if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return;
112
+ const el = els.find((x) => x.id === selected);
113
+ if (!el) return;
114
+ const step = e.shiftKey ? 5 : 1;
115
+ const nudge = (ddx, ddy) => {
116
+ e.preventDefault();
117
+ 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));
118
+ };
119
+ if (e.key === "ArrowLeft") nudge(-step, 0);
120
+ else if (e.key === "ArrowRight") nudge(step, 0);
121
+ else if (e.key === "ArrowUp") nudge(0, -step);
122
+ else if (e.key === "ArrowDown") nudge(0, step);
123
+ else if (e.key === "Delete" || e.key === "Backspace") {
124
+ e.preventDefault();
125
+ commit(els.filter((x) => x.id !== selected));
126
+ setSelected(null);
127
+ } else if ((e.metaKey || e.ctrlKey) && (e.key === "d" || e.key === "D")) {
128
+ e.preventDefault();
129
+ duplicate(el);
130
+ } else if (e.key === "Escape") {
131
+ setSelected(null);
132
+ }
133
+ };
134
+ window.addEventListener("keydown", onKey);
135
+ return () => window.removeEventListener("keydown", onKey);
136
+ }, [selected, editingId, els]);
137
+ return /* @__PURE__ */ jsxs(
138
+ "div",
139
+ {
140
+ className: "cv-free",
141
+ ref: slideRef,
142
+ style: padding ? { inset: `${padding}%` } : void 0,
143
+ onPointerMove: onMove,
144
+ onPointerUp: onUp,
145
+ onPointerLeave: onUp,
146
+ onClick: () => {
147
+ setSelected(null);
148
+ setEditingId(null);
149
+ },
150
+ children: [
151
+ guides.x !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--v", style: { left: `${guides.x}%` } }),
152
+ guides.y !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--h", style: { top: `${guides.y}%` } }),
153
+ els.map((el) => /* @__PURE__ */ jsxs(
154
+ "div",
155
+ {
156
+ className: `cv-free__el ${selected === el.id ? "is-selected" : ""}`,
157
+ style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` },
158
+ onPointerDown: (e) => onDown(e, el, "move"),
159
+ onDoubleClick: (e) => {
160
+ if (el.type === "text") {
161
+ e.stopPropagation();
162
+ setEditingId(el.id);
163
+ }
164
+ },
165
+ onClick: (e) => {
166
+ e.stopPropagation();
167
+ setSelected(el.id);
168
+ },
169
+ children: [
170
+ el.type === "text" ? /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ className: "cv-free__text",
174
+ contentEditable: editingId === el.id,
175
+ suppressContentEditableWarning: true,
176
+ style: {
177
+ fontSize: el.fontSize ?? 24,
178
+ fontWeight: el.bold ? 700 : 400,
179
+ color: el.color,
180
+ textAlign: el.align ?? "left"
181
+ },
182
+ onBlur: (e) => {
183
+ setEditingId(null);
184
+ updateEl(el.id, { text: e.currentTarget.textContent ?? "" });
185
+ },
186
+ children: el.text
187
+ }
188
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: assetUrl(el.src), alt: "", draggable: false }),
189
+ 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: [
190
+ /* @__PURE__ */ jsx("button", { className: el.bold ? "is-on" : "", onClick: () => updateEl(el.id, { bold: !el.bold }), title: "Bold", children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
191
+ /* @__PURE__ */ jsx(
192
+ "input",
193
+ {
194
+ type: "number",
195
+ min: 8,
196
+ max: 120,
197
+ value: el.fontSize ?? 24,
198
+ onChange: (e) => updateEl(el.id, { fontSize: Number(e.target.value) }),
199
+ title: "Font size"
200
+ }
201
+ ),
202
+ /* @__PURE__ */ jsx("input", { type: "color", value: el.color ?? "#1f2328", onChange: (e) => updateEl(el.id, { color: e.target.value }), title: "Text color" }),
203
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "left" }), title: "Align left", children: "\u27F8" }),
204
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
205
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
206
+ ] }),
207
+ selected === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
208
+ /* @__PURE__ */ jsx("span", { className: "cv-free__resize", onPointerDown: (e) => onDown(e, el, "resize") }),
209
+ /* @__PURE__ */ jsxs("div", { className: `cv-free__ctl ${el.y < 16 ? "cv-free__ctl--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), children: [
210
+ el.type === "shape" && /* @__PURE__ */ jsx("input", { className: "cv-free__ctl-fill", type: "color", value: el.fill ?? "#5b5bd6", onChange: (e) => updateEl(el.id, { fill: e.target.value }), onClick: (e) => e.stopPropagation(), title: "Fill color" }),
211
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
212
+ e.stopPropagation();
213
+ duplicate(el);
214
+ }, title: "Duplicate", children: "\u29C9" }),
215
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
216
+ e.stopPropagation();
217
+ zorder(el.id, 1);
218
+ }, title: "Bring forward", children: "\u2191" }),
219
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
220
+ e.stopPropagation();
221
+ zorder(el.id, -1);
222
+ }, title: "Send back", children: "\u2193" }),
223
+ /* @__PURE__ */ jsx("button", { className: "cv-free__ctl-del", onClick: (e) => {
224
+ e.stopPropagation();
225
+ commit(els.filter((x) => x.id !== el.id));
226
+ }, title: "Delete", children: "\xD7" })
227
+ ] })
228
+ ] })
229
+ ]
230
+ },
231
+ el.id
232
+ ))
233
+ ]
234
+ }
235
+ );
236
+ }
237
+ var THEMES = [
238
+ { id: "light", label: "Light", bg: "#ffffff", text: "#1f2328" },
239
+ { id: "paper", label: "Paper", bg: "#f7f5ef", text: "#2b2a26" },
240
+ { id: "dark", label: "Dark", bg: "#14171f", text: "#f0f2f5" },
241
+ { id: "midnight", label: "Midnight", bg: "#0b1020", text: "#e6e8ef" },
242
+ { id: "navy", label: "Navy", bg: "#0d1b3e", text: "#eef2ff" },
243
+ { id: "forest", label: "Forest", bg: "#0f2a22", text: "#e6f2ec" },
244
+ { id: "sunset", label: "Sunset", bg: "#2b1a2e", text: "#ffe8d6" },
245
+ { id: "mint", label: "Mint", bg: "#0f2a24", text: "#d7f5ec" }
246
+ ];
247
+ var SHAPES = [
248
+ { id: "rect", label: "\u25AD Rectangle", box: { w: 30, h: 20 } },
249
+ { id: "ellipse", label: "\u25EF Ellipse", box: { w: 24, h: 24 } },
250
+ { id: "line", label: "\u2014 Line", box: { w: 40, h: 2 } }
251
+ ];
252
+ var LAYOUTS = {
253
+ title: { label: "Title", els: [
254
+ { type: "text", x: 12, y: 34, w: 76, h: 18, text: "Presentation title", fontSize: 54, bold: true, align: "center" },
255
+ { type: "text", x: 20, y: 56, w: 60, h: 10, text: "Subtitle or one-line summary", fontSize: 24, align: "center" }
256
+ ] },
257
+ section: { label: "Section", els: [
258
+ { type: "shape", shape: "rect", x: 10, y: 34, w: 8, h: 3, fill: "#5b5bd6" },
259
+ { type: "text", x: 10, y: 40, w: 80, h: 16, text: "Section title", fontSize: 46, bold: true }
260
+ ] },
261
+ bullets: { label: "Bullets", els: [
262
+ { type: "text", x: 10, y: 12, w: 80, h: 12, text: "Heading", fontSize: 36, bold: true },
263
+ { type: "text", x: 10, y: 30, w: 80, h: 50, text: "\u2022 First key point\n\u2022 Second key point\n\u2022 Third key point", fontSize: 26 }
264
+ ] },
265
+ "two-column": { label: "Two column", els: [
266
+ { type: "text", x: 8, y: 16, w: 40, h: 60, text: "Left column\n\n\u2022 point\n\u2022 point", fontSize: 24 },
267
+ { type: "text", x: 52, y: 16, w: 40, h: 60, text: "Right column\n\n\u2022 point\n\u2022 point", fontSize: 24 }
268
+ ] },
269
+ quote: { label: "Quote", els: [
270
+ { type: "text", x: 12, y: 30, w: 76, h: 30, text: "\u201CA short, memorable quote.\u201D", fontSize: 40, bold: true },
271
+ { type: "text", x: 12, y: 62, w: 50, h: 8, text: "\u2014 Attribution", fontSize: 22 }
272
+ ] }
273
+ };
274
+ var elementSeq = 0;
275
+ var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
276
+ function SlidesRenderer({ artifact }) {
277
+ const slides = artifact.data.slides ?? [];
278
+ const patch = useArtifactPatch(artifact.id);
279
+ const assetUrl = useAssetUrl();
280
+ const [index, setIndex] = useState(0);
281
+ const [dragIndex, setDragIndex] = useState(null);
282
+ const [presenting, setPresenting] = useState(false);
283
+ const imgRef = useRef(null);
284
+ const bgRef = useRef(null);
285
+ useEffect(() => {
286
+ if (!presenting) return;
287
+ const onKey = (e) => {
288
+ if (e.key === "ArrowRight" || e.key === " ") setIndex((i) => Math.min(i + 1, slides.length - 1));
289
+ else if (e.key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
290
+ else if (e.key === "Escape") setPresenting(false);
291
+ };
292
+ window.addEventListener("keydown", onKey);
293
+ return () => window.removeEventListener("keydown", onKey);
294
+ }, [presenting]);
295
+ if (slides.length === 0) {
296
+ return /* @__PURE__ */ jsx("div", { className: "cv-deck cv-deck--empty", children: "No slides yet\u2026" });
297
+ }
298
+ const at = Math.min(index, slides.length - 1);
299
+ const slide = slides[at];
300
+ const slideStyle = {
301
+ ...slide.background ? { background: slide.background } : {},
302
+ ...slide.textColor ? { color: slide.textColor } : {}
303
+ };
304
+ const setSlides = (next) => patch({ slides: next });
305
+ const update = (partial) => setSlides(slides.map((s, i) => i === at ? { ...s, ...partial } : s));
306
+ const addSlide = () => {
307
+ const next = [...slides];
308
+ 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 }] });
309
+ setSlides(next);
310
+ setIndex(at + 1);
311
+ };
312
+ const duplicateSlide = () => {
313
+ const next = [...slides];
314
+ next.splice(at + 1, 0, { ...slide, elements: resolveElements(slide).map((e) => ({ ...e })) });
315
+ setSlides(next);
316
+ setIndex(at + 1);
317
+ };
318
+ const deleteSlide = () => {
319
+ if (slides.length === 1) return;
320
+ setSlides(slides.filter((_, i) => i !== at));
321
+ setIndex(Math.max(0, at - 1));
322
+ };
323
+ const moveSlide = (dir) => {
324
+ const j = at + dir;
325
+ if (j < 0 || j >= slides.length) return;
326
+ const next = [...slides];
327
+ [next[at], next[j]] = [next[j], next[at]];
328
+ setSlides(next);
329
+ setIndex(j);
330
+ };
331
+ const reorder = (from, to) => {
332
+ if (from === to) return;
333
+ const next = [...slides];
334
+ const [moved] = next.splice(from, 1);
335
+ next.splice(to, 0, moved);
336
+ setSlides(next);
337
+ setIndex(to);
338
+ };
339
+ const addElement = (el) => update({ elements: [...resolveElements(slide), { ...el, id: newElementId() }] });
340
+ const addTextEl = () => addElement({ type: "text", x: 12, y: 16, w: 45, h: 16, text: "Text", fontSize: 24 });
341
+ const addImageEl = (file) => {
342
+ if (!file) return;
343
+ const reader = new FileReader();
344
+ reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
345
+ reader.readAsDataURL(file);
346
+ };
347
+ const addShapeEl = (shape) => {
348
+ const s = SHAPES.find((x) => x.id === shape);
349
+ if (s) addElement({ type: "shape", shape, x: 20, y: 20, w: s.box.w, h: s.box.h, fill: "#5b5bd6" });
350
+ };
351
+ const applyLayout = (key) => {
352
+ const l = LAYOUTS[key];
353
+ if (l) update({ elements: l.els.map((e) => ({ ...e, id: newElementId() })) });
354
+ };
355
+ const setSlideBgImage = (file) => {
356
+ if (!file) return;
357
+ const reader = new FileReader();
358
+ reader.onload = () => update({ background: `#000 url("${String(reader.result)}") center/cover no-repeat` });
359
+ reader.readAsDataURL(file);
360
+ };
361
+ return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
362
+ /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
363
+ slides.map((s, i) => /* @__PURE__ */ jsx(
364
+ "div",
365
+ {
366
+ className: `cv-deck__thumb-wrap ${i === at ? "is-active" : ""} ${dragIndex === i ? "is-dragging" : ""}`,
367
+ draggable: true,
368
+ onDragStart: () => setDragIndex(i),
369
+ onDragOver: (e) => e.preventDefault(),
370
+ onDrop: () => {
371
+ if (dragIndex !== null) reorder(dragIndex, i);
372
+ setDragIndex(null);
373
+ },
374
+ onDragEnd: () => setDragIndex(null),
375
+ children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
376
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
377
+ /* @__PURE__ */ jsx("div", { className: "cv-deck__thumb-slide", style: s.background ? { background: s.background } : void 0, children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: `${s.padding ?? 0}%` }, children: resolveElements(s).map(
378
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx(
379
+ "span",
380
+ {
381
+ style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * 0.12, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden", whiteSpace: "pre-wrap" },
382
+ children: el.text
383
+ },
384
+ el.id
385
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: s.textColor, ...shapeStyle(el) } }, el.id) : /* @__PURE__ */ jsx("img", { src: assetUrl(el.src), alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
386
+ ) }) })
387
+ ] })
388
+ },
389
+ i
390
+ )),
391
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__addslide", onClick: addSlide, children: "+ Add slide" })
392
+ ] }),
393
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__main", children: [
394
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__toolbar cv-chrome", children: [
395
+ /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
396
+ /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
397
+ /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
398
+ /* @__PURE__ */ jsx("input", { ref: bgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => setSlideBgImage(e.target.files?.[0]) }),
399
+ /* @__PURE__ */ jsxs(
400
+ "select",
401
+ {
402
+ className: "cv-deck__theme",
403
+ value: "",
404
+ title: "Add a shape",
405
+ onChange: (e) => {
406
+ if (e.target.value) addShapeEl(e.target.value);
407
+ e.currentTarget.value = "";
408
+ },
409
+ children: [
410
+ /* @__PURE__ */ jsx("option", { value: "", children: "+ Shape" }),
411
+ SHAPES.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.label }, s.id))
412
+ ]
413
+ }
414
+ ),
415
+ /* @__PURE__ */ jsxs(
416
+ "select",
417
+ {
418
+ className: "cv-deck__theme",
419
+ value: "",
420
+ title: "Apply a layout",
421
+ onChange: (e) => {
422
+ if (e.target.value) applyLayout(e.target.value);
423
+ e.currentTarget.value = "";
424
+ },
425
+ children: [
426
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
427
+ Object.entries(LAYOUTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
428
+ ]
429
+ }
430
+ ),
431
+ /* @__PURE__ */ jsxs(
432
+ "select",
433
+ {
434
+ className: "cv-deck__theme",
435
+ value: "",
436
+ title: "Theme",
437
+ onChange: (e) => {
438
+ const t = THEMES.find((x) => x.id === e.target.value);
439
+ if (t) update({ background: t.bg, textColor: t.text });
440
+ e.currentTarget.value = "";
441
+ },
442
+ children: [
443
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
444
+ THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
445
+ ]
446
+ }
447
+ ),
448
+ /* @__PURE__ */ jsx("label", { className: "cv-deck__bg", title: "Background color", children: /* @__PURE__ */ jsx("input", { type: "color", value: /^#/.test(slide.background ?? "") ? slide.background : "#ffffff", onChange: (e) => update({ background: e.target.value }) }) }),
449
+ /* @__PURE__ */ jsx("button", { onClick: () => bgRef.current?.click(), title: "Background image", children: "\u{1F5BC} BG" }),
450
+ /* @__PURE__ */ jsxs("label", { className: "cv-deck__pad", title: "Content padding (% of slide)", children: [
451
+ "Pad",
452
+ /* @__PURE__ */ jsx("input", { type: "number", min: 0, max: 20, value: slide.padding ?? 0, onChange: (e) => update({ padding: Number(e.target.value) || void 0 }) })
453
+ ] }),
454
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
455
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
456
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
457
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(1), title: "Move down", disabled: at === slides.length - 1, children: "\u25BC" }),
458
+ /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
459
+ /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
460
+ ] }),
461
+ /* @__PURE__ */ jsx("div", { className: "cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx(FreeSlide, { elements: resolveElements(slide), onChange: (elements) => update({ elements }), padding: slide.padding }) }),
462
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
463
+ /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
464
+ /* @__PURE__ */ jsxs("span", { children: [
465
+ at + 1,
466
+ " / ",
467
+ slides.length
468
+ ] }),
469
+ /* @__PURE__ */ jsx("button", { disabled: at === slides.length - 1, onClick: () => setIndex(at + 1), "aria-label": "Next slide", children: "\u203A" })
470
+ ] }),
471
+ /* @__PURE__ */ jsx(
472
+ "textarea",
473
+ {
474
+ className: "cv-deck__notes cv-chrome",
475
+ value: slide.notes ?? "",
476
+ placeholder: "Speaker notes\u2026",
477
+ onChange: (e) => update({ notes: e.target.value })
478
+ }
479
+ )
480
+ ] }),
481
+ presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
482
+ /* @__PURE__ */ jsx("div", { className: "cv-present__slide cv-present__fade cv-slide cv-slide--blank", style: slideStyle, children: /* @__PURE__ */ jsx("div", { className: "cv-free", style: slide.padding ? { inset: `${slide.padding}%` } : void 0, children: resolveElements(slide).map(
483
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left", whiteSpace: "pre-wrap" }, children: el.text }) }, el.id) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: slide.textColor }, children: /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: assetUrl(el.src), alt: "" }) }, el.id)
484
+ ) }) }, at),
485
+ slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
486
+ /* @__PURE__ */ jsxs("div", { className: "cv-present__hint", children: [
487
+ at + 1,
488
+ " / ",
489
+ slides.length,
490
+ " \xB7 \u2190 \u2192 to navigate \xB7 Esc to exit"
491
+ ] })
492
+ ] })
493
+ ] });
494
+ }
495
+
496
+ export { SlidesRenderer };