@braincrew-lab/langchain-canvas 0.1.13 → 0.2.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,877 @@
1
+ import { resolveElements } from './chunk-6L3AL6W4.js';
2
+ import { useArtifactPatch } from './chunk-UL5F66PN.js';
3
+ import './chunk-S54GJDSJ.js';
4
+ import { useState, useRef, useEffect } from 'react';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ function shapeStyle(el) {
8
+ const fill = el.fill ?? "currentColor";
9
+ if (el.shape === "ellipse") return { width: "100%", height: "100%", background: fill, borderRadius: "50%" };
10
+ if (el.shape === "line") return { width: "100%", height: "100%", background: fill, borderRadius: 2 };
11
+ return { width: "100%", height: "100%", background: fill, borderRadius: 8 };
12
+ }
13
+ var clamp = (v, min, max) => Math.max(min, Math.min(max, v));
14
+ var dupSeq = 0;
15
+ var dupId = (base) => `${base}_c${Date.now().toString(36)}${dupSeq++}`;
16
+ var groupSeq = 0;
17
+ var newGroupId = () => `grp_${Date.now().toString(36)}${groupSeq++}`;
18
+ var elementClipboard = [];
19
+ var SNAP = 1.2;
20
+ var NUDGE_STEP = 0.5;
21
+ var NUDGE_STEP_BIG = 2;
22
+ function snapAxis(pos, size, targets) {
23
+ const anchors = [pos, pos + size / 2, pos + size];
24
+ let best = null;
25
+ for (const anchor of anchors) {
26
+ for (const t of targets) {
27
+ const delta = t - anchor;
28
+ if (Math.abs(delta) <= SNAP && (!best || Math.abs(delta) < Math.abs(best.delta))) {
29
+ best = { delta, guide: t };
30
+ }
31
+ }
32
+ }
33
+ return best ? { pos: pos + best.delta, guide: best.guide } : { pos, guide: null };
34
+ }
35
+ function boundsOf(els) {
36
+ const x1 = Math.min(...els.map((el) => el.x));
37
+ const y1 = Math.min(...els.map((el) => el.y));
38
+ const x2 = Math.max(...els.map((el) => el.x + el.w));
39
+ const y2 = Math.max(...els.map((el) => el.y + el.h));
40
+ return { x1, y1, x2, y2 };
41
+ }
42
+ function FreeSlide({ elements, onChange, padding }) {
43
+ const slideRef = useRef(null);
44
+ const [els, setEls] = useState(elements);
45
+ const [selected, setSelected] = useState([]);
46
+ const [editingId, setEditingId] = useState(null);
47
+ const [guides, setGuides] = useState({ x: null, y: null });
48
+ const drag = useRef(null);
49
+ const marquee = useRef(null);
50
+ const [marqueeBox, setMarqueeBox] = useState(null);
51
+ const suppressCanvasClick = useRef(false);
52
+ useEffect(() => {
53
+ if (!drag.current) setEls(elements);
54
+ setSelected((prev) => prev.filter((id) => elements.some((el) => el.id === id)));
55
+ }, [elements]);
56
+ const selectedSet = new Set(selected);
57
+ const selEls = els.filter((el) => selectedSet.has(el.id));
58
+ const soloId = selEls.length === 1 ? selEls[0].id : null;
59
+ const commit = (next) => {
60
+ setEls(next);
61
+ onChange(next);
62
+ };
63
+ const updateEl = (id, partial) => commit(els.map((el) => el.id === id ? { ...el, ...partial } : el));
64
+ const groupOf = (id) => {
65
+ const el = els.find((x) => x.id === id);
66
+ if (!el?.group) return [id];
67
+ const gid = el.group;
68
+ return els.filter((x) => x.group === gid).map((x) => x.id);
69
+ };
70
+ const expandGroups = (ids) => {
71
+ const out = /* @__PURE__ */ new Set();
72
+ for (const id of ids) for (const member of groupOf(id)) out.add(member);
73
+ return [...out];
74
+ };
75
+ const cloneElements = (source, offset) => {
76
+ const groupMap = /* @__PURE__ */ new Map();
77
+ return source.map((el) => {
78
+ const copy = {
79
+ ...el,
80
+ id: dupId(el.id),
81
+ x: clamp(el.x + offset, 0, 100 - el.w),
82
+ y: clamp(el.y + offset, 0, 100 - el.h)
83
+ };
84
+ if (el.group) {
85
+ if (!groupMap.has(el.group)) groupMap.set(el.group, newGroupId());
86
+ copy.group = groupMap.get(el.group);
87
+ }
88
+ return copy;
89
+ });
90
+ };
91
+ const duplicateSelection = () => {
92
+ if (selEls.length === 0) return;
93
+ const copies = cloneElements(selEls, 4);
94
+ commit([...els, ...copies]);
95
+ setSelected(copies.map((c) => c.id));
96
+ };
97
+ const copySelection = () => {
98
+ if (selEls.length > 0) elementClipboard = selEls.map((el) => ({ ...el }));
99
+ };
100
+ const pasteClipboard = () => {
101
+ if (elementClipboard.length === 0) return;
102
+ const pasted = cloneElements(elementClipboard, 3);
103
+ commit([...els, ...pasted]);
104
+ setSelected(pasted.map((p) => p.id));
105
+ };
106
+ const deleteSelection = () => {
107
+ if (selEls.length === 0) return;
108
+ commit(els.filter((el) => !selectedSet.has(el.id)));
109
+ setSelected([]);
110
+ };
111
+ const groupSelection = () => {
112
+ if (selEls.length < 2) return;
113
+ const gid = newGroupId();
114
+ commit(els.map((el) => selectedSet.has(el.id) ? { ...el, group: gid } : el));
115
+ };
116
+ const ungroupSelection = () => {
117
+ if (!selEls.some((el) => el.group)) return;
118
+ commit(
119
+ els.map((el) => {
120
+ if (!selectedSet.has(el.id) || !el.group) return el;
121
+ const { group: _group, ...rest } = el;
122
+ return rest;
123
+ })
124
+ );
125
+ };
126
+ const reorderSelection = (dir, extreme = false) => {
127
+ if (selEls.length === 0) return;
128
+ if (extreme) {
129
+ const sel = els.filter((el) => selectedSet.has(el.id));
130
+ const rest = els.filter((el) => !selectedSet.has(el.id));
131
+ commit(dir === 1 ? [...rest, ...sel] : [...sel, ...rest]);
132
+ return;
133
+ }
134
+ const next = [...els];
135
+ if (dir === 1) {
136
+ for (let i = next.length - 2; i >= 0; i--) {
137
+ if (selectedSet.has(next[i].id) && !selectedSet.has(next[i + 1].id)) [next[i], next[i + 1]] = [next[i + 1], next[i]];
138
+ }
139
+ } else {
140
+ for (let i = 1; i < next.length; i++) {
141
+ if (selectedSet.has(next[i].id) && !selectedSet.has(next[i - 1].id)) [next[i], next[i - 1]] = [next[i - 1], next[i]];
142
+ }
143
+ }
144
+ commit(next);
145
+ };
146
+ const clampDelta = (dx, dy, targets) => {
147
+ let loX = -100, hiX = 100, loY = -100, hiY = 100;
148
+ for (const el of targets) {
149
+ loX = Math.max(loX, -el.x);
150
+ hiX = Math.min(hiX, 100 - el.w - el.x);
151
+ loY = Math.max(loY, -el.y);
152
+ hiY = Math.min(hiY, 100 - el.h - el.y);
153
+ }
154
+ return { dx: clamp(dx, loX, hiX), dy: clamp(dy, loY, hiY) };
155
+ };
156
+ const nudgeSelection = (ddx, ddy) => {
157
+ const { dx, dy } = clampDelta(ddx, ddy, selEls);
158
+ if (dx === 0 && dy === 0) return;
159
+ commit(els.map((el) => selectedSet.has(el.id) ? { ...el, x: el.x + dx, y: el.y + dy } : el));
160
+ };
161
+ const alignSelection = (op) => {
162
+ if (selEls.length < 2) return;
163
+ const b = boundsOf(selEls);
164
+ commit(
165
+ els.map((el) => {
166
+ if (!selectedSet.has(el.id)) return el;
167
+ if (op === "left") return { ...el, x: b.x1 };
168
+ if (op === "center") return { ...el, x: b.x1 + (b.x2 - b.x1 - el.w) / 2 };
169
+ if (op === "right") return { ...el, x: b.x2 - el.w };
170
+ if (op === "top") return { ...el, y: b.y1 };
171
+ if (op === "middle") return { ...el, y: b.y1 + (b.y2 - b.y1 - el.h) / 2 };
172
+ return { ...el, y: b.y2 - el.h };
173
+ })
174
+ );
175
+ };
176
+ const toPct = (e) => {
177
+ const rect = slideRef.current.getBoundingClientRect();
178
+ return {
179
+ x: clamp((e.clientX - rect.left) / rect.width * 100, 0, 100),
180
+ y: clamp((e.clientY - rect.top) / rect.height * 100, 0, 100)
181
+ };
182
+ };
183
+ const onDown = (e, el, mode) => {
184
+ if (editingId === el.id && mode === "move") return;
185
+ e.preventDefault();
186
+ e.stopPropagation();
187
+ slideRef.current?.focus();
188
+ if (mode === "move" && e.shiftKey) {
189
+ const unit = groupOf(el.id);
190
+ setSelected(
191
+ (prev) => prev.some((id) => unit.includes(id)) ? prev.filter((id) => !unit.includes(id)) : [...prev, ...unit.filter((id) => !prev.includes(id))]
192
+ );
193
+ return;
194
+ }
195
+ const wasSelected = selectedSet.has(el.id);
196
+ const ids = mode === "resize" ? [el.id] : wasSelected ? selected : groupOf(el.id);
197
+ if (!wasSelected) setSelected(mode === "resize" ? [el.id] : groupOf(el.id));
198
+ drag.current = {
199
+ ids,
200
+ id: el.id,
201
+ mode,
202
+ sx: e.clientX,
203
+ sy: e.clientY,
204
+ origs: new Map(els.filter((x) => ids.includes(x.id)).map((x) => [x.id, { ...x }])),
205
+ moved: false,
206
+ wasSelected
207
+ };
208
+ e.currentTarget.setPointerCapture(e.pointerId);
209
+ };
210
+ const onCanvasDown = (e) => {
211
+ if (e.target !== e.currentTarget) return;
212
+ e.preventDefault();
213
+ slideRef.current?.focus();
214
+ const p = toPct(e);
215
+ marquee.current = { x0: p.x, y0: p.y, x1: p.x, y1: p.y, moved: false };
216
+ e.currentTarget.setPointerCapture(e.pointerId);
217
+ };
218
+ const onMove = (e) => {
219
+ const rect = slideRef.current?.getBoundingClientRect();
220
+ if (!rect) return;
221
+ const mq = marquee.current;
222
+ if (mq) {
223
+ const p = toPct(e);
224
+ mq.x1 = p.x;
225
+ mq.y1 = p.y;
226
+ mq.moved = true;
227
+ setMarqueeBox({ x0: mq.x0, y0: mq.y0, x1: mq.x1, y1: mq.y1 });
228
+ const rx1 = Math.min(mq.x0, mq.x1), rx2 = Math.max(mq.x0, mq.x1);
229
+ const ry1 = Math.min(mq.y0, mq.y1), ry2 = Math.max(mq.y0, mq.y1);
230
+ const hits = els.filter((el) => el.x < rx2 && el.x + el.w > rx1 && el.y < ry2 && el.y + el.h > ry1).map((el) => el.id);
231
+ setSelected(expandGroups(hits));
232
+ return;
233
+ }
234
+ const d = drag.current;
235
+ if (!d) return;
236
+ const dx = (e.clientX - d.sx) / rect.width * 100;
237
+ const dy = (e.clientY - d.sy) / rect.height * 100;
238
+ if (Math.abs(dx) > 0.1 || Math.abs(dy) > 0.1) d.moved = true;
239
+ const orig = d.origs.get(d.id);
240
+ if (!orig) return;
241
+ const others = els.filter((el) => !d.ids.includes(el.id));
242
+ const xTargets = [0, 50, 100, ...others.flatMap((o) => [o.x, o.x + o.w / 2, o.x + o.w])];
243
+ const yTargets = [0, 50, 100, ...others.flatMap((o) => [o.y, o.y + o.h / 2, o.y + o.h])];
244
+ if (d.mode === "resize") {
245
+ if (e.shiftKey) {
246
+ const scale = Math.abs(dx) / orig.w > Math.abs(dy) / orig.h ? (orig.w + dx) / orig.w : (orig.h + dy) / orig.h;
247
+ const sMin = Math.max(6 / orig.w, 5 / orig.h);
248
+ const sMax = Math.min((100 - orig.x) / orig.w, (100 - orig.y) / orig.h);
249
+ const s = clamp(scale, sMin, sMax);
250
+ setGuides({ x: null, y: null });
251
+ setEls((prev) => prev.map((el) => el.id === d.id ? { ...el, w: orig.w * s, h: orig.h * s } : el));
252
+ return;
253
+ }
254
+ const gx = snapAxis(orig.x + clamp(orig.w + dx, 6, 100 - orig.x), 0, xTargets);
255
+ const gy = snapAxis(orig.y + clamp(orig.h + dy, 5, 100 - orig.y), 0, yTargets);
256
+ const w = clamp(gx.pos - orig.x, 6, 100 - orig.x);
257
+ const h = clamp(gy.pos - orig.y, 5, 100 - orig.y);
258
+ setGuides({
259
+ x: gx.guide !== null && Math.abs(orig.x + w - gx.guide) < 0.01 ? gx.guide : null,
260
+ y: gy.guide !== null && Math.abs(orig.y + h - gy.guide) < 0.01 ? gy.guide : null
261
+ });
262
+ setEls((prev) => prev.map((el) => el.id === d.id ? { ...el, w, h } : el));
263
+ return;
264
+ }
265
+ const origs = [...d.origs.values()];
266
+ const c = clampDelta(dx, dy, origs);
267
+ const sx = snapAxis(orig.x + c.dx, orig.w, xTargets);
268
+ const sy = snapAxis(orig.y + c.dy, orig.h, yTargets);
269
+ const snapped = clampDelta(sx.pos - orig.x, sy.pos - orig.y, origs);
270
+ setGuides({
271
+ x: sx.guide !== null && Math.abs(snapped.dx - (sx.pos - orig.x)) < 0.01 ? sx.guide : null,
272
+ y: sy.guide !== null && Math.abs(snapped.dy - (sy.pos - orig.y)) < 0.01 ? sy.guide : null
273
+ });
274
+ setEls(
275
+ (prev) => prev.map((el) => {
276
+ const o = d.origs.get(el.id);
277
+ return o ? { ...el, x: o.x + snapped.dx, y: o.y + snapped.dy } : el;
278
+ })
279
+ );
280
+ };
281
+ const onUp = () => {
282
+ if (marquee.current) {
283
+ if (marquee.current.moved) suppressCanvasClick.current = true;
284
+ marquee.current = null;
285
+ setMarqueeBox(null);
286
+ return;
287
+ }
288
+ const d = drag.current;
289
+ if (!d) return;
290
+ drag.current = null;
291
+ setGuides({ x: null, y: null });
292
+ if (d.moved) {
293
+ onChange(els);
294
+ return;
295
+ }
296
+ if (d.mode === "move" && d.wasSelected) setSelected(groupOf(d.id));
297
+ };
298
+ const onKeyDown = (e) => {
299
+ if (editingId) return;
300
+ const t = e.target;
301
+ if (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable) return;
302
+ const meta = e.metaKey || e.ctrlKey;
303
+ if (e.key === "Escape") {
304
+ setSelected([]);
305
+ setEditingId(null);
306
+ return;
307
+ }
308
+ if (meta && (e.key === "v" || e.key === "V")) {
309
+ e.preventDefault();
310
+ pasteClipboard();
311
+ return;
312
+ }
313
+ if (selEls.length === 0) return;
314
+ const step = e.shiftKey ? NUDGE_STEP_BIG : NUDGE_STEP;
315
+ if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "ArrowUp" || e.key === "ArrowDown") {
316
+ e.preventDefault();
317
+ if (e.key === "ArrowLeft") nudgeSelection(-step, 0);
318
+ else if (e.key === "ArrowRight") nudgeSelection(step, 0);
319
+ else if (e.key === "ArrowUp") nudgeSelection(0, -step);
320
+ else nudgeSelection(0, step);
321
+ } else if (e.key === "Delete" || e.key === "Backspace") {
322
+ e.preventDefault();
323
+ deleteSelection();
324
+ } else if (meta && (e.key === "d" || e.key === "D")) {
325
+ e.preventDefault();
326
+ duplicateSelection();
327
+ } else if (meta && (e.key === "c" || e.key === "C")) {
328
+ e.preventDefault();
329
+ copySelection();
330
+ } else if (meta && (e.key === "g" || e.key === "G")) {
331
+ e.preventDefault();
332
+ if (e.shiftKey) ungroupSelection();
333
+ else groupSelection();
334
+ } else if (meta && e.key === "]") {
335
+ e.preventDefault();
336
+ reorderSelection(1);
337
+ } else if (meta && e.key === "[") {
338
+ e.preventDefault();
339
+ reorderSelection(-1);
340
+ }
341
+ };
342
+ const multiBounds = selEls.length >= 2 ? boundsOf(selEls) : null;
343
+ const multibarBelow = multiBounds !== null && multiBounds.y1 < 16;
344
+ return /* @__PURE__ */ jsxs(
345
+ "div",
346
+ {
347
+ className: "cv-free",
348
+ ref: slideRef,
349
+ tabIndex: 0,
350
+ style: padding ? { inset: `${padding}%` } : void 0,
351
+ onPointerDown: onCanvasDown,
352
+ onPointerMove: onMove,
353
+ onPointerUp: onUp,
354
+ onPointerLeave: onUp,
355
+ onKeyDown,
356
+ onClick: () => {
357
+ if (suppressCanvasClick.current) {
358
+ suppressCanvasClick.current = false;
359
+ return;
360
+ }
361
+ setSelected([]);
362
+ setEditingId(null);
363
+ },
364
+ children: [
365
+ guides.x !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--v", style: { left: `${guides.x}%` } }),
366
+ guides.y !== null && /* @__PURE__ */ jsx("span", { className: "cv-free__guide cv-free__guide--h", style: { top: `${guides.y}%` } }),
367
+ marqueeBox && /* @__PURE__ */ jsx(
368
+ "span",
369
+ {
370
+ className: "cv-free__marquee",
371
+ style: {
372
+ left: `${Math.min(marqueeBox.x0, marqueeBox.x1)}%`,
373
+ top: `${Math.min(marqueeBox.y0, marqueeBox.y1)}%`,
374
+ width: `${Math.abs(marqueeBox.x1 - marqueeBox.x0)}%`,
375
+ height: `${Math.abs(marqueeBox.y1 - marqueeBox.y0)}%`
376
+ }
377
+ }
378
+ ),
379
+ els.map((el) => /* @__PURE__ */ jsxs(
380
+ "div",
381
+ {
382
+ className: `cv-free__el ${selectedSet.has(el.id) ? "is-selected cv-free__el--selected" : ""}`,
383
+ style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` },
384
+ onPointerDown: (e) => onDown(e, el, "move"),
385
+ onDoubleClick: (e) => {
386
+ if (el.type === "text") {
387
+ e.stopPropagation();
388
+ setEditingId(el.id);
389
+ }
390
+ },
391
+ onClick: (e) => e.stopPropagation(),
392
+ children: [
393
+ el.type === "text" ? /* @__PURE__ */ jsx(
394
+ "div",
395
+ {
396
+ className: "cv-free__text",
397
+ contentEditable: editingId === el.id,
398
+ suppressContentEditableWarning: true,
399
+ style: {
400
+ fontSize: el.fontSize ?? 24,
401
+ fontWeight: el.bold ? 700 : 400,
402
+ color: el.color,
403
+ textAlign: el.align ?? "left"
404
+ },
405
+ onBlur: (e) => {
406
+ setEditingId(null);
407
+ updateEl(el.id, { text: e.currentTarget.textContent ?? "" });
408
+ },
409
+ children: el.text
410
+ }
411
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) : /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "", draggable: false }),
412
+ soloId === 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: [
413
+ /* @__PURE__ */ jsx("button", { className: el.bold ? "is-on" : "", onClick: () => updateEl(el.id, { bold: !el.bold }), title: "Bold", children: /* @__PURE__ */ jsx("b", { children: "B" }) }),
414
+ /* @__PURE__ */ jsx(
415
+ "input",
416
+ {
417
+ type: "number",
418
+ min: 8,
419
+ max: 120,
420
+ value: el.fontSize ?? 24,
421
+ onChange: (e) => updateEl(el.id, { fontSize: Number(e.target.value) }),
422
+ title: "Font size"
423
+ }
424
+ ),
425
+ /* @__PURE__ */ jsx("input", { type: "color", value: el.color ?? "#1f2328", onChange: (e) => updateEl(el.id, { color: e.target.value }), title: "Text color" }),
426
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "left" }), title: "Align left", children: "\u27F8" }),
427
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "center" }), title: "Align center", children: "\u2261" }),
428
+ /* @__PURE__ */ jsx("button", { onClick: () => updateEl(el.id, { align: "right" }), title: "Align right", children: "\u27F9" })
429
+ ] }),
430
+ soloId === el.id && /* @__PURE__ */ jsxs(Fragment, { children: [
431
+ /* @__PURE__ */ jsx("span", { className: "cv-free__resize", title: "Resize (hold Shift to lock aspect)", onPointerDown: (e) => onDown(e, el, "resize") }),
432
+ /* @__PURE__ */ jsxs("div", { className: `cv-free__ctl ${el.y < 16 ? "cv-free__ctl--below" : ""}`, onPointerDown: (e) => e.stopPropagation(), children: [
433
+ 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" }),
434
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
435
+ e.stopPropagation();
436
+ duplicateSelection();
437
+ }, title: "Duplicate (\u2318D)", children: "\u29C9" }),
438
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
439
+ e.stopPropagation();
440
+ reorderSelection(1, true);
441
+ }, title: "Bring to front", children: "\u2912" }),
442
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
443
+ e.stopPropagation();
444
+ reorderSelection(1);
445
+ }, title: "Bring forward (\u2318])", children: "\u2191" }),
446
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
447
+ e.stopPropagation();
448
+ reorderSelection(-1);
449
+ }, title: "Send backward (\u2318[)", children: "\u2193" }),
450
+ /* @__PURE__ */ jsx("button", { onClick: (e) => {
451
+ e.stopPropagation();
452
+ reorderSelection(-1, true);
453
+ }, title: "Send to back", children: "\u2913" }),
454
+ /* @__PURE__ */ jsx("button", { className: "cv-free__ctl-del", onClick: (e) => {
455
+ e.stopPropagation();
456
+ deleteSelection();
457
+ }, title: "Delete", children: "\xD7" })
458
+ ] })
459
+ ] })
460
+ ]
461
+ },
462
+ el.id
463
+ )),
464
+ multiBounds && !marqueeBox && /* @__PURE__ */ jsxs(
465
+ "div",
466
+ {
467
+ className: `cv-free__multibar ${multibarBelow ? "cv-free__multibar--below" : ""}`,
468
+ style: {
469
+ left: `${(multiBounds.x1 + multiBounds.x2) / 2}%`,
470
+ top: multibarBelow ? `${multiBounds.y2}%` : `${multiBounds.y1}%`
471
+ },
472
+ onPointerDown: (e) => e.stopPropagation(),
473
+ onClick: (e) => e.stopPropagation(),
474
+ children: [
475
+ /* @__PURE__ */ jsx("button", { onClick: groupSelection, title: "Group (\u2318G)", children: "\u229E" }),
476
+ /* @__PURE__ */ jsx("button", { onClick: ungroupSelection, disabled: !selEls.some((el) => el.group), title: "Ungroup (\u2318\u21E7G)", children: "\u229F" }),
477
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("left"), title: "Align left edges", children: "\u21E4" }),
478
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("center"), title: "Align horizontal centers", children: "\u2194" }),
479
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("right"), title: "Align right edges", children: "\u21E5" }),
480
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("top"), title: "Align top edges", children: "\u2912" }),
481
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("middle"), title: "Align vertical centers", children: "\u2195" }),
482
+ /* @__PURE__ */ jsx("button", { onClick: () => alignSelection("bottom"), title: "Align bottom edges", children: "\u2913" }),
483
+ /* @__PURE__ */ jsx("button", { onClick: duplicateSelection, title: "Duplicate (\u2318D)", children: "\u29C9" }),
484
+ /* @__PURE__ */ jsx("button", { className: "cv-free__multibar-del", onClick: deleteSelection, title: "Delete selection", children: "\xD7" })
485
+ ]
486
+ }
487
+ )
488
+ ]
489
+ }
490
+ );
491
+ }
492
+ var THEMES = [
493
+ // Light — warm white + crimson serif, magazine front-of-book.
494
+ { id: "editorial", label: "Editorial", bg: "#f9f8f4", text: "#1c1a17", accent: "#a51c30", font: '"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif' },
495
+ // Light — pure white, pure black, no color at all: gallery-catalog restraint.
496
+ { id: "gallery", label: "Gallery", bg: "#ffffff", text: "#111111", accent: "#111111", font: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
497
+ // Light — cool paper + navy ink + cobalt, the quiet corporate default.
498
+ { id: "boardroom", label: "Boardroom", bg: "#f3f5f8", text: "#142743", accent: "#1f56c9", font: 'ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif' },
499
+ // Light — sage paper + moss ink + fern, humanist and unhurried.
500
+ { id: "sage", label: "Sage", bg: "#edf0e8", text: "#273428", accent: "#4c7a4f", font: 'Seravek, "Gill Sans", "Trebuchet MS", Verdana, sans-serif' },
501
+ // Dark — soft charcoal + amber, geometric grotesque; studio pitch deck.
502
+ { id: "graphite", label: "Graphite", bg: "#1e1f21", text: "#f1f0ed", accent: "#ecb22e", font: '"Avenir Next", Avenir, Futura, "Century Gothic", sans-serif' },
503
+ // Dark — midnight blue + starlight gold, high-contrast didone serif.
504
+ { id: "observatory", label: "Observatory", bg: "#0d1526", text: "#e2e8f4", accent: "#d4af6a", font: 'Didot, "Bodoni MT", "Bodoni 72", Georgia, serif' },
505
+ // Mid — Klein blue + signal yellow, Swiss poster energy.
506
+ { id: "ultramarine", label: "Ultramarine", bg: "#002fa7", text: "#f2f5ff", accent: "#ffd02f", font: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
507
+ // Dark — wine-cellar aubergine + rosé, old-style serif; dinner-party keynote.
508
+ { id: "bordeaux", label: "Bordeaux", bg: "#241722", text: "#f0e8ee", accent: "#d98ca0", font: '"Hoefler Text", Baskerville, "Baskerville Old Face", Georgia, serif' }
509
+ ];
510
+ var FALLBACK_ACCENT = "#5b5bd6";
511
+ var SHAPES = [
512
+ { id: "rect", label: "\u25AD Rectangle", box: { w: 30, h: 20 } },
513
+ { id: "ellipse", label: "\u25EF Ellipse", box: { w: 24, h: 24 } },
514
+ { id: "line", label: "\u2014 Line", box: { w: 40, h: 2 } }
515
+ ];
516
+ var LAYOUTS = {
517
+ title: { label: "Title", els: (accent) => [
518
+ { type: "shape", shape: "rect", x: 8, y: 30, w: 7, h: 1.4, fill: accent },
519
+ { type: "text", x: 8, y: 35, w: 72, h: 20, text: "Presentation title", fontSize: 52, bold: true },
520
+ { type: "text", x: 8, y: 57, w: 56, h: 8, text: "A one-line summary of the story", fontSize: 22 },
521
+ { type: "text", x: 8, y: 86, w: 50, h: 5, text: "Team name \xB7 Date", fontSize: 14 }
522
+ ] },
523
+ section: { label: "Section divider", els: (accent) => [
524
+ { type: "text", x: 8, y: 12, w: 26, h: 26, text: "01", fontSize: 104, bold: true, color: accent },
525
+ { type: "shape", shape: "rect", x: 8, y: 50, w: 84, h: 0.5, fill: accent },
526
+ { type: "text", x: 8, y: 55, w: 76, h: 14, text: "Section title", fontSize: 44, bold: true },
527
+ { type: "text", x: 8, y: 71, w: 60, h: 7, text: "What this chapter covers, in one line", fontSize: 20 }
528
+ ] },
529
+ agenda: { label: "Agenda", els: (accent) => [
530
+ { type: "text", x: 8, y: 10, w: 40, h: 10, text: "Agenda", fontSize: 40, bold: true },
531
+ { type: "shape", shape: "rect", x: 8, y: 22, w: 7, h: 1.2, fill: accent },
532
+ { type: "text", x: 8, y: 32, w: 7, h: 8, text: "01", fontSize: 20, bold: true, color: accent },
533
+ { type: "text", x: 17, y: 32, w: 66, h: 8, text: "First topic", fontSize: 24 },
534
+ { type: "text", x: 8, y: 46, w: 7, h: 8, text: "02", fontSize: 20, bold: true, color: accent },
535
+ { type: "text", x: 17, y: 46, w: 66, h: 8, text: "Second topic", fontSize: 24 },
536
+ { type: "text", x: 8, y: 60, w: 7, h: 8, text: "03", fontSize: 20, bold: true, color: accent },
537
+ { type: "text", x: 17, y: 60, w: 66, h: 8, text: "Third topic", fontSize: 24 },
538
+ { type: "text", x: 8, y: 74, w: 7, h: 8, text: "04", fontSize: 20, bold: true, color: accent },
539
+ { type: "text", x: 17, y: 74, w: 66, h: 8, text: "Fourth topic", fontSize: 24 }
540
+ ] },
541
+ bullets: { label: "Bullets", els: (accent) => [
542
+ { type: "text", x: 8, y: 9, w: 60, h: 5, text: "Where we are", fontSize: 14, bold: true, color: accent },
543
+ { type: "text", x: 8, y: 16, w: 80, h: 12, text: "Heading that states the takeaway", fontSize: 38, bold: true },
544
+ { type: "text", x: 8, y: 34, w: 78, h: 54, text: "\u2022 First key point, one line each\n\n\u2022 Second key point\n\n\u2022 Third key point", fontSize: 22 }
545
+ ] },
546
+ "two-column": { label: "Two column", els: (accent) => [
547
+ { type: "text", x: 8, y: 10, w: 84, h: 10, text: "Two sides of the argument", fontSize: 36, bold: true },
548
+ { type: "shape", shape: "rect", x: 49.9, y: 28, w: 0.2, h: 54, fill: accent },
549
+ { type: "text", x: 8, y: 28, w: 36, h: 6, text: "Before", fontSize: 20, bold: true, color: accent },
550
+ { type: "text", x: 8, y: 37, w: 36, h: 45, text: "\u2022 point\n\n\u2022 point\n\n\u2022 point", fontSize: 20 },
551
+ { type: "text", x: 56, y: 28, w: 36, h: 6, text: "After", fontSize: 20, bold: true, color: accent },
552
+ { type: "text", x: 56, y: 37, w: 36, h: 45, text: "\u2022 point\n\n\u2022 point\n\n\u2022 point", fontSize: 20 }
553
+ ] },
554
+ stat: { label: "Big stat", els: (accent) => [
555
+ { type: "text", x: 8, y: 14, w: 60, h: 6, text: "The number that matters", fontSize: 14, bold: true },
556
+ { type: "text", x: 8, y: 24, w: 66, h: 32, text: "3.4\xD7", fontSize: 116, bold: true, color: accent },
557
+ { type: "shape", shape: "rect", x: 8, y: 64, w: 10, h: 0.8, fill: accent },
558
+ { type: "text", x: 8, y: 69, w: 62, h: 12, text: "One line explaining what the number means and why it matters", fontSize: 24 }
559
+ ] },
560
+ quote: { label: "Quote", els: (accent) => [
561
+ { type: "text", x: 6, y: 6, w: 12, h: 18, text: "\u201C", fontSize: 120, bold: true, color: accent },
562
+ { type: "text", x: 14, y: 28, w: 72, h: 30, text: "The quote \u2014 one strong sentence people will remember.", fontSize: 36, bold: true },
563
+ { type: "shape", shape: "rect", x: 14, y: 64, w: 8, h: 0.8, fill: accent },
564
+ { type: "text", x: 14, y: 68, w: 56, h: 7, text: "Name Surname \u2014 Role, Company", fontSize: 18 }
565
+ ] }
566
+ };
567
+ var elementSeq = 0;
568
+ var newElementId = () => `el_${Date.now().toString(36)}_${elementSeq++}`;
569
+ function SlidesRenderer({ artifact }) {
570
+ const slides = artifact.data.slides ?? [];
571
+ const patch = useArtifactPatch(artifact.id);
572
+ const [index, setIndex] = useState(0);
573
+ const [dragIndex, setDragIndex] = useState(null);
574
+ const [presenting, setPresenting] = useState(false);
575
+ const imgRef = useRef(null);
576
+ const bgRef = useRef(null);
577
+ useEffect(() => {
578
+ if (!presenting) return;
579
+ const onKey = (e) => {
580
+ if (e.key === "ArrowRight" || e.key === " ") setIndex((i) => Math.min(i + 1, slides.length - 1));
581
+ else if (e.key === "ArrowLeft") setIndex((i) => Math.max(i - 1, 0));
582
+ else if (e.key === "Escape") setPresenting(false);
583
+ };
584
+ window.addEventListener("keydown", onKey);
585
+ return () => window.removeEventListener("keydown", onKey);
586
+ }, [presenting]);
587
+ if (slides.length === 0) {
588
+ return /* @__PURE__ */ jsx("div", { className: "cv-deck cv-deck--empty", children: "No slides yet\u2026" });
589
+ }
590
+ const at = Math.min(index, slides.length - 1);
591
+ const slide = slides[at];
592
+ const slideStyle = {
593
+ ...slide.background ? { background: slide.background } : {},
594
+ ...slide.textColor ? { color: slide.textColor } : {},
595
+ ...slide.fontFamily ? { fontFamily: slide.fontFamily } : {}
596
+ };
597
+ const accent = slide.accent ?? FALLBACK_ACCENT;
598
+ const setSlides = (next) => patch({ slides: next });
599
+ const update = (partial) => setSlides(slides.map((s, i) => i === at ? { ...s, ...partial } : s));
600
+ const addSlide = () => {
601
+ const next = [...slides];
602
+ 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 }] });
603
+ setSlides(next);
604
+ setIndex(at + 1);
605
+ };
606
+ const duplicateSlide = () => {
607
+ const next = [...slides];
608
+ next.splice(at + 1, 0, { ...slide, elements: resolveElements(slide).map((e) => ({ ...e })) });
609
+ setSlides(next);
610
+ setIndex(at + 1);
611
+ };
612
+ const deleteSlide = () => {
613
+ if (slides.length === 1) return;
614
+ setSlides(slides.filter((_, i) => i !== at));
615
+ setIndex(Math.max(0, at - 1));
616
+ };
617
+ const moveSlide = (dir) => {
618
+ const j = at + dir;
619
+ if (j < 0 || j >= slides.length) return;
620
+ const next = [...slides];
621
+ [next[at], next[j]] = [next[j], next[at]];
622
+ setSlides(next);
623
+ setIndex(j);
624
+ };
625
+ const reorder = (from, to) => {
626
+ if (from === to) return;
627
+ const next = [...slides];
628
+ const [moved] = next.splice(from, 1);
629
+ next.splice(to, 0, moved);
630
+ setSlides(next);
631
+ setIndex(to);
632
+ };
633
+ const addElement = (el) => update({ elements: [...resolveElements(slide), { ...el, id: newElementId() }] });
634
+ const addTextEl = () => addElement({ type: "text", x: 12, y: 16, w: 45, h: 16, text: "Text", fontSize: 24 });
635
+ const addImageEl = (file) => {
636
+ if (!file) return;
637
+ const reader = new FileReader();
638
+ reader.onload = () => addElement({ type: "image", x: 22, y: 22, w: 40, h: 34, src: String(reader.result) });
639
+ reader.readAsDataURL(file);
640
+ };
641
+ const addShapeEl = (shape) => {
642
+ const s = SHAPES.find((x) => x.id === shape);
643
+ if (s) addElement({ type: "shape", shape, x: 20, y: 20, w: s.box.w, h: s.box.h, fill: accent });
644
+ };
645
+ const applyLayout = (key) => {
646
+ const l = LAYOUTS[key];
647
+ if (l) update({ elements: l.els(accent).map((e) => ({ ...e, id: newElementId() })) });
648
+ };
649
+ const setSlideBgImage = (file) => {
650
+ if (!file) return;
651
+ const reader = new FileReader();
652
+ reader.onload = () => update({ background: `#000 url("${String(reader.result)}") center/cover no-repeat` });
653
+ reader.readAsDataURL(file);
654
+ };
655
+ const arrange = (op) => {
656
+ const els = resolveElements(slide);
657
+ if (els.length === 0) return;
658
+ const GAP = 4;
659
+ const M = 6;
660
+ let next;
661
+ if (op === "stack-v") {
662
+ let y = M;
663
+ next = [...els].sort((a, b) => a.y - b.y).map((el) => {
664
+ const placed = { ...el, x: (100 - el.w) / 2, y };
665
+ y += el.h + GAP;
666
+ return placed;
667
+ });
668
+ } else if (op === "stack-h") {
669
+ let x = M;
670
+ next = [...els].sort((a, b) => a.x - b.x).map((el) => {
671
+ const placed = { ...el, x, y: (100 - el.h) / 2 };
672
+ x += el.w + GAP;
673
+ return placed;
674
+ });
675
+ } else if (op === "align-left") next = els.map((el) => ({ ...el, x: M }));
676
+ else if (op === "align-center") next = els.map((el) => ({ ...el, x: (100 - el.w) / 2 }));
677
+ else if (op === "align-right") next = els.map((el) => ({ ...el, x: 100 - M - el.w }));
678
+ else if (op === "align-top") next = els.map((el) => ({ ...el, y: M }));
679
+ else if (op === "align-middle") next = els.map((el) => ({ ...el, y: (100 - el.h) / 2 }));
680
+ else if (op === "align-bottom") next = els.map((el) => ({ ...el, y: 100 - M - el.h }));
681
+ else if (op === "dist-v" || op === "dist-h") {
682
+ if (els.length < 3) return;
683
+ const k = op === "dist-v" ? "y" : "x";
684
+ const sk = op === "dist-v" ? "h" : "w";
685
+ const sorted = [...els].sort((a, b) => a[k] - b[k]);
686
+ const start = sorted[0][k];
687
+ const end = sorted[sorted.length - 1][k] + sorted[sorted.length - 1][sk];
688
+ const total = sorted.reduce((acc, e) => acc + e[sk], 0);
689
+ const g = (end - start - total) / (sorted.length - 1);
690
+ let pos = start;
691
+ const placed = /* @__PURE__ */ new Map();
692
+ for (const el of sorted) {
693
+ placed.set(el.id, { ...el, [k]: pos });
694
+ pos += el[sk] + g;
695
+ }
696
+ next = els.map((e) => placed.get(e.id) ?? e);
697
+ } else return;
698
+ update({ elements: next });
699
+ };
700
+ return /* @__PURE__ */ jsxs("div", { className: "cv-deck", children: [
701
+ /* @__PURE__ */ jsxs("aside", { className: "cv-deck__rail cv-chrome", children: [
702
+ slides.map((s, i) => /* @__PURE__ */ jsx(
703
+ "div",
704
+ {
705
+ className: `cv-deck__thumb-wrap ${i === at ? "is-active" : ""} ${dragIndex === i ? "is-dragging" : ""}`,
706
+ draggable: true,
707
+ onDragStart: () => setDragIndex(i),
708
+ onDragOver: (e) => e.preventDefault(),
709
+ onDrop: () => {
710
+ if (dragIndex !== null) reorder(dragIndex, i);
711
+ setDragIndex(null);
712
+ },
713
+ onDragEnd: () => setDragIndex(null),
714
+ children: /* @__PURE__ */ jsxs("button", { className: "cv-deck__thumb", onClick: () => setIndex(i), children: [
715
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__thumb-n", children: i + 1 }),
716
+ /* @__PURE__ */ jsx(
717
+ "div",
718
+ {
719
+ className: "cv-deck__thumb-slide",
720
+ style: {
721
+ ...s.background ? { background: s.background } : {},
722
+ ...s.fontFamily ? { fontFamily: s.fontFamily } : {}
723
+ },
724
+ children: /* @__PURE__ */ jsx("div", { style: { position: "absolute", inset: `${s.padding ?? 0}%` }, children: resolveElements(s).map(
725
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx(
726
+ "span",
727
+ {
728
+ 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" },
729
+ children: el.text
730
+ },
731
+ el.id
732
+ ) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: s.textColor, ...shapeStyle(el) } }, el.id) : /* @__PURE__ */ jsx("img", { src: el.src, alt: "", style: { position: "absolute", left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, objectFit: "contain" } }, el.id)
733
+ ) })
734
+ }
735
+ )
736
+ ] })
737
+ },
738
+ i
739
+ )),
740
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__addslide", onClick: addSlide, children: "+ Add slide" })
741
+ ] }),
742
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__main", children: [
743
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__toolbar cv-chrome", children: [
744
+ /* @__PURE__ */ jsx("button", { onClick: addTextEl, title: "Add text box", children: "+ Text" }),
745
+ /* @__PURE__ */ jsx("button", { onClick: () => imgRef.current?.click(), title: "Add image", children: "+ Image" }),
746
+ /* @__PURE__ */ jsx("input", { ref: imgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => addImageEl(e.target.files?.[0]) }),
747
+ /* @__PURE__ */ jsx("input", { ref: bgRef, type: "file", accept: "image/*", hidden: true, onChange: (e) => setSlideBgImage(e.target.files?.[0]) }),
748
+ /* @__PURE__ */ jsxs(
749
+ "select",
750
+ {
751
+ className: "cv-deck__theme",
752
+ value: "",
753
+ title: "Add a shape",
754
+ onChange: (e) => {
755
+ if (e.target.value) addShapeEl(e.target.value);
756
+ e.currentTarget.value = "";
757
+ },
758
+ children: [
759
+ /* @__PURE__ */ jsx("option", { value: "", children: "+ Shape" }),
760
+ SHAPES.map((s) => /* @__PURE__ */ jsx("option", { value: s.id, children: s.label }, s.id))
761
+ ]
762
+ }
763
+ ),
764
+ /* @__PURE__ */ jsxs(
765
+ "select",
766
+ {
767
+ className: "cv-deck__theme",
768
+ value: "",
769
+ title: "Apply a layout",
770
+ onChange: (e) => {
771
+ if (e.target.value) applyLayout(e.target.value);
772
+ e.currentTarget.value = "";
773
+ },
774
+ children: [
775
+ /* @__PURE__ */ jsx("option", { value: "", children: "Layout\u2026" }),
776
+ Object.entries(LAYOUTS).map(([k, v]) => /* @__PURE__ */ jsx("option", { value: k, children: v.label }, k))
777
+ ]
778
+ }
779
+ ),
780
+ /* @__PURE__ */ jsxs(
781
+ "select",
782
+ {
783
+ className: "cv-deck__theme",
784
+ value: "",
785
+ title: "Auto layout \u2014 align & distribute elements",
786
+ onChange: (e) => {
787
+ if (e.target.value) arrange(e.target.value);
788
+ e.currentTarget.value = "";
789
+ },
790
+ children: [
791
+ /* @__PURE__ */ jsx("option", { value: "", children: "\u2922 Arrange\u2026" }),
792
+ /* @__PURE__ */ jsxs("optgroup", { label: "Auto layout", children: [
793
+ /* @__PURE__ */ jsx("option", { value: "stack-v", children: "Stack vertical" }),
794
+ /* @__PURE__ */ jsx("option", { value: "stack-h", children: "Stack horizontal" })
795
+ ] }),
796
+ /* @__PURE__ */ jsxs("optgroup", { label: "Align", children: [
797
+ /* @__PURE__ */ jsx("option", { value: "align-left", children: "Left" }),
798
+ /* @__PURE__ */ jsx("option", { value: "align-center", children: "Center" }),
799
+ /* @__PURE__ */ jsx("option", { value: "align-right", children: "Right" }),
800
+ /* @__PURE__ */ jsx("option", { value: "align-top", children: "Top" }),
801
+ /* @__PURE__ */ jsx("option", { value: "align-middle", children: "Middle" }),
802
+ /* @__PURE__ */ jsx("option", { value: "align-bottom", children: "Bottom" })
803
+ ] }),
804
+ /* @__PURE__ */ jsxs("optgroup", { label: "Distribute", children: [
805
+ /* @__PURE__ */ jsx("option", { value: "dist-v", children: "Vertical gaps" }),
806
+ /* @__PURE__ */ jsx("option", { value: "dist-h", children: "Horizontal gaps" })
807
+ ] })
808
+ ]
809
+ }
810
+ ),
811
+ /* @__PURE__ */ jsxs(
812
+ "select",
813
+ {
814
+ className: "cv-deck__theme",
815
+ value: "",
816
+ title: "Theme",
817
+ onChange: (e) => {
818
+ const t = THEMES.find((x) => x.id === e.target.value);
819
+ if (t) update({ background: t.bg, textColor: t.text, accent: t.accent, fontFamily: t.font });
820
+ e.currentTarget.value = "";
821
+ },
822
+ children: [
823
+ /* @__PURE__ */ jsx("option", { value: "", children: "Theme\u2026" }),
824
+ THEMES.map((t) => /* @__PURE__ */ jsx("option", { value: t.id, children: t.label }, t.id))
825
+ ]
826
+ }
827
+ ),
828
+ /* @__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 }) }) }),
829
+ /* @__PURE__ */ jsx("button", { onClick: () => bgRef.current?.click(), title: "Background image", children: "\u{1F5BC} BG" }),
830
+ /* @__PURE__ */ jsxs("label", { className: "cv-deck__pad", title: "Content padding (% of slide)", children: [
831
+ "Pad",
832
+ /* @__PURE__ */ jsx("input", { type: "number", min: 0, max: 20, value: slide.padding ?? 0, onChange: (e) => update({ padding: Number(e.target.value) || void 0 }) })
833
+ ] }),
834
+ /* @__PURE__ */ jsx("span", { className: "cv-deck__spacer" }),
835
+ /* @__PURE__ */ jsx("button", { className: "cv-deck__present", onClick: () => setPresenting(true), title: "Present (full screen)", children: "\u25B6 Present" }),
836
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(-1), title: "Move up", disabled: at === 0, children: "\u25B2" }),
837
+ /* @__PURE__ */ jsx("button", { onClick: () => moveSlide(1), title: "Move down", disabled: at === slides.length - 1, children: "\u25BC" }),
838
+ /* @__PURE__ */ jsx("button", { onClick: duplicateSlide, title: "Duplicate slide", children: "\u29C9" }),
839
+ /* @__PURE__ */ jsx("button", { onClick: deleteSlide, title: "Delete slide", disabled: slides.length === 1, children: "\u{1F5D1}" })
840
+ ] }),
841
+ /* @__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 }) }),
842
+ /* @__PURE__ */ jsxs("div", { className: "cv-deck__nav cv-chrome", children: [
843
+ /* @__PURE__ */ jsx("button", { disabled: at === 0, onClick: () => setIndex(at - 1), "aria-label": "Previous slide", children: "\u2039" }),
844
+ /* @__PURE__ */ jsxs("span", { children: [
845
+ at + 1,
846
+ " / ",
847
+ slides.length
848
+ ] }),
849
+ /* @__PURE__ */ jsx("button", { disabled: at === slides.length - 1, onClick: () => setIndex(at + 1), "aria-label": "Next slide", children: "\u203A" })
850
+ ] }),
851
+ /* @__PURE__ */ jsx(
852
+ "textarea",
853
+ {
854
+ className: "cv-deck__notes cv-chrome",
855
+ value: slide.notes ?? "",
856
+ placeholder: "Speaker notes\u2026",
857
+ onChange: (e) => update({ notes: e.target.value })
858
+ }
859
+ )
860
+ ] }),
861
+ presenting && /* @__PURE__ */ jsxs("div", { className: "cv-present", onClick: () => setIndex(Math.min(at + 1, slides.length - 1)), children: [
862
+ /* @__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(
863
+ (el) => el.type === "text" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("div", { className: "cv-free__text", style: { fontSize: el.fontSize ?? 24, fontWeight: el.bold ? 700 : 400, color: el.color, textAlign: el.align ?? "left", whiteSpace: "pre-wrap" }, children: el.text }) }, el.id) : el.type === "shape" ? /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%`, color: slide.textColor }, children: /* @__PURE__ */ jsx("div", { style: shapeStyle(el) }) }, el.id) : /* @__PURE__ */ jsx("div", { className: "cv-free__el", style: { left: `${el.x}%`, top: `${el.y}%`, width: `${el.w}%`, height: `${el.h}%` }, children: /* @__PURE__ */ jsx("img", { className: "cv-free__img", src: el.src, alt: "" }) }, el.id)
864
+ ) }) }, at),
865
+ slide.notes ? /* @__PURE__ */ jsx("div", { className: "cv-present__notes", onClick: (e) => e.stopPropagation(), children: slide.notes }) : null,
866
+ /* @__PURE__ */ jsx("div", { className: "cv-present__progress", children: /* @__PURE__ */ jsx("span", { className: "cv-present__progress-fill", style: { width: `${(at + 1) / slides.length * 100}%` } }) }),
867
+ /* @__PURE__ */ jsxs("div", { className: "cv-present__count", children: [
868
+ at + 1,
869
+ " / ",
870
+ slides.length
871
+ ] }),
872
+ /* @__PURE__ */ jsx("div", { className: "cv-present__hint", children: "\u2190 \u2192 to navigate \xB7 Esc to exit" })
873
+ ] })
874
+ ] });
875
+ }
876
+
877
+ export { SlidesRenderer };