@particle-academy/fancy-slides 0.1.3

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.
package/dist/index.js ADDED
@@ -0,0 +1,1761 @@
1
+ import { useId, useRef, useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { ContentRenderer, Text, Action, ContextMenu, Separator, Tooltip, Dropdown, Badge, Heading, Tabs, Card, Input, Slider, Textarea, Select, ColorPicker } from '@particle-academy/react-fancy';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/components/Slide/Slide.tsx
6
+
7
+ // src/theme/default-theme.ts
8
+ var defaultTheme = {
9
+ name: "default",
10
+ aspectRatio: 16 / 9,
11
+ slideWidth: 1920,
12
+ colors: {
13
+ background: "#ffffff",
14
+ text: "#0f172a",
15
+ muted: "#64748b",
16
+ accent: "#8b5cf6",
17
+ surface: "#f8fafc"
18
+ },
19
+ fonts: {
20
+ heading: '"Instrument Sans", ui-sans-serif, system-ui, sans-serif',
21
+ body: '"Instrument Sans", ui-sans-serif, system-ui, sans-serif',
22
+ mono: 'ui-monospace, "JetBrains Mono", "Fira Code", monospace'
23
+ },
24
+ defaultTransition: { kind: "fade", duration: 200 }
25
+ };
26
+ var darkTheme = {
27
+ ...defaultTheme,
28
+ name: "dark",
29
+ colors: {
30
+ background: "#0b1220",
31
+ text: "#f8fafc",
32
+ muted: "#94a3b8",
33
+ accent: "#a855f7",
34
+ surface: "#1e293b"
35
+ }
36
+ };
37
+ var vividTheme = {
38
+ ...defaultTheme,
39
+ name: "vivid",
40
+ colors: {
41
+ background: "#0f172a",
42
+ text: "#f8fafc",
43
+ muted: "#cbd5e1",
44
+ accent: "#22d3ee",
45
+ surface: "#1e293b"
46
+ }
47
+ };
48
+ var builtinThemes = {
49
+ default: defaultTheme,
50
+ dark: darkTheme,
51
+ vivid: vividTheme
52
+ };
53
+
54
+ // src/theme/theme-utils.ts
55
+ function defineTheme(overrides) {
56
+ return {
57
+ ...defaultTheme,
58
+ ...overrides,
59
+ colors: { ...defaultTheme.colors, ...overrides.colors },
60
+ fonts: { ...defaultTheme.fonts, ...overrides.fonts },
61
+ defaultTransition: overrides.defaultTransition ?? defaultTheme.defaultTransition
62
+ };
63
+ }
64
+ function resolveTheme(theme) {
65
+ if (!theme) return defaultTheme;
66
+ return defineTheme(theme);
67
+ }
68
+
69
+ // src/utils/cn.ts
70
+ function cn(...parts) {
71
+ return parts.filter(Boolean).join(" ");
72
+ }
73
+ function TextElementRenderer({
74
+ element,
75
+ theme,
76
+ slideWidthPx,
77
+ editing = false,
78
+ selected = false,
79
+ onContentChange
80
+ }) {
81
+ const t = resolveTheme(theme);
82
+ const style = element.style ?? {};
83
+ const designWidth = t.slideWidth ?? 1920;
84
+ const scale = slideWidthPx / designWidth;
85
+ const format = element.format ?? "markdown";
86
+ const scopeId = useId();
87
+ const css = {
88
+ fontFamily: style.fontFamily ?? t.fonts?.body,
89
+ fontSize: `${(style.fontSize ?? 28) * scale}px`,
90
+ fontWeight: weight(style.weight) ?? 400,
91
+ fontStyle: style.italic ? "italic" : "normal",
92
+ textDecoration: style.underline ? "underline" : "none",
93
+ color: style.color ?? t.colors?.text,
94
+ textAlign: style.align ?? "left",
95
+ lineHeight: style.lineHeight ?? 1.4,
96
+ display: "flex",
97
+ flexDirection: "column",
98
+ justifyContent: style.verticalAlign === "middle" ? "center" : style.verticalAlign === "bottom" ? "flex-end" : "flex-start",
99
+ width: "100%",
100
+ height: "100%",
101
+ padding: 0,
102
+ margin: 0,
103
+ outline: "none",
104
+ background: "transparent",
105
+ whiteSpace: format === "plain" ? "pre-wrap" : "normal",
106
+ wordBreak: "break-word",
107
+ overflow: "hidden"
108
+ };
109
+ if (editing && selected) {
110
+ return /* @__PURE__ */ jsx(
111
+ "textarea",
112
+ {
113
+ value: element.content,
114
+ onChange: (e) => onContentChange?.(e.target.value),
115
+ style: {
116
+ ...css,
117
+ whiteSpace: "pre-wrap",
118
+ resize: "none",
119
+ border: "none",
120
+ pointerEvents: "auto",
121
+ cursor: "text"
122
+ }
123
+ }
124
+ );
125
+ }
126
+ if (format === "plain") {
127
+ return /* @__PURE__ */ jsx("div", { style: css, children: element.content });
128
+ }
129
+ const proseScope = `[data-fs-text-scope="${scopeId}"]`;
130
+ const doubleScope = `${proseScope}${proseScope}`;
131
+ return /* @__PURE__ */ jsxs("div", { "data-fs-text-scope": scopeId, style: css, children: [
132
+ /* @__PURE__ */ jsx("style", { children: `
133
+ ${proseScope} > div { width: 100%; height: 100%; font-size: inherit; }
134
+ ${doubleScope} :is(p, ul, ol, li, blockquote, h1, h2, h3, h4, h5, h6, pre, code, strong, em, a) {
135
+ font-size: inherit;
136
+ }
137
+ ${doubleScope} h1 { font-size: 1.6em; font-weight: 700; }
138
+ ${doubleScope} h2 { font-size: 1.35em; font-weight: 700; }
139
+ ${doubleScope} h3 { font-size: 1.15em; font-weight: 600; }
140
+ ${proseScope} :where(p, ul, ol, h1, h2, h3, h4, h5, h6, pre, blockquote) {
141
+ margin: 0;
142
+ padding: 0;
143
+ }
144
+ ${proseScope} :where(p, li) + :where(p, li, ul, ol) { margin-top: 0.4em; }
145
+ ${proseScope} :where(ul, ol) { padding-left: 1.4em; }
146
+ ${proseScope} :where(strong) { font-weight: ${Math.max(700, weight(style.weight) ?? 400 + 200)}; }
147
+ ${proseScope} :where(a) { color: inherit; text-decoration: underline; }
148
+ ${proseScope} :where(code) { font-family: ${t.fonts?.mono ?? "monospace"}; }
149
+ ` }),
150
+ /* @__PURE__ */ jsx(ContentRenderer, { value: element.content, format: format === "html" ? "html" : "markdown" })
151
+ ] });
152
+ }
153
+ function weight(w) {
154
+ if (typeof w === "number") return w;
155
+ if (w === "normal") return 400;
156
+ if (w === "medium") return 500;
157
+ if (w === "semibold") return 600;
158
+ if (w === "bold") return 700;
159
+ return void 0;
160
+ }
161
+ function ImageElementRenderer({ element }) {
162
+ return /* @__PURE__ */ jsx(
163
+ "img",
164
+ {
165
+ src: element.src,
166
+ alt: element.alt ?? "",
167
+ style: {
168
+ width: "100%",
169
+ height: "100%",
170
+ objectFit: element.fit ?? "contain",
171
+ display: "block"
172
+ },
173
+ draggable: false
174
+ }
175
+ );
176
+ }
177
+ function ShapeElementRenderer({ element, theme, slideWidthPx }) {
178
+ const t = resolveTheme(theme);
179
+ const designWidth = t.slideWidth ?? 1920;
180
+ const scale = slideWidthPx / designWidth;
181
+ const fill = element.fill ?? "rgba(139, 92, 246, 0.15)";
182
+ const stroke = element.stroke ?? t.colors?.accent ?? "#8b5cf6";
183
+ const strokeWidth = (element.strokeWidth ?? 2) * scale;
184
+ const dasharray = element.dashed ? `${6 * scale} ${4 * scale}` : void 0;
185
+ return /* @__PURE__ */ jsx(
186
+ "svg",
187
+ {
188
+ viewBox: "0 0 100 100",
189
+ preserveAspectRatio: "none",
190
+ style: { width: "100%", height: "100%", display: "block", overflow: "visible" },
191
+ children: renderShape(element, { fill, stroke, strokeWidth, dasharray })
192
+ }
193
+ );
194
+ }
195
+ function renderShape(el, s) {
196
+ const common = {
197
+ fill: s.fill,
198
+ stroke: s.stroke,
199
+ strokeWidth: s.strokeWidth,
200
+ strokeDasharray: s.dasharray,
201
+ vectorEffect: "non-scaling-stroke"
202
+ };
203
+ switch (el.shape) {
204
+ case "rect":
205
+ return /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "100", height: "100", ...common });
206
+ case "rounded-rect": {
207
+ const r = el.radius ?? 8;
208
+ return /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "100", height: "100", rx: r, ry: r, ...common });
209
+ }
210
+ case "ellipse":
211
+ return /* @__PURE__ */ jsx("ellipse", { cx: "50", cy: "50", rx: "50", ry: "50", ...common });
212
+ case "triangle":
213
+ return /* @__PURE__ */ jsx("polygon", { points: "50,0 100,100 0,100", ...common });
214
+ case "line":
215
+ return /* @__PURE__ */ jsx("line", { x1: "0", y1: "50", x2: "100", y2: "50", ...common, fill: "none" });
216
+ case "arrow":
217
+ return /* @__PURE__ */ jsxs("g", { children: [
218
+ /* @__PURE__ */ jsx("line", { x1: "0", y1: "50", x2: "85", y2: "50", ...common, fill: "none" }),
219
+ /* @__PURE__ */ jsx("polygon", { points: "100,50 80,30 80,70", fill: s.stroke, stroke: "none" })
220
+ ] });
221
+ default:
222
+ return null;
223
+ }
224
+ }
225
+ function Slide({
226
+ slide,
227
+ theme,
228
+ width,
229
+ aspectRatio,
230
+ editing = false,
231
+ onElementContentChange,
232
+ onElementSelect,
233
+ selectedElementId,
234
+ onElementMove,
235
+ onElementResize,
236
+ renderElement,
237
+ className,
238
+ style
239
+ }) {
240
+ const t = resolveTheme(theme);
241
+ const ratio = aspectRatio ?? t.aspectRatio ?? 16 / 9;
242
+ const ref = useRef(null);
243
+ const [measured, setMeasured] = useState(width ?? 0);
244
+ useEffect(() => {
245
+ if (width !== void 0) {
246
+ setMeasured(width);
247
+ return;
248
+ }
249
+ const el = ref.current;
250
+ if (!el) return;
251
+ const ro = new ResizeObserver((entries) => {
252
+ for (const e of entries) {
253
+ setMeasured(e.contentRect.width);
254
+ }
255
+ });
256
+ ro.observe(el);
257
+ return () => ro.disconnect();
258
+ }, [width]);
259
+ const slideWidthPx = measured || 1;
260
+ const slideHeightPx = slideWidthPx / ratio;
261
+ const bg = slide.background;
262
+ const backgroundStyle = {
263
+ background: bg?.gradient ? bg.gradient : bg?.image ? `${bg.color ?? "transparent"} url(${bg.image}) center/${bg.imageFit ?? "cover"} no-repeat` : bg?.color ?? t.colors?.background ?? "#ffffff"
264
+ };
265
+ return /* @__PURE__ */ jsx(
266
+ "div",
267
+ {
268
+ ref,
269
+ className: cn("fs-slide", className),
270
+ style: {
271
+ width: width ? `${width}px` : "100%",
272
+ height: width ? `${width / ratio}px` : `${slideHeightPx}px`,
273
+ position: "relative",
274
+ overflow: "hidden",
275
+ color: t.colors?.text,
276
+ ...backgroundStyle,
277
+ ...style
278
+ },
279
+ "data-fancy-slides-slide": slide.id,
280
+ onClick: (e) => {
281
+ if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
282
+ },
283
+ children: orderedElements(slide.elements).map((element) => /* @__PURE__ */ jsx(
284
+ SlideElementHost,
285
+ {
286
+ element,
287
+ theme: t,
288
+ slideWidthPx,
289
+ slideHeightPx,
290
+ editing,
291
+ selected: selectedElementId === element.id,
292
+ onContentChange: onElementContentChange,
293
+ onSelect: onElementSelect,
294
+ onMove: onElementMove,
295
+ onResize: onElementResize,
296
+ renderElement
297
+ },
298
+ element.id
299
+ ))
300
+ }
301
+ );
302
+ }
303
+ var MIN_DIM = 0.02;
304
+ var CLICK_DRAG_THRESHOLD_PX = 3;
305
+ function SlideElementHost({
306
+ element,
307
+ theme,
308
+ slideWidthPx,
309
+ slideHeightPx,
310
+ editing,
311
+ selected,
312
+ onContentChange,
313
+ onSelect,
314
+ onMove,
315
+ onResize,
316
+ renderElement
317
+ }) {
318
+ const dragRef = useRef(null);
319
+ if (element.hidden) return null;
320
+ const left = element.x * slideWidthPx;
321
+ const top = element.y * slideHeightPx;
322
+ const width = element.w * slideWidthPx;
323
+ const height = element.h * slideHeightPx;
324
+ const interactive = editing && !element.locked;
325
+ const canMove = interactive && !!onMove;
326
+ const canResize = interactive && !!onResize && selected;
327
+ const startDrag = (mode) => (e) => {
328
+ e.stopPropagation();
329
+ e.target.setPointerCapture(e.pointerId);
330
+ dragRef.current = {
331
+ mode,
332
+ startClientX: e.clientX,
333
+ startClientY: e.clientY,
334
+ startX: element.x,
335
+ startY: element.y,
336
+ startW: element.w,
337
+ startH: element.h,
338
+ didMove: false
339
+ };
340
+ };
341
+ const onPointerMove = (e) => {
342
+ const drag = dragRef.current;
343
+ if (!drag) return;
344
+ const dxPx = e.clientX - drag.startClientX;
345
+ const dyPx = e.clientY - drag.startClientY;
346
+ if (Math.hypot(dxPx, dyPx) >= CLICK_DRAG_THRESHOLD_PX) drag.didMove = true;
347
+ const dx = dxPx / slideWidthPx;
348
+ const dy = dyPx / slideHeightPx;
349
+ if (drag.mode === "move") {
350
+ if (!onMove) return;
351
+ const maxX = 1 - element.w;
352
+ const maxY = 1 - element.h;
353
+ const nextX = clamp(drag.startX + dx, 0, Math.max(0, maxX));
354
+ const nextY = clamp(drag.startY + dy, 0, Math.max(0, maxY));
355
+ onMove(element.id, nextX, nextY);
356
+ return;
357
+ }
358
+ if (!onResize) return;
359
+ const patch = computeResize(drag, dx, dy);
360
+ onResize(element.id, patch);
361
+ };
362
+ const endDrag = (e) => {
363
+ const drag = dragRef.current;
364
+ if (!drag) return;
365
+ try {
366
+ e.target.releasePointerCapture(e.pointerId);
367
+ } catch {
368
+ }
369
+ const wasMove = drag.didMove;
370
+ dragRef.current = null;
371
+ if (!wasMove && onSelect) onSelect(element.id);
372
+ };
373
+ const box = {
374
+ position: "absolute",
375
+ left: `${left}px`,
376
+ top: `${top}px`,
377
+ width: `${width}px`,
378
+ height: `${height}px`,
379
+ transform: element.rotation ? `rotate(${element.rotation}deg)` : void 0,
380
+ transformOrigin: "center center",
381
+ zIndex: element.z ?? "auto",
382
+ outline: selected ? "2px solid #8b5cf6" : void 0,
383
+ outlineOffset: selected ? 2 : void 0,
384
+ cursor: canMove ? "move" : interactive ? "pointer" : "default",
385
+ touchAction: canMove ? "none" : void 0
386
+ };
387
+ const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
388
+ return /* @__PURE__ */ jsxs(
389
+ "div",
390
+ {
391
+ style: box,
392
+ "data-fancy-slides-element": element.id,
393
+ "data-fancy-slides-element-type": element.type,
394
+ onPointerDown: canMove ? startDrag("move") : void 0,
395
+ onPointerMove: canMove ? onPointerMove : void 0,
396
+ onPointerUp: canMove ? endDrag : void 0,
397
+ onPointerCancel: canMove ? endDrag : void 0,
398
+ onClick: (e) => {
399
+ if (!onSelect || canMove) return;
400
+ e.stopPropagation();
401
+ onSelect(element.id);
402
+ },
403
+ children: [
404
+ inner,
405
+ canResize && /* @__PURE__ */ jsx(
406
+ ResizeHandles,
407
+ {
408
+ onStart: (anchor) => startDrag(anchor),
409
+ onMove: onPointerMove,
410
+ onEnd: endDrag
411
+ }
412
+ )
413
+ ]
414
+ }
415
+ );
416
+ }
417
+ function ResizeHandles({ onStart, onMove, onEnd }) {
418
+ const anchors = [
419
+ { anchor: "nw", style: { left: -5, top: -5 }, cursor: "nwse-resize" },
420
+ { anchor: "n", style: { left: "calc(50% - 5px)", top: -5 }, cursor: "ns-resize" },
421
+ { anchor: "ne", style: { right: -5, top: -5 }, cursor: "nesw-resize" },
422
+ { anchor: "e", style: { right: -5, top: "calc(50% - 5px)" }, cursor: "ew-resize" },
423
+ { anchor: "se", style: { right: -5, bottom: -5 }, cursor: "nwse-resize" },
424
+ { anchor: "s", style: { left: "calc(50% - 5px)", bottom: -5 }, cursor: "ns-resize" },
425
+ { anchor: "sw", style: { left: -5, bottom: -5 }, cursor: "nesw-resize" },
426
+ { anchor: "w", style: { left: -5, top: "calc(50% - 5px)" }, cursor: "ew-resize" }
427
+ ];
428
+ return /* @__PURE__ */ jsx(Fragment, { children: anchors.map(({ anchor, style, cursor }) => /* @__PURE__ */ jsx(
429
+ "div",
430
+ {
431
+ style: {
432
+ position: "absolute",
433
+ width: 10,
434
+ height: 10,
435
+ background: "#ffffff",
436
+ border: "1.5px solid #8b5cf6",
437
+ borderRadius: 2,
438
+ cursor,
439
+ touchAction: "none",
440
+ boxShadow: "0 1px 2px rgba(0,0,0,0.15)",
441
+ ...style
442
+ },
443
+ "data-fancy-slides-resize-handle": anchor,
444
+ onPointerDown: onStart(anchor),
445
+ onPointerMove: onMove,
446
+ onPointerUp: onEnd,
447
+ onPointerCancel: onEnd
448
+ },
449
+ anchor
450
+ )) });
451
+ }
452
+ function renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) {
453
+ switch (element.type) {
454
+ case "text":
455
+ return /* @__PURE__ */ jsx(
456
+ TextElementRenderer,
457
+ {
458
+ element,
459
+ theme,
460
+ slideWidthPx,
461
+ editing,
462
+ selected,
463
+ onContentChange: onContentChange ? (c) => onContentChange(element.id, c) : void 0
464
+ }
465
+ );
466
+ case "image":
467
+ return /* @__PURE__ */ jsx(ImageElementRenderer, { element });
468
+ case "shape":
469
+ return /* @__PURE__ */ jsx(ShapeElementRenderer, { element, theme, slideWidthPx });
470
+ case "chart":
471
+ case "code":
472
+ case "table":
473
+ case "embed":
474
+ return void 0;
475
+ default:
476
+ return null;
477
+ }
478
+ }
479
+ function orderedElements(elements) {
480
+ return [...elements].sort((a, b) => {
481
+ const az = a.z ?? -1;
482
+ const bz = b.z ?? -1;
483
+ if (az === bz) return 0;
484
+ return az < bz ? -1 : 1;
485
+ });
486
+ }
487
+ function clamp(v, min, max) {
488
+ return Math.max(min, Math.min(max, v));
489
+ }
490
+ function computeResize(drag, dx, dy) {
491
+ let { startX: x, startY: y, startW: w, startH: h } = drag;
492
+ const right = drag.startX + drag.startW;
493
+ const bottom = drag.startY + drag.startH;
494
+ const anchor = drag.mode;
495
+ if (anchor.includes("w")) {
496
+ const newX = clamp(drag.startX + dx, 0, right - MIN_DIM);
497
+ x = newX;
498
+ w = right - newX;
499
+ } else if (anchor.includes("e")) {
500
+ w = clamp(drag.startW + dx, MIN_DIM, 1 - drag.startX);
501
+ }
502
+ if (anchor.includes("n")) {
503
+ const newY = clamp(drag.startY + dy, 0, bottom - MIN_DIM);
504
+ y = newY;
505
+ h = bottom - newY;
506
+ } else if (anchor.includes("s")) {
507
+ h = clamp(drag.startH + dy, MIN_DIM, 1 - drag.startY);
508
+ }
509
+ return { x, y, w, h };
510
+ }
511
+ function useSlideKeyboard({
512
+ total,
513
+ index,
514
+ goTo,
515
+ onExit,
516
+ onBlank,
517
+ onFullscreen,
518
+ enabled = true
519
+ }) {
520
+ useEffect(() => {
521
+ if (!enabled) return;
522
+ const handler = (e) => {
523
+ const target = e.target;
524
+ if (target && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)) {
525
+ return;
526
+ }
527
+ switch (e.key) {
528
+ case "ArrowLeft":
529
+ case "PageUp":
530
+ e.preventDefault();
531
+ if (index > 0) goTo(index - 1);
532
+ return;
533
+ case "ArrowRight":
534
+ case "PageDown":
535
+ case " ":
536
+ e.preventDefault();
537
+ if (index < total - 1) goTo(index + 1);
538
+ return;
539
+ case "Home":
540
+ e.preventDefault();
541
+ goTo(0);
542
+ return;
543
+ case "End":
544
+ e.preventDefault();
545
+ goTo(total - 1);
546
+ return;
547
+ case "Escape":
548
+ if (onExit) {
549
+ e.preventDefault();
550
+ onExit();
551
+ }
552
+ return;
553
+ case "b":
554
+ case "B":
555
+ case ".":
556
+ if (onBlank) {
557
+ e.preventDefault();
558
+ onBlank();
559
+ }
560
+ return;
561
+ case "f":
562
+ case "F":
563
+ if (onFullscreen) {
564
+ e.preventDefault();
565
+ onFullscreen();
566
+ }
567
+ return;
568
+ default: {
569
+ const n = parseInt(e.key, 10);
570
+ if (Number.isFinite(n) && n >= 1 && n <= 9) {
571
+ e.preventDefault();
572
+ goTo(Math.min(total - 1, n - 1));
573
+ }
574
+ }
575
+ }
576
+ };
577
+ window.addEventListener("keydown", handler);
578
+ return () => window.removeEventListener("keydown", handler);
579
+ }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
580
+ }
581
+ function SlideViewer({
582
+ deck,
583
+ index: controlledIndex,
584
+ defaultIndex,
585
+ onIndexChange,
586
+ onExit,
587
+ autoAdvanceMs,
588
+ hideChrome = false,
589
+ renderElement,
590
+ className
591
+ }) {
592
+ const isControlled = controlledIndex !== void 0;
593
+ const [internalIndex, setInternalIndex] = useState(defaultIndex ?? 0);
594
+ const index = isControlled ? controlledIndex : internalIndex;
595
+ const goTo = useCallback(
596
+ (i) => {
597
+ const clamped = Math.max(0, Math.min(deck.slides.length - 1, i));
598
+ if (!isControlled) setInternalIndex(clamped);
599
+ onIndexChange?.(clamped);
600
+ },
601
+ [deck.slides.length, isControlled, onIndexChange]
602
+ );
603
+ const [blanked, setBlanked] = useState(false);
604
+ const containerRef = useRef(null);
605
+ useSlideKeyboard({
606
+ total: deck.slides.length,
607
+ index,
608
+ goTo,
609
+ onExit,
610
+ onBlank: () => setBlanked((b) => !b),
611
+ onFullscreen: () => {
612
+ const el = containerRef.current;
613
+ if (!el) return;
614
+ if (document.fullscreenElement) document.exitFullscreen();
615
+ else el.requestFullscreen?.();
616
+ }
617
+ });
618
+ useEffect(() => {
619
+ if (!autoAdvanceMs || deck.slides.length <= 1) return;
620
+ const t = setTimeout(() => {
621
+ goTo(index + 1 < deck.slides.length ? index + 1 : 0);
622
+ }, autoAdvanceMs);
623
+ return () => clearTimeout(t);
624
+ }, [autoAdvanceMs, index, deck.slides.length, goTo]);
625
+ const slide = deck.slides[index];
626
+ const theme = resolveTheme(deck.theme);
627
+ const aspectRatio = theme.aspectRatio ?? 16 / 9;
628
+ return /* @__PURE__ */ jsxs(
629
+ "div",
630
+ {
631
+ ref: containerRef,
632
+ className: cn("fs-viewer", className),
633
+ style: {
634
+ position: "relative",
635
+ width: "100%",
636
+ height: "100%",
637
+ background: blanked ? "#000000" : theme.colors?.background ?? "#000000",
638
+ display: "flex",
639
+ alignItems: "center",
640
+ justifyContent: "center",
641
+ overflow: "hidden"
642
+ },
643
+ tabIndex: 0,
644
+ "data-fancy-slides-viewer": deck.id,
645
+ children: [
646
+ !blanked && slide && /* @__PURE__ */ jsx(
647
+ "div",
648
+ {
649
+ style: {
650
+ // Box that fits the slide while preserving aspect ratio.
651
+ width: "min(100%, calc(100vh * var(--fs-ratio)))",
652
+ aspectRatio: String(aspectRatio),
653
+ // CSS var lets us inline-style the aspect ratio so it works in any container.
654
+ ["--fs-ratio"]: aspectRatio.toString(),
655
+ boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
656
+ },
657
+ children: /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement })
658
+ }
659
+ ),
660
+ !hideChrome && !blanked && /* @__PURE__ */ jsxs(
661
+ "div",
662
+ {
663
+ style: {
664
+ position: "absolute",
665
+ bottom: 16,
666
+ right: 16,
667
+ padding: "4px 10px",
668
+ borderRadius: 999,
669
+ background: "rgba(15, 23, 42, 0.6)",
670
+ color: "#f8fafc",
671
+ fontSize: 12,
672
+ fontFamily: theme.fonts?.mono,
673
+ backdropFilter: "blur(6px)"
674
+ },
675
+ "aria-label": "Slide counter",
676
+ children: [
677
+ index + 1,
678
+ " / ",
679
+ deck.slides.length
680
+ ]
681
+ }
682
+ )
683
+ ]
684
+ }
685
+ );
686
+ }
687
+ function PresenterView({
688
+ deck,
689
+ index: controlledIndex,
690
+ defaultIndex,
691
+ onIndexChange,
692
+ onExit,
693
+ startedAt,
694
+ renderElement,
695
+ className
696
+ }) {
697
+ const isControlled = controlledIndex !== void 0;
698
+ const [internalIndex, setInternalIndex] = useState(defaultIndex ?? 0);
699
+ const index = isControlled ? controlledIndex : internalIndex;
700
+ const goTo = useCallback(
701
+ (i) => {
702
+ const clamped = Math.max(0, Math.min(deck.slides.length - 1, i));
703
+ if (!isControlled) setInternalIndex(clamped);
704
+ onIndexChange?.(clamped);
705
+ },
706
+ [deck.slides.length, isControlled, onIndexChange]
707
+ );
708
+ useSlideKeyboard({
709
+ total: deck.slides.length,
710
+ index,
711
+ goTo,
712
+ onExit
713
+ });
714
+ const theme = resolveTheme(deck.theme);
715
+ const slide = deck.slides[index];
716
+ const nextSlide = deck.slides[index + 1];
717
+ const [now, setNow] = useState(() => Date.now());
718
+ useEffect(() => {
719
+ const id = setInterval(() => setNow(Date.now()), 1e3);
720
+ return () => clearInterval(id);
721
+ }, []);
722
+ const startedAtRef = useMemo(() => startedAt ?? now, [startedAt]);
723
+ return /* @__PURE__ */ jsxs(
724
+ "div",
725
+ {
726
+ className: cn("fs-presenter", className),
727
+ style: {
728
+ display: "grid",
729
+ gridTemplateRows: "1fr auto",
730
+ gridTemplateColumns: "minmax(0, 2fr) minmax(0, 1fr)",
731
+ width: "100%",
732
+ height: "100%",
733
+ background: "#0b1220",
734
+ color: "#f8fafc",
735
+ fontFamily: theme.fonts?.body
736
+ },
737
+ "data-fancy-slides-presenter": deck.id,
738
+ children: [
739
+ /* @__PURE__ */ jsx(
740
+ "div",
741
+ {
742
+ style: {
743
+ gridRow: 1,
744
+ gridColumn: 1,
745
+ padding: 24,
746
+ display: "flex",
747
+ alignItems: "center",
748
+ justifyContent: "center",
749
+ minHeight: 0
750
+ },
751
+ children: /* @__PURE__ */ jsx(
752
+ "div",
753
+ {
754
+ style: {
755
+ width: "100%",
756
+ aspectRatio: String(theme.aspectRatio ?? 16 / 9),
757
+ maxHeight: "100%",
758
+ boxShadow: "0 12px 40px rgba(0,0,0,0.5)",
759
+ borderRadius: 8,
760
+ overflow: "hidden"
761
+ },
762
+ children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) : null
763
+ }
764
+ )
765
+ }
766
+ ),
767
+ /* @__PURE__ */ jsxs(
768
+ "div",
769
+ {
770
+ style: {
771
+ gridRow: 1,
772
+ gridColumn: 2,
773
+ display: "grid",
774
+ gridTemplateRows: "auto 1fr",
775
+ gap: 12,
776
+ padding: 24,
777
+ paddingLeft: 0,
778
+ minHeight: 0
779
+ },
780
+ children: [
781
+ /* @__PURE__ */ jsxs("div", { children: [
782
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Up next" }),
783
+ nextSlide ? /* @__PURE__ */ jsx(
784
+ "div",
785
+ {
786
+ style: {
787
+ marginTop: 8,
788
+ width: "100%",
789
+ aspectRatio: String(theme.aspectRatio ?? 16 / 9),
790
+ boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
791
+ borderRadius: 6,
792
+ overflow: "hidden",
793
+ opacity: 0.85
794
+ },
795
+ children: /* @__PURE__ */ jsx(Slide, { slide: nextSlide, theme, renderElement })
796
+ }
797
+ ) : /* @__PURE__ */ jsx(
798
+ "div",
799
+ {
800
+ style: {
801
+ marginTop: 8,
802
+ display: "grid",
803
+ placeItems: "center",
804
+ aspectRatio: String(theme.aspectRatio ?? 16 / 9),
805
+ borderRadius: 6,
806
+ border: "1px dashed rgba(255,255,255,0.2)",
807
+ color: "rgba(255,255,255,0.4)",
808
+ fontSize: 13
809
+ },
810
+ children: "End of deck"
811
+ }
812
+ )
813
+ ] }),
814
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", minHeight: 0 }, children: [
815
+ /* @__PURE__ */ jsx(SectionLabel, { children: "Speaker notes" }),
816
+ /* @__PURE__ */ jsx(
817
+ "pre",
818
+ {
819
+ style: {
820
+ marginTop: 8,
821
+ flex: 1,
822
+ overflow: "auto",
823
+ background: "rgba(255,255,255,0.04)",
824
+ border: "1px solid rgba(255,255,255,0.08)",
825
+ borderRadius: 6,
826
+ padding: 12,
827
+ fontFamily: theme.fonts?.body,
828
+ fontSize: 15,
829
+ lineHeight: 1.5,
830
+ whiteSpace: "pre-wrap",
831
+ wordBreak: "break-word",
832
+ color: "rgba(248,250,252,0.92)",
833
+ margin: 0
834
+ },
835
+ children: slide?.notes?.trim() || /* @__PURE__ */ jsx("span", { style: { color: "rgba(255,255,255,0.35)", fontStyle: "italic" }, children: "No notes for this slide." })
836
+ }
837
+ )
838
+ ] })
839
+ ]
840
+ }
841
+ ),
842
+ /* @__PURE__ */ jsxs(
843
+ "div",
844
+ {
845
+ style: {
846
+ gridRow: 2,
847
+ gridColumn: "1 / span 2",
848
+ display: "flex",
849
+ alignItems: "center",
850
+ gap: 24,
851
+ padding: "12px 24px",
852
+ borderTop: "1px solid rgba(255,255,255,0.1)",
853
+ fontSize: 13,
854
+ color: "rgba(248,250,252,0.7)",
855
+ fontFamily: theme.fonts?.mono
856
+ },
857
+ children: [
858
+ /* @__PURE__ */ jsxs(StatusChip, { label: "Slide", children: [
859
+ index + 1,
860
+ " / ",
861
+ deck.slides.length
862
+ ] }),
863
+ /* @__PURE__ */ jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
864
+ /* @__PURE__ */ jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
865
+ /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
866
+ /* @__PURE__ */ jsx(NavButton, { onClick: () => goTo(index - 1), disabled: index === 0, children: "\u2190 Prev" }),
867
+ /* @__PURE__ */ jsx(NavButton, { onClick: () => goTo(index + 1), disabled: index >= deck.slides.length - 1, children: "Next \u2192" })
868
+ ] })
869
+ ]
870
+ }
871
+ )
872
+ ]
873
+ }
874
+ );
875
+ }
876
+ function SectionLabel({ children }) {
877
+ return /* @__PURE__ */ jsx(
878
+ "div",
879
+ {
880
+ style: {
881
+ fontSize: 10,
882
+ textTransform: "uppercase",
883
+ letterSpacing: "0.08em",
884
+ color: "rgba(248,250,252,0.5)",
885
+ fontWeight: 600
886
+ },
887
+ children
888
+ }
889
+ );
890
+ }
891
+ function StatusChip({ label, children }) {
892
+ return /* @__PURE__ */ jsxs("div", { style: { display: "inline-flex", alignItems: "baseline", gap: 6 }, children: [
893
+ /* @__PURE__ */ jsx(
894
+ "span",
895
+ {
896
+ style: {
897
+ fontSize: 10,
898
+ textTransform: "uppercase",
899
+ letterSpacing: "0.08em",
900
+ color: "rgba(248,250,252,0.4)"
901
+ },
902
+ children: label
903
+ }
904
+ ),
905
+ /* @__PURE__ */ jsx("span", { style: { color: "rgba(248,250,252,0.92)", fontWeight: 600 }, children })
906
+ ] });
907
+ }
908
+ function NavButton({ onClick, disabled, children }) {
909
+ return /* @__PURE__ */ jsx(
910
+ "button",
911
+ {
912
+ onClick,
913
+ disabled,
914
+ style: {
915
+ padding: "4px 10px",
916
+ borderRadius: 6,
917
+ background: "rgba(255,255,255,0.08)",
918
+ border: "1px solid rgba(255,255,255,0.15)",
919
+ color: "rgba(248,250,252,0.92)",
920
+ cursor: disabled ? "not-allowed" : "pointer",
921
+ opacity: disabled ? 0.4 : 1,
922
+ fontSize: 13,
923
+ fontFamily: "inherit"
924
+ },
925
+ children
926
+ }
927
+ );
928
+ }
929
+ function formatClock(ms) {
930
+ const d = new Date(ms);
931
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
932
+ }
933
+ function formatElapsed(ms) {
934
+ const totalSec = Math.max(0, Math.floor(ms / 1e3));
935
+ const h = Math.floor(totalSec / 3600);
936
+ const m = Math.floor(totalSec % 3600 / 60);
937
+ const s = totalSec % 60;
938
+ if (h > 0) return `${h}:${pad(m)}:${pad(s)}`;
939
+ return `${pad(m)}:${pad(s)}`;
940
+ }
941
+ function pad(n) {
942
+ return n < 10 ? `0${n}` : String(n);
943
+ }
944
+ function SlideThumbnail({
945
+ slide,
946
+ theme,
947
+ width = 200,
948
+ active = false,
949
+ onClick,
950
+ onContextMenu,
951
+ renderElement,
952
+ className,
953
+ style
954
+ }) {
955
+ return /* @__PURE__ */ jsx(
956
+ "div",
957
+ {
958
+ className: cn("fs-thumbnail", className),
959
+ style: {
960
+ cursor: onClick ? "pointer" : "default",
961
+ borderRadius: 6,
962
+ border: active ? "2px solid #8b5cf6" : "1px solid rgba(0,0,0,0.08)",
963
+ overflow: "hidden",
964
+ boxShadow: active ? "0 0 0 3px rgba(139, 92, 246, 0.2)" : "0 1px 2px rgba(0,0,0,0.05)",
965
+ background: "#ffffff",
966
+ width,
967
+ ...style
968
+ },
969
+ onClick,
970
+ onContextMenu,
971
+ "data-fancy-slides-thumbnail": slide.id,
972
+ children: /* @__PURE__ */ jsx(Slide, { slide, theme, width, renderElement })
973
+ }
974
+ );
975
+ }
976
+
977
+ // src/utils/ids.ts
978
+ var counter = 0;
979
+ function nextId(prefix = "id") {
980
+ counter = (counter + 1) % 1e6;
981
+ const t = Date.now().toString(36);
982
+ const c = counter.toString(36).padStart(4, "0");
983
+ return `${prefix}-${t}-${c}`;
984
+ }
985
+ function slideId() {
986
+ return nextId("s");
987
+ }
988
+ function elementId() {
989
+ return nextId("e");
990
+ }
991
+ function deckId() {
992
+ return nextId("d");
993
+ }
994
+ function useDeckState({ value, onChange, onOp }) {
995
+ const apply = useCallback(
996
+ (op) => {
997
+ const next = reduce(value, op);
998
+ onChange(next);
999
+ onOp?.(op);
1000
+ },
1001
+ [value, onChange, onOp]
1002
+ );
1003
+ return useMemo(() => {
1004
+ return {
1005
+ apply,
1006
+ setTitle: (title) => apply({ kind: "deck_set_title", title }),
1007
+ applyTheme: (theme) => apply({ kind: "deck_apply_theme", theme }),
1008
+ addSlide: (index, partial) => {
1009
+ const id = partial?.id ?? slideId();
1010
+ const slide = {
1011
+ id,
1012
+ layout: partial?.layout ?? "blank",
1013
+ elements: partial?.elements ?? [],
1014
+ background: partial?.background,
1015
+ transition: partial?.transition,
1016
+ notes: partial?.notes,
1017
+ metadata: partial?.metadata
1018
+ };
1019
+ apply({ kind: "slide_add", index: index ?? value.slides.length, slide });
1020
+ return id;
1021
+ },
1022
+ duplicateSlide: (id) => {
1023
+ const src = value.slides.find((s) => s.id === id);
1024
+ if (!src) return id;
1025
+ const newId = slideId();
1026
+ const clone = {
1027
+ ...src,
1028
+ id: newId,
1029
+ elements: src.elements.map((e) => ({ ...e, id: elementId() }))
1030
+ };
1031
+ const idx = value.slides.findIndex((s) => s.id === id);
1032
+ apply({ kind: "slide_add", index: idx + 1, slide: clone });
1033
+ return newId;
1034
+ },
1035
+ removeSlide: (id) => apply({ kind: "slide_remove", id }),
1036
+ reorderSlide: (id, toIndex) => apply({ kind: "slide_reorder", id, toIndex }),
1037
+ setLayout: (id, layout) => apply({ kind: "slide_set_layout", id, layout }),
1038
+ setNotes: (id, notes) => apply({ kind: "slide_set_notes", id, notes }),
1039
+ setBackground: (id, background) => apply({ kind: "slide_set_background", id, background }),
1040
+ addElement: (slideId2, element) => {
1041
+ const id = element.id ?? elementId();
1042
+ apply({ kind: "element_add", slideId: slideId2, element: { ...element, id } });
1043
+ return id;
1044
+ },
1045
+ removeElement: (slideIdArg, elementIdArg) => apply({ kind: "element_remove", slideId: slideIdArg, elementId: elementIdArg }),
1046
+ updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1047
+ moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1048
+ resizeElement: (slideIdArg, elementIdArg, w, h) => apply({ kind: "element_resize", slideId: slideIdArg, elementId: elementIdArg, w, h }),
1049
+ getSlide: (id) => value.slides.find((s) => s.id === id),
1050
+ getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1051
+ };
1052
+ }, [apply, value.slides]);
1053
+ }
1054
+ function reduce(deck, op) {
1055
+ switch (op.kind) {
1056
+ case "deck_set_title":
1057
+ return { ...deck, title: op.title };
1058
+ case "deck_apply_theme":
1059
+ return { ...deck, theme: op.theme };
1060
+ case "slide_add": {
1061
+ const slides = [...deck.slides];
1062
+ slides.splice(Math.max(0, Math.min(slides.length, op.index)), 0, op.slide);
1063
+ return { ...deck, slides };
1064
+ }
1065
+ case "slide_remove":
1066
+ return { ...deck, slides: deck.slides.filter((s) => s.id !== op.id) };
1067
+ case "slide_reorder": {
1068
+ const idx = deck.slides.findIndex((s) => s.id === op.id);
1069
+ if (idx < 0) return deck;
1070
+ const slides = [...deck.slides];
1071
+ const [moved] = slides.splice(idx, 1);
1072
+ slides.splice(Math.max(0, Math.min(slides.length, op.toIndex)), 0, moved);
1073
+ return { ...deck, slides };
1074
+ }
1075
+ case "slide_set_layout":
1076
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, layout: op.layout } : s) };
1077
+ case "slide_set_notes":
1078
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, notes: op.notes } : s) };
1079
+ case "slide_set_background":
1080
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, background: op.background } : s) };
1081
+ case "element_add":
1082
+ return {
1083
+ ...deck,
1084
+ slides: deck.slides.map((s) => s.id === op.slideId ? { ...s, elements: [...s.elements, op.element] } : s)
1085
+ };
1086
+ case "element_remove":
1087
+ return {
1088
+ ...deck,
1089
+ slides: deck.slides.map(
1090
+ (s) => s.id === op.slideId ? { ...s, elements: s.elements.filter((e) => e.id !== op.elementId) } : s
1091
+ )
1092
+ };
1093
+ case "element_update":
1094
+ return {
1095
+ ...deck,
1096
+ slides: deck.slides.map(
1097
+ (s) => s.id === op.slideId ? { ...s, elements: s.elements.map((e) => e.id === op.elementId ? { ...e, ...op.patch } : e) } : s
1098
+ )
1099
+ };
1100
+ case "element_move":
1101
+ return {
1102
+ ...deck,
1103
+ slides: deck.slides.map(
1104
+ (s) => s.id === op.slideId ? { ...s, elements: s.elements.map((e) => e.id === op.elementId ? { ...e, x: op.x, y: op.y } : e) } : s
1105
+ )
1106
+ };
1107
+ case "element_resize":
1108
+ return {
1109
+ ...deck,
1110
+ slides: deck.slides.map(
1111
+ (s) => s.id === op.slideId ? { ...s, elements: s.elements.map((e) => e.id === op.elementId ? { ...e, w: op.w, h: op.h } : e) } : s
1112
+ )
1113
+ };
1114
+ }
1115
+ }
1116
+ function SlideRail({
1117
+ slides,
1118
+ selectedId,
1119
+ theme,
1120
+ onSelect,
1121
+ onAdd,
1122
+ onDuplicate,
1123
+ onRemove,
1124
+ onReorder,
1125
+ renderElement,
1126
+ thumbnailWidth = 184
1127
+ }) {
1128
+ const [dragOver, setDragOver] = useState(null);
1129
+ return /* @__PURE__ */ jsxs(
1130
+ "aside",
1131
+ {
1132
+ "data-react-fancy-slide-rail": "",
1133
+ className: "fs-rail flex h-full w-full min-w-0 flex-col gap-0.5",
1134
+ children: [
1135
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 px-3 py-2", children: [
1136
+ /* @__PURE__ */ jsxs(Text, { size: "xs", weight: "semibold", className: "!uppercase !tracking-wider !text-zinc-500", children: [
1137
+ "Slides \xB7 ",
1138
+ slides.length
1139
+ ] }),
1140
+ /* @__PURE__ */ jsx(Action, { size: "xs", icon: "plus", onClick: () => onAdd(), "aria-label": "Add slide", children: "Add" })
1141
+ ] }),
1142
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 px-3 pb-3", children: [
1143
+ slides.map((slide, i) => /* @__PURE__ */ jsx(
1144
+ "div",
1145
+ {
1146
+ draggable: true,
1147
+ onDragStart: (e) => {
1148
+ e.dataTransfer.effectAllowed = "move";
1149
+ e.dataTransfer.setData("text/plain", slide.id);
1150
+ },
1151
+ onDragOver: (e) => {
1152
+ e.preventDefault();
1153
+ e.dataTransfer.dropEffect = "move";
1154
+ setDragOver(slide.id);
1155
+ },
1156
+ onDragLeave: () => setDragOver(null),
1157
+ onDrop: (e) => {
1158
+ e.preventDefault();
1159
+ const id = e.dataTransfer.getData("text/plain");
1160
+ setDragOver(null);
1161
+ if (id && id !== slide.id) onReorder(id, i);
1162
+ },
1163
+ style: {
1164
+ position: "relative",
1165
+ paddingTop: dragOver === slide.id ? 3 : 0,
1166
+ borderTop: dragOver === slide.id ? "2px solid #8b5cf6" : void 0,
1167
+ transition: "padding 80ms ease"
1168
+ },
1169
+ children: /* @__PURE__ */ jsxs(ContextMenu, { children: [
1170
+ /* @__PURE__ */ jsx(ContextMenu.Trigger, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
1171
+ /* @__PURE__ */ jsx(Text, { size: "xs", className: "!w-6 shrink-0 !pt-1 !text-right !font-mono !text-zinc-400", children: i + 1 }),
1172
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
1173
+ SlideThumbnail,
1174
+ {
1175
+ slide,
1176
+ theme,
1177
+ width: thumbnailWidth - 32,
1178
+ active: selectedId === slide.id,
1179
+ onClick: () => onSelect(slide.id),
1180
+ renderElement
1181
+ }
1182
+ ) })
1183
+ ] }) }),
1184
+ /* @__PURE__ */ jsxs(ContextMenu.Content, { children: [
1185
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onSelect(slide.id), children: "Open" }),
1186
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onDuplicate(slide.id), children: "Duplicate" }),
1187
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onAdd(i), children: "Insert above" }),
1188
+ /* @__PURE__ */ jsx(ContextMenu.Item, { onClick: () => onAdd(i + 1), children: "Insert below" }),
1189
+ /* @__PURE__ */ jsx(ContextMenu.Separator, {}),
1190
+ /* @__PURE__ */ jsx(ContextMenu.Item, { danger: true, onClick: () => onRemove(slide.id), children: "Delete" })
1191
+ ] })
1192
+ ] })
1193
+ },
1194
+ slide.id
1195
+ )),
1196
+ slides.length === 0 && /* @__PURE__ */ jsx("div", { className: "grid place-items-center rounded-md border border-dashed border-zinc-300 px-3 py-8 text-center text-xs text-zinc-500 dark:border-zinc-700", children: "Empty deck \u2014 add a slide to begin." })
1197
+ ] })
1198
+ ]
1199
+ }
1200
+ );
1201
+ }
1202
+ function EditorToolbar({
1203
+ title,
1204
+ onTitleChange,
1205
+ themeName,
1206
+ onApplyTheme,
1207
+ onInsertText,
1208
+ onInsertImage,
1209
+ onInsertShape,
1210
+ onInsertChart,
1211
+ onInsertCode,
1212
+ onInsertTable,
1213
+ onPresent,
1214
+ disabled = false
1215
+ }) {
1216
+ return /* @__PURE__ */ jsxs("div", { className: "fs-toolbar flex items-center gap-2 border-b border-zinc-200 bg-white px-4 py-2 dark:border-zinc-800 dark:bg-zinc-950", children: [
1217
+ /* @__PURE__ */ jsx(
1218
+ "input",
1219
+ {
1220
+ value: title,
1221
+ onChange: (e) => onTitleChange?.(e.target.value),
1222
+ placeholder: "Untitled deck",
1223
+ className: "min-w-0 flex-1 max-w-xs border-0 bg-transparent px-1 text-sm font-semibold text-zinc-900 outline-none placeholder:text-zinc-400 focus:bg-zinc-50 dark:text-zinc-100 dark:focus:bg-zinc-900",
1224
+ "aria-label": "Deck title"
1225
+ }
1226
+ ),
1227
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical" }),
1228
+ /* @__PURE__ */ jsx(Tooltip, { content: "Insert text", children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "type", onClick: onInsertText, disabled, "aria-label": "Insert text" }) }),
1229
+ /* @__PURE__ */ jsx(Tooltip, { content: "Insert image", children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "image", onClick: onInsertImage, disabled, "aria-label": "Insert image" }) }),
1230
+ /* @__PURE__ */ jsxs(Dropdown, { children: [
1231
+ /* @__PURE__ */ jsx(Dropdown.Trigger, { children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "square", iconTrailing: "chevron-down", disabled, children: "Shape" }) }),
1232
+ /* @__PURE__ */ jsxs(Dropdown.Items, { children: [
1233
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("rect"), children: "Rectangle" }),
1234
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("rounded-rect"), children: "Rounded rectangle" }),
1235
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("ellipse"), children: "Ellipse" }),
1236
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("triangle"), children: "Triangle" }),
1237
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("line"), children: "Line" }),
1238
+ /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onInsertShape?.("arrow"), children: "Arrow" })
1239
+ ] })
1240
+ ] }),
1241
+ /* @__PURE__ */ jsx(Tooltip, { content: "Insert chart", children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "bar-chart", onClick: onInsertChart, disabled, "aria-label": "Insert chart" }) }),
1242
+ /* @__PURE__ */ jsx(Tooltip, { content: "Insert code", children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "code", onClick: onInsertCode, disabled, "aria-label": "Insert code" }) }),
1243
+ /* @__PURE__ */ jsx(Tooltip, { content: "Insert table", children: /* @__PURE__ */ jsx(Action, { variant: "ghost", size: "sm", icon: "table", onClick: onInsertTable, disabled, "aria-label": "Insert table" }) }),
1244
+ /* @__PURE__ */ jsx(Separator, { orientation: "vertical" }),
1245
+ /* @__PURE__ */ jsxs(Dropdown, { children: [
1246
+ /* @__PURE__ */ jsx(Dropdown.Trigger, { children: /* @__PURE__ */ jsxs(Action, { variant: "ghost", size: "sm", iconTrailing: "chevron-down", children: [
1247
+ /* @__PURE__ */ jsx(Badge, { size: "sm", color: "zinc", children: themeName ?? "default" }),
1248
+ /* @__PURE__ */ jsx("span", { className: "ml-2", children: "Theme" })
1249
+ ] }) }),
1250
+ /* @__PURE__ */ jsx(Dropdown.Items, { children: Object.values(builtinThemes).map((t) => /* @__PURE__ */ jsx(Dropdown.Item, { onClick: () => onApplyTheme?.(t), children: t.name }, t.name)) })
1251
+ ] }),
1252
+ /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-2", children: /* @__PURE__ */ jsx(Tooltip, { content: "Present (F)", children: /* @__PURE__ */ jsx(Action, { color: "violet", size: "sm", icon: "play", onClick: onPresent, children: "Present" }) }) })
1253
+ ] });
1254
+ }
1255
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1256
+ if (!element) {
1257
+ return /* @__PURE__ */ jsxs("div", { className: "fs-inspector flex h-full flex-col border-l border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900", children: [
1258
+ /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
1259
+ /* @__PURE__ */ jsx(Text, { size: "sm", className: "mt-2 !text-zinc-500", children: "Select an element to edit its properties." })
1260
+ ] });
1261
+ }
1262
+ return /* @__PURE__ */ jsxs("div", { className: "fs-inspector flex h-full w-full flex-col border-l border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900", children: [
1263
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 px-3 py-2 dark:border-zinc-800", children: [
1264
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1265
+ /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!font-mono !uppercase !tracking-wider !text-zinc-500", children: element.type }),
1266
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
1267
+ "#",
1268
+ element.id.slice(-6)
1269
+ ] })
1270
+ ] }),
1271
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
1272
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: element.locked ? "lock" : "unlock", onClick: () => onLockToggle?.(!element.locked), "aria-label": element.locked ? "Unlock" : "Lock" }),
1273
+ onDelete && /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "trash", onClick: onDelete, "aria-label": "Delete" })
1274
+ ] })
1275
+ ] }),
1276
+ /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-y-auto p-3", children: /* @__PURE__ */ jsxs(Tabs, { defaultTab: "style", variant: "pills", children: [
1277
+ /* @__PURE__ */ jsxs(Tabs.List, { children: [
1278
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "style", children: "Style" }),
1279
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "layout", children: "Layout" }),
1280
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "advanced", children: "Advanced" })
1281
+ ] }),
1282
+ /* @__PURE__ */ jsxs(Tabs.Panels, { children: [
1283
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "style", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(StyleSection, { element, onPatch }) }) }),
1284
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "layout", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(LayoutSection, { element, onPatch }) }) }),
1285
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "advanced", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(AdvancedSection, { element, onPatch }) }) })
1286
+ ] })
1287
+ ] }) })
1288
+ ] });
1289
+ }
1290
+ function LayoutSection({ element, onPatch }) {
1291
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1292
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
1293
+ /* @__PURE__ */ jsx(Input, { label: "X", type: "number", value: String(roundFrac(element.x)), onChange: (e) => onPatch({ x: clamp2(parseFloat(e.target.value), 0, 1) }) }),
1294
+ /* @__PURE__ */ jsx(Input, { label: "Y", type: "number", value: String(roundFrac(element.y)), onChange: (e) => onPatch({ y: clamp2(parseFloat(e.target.value), 0, 1) }) }),
1295
+ /* @__PURE__ */ jsx(Input, { label: "Width", type: "number", value: String(roundFrac(element.w)), onChange: (e) => onPatch({ w: clamp2(parseFloat(e.target.value), 0, 1) }) }),
1296
+ /* @__PURE__ */ jsx(Input, { label: "Height", type: "number", value: String(roundFrac(element.h)), onChange: (e) => onPatch({ h: clamp2(parseFloat(e.target.value), 0, 1) }) })
1297
+ ] }),
1298
+ /* @__PURE__ */ jsx(Separator, {}),
1299
+ /* @__PURE__ */ jsx(Slider, { label: "Rotation", value: element.rotation ?? 0, onValueChange: (v) => onPatch({ rotation: Number(v) }), min: -180, max: 180 }),
1300
+ /* @__PURE__ */ jsx(Input, { label: "Z-index", type: "number", value: String(element.z ?? 0), onChange: (e) => onPatch({ z: parseInt(e.target.value, 10) || 0 }) })
1301
+ ] });
1302
+ }
1303
+ function AdvancedSection({ element, onPatch }) {
1304
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1305
+ /* @__PURE__ */ jsx(Input, { label: "Element id", value: element.id, disabled: true }),
1306
+ /* @__PURE__ */ jsx(Text, { size: "xs", className: "!text-zinc-500", children: "The element id is stable \u2014 agents reference elements by id." }),
1307
+ /* @__PURE__ */ jsx(Separator, {}),
1308
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(Action, { size: "sm", variant: element.hidden ? "default" : "ghost", onClick: () => onPatch({ hidden: !element.hidden }), children: element.hidden ? "Hidden \u2014 show" : "Hide on slide" }) })
1309
+ ] });
1310
+ }
1311
+ function StyleSection({ element, onPatch }) {
1312
+ switch (element.type) {
1313
+ case "text":
1314
+ return /* @__PURE__ */ jsx(TextStyleControls, { element, onPatch });
1315
+ case "image":
1316
+ return /* @__PURE__ */ jsx(ImageStyleControls, { element, onPatch });
1317
+ case "shape":
1318
+ return /* @__PURE__ */ jsx(ShapeStyleControls, { element, onPatch });
1319
+ case "code":
1320
+ return /* @__PURE__ */ jsx(CodeStyleControls, { element, onPatch });
1321
+ case "chart":
1322
+ return /* @__PURE__ */ jsx(ChartStyleControls, { element, onPatch });
1323
+ case "table":
1324
+ return /* @__PURE__ */ jsx(TableStyleControls, { element, onPatch });
1325
+ case "embed":
1326
+ return /* @__PURE__ */ jsx(EmbedStyleControls, { element, onPatch });
1327
+ default:
1328
+ return /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "No style controls for this element type." });
1329
+ }
1330
+ }
1331
+ function TextStyleControls({ element, onPatch }) {
1332
+ const setStyle = (next) => onPatch({ style: { ...element.style, ...next } });
1333
+ const s = element.style ?? {};
1334
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1335
+ /* @__PURE__ */ jsx(Textarea, { label: "Content", value: element.content, onValueChange: (v) => onPatch({ content: v }), rows: 4, autoResize: true }),
1336
+ /* @__PURE__ */ jsx(
1337
+ Select,
1338
+ {
1339
+ label: "Format",
1340
+ list: [
1341
+ { value: "markdown", label: "Markdown" },
1342
+ { value: "plain", label: "Plain" },
1343
+ { value: "html", label: "HTML" }
1344
+ ],
1345
+ value: element.format ?? "markdown",
1346
+ onValueChange: (v) => onPatch({ format: v })
1347
+ }
1348
+ ),
1349
+ /* @__PURE__ */ jsx(Separator, {}),
1350
+ /* @__PURE__ */ jsx(Input, { label: "Font size", type: "number", value: String(s.fontSize ?? 28), onChange: (e) => setStyle({ fontSize: parseFloat(e.target.value) || 28 }) }),
1351
+ /* @__PURE__ */ jsx(
1352
+ Select,
1353
+ {
1354
+ label: "Weight",
1355
+ list: [
1356
+ { value: "normal", label: "Normal" },
1357
+ { value: "medium", label: "Medium" },
1358
+ { value: "semibold", label: "Semibold" },
1359
+ { value: "bold", label: "Bold" }
1360
+ ],
1361
+ value: s.weight ?? "normal",
1362
+ onValueChange: (v) => setStyle({ weight: v })
1363
+ }
1364
+ ),
1365
+ /* @__PURE__ */ jsx(
1366
+ Select,
1367
+ {
1368
+ label: "Align",
1369
+ list: [
1370
+ { value: "left", label: "Left" },
1371
+ { value: "center", label: "Center" },
1372
+ { value: "right", label: "Right" },
1373
+ { value: "justify", label: "Justify" }
1374
+ ],
1375
+ value: s.align ?? "left",
1376
+ onValueChange: (v) => setStyle({ align: v })
1377
+ }
1378
+ ),
1379
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsx(ColorPicker, { value: s.color ?? "#0f172a", onChange: (c) => setStyle({ color: c }) }) })
1380
+ ] });
1381
+ }
1382
+ function ImageStyleControls({ element, onPatch }) {
1383
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1384
+ /* @__PURE__ */ jsx(Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1385
+ /* @__PURE__ */ jsx(Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1386
+ /* @__PURE__ */ jsx(
1387
+ Select,
1388
+ {
1389
+ label: "Fit",
1390
+ list: [
1391
+ { value: "contain", label: "Contain" },
1392
+ { value: "cover", label: "Cover" },
1393
+ { value: "fill", label: "Fill (stretch)" },
1394
+ { value: "scale-down", label: "Scale down" }
1395
+ ],
1396
+ value: element.fit ?? "contain",
1397
+ onValueChange: (v) => onPatch({ fit: v })
1398
+ }
1399
+ )
1400
+ ] });
1401
+ }
1402
+ function ShapeStyleControls({ element, onPatch }) {
1403
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1404
+ /* @__PURE__ */ jsx(
1405
+ Select,
1406
+ {
1407
+ label: "Shape",
1408
+ list: [
1409
+ { value: "rect", label: "Rectangle" },
1410
+ { value: "rounded-rect", label: "Rounded rectangle" },
1411
+ { value: "ellipse", label: "Ellipse" },
1412
+ { value: "triangle", label: "Triangle" },
1413
+ { value: "line", label: "Line" },
1414
+ { value: "arrow", label: "Arrow" }
1415
+ ],
1416
+ value: element.shape,
1417
+ onValueChange: (v) => onPatch({ shape: v })
1418
+ }
1419
+ ),
1420
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Fill", children: /* @__PURE__ */ jsx(ColorPicker, { value: element.fill ?? "#ffffff", onChange: (c) => onPatch({ fill: c }) }) }),
1421
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Stroke", children: /* @__PURE__ */ jsx(ColorPicker, { value: element.stroke ?? "#0f172a", onChange: (c) => onPatch({ stroke: c }) }) }),
1422
+ /* @__PURE__ */ jsx(Slider, { label: "Stroke width", value: element.strokeWidth ?? 2, onValueChange: (v) => onPatch({ strokeWidth: Number(v) }), min: 0, max: 20, step: 0.5 }),
1423
+ (element.shape === "rounded-rect" || element.shape === "rect") && /* @__PURE__ */ jsx(Slider, { label: "Corner radius", value: element.radius ?? 0, onValueChange: (v) => onPatch({ radius: Number(v) }), min: 0, max: 40 })
1424
+ ] });
1425
+ }
1426
+ function CodeStyleControls({ element, onPatch }) {
1427
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1428
+ /* @__PURE__ */ jsx(Textarea, { label: "Code", value: element.code, onValueChange: (v) => onPatch({ code: v }), rows: 6, autoResize: true }),
1429
+ /* @__PURE__ */ jsx(Input, { label: "Language", value: element.language ?? "javascript", onChange: (e) => onPatch({ language: e.target.value }) }),
1430
+ /* @__PURE__ */ jsx(
1431
+ Select,
1432
+ {
1433
+ label: "Theme",
1434
+ list: [
1435
+ { value: "auto", label: "Auto" },
1436
+ { value: "light", label: "Light" },
1437
+ { value: "dark", label: "Dark" }
1438
+ ],
1439
+ value: element.codeTheme ?? "auto",
1440
+ onValueChange: (v) => onPatch({ codeTheme: v })
1441
+ }
1442
+ )
1443
+ ] });
1444
+ }
1445
+ function ChartStyleControls({ element, onPatch }) {
1446
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1447
+ /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "Chart option is JSON \u2014 paste any ECharts option here." }),
1448
+ /* @__PURE__ */ jsx(
1449
+ Textarea,
1450
+ {
1451
+ label: "ECharts option (JSON)",
1452
+ value: JSON.stringify(element.option, null, 2),
1453
+ onValueChange: (v) => {
1454
+ try {
1455
+ onPatch({ option: JSON.parse(v) });
1456
+ } catch {
1457
+ }
1458
+ },
1459
+ rows: 10
1460
+ }
1461
+ )
1462
+ ] });
1463
+ }
1464
+ function TableStyleControls({ element, onPatch }) {
1465
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1466
+ /* @__PURE__ */ jsx(
1467
+ Textarea,
1468
+ {
1469
+ label: "Columns (JSON)",
1470
+ value: JSON.stringify(element.columns, null, 2),
1471
+ onValueChange: (v) => {
1472
+ try {
1473
+ onPatch({ columns: JSON.parse(v) });
1474
+ } catch {
1475
+ }
1476
+ },
1477
+ rows: 5
1478
+ }
1479
+ ),
1480
+ /* @__PURE__ */ jsx(
1481
+ Textarea,
1482
+ {
1483
+ label: "Rows (JSON)",
1484
+ value: JSON.stringify(element.rows, null, 2),
1485
+ onValueChange: (v) => {
1486
+ try {
1487
+ onPatch({ rows: JSON.parse(v) });
1488
+ } catch {
1489
+ }
1490
+ },
1491
+ rows: 8
1492
+ }
1493
+ )
1494
+ ] });
1495
+ }
1496
+ function EmbedStyleControls({ element, onPatch }) {
1497
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1498
+ /* @__PURE__ */ jsx(Input, { label: "Embed URL", value: element.src, onChange: (e) => onPatch({ src: e.target.value }) }),
1499
+ /* @__PURE__ */ jsx(Input, { label: "Title (a11y)", value: element.title ?? "", onChange: (e) => onPatch({ title: e.target.value }) }),
1500
+ /* @__PURE__ */ jsx(Input, { label: "Sandbox", value: element.sandbox ?? "allow-scripts", onChange: (e) => onPatch({ sandbox: e.target.value }) })
1501
+ ] });
1502
+ }
1503
+ function FieldLabel({ label, children }) {
1504
+ return /* @__PURE__ */ jsxs("label", { className: "block", children: [
1505
+ /* @__PURE__ */ jsx("span", { className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: label }),
1506
+ children
1507
+ ] });
1508
+ }
1509
+ function clamp2(n, min, max) {
1510
+ if (!Number.isFinite(n)) return min;
1511
+ return Math.max(min, Math.min(max, n));
1512
+ }
1513
+ function roundFrac(n) {
1514
+ return Math.round(n * 1e3) / 1e3;
1515
+ }
1516
+ function SpeakerNotes({ notes, onChange, placeholder }) {
1517
+ return /* @__PURE__ */ jsxs("div", { className: "fs-notes border-t border-zinc-200 bg-white p-3 dark:border-zinc-800 dark:bg-zinc-950", children: [
1518
+ /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "mb-1 !uppercase !tracking-wider !text-zinc-500", children: "Speaker notes" }),
1519
+ /* @__PURE__ */ jsx(
1520
+ Textarea,
1521
+ {
1522
+ value: notes ?? "",
1523
+ onValueChange: onChange,
1524
+ placeholder: placeholder ?? "Notes are visible only to the presenter\u2026",
1525
+ rows: 3,
1526
+ autoResize: true,
1527
+ minRows: 2,
1528
+ maxRows: 6
1529
+ }
1530
+ )
1531
+ ] });
1532
+ }
1533
+ function DeckEditor({
1534
+ value,
1535
+ onChange,
1536
+ onOp,
1537
+ onPresent,
1538
+ selectedSlideId: controlledSlideId,
1539
+ onSelectedSlideChange,
1540
+ renderElement,
1541
+ hideRail = false,
1542
+ hideNotes = false,
1543
+ hideToolbar = false,
1544
+ hideInspector = false,
1545
+ toolbarExtra,
1546
+ className
1547
+ }) {
1548
+ const deck = value;
1549
+ const ops = useDeckState({ value: deck, onChange, onOp });
1550
+ const [internalSlideId, setInternalSlideId] = useState(deck.slides[0]?.id ?? null);
1551
+ const isControlled = controlledSlideId !== void 0;
1552
+ const slideId2 = isControlled ? controlledSlideId : internalSlideId;
1553
+ const setSlideId = useCallback(
1554
+ (id) => {
1555
+ if (!isControlled) setInternalSlideId(id);
1556
+ onSelectedSlideChange?.(id);
1557
+ },
1558
+ [isControlled, onSelectedSlideChange]
1559
+ );
1560
+ useEffect(() => {
1561
+ if (slideId2 && !deck.slides.some((s) => s.id === slideId2)) {
1562
+ setSlideId(deck.slides[0]?.id ?? null);
1563
+ } else if (!slideId2 && deck.slides.length > 0) {
1564
+ setSlideId(deck.slides[0].id);
1565
+ }
1566
+ }, [deck.slides, slideId2, setSlideId]);
1567
+ const slide = deck.slides.find((s) => s.id === slideId2);
1568
+ const [elementIdSelected, setElementIdSelected] = useState(null);
1569
+ useEffect(() => {
1570
+ setElementIdSelected(null);
1571
+ }, [slideId2]);
1572
+ const selectedElement = slide && elementIdSelected ? slide.elements.find((e) => e.id === elementIdSelected) ?? null : null;
1573
+ const insert = useCallback(
1574
+ (element) => {
1575
+ if (!slide) return;
1576
+ const id = ops.addElement(slide.id, { id: elementId(), ...element });
1577
+ setElementIdSelected(id);
1578
+ },
1579
+ [slide, ops]
1580
+ );
1581
+ const insertText = useCallback(
1582
+ () => insert({
1583
+ type: "text",
1584
+ x: 0.1,
1585
+ y: 0.4,
1586
+ w: 0.8,
1587
+ h: 0.2,
1588
+ content: "Click to edit",
1589
+ format: "plain",
1590
+ style: { fontSize: 36, weight: "semibold", align: "center" }
1591
+ }),
1592
+ [insert]
1593
+ );
1594
+ const insertImage = useCallback(
1595
+ () => insert({
1596
+ type: "image",
1597
+ x: 0.25,
1598
+ y: 0.25,
1599
+ w: 0.5,
1600
+ h: 0.5,
1601
+ src: "https://placehold.co/600x400?text=Image",
1602
+ fit: "contain"
1603
+ }),
1604
+ [insert]
1605
+ );
1606
+ const insertShape = useCallback(
1607
+ (shape) => insert({
1608
+ type: "shape",
1609
+ shape,
1610
+ x: 0.3,
1611
+ y: 0.3,
1612
+ w: 0.4,
1613
+ h: 0.4,
1614
+ fill: shape === "line" || shape === "arrow" ? "none" : "rgba(139,92,246,0.15)",
1615
+ stroke: "#8b5cf6",
1616
+ strokeWidth: 2
1617
+ }),
1618
+ [insert]
1619
+ );
1620
+ const insertChart = useCallback(
1621
+ () => insert({
1622
+ type: "chart",
1623
+ x: 0.1,
1624
+ y: 0.2,
1625
+ w: 0.8,
1626
+ h: 0.6,
1627
+ option: {
1628
+ xAxis: { type: "category", data: ["Q1", "Q2", "Q3", "Q4"] },
1629
+ yAxis: { type: "value" },
1630
+ series: [{ type: "bar", data: [24, 38, 31, 47] }]
1631
+ }
1632
+ }),
1633
+ [insert]
1634
+ );
1635
+ const insertCode = useCallback(
1636
+ () => insert({
1637
+ type: "code",
1638
+ x: 0.15,
1639
+ y: 0.2,
1640
+ w: 0.7,
1641
+ h: 0.6,
1642
+ code: 'function hello() {\n return "world";\n}\n',
1643
+ language: "typescript",
1644
+ codeTheme: "dark"
1645
+ }),
1646
+ [insert]
1647
+ );
1648
+ const insertTable = useCallback(
1649
+ () => insert({
1650
+ type: "table",
1651
+ x: 0.15,
1652
+ y: 0.25,
1653
+ w: 0.7,
1654
+ h: 0.5,
1655
+ columns: [
1656
+ { key: "name", label: "Name" },
1657
+ { key: "value", label: "Value" }
1658
+ ],
1659
+ rows: [
1660
+ { name: "Alpha", value: 12 },
1661
+ { name: "Beta", value: 34 },
1662
+ { name: "Gamma", value: 56 }
1663
+ ]
1664
+ }),
1665
+ [insert]
1666
+ );
1667
+ return /* @__PURE__ */ jsxs(
1668
+ "div",
1669
+ {
1670
+ className: `fs-editor flex h-full w-full flex-col bg-zinc-100 dark:bg-zinc-950 ${className ?? ""}`,
1671
+ "data-fancy-slides-editor": deck.id,
1672
+ children: [
1673
+ !hideToolbar && /* @__PURE__ */ jsx(
1674
+ EditorToolbar,
1675
+ {
1676
+ title: deck.title,
1677
+ onTitleChange: (t) => ops.setTitle(t),
1678
+ themeName: deck.theme.name,
1679
+ onApplyTheme: (t) => ops.applyTheme(t),
1680
+ onInsertText: insertText,
1681
+ onInsertImage: insertImage,
1682
+ onInsertShape: insertShape,
1683
+ onInsertChart: insertChart,
1684
+ onInsertCode: insertCode,
1685
+ onInsertTable: insertTable,
1686
+ onPresent,
1687
+ disabled: !slide
1688
+ }
1689
+ ),
1690
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1", children: [
1691
+ !hideRail && /* @__PURE__ */ jsx("div", { className: "w-56 shrink-0 overflow-y-auto border-r border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-950", children: /* @__PURE__ */ jsx(
1692
+ SlideRail,
1693
+ {
1694
+ slides: deck.slides,
1695
+ selectedId: slideId2,
1696
+ theme: deck.theme,
1697
+ onSelect: setSlideId,
1698
+ onAdd: (after) => {
1699
+ const id = ops.addSlide(after !== void 0 ? after : deck.slides.length);
1700
+ setSlideId(id);
1701
+ },
1702
+ onDuplicate: (id) => {
1703
+ const newId = ops.duplicateSlide(id);
1704
+ setSlideId(newId);
1705
+ },
1706
+ onRemove: (id) => ops.removeSlide(id),
1707
+ onReorder: (id, toIndex) => ops.reorderSlide(id, toIndex),
1708
+ renderElement
1709
+ }
1710
+ ) }),
1711
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
1712
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center overflow-auto p-6", children: slide ? /* @__PURE__ */ jsx(
1713
+ "div",
1714
+ {
1715
+ className: "rounded-lg shadow-xl",
1716
+ style: {
1717
+ width: "min(96%, 1280px)",
1718
+ aspectRatio: String(resolveTheme(deck.theme).aspectRatio ?? 16 / 9),
1719
+ background: "white"
1720
+ },
1721
+ children: /* @__PURE__ */ jsx(
1722
+ Slide,
1723
+ {
1724
+ slide,
1725
+ theme: deck.theme,
1726
+ editing: true,
1727
+ onElementContentChange: (eid, content) => ops.updateElement(slide.id, eid, { content }),
1728
+ onElementSelect: setElementIdSelected,
1729
+ selectedElementId: elementIdSelected,
1730
+ onElementMove: (eid, x, y) => ops.moveElement(slide.id, eid, x, y),
1731
+ onElementResize: (eid, patch) => ops.updateElement(slide.id, eid, patch),
1732
+ renderElement
1733
+ }
1734
+ )
1735
+ }
1736
+ ) : /* @__PURE__ */ jsx("div", { className: "grid place-items-center rounded-lg border border-dashed border-zinc-300 bg-white px-12 py-24 text-sm text-zinc-500 dark:border-zinc-700 dark:bg-zinc-950", children: "Add a slide to start editing." }) }),
1737
+ !hideNotes && slide && /* @__PURE__ */ jsx(SpeakerNotes, { notes: slide.notes, onChange: (n) => ops.setNotes(slide.id, n) })
1738
+ ] }),
1739
+ !hideInspector && /* @__PURE__ */ jsx("div", { className: "w-72 shrink-0 overflow-y-auto", children: /* @__PURE__ */ jsx(
1740
+ ElementInspector,
1741
+ {
1742
+ element: selectedElement,
1743
+ onPatch: (patch) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, patch),
1744
+ onDelete: () => {
1745
+ if (!slide || !elementIdSelected) return;
1746
+ ops.removeElement(slide.id, elementIdSelected);
1747
+ setElementIdSelected(null);
1748
+ },
1749
+ onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked })
1750
+ }
1751
+ ) })
1752
+ ] }),
1753
+ toolbarExtra
1754
+ ]
1755
+ }
1756
+ );
1757
+ }
1758
+
1759
+ export { DeckEditor, EditorToolbar, ElementInspector, ImageElementRenderer, PresenterView, ShapeElementRenderer, Slide, SlideRail, SlideThumbnail, SlideViewer, SpeakerNotes, TextElementRenderer, builtinThemes, darkTheme, deckId, defaultTheme, defineTheme, elementId, nextId, reduce as reduceDeck, resolveTheme, slideId, useDeckState, useSlideKeyboard, vividTheme };
1760
+ //# sourceMappingURL=index.js.map
1761
+ //# sourceMappingURL=index.js.map