@particle-academy/fancy-slides 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -72,6 +72,79 @@ function resolveTheme(theme) {
72
72
  function cn(...parts) {
73
73
  return parts.filter(Boolean).join(" ");
74
74
  }
75
+
76
+ // src/utils/builds.ts
77
+ var DEFAULT_BUILD_DURATION = 500;
78
+ function collectBuilds(slide) {
79
+ if (!slide) return [];
80
+ const builds = [];
81
+ slide.elements.forEach((element, index) => {
82
+ if (element.animation) {
83
+ builds.push({ element, animation: element.animation, index });
84
+ }
85
+ });
86
+ return builds.sort((a, b) => {
87
+ const ao = a.animation.order ?? 0;
88
+ const bo = b.animation.order ?? 0;
89
+ if (ao !== bo) return ao - bo;
90
+ return a.index - b.index;
91
+ });
92
+ }
93
+ function buildSteps(slide) {
94
+ const builds = collectBuilds(slide);
95
+ const steps = [];
96
+ for (const build of builds) {
97
+ const trigger = build.animation.trigger ?? "on-click";
98
+ if (steps.length === 0 || trigger === "on-click") {
99
+ steps.push({ builds: [build] });
100
+ } else {
101
+ steps[steps.length - 1].builds.push(build);
102
+ }
103
+ }
104
+ return steps;
105
+ }
106
+ function totalBuildSteps(slide) {
107
+ return buildSteps(slide).length;
108
+ }
109
+ function visibleElementIds(slide, buildStep) {
110
+ const visible = /* @__PURE__ */ new Set();
111
+ if (!slide) return visible;
112
+ const steps = buildSteps(slide);
113
+ const stepOfElement = /* @__PURE__ */ new Map();
114
+ steps.forEach((step, i) => {
115
+ for (const b of step.builds) stepOfElement.set(b.element.id, i + 1);
116
+ });
117
+ for (const element of slide.elements) {
118
+ const revealStep = stepOfElement.get(element.id);
119
+ if (revealStep === void 0) {
120
+ visible.add(element.id);
121
+ } else if (buildStep >= revealStep) {
122
+ visible.add(element.id);
123
+ }
124
+ }
125
+ return visible;
126
+ }
127
+ function buildsForStep(slide, buildStep) {
128
+ const steps = buildSteps(slide);
129
+ const step = steps[buildStep - 1];
130
+ return step ? step.builds : [];
131
+ }
132
+ function stepDelays(builds) {
133
+ const delays = /* @__PURE__ */ new Map();
134
+ const lead = builds[0];
135
+ if (!lead) return delays;
136
+ const leadDelay = lead.animation.delay ?? 0;
137
+ const leadDuration = lead.animation.duration ?? DEFAULT_BUILD_DURATION;
138
+ delays.set(lead.element.id, leadDelay);
139
+ for (let i = 1; i < builds.length; i++) {
140
+ const b = builds[i];
141
+ const own = b.animation.delay ?? 0;
142
+ const trigger = b.animation.trigger ?? "on-click";
143
+ const base = trigger === "after-prev" ? leadDelay + leadDuration : leadDelay;
144
+ delays.set(b.element.id, base + own);
145
+ }
146
+ return delays;
147
+ }
75
148
  function TextElementRenderer({
76
149
  element,
77
150
  theme,
@@ -161,6 +234,22 @@ function weight(w) {
161
234
  return void 0;
162
235
  }
163
236
  function ImageElementRenderer({ element }) {
237
+ const crop = element.crop;
238
+ const fit = element.fit ?? "contain";
239
+ if (crop && crop.w > 0 && crop.h > 0) {
240
+ const inner = {
241
+ position: "absolute",
242
+ left: 0,
243
+ top: 0,
244
+ width: `${1 / crop.w * 100}%`,
245
+ height: `${1 / crop.h * 100}%`,
246
+ transform: `translate(${-crop.x / crop.w * 100}%, ${-crop.y / crop.h * 100}%)`,
247
+ transformOrigin: "top left",
248
+ objectFit: fit,
249
+ display: "block"
250
+ };
251
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", width: "100%", height: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: element.src, alt: element.alt ?? "", style: inner, draggable: false }) });
252
+ }
164
253
  return /* @__PURE__ */ jsxRuntime.jsx(
165
254
  "img",
166
255
  {
@@ -169,7 +258,7 @@ function ImageElementRenderer({ element }) {
169
258
  style: {
170
259
  width: "100%",
171
260
  height: "100%",
172
- objectFit: element.fit ?? "contain",
261
+ objectFit: fit,
173
262
  display: "block"
174
263
  },
175
264
  draggable: false
@@ -264,12 +353,94 @@ function relativeLuminance(r, g, b) {
264
353
  };
265
354
  return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
266
355
  }
356
+
357
+ // src/components/Slide/builds-style.ts
358
+ var DEFAULT_BUILD_DURATION2 = 500;
359
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
360
+ function buildEnterStyle(animation, effectiveDelay) {
361
+ const duration = animation.duration ?? DEFAULT_BUILD_DURATION2;
362
+ const dir = animation.direction ?? "left";
363
+ let name;
364
+ switch (animation.effect) {
365
+ case "fade":
366
+ name = "fs-build-fade";
367
+ break;
368
+ case "zoom":
369
+ name = "fs-build-zoom";
370
+ break;
371
+ case "fly-in":
372
+ name = `fs-build-fly-${dir}`;
373
+ break;
374
+ case "wipe":
375
+ name = `fs-build-wipe-${dir}`;
376
+ break;
377
+ default:
378
+ name = "fs-build-fade";
379
+ }
380
+ return {
381
+ animationName: name,
382
+ animationDuration: `${duration}ms`,
383
+ animationDelay: `${effectiveDelay}ms`,
384
+ animationTimingFunction: EASE,
385
+ animationFillMode: "both"
386
+ };
387
+ }
388
+ var BUILD_KEYFRAMES = `
389
+ @media (prefers-reduced-motion: reduce) {
390
+ .fs-build-enter { animation: none !important; }
391
+ }
392
+ @media (prefers-reduced-motion: no-preference) {
393
+ @keyframes fs-build-fade {
394
+ from { opacity: 0; }
395
+ to { opacity: 1; }
396
+ }
397
+ @keyframes fs-build-zoom {
398
+ from { opacity: 0; transform: scale(0.8); }
399
+ to { opacity: 1; transform: scale(1); }
400
+ }
401
+ @keyframes fs-build-fly-left {
402
+ from { opacity: 0; transform: translateX(-24%); }
403
+ to { opacity: 1; transform: translateX(0); }
404
+ }
405
+ @keyframes fs-build-fly-right {
406
+ from { opacity: 0; transform: translateX(24%); }
407
+ to { opacity: 1; transform: translateX(0); }
408
+ }
409
+ @keyframes fs-build-fly-up {
410
+ from { opacity: 0; transform: translateY(24%); }
411
+ to { opacity: 1; transform: translateY(0); }
412
+ }
413
+ @keyframes fs-build-fly-down {
414
+ from { opacity: 0; transform: translateY(-24%); }
415
+ to { opacity: 1; transform: translateY(0); }
416
+ }
417
+ /* wipe: clip-path inset reveals from the named edge toward the opposite one.
418
+ inset(top right bottom left) \u2014 start fully clipped on the far side. */
419
+ @keyframes fs-build-wipe-left {
420
+ from { clip-path: inset(0 100% 0 0); }
421
+ to { clip-path: inset(0 0 0 0); }
422
+ }
423
+ @keyframes fs-build-wipe-right {
424
+ from { clip-path: inset(0 0 0 100%); }
425
+ to { clip-path: inset(0 0 0 0); }
426
+ }
427
+ @keyframes fs-build-wipe-up {
428
+ from { clip-path: inset(100% 0 0 0); }
429
+ to { clip-path: inset(0 0 0 0); }
430
+ }
431
+ @keyframes fs-build-wipe-down {
432
+ from { clip-path: inset(0 0 100% 0); }
433
+ to { clip-path: inset(0 0 0 0); }
434
+ }
435
+ }
436
+ `;
267
437
  function Slide({
268
438
  slide,
269
439
  theme,
270
440
  width,
271
441
  aspectRatio,
272
442
  editing = false,
443
+ buildStep,
273
444
  onElementContentChange,
274
445
  onElementSelect,
275
446
  selectedElementId,
@@ -313,7 +484,21 @@ function Slide({
313
484
  }),
314
485
  [t, effectiveBg, slideWidthPx]
315
486
  );
316
- return /* @__PURE__ */ jsxRuntime.jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsxRuntime.jsx(
487
+ const buildInfo = react.useMemo(() => {
488
+ if (editing) return null;
489
+ const steps = buildSteps(slide);
490
+ if (steps.length === 0) return null;
491
+ const revealStep = /* @__PURE__ */ new Map();
492
+ steps.forEach((step, i) => {
493
+ for (const b of step.builds) revealStep.set(b.element.id, i + 1);
494
+ });
495
+ const driven = buildStep !== void 0;
496
+ const currentStep = driven ? buildStep : steps.length;
497
+ const firing = driven ? steps[currentStep - 1] : void 0;
498
+ const delays = firing ? stepDelays(firing.builds) : /* @__PURE__ */ new Map();
499
+ return { revealStep, currentStep, delays };
500
+ }, [editing, slide, buildStep]);
501
+ return /* @__PURE__ */ jsxRuntime.jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsxRuntime.jsxs(
317
502
  "div",
318
503
  {
319
504
  ref,
@@ -331,23 +516,45 @@ function Slide({
331
516
  onClick: (e) => {
332
517
  if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
333
518
  },
334
- children: orderedElements(slide.elements).map((element) => /* @__PURE__ */ jsxRuntime.jsx(
335
- SlideElementHost,
336
- {
337
- element,
338
- theme: t,
339
- slideWidthPx,
340
- slideHeightPx,
341
- editing,
342
- selected: selectedElementId === element.id,
343
- onContentChange: onElementContentChange,
344
- onSelect: onElementSelect,
345
- onMove: onElementMove,
346
- onResize: onElementResize,
347
- renderElement
348
- },
349
- element.id
350
- ))
519
+ children: [
520
+ buildInfo && /* @__PURE__ */ jsxRuntime.jsx("style", { children: BUILD_KEYFRAMES }),
521
+ orderedElements(slide.elements).map((element) => {
522
+ let buildHidden = false;
523
+ let buildAnimation;
524
+ let buildDelay = 0;
525
+ if (buildInfo) {
526
+ const step = buildInfo.revealStep.get(element.id);
527
+ if (step !== void 0) {
528
+ if (buildInfo.currentStep < step) {
529
+ buildHidden = true;
530
+ } else if (buildInfo.currentStep === step && element.animation) {
531
+ buildAnimation = element.animation;
532
+ buildDelay = buildInfo.delays.get(element.id) ?? 0;
533
+ }
534
+ }
535
+ }
536
+ if (buildHidden) return null;
537
+ return /* @__PURE__ */ jsxRuntime.jsx(
538
+ SlideElementHost,
539
+ {
540
+ element,
541
+ theme: t,
542
+ slideWidthPx,
543
+ slideHeightPx,
544
+ editing,
545
+ selected: selectedElementId === element.id,
546
+ onContentChange: onElementContentChange,
547
+ onSelect: onElementSelect,
548
+ onMove: onElementMove,
549
+ onResize: onElementResize,
550
+ renderElement,
551
+ buildAnimation,
552
+ buildDelay
553
+ },
554
+ element.id
555
+ );
556
+ })
557
+ ]
351
558
  }
352
559
  ) });
353
560
  }
@@ -364,7 +571,9 @@ function SlideElementHost({
364
571
  onSelect,
365
572
  onMove,
366
573
  onResize,
367
- renderElement
574
+ renderElement,
575
+ buildAnimation,
576
+ buildDelay = 0
368
577
  }) {
369
578
  const dragRef = react.useRef(null);
370
579
  if (element.hidden) return null;
@@ -433,15 +642,18 @@ function SlideElementHost({
433
642
  outline: selected ? "2px solid #8b5cf6" : void 0,
434
643
  outlineOffset: selected ? 2 : void 0,
435
644
  cursor: canMove ? "move" : interactive ? "pointer" : "default",
436
- touchAction: canMove ? "none" : void 0
645
+ touchAction: canMove ? "none" : void 0,
646
+ ...buildAnimation ? buildEnterStyle(buildAnimation, buildDelay) : null
437
647
  };
438
648
  const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
439
649
  return /* @__PURE__ */ jsxRuntime.jsxs(
440
650
  "div",
441
651
  {
652
+ className: buildAnimation ? "fs-build-enter" : void 0,
442
653
  style: box,
443
654
  "data-fancy-slides-element": element.id,
444
655
  "data-fancy-slides-element-type": element.type,
656
+ "data-fancy-slides-build": buildAnimation ? "" : void 0,
445
657
  onPointerDown: canMove ? startDrag("move") : void 0,
446
658
  onPointerMove: canMove ? onPointerMove : void 0,
447
659
  onPointerUp: canMove ? endDrag : void 0,
@@ -563,6 +775,8 @@ function useSlideKeyboard({
563
775
  total,
564
776
  index,
565
777
  goTo,
778
+ onAdvance,
779
+ onRetreat,
566
780
  onExit,
567
781
  onBlank,
568
782
  onFullscreen,
@@ -579,13 +793,15 @@ function useSlideKeyboard({
579
793
  case "ArrowLeft":
580
794
  case "PageUp":
581
795
  e.preventDefault();
582
- if (index > 0) goTo(index - 1);
796
+ if (onRetreat) onRetreat();
797
+ else if (index > 0) goTo(index - 1);
583
798
  return;
584
799
  case "ArrowRight":
585
800
  case "PageDown":
586
801
  case " ":
587
802
  e.preventDefault();
588
- if (index < total - 1) goTo(index + 1);
803
+ if (onAdvance) onAdvance();
804
+ else if (index < total - 1) goTo(index + 1);
589
805
  return;
590
806
  case "Home":
591
807
  e.preventDefault();
@@ -627,7 +843,7 @@ function useSlideKeyboard({
627
843
  };
628
844
  window.addEventListener("keydown", handler);
629
845
  return () => window.removeEventListener("keydown", handler);
630
- }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
846
+ }, [enabled, index, total, goTo, onAdvance, onRetreat, onExit, onBlank, onFullscreen]);
631
847
  }
632
848
  function SlideViewer({
633
849
  deck,
@@ -655,13 +871,34 @@ function SlideViewer({
655
871
  const containerRef = react.useRef(null);
656
872
  const prevIndexRef = react.useRef(index);
657
873
  const forward = index >= prevIndexRef.current;
874
+ const slide = deck.slides[index];
875
+ const totalSteps = totalBuildSteps(slide);
876
+ const [buildStep, setBuildStep] = react.useState(0);
877
+ const nextFreshRef = react.useRef(false);
658
878
  react.useEffect(() => {
879
+ if (index === prevIndexRef.current) return;
659
880
  prevIndexRef.current = index;
660
- }, [index]);
881
+ const fresh = nextFreshRef.current;
882
+ nextFreshRef.current = false;
883
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
884
+ }, [index, deck.slides]);
885
+ const advance = react.useCallback(() => {
886
+ if (buildStep < totalSteps) {
887
+ setBuildStep((s) => s + 1);
888
+ } else if (index < deck.slides.length - 1) {
889
+ nextFreshRef.current = true;
890
+ goTo(index + 1);
891
+ }
892
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
893
+ const retreat = react.useCallback(() => {
894
+ if (index > 0) goTo(index - 1);
895
+ }, [index, goTo]);
661
896
  useSlideKeyboard({
662
897
  total: deck.slides.length,
663
898
  index,
664
899
  goTo,
900
+ onAdvance: advance,
901
+ onRetreat: retreat,
665
902
  onExit,
666
903
  onBlank: () => setBlanked((b) => !b),
667
904
  onFullscreen: () => {
@@ -674,11 +911,15 @@ function SlideViewer({
674
911
  react.useEffect(() => {
675
912
  if (!autoAdvanceMs || deck.slides.length <= 1) return;
676
913
  const t = setTimeout(() => {
677
- goTo(index + 1 < deck.slides.length ? index + 1 : 0);
914
+ if (buildStep < totalSteps) {
915
+ setBuildStep((s) => s + 1);
916
+ } else {
917
+ nextFreshRef.current = true;
918
+ goTo(index + 1 < deck.slides.length ? index + 1 : 0);
919
+ }
678
920
  }, autoAdvanceMs);
679
921
  return () => clearTimeout(t);
680
- }, [autoAdvanceMs, index, deck.slides.length, goTo]);
681
- const slide = deck.slides[index];
922
+ }, [autoAdvanceMs, index, deck.slides.length, goTo, buildStep, totalSteps]);
682
923
  const theme = resolveTheme(deck.theme);
683
924
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
684
925
  const transition = slide?.transition ?? theme.defaultTransition;
@@ -700,6 +941,11 @@ function SlideViewer({
700
941
  },
701
942
  tabIndex: 0,
702
943
  "data-fancy-slides-viewer": deck.id,
944
+ "data-fancy-slides-build-step": buildStep,
945
+ onClick: () => {
946
+ if (blanked) return;
947
+ advance();
948
+ },
703
949
  children: [
704
950
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: TRANSITION_KEYFRAMES }),
705
951
  !blanked && slide && /* @__PURE__ */ jsxRuntime.jsx(
@@ -713,7 +959,7 @@ function SlideViewer({
713
959
  ["--fs-ratio"]: aspectRatio.toString(),
714
960
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
715
961
  },
716
- children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement }) }, index)
962
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, buildStep, renderElement }) }, index)
717
963
  }
718
964
  ),
719
965
  !hideChrome && !blanked && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -744,7 +990,7 @@ function SlideViewer({
744
990
  );
745
991
  }
746
992
  var DEFAULT_DURATION = 400;
747
- var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
993
+ var EASE2 = "cubic-bezier(0.16, 1, 0.3, 1)";
748
994
  function transitionEnterStyle(transition, forward) {
749
995
  const kind = transition?.kind ?? "none";
750
996
  if (kind === "none") return { width: "100%", height: "100%" };
@@ -770,7 +1016,7 @@ function transitionEnterStyle(transition, forward) {
770
1016
  height: "100%",
771
1017
  animationName: name,
772
1018
  animationDuration: `${duration}ms`,
773
- animationTimingFunction: EASE,
1019
+ animationTimingFunction: EASE2,
774
1020
  animationFillMode: "both"
775
1021
  };
776
1022
  }
@@ -826,14 +1072,38 @@ function PresenterView({
826
1072
  },
827
1073
  [deck.slides.length, isControlled, onIndexChange]
828
1074
  );
1075
+ const slide = deck.slides[index];
1076
+ const totalSteps = totalBuildSteps(slide);
1077
+ const [buildStep, setBuildStep] = react.useState(0);
1078
+ const prevIndexRef = react.useRef(index);
1079
+ const nextFreshRef = react.useRef(false);
1080
+ react.useEffect(() => {
1081
+ if (index === prevIndexRef.current) return;
1082
+ prevIndexRef.current = index;
1083
+ const fresh = nextFreshRef.current;
1084
+ nextFreshRef.current = false;
1085
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
1086
+ }, [index, deck.slides]);
1087
+ const advance = react.useCallback(() => {
1088
+ if (buildStep < totalSteps) {
1089
+ setBuildStep((s) => s + 1);
1090
+ } else if (index < deck.slides.length - 1) {
1091
+ nextFreshRef.current = true;
1092
+ goTo(index + 1);
1093
+ }
1094
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
1095
+ const retreat = react.useCallback(() => {
1096
+ if (index > 0) goTo(index - 1);
1097
+ }, [index, goTo]);
829
1098
  useSlideKeyboard({
830
1099
  total: deck.slides.length,
831
1100
  index,
832
1101
  goTo,
1102
+ onAdvance: advance,
1103
+ onRetreat: retreat,
833
1104
  onExit
834
1105
  });
835
1106
  const theme = resolveTheme(deck.theme);
836
- const slide = deck.slides[index];
837
1107
  const nextSlide = deck.slides[index + 1];
838
1108
  const [now, setNow] = react.useState(() => Date.now());
839
1109
  react.useEffect(() => {
@@ -880,7 +1150,7 @@ function PresenterView({
880
1150
  borderRadius: 8,
881
1151
  overflow: "hidden"
882
1152
  },
883
- children: slide ? /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement }) : null
1153
+ children: slide ? /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, buildStep, renderElement }) : null
884
1154
  }
885
1155
  )
886
1156
  }
@@ -984,8 +1254,8 @@ function PresenterView({
984
1254
  /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
985
1255
  /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
986
1256
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
987
- /* @__PURE__ */ jsxRuntime.jsx(NavButton, { onClick: () => goTo(index - 1), disabled: index === 0, children: "\u2190 Prev" }),
988
- /* @__PURE__ */ jsxRuntime.jsx(NavButton, { onClick: () => goTo(index + 1), disabled: index >= deck.slides.length - 1, children: "Next \u2192" })
1257
+ /* @__PURE__ */ jsxRuntime.jsx(NavButton, { onClick: retreat, disabled: index === 0, children: "\u2190 Prev" }),
1258
+ /* @__PURE__ */ jsxRuntime.jsx(NavButton, { onClick: advance, disabled: index >= deck.slides.length - 1 && buildStep >= totalSteps, children: "Next \u2192" })
989
1259
  ] })
990
1260
  ]
991
1261
  }
@@ -1168,6 +1438,7 @@ function useDeckState({ value, onChange, onOp }) {
1168
1438
  updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1169
1439
  moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1170
1440
  resizeElement: (slideIdArg, elementIdArg, w, h) => apply({ kind: "element_resize", slideId: slideIdArg, elementId: elementIdArg, w, h }),
1441
+ setAnimation: (slideIdArg, elementIdArg, animation) => apply({ kind: "element_set_animation", slideId: slideIdArg, elementId: elementIdArg, animation }),
1171
1442
  getSlide: (id) => value.slides.find((s) => s.id === id),
1172
1443
  getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1173
1444
  };
@@ -1235,6 +1506,23 @@ function reduce(deck, op) {
1235
1506
  (s) => s.id === op.slideId ? { ...s, elements: s.elements.map((e) => e.id === op.elementId ? { ...e, w: op.w, h: op.h } : e) } : s
1236
1507
  )
1237
1508
  };
1509
+ case "element_set_animation":
1510
+ return {
1511
+ ...deck,
1512
+ slides: deck.slides.map(
1513
+ (s) => s.id === op.slideId ? {
1514
+ ...s,
1515
+ elements: s.elements.map((e) => {
1516
+ if (e.id !== op.elementId) return e;
1517
+ if (op.animation === void 0) {
1518
+ const { animation: _drop, ...rest } = e;
1519
+ return rest;
1520
+ }
1521
+ return { ...e, animation: op.animation };
1522
+ })
1523
+ } : s
1524
+ )
1525
+ };
1238
1526
  }
1239
1527
  }
1240
1528
 
@@ -1321,6 +1609,113 @@ function chartStarterOption(kind) {
1321
1609
  };
1322
1610
  }
1323
1611
  }
1612
+ var CHART_PALETTE = [
1613
+ "#8b5cf6",
1614
+ "#3b82f6",
1615
+ "#10b981",
1616
+ "#f59e0b",
1617
+ "#ef4444",
1618
+ "#ec4899",
1619
+ "#14b8a6",
1620
+ "#6366f1"
1621
+ ];
1622
+ function chartColorAt(index) {
1623
+ return CHART_PALETTE[index % CHART_PALETTE.length];
1624
+ }
1625
+ function isPlainObject(v) {
1626
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1627
+ }
1628
+ function toNumber(v) {
1629
+ const n = typeof v === "number" ? v : parseFloat(String(v));
1630
+ return Number.isFinite(n) ? n : 0;
1631
+ }
1632
+ function chartModelFromOption(option) {
1633
+ if (!isPlainObject(option)) return null;
1634
+ const seriesRaw = option.series;
1635
+ if (!Array.isArray(seriesRaw) || seriesRaw.length === 0) return null;
1636
+ if (!seriesRaw.every(isPlainObject)) return null;
1637
+ const types = seriesRaw.map((s) => String(s.type ?? ""));
1638
+ if (types[0] === "pie") {
1639
+ if (seriesRaw.length !== 1) return null;
1640
+ const data = seriesRaw[0].data;
1641
+ if (!Array.isArray(data)) return null;
1642
+ const slices = [];
1643
+ for (const d of data) {
1644
+ if (!isPlainObject(d)) return null;
1645
+ slices.push({ name: String(d.name ?? ""), value: toNumber(d.value) });
1646
+ }
1647
+ return { kind: "pie", categories: [], series: [], slices };
1648
+ }
1649
+ const cartesian = /* @__PURE__ */ new Set(["bar", "line", "scatter"]);
1650
+ if (!types.every((t) => cartesian.has(t))) return null;
1651
+ if (new Set(types).size !== 1) return null;
1652
+ const baseType = types[0];
1653
+ const isArea = baseType === "line" && seriesRaw.every((s) => isPlainObject(s.areaStyle) || s.areaStyle != null);
1654
+ const kind = baseType === "line" ? isArea ? "area" : "line" : baseType;
1655
+ const xAxis = option.xAxis;
1656
+ const axisData = isPlainObject(xAxis) ? xAxis.data : void 0;
1657
+ const categories = Array.isArray(axisData) ? axisData.map((c) => String(c)) : [];
1658
+ const firstData = seriesRaw[0].data;
1659
+ const valueCount = Array.isArray(firstData) ? firstData.length : 0;
1660
+ const cats = categories.length > 0 ? categories : Array.from({ length: valueCount }, (_, i) => String(i + 1));
1661
+ const series = [];
1662
+ for (const s of seriesRaw) {
1663
+ const data = s.data;
1664
+ if (!Array.isArray(data)) return null;
1665
+ const values = data.map((d) => {
1666
+ if (Array.isArray(d)) return toNumber(d[1]);
1667
+ if (isPlainObject(d)) return toNumber(d.value);
1668
+ return toNumber(d);
1669
+ });
1670
+ series.push({ name: String(s.name ?? "Series"), color: typeof s.itemStyle === "object" && s.itemStyle && isPlainObject(s.itemStyle) ? typeof s.itemStyle.color === "string" ? s.itemStyle.color : void 0 : typeof s.color === "string" ? s.color : void 0, values });
1671
+ }
1672
+ return { kind, categories: cats, series, slices: [] };
1673
+ }
1674
+ function chartOptionFromModel(model) {
1675
+ if (model.kind === "pie") {
1676
+ return {
1677
+ tooltip: { trigger: "item" },
1678
+ legend: { bottom: 0 },
1679
+ color: model.slices.map((_, i) => chartColorAt(i)),
1680
+ series: [
1681
+ {
1682
+ type: "pie",
1683
+ radius: ["40%", "70%"],
1684
+ name: "Segment",
1685
+ data: model.slices.map((s) => ({ name: s.name, value: s.value }))
1686
+ }
1687
+ ]
1688
+ };
1689
+ }
1690
+ const isScatter = model.kind === "scatter";
1691
+ const isArea = model.kind === "area";
1692
+ const seriesType = model.kind === "bar" ? "bar" : model.kind === "scatter" ? "scatter" : "line";
1693
+ const series = model.series.map((s, i) => {
1694
+ const color = s.color ?? chartColorAt(i);
1695
+ const base = {
1696
+ type: seriesType,
1697
+ name: s.name,
1698
+ itemStyle: { color }
1699
+ };
1700
+ if (isScatter) {
1701
+ base.symbolSize = 12;
1702
+ base.data = s.values.map((v, idx) => [idx, v]);
1703
+ } else {
1704
+ base.data = s.values;
1705
+ }
1706
+ if (seriesType === "line") base.smooth = true;
1707
+ if (isArea) base.areaStyle = { color };
1708
+ return base;
1709
+ });
1710
+ return {
1711
+ grid: { top: 24, left: 56, right: 16, bottom: isScatter ? 32 : 40 },
1712
+ tooltip: { trigger: isScatter ? "item" : "axis" },
1713
+ legend: model.series.length > 1 ? { bottom: 0 } : void 0,
1714
+ xAxis: isScatter ? { type: "value" } : { type: "category", data: [...model.categories] },
1715
+ yAxis: { type: "value" },
1716
+ series
1717
+ };
1718
+ }
1324
1719
  function SlideRail({
1325
1720
  slides,
1326
1721
  selectedId,
@@ -1469,10 +1864,10 @@ function EditorToolbar({
1469
1864
  /* @__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" }) }) })
1470
1865
  ] });
1471
1866
  }
1472
- function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1867
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground, onSetAnimation, onSetElementAnimation }) {
1473
1868
  if (!element) {
1474
1869
  if (slide) {
1475
- return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1870
+ return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground, onSetElementAnimation });
1476
1871
  }
1477
1872
  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: [
1478
1873
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
@@ -1497,10 +1892,12 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1497
1892
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Tabs.List, { children: [
1498
1893
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "style", children: "Style" }),
1499
1894
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "layout", children: "Layout" }),
1895
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "build", children: "Build" }),
1500
1896
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "advanced", children: "Advanced" })
1501
1897
  ] }),
1502
1898
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Tabs.Panels, { children: [
1503
1899
  /* @__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 }) }) }),
1900
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Panel, { value: "build", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsx(AnimateSection, { animation: element.animation, onSetAnimation }) }) }),
1504
1901
  /* @__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 }) }) }),
1505
1902
  /* @__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 }) }) })
1506
1903
  ] })
@@ -1510,7 +1907,8 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1510
1907
  function SlideSettings({
1511
1908
  slide,
1512
1909
  onSetTransition,
1513
- onSetBackground
1910
+ onSetBackground,
1911
+ onSetElementAnimation
1514
1912
  }) {
1515
1913
  const transition = slide.transition;
1516
1914
  const kind = transition?.kind ?? "none";
@@ -1577,10 +1975,54 @@ function SlideSettings({
1577
1975
  onChange: (c) => onSetBackground({ ...slide.background, color: c })
1578
1976
  }
1579
1977
  ) })
1580
- ] }) })
1978
+ ] }) }),
1979
+ onSetElementAnimation && /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsx(BuildOrderList, { slide, onSetElementAnimation }) })
1581
1980
  ] })
1582
1981
  ] });
1583
1982
  }
1983
+ function BuildOrderList({
1984
+ slide,
1985
+ onSetElementAnimation
1986
+ }) {
1987
+ const builds = collectBuilds(slide);
1988
+ const move = (from, to) => {
1989
+ if (to < 0 || to >= builds.length) return;
1990
+ const reordered = [...builds];
1991
+ const [item] = reordered.splice(from, 1);
1992
+ reordered.splice(to, 0, item);
1993
+ reordered.forEach((b, i) => {
1994
+ if ((b.animation.order ?? 0) !== i) {
1995
+ onSetElementAnimation(b.element.id, { ...b.animation, order: i });
1996
+ }
1997
+ });
1998
+ };
1999
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2000
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Build order" }),
2001
+ builds.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: "No animated elements yet. Select an element and add a build under its Build tab." }) : builds.map((b, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2002
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400 w-5", children: [
2003
+ i + 1,
2004
+ "."
2005
+ ] }),
2006
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [
2007
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "truncate", children: buildLabel(b.element) }),
2008
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
2009
+ b.animation.effect,
2010
+ " \xB7 ",
2011
+ b.animation.trigger ?? "on-click"
2012
+ ] })
2013
+ ] }),
2014
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "chevron-up", onClick: () => move(i, i - 1), disabled: i === 0, "aria-label": "Move earlier" }),
2015
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "chevron-down", onClick: () => move(i, i + 1), disabled: i === builds.length - 1, "aria-label": "Move later" })
2016
+ ] }, b.element.id))
2017
+ ] });
2018
+ }
2019
+ function buildLabel(element) {
2020
+ if (element.type === "text") {
2021
+ const text = element.content.replace(/\s+/g, " ").trim();
2022
+ return text ? text.length > 28 ? `${text.slice(0, 28)}\u2026` : text : "Text";
2023
+ }
2024
+ return `${element.type} #${element.id.slice(-6)}`;
2025
+ }
1584
2026
  function LayoutSection({ element, onPatch }) {
1585
2027
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1586
2028
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1602,6 +2044,100 @@ function AdvancedSection({ element, onPatch }) {
1602
2044
  /* @__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" }) })
1603
2045
  ] });
1604
2046
  }
2047
+ var NO_ANIMATION = "none";
2048
+ function AnimateSection({
2049
+ animation,
2050
+ onSetAnimation
2051
+ }) {
2052
+ if (!onSetAnimation) {
2053
+ return /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "!text-zinc-500", children: "Build animations aren't wired up in this editor." });
2054
+ }
2055
+ const effect = animation?.effect;
2056
+ const set = (next) => {
2057
+ const base = animation ?? { effect: "fade" };
2058
+ onSetAnimation({ ...base, ...next });
2059
+ };
2060
+ const showDirection = effect === "fly-in" || effect === "wipe";
2061
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2062
+ /* @__PURE__ */ jsxRuntime.jsx(
2063
+ reactFancy.Select,
2064
+ {
2065
+ label: "Effect",
2066
+ list: [
2067
+ { value: NO_ANIMATION, label: "None" },
2068
+ { value: "fade", label: "Fade" },
2069
+ { value: "fly-in", label: "Fly in" },
2070
+ { value: "zoom", label: "Zoom" },
2071
+ { value: "wipe", label: "Wipe" }
2072
+ ],
2073
+ value: effect ?? NO_ANIMATION,
2074
+ onValueChange: (v) => {
2075
+ if (v === NO_ANIMATION) onSetAnimation(void 0);
2076
+ else set({ effect: v });
2077
+ }
2078
+ }
2079
+ ),
2080
+ effect && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2081
+ /* @__PURE__ */ jsxRuntime.jsx(
2082
+ reactFancy.Select,
2083
+ {
2084
+ label: "Trigger",
2085
+ list: [
2086
+ { value: "on-click", label: "On click" },
2087
+ { value: "with-prev", label: "With previous" },
2088
+ { value: "after-prev", label: "After previous" }
2089
+ ],
2090
+ value: animation?.trigger ?? "on-click",
2091
+ onValueChange: (v) => set({ trigger: v })
2092
+ }
2093
+ ),
2094
+ showDirection && /* @__PURE__ */ jsxRuntime.jsx(
2095
+ reactFancy.Select,
2096
+ {
2097
+ label: "Direction",
2098
+ list: [
2099
+ { value: "left", label: "From left" },
2100
+ { value: "right", label: "From right" },
2101
+ { value: "up", label: "From bottom" },
2102
+ { value: "down", label: "From top" }
2103
+ ],
2104
+ value: animation?.direction ?? "left",
2105
+ onValueChange: (v) => set({ direction: v })
2106
+ }
2107
+ ),
2108
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
2109
+ /* @__PURE__ */ jsxRuntime.jsx(
2110
+ reactFancy.Input,
2111
+ {
2112
+ label: "Duration (ms)",
2113
+ type: "number",
2114
+ value: String(animation?.duration ?? 500),
2115
+ onChange: (e) => set({ duration: parseInt(e.target.value, 10) || 500 })
2116
+ }
2117
+ ),
2118
+ /* @__PURE__ */ jsxRuntime.jsx(
2119
+ reactFancy.Input,
2120
+ {
2121
+ label: "Delay (ms)",
2122
+ type: "number",
2123
+ value: String(animation?.delay ?? 0),
2124
+ onChange: (e) => set({ delay: parseInt(e.target.value, 10) || 0 })
2125
+ }
2126
+ )
2127
+ ] }),
2128
+ /* @__PURE__ */ jsxRuntime.jsx(
2129
+ reactFancy.Input,
2130
+ {
2131
+ label: "Order",
2132
+ type: "number",
2133
+ value: String(animation?.order ?? 0),
2134
+ onChange: (e) => set({ order: parseInt(e.target.value, 10) || 0 })
2135
+ }
2136
+ ),
2137
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: `Builds reveal in ascending order. "On click" starts a new step; "with previous" plays alongside the step's lead; "after previous" follows it. Honors prefers-reduced-motion.` })
2138
+ ] })
2139
+ ] });
2140
+ }
1605
2141
  function StyleSection({ element, onPatch }) {
1606
2142
  switch (element.type) {
1607
2143
  case "text":
@@ -1674,7 +2210,35 @@ function TextStyleControls({ element, onPatch }) {
1674
2210
  ] });
1675
2211
  }
1676
2212
  function ImageStyleControls({ element, onPatch }) {
2213
+ const fileRef = react.useRef(null);
2214
+ const crop = element.crop;
2215
+ const onFile = (file) => {
2216
+ if (!file) return;
2217
+ const reader = new FileReader();
2218
+ reader.onload = () => {
2219
+ if (typeof reader.result === "string") onPatch({ src: reader.result });
2220
+ };
2221
+ reader.readAsDataURL(file);
2222
+ };
2223
+ const setCrop = (next) => {
2224
+ const base = crop ?? { x: 0, y: 0, w: 1, h: 1 };
2225
+ onPatch({ crop: { ...base, ...next } });
2226
+ };
1677
2227
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2228
+ /* @__PURE__ */ jsxRuntime.jsx(
2229
+ "input",
2230
+ {
2231
+ ref: fileRef,
2232
+ type: "file",
2233
+ accept: "image/*",
2234
+ className: "hidden",
2235
+ onChange: (e) => {
2236
+ onFile(e.target.files?.[0]);
2237
+ e.target.value = "";
2238
+ }
2239
+ }
2240
+ ),
2241
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "sm", variant: "ghost", icon: "upload", onClick: () => fileRef.current?.click(), children: "Upload image" }),
1678
2242
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1679
2243
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1680
2244
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1690,7 +2254,17 @@ function ImageStyleControls({ element, onPatch }) {
1690
2254
  value: element.fit ?? "contain",
1691
2255
  onValueChange: (v) => onPatch({ fit: v })
1692
2256
  }
1693
- )
2257
+ ),
2258
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
2259
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
2260
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Crop" }),
2261
+ crop && /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", onClick: () => onPatch({ crop: void 0 }), children: "Clear crop" })
2262
+ ] }),
2263
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "X", value: crop?.x ?? 0, onValueChange: (v) => setCrop({ x: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
2264
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Y", value: crop?.y ?? 0, onValueChange: (v) => setCrop({ y: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
2265
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Width", value: crop?.w ?? 1, onValueChange: (v) => setCrop({ w: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
2266
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Height", value: crop?.h ?? 1, onValueChange: (v) => setCrop({ h: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
2267
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: "Crop is a window into the source image (0..1). Width/height shrink the visible region; X/Y pan it." })
1694
2268
  ] });
1695
2269
  }
1696
2270
  function ShapeStyleControls({ element, onPatch }) {
@@ -1737,54 +2311,254 @@ function CodeStyleControls({ element, onPatch }) {
1737
2311
  ] });
1738
2312
  }
1739
2313
  function ChartStyleControls({ element, onPatch }) {
2314
+ const model = chartModelFromOption(element.option);
2315
+ const writeModel = (m) => onPatch({ option: chartOptionFromModel(m) });
2316
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2317
+ model ? /* @__PURE__ */ jsxRuntime.jsx(ChartModelEditor, { model, onChange: writeModel }) : /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "rounded-md bg-amber-50 p-2 !text-amber-700 dark:bg-amber-950/40 dark:!text-amber-400", children: "This chart's option is too custom for the visual editor. Edit it as JSON below." }),
2318
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2319
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: "Advanced \u2014 edit option JSON" }),
2320
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 pt-0", children: /* @__PURE__ */ jsxRuntime.jsx(
2321
+ reactFancy.Textarea,
2322
+ {
2323
+ label: "ECharts option (JSON)",
2324
+ value: JSON.stringify(element.option, null, 2),
2325
+ onValueChange: (v) => {
2326
+ try {
2327
+ onPatch({ option: JSON.parse(v) });
2328
+ } catch {
2329
+ }
2330
+ },
2331
+ rows: 10
2332
+ }
2333
+ ) })
2334
+ ] })
2335
+ ] });
2336
+ }
2337
+ var CHART_TYPE_OPTIONS = [
2338
+ { value: "bar", label: "Bar" },
2339
+ { value: "line", label: "Line" },
2340
+ { value: "area", label: "Area" },
2341
+ { value: "pie", label: "Pie" },
2342
+ { value: "scatter", label: "Scatter" }
2343
+ ];
2344
+ function ChartModelEditor({ model, onChange }) {
2345
+ const setKind = (kind) => {
2346
+ if (kind === model.kind) return;
2347
+ if (kind === "pie") {
2348
+ const first = model.series[0];
2349
+ const slices = model.slices.length ? model.slices : model.categories.length ? model.categories.map((name, i) => ({ name, value: first?.values[i] ?? 0 })) : [{ name: "Slice 1", value: 1 }];
2350
+ onChange({ ...model, kind, slices });
2351
+ return;
2352
+ }
2353
+ if (model.kind === "pie") {
2354
+ const categories = model.slices.length ? model.slices.map((s) => s.name) : ["A", "B", "C"];
2355
+ const values = model.slices.length ? model.slices.map((s) => s.value) : [1, 2, 3];
2356
+ onChange({ ...model, kind, categories, series: [{ name: "Series 1", color: chartColorAt(0), values }] });
2357
+ return;
2358
+ }
2359
+ onChange({ ...model, kind });
2360
+ };
1740
2361
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1741
- /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "!text-zinc-500", children: "Chart option is JSON \u2014 paste any ECharts option here." }),
1742
2362
  /* @__PURE__ */ jsxRuntime.jsx(
1743
- reactFancy.Textarea,
2363
+ reactFancy.Select,
1744
2364
  {
1745
- label: "ECharts option (JSON)",
1746
- value: JSON.stringify(element.option, null, 2),
1747
- onValueChange: (v) => {
1748
- try {
1749
- onPatch({ option: JSON.parse(v) });
1750
- } catch {
1751
- }
1752
- },
1753
- rows: 10
2365
+ label: "Chart type",
2366
+ list: CHART_TYPE_OPTIONS,
2367
+ value: model.kind,
2368
+ onValueChange: (v) => setKind(v)
1754
2369
  }
1755
- )
2370
+ ),
2371
+ model.kind === "pie" ? /* @__PURE__ */ jsxRuntime.jsx(PieSliceEditor, { model, onChange }) : /* @__PURE__ */ jsxRuntime.jsx(CartesianChartEditor, { model, onChange })
2372
+ ] });
2373
+ }
2374
+ function PieSliceEditor({ model, onChange }) {
2375
+ const slices = model.slices;
2376
+ const update = (i, next) => {
2377
+ const copy = slices.map((s, idx) => idx === i ? { ...s, ...next } : s);
2378
+ onChange({ ...model, slices: copy });
2379
+ };
2380
+ const remove = (i) => onChange({ ...model, slices: slices.filter((_, idx) => idx !== i) });
2381
+ const add = () => onChange({ ...model, slices: [...slices, { name: `Slice ${slices.length + 1}`, value: 0 }] });
2382
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2383
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Slices" }),
2384
+ slices.map((s, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
2385
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: i === 0 ? "Name" : void 0, value: s.name, onChange: (e) => update(i, { name: e.target.value }) }) }),
2386
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: i === 0 ? "Value" : void 0, type: "number", value: String(s.value), onChange: (e) => update(i, { value: parseFloat(e.target.value) || 0 }) }) }),
2387
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => remove(i), "aria-label": "Remove slice" })
2388
+ ] }, i)),
2389
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: add, children: "Add slice" })
2390
+ ] });
2391
+ }
2392
+ function CartesianChartEditor({ model, onChange }) {
2393
+ const { categories, series } = model;
2394
+ const updateCategory = (i, label) => {
2395
+ onChange({ ...model, categories: categories.map((c, idx) => idx === i ? label : c) });
2396
+ };
2397
+ const removeCategory = (i) => {
2398
+ onChange({
2399
+ ...model,
2400
+ categories: categories.filter((_, idx) => idx !== i),
2401
+ series: series.map((s) => ({ ...s, values: s.values.filter((_, idx) => idx !== i) }))
2402
+ });
2403
+ };
2404
+ const addCategory = () => {
2405
+ onChange({
2406
+ ...model,
2407
+ categories: [...categories, `Cat ${categories.length + 1}`],
2408
+ series: series.map((s) => ({ ...s, values: [...s.values, 0] }))
2409
+ });
2410
+ };
2411
+ const updateSeries = (si, next) => {
2412
+ onChange({ ...model, series: series.map((s, idx) => idx === si ? { ...s, ...next } : s) });
2413
+ };
2414
+ const updateValue = (si, ci, value) => {
2415
+ onChange({
2416
+ ...model,
2417
+ series: series.map(
2418
+ (s, idx) => idx === si ? { ...s, values: s.values.map((v, vi) => vi === ci ? value : v) } : s
2419
+ )
2420
+ });
2421
+ };
2422
+ const removeSeries = (si) => onChange({ ...model, series: series.filter((_, idx) => idx !== si) });
2423
+ const addSeries = () => onChange({
2424
+ ...model,
2425
+ series: [
2426
+ ...series,
2427
+ { name: `Series ${series.length + 1}`, color: chartColorAt(series.length), values: categories.map(() => 0) }
2428
+ ]
2429
+ });
2430
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2431
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2432
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Categories" }),
2433
+ categories.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2434
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { value: c, onChange: (e) => updateCategory(i, e.target.value) }) }),
2435
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeCategory(i), "aria-label": "Remove category" })
2436
+ ] }, i)),
2437
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addCategory, children: "Add category" })
2438
+ ] }),
2439
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
2440
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2441
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Series" }),
2442
+ series.map((s, si) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 rounded-md border border-zinc-200 p-2 dark:border-zinc-800", children: [
2443
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
2444
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: "Name", value: s.name, onChange: (e) => updateSeries(si, { name: e.target.value }) }) }),
2445
+ /* @__PURE__ */ jsxRuntime.jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ColorPicker, { value: s.color ?? chartColorAt(si), onChange: (c) => updateSeries(si, { color: c }) }) }),
2446
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeSeries(si), "aria-label": "Remove series" })
2447
+ ] }),
2448
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: categories.map((c, ci) => /* @__PURE__ */ jsxRuntime.jsx(
2449
+ reactFancy.Input,
2450
+ {
2451
+ label: c,
2452
+ type: "number",
2453
+ value: String(s.values[ci] ?? 0),
2454
+ onChange: (e) => updateValue(si, ci, parseFloat(e.target.value) || 0)
2455
+ },
2456
+ ci
2457
+ )) })
2458
+ ] }, si)),
2459
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addSeries, children: "Add series" })
2460
+ ] })
1756
2461
  ] });
1757
2462
  }
1758
2463
  function TableStyleControls({ element, onPatch }) {
2464
+ const columns = element.columns;
2465
+ const rows = element.rows;
2466
+ const nextColKey = () => {
2467
+ const existing = new Set(columns.map((c) => c.key));
2468
+ let n = columns.length + 1;
2469
+ while (existing.has(`col${n}`)) n++;
2470
+ return `col${n}`;
2471
+ };
2472
+ const setColumnLabel = (i, label) => {
2473
+ onPatch({ columns: columns.map((c, idx) => idx === i ? { ...c, label } : c) });
2474
+ };
2475
+ const removeColumn = (i) => {
2476
+ const key = columns[i]?.key;
2477
+ const nextCols = columns.filter((_, idx) => idx !== i);
2478
+ const nextRows = key ? rows.map((r) => {
2479
+ const { [key]: _drop, ...rest } = r;
2480
+ return rest;
2481
+ }) : rows;
2482
+ onPatch({ columns: nextCols, rows: nextRows });
2483
+ };
2484
+ const addColumn = () => {
2485
+ const key = nextColKey();
2486
+ onPatch({
2487
+ columns: [...columns, { key, label: `Column ${columns.length + 1}` }],
2488
+ rows: rows.map((r) => ({ ...r, [key]: "" }))
2489
+ });
2490
+ };
2491
+ const setCell = (rowIdx, key, value) => {
2492
+ onPatch({ rows: rows.map((r, idx) => idx === rowIdx ? { ...r, [key]: value } : r) });
2493
+ };
2494
+ const removeRow = (rowIdx) => onPatch({ rows: rows.filter((_, idx) => idx !== rowIdx) });
2495
+ const addRow = () => {
2496
+ const blank = {};
2497
+ for (const c of columns) blank[c.key] = "";
2498
+ onPatch({ rows: [...rows, blank] });
2499
+ };
1759
2500
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1760
- /* @__PURE__ */ jsxRuntime.jsx(
1761
- reactFancy.Textarea,
1762
- {
1763
- label: "Columns (JSON)",
1764
- value: JSON.stringify(element.columns, null, 2),
1765
- onValueChange: (v) => {
1766
- try {
1767
- onPatch({ columns: JSON.parse(v) });
1768
- } catch {
2501
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2502
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Columns" }),
2503
+ columns.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2504
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { value: c.label, onChange: (e) => setColumnLabel(i, e.target.value), "aria-label": `Column ${i + 1} label` }) }),
2505
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400", children: c.key }),
2506
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeColumn(i), "aria-label": "Remove column" })
2507
+ ] }, c.key)),
2508
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addColumn, children: "Add column" })
2509
+ ] }),
2510
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
2511
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2512
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Rows" }),
2513
+ columns.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: "Add a column to start adding rows." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2514
+ rows.map((r, rowIdx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 border-b border-zinc-100 pb-2 dark:border-zinc-800", children: [
2515
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid flex-1 grid-cols-1 gap-1", children: columns.map((c) => /* @__PURE__ */ jsxRuntime.jsx(
2516
+ reactFancy.Input,
2517
+ {
2518
+ label: c.label,
2519
+ value: r[c.key] == null ? "" : String(r[c.key]),
2520
+ onChange: (e) => setCell(rowIdx, c.key, e.target.value)
2521
+ },
2522
+ c.key
2523
+ )) }),
2524
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeRow(rowIdx), "aria-label": "Remove row" })
2525
+ ] }, rowIdx)),
2526
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addRow, children: "Add row" })
2527
+ ] })
2528
+ ] }),
2529
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2530
+ /* @__PURE__ */ jsxRuntime.jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: "Edit as JSON" }),
2531
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 p-2 pt-0", children: [
2532
+ /* @__PURE__ */ jsxRuntime.jsx(
2533
+ reactFancy.Textarea,
2534
+ {
2535
+ label: "Columns (JSON)",
2536
+ value: JSON.stringify(columns, null, 2),
2537
+ onValueChange: (v) => {
2538
+ try {
2539
+ onPatch({ columns: JSON.parse(v) });
2540
+ } catch {
2541
+ }
2542
+ },
2543
+ rows: 5
1769
2544
  }
1770
- },
1771
- rows: 5
1772
- }
1773
- ),
1774
- /* @__PURE__ */ jsxRuntime.jsx(
1775
- reactFancy.Textarea,
1776
- {
1777
- label: "Rows (JSON)",
1778
- value: JSON.stringify(element.rows, null, 2),
1779
- onValueChange: (v) => {
1780
- try {
1781
- onPatch({ rows: JSON.parse(v) });
1782
- } catch {
2545
+ ),
2546
+ /* @__PURE__ */ jsxRuntime.jsx(
2547
+ reactFancy.Textarea,
2548
+ {
2549
+ label: "Rows (JSON)",
2550
+ value: JSON.stringify(rows, null, 2),
2551
+ onValueChange: (v) => {
2552
+ try {
2553
+ onPatch({ rows: JSON.parse(v) });
2554
+ } catch {
2555
+ }
2556
+ },
2557
+ rows: 8
1783
2558
  }
1784
- },
1785
- rows: 8
1786
- }
1787
- )
2559
+ )
2560
+ ] })
2561
+ ] })
1788
2562
  ] });
1789
2563
  }
1790
2564
  function EmbedStyleControls({ element, onPatch }) {
@@ -2039,7 +2813,9 @@ function DeckEditor({
2039
2813
  },
2040
2814
  onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2041
2815
  onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2042
- onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
2816
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background),
2817
+ onSetAnimation: (animation) => slide && elementIdSelected && ops.setAnimation(slide.id, elementIdSelected, animation),
2818
+ onSetElementAnimation: (eid, animation) => slide && ops.setAnimation(slide.id, eid, animation)
2043
2819
  }
2044
2820
  ) })
2045
2821
  ] }),
@@ -2061,8 +2837,11 @@ exports.SlideThumbnail = SlideThumbnail;
2061
2837
  exports.SlideViewer = SlideViewer;
2062
2838
  exports.SpeakerNotes = SpeakerNotes;
2063
2839
  exports.TextElementRenderer = TextElementRenderer;
2840
+ exports.buildSteps = buildSteps;
2841
+ exports.buildsForStep = buildsForStep;
2064
2842
  exports.builtinThemes = builtinThemes;
2065
2843
  exports.chartStarterOption = chartStarterOption;
2844
+ exports.collectBuilds = collectBuilds;
2066
2845
  exports.darkTheme = darkTheme;
2067
2846
  exports.deckId = deckId;
2068
2847
  exports.defaultTheme = defaultTheme;
@@ -2072,11 +2851,14 @@ exports.nextId = nextId;
2072
2851
  exports.reduceDeck = reduce;
2073
2852
  exports.resolveTheme = resolveTheme;
2074
2853
  exports.slideId = slideId;
2854
+ exports.stepDelays = stepDelays;
2855
+ exports.totalBuildSteps = totalBuildSteps;
2075
2856
  exports.useDeckState = useDeckState;
2076
2857
  exports.useIsDarkSlide = useIsDarkSlide;
2077
2858
  exports.useSlideContext = useSlideContext;
2078
2859
  exports.useSlideKeyboard = useSlideKeyboard;
2079
2860
  exports.useSlideTheme = useSlideTheme;
2861
+ exports.visibleElementIds = visibleElementIds;
2080
2862
  exports.vividTheme = vividTheme;
2081
2863
  //# sourceMappingURL=index.cjs.map
2082
2864
  //# sourceMappingURL=index.cjs.map