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