@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.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,
@@ -280,12 +353,94 @@ function relativeLuminance(r, g, b) {
280
353
  };
281
354
  return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
282
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
+ `;
283
437
  function Slide({
284
438
  slide,
285
439
  theme,
286
440
  width,
287
441
  aspectRatio,
288
442
  editing = false,
443
+ buildStep,
289
444
  onElementContentChange,
290
445
  onElementSelect,
291
446
  selectedElementId,
@@ -329,7 +484,21 @@ function Slide({
329
484
  }),
330
485
  [t, effectiveBg, slideWidthPx]
331
486
  );
332
- 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(
333
502
  "div",
334
503
  {
335
504
  ref,
@@ -347,23 +516,45 @@ function Slide({
347
516
  onClick: (e) => {
348
517
  if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
349
518
  },
350
- children: orderedElements(slide.elements).map((element) => /* @__PURE__ */ jsxRuntime.jsx(
351
- SlideElementHost,
352
- {
353
- element,
354
- theme: t,
355
- slideWidthPx,
356
- slideHeightPx,
357
- editing,
358
- selected: selectedElementId === element.id,
359
- onContentChange: onElementContentChange,
360
- onSelect: onElementSelect,
361
- onMove: onElementMove,
362
- onResize: onElementResize,
363
- renderElement
364
- },
365
- element.id
366
- ))
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
+ ]
367
558
  }
368
559
  ) });
369
560
  }
@@ -380,7 +571,9 @@ function SlideElementHost({
380
571
  onSelect,
381
572
  onMove,
382
573
  onResize,
383
- renderElement
574
+ renderElement,
575
+ buildAnimation,
576
+ buildDelay = 0
384
577
  }) {
385
578
  const dragRef = react.useRef(null);
386
579
  if (element.hidden) return null;
@@ -449,15 +642,18 @@ function SlideElementHost({
449
642
  outline: selected ? "2px solid #8b5cf6" : void 0,
450
643
  outlineOffset: selected ? 2 : void 0,
451
644
  cursor: canMove ? "move" : interactive ? "pointer" : "default",
452
- touchAction: canMove ? "none" : void 0
645
+ touchAction: canMove ? "none" : void 0,
646
+ ...buildAnimation ? buildEnterStyle(buildAnimation, buildDelay) : null
453
647
  };
454
648
  const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
455
649
  return /* @__PURE__ */ jsxRuntime.jsxs(
456
650
  "div",
457
651
  {
652
+ className: buildAnimation ? "fs-build-enter" : void 0,
458
653
  style: box,
459
654
  "data-fancy-slides-element": element.id,
460
655
  "data-fancy-slides-element-type": element.type,
656
+ "data-fancy-slides-build": buildAnimation ? "" : void 0,
461
657
  onPointerDown: canMove ? startDrag("move") : void 0,
462
658
  onPointerMove: canMove ? onPointerMove : void 0,
463
659
  onPointerUp: canMove ? endDrag : void 0,
@@ -579,6 +775,8 @@ function useSlideKeyboard({
579
775
  total,
580
776
  index,
581
777
  goTo,
778
+ onAdvance,
779
+ onRetreat,
582
780
  onExit,
583
781
  onBlank,
584
782
  onFullscreen,
@@ -595,13 +793,15 @@ function useSlideKeyboard({
595
793
  case "ArrowLeft":
596
794
  case "PageUp":
597
795
  e.preventDefault();
598
- if (index > 0) goTo(index - 1);
796
+ if (onRetreat) onRetreat();
797
+ else if (index > 0) goTo(index - 1);
599
798
  return;
600
799
  case "ArrowRight":
601
800
  case "PageDown":
602
801
  case " ":
603
802
  e.preventDefault();
604
- if (index < total - 1) goTo(index + 1);
803
+ if (onAdvance) onAdvance();
804
+ else if (index < total - 1) goTo(index + 1);
605
805
  return;
606
806
  case "Home":
607
807
  e.preventDefault();
@@ -643,7 +843,7 @@ function useSlideKeyboard({
643
843
  };
644
844
  window.addEventListener("keydown", handler);
645
845
  return () => window.removeEventListener("keydown", handler);
646
- }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
846
+ }, [enabled, index, total, goTo, onAdvance, onRetreat, onExit, onBlank, onFullscreen]);
647
847
  }
648
848
  function SlideViewer({
649
849
  deck,
@@ -671,13 +871,34 @@ function SlideViewer({
671
871
  const containerRef = react.useRef(null);
672
872
  const prevIndexRef = react.useRef(index);
673
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);
674
878
  react.useEffect(() => {
879
+ if (index === prevIndexRef.current) return;
675
880
  prevIndexRef.current = index;
676
- }, [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]);
677
896
  useSlideKeyboard({
678
897
  total: deck.slides.length,
679
898
  index,
680
899
  goTo,
900
+ onAdvance: advance,
901
+ onRetreat: retreat,
681
902
  onExit,
682
903
  onBlank: () => setBlanked((b) => !b),
683
904
  onFullscreen: () => {
@@ -690,11 +911,15 @@ function SlideViewer({
690
911
  react.useEffect(() => {
691
912
  if (!autoAdvanceMs || deck.slides.length <= 1) return;
692
913
  const t = setTimeout(() => {
693
- 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
+ }
694
920
  }, autoAdvanceMs);
695
921
  return () => clearTimeout(t);
696
- }, [autoAdvanceMs, index, deck.slides.length, goTo]);
697
- const slide = deck.slides[index];
922
+ }, [autoAdvanceMs, index, deck.slides.length, goTo, buildStep, totalSteps]);
698
923
  const theme = resolveTheme(deck.theme);
699
924
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
700
925
  const transition = slide?.transition ?? theme.defaultTransition;
@@ -716,6 +941,11 @@ function SlideViewer({
716
941
  },
717
942
  tabIndex: 0,
718
943
  "data-fancy-slides-viewer": deck.id,
944
+ "data-fancy-slides-build-step": buildStep,
945
+ onClick: () => {
946
+ if (blanked) return;
947
+ advance();
948
+ },
719
949
  children: [
720
950
  /* @__PURE__ */ jsxRuntime.jsx("style", { children: TRANSITION_KEYFRAMES }),
721
951
  !blanked && slide && /* @__PURE__ */ jsxRuntime.jsx(
@@ -729,7 +959,7 @@ function SlideViewer({
729
959
  ["--fs-ratio"]: aspectRatio.toString(),
730
960
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
731
961
  },
732
- 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)
733
963
  }
734
964
  ),
735
965
  !hideChrome && !blanked && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -760,7 +990,7 @@ function SlideViewer({
760
990
  );
761
991
  }
762
992
  var DEFAULT_DURATION = 400;
763
- var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
993
+ var EASE2 = "cubic-bezier(0.16, 1, 0.3, 1)";
764
994
  function transitionEnterStyle(transition, forward) {
765
995
  const kind = transition?.kind ?? "none";
766
996
  if (kind === "none") return { width: "100%", height: "100%" };
@@ -786,7 +1016,7 @@ function transitionEnterStyle(transition, forward) {
786
1016
  height: "100%",
787
1017
  animationName: name,
788
1018
  animationDuration: `${duration}ms`,
789
- animationTimingFunction: EASE,
1019
+ animationTimingFunction: EASE2,
790
1020
  animationFillMode: "both"
791
1021
  };
792
1022
  }
@@ -842,14 +1072,38 @@ function PresenterView({
842
1072
  },
843
1073
  [deck.slides.length, isControlled, onIndexChange]
844
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]);
845
1098
  useSlideKeyboard({
846
1099
  total: deck.slides.length,
847
1100
  index,
848
1101
  goTo,
1102
+ onAdvance: advance,
1103
+ onRetreat: retreat,
849
1104
  onExit
850
1105
  });
851
1106
  const theme = resolveTheme(deck.theme);
852
- const slide = deck.slides[index];
853
1107
  const nextSlide = deck.slides[index + 1];
854
1108
  const [now, setNow] = react.useState(() => Date.now());
855
1109
  react.useEffect(() => {
@@ -896,7 +1150,7 @@ function PresenterView({
896
1150
  borderRadius: 8,
897
1151
  overflow: "hidden"
898
1152
  },
899
- children: slide ? /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement }) : null
1153
+ children: slide ? /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, buildStep, renderElement }) : null
900
1154
  }
901
1155
  )
902
1156
  }
@@ -1000,8 +1254,8 @@ function PresenterView({
1000
1254
  /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
1001
1255
  /* @__PURE__ */ jsxRuntime.jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
1002
1256
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
1003
- /* @__PURE__ */ jsxRuntime.jsx(NavButton, { onClick: () => goTo(index - 1), disabled: index === 0, children: "\u2190 Prev" }),
1004
- /* @__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" })
1005
1259
  ] })
1006
1260
  ]
1007
1261
  }
@@ -1184,6 +1438,7 @@ function useDeckState({ value, onChange, onOp }) {
1184
1438
  updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1185
1439
  moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1186
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 }),
1187
1442
  getSlide: (id) => value.slides.find((s) => s.id === id),
1188
1443
  getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1189
1444
  };
@@ -1251,6 +1506,23 @@ function reduce(deck, op) {
1251
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
1252
1507
  )
1253
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
+ };
1254
1526
  }
1255
1527
  }
1256
1528
 
@@ -1592,10 +1864,10 @@ function EditorToolbar({
1592
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" }) }) })
1593
1865
  ] });
1594
1866
  }
1595
- function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1867
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground, onSetAnimation, onSetElementAnimation }) {
1596
1868
  if (!element) {
1597
1869
  if (slide) {
1598
- return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1870
+ return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground, onSetElementAnimation });
1599
1871
  }
1600
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: [
1601
1873
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
@@ -1620,10 +1892,12 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1620
1892
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Tabs.List, { children: [
1621
1893
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "style", children: "Style" }),
1622
1894
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "layout", children: "Layout" }),
1895
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "build", children: "Build" }),
1623
1896
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tabs.Tab, { value: "advanced", children: "Advanced" })
1624
1897
  ] }),
1625
1898
  /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Tabs.Panels, { children: [
1626
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 }) }) }),
1627
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 }) }) }),
1628
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 }) }) })
1629
1903
  ] })
@@ -1633,7 +1907,8 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1633
1907
  function SlideSettings({
1634
1908
  slide,
1635
1909
  onSetTransition,
1636
- onSetBackground
1910
+ onSetBackground,
1911
+ onSetElementAnimation
1637
1912
  }) {
1638
1913
  const transition = slide.transition;
1639
1914
  const kind = transition?.kind ?? "none";
@@ -1700,10 +1975,54 @@ function SlideSettings({
1700
1975
  onChange: (c) => onSetBackground({ ...slide.background, color: c })
1701
1976
  }
1702
1977
  ) })
1703
- ] }) })
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 }) })
1704
1980
  ] })
1705
1981
  ] });
1706
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
+ }
1707
2026
  function LayoutSection({ element, onPatch }) {
1708
2027
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1709
2028
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1725,6 +2044,100 @@ function AdvancedSection({ element, onPatch }) {
1725
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" }) })
1726
2045
  ] });
1727
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
+ }
1728
2141
  function StyleSection({ element, onPatch }) {
1729
2142
  switch (element.type) {
1730
2143
  case "text":
@@ -2400,7 +2813,9 @@ function DeckEditor({
2400
2813
  },
2401
2814
  onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2402
2815
  onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2403
- 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)
2404
2819
  }
2405
2820
  ) })
2406
2821
  ] }),
@@ -2422,8 +2837,11 @@ exports.SlideThumbnail = SlideThumbnail;
2422
2837
  exports.SlideViewer = SlideViewer;
2423
2838
  exports.SpeakerNotes = SpeakerNotes;
2424
2839
  exports.TextElementRenderer = TextElementRenderer;
2840
+ exports.buildSteps = buildSteps;
2841
+ exports.buildsForStep = buildsForStep;
2425
2842
  exports.builtinThemes = builtinThemes;
2426
2843
  exports.chartStarterOption = chartStarterOption;
2844
+ exports.collectBuilds = collectBuilds;
2427
2845
  exports.darkTheme = darkTheme;
2428
2846
  exports.deckId = deckId;
2429
2847
  exports.defaultTheme = defaultTheme;
@@ -2433,11 +2851,14 @@ exports.nextId = nextId;
2433
2851
  exports.reduceDeck = reduce;
2434
2852
  exports.resolveTheme = resolveTheme;
2435
2853
  exports.slideId = slideId;
2854
+ exports.stepDelays = stepDelays;
2855
+ exports.totalBuildSteps = totalBuildSteps;
2436
2856
  exports.useDeckState = useDeckState;
2437
2857
  exports.useIsDarkSlide = useIsDarkSlide;
2438
2858
  exports.useSlideContext = useSlideContext;
2439
2859
  exports.useSlideKeyboard = useSlideKeyboard;
2440
2860
  exports.useSlideTheme = useSlideTheme;
2861
+ exports.visibleElementIds = visibleElementIds;
2441
2862
  exports.vividTheme = vividTheme;
2442
2863
  //# sourceMappingURL=index.cjs.map
2443
2864
  //# sourceMappingURL=index.cjs.map