@particle-academy/fancy-slides 0.3.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.js CHANGED
@@ -70,6 +70,79 @@ function resolveTheme(theme) {
70
70
  function cn(...parts) {
71
71
  return parts.filter(Boolean).join(" ");
72
72
  }
73
+
74
+ // src/utils/builds.ts
75
+ var DEFAULT_BUILD_DURATION = 500;
76
+ function collectBuilds(slide) {
77
+ if (!slide) return [];
78
+ const builds = [];
79
+ slide.elements.forEach((element, index) => {
80
+ if (element.animation) {
81
+ builds.push({ element, animation: element.animation, index });
82
+ }
83
+ });
84
+ return builds.sort((a, b) => {
85
+ const ao = a.animation.order ?? 0;
86
+ const bo = b.animation.order ?? 0;
87
+ if (ao !== bo) return ao - bo;
88
+ return a.index - b.index;
89
+ });
90
+ }
91
+ function buildSteps(slide) {
92
+ const builds = collectBuilds(slide);
93
+ const steps = [];
94
+ for (const build of builds) {
95
+ const trigger = build.animation.trigger ?? "on-click";
96
+ if (steps.length === 0 || trigger === "on-click") {
97
+ steps.push({ builds: [build] });
98
+ } else {
99
+ steps[steps.length - 1].builds.push(build);
100
+ }
101
+ }
102
+ return steps;
103
+ }
104
+ function totalBuildSteps(slide) {
105
+ return buildSteps(slide).length;
106
+ }
107
+ function visibleElementIds(slide, buildStep) {
108
+ const visible = /* @__PURE__ */ new Set();
109
+ if (!slide) return visible;
110
+ const steps = buildSteps(slide);
111
+ const stepOfElement = /* @__PURE__ */ new Map();
112
+ steps.forEach((step, i) => {
113
+ for (const b of step.builds) stepOfElement.set(b.element.id, i + 1);
114
+ });
115
+ for (const element of slide.elements) {
116
+ const revealStep = stepOfElement.get(element.id);
117
+ if (revealStep === void 0) {
118
+ visible.add(element.id);
119
+ } else if (buildStep >= revealStep) {
120
+ visible.add(element.id);
121
+ }
122
+ }
123
+ return visible;
124
+ }
125
+ function buildsForStep(slide, buildStep) {
126
+ const steps = buildSteps(slide);
127
+ const step = steps[buildStep - 1];
128
+ return step ? step.builds : [];
129
+ }
130
+ function stepDelays(builds) {
131
+ const delays = /* @__PURE__ */ new Map();
132
+ const lead = builds[0];
133
+ if (!lead) return delays;
134
+ const leadDelay = lead.animation.delay ?? 0;
135
+ const leadDuration = lead.animation.duration ?? DEFAULT_BUILD_DURATION;
136
+ delays.set(lead.element.id, leadDelay);
137
+ for (let i = 1; i < builds.length; i++) {
138
+ const b = builds[i];
139
+ const own = b.animation.delay ?? 0;
140
+ const trigger = b.animation.trigger ?? "on-click";
141
+ const base = trigger === "after-prev" ? leadDelay + leadDuration : leadDelay;
142
+ delays.set(b.element.id, base + own);
143
+ }
144
+ return delays;
145
+ }
73
146
  function TextElementRenderer({
74
147
  element,
75
148
  theme,
@@ -238,12 +311,94 @@ function renderShape(el, s) {
238
311
  return null;
239
312
  }
240
313
  }
314
+
315
+ // src/components/Slide/builds-style.ts
316
+ var DEFAULT_BUILD_DURATION2 = 500;
317
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
318
+ function buildEnterStyle(animation, effectiveDelay) {
319
+ const duration = animation.duration ?? DEFAULT_BUILD_DURATION2;
320
+ const dir = animation.direction ?? "left";
321
+ let name;
322
+ switch (animation.effect) {
323
+ case "fade":
324
+ name = "fs-build-fade";
325
+ break;
326
+ case "zoom":
327
+ name = "fs-build-zoom";
328
+ break;
329
+ case "fly-in":
330
+ name = `fs-build-fly-${dir}`;
331
+ break;
332
+ case "wipe":
333
+ name = `fs-build-wipe-${dir}`;
334
+ break;
335
+ default:
336
+ name = "fs-build-fade";
337
+ }
338
+ return {
339
+ animationName: name,
340
+ animationDuration: `${duration}ms`,
341
+ animationDelay: `${effectiveDelay}ms`,
342
+ animationTimingFunction: EASE,
343
+ animationFillMode: "both"
344
+ };
345
+ }
346
+ var BUILD_KEYFRAMES = `
347
+ @media (prefers-reduced-motion: reduce) {
348
+ .fs-build-enter { animation: none !important; }
349
+ }
350
+ @media (prefers-reduced-motion: no-preference) {
351
+ @keyframes fs-build-fade {
352
+ from { opacity: 0; }
353
+ to { opacity: 1; }
354
+ }
355
+ @keyframes fs-build-zoom {
356
+ from { opacity: 0; transform: scale(0.8); }
357
+ to { opacity: 1; transform: scale(1); }
358
+ }
359
+ @keyframes fs-build-fly-left {
360
+ from { opacity: 0; transform: translateX(-24%); }
361
+ to { opacity: 1; transform: translateX(0); }
362
+ }
363
+ @keyframes fs-build-fly-right {
364
+ from { opacity: 0; transform: translateX(24%); }
365
+ to { opacity: 1; transform: translateX(0); }
366
+ }
367
+ @keyframes fs-build-fly-up {
368
+ from { opacity: 0; transform: translateY(24%); }
369
+ to { opacity: 1; transform: translateY(0); }
370
+ }
371
+ @keyframes fs-build-fly-down {
372
+ from { opacity: 0; transform: translateY(-24%); }
373
+ to { opacity: 1; transform: translateY(0); }
374
+ }
375
+ /* wipe: clip-path inset reveals from the named edge toward the opposite one.
376
+ inset(top right bottom left) \u2014 start fully clipped on the far side. */
377
+ @keyframes fs-build-wipe-left {
378
+ from { clip-path: inset(0 100% 0 0); }
379
+ to { clip-path: inset(0 0 0 0); }
380
+ }
381
+ @keyframes fs-build-wipe-right {
382
+ from { clip-path: inset(0 0 0 100%); }
383
+ to { clip-path: inset(0 0 0 0); }
384
+ }
385
+ @keyframes fs-build-wipe-up {
386
+ from { clip-path: inset(100% 0 0 0); }
387
+ to { clip-path: inset(0 0 0 0); }
388
+ }
389
+ @keyframes fs-build-wipe-down {
390
+ from { clip-path: inset(0 0 100% 0); }
391
+ to { clip-path: inset(0 0 0 0); }
392
+ }
393
+ }
394
+ `;
241
395
  function Slide({
242
396
  slide,
243
397
  theme,
244
398
  width,
245
399
  aspectRatio,
246
400
  editing = false,
401
+ buildStep,
247
402
  onElementContentChange,
248
403
  onElementSelect,
249
404
  selectedElementId,
@@ -287,7 +442,21 @@ function Slide({
287
442
  }),
288
443
  [t, effectiveBg, slideWidthPx]
289
444
  );
290
- return /* @__PURE__ */ jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsx(
445
+ const buildInfo = useMemo(() => {
446
+ if (editing) return null;
447
+ const steps = buildSteps(slide);
448
+ if (steps.length === 0) return null;
449
+ const revealStep = /* @__PURE__ */ new Map();
450
+ steps.forEach((step, i) => {
451
+ for (const b of step.builds) revealStep.set(b.element.id, i + 1);
452
+ });
453
+ const driven = buildStep !== void 0;
454
+ const currentStep = driven ? buildStep : steps.length;
455
+ const firing = driven ? steps[currentStep - 1] : void 0;
456
+ const delays = firing ? stepDelays(firing.builds) : /* @__PURE__ */ new Map();
457
+ return { revealStep, currentStep, delays };
458
+ }, [editing, slide, buildStep]);
459
+ return /* @__PURE__ */ jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsxs(
291
460
  "div",
292
461
  {
293
462
  ref,
@@ -305,23 +474,45 @@ function Slide({
305
474
  onClick: (e) => {
306
475
  if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
307
476
  },
308
- children: orderedElements(slide.elements).map((element) => /* @__PURE__ */ jsx(
309
- SlideElementHost,
310
- {
311
- element,
312
- theme: t,
313
- slideWidthPx,
314
- slideHeightPx,
315
- editing,
316
- selected: selectedElementId === element.id,
317
- onContentChange: onElementContentChange,
318
- onSelect: onElementSelect,
319
- onMove: onElementMove,
320
- onResize: onElementResize,
321
- renderElement
322
- },
323
- element.id
324
- ))
477
+ children: [
478
+ buildInfo && /* @__PURE__ */ jsx("style", { children: BUILD_KEYFRAMES }),
479
+ orderedElements(slide.elements).map((element) => {
480
+ let buildHidden = false;
481
+ let buildAnimation;
482
+ let buildDelay = 0;
483
+ if (buildInfo) {
484
+ const step = buildInfo.revealStep.get(element.id);
485
+ if (step !== void 0) {
486
+ if (buildInfo.currentStep < step) {
487
+ buildHidden = true;
488
+ } else if (buildInfo.currentStep === step && element.animation) {
489
+ buildAnimation = element.animation;
490
+ buildDelay = buildInfo.delays.get(element.id) ?? 0;
491
+ }
492
+ }
493
+ }
494
+ if (buildHidden) return null;
495
+ return /* @__PURE__ */ jsx(
496
+ SlideElementHost,
497
+ {
498
+ element,
499
+ theme: t,
500
+ slideWidthPx,
501
+ slideHeightPx,
502
+ editing,
503
+ selected: selectedElementId === element.id,
504
+ onContentChange: onElementContentChange,
505
+ onSelect: onElementSelect,
506
+ onMove: onElementMove,
507
+ onResize: onElementResize,
508
+ renderElement,
509
+ buildAnimation,
510
+ buildDelay
511
+ },
512
+ element.id
513
+ );
514
+ })
515
+ ]
325
516
  }
326
517
  ) });
327
518
  }
@@ -338,7 +529,9 @@ function SlideElementHost({
338
529
  onSelect,
339
530
  onMove,
340
531
  onResize,
341
- renderElement
532
+ renderElement,
533
+ buildAnimation,
534
+ buildDelay = 0
342
535
  }) {
343
536
  const dragRef = useRef(null);
344
537
  if (element.hidden) return null;
@@ -407,15 +600,18 @@ function SlideElementHost({
407
600
  outline: selected ? "2px solid #8b5cf6" : void 0,
408
601
  outlineOffset: selected ? 2 : void 0,
409
602
  cursor: canMove ? "move" : interactive ? "pointer" : "default",
410
- touchAction: canMove ? "none" : void 0
603
+ touchAction: canMove ? "none" : void 0,
604
+ ...buildAnimation ? buildEnterStyle(buildAnimation, buildDelay) : null
411
605
  };
412
606
  const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
413
607
  return /* @__PURE__ */ jsxs(
414
608
  "div",
415
609
  {
610
+ className: buildAnimation ? "fs-build-enter" : void 0,
416
611
  style: box,
417
612
  "data-fancy-slides-element": element.id,
418
613
  "data-fancy-slides-element-type": element.type,
614
+ "data-fancy-slides-build": buildAnimation ? "" : void 0,
419
615
  onPointerDown: canMove ? startDrag("move") : void 0,
420
616
  onPointerMove: canMove ? onPointerMove : void 0,
421
617
  onPointerUp: canMove ? endDrag : void 0,
@@ -537,6 +733,8 @@ function useSlideKeyboard({
537
733
  total,
538
734
  index,
539
735
  goTo,
736
+ onAdvance,
737
+ onRetreat,
540
738
  onExit,
541
739
  onBlank,
542
740
  onFullscreen,
@@ -553,13 +751,15 @@ function useSlideKeyboard({
553
751
  case "ArrowLeft":
554
752
  case "PageUp":
555
753
  e.preventDefault();
556
- if (index > 0) goTo(index - 1);
754
+ if (onRetreat) onRetreat();
755
+ else if (index > 0) goTo(index - 1);
557
756
  return;
558
757
  case "ArrowRight":
559
758
  case "PageDown":
560
759
  case " ":
561
760
  e.preventDefault();
562
- if (index < total - 1) goTo(index + 1);
761
+ if (onAdvance) onAdvance();
762
+ else if (index < total - 1) goTo(index + 1);
563
763
  return;
564
764
  case "Home":
565
765
  e.preventDefault();
@@ -601,7 +801,7 @@ function useSlideKeyboard({
601
801
  };
602
802
  window.addEventListener("keydown", handler);
603
803
  return () => window.removeEventListener("keydown", handler);
604
- }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
804
+ }, [enabled, index, total, goTo, onAdvance, onRetreat, onExit, onBlank, onFullscreen]);
605
805
  }
606
806
  function SlideViewer({
607
807
  deck,
@@ -629,13 +829,34 @@ function SlideViewer({
629
829
  const containerRef = useRef(null);
630
830
  const prevIndexRef = useRef(index);
631
831
  const forward = index >= prevIndexRef.current;
832
+ const slide = deck.slides[index];
833
+ const totalSteps = totalBuildSteps(slide);
834
+ const [buildStep, setBuildStep] = useState(0);
835
+ const nextFreshRef = useRef(false);
632
836
  useEffect(() => {
837
+ if (index === prevIndexRef.current) return;
633
838
  prevIndexRef.current = index;
634
- }, [index]);
839
+ const fresh = nextFreshRef.current;
840
+ nextFreshRef.current = false;
841
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
842
+ }, [index, deck.slides]);
843
+ const advance = useCallback(() => {
844
+ if (buildStep < totalSteps) {
845
+ setBuildStep((s) => s + 1);
846
+ } else if (index < deck.slides.length - 1) {
847
+ nextFreshRef.current = true;
848
+ goTo(index + 1);
849
+ }
850
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
851
+ const retreat = useCallback(() => {
852
+ if (index > 0) goTo(index - 1);
853
+ }, [index, goTo]);
635
854
  useSlideKeyboard({
636
855
  total: deck.slides.length,
637
856
  index,
638
857
  goTo,
858
+ onAdvance: advance,
859
+ onRetreat: retreat,
639
860
  onExit,
640
861
  onBlank: () => setBlanked((b) => !b),
641
862
  onFullscreen: () => {
@@ -648,11 +869,15 @@ function SlideViewer({
648
869
  useEffect(() => {
649
870
  if (!autoAdvanceMs || deck.slides.length <= 1) return;
650
871
  const t = setTimeout(() => {
651
- goTo(index + 1 < deck.slides.length ? index + 1 : 0);
872
+ if (buildStep < totalSteps) {
873
+ setBuildStep((s) => s + 1);
874
+ } else {
875
+ nextFreshRef.current = true;
876
+ goTo(index + 1 < deck.slides.length ? index + 1 : 0);
877
+ }
652
878
  }, autoAdvanceMs);
653
879
  return () => clearTimeout(t);
654
- }, [autoAdvanceMs, index, deck.slides.length, goTo]);
655
- const slide = deck.slides[index];
880
+ }, [autoAdvanceMs, index, deck.slides.length, goTo, buildStep, totalSteps]);
656
881
  const theme = resolveTheme(deck.theme);
657
882
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
658
883
  const transition = slide?.transition ?? theme.defaultTransition;
@@ -674,6 +899,11 @@ function SlideViewer({
674
899
  },
675
900
  tabIndex: 0,
676
901
  "data-fancy-slides-viewer": deck.id,
902
+ "data-fancy-slides-build-step": buildStep,
903
+ onClick: () => {
904
+ if (blanked) return;
905
+ advance();
906
+ },
677
907
  children: [
678
908
  /* @__PURE__ */ jsx("style", { children: TRANSITION_KEYFRAMES }),
679
909
  !blanked && slide && /* @__PURE__ */ jsx(
@@ -687,7 +917,7 @@ function SlideViewer({
687
917
  ["--fs-ratio"]: aspectRatio.toString(),
688
918
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
689
919
  },
690
- children: /* @__PURE__ */ jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) }, index)
920
+ children: /* @__PURE__ */ jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsx(Slide, { slide, theme, buildStep, renderElement }) }, index)
691
921
  }
692
922
  ),
693
923
  !hideChrome && !blanked && /* @__PURE__ */ jsxs(
@@ -718,7 +948,7 @@ function SlideViewer({
718
948
  );
719
949
  }
720
950
  var DEFAULT_DURATION = 400;
721
- var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
951
+ var EASE2 = "cubic-bezier(0.16, 1, 0.3, 1)";
722
952
  function transitionEnterStyle(transition, forward) {
723
953
  const kind = transition?.kind ?? "none";
724
954
  if (kind === "none") return { width: "100%", height: "100%" };
@@ -744,7 +974,7 @@ function transitionEnterStyle(transition, forward) {
744
974
  height: "100%",
745
975
  animationName: name,
746
976
  animationDuration: `${duration}ms`,
747
- animationTimingFunction: EASE,
977
+ animationTimingFunction: EASE2,
748
978
  animationFillMode: "both"
749
979
  };
750
980
  }
@@ -800,14 +1030,38 @@ function PresenterView({
800
1030
  },
801
1031
  [deck.slides.length, isControlled, onIndexChange]
802
1032
  );
1033
+ const slide = deck.slides[index];
1034
+ const totalSteps = totalBuildSteps(slide);
1035
+ const [buildStep, setBuildStep] = useState(0);
1036
+ const prevIndexRef = useRef(index);
1037
+ const nextFreshRef = useRef(false);
1038
+ useEffect(() => {
1039
+ if (index === prevIndexRef.current) return;
1040
+ prevIndexRef.current = index;
1041
+ const fresh = nextFreshRef.current;
1042
+ nextFreshRef.current = false;
1043
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
1044
+ }, [index, deck.slides]);
1045
+ const advance = useCallback(() => {
1046
+ if (buildStep < totalSteps) {
1047
+ setBuildStep((s) => s + 1);
1048
+ } else if (index < deck.slides.length - 1) {
1049
+ nextFreshRef.current = true;
1050
+ goTo(index + 1);
1051
+ }
1052
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
1053
+ const retreat = useCallback(() => {
1054
+ if (index > 0) goTo(index - 1);
1055
+ }, [index, goTo]);
803
1056
  useSlideKeyboard({
804
1057
  total: deck.slides.length,
805
1058
  index,
806
1059
  goTo,
1060
+ onAdvance: advance,
1061
+ onRetreat: retreat,
807
1062
  onExit
808
1063
  });
809
1064
  const theme = resolveTheme(deck.theme);
810
- const slide = deck.slides[index];
811
1065
  const nextSlide = deck.slides[index + 1];
812
1066
  const [now, setNow] = useState(() => Date.now());
813
1067
  useEffect(() => {
@@ -854,7 +1108,7 @@ function PresenterView({
854
1108
  borderRadius: 8,
855
1109
  overflow: "hidden"
856
1110
  },
857
- children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) : null
1111
+ children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, buildStep, renderElement }) : null
858
1112
  }
859
1113
  )
860
1114
  }
@@ -958,8 +1212,8 @@ function PresenterView({
958
1212
  /* @__PURE__ */ jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
959
1213
  /* @__PURE__ */ jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
960
1214
  /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
961
- /* @__PURE__ */ jsx(NavButton, { onClick: () => goTo(index - 1), disabled: index === 0, children: "\u2190 Prev" }),
962
- /* @__PURE__ */ jsx(NavButton, { onClick: () => goTo(index + 1), disabled: index >= deck.slides.length - 1, children: "Next \u2192" })
1215
+ /* @__PURE__ */ jsx(NavButton, { onClick: retreat, disabled: index === 0, children: "\u2190 Prev" }),
1216
+ /* @__PURE__ */ jsx(NavButton, { onClick: advance, disabled: index >= deck.slides.length - 1 && buildStep >= totalSteps, children: "Next \u2192" })
963
1217
  ] })
964
1218
  ]
965
1219
  }
@@ -1142,6 +1396,7 @@ function useDeckState({ value, onChange, onOp }) {
1142
1396
  updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1143
1397
  moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1144
1398
  resizeElement: (slideIdArg, elementIdArg, w, h) => apply({ kind: "element_resize", slideId: slideIdArg, elementId: elementIdArg, w, h }),
1399
+ setAnimation: (slideIdArg, elementIdArg, animation) => apply({ kind: "element_set_animation", slideId: slideIdArg, elementId: elementIdArg, animation }),
1145
1400
  getSlide: (id) => value.slides.find((s) => s.id === id),
1146
1401
  getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1147
1402
  };
@@ -1209,6 +1464,23 @@ function reduce(deck, op) {
1209
1464
  (s) => s.id === op.slideId ? { ...s, elements: s.elements.map((e) => e.id === op.elementId ? { ...e, w: op.w, h: op.h } : e) } : s
1210
1465
  )
1211
1466
  };
1467
+ case "element_set_animation":
1468
+ return {
1469
+ ...deck,
1470
+ slides: deck.slides.map(
1471
+ (s) => s.id === op.slideId ? {
1472
+ ...s,
1473
+ elements: s.elements.map((e) => {
1474
+ if (e.id !== op.elementId) return e;
1475
+ if (op.animation === void 0) {
1476
+ const { animation: _drop, ...rest } = e;
1477
+ return rest;
1478
+ }
1479
+ return { ...e, animation: op.animation };
1480
+ })
1481
+ } : s
1482
+ )
1483
+ };
1212
1484
  }
1213
1485
  }
1214
1486
 
@@ -1550,10 +1822,10 @@ function EditorToolbar({
1550
1822
  /* @__PURE__ */ jsx("div", { className: "ml-auto flex items-center gap-2", children: /* @__PURE__ */ jsx(Tooltip, { content: "Present (F)", children: /* @__PURE__ */ jsx(Action, { color: "violet", size: "sm", icon: "play", onClick: onPresent, children: "Present" }) }) })
1551
1823
  ] });
1552
1824
  }
1553
- function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1825
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground, onSetAnimation, onSetElementAnimation }) {
1554
1826
  if (!element) {
1555
1827
  if (slide) {
1556
- return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1828
+ return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground, onSetElementAnimation });
1557
1829
  }
1558
1830
  return /* @__PURE__ */ jsxs("div", { className: "fs-inspector flex h-full flex-col border-l border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900", children: [
1559
1831
  /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
@@ -1578,10 +1850,12 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1578
1850
  /* @__PURE__ */ jsxs(Tabs.List, { children: [
1579
1851
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "style", children: "Style" }),
1580
1852
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "layout", children: "Layout" }),
1853
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "build", children: "Build" }),
1581
1854
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "advanced", children: "Advanced" })
1582
1855
  ] }),
1583
1856
  /* @__PURE__ */ jsxs(Tabs.Panels, { children: [
1584
1857
  /* @__PURE__ */ jsx(Tabs.Panel, { value: "style", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(StyleSection, { element, onPatch }) }) }),
1858
+ /* @__PURE__ */ jsx(Tabs.Panel, { value: "build", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(AnimateSection, { animation: element.animation, onSetAnimation }) }) }),
1585
1859
  /* @__PURE__ */ jsx(Tabs.Panel, { value: "layout", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(LayoutSection, { element, onPatch }) }) }),
1586
1860
  /* @__PURE__ */ jsx(Tabs.Panel, { value: "advanced", children: /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(AdvancedSection, { element, onPatch }) }) })
1587
1861
  ] })
@@ -1591,7 +1865,8 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1591
1865
  function SlideSettings({
1592
1866
  slide,
1593
1867
  onSetTransition,
1594
- onSetBackground
1868
+ onSetBackground,
1869
+ onSetElementAnimation
1595
1870
  }) {
1596
1871
  const transition = slide.transition;
1597
1872
  const kind = transition?.kind ?? "none";
@@ -1658,10 +1933,54 @@ function SlideSettings({
1658
1933
  onChange: (c) => onSetBackground({ ...slide.background, color: c })
1659
1934
  }
1660
1935
  ) })
1661
- ] }) })
1936
+ ] }) }),
1937
+ onSetElementAnimation && /* @__PURE__ */ jsx(Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(BuildOrderList, { slide, onSetElementAnimation }) })
1662
1938
  ] })
1663
1939
  ] });
1664
1940
  }
1941
+ function BuildOrderList({
1942
+ slide,
1943
+ onSetElementAnimation
1944
+ }) {
1945
+ const builds = collectBuilds(slide);
1946
+ const move = (from, to) => {
1947
+ if (to < 0 || to >= builds.length) return;
1948
+ const reordered = [...builds];
1949
+ const [item] = reordered.splice(from, 1);
1950
+ reordered.splice(to, 0, item);
1951
+ reordered.forEach((b, i) => {
1952
+ if ((b.animation.order ?? 0) !== i) {
1953
+ onSetElementAnimation(b.element.id, { ...b.animation, order: i });
1954
+ }
1955
+ });
1956
+ };
1957
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1958
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Build order" }),
1959
+ builds.length === 0 ? /* @__PURE__ */ jsx(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__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1960
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400 w-5", children: [
1961
+ i + 1,
1962
+ "."
1963
+ ] }),
1964
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
1965
+ /* @__PURE__ */ jsx(Text, { size: "sm", className: "truncate", children: buildLabel(b.element) }),
1966
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
1967
+ b.animation.effect,
1968
+ " \xB7 ",
1969
+ b.animation.trigger ?? "on-click"
1970
+ ] })
1971
+ ] }),
1972
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "chevron-up", onClick: () => move(i, i - 1), disabled: i === 0, "aria-label": "Move earlier" }),
1973
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "chevron-down", onClick: () => move(i, i + 1), disabled: i === builds.length - 1, "aria-label": "Move later" })
1974
+ ] }, b.element.id))
1975
+ ] });
1976
+ }
1977
+ function buildLabel(element) {
1978
+ if (element.type === "text") {
1979
+ const text = element.content.replace(/\s+/g, " ").trim();
1980
+ return text ? text.length > 28 ? `${text.slice(0, 28)}\u2026` : text : "Text";
1981
+ }
1982
+ return `${element.type} #${element.id.slice(-6)}`;
1983
+ }
1665
1984
  function LayoutSection({ element, onPatch }) {
1666
1985
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1667
1986
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1683,6 +2002,100 @@ function AdvancedSection({ element, onPatch }) {
1683
2002
  /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsx(Action, { size: "sm", variant: element.hidden ? "default" : "ghost", onClick: () => onPatch({ hidden: !element.hidden }), children: element.hidden ? "Hidden \u2014 show" : "Hide on slide" }) })
1684
2003
  ] });
1685
2004
  }
2005
+ var NO_ANIMATION = "none";
2006
+ function AnimateSection({
2007
+ animation,
2008
+ onSetAnimation
2009
+ }) {
2010
+ if (!onSetAnimation) {
2011
+ return /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "Build animations aren't wired up in this editor." });
2012
+ }
2013
+ const effect = animation?.effect;
2014
+ const set = (next) => {
2015
+ const base = animation ?? { effect: "fade" };
2016
+ onSetAnimation({ ...base, ...next });
2017
+ };
2018
+ const showDirection = effect === "fly-in" || effect === "wipe";
2019
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2020
+ /* @__PURE__ */ jsx(
2021
+ Select,
2022
+ {
2023
+ label: "Effect",
2024
+ list: [
2025
+ { value: NO_ANIMATION, label: "None" },
2026
+ { value: "fade", label: "Fade" },
2027
+ { value: "fly-in", label: "Fly in" },
2028
+ { value: "zoom", label: "Zoom" },
2029
+ { value: "wipe", label: "Wipe" }
2030
+ ],
2031
+ value: effect ?? NO_ANIMATION,
2032
+ onValueChange: (v) => {
2033
+ if (v === NO_ANIMATION) onSetAnimation(void 0);
2034
+ else set({ effect: v });
2035
+ }
2036
+ }
2037
+ ),
2038
+ effect && /* @__PURE__ */ jsxs(Fragment, { children: [
2039
+ /* @__PURE__ */ jsx(
2040
+ Select,
2041
+ {
2042
+ label: "Trigger",
2043
+ list: [
2044
+ { value: "on-click", label: "On click" },
2045
+ { value: "with-prev", label: "With previous" },
2046
+ { value: "after-prev", label: "After previous" }
2047
+ ],
2048
+ value: animation?.trigger ?? "on-click",
2049
+ onValueChange: (v) => set({ trigger: v })
2050
+ }
2051
+ ),
2052
+ showDirection && /* @__PURE__ */ jsx(
2053
+ Select,
2054
+ {
2055
+ label: "Direction",
2056
+ list: [
2057
+ { value: "left", label: "From left" },
2058
+ { value: "right", label: "From right" },
2059
+ { value: "up", label: "From bottom" },
2060
+ { value: "down", label: "From top" }
2061
+ ],
2062
+ value: animation?.direction ?? "left",
2063
+ onValueChange: (v) => set({ direction: v })
2064
+ }
2065
+ ),
2066
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
2067
+ /* @__PURE__ */ jsx(
2068
+ Input,
2069
+ {
2070
+ label: "Duration (ms)",
2071
+ type: "number",
2072
+ value: String(animation?.duration ?? 500),
2073
+ onChange: (e) => set({ duration: parseInt(e.target.value, 10) || 500 })
2074
+ }
2075
+ ),
2076
+ /* @__PURE__ */ jsx(
2077
+ Input,
2078
+ {
2079
+ label: "Delay (ms)",
2080
+ type: "number",
2081
+ value: String(animation?.delay ?? 0),
2082
+ onChange: (e) => set({ delay: parseInt(e.target.value, 10) || 0 })
2083
+ }
2084
+ )
2085
+ ] }),
2086
+ /* @__PURE__ */ jsx(
2087
+ Input,
2088
+ {
2089
+ label: "Order",
2090
+ type: "number",
2091
+ value: String(animation?.order ?? 0),
2092
+ onChange: (e) => set({ order: parseInt(e.target.value, 10) || 0 })
2093
+ }
2094
+ ),
2095
+ /* @__PURE__ */ jsx(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.` })
2096
+ ] })
2097
+ ] });
2098
+ }
1686
2099
  function StyleSection({ element, onPatch }) {
1687
2100
  switch (element.type) {
1688
2101
  case "text":
@@ -2358,7 +2771,9 @@ function DeckEditor({
2358
2771
  },
2359
2772
  onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2360
2773
  onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2361
- onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
2774
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background),
2775
+ onSetAnimation: (animation) => slide && elementIdSelected && ops.setAnimation(slide.id, elementIdSelected, animation),
2776
+ onSetElementAnimation: (eid, animation) => slide && ops.setAnimation(slide.id, eid, animation)
2362
2777
  }
2363
2778
  ) })
2364
2779
  ] }),
@@ -2368,6 +2783,6 @@ function DeckEditor({
2368
2783
  );
2369
2784
  }
2370
2785
 
2371
- export { DeckEditor, EditorToolbar, ElementInspector, ImageElementRenderer, PresenterView, ShapeElementRenderer, Slide, SlideRail, SlideThumbnail, SlideViewer, SpeakerNotes, TextElementRenderer, builtinThemes, chartStarterOption, darkTheme, deckId, defaultTheme, defineTheme, elementId, nextId, reduce as reduceDeck, resolveTheme, slideId, useDeckState, useSlideKeyboard, vividTheme };
2786
+ export { DeckEditor, EditorToolbar, ElementInspector, ImageElementRenderer, PresenterView, ShapeElementRenderer, Slide, SlideRail, SlideThumbnail, SlideViewer, SpeakerNotes, TextElementRenderer, buildSteps, buildsForStep, builtinThemes, chartStarterOption, collectBuilds, darkTheme, deckId, defaultTheme, defineTheme, elementId, nextId, reduce as reduceDeck, resolveTheme, slideId, stepDelays, totalBuildSteps, useDeckState, useSlideKeyboard, visibleElementIds, vividTheme };
2372
2787
  //# sourceMappingURL=index.js.map
2373
2788
  //# sourceMappingURL=index.js.map