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