@braincrew-lab/langchain-canvas 0.3.0 → 0.5.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,514 @@
1
+ import { deckPage, pageAspect, DEFAULT_SLIDE_PAGE_IN, resolveElements, fontScaleFor } from './chunk-SE6AP3A7.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, fontScale = 1 }) {
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) * fontScale,
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
+ function useFontScale(page) {
238
+ const [node, setNode] = useState(null);
239
+ const [scale, setScale] = useState(1);
240
+ useEffect(() => {
241
+ if (!node) return;
242
+ const measure = () => setScale(fontScaleFor(node.clientWidth, page));
243
+ measure();
244
+ const observer = new ResizeObserver(measure);
245
+ observer.observe(node);
246
+ return () => observer.disconnect();
247
+ }, [node, page.widthIn, page.heightIn]);
248
+ return { ref: setNode, scale };
249
+ }
250
+ var THEMES = [
251
+ { id: "light", label: "Light", bg: "#ffffff", text: "#1f2328" },
252
+ { id: "paper", label: "Paper", bg: "#f7f5ef", text: "#2b2a26" },
253
+ { id: "dark", label: "Dark", bg: "#14171f", text: "#f0f2f5" },
254
+ { id: "midnight", label: "Midnight", bg: "#0b1020", text: "#e6e8ef" },
255
+ { id: "navy", label: "Navy", bg: "#0d1b3e", text: "#eef2ff" },
256
+ { id: "forest", label: "Forest", bg: "#0f2a22", text: "#e6f2ec" },
257
+ { id: "sunset", label: "Sunset", bg: "#2b1a2e", text: "#ffe8d6" },
258
+ { id: "mint", label: "Mint", bg: "#0f2a24", text: "#d7f5ec" }
259
+ ];
260
+ var SHAPES = [
261
+ { id: "rect", label: "\u25AD Rectangle", box: { w: 30, h: 20 } },
262
+ { id: "ellipse", label: "\u25EF Ellipse", box: { w: 24, h: 24 } },
263
+ { id: "line", label: "\u2014 Line", box: { w: 40, h: 2 } }
264
+ ];
265
+ var LAYOUTS = {
266
+ title: { label: "Title", els: [
267
+ { type: "text", x: 12, y: 34, w: 76, h: 18, text: "Presentation title", fontSize: 54, bold: true, align: "center" },
268
+ { type: "text", x: 20, y: 56, w: 60, h: 10, text: "Subtitle or one-line summary", fontSize: 24, align: "center" }
269
+ ] },
270
+ section: { label: "Section", els: [
271
+ { type: "shape", shape: "rect", x: 10, y: 34, w: 8, h: 3, fill: "#5b5bd6" },
272
+ { type: "text", x: 10, y: 40, w: 80, h: 16, text: "Section title", fontSize: 46, bold: true }
273
+ ] },
274
+ bullets: { label: "Bullets", els: [
275
+ { type: "text", x: 10, y: 12, w: 80, h: 12, text: "Heading", fontSize: 36, bold: true },
276
+ { 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 }
277
+ ] },
278
+ "two-column": { label: "Two column", els: [
279
+ { type: "text", x: 8, y: 16, w: 40, h: 60, text: "Left column\n\n\u2022 point\n\u2022 point", fontSize: 24 },
280
+ { type: "text", x: 52, y: 16, w: 40, h: 60, text: "Right column\n\n\u2022 point\n\u2022 point", fontSize: 24 }
281
+ ] },
282
+ quote: { label: "Quote", els: [
283
+ { type: "text", x: 12, y: 30, w: 76, h: 30, text: "\u201CA short, memorable quote.\u201D", fontSize: 40, bold: true },
284
+ { type: "text", x: 12, y: 62, w: 50, h: 8, text: "\u2014 Attribution", fontSize: 22 }
285
+ ] }
286
+ };
287
+ var elementSeq = 0;
288
+ var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
289
+ function SlidesRenderer({ artifact }) {
290
+ const slides = artifact.data.slides ?? [];
291
+ const patch = useArtifactPatch(artifact.id);
292
+ const assetUrl = useAssetUrl();
293
+ const [index, setIndex] = useState(0);
294
+ const [dragIndex, setDragIndex] = useState(null);
295
+ const [presenting, setPresenting] = useState(false);
296
+ const imgRef = useRef(null);
297
+ const bgRef = useRef(null);
298
+ const page = deckPage(artifact.data);
299
+ const aspect = pageAspect(page);
300
+ const editBox = useFontScale(page);
301
+ const presentBox = useFontScale(page);
302
+ const thumbFont = 0.12 * (DEFAULT_SLIDE_PAGE_IN.widthIn / page.widthIn);
303
+ useEffect(() => {
304
+ if (!presenting) return;
305
+ const onKey = (e) => {
306
+ if (e.key === "ArrowRight" || e.key === " ") setIndex((i) => Math.min(i + 1, slides.length - 1));
307
+ else if (e.key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
308
+ else if (e.key === "Escape") setPresenting(false);
309
+ };
310
+ window.addEventListener("keydown", onKey);
311
+ return () => window.removeEventListener("keydown", onKey);
312
+ }, [presenting]);
313
+ if (slides.length === 0) {
314
+ return /* @__PURE__ */ jsx("div", { className: "cv-deck cv-deck--empty", children: "No slides yet\u2026" });
315
+ }
316
+ const at = Math.min(index, slides.length - 1);
317
+ const slide = slides[at];
318
+ const slideStyle = {
319
+ ...slide.background ? { background: slide.background } : {},
320
+ ...slide.textColor ? { color: slide.textColor } : {}
321
+ };
322
+ const setSlides = (next) => patch({ slides: next });
323
+ const update = (partial) => setSlides(slides.map((s, i) => i === at ? { ...s, ...partial } : s));
324
+ const addSlide = () => {
325
+ const next = [...slides];
326
+ 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 }] });
327
+ setSlides(next);
328
+ setIndex(at + 1);
329
+ };
330
+ const duplicateSlide = () => {
331
+ const next = [...slides];
332
+ next.splice(at + 1, 0, { ...slide, elements: resolveElements(slide).map((e) => ({ ...e })) });
333
+ setSlides(next);
334
+ setIndex(at + 1);
335
+ };
336
+ const deleteSlide = () => {
337
+ if (slides.length === 1) return;
338
+ setSlides(slides.filter((_, i) => i !== at));
339
+ setIndex(Math.max(0, at - 1));
340
+ };
341
+ const moveSlide = (dir) => {
342
+ const j = at + dir;
343
+ if (j < 0 || j >= slides.length) return;
344
+ const next = [...slides];
345
+ [next[at], next[j]] = [next[j], next[at]];
346
+ setSlides(next);
347
+ setIndex(j);
348
+ };
349
+ const reorder = (from, to) => {
350
+ if (from === to) return;
351
+ const next = [...slides];
352
+ const [moved] = next.splice(from, 1);
353
+ next.splice(to, 0, moved);
354
+ setSlides(next);
355
+ setIndex(to);
356
+ };
357
+ const addElement = (el) => update({ elements: [...resolveElements(slide), { ...el, id: newElementId() }] });
358
+ const addTextEl = () => addElement({ type: "text", x: 12, y: 16, w: 45, h: 16, text: "Text", fontSize: 24 });
359
+ const addImageEl = (file) => {
360
+ if (!file) return;
361
+ const reader = new FileReader();
362
+ reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
363
+ reader.readAsDataURL(file);
364
+ };
365
+ const addShapeEl = (shape) => {
366
+ const s = SHAPES.find((x) => x.id === shape);
367
+ if (s) addElement({ type: "shape", shape, x: 20, y: 20, w: s.box.w, h: s.box.h, fill: "#5b5bd6" });
368
+ };
369
+ const applyLayout = (key) => {
370
+ const l = LAYOUTS[key];
371
+ if (l) update({ elements: l.els.map((e) => ({ ...e, id: newElementId() })) });
372
+ };
373
+ const setSlideBgImage = (file) => {
374
+ if (!file) return;
375
+ const reader = new FileReader();
376
+ reader.onload = () => update({ background: `#000 url("${String(reader.result)}") center/cover no-repeat` });
377
+ reader.readAsDataURL(file);
378
+ };
379
+ return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
380
+ /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
381
+ slides.map((s, i) => /* @__PURE__ */ jsx(
382
+ "div",
383
+ {
384
+ className: `cv-deck__thumb-wrap ${i === at ? "is-active" : ""} ${dragIndex === i ? "is-dragging" : ""}`,
385
+ draggable: true,
386
+ onDragStart: () => setDragIndex(i),
387
+ onDragOver: (e) => e.preventDefault(),
388
+ onDrop: () => {
389
+ if (dragIndex !== null) reorder(dragIndex, i);
390
+ setDragIndex(null);
391
+ },
392
+ onDragEnd: () => setDragIndex(null),
393
+ children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
394
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
395
+ /* @__PURE__ */ jsx("div", { className: "cv-deck__thumb-slide", style: { aspectRatio: aspect, ...s.background ? { background: s.background } : {} }, children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: `${s.padding ?? 0}%` }, children: resolveElements(s).map(
396
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx(
397
+ "span",
398
+ {
399
+ style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, fontSize: (el.fontSize ?? 24) * thumbFont, fontWeight: el.bold ? 700 : 400, color: el.color ?? s.textColor, overflow: "hidden", whiteSpace: "pre-wrap" },
400
+ children: el.text
401
+ },
402
+ el.id
403
+ ) : 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)
404
+ ) }) })
405
+ ] })
406
+ },
407
+ i
408
+ )),
409
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__addslide", onClick: addSlide, children: "+ Add slide" })
410
+ ] }),
411
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__main", children: [
412
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__toolbar cv-chrome", children: [
413
+ /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
414
+ /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
415
+ /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
416
+ /* @__PURE__ */ jsx("input", { ref: bgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => setSlideBgImage(e.target.files?.[0]) }),
417
+ /* @__PURE__ */ jsxs(
418
+ "select",
419
+ {
420
+ className: "cv-deck__theme",
421
+ value: "",
422
+ title: "Add a shape",
423
+ onChange: (e) => {
424
+ if (e.target.value) addShapeEl(e.target.value);
425
+ e.currentTarget.value = "";
426
+ },
427
+ children: [
428
+ /* @__PURE__ */ jsx("option", { value: "", children: "+ Shape" }),
429
+ SHAPES.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.label }, s.id))
430
+ ]
431
+ }
432
+ ),
433
+ /* @__PURE__ */ jsxs(
434
+ "select",
435
+ {
436
+ className: "cv-deck__theme",
437
+ value: "",
438
+ title: "Apply a layout",
439
+ onChange: (e) => {
440
+ if (e.target.value) applyLayout(e.target.value);
441
+ e.currentTarget.value = "";
442
+ },
443
+ children: [
444
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
445
+ Object.entries(LAYOUTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
446
+ ]
447
+ }
448
+ ),
449
+ /* @__PURE__ */ jsxs(
450
+ "select",
451
+ {
452
+ className: "cv-deck__theme",
453
+ value: "",
454
+ title: "Theme",
455
+ onChange: (e) => {
456
+ const t = THEMES.find((x) => x.id === e.target.value);
457
+ if (t) update({ background: t.bg, textColor: t.text });
458
+ e.currentTarget.value = "";
459
+ },
460
+ children: [
461
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
462
+ THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
463
+ ]
464
+ }
465
+ ),
466
+ /* @__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 }) }) }),
467
+ /* @__PURE__ */ jsx("button", { onClick: () => bgRef.current?.click(), title: "Background image", children: "\u{1F5BC} BG" }),
468
+ /* @__PURE__ */ jsxs("label", { className: "cv-deck__pad", title: "Content padding (% of slide)", children: [
469
+ "Pad",
470
+ /* @__PURE__ */ jsx("input", { type: "number", min: 0, max: 20, value: slide.padding ?? 0, onChange: (e) => update({ padding: Number(e.target.value) || void 0 }) })
471
+ ] }),
472
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
473
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
474
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
475
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(1), title: "Move down", disabled: at === slides.length - 1, children: "\u25BC" }),
476
+ /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
477
+ /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
478
+ ] }),
479
+ /* @__PURE__ */ jsx("div", { className: "cv-slide cv-slide--blank", ref: editBox.ref, style: { aspectRatio: aspect, ...slideStyle }, children: /* @__PURE__ */ jsx(FreeSlide, { elements: resolveElements(slide), onChange: (elements) => update({ elements }), padding: slide.padding, fontScale: editBox.scale }) }),
480
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
481
+ /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
482
+ /* @__PURE__ */ jsxs("span", { children: [
483
+ at + 1,
484
+ " / ",
485
+ slides.length
486
+ ] }),
487
+ /* @__PURE__ */ jsx("button", { disabled: at === slides.length - 1, onClick: () => setIndex(at + 1), "aria-label": "Next slide", children: "\u203A" })
488
+ ] }),
489
+ /* @__PURE__ */ jsx(
490
+ "textarea",
491
+ {
492
+ className: "cv-deck__notes cv-chrome",
493
+ value: slide.notes ?? "",
494
+ placeholder: "Speaker notes\u2026",
495
+ onChange: (e) => update({ notes: e.target.value })
496
+ }
497
+ )
498
+ ] }),
499
+ presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
500
+ /* @__PURE__ */ jsx("div", { ref: presentBox.ref, className: "cv-present__slide cv-present__fade cv-slide cv-slide--blank", style: { aspectRatio: aspect, ...slideStyle }, children: /* @__PURE__ */ jsx("div", { className: "cv-free", style: slide.padding ? { inset: `${slide.padding}%` } : void 0, children: resolveElements(slide).map(
501
+ (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) * presentBox.scale, 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)
502
+ ) }) }, at),
503
+ slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
504
+ /* @__PURE__ */ jsxs("div", { className: "cv-present__hint", children: [
505
+ at + 1,
506
+ " / ",
507
+ slides.length,
508
+ " \xB7 \u2190 \u2192 to navigate \xB7 Esc to exit"
509
+ ] })
510
+ ] })
511
+ ] });
512
+ }
513
+
514
+ export { SlidesRenderer };