@particle-academy/fancy-slides 0.3.0 → 0.5.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
@@ -1,7 +1,7 @@
1
1
  import { isDarkColor, SlideContext } from './chunk-WIUXPQAK.js';
2
2
  export { useIsDarkSlide, useSlideContext, useSlideTheme } from './chunk-WIUXPQAK.js';
3
3
  import { useId, useRef, useState, useEffect, useMemo, useCallback } from 'react';
4
- import { ContentRenderer, Text, Action, ContextMenu, Separator, Tooltip, Dropdown, Badge, Heading, Tabs, Card, Select, Input, ColorPicker, Slider, Textarea } from '@particle-academy/react-fancy';
4
+ import { ContentRenderer, Text, Action, ContextMenu, Separator, Tooltip, Dropdown, Badge, Heading, Tabs, Card, Select, Input, ColorPicker, Slider, Switch, Textarea } from '@particle-academy/react-fancy';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/theme/default-theme.ts
@@ -70,13 +70,211 @@ function resolveTheme(theme) {
70
70
  function cn(...parts) {
71
71
  return parts.filter(Boolean).join(" ");
72
72
  }
73
+
74
+ // src/utils/builds.ts
75
+ function splitParagraphs(content) {
76
+ const lines = content.split("\n");
77
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
78
+ return lines;
79
+ }
80
+ function isByParagraph(element, animation) {
81
+ if (!animation.byParagraph || element.type !== "text") return false;
82
+ return splitParagraphs(element.content).length > 1;
83
+ }
84
+ var DEFAULT_BUILD_DURATION = 500;
85
+ function collectBuilds(slide) {
86
+ if (!slide) return [];
87
+ const builds = [];
88
+ slide.elements.forEach((element, index) => {
89
+ if (element.animation) {
90
+ builds.push({ element, animation: element.animation, index });
91
+ }
92
+ });
93
+ const ordered = builds.sort((a, b) => {
94
+ const ao = a.animation.order ?? 0;
95
+ const bo = b.animation.order ?? 0;
96
+ if (ao !== bo) return ao - bo;
97
+ return a.index - b.index;
98
+ });
99
+ const expanded = [];
100
+ for (const build of ordered) {
101
+ if (isByParagraph(build.element, build.animation)) {
102
+ const paras = splitParagraphs(build.element.content);
103
+ paras.forEach((_, paraIndex) => {
104
+ const animation = paraIndex === 0 ? build.animation : { ...build.animation, trigger: "on-click" };
105
+ expanded.push({ element: build.element, animation, index: build.index, paraIndex });
106
+ });
107
+ } else {
108
+ expanded.push(build);
109
+ }
110
+ }
111
+ return expanded;
112
+ }
113
+ function buildSteps(slide) {
114
+ const builds = collectBuilds(slide);
115
+ const steps = [];
116
+ for (const build of builds) {
117
+ const trigger = build.animation.trigger ?? "on-click";
118
+ if (steps.length === 0 || trigger === "on-click") {
119
+ steps.push({ builds: [build] });
120
+ } else {
121
+ steps[steps.length - 1].builds.push(build);
122
+ }
123
+ }
124
+ return steps;
125
+ }
126
+ function totalBuildSteps(slide) {
127
+ return buildSteps(slide).length;
128
+ }
129
+ function visibleElementIds(slide, buildStep) {
130
+ const visible = /* @__PURE__ */ new Set();
131
+ if (!slide) return visible;
132
+ const steps = buildSteps(slide);
133
+ const stepOfElement = /* @__PURE__ */ new Map();
134
+ steps.forEach((step, i) => {
135
+ for (const b of step.builds) {
136
+ if (!stepOfElement.has(b.element.id)) stepOfElement.set(b.element.id, i + 1);
137
+ }
138
+ });
139
+ for (const element of slide.elements) {
140
+ const revealStep = stepOfElement.get(element.id);
141
+ if (revealStep === void 0) {
142
+ visible.add(element.id);
143
+ } else if (buildStep >= revealStep) {
144
+ visible.add(element.id);
145
+ }
146
+ }
147
+ return visible;
148
+ }
149
+ function paragraphReveals(slide, buildStep) {
150
+ const out = /* @__PURE__ */ new Map();
151
+ if (!slide) return out;
152
+ const steps = buildSteps(slide);
153
+ steps.forEach((step, i) => {
154
+ const stepNum = i + 1;
155
+ for (const b of step.builds) {
156
+ if (b.paraIndex === void 0) continue;
157
+ const fired = buildStep >= stepNum;
158
+ const prev = out.get(b.element.id) ?? { revealed: 0 };
159
+ if (fired) {
160
+ prev.revealed = Math.max(prev.revealed, b.paraIndex + 1);
161
+ if (stepNum === buildStep) prev.firingParaIndex = b.paraIndex;
162
+ }
163
+ out.set(b.element.id, prev);
164
+ }
165
+ });
166
+ return out;
167
+ }
168
+ function buildsForStep(slide, buildStep) {
169
+ const steps = buildSteps(slide);
170
+ const step = steps[buildStep - 1];
171
+ return step ? step.builds : [];
172
+ }
173
+ function stepDelays(builds) {
174
+ const delays = /* @__PURE__ */ new Map();
175
+ const lead = builds[0];
176
+ if (!lead) return delays;
177
+ const leadDelay = lead.animation.delay ?? 0;
178
+ const leadDuration = lead.animation.duration ?? DEFAULT_BUILD_DURATION;
179
+ delays.set(lead.element.id, leadDelay);
180
+ for (let i = 1; i < builds.length; i++) {
181
+ const b = builds[i];
182
+ const own = b.animation.delay ?? 0;
183
+ const trigger = b.animation.trigger ?? "on-click";
184
+ const base = trigger === "after-prev" ? leadDelay + leadDuration : leadDelay;
185
+ delays.set(b.element.id, base + own);
186
+ }
187
+ return delays;
188
+ }
189
+
190
+ // src/components/Slide/builds-style.ts
191
+ var DEFAULT_BUILD_DURATION2 = 500;
192
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
193
+ function buildEnterStyle(animation, effectiveDelay) {
194
+ const duration = animation.duration ?? DEFAULT_BUILD_DURATION2;
195
+ const dir = animation.direction ?? "left";
196
+ let name;
197
+ switch (animation.effect) {
198
+ case "fade":
199
+ name = "fs-build-fade";
200
+ break;
201
+ case "zoom":
202
+ name = "fs-build-zoom";
203
+ break;
204
+ case "fly-in":
205
+ name = `fs-build-fly-${dir}`;
206
+ break;
207
+ case "wipe":
208
+ name = `fs-build-wipe-${dir}`;
209
+ break;
210
+ default:
211
+ name = "fs-build-fade";
212
+ }
213
+ return {
214
+ animationName: name,
215
+ animationDuration: `${duration}ms`,
216
+ animationDelay: `${effectiveDelay}ms`,
217
+ animationTimingFunction: EASE,
218
+ animationFillMode: "both"
219
+ };
220
+ }
221
+ var BUILD_KEYFRAMES = `
222
+ @media (prefers-reduced-motion: reduce) {
223
+ .fs-build-enter { animation: none !important; }
224
+ }
225
+ @media (prefers-reduced-motion: no-preference) {
226
+ @keyframes fs-build-fade {
227
+ from { opacity: 0; }
228
+ to { opacity: 1; }
229
+ }
230
+ @keyframes fs-build-zoom {
231
+ from { opacity: 0; transform: scale(0.8); }
232
+ to { opacity: 1; transform: scale(1); }
233
+ }
234
+ @keyframes fs-build-fly-left {
235
+ from { opacity: 0; transform: translateX(-24%); }
236
+ to { opacity: 1; transform: translateX(0); }
237
+ }
238
+ @keyframes fs-build-fly-right {
239
+ from { opacity: 0; transform: translateX(24%); }
240
+ to { opacity: 1; transform: translateX(0); }
241
+ }
242
+ @keyframes fs-build-fly-up {
243
+ from { opacity: 0; transform: translateY(24%); }
244
+ to { opacity: 1; transform: translateY(0); }
245
+ }
246
+ @keyframes fs-build-fly-down {
247
+ from { opacity: 0; transform: translateY(-24%); }
248
+ to { opacity: 1; transform: translateY(0); }
249
+ }
250
+ /* wipe: clip-path inset reveals from the named edge toward the opposite one.
251
+ inset(top right bottom left) \u2014 start fully clipped on the far side. */
252
+ @keyframes fs-build-wipe-left {
253
+ from { clip-path: inset(0 100% 0 0); }
254
+ to { clip-path: inset(0 0 0 0); }
255
+ }
256
+ @keyframes fs-build-wipe-right {
257
+ from { clip-path: inset(0 0 0 100%); }
258
+ to { clip-path: inset(0 0 0 0); }
259
+ }
260
+ @keyframes fs-build-wipe-up {
261
+ from { clip-path: inset(100% 0 0 0); }
262
+ to { clip-path: inset(0 0 0 0); }
263
+ }
264
+ @keyframes fs-build-wipe-down {
265
+ from { clip-path: inset(0 0 100% 0); }
266
+ to { clip-path: inset(0 0 0 0); }
267
+ }
268
+ }
269
+ `;
73
270
  function TextElementRenderer({
74
271
  element,
75
272
  theme,
76
273
  slideWidthPx,
77
274
  editing = false,
78
275
  selected = false,
79
- onContentChange
276
+ onContentChange,
277
+ paraReveal
80
278
  }) {
81
279
  const t = resolveTheme(theme);
82
280
  const style = element.style ?? {};
@@ -123,30 +321,53 @@ function TextElementRenderer({
123
321
  }
124
322
  );
125
323
  }
324
+ const proseScope = `[data-fs-text-scope="${scopeId}"]`;
325
+ const doubleScope = `${proseScope}${proseScope}`;
326
+ const proseStyle = /* @__PURE__ */ jsx("style", { children: `
327
+ ${proseScope} > div { width: 100%; height: 100%; font-size: inherit; }
328
+ ${doubleScope} :is(p, ul, ol, li, blockquote, h1, h2, h3, h4, h5, h6, pre, code, strong, em, a) {
329
+ font-size: inherit;
330
+ }
331
+ ${doubleScope} h1 { font-size: 1.6em; font-weight: 700; }
332
+ ${doubleScope} h2 { font-size: 1.35em; font-weight: 700; }
333
+ ${doubleScope} h3 { font-size: 1.15em; font-weight: 600; }
334
+ ${proseScope} :where(p, ul, ol, h1, h2, h3, h4, h5, h6, pre, blockquote) {
335
+ margin: 0;
336
+ padding: 0;
337
+ }
338
+ ${proseScope} :where(p, li) + :where(p, li, ul, ol) { margin-top: 0.4em; }
339
+ ${proseScope} :where(ul, ol) { padding-left: 1.4em; }
340
+ ${proseScope} :where(strong) { font-weight: ${Math.max(700, weight(style.weight) ?? 400 + 200)}; }
341
+ ${proseScope} :where(a) { color: inherit; text-decoration: underline; }
342
+ ${proseScope} :where(code) { font-family: ${t.fonts?.mono ?? "monospace"}; }
343
+ ` });
344
+ const renderChunk = (content) => format === "plain" ? content : /* @__PURE__ */ jsx(ContentRenderer, { value: content, format: format === "html" ? "html" : "markdown" });
345
+ if (paraReveal) {
346
+ const paras = splitParagraphs(element.content);
347
+ return /* @__PURE__ */ jsxs("div", { "data-fs-text-scope": scopeId, style: css, children: [
348
+ proseStyle,
349
+ paras.map((para, i) => {
350
+ if (i >= paraReveal.revealed) return null;
351
+ const firing = i === paraReveal.firingParaIndex && !!element.animation;
352
+ const enter = firing ? buildEnterStyle(element.animation, element.animation.delay ?? 0) : null;
353
+ return /* @__PURE__ */ jsx(
354
+ "div",
355
+ {
356
+ className: firing ? "fs-build-enter" : void 0,
357
+ style: { whiteSpace: format === "plain" ? "pre-wrap" : "normal", ...enter },
358
+ "data-fancy-slides-paragraph": i,
359
+ children: renderChunk(para)
360
+ },
361
+ i
362
+ );
363
+ })
364
+ ] });
365
+ }
126
366
  if (format === "plain") {
127
367
  return /* @__PURE__ */ jsx("div", { style: css, children: element.content });
128
368
  }
129
- const proseScope = `[data-fs-text-scope="${scopeId}"]`;
130
- const doubleScope = `${proseScope}${proseScope}`;
131
369
  return /* @__PURE__ */ jsxs("div", { "data-fs-text-scope": scopeId, style: css, children: [
132
- /* @__PURE__ */ jsx("style", { children: `
133
- ${proseScope} > div { width: 100%; height: 100%; font-size: inherit; }
134
- ${doubleScope} :is(p, ul, ol, li, blockquote, h1, h2, h3, h4, h5, h6, pre, code, strong, em, a) {
135
- font-size: inherit;
136
- }
137
- ${doubleScope} h1 { font-size: 1.6em; font-weight: 700; }
138
- ${doubleScope} h2 { font-size: 1.35em; font-weight: 700; }
139
- ${doubleScope} h3 { font-size: 1.15em; font-weight: 600; }
140
- ${proseScope} :where(p, ul, ol, h1, h2, h3, h4, h5, h6, pre, blockquote) {
141
- margin: 0;
142
- padding: 0;
143
- }
144
- ${proseScope} :where(p, li) + :where(p, li, ul, ol) { margin-top: 0.4em; }
145
- ${proseScope} :where(ul, ol) { padding-left: 1.4em; }
146
- ${proseScope} :where(strong) { font-weight: ${Math.max(700, weight(style.weight) ?? 400 + 200)}; }
147
- ${proseScope} :where(a) { color: inherit; text-decoration: underline; }
148
- ${proseScope} :where(code) { font-family: ${t.fonts?.mono ?? "monospace"}; }
149
- ` }),
370
+ proseStyle,
150
371
  /* @__PURE__ */ jsx(ContentRenderer, { value: element.content, format: format === "html" ? "html" : "markdown" })
151
372
  ] });
152
373
  }
@@ -244,6 +465,7 @@ function Slide({
244
465
  width,
245
466
  aspectRatio,
246
467
  editing = false,
468
+ buildStep,
247
469
  onElementContentChange,
248
470
  onElementSelect,
249
471
  selectedElementId,
@@ -287,7 +509,24 @@ function Slide({
287
509
  }),
288
510
  [t, effectiveBg, slideWidthPx]
289
511
  );
290
- return /* @__PURE__ */ jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsx(
512
+ const buildInfo = useMemo(() => {
513
+ if (editing) return null;
514
+ const steps = buildSteps(slide);
515
+ if (steps.length === 0) return null;
516
+ const revealStep = /* @__PURE__ */ new Map();
517
+ steps.forEach((step, i) => {
518
+ for (const b of step.builds) {
519
+ if (!revealStep.has(b.element.id)) revealStep.set(b.element.id, i + 1);
520
+ }
521
+ });
522
+ const driven = buildStep !== void 0;
523
+ const currentStep = driven ? buildStep : steps.length;
524
+ const firing = driven ? steps[currentStep - 1] : void 0;
525
+ const delays = firing ? stepDelays(firing.builds) : /* @__PURE__ */ new Map();
526
+ const paraReveals = driven ? paragraphReveals(slide, currentStep) : /* @__PURE__ */ new Map();
527
+ return { revealStep, currentStep, delays, paraReveals, driven };
528
+ }, [editing, slide, buildStep]);
529
+ return /* @__PURE__ */ jsx(SlideContext.Provider, { value: slideContext, children: /* @__PURE__ */ jsxs(
291
530
  "div",
292
531
  {
293
532
  ref,
@@ -305,23 +544,47 @@ function Slide({
305
544
  onClick: (e) => {
306
545
  if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
307
546
  },
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
- ))
547
+ children: [
548
+ buildInfo && /* @__PURE__ */ jsx("style", { children: BUILD_KEYFRAMES }),
549
+ orderedElements(slide.elements).map((element) => {
550
+ let buildHidden = false;
551
+ let buildAnimation;
552
+ let buildDelay = 0;
553
+ const paraReveal = buildInfo?.paraReveals.get(element.id);
554
+ if (buildInfo) {
555
+ const step = buildInfo.revealStep.get(element.id);
556
+ if (step !== void 0) {
557
+ if (buildInfo.currentStep < step) {
558
+ buildHidden = true;
559
+ } else if (paraReveal) ; else if (buildInfo.currentStep === step && element.animation) {
560
+ buildAnimation = element.animation;
561
+ buildDelay = buildInfo.delays.get(element.id) ?? 0;
562
+ }
563
+ }
564
+ }
565
+ if (buildHidden) return null;
566
+ return /* @__PURE__ */ jsx(
567
+ SlideElementHost,
568
+ {
569
+ element,
570
+ theme: t,
571
+ slideWidthPx,
572
+ slideHeightPx,
573
+ editing,
574
+ selected: selectedElementId === element.id,
575
+ onContentChange: onElementContentChange,
576
+ onSelect: onElementSelect,
577
+ onMove: onElementMove,
578
+ onResize: onElementResize,
579
+ renderElement,
580
+ buildAnimation,
581
+ buildDelay,
582
+ paraReveal
583
+ },
584
+ element.id
585
+ );
586
+ })
587
+ ]
325
588
  }
326
589
  ) });
327
590
  }
@@ -338,7 +601,10 @@ function SlideElementHost({
338
601
  onSelect,
339
602
  onMove,
340
603
  onResize,
341
- renderElement
604
+ renderElement,
605
+ buildAnimation,
606
+ buildDelay = 0,
607
+ paraReveal
342
608
  }) {
343
609
  const dragRef = useRef(null);
344
610
  if (element.hidden) return null;
@@ -407,15 +673,18 @@ function SlideElementHost({
407
673
  outline: selected ? "2px solid #8b5cf6" : void 0,
408
674
  outlineOffset: selected ? 2 : void 0,
409
675
  cursor: canMove ? "move" : interactive ? "pointer" : "default",
410
- touchAction: canMove ? "none" : void 0
676
+ touchAction: canMove ? "none" : void 0,
677
+ ...buildAnimation ? buildEnterStyle(buildAnimation, buildDelay) : null
411
678
  };
412
- const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
679
+ const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange, paraReveal }) ?? renderElement?.(element, slideWidthPx);
413
680
  return /* @__PURE__ */ jsxs(
414
681
  "div",
415
682
  {
683
+ className: buildAnimation ? "fs-build-enter" : void 0,
416
684
  style: box,
417
685
  "data-fancy-slides-element": element.id,
418
686
  "data-fancy-slides-element-type": element.type,
687
+ "data-fancy-slides-build": buildAnimation ? "" : void 0,
419
688
  onPointerDown: canMove ? startDrag("move") : void 0,
420
689
  onPointerMove: canMove ? onPointerMove : void 0,
421
690
  onPointerUp: canMove ? endDrag : void 0,
@@ -474,7 +743,7 @@ function ResizeHandles({ onStart, onMove, onEnd }) {
474
743
  anchor
475
744
  )) });
476
745
  }
477
- function renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) {
746
+ function renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange, paraReveal }) {
478
747
  switch (element.type) {
479
748
  case "text":
480
749
  return /* @__PURE__ */ jsx(
@@ -485,7 +754,8 @@ function renderInner({ element, theme, slideWidthPx, editing, selected, onConten
485
754
  slideWidthPx,
486
755
  editing,
487
756
  selected,
488
- onContentChange: onContentChange ? (c) => onContentChange(element.id, c) : void 0
757
+ onContentChange: onContentChange ? (c) => onContentChange(element.id, c) : void 0,
758
+ paraReveal
489
759
  }
490
760
  );
491
761
  case "image":
@@ -537,6 +807,8 @@ function useSlideKeyboard({
537
807
  total,
538
808
  index,
539
809
  goTo,
810
+ onAdvance,
811
+ onRetreat,
540
812
  onExit,
541
813
  onBlank,
542
814
  onFullscreen,
@@ -553,13 +825,15 @@ function useSlideKeyboard({
553
825
  case "ArrowLeft":
554
826
  case "PageUp":
555
827
  e.preventDefault();
556
- if (index > 0) goTo(index - 1);
828
+ if (onRetreat) onRetreat();
829
+ else if (index > 0) goTo(index - 1);
557
830
  return;
558
831
  case "ArrowRight":
559
832
  case "PageDown":
560
833
  case " ":
561
834
  e.preventDefault();
562
- if (index < total - 1) goTo(index + 1);
835
+ if (onAdvance) onAdvance();
836
+ else if (index < total - 1) goTo(index + 1);
563
837
  return;
564
838
  case "Home":
565
839
  e.preventDefault();
@@ -601,7 +875,7 @@ function useSlideKeyboard({
601
875
  };
602
876
  window.addEventListener("keydown", handler);
603
877
  return () => window.removeEventListener("keydown", handler);
604
- }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
878
+ }, [enabled, index, total, goTo, onAdvance, onRetreat, onExit, onBlank, onFullscreen]);
605
879
  }
606
880
  function SlideViewer({
607
881
  deck,
@@ -629,13 +903,34 @@ function SlideViewer({
629
903
  const containerRef = useRef(null);
630
904
  const prevIndexRef = useRef(index);
631
905
  const forward = index >= prevIndexRef.current;
906
+ const slide = deck.slides[index];
907
+ const totalSteps = totalBuildSteps(slide);
908
+ const [buildStep, setBuildStep] = useState(0);
909
+ const nextFreshRef = useRef(false);
632
910
  useEffect(() => {
911
+ if (index === prevIndexRef.current) return;
633
912
  prevIndexRef.current = index;
634
- }, [index]);
913
+ const fresh = nextFreshRef.current;
914
+ nextFreshRef.current = false;
915
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
916
+ }, [index, deck.slides]);
917
+ const advance = useCallback(() => {
918
+ if (buildStep < totalSteps) {
919
+ setBuildStep((s) => s + 1);
920
+ } else if (index < deck.slides.length - 1) {
921
+ nextFreshRef.current = true;
922
+ goTo(index + 1);
923
+ }
924
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
925
+ const retreat = useCallback(() => {
926
+ if (index > 0) goTo(index - 1);
927
+ }, [index, goTo]);
635
928
  useSlideKeyboard({
636
929
  total: deck.slides.length,
637
930
  index,
638
931
  goTo,
932
+ onAdvance: advance,
933
+ onRetreat: retreat,
639
934
  onExit,
640
935
  onBlank: () => setBlanked((b) => !b),
641
936
  onFullscreen: () => {
@@ -648,11 +943,15 @@ function SlideViewer({
648
943
  useEffect(() => {
649
944
  if (!autoAdvanceMs || deck.slides.length <= 1) return;
650
945
  const t = setTimeout(() => {
651
- goTo(index + 1 < deck.slides.length ? index + 1 : 0);
946
+ if (buildStep < totalSteps) {
947
+ setBuildStep((s) => s + 1);
948
+ } else {
949
+ nextFreshRef.current = true;
950
+ goTo(index + 1 < deck.slides.length ? index + 1 : 0);
951
+ }
652
952
  }, autoAdvanceMs);
653
953
  return () => clearTimeout(t);
654
- }, [autoAdvanceMs, index, deck.slides.length, goTo]);
655
- const slide = deck.slides[index];
954
+ }, [autoAdvanceMs, index, deck.slides.length, goTo, buildStep, totalSteps]);
656
955
  const theme = resolveTheme(deck.theme);
657
956
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
658
957
  const transition = slide?.transition ?? theme.defaultTransition;
@@ -674,6 +973,11 @@ function SlideViewer({
674
973
  },
675
974
  tabIndex: 0,
676
975
  "data-fancy-slides-viewer": deck.id,
976
+ "data-fancy-slides-build-step": buildStep,
977
+ onClick: () => {
978
+ if (blanked) return;
979
+ advance();
980
+ },
677
981
  children: [
678
982
  /* @__PURE__ */ jsx("style", { children: TRANSITION_KEYFRAMES }),
679
983
  !blanked && slide && /* @__PURE__ */ jsx(
@@ -687,7 +991,7 @@ function SlideViewer({
687
991
  ["--fs-ratio"]: aspectRatio.toString(),
688
992
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
689
993
  },
690
- children: /* @__PURE__ */ jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) }, index)
994
+ children: /* @__PURE__ */ jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsx(Slide, { slide, theme, buildStep, renderElement }) }, index)
691
995
  }
692
996
  ),
693
997
  !hideChrome && !blanked && /* @__PURE__ */ jsxs(
@@ -718,7 +1022,7 @@ function SlideViewer({
718
1022
  );
719
1023
  }
720
1024
  var DEFAULT_DURATION = 400;
721
- var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
1025
+ var EASE2 = "cubic-bezier(0.16, 1, 0.3, 1)";
722
1026
  function transitionEnterStyle(transition, forward) {
723
1027
  const kind = transition?.kind ?? "none";
724
1028
  if (kind === "none") return { width: "100%", height: "100%" };
@@ -744,7 +1048,7 @@ function transitionEnterStyle(transition, forward) {
744
1048
  height: "100%",
745
1049
  animationName: name,
746
1050
  animationDuration: `${duration}ms`,
747
- animationTimingFunction: EASE,
1051
+ animationTimingFunction: EASE2,
748
1052
  animationFillMode: "both"
749
1053
  };
750
1054
  }
@@ -800,14 +1104,38 @@ function PresenterView({
800
1104
  },
801
1105
  [deck.slides.length, isControlled, onIndexChange]
802
1106
  );
1107
+ const slide = deck.slides[index];
1108
+ const totalSteps = totalBuildSteps(slide);
1109
+ const [buildStep, setBuildStep] = useState(0);
1110
+ const prevIndexRef = useRef(index);
1111
+ const nextFreshRef = useRef(false);
1112
+ useEffect(() => {
1113
+ if (index === prevIndexRef.current) return;
1114
+ prevIndexRef.current = index;
1115
+ const fresh = nextFreshRef.current;
1116
+ nextFreshRef.current = false;
1117
+ setBuildStep(fresh ? 0 : totalBuildSteps(deck.slides[index]));
1118
+ }, [index, deck.slides]);
1119
+ const advance = useCallback(() => {
1120
+ if (buildStep < totalSteps) {
1121
+ setBuildStep((s) => s + 1);
1122
+ } else if (index < deck.slides.length - 1) {
1123
+ nextFreshRef.current = true;
1124
+ goTo(index + 1);
1125
+ }
1126
+ }, [buildStep, totalSteps, index, deck.slides.length, goTo]);
1127
+ const retreat = useCallback(() => {
1128
+ if (index > 0) goTo(index - 1);
1129
+ }, [index, goTo]);
803
1130
  useSlideKeyboard({
804
1131
  total: deck.slides.length,
805
1132
  index,
806
1133
  goTo,
1134
+ onAdvance: advance,
1135
+ onRetreat: retreat,
807
1136
  onExit
808
1137
  });
809
1138
  const theme = resolveTheme(deck.theme);
810
- const slide = deck.slides[index];
811
1139
  const nextSlide = deck.slides[index + 1];
812
1140
  const [now, setNow] = useState(() => Date.now());
813
1141
  useEffect(() => {
@@ -854,7 +1182,7 @@ function PresenterView({
854
1182
  borderRadius: 8,
855
1183
  overflow: "hidden"
856
1184
  },
857
- children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) : null
1185
+ children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, buildStep, renderElement }) : null
858
1186
  }
859
1187
  )
860
1188
  }
@@ -958,8 +1286,8 @@ function PresenterView({
958
1286
  /* @__PURE__ */ jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
959
1287
  /* @__PURE__ */ jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
960
1288
  /* @__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" })
1289
+ /* @__PURE__ */ jsx(NavButton, { onClick: retreat, disabled: index === 0, children: "\u2190 Prev" }),
1290
+ /* @__PURE__ */ jsx(NavButton, { onClick: advance, disabled: index >= deck.slides.length - 1 && buildStep >= totalSteps, children: "Next \u2192" })
963
1291
  ] })
964
1292
  ]
965
1293
  }
@@ -1142,6 +1470,7 @@ function useDeckState({ value, onChange, onOp }) {
1142
1470
  updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1143
1471
  moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1144
1472
  resizeElement: (slideIdArg, elementIdArg, w, h) => apply({ kind: "element_resize", slideId: slideIdArg, elementId: elementIdArg, w, h }),
1473
+ setAnimation: (slideIdArg, elementIdArg, animation) => apply({ kind: "element_set_animation", slideId: slideIdArg, elementId: elementIdArg, animation }),
1145
1474
  getSlide: (id) => value.slides.find((s) => s.id === id),
1146
1475
  getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1147
1476
  };
@@ -1209,6 +1538,23 @@ function reduce(deck, op) {
1209
1538
  (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
1539
  )
1211
1540
  };
1541
+ case "element_set_animation":
1542
+ return {
1543
+ ...deck,
1544
+ slides: deck.slides.map(
1545
+ (s) => s.id === op.slideId ? {
1546
+ ...s,
1547
+ elements: s.elements.map((e) => {
1548
+ if (e.id !== op.elementId) return e;
1549
+ if (op.animation === void 0) {
1550
+ const { animation: _drop, ...rest } = e;
1551
+ return rest;
1552
+ }
1553
+ return { ...e, animation: op.animation };
1554
+ })
1555
+ } : s
1556
+ )
1557
+ };
1212
1558
  }
1213
1559
  }
1214
1560
 
@@ -1550,10 +1896,10 @@ function EditorToolbar({
1550
1896
  /* @__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
1897
  ] });
1552
1898
  }
1553
- function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1899
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground, onSetAnimation, onSetElementAnimation }) {
1554
1900
  if (!element) {
1555
1901
  if (slide) {
1556
- return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1902
+ return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground, onSetElementAnimation });
1557
1903
  }
1558
1904
  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
1905
  /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
@@ -1578,10 +1924,12 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1578
1924
  /* @__PURE__ */ jsxs(Tabs.List, { children: [
1579
1925
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "style", children: "Style" }),
1580
1926
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "layout", children: "Layout" }),
1927
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "build", children: "Build" }),
1581
1928
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "advanced", children: "Advanced" })
1582
1929
  ] }),
1583
1930
  /* @__PURE__ */ jsxs(Tabs.Panels, { children: [
1584
1931
  /* @__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 }) }) }),
1932
+ /* @__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, isText: element.type === "text" }) }) }),
1585
1933
  /* @__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
1934
  /* @__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
1935
  ] })
@@ -1591,7 +1939,8 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1591
1939
  function SlideSettings({
1592
1940
  slide,
1593
1941
  onSetTransition,
1594
- onSetBackground
1942
+ onSetBackground,
1943
+ onSetElementAnimation
1595
1944
  }) {
1596
1945
  const transition = slide.transition;
1597
1946
  const kind = transition?.kind ?? "none";
@@ -1658,10 +2007,54 @@ function SlideSettings({
1658
2007
  onChange: (c) => onSetBackground({ ...slide.background, color: c })
1659
2008
  }
1660
2009
  ) })
1661
- ] }) })
2010
+ ] }) }),
2011
+ onSetElementAnimation && /* @__PURE__ */ jsx(Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(BuildOrderList, { slide, onSetElementAnimation }) })
1662
2012
  ] })
1663
2013
  ] });
1664
2014
  }
2015
+ function BuildOrderList({
2016
+ slide,
2017
+ onSetElementAnimation
2018
+ }) {
2019
+ const builds = collectBuilds(slide);
2020
+ const move = (from, to) => {
2021
+ if (to < 0 || to >= builds.length) return;
2022
+ const reordered = [...builds];
2023
+ const [item] = reordered.splice(from, 1);
2024
+ reordered.splice(to, 0, item);
2025
+ reordered.forEach((b, i) => {
2026
+ if ((b.animation.order ?? 0) !== i) {
2027
+ onSetElementAnimation(b.element.id, { ...b.animation, order: i });
2028
+ }
2029
+ });
2030
+ };
2031
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2032
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Build order" }),
2033
+ 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: [
2034
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400 w-5", children: [
2035
+ i + 1,
2036
+ "."
2037
+ ] }),
2038
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2039
+ /* @__PURE__ */ jsx(Text, { size: "sm", className: "truncate", children: buildLabel(b.element) }),
2040
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
2041
+ b.animation.effect,
2042
+ " \xB7 ",
2043
+ b.animation.trigger ?? "on-click"
2044
+ ] })
2045
+ ] }),
2046
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "chevron-up", onClick: () => move(i, i - 1), disabled: i === 0, "aria-label": "Move earlier" }),
2047
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "chevron-down", onClick: () => move(i, i + 1), disabled: i === builds.length - 1, "aria-label": "Move later" })
2048
+ ] }, b.element.id))
2049
+ ] });
2050
+ }
2051
+ function buildLabel(element) {
2052
+ if (element.type === "text") {
2053
+ const text = element.content.replace(/\s+/g, " ").trim();
2054
+ return text ? text.length > 28 ? `${text.slice(0, 28)}\u2026` : text : "Text";
2055
+ }
2056
+ return `${element.type} #${element.id.slice(-6)}`;
2057
+ }
1665
2058
  function LayoutSection({ element, onPatch }) {
1666
2059
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1667
2060
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1683,6 +2076,109 @@ function AdvancedSection({ element, onPatch }) {
1683
2076
  /* @__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
2077
  ] });
1685
2078
  }
2079
+ var NO_ANIMATION = "none";
2080
+ function AnimateSection({
2081
+ animation,
2082
+ onSetAnimation,
2083
+ isText
2084
+ }) {
2085
+ if (!onSetAnimation) {
2086
+ return /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "Build animations aren't wired up in this editor." });
2087
+ }
2088
+ const effect = animation?.effect;
2089
+ const set = (next) => {
2090
+ const base = animation ?? { effect: "fade" };
2091
+ onSetAnimation({ ...base, ...next });
2092
+ };
2093
+ const showDirection = effect === "fly-in" || effect === "wipe";
2094
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2095
+ /* @__PURE__ */ jsx(
2096
+ Select,
2097
+ {
2098
+ label: "Effect",
2099
+ list: [
2100
+ { value: NO_ANIMATION, label: "None" },
2101
+ { value: "fade", label: "Fade" },
2102
+ { value: "fly-in", label: "Fly in" },
2103
+ { value: "zoom", label: "Zoom" },
2104
+ { value: "wipe", label: "Wipe" }
2105
+ ],
2106
+ value: effect ?? NO_ANIMATION,
2107
+ onValueChange: (v) => {
2108
+ if (v === NO_ANIMATION) onSetAnimation(void 0);
2109
+ else set({ effect: v });
2110
+ }
2111
+ }
2112
+ ),
2113
+ effect && /* @__PURE__ */ jsxs(Fragment, { children: [
2114
+ /* @__PURE__ */ jsx(
2115
+ Select,
2116
+ {
2117
+ label: "Trigger",
2118
+ list: [
2119
+ { value: "on-click", label: "On click" },
2120
+ { value: "with-prev", label: "With previous" },
2121
+ { value: "after-prev", label: "After previous" }
2122
+ ],
2123
+ value: animation?.trigger ?? "on-click",
2124
+ onValueChange: (v) => set({ trigger: v })
2125
+ }
2126
+ ),
2127
+ showDirection && /* @__PURE__ */ jsx(
2128
+ Select,
2129
+ {
2130
+ label: "Direction",
2131
+ list: [
2132
+ { value: "left", label: "From left" },
2133
+ { value: "right", label: "From right" },
2134
+ { value: "up", label: "From bottom" },
2135
+ { value: "down", label: "From top" }
2136
+ ],
2137
+ value: animation?.direction ?? "left",
2138
+ onValueChange: (v) => set({ direction: v })
2139
+ }
2140
+ ),
2141
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
2142
+ /* @__PURE__ */ jsx(
2143
+ Input,
2144
+ {
2145
+ label: "Duration (ms)",
2146
+ type: "number",
2147
+ value: String(animation?.duration ?? 500),
2148
+ onChange: (e) => set({ duration: parseInt(e.target.value, 10) || 500 })
2149
+ }
2150
+ ),
2151
+ /* @__PURE__ */ jsx(
2152
+ Input,
2153
+ {
2154
+ label: "Delay (ms)",
2155
+ type: "number",
2156
+ value: String(animation?.delay ?? 0),
2157
+ onChange: (e) => set({ delay: parseInt(e.target.value, 10) || 0 })
2158
+ }
2159
+ )
2160
+ ] }),
2161
+ /* @__PURE__ */ jsx(
2162
+ Input,
2163
+ {
2164
+ label: "Order",
2165
+ type: "number",
2166
+ value: String(animation?.order ?? 0),
2167
+ onChange: (e) => set({ order: parseInt(e.target.value, 10) || 0 })
2168
+ }
2169
+ ),
2170
+ isText && /* @__PURE__ */ jsx(
2171
+ Switch,
2172
+ {
2173
+ label: "Animate by paragraph (one line per click)",
2174
+ checked: !!animation?.byParagraph,
2175
+ onCheckedChange: (v) => set({ byParagraph: v })
2176
+ }
2177
+ ),
2178
+ /* @__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.` })
2179
+ ] })
2180
+ ] });
2181
+ }
1686
2182
  function StyleSection({ element, onPatch }) {
1687
2183
  switch (element.type) {
1688
2184
  case "text":
@@ -2358,7 +2854,9 @@ function DeckEditor({
2358
2854
  },
2359
2855
  onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2360
2856
  onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2361
- onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
2857
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background),
2858
+ onSetAnimation: (animation) => slide && elementIdSelected && ops.setAnimation(slide.id, elementIdSelected, animation),
2859
+ onSetElementAnimation: (eid, animation) => slide && ops.setAnimation(slide.id, eid, animation)
2362
2860
  }
2363
2861
  ) })
2364
2862
  ] }),
@@ -2368,6 +2866,6 @@ function DeckEditor({
2368
2866
  );
2369
2867
  }
2370
2868
 
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 };
2869
+ export { DeckEditor, EditorToolbar, ElementInspector, ImageElementRenderer, PresenterView, ShapeElementRenderer, Slide, SlideRail, SlideThumbnail, SlideViewer, SpeakerNotes, TextElementRenderer, buildSteps, buildsForStep, builtinThemes, chartStarterOption, collectBuilds, darkTheme, deckId, defaultTheme, defineTheme, elementId, isByParagraph, nextId, paragraphReveals, reduce as reduceDeck, resolveTheme, slideId, splitParagraphs, stepDelays, totalBuildSteps, useDeckState, useSlideKeyboard, visibleElementIds, vividTheme };
2372
2870
  //# sourceMappingURL=index.js.map
2373
2871
  //# sourceMappingURL=index.js.map