@braincrew-lab/langchain-canvas 0.3.0 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,964 +0,0 @@
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 };