@particle-academy/fancy-slides 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,
@@ -159,6 +232,22 @@ function weight(w) {
159
232
  return void 0;
160
233
  }
161
234
  function ImageElementRenderer({ element }) {
235
+ const crop = element.crop;
236
+ const fit = element.fit ?? "contain";
237
+ if (crop && crop.w > 0 && crop.h > 0) {
238
+ const inner = {
239
+ position: "absolute",
240
+ left: 0,
241
+ top: 0,
242
+ width: `${1 / crop.w * 100}%`,
243
+ height: `${1 / crop.h * 100}%`,
244
+ transform: `translate(${-crop.x / crop.w * 100}%, ${-crop.y / crop.h * 100}%)`,
245
+ transformOrigin: "top left",
246
+ objectFit: fit,
247
+ display: "block"
248
+ };
249
+ return /* @__PURE__ */ jsx("div", { style: { position: "relative", width: "100%", height: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsx("img", { src: element.src, alt: element.alt ?? "", style: inner, draggable: false }) });
250
+ }
162
251
  return /* @__PURE__ */ jsx(
163
252
  "img",
164
253
  {
@@ -167,7 +256,7 @@ function ImageElementRenderer({ element }) {
167
256
  style: {
168
257
  width: "100%",
169
258
  height: "100%",
170
- objectFit: element.fit ?? "contain",
259
+ objectFit: fit,
171
260
  display: "block"
172
261
  },
173
262
  draggable: false
@@ -222,12 +311,94 @@ function renderShape(el, s) {
222
311
  return null;
223
312
  }
224
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
+ `;
225
395
  function Slide({
226
396
  slide,
227
397
  theme,
228
398
  width,
229
399
  aspectRatio,
230
400
  editing = false,
401
+ buildStep,
231
402
  onElementContentChange,
232
403
  onElementSelect,
233
404
  selectedElementId,
@@ -271,7 +442,21 @@ function Slide({
271
442
  }),
272
443
  [t, effectiveBg, slideWidthPx]
273
444
  );
274
- 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(
275
460
  "div",
276
461
  {
277
462
  ref,
@@ -289,23 +474,45 @@ function Slide({
289
474
  onClick: (e) => {
290
475
  if (e.target === e.currentTarget && onElementSelect) onElementSelect(null);
291
476
  },
292
- children: orderedElements(slide.elements).map((element) => /* @__PURE__ */ jsx(
293
- SlideElementHost,
294
- {
295
- element,
296
- theme: t,
297
- slideWidthPx,
298
- slideHeightPx,
299
- editing,
300
- selected: selectedElementId === element.id,
301
- onContentChange: onElementContentChange,
302
- onSelect: onElementSelect,
303
- onMove: onElementMove,
304
- onResize: onElementResize,
305
- renderElement
306
- },
307
- element.id
308
- ))
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
+ ]
309
516
  }
310
517
  ) });
311
518
  }
@@ -322,7 +529,9 @@ function SlideElementHost({
322
529
  onSelect,
323
530
  onMove,
324
531
  onResize,
325
- renderElement
532
+ renderElement,
533
+ buildAnimation,
534
+ buildDelay = 0
326
535
  }) {
327
536
  const dragRef = useRef(null);
328
537
  if (element.hidden) return null;
@@ -391,15 +600,18 @@ function SlideElementHost({
391
600
  outline: selected ? "2px solid #8b5cf6" : void 0,
392
601
  outlineOffset: selected ? 2 : void 0,
393
602
  cursor: canMove ? "move" : interactive ? "pointer" : "default",
394
- touchAction: canMove ? "none" : void 0
603
+ touchAction: canMove ? "none" : void 0,
604
+ ...buildAnimation ? buildEnterStyle(buildAnimation, buildDelay) : null
395
605
  };
396
606
  const inner = renderInner({ element, theme, slideWidthPx, editing, selected, onContentChange }) ?? renderElement?.(element, slideWidthPx);
397
607
  return /* @__PURE__ */ jsxs(
398
608
  "div",
399
609
  {
610
+ className: buildAnimation ? "fs-build-enter" : void 0,
400
611
  style: box,
401
612
  "data-fancy-slides-element": element.id,
402
613
  "data-fancy-slides-element-type": element.type,
614
+ "data-fancy-slides-build": buildAnimation ? "" : void 0,
403
615
  onPointerDown: canMove ? startDrag("move") : void 0,
404
616
  onPointerMove: canMove ? onPointerMove : void 0,
405
617
  onPointerUp: canMove ? endDrag : void 0,
@@ -521,6 +733,8 @@ function useSlideKeyboard({
521
733
  total,
522
734
  index,
523
735
  goTo,
736
+ onAdvance,
737
+ onRetreat,
524
738
  onExit,
525
739
  onBlank,
526
740
  onFullscreen,
@@ -537,13 +751,15 @@ function useSlideKeyboard({
537
751
  case "ArrowLeft":
538
752
  case "PageUp":
539
753
  e.preventDefault();
540
- if (index > 0) goTo(index - 1);
754
+ if (onRetreat) onRetreat();
755
+ else if (index > 0) goTo(index - 1);
541
756
  return;
542
757
  case "ArrowRight":
543
758
  case "PageDown":
544
759
  case " ":
545
760
  e.preventDefault();
546
- if (index < total - 1) goTo(index + 1);
761
+ if (onAdvance) onAdvance();
762
+ else if (index < total - 1) goTo(index + 1);
547
763
  return;
548
764
  case "Home":
549
765
  e.preventDefault();
@@ -585,7 +801,7 @@ function useSlideKeyboard({
585
801
  };
586
802
  window.addEventListener("keydown", handler);
587
803
  return () => window.removeEventListener("keydown", handler);
588
- }, [enabled, index, total, goTo, onExit, onBlank, onFullscreen]);
804
+ }, [enabled, index, total, goTo, onAdvance, onRetreat, onExit, onBlank, onFullscreen]);
589
805
  }
590
806
  function SlideViewer({
591
807
  deck,
@@ -613,13 +829,34 @@ function SlideViewer({
613
829
  const containerRef = useRef(null);
614
830
  const prevIndexRef = useRef(index);
615
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);
616
836
  useEffect(() => {
837
+ if (index === prevIndexRef.current) return;
617
838
  prevIndexRef.current = index;
618
- }, [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]);
619
854
  useSlideKeyboard({
620
855
  total: deck.slides.length,
621
856
  index,
622
857
  goTo,
858
+ onAdvance: advance,
859
+ onRetreat: retreat,
623
860
  onExit,
624
861
  onBlank: () => setBlanked((b) => !b),
625
862
  onFullscreen: () => {
@@ -632,11 +869,15 @@ function SlideViewer({
632
869
  useEffect(() => {
633
870
  if (!autoAdvanceMs || deck.slides.length <= 1) return;
634
871
  const t = setTimeout(() => {
635
- 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
+ }
636
878
  }, autoAdvanceMs);
637
879
  return () => clearTimeout(t);
638
- }, [autoAdvanceMs, index, deck.slides.length, goTo]);
639
- const slide = deck.slides[index];
880
+ }, [autoAdvanceMs, index, deck.slides.length, goTo, buildStep, totalSteps]);
640
881
  const theme = resolveTheme(deck.theme);
641
882
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
642
883
  const transition = slide?.transition ?? theme.defaultTransition;
@@ -658,6 +899,11 @@ function SlideViewer({
658
899
  },
659
900
  tabIndex: 0,
660
901
  "data-fancy-slides-viewer": deck.id,
902
+ "data-fancy-slides-build-step": buildStep,
903
+ onClick: () => {
904
+ if (blanked) return;
905
+ advance();
906
+ },
661
907
  children: [
662
908
  /* @__PURE__ */ jsx("style", { children: TRANSITION_KEYFRAMES }),
663
909
  !blanked && slide && /* @__PURE__ */ jsx(
@@ -671,7 +917,7 @@ function SlideViewer({
671
917
  ["--fs-ratio"]: aspectRatio.toString(),
672
918
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
673
919
  },
674
- 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)
675
921
  }
676
922
  ),
677
923
  !hideChrome && !blanked && /* @__PURE__ */ jsxs(
@@ -702,7 +948,7 @@ function SlideViewer({
702
948
  );
703
949
  }
704
950
  var DEFAULT_DURATION = 400;
705
- var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
951
+ var EASE2 = "cubic-bezier(0.16, 1, 0.3, 1)";
706
952
  function transitionEnterStyle(transition, forward) {
707
953
  const kind = transition?.kind ?? "none";
708
954
  if (kind === "none") return { width: "100%", height: "100%" };
@@ -728,7 +974,7 @@ function transitionEnterStyle(transition, forward) {
728
974
  height: "100%",
729
975
  animationName: name,
730
976
  animationDuration: `${duration}ms`,
731
- animationTimingFunction: EASE,
977
+ animationTimingFunction: EASE2,
732
978
  animationFillMode: "both"
733
979
  };
734
980
  }
@@ -784,14 +1030,38 @@ function PresenterView({
784
1030
  },
785
1031
  [deck.slides.length, isControlled, onIndexChange]
786
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]);
787
1056
  useSlideKeyboard({
788
1057
  total: deck.slides.length,
789
1058
  index,
790
1059
  goTo,
1060
+ onAdvance: advance,
1061
+ onRetreat: retreat,
791
1062
  onExit
792
1063
  });
793
1064
  const theme = resolveTheme(deck.theme);
794
- const slide = deck.slides[index];
795
1065
  const nextSlide = deck.slides[index + 1];
796
1066
  const [now, setNow] = useState(() => Date.now());
797
1067
  useEffect(() => {
@@ -838,7 +1108,7 @@ function PresenterView({
838
1108
  borderRadius: 8,
839
1109
  overflow: "hidden"
840
1110
  },
841
- children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) : null
1111
+ children: slide ? /* @__PURE__ */ jsx(Slide, { slide, theme, buildStep, renderElement }) : null
842
1112
  }
843
1113
  )
844
1114
  }
@@ -942,8 +1212,8 @@ function PresenterView({
942
1212
  /* @__PURE__ */ jsx(StatusChip, { label: "Elapsed", children: formatElapsed(now - startedAtRef) }),
943
1213
  /* @__PURE__ */ jsx(StatusChip, { label: "Clock", children: formatClock(now) }),
944
1214
  /* @__PURE__ */ jsxs("div", { style: { marginLeft: "auto", display: "flex", gap: 8 }, children: [
945
- /* @__PURE__ */ jsx(NavButton, { onClick: () => goTo(index - 1), disabled: index === 0, children: "\u2190 Prev" }),
946
- /* @__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" })
947
1217
  ] })
948
1218
  ]
949
1219
  }
@@ -1126,6 +1396,7 @@ function useDeckState({ value, onChange, onOp }) {
1126
1396
  updateElement: (slideIdArg, elementIdArg, patch) => apply({ kind: "element_update", slideId: slideIdArg, elementId: elementIdArg, patch }),
1127
1397
  moveElement: (slideIdArg, elementIdArg, x, y) => apply({ kind: "element_move", slideId: slideIdArg, elementId: elementIdArg, x, y }),
1128
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 }),
1129
1400
  getSlide: (id) => value.slides.find((s) => s.id === id),
1130
1401
  getElement: (slideIdArg, elementIdArg) => value.slides.find((s) => s.id === slideIdArg)?.elements.find((e) => e.id === elementIdArg)
1131
1402
  };
@@ -1193,6 +1464,23 @@ function reduce(deck, op) {
1193
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
1194
1465
  )
1195
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
+ };
1196
1484
  }
1197
1485
  }
1198
1486
 
@@ -1279,6 +1567,113 @@ function chartStarterOption(kind) {
1279
1567
  };
1280
1568
  }
1281
1569
  }
1570
+ var CHART_PALETTE = [
1571
+ "#8b5cf6",
1572
+ "#3b82f6",
1573
+ "#10b981",
1574
+ "#f59e0b",
1575
+ "#ef4444",
1576
+ "#ec4899",
1577
+ "#14b8a6",
1578
+ "#6366f1"
1579
+ ];
1580
+ function chartColorAt(index) {
1581
+ return CHART_PALETTE[index % CHART_PALETTE.length];
1582
+ }
1583
+ function isPlainObject(v) {
1584
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1585
+ }
1586
+ function toNumber(v) {
1587
+ const n = typeof v === "number" ? v : parseFloat(String(v));
1588
+ return Number.isFinite(n) ? n : 0;
1589
+ }
1590
+ function chartModelFromOption(option) {
1591
+ if (!isPlainObject(option)) return null;
1592
+ const seriesRaw = option.series;
1593
+ if (!Array.isArray(seriesRaw) || seriesRaw.length === 0) return null;
1594
+ if (!seriesRaw.every(isPlainObject)) return null;
1595
+ const types = seriesRaw.map((s) => String(s.type ?? ""));
1596
+ if (types[0] === "pie") {
1597
+ if (seriesRaw.length !== 1) return null;
1598
+ const data = seriesRaw[0].data;
1599
+ if (!Array.isArray(data)) return null;
1600
+ const slices = [];
1601
+ for (const d of data) {
1602
+ if (!isPlainObject(d)) return null;
1603
+ slices.push({ name: String(d.name ?? ""), value: toNumber(d.value) });
1604
+ }
1605
+ return { kind: "pie", categories: [], series: [], slices };
1606
+ }
1607
+ const cartesian = /* @__PURE__ */ new Set(["bar", "line", "scatter"]);
1608
+ if (!types.every((t) => cartesian.has(t))) return null;
1609
+ if (new Set(types).size !== 1) return null;
1610
+ const baseType = types[0];
1611
+ const isArea = baseType === "line" && seriesRaw.every((s) => isPlainObject(s.areaStyle) || s.areaStyle != null);
1612
+ const kind = baseType === "line" ? isArea ? "area" : "line" : baseType;
1613
+ const xAxis = option.xAxis;
1614
+ const axisData = isPlainObject(xAxis) ? xAxis.data : void 0;
1615
+ const categories = Array.isArray(axisData) ? axisData.map((c) => String(c)) : [];
1616
+ const firstData = seriesRaw[0].data;
1617
+ const valueCount = Array.isArray(firstData) ? firstData.length : 0;
1618
+ const cats = categories.length > 0 ? categories : Array.from({ length: valueCount }, (_, i) => String(i + 1));
1619
+ const series = [];
1620
+ for (const s of seriesRaw) {
1621
+ const data = s.data;
1622
+ if (!Array.isArray(data)) return null;
1623
+ const values = data.map((d) => {
1624
+ if (Array.isArray(d)) return toNumber(d[1]);
1625
+ if (isPlainObject(d)) return toNumber(d.value);
1626
+ return toNumber(d);
1627
+ });
1628
+ series.push({ name: String(s.name ?? "Series"), color: typeof s.itemStyle === "object" && s.itemStyle && isPlainObject(s.itemStyle) ? typeof s.itemStyle.color === "string" ? s.itemStyle.color : void 0 : typeof s.color === "string" ? s.color : void 0, values });
1629
+ }
1630
+ return { kind, categories: cats, series, slices: [] };
1631
+ }
1632
+ function chartOptionFromModel(model) {
1633
+ if (model.kind === "pie") {
1634
+ return {
1635
+ tooltip: { trigger: "item" },
1636
+ legend: { bottom: 0 },
1637
+ color: model.slices.map((_, i) => chartColorAt(i)),
1638
+ series: [
1639
+ {
1640
+ type: "pie",
1641
+ radius: ["40%", "70%"],
1642
+ name: "Segment",
1643
+ data: model.slices.map((s) => ({ name: s.name, value: s.value }))
1644
+ }
1645
+ ]
1646
+ };
1647
+ }
1648
+ const isScatter = model.kind === "scatter";
1649
+ const isArea = model.kind === "area";
1650
+ const seriesType = model.kind === "bar" ? "bar" : model.kind === "scatter" ? "scatter" : "line";
1651
+ const series = model.series.map((s, i) => {
1652
+ const color = s.color ?? chartColorAt(i);
1653
+ const base = {
1654
+ type: seriesType,
1655
+ name: s.name,
1656
+ itemStyle: { color }
1657
+ };
1658
+ if (isScatter) {
1659
+ base.symbolSize = 12;
1660
+ base.data = s.values.map((v, idx) => [idx, v]);
1661
+ } else {
1662
+ base.data = s.values;
1663
+ }
1664
+ if (seriesType === "line") base.smooth = true;
1665
+ if (isArea) base.areaStyle = { color };
1666
+ return base;
1667
+ });
1668
+ return {
1669
+ grid: { top: 24, left: 56, right: 16, bottom: isScatter ? 32 : 40 },
1670
+ tooltip: { trigger: isScatter ? "item" : "axis" },
1671
+ legend: model.series.length > 1 ? { bottom: 0 } : void 0,
1672
+ xAxis: isScatter ? { type: "value" } : { type: "category", data: [...model.categories] },
1673
+ yAxis: { type: "value" },
1674
+ series
1675
+ };
1676
+ }
1282
1677
  function SlideRail({
1283
1678
  slides,
1284
1679
  selectedId,
@@ -1427,10 +1822,10 @@ function EditorToolbar({
1427
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" }) }) })
1428
1823
  ] });
1429
1824
  }
1430
- function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1825
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground, onSetAnimation, onSetElementAnimation }) {
1431
1826
  if (!element) {
1432
1827
  if (slide) {
1433
- return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1828
+ return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground, onSetElementAnimation });
1434
1829
  }
1435
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: [
1436
1831
  /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
@@ -1455,10 +1850,12 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1455
1850
  /* @__PURE__ */ jsxs(Tabs.List, { children: [
1456
1851
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "style", children: "Style" }),
1457
1852
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "layout", children: "Layout" }),
1853
+ /* @__PURE__ */ jsx(Tabs.Tab, { value: "build", children: "Build" }),
1458
1854
  /* @__PURE__ */ jsx(Tabs.Tab, { value: "advanced", children: "Advanced" })
1459
1855
  ] }),
1460
1856
  /* @__PURE__ */ jsxs(Tabs.Panels, { children: [
1461
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 }) }) }),
1462
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 }) }) }),
1463
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 }) }) })
1464
1861
  ] })
@@ -1468,7 +1865,8 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onS
1468
1865
  function SlideSettings({
1469
1866
  slide,
1470
1867
  onSetTransition,
1471
- onSetBackground
1868
+ onSetBackground,
1869
+ onSetElementAnimation
1472
1870
  }) {
1473
1871
  const transition = slide.transition;
1474
1872
  const kind = transition?.kind ?? "none";
@@ -1535,10 +1933,54 @@ function SlideSettings({
1535
1933
  onChange: (c) => onSetBackground({ ...slide.background, color: c })
1536
1934
  }
1537
1935
  ) })
1538
- ] }) })
1936
+ ] }) }),
1937
+ onSetElementAnimation && /* @__PURE__ */ jsx(Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsx(BuildOrderList, { slide, onSetElementAnimation }) })
1539
1938
  ] })
1540
1939
  ] });
1541
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
+ }
1542
1984
  function LayoutSection({ element, onPatch }) {
1543
1985
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1544
1986
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1560,6 +2002,100 @@ function AdvancedSection({ element, onPatch }) {
1560
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" }) })
1561
2003
  ] });
1562
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
+ }
1563
2099
  function StyleSection({ element, onPatch }) {
1564
2100
  switch (element.type) {
1565
2101
  case "text":
@@ -1632,7 +2168,35 @@ function TextStyleControls({ element, onPatch }) {
1632
2168
  ] });
1633
2169
  }
1634
2170
  function ImageStyleControls({ element, onPatch }) {
2171
+ const fileRef = useRef(null);
2172
+ const crop = element.crop;
2173
+ const onFile = (file) => {
2174
+ if (!file) return;
2175
+ const reader = new FileReader();
2176
+ reader.onload = () => {
2177
+ if (typeof reader.result === "string") onPatch({ src: reader.result });
2178
+ };
2179
+ reader.readAsDataURL(file);
2180
+ };
2181
+ const setCrop = (next) => {
2182
+ const base = crop ?? { x: 0, y: 0, w: 1, h: 1 };
2183
+ onPatch({ crop: { ...base, ...next } });
2184
+ };
1635
2185
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2186
+ /* @__PURE__ */ jsx(
2187
+ "input",
2188
+ {
2189
+ ref: fileRef,
2190
+ type: "file",
2191
+ accept: "image/*",
2192
+ className: "hidden",
2193
+ onChange: (e) => {
2194
+ onFile(e.target.files?.[0]);
2195
+ e.target.value = "";
2196
+ }
2197
+ }
2198
+ ),
2199
+ /* @__PURE__ */ jsx(Action, { size: "sm", variant: "ghost", icon: "upload", onClick: () => fileRef.current?.click(), children: "Upload image" }),
1636
2200
  /* @__PURE__ */ jsx(Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1637
2201
  /* @__PURE__ */ jsx(Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1638
2202
  /* @__PURE__ */ jsx(
@@ -1648,7 +2212,17 @@ function ImageStyleControls({ element, onPatch }) {
1648
2212
  value: element.fit ?? "contain",
1649
2213
  onValueChange: (v) => onPatch({ fit: v })
1650
2214
  }
1651
- )
2215
+ ),
2216
+ /* @__PURE__ */ jsx(Separator, {}),
2217
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
2218
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Crop" }),
2219
+ crop && /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", onClick: () => onPatch({ crop: void 0 }), children: "Clear crop" })
2220
+ ] }),
2221
+ /* @__PURE__ */ jsx(Slider, { label: "X", value: crop?.x ?? 0, onValueChange: (v) => setCrop({ x: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
2222
+ /* @__PURE__ */ jsx(Slider, { label: "Y", value: crop?.y ?? 0, onValueChange: (v) => setCrop({ y: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
2223
+ /* @__PURE__ */ jsx(Slider, { label: "Width", value: crop?.w ?? 1, onValueChange: (v) => setCrop({ w: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
2224
+ /* @__PURE__ */ jsx(Slider, { label: "Height", value: crop?.h ?? 1, onValueChange: (v) => setCrop({ h: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
2225
+ /* @__PURE__ */ jsx(Text, { size: "xs", className: "!text-zinc-500", children: "Crop is a window into the source image (0..1). Width/height shrink the visible region; X/Y pan it." })
1652
2226
  ] });
1653
2227
  }
1654
2228
  function ShapeStyleControls({ element, onPatch }) {
@@ -1695,54 +2269,254 @@ function CodeStyleControls({ element, onPatch }) {
1695
2269
  ] });
1696
2270
  }
1697
2271
  function ChartStyleControls({ element, onPatch }) {
2272
+ const model = chartModelFromOption(element.option);
2273
+ const writeModel = (m) => onPatch({ option: chartOptionFromModel(m) });
2274
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2275
+ model ? /* @__PURE__ */ jsx(ChartModelEditor, { model, onChange: writeModel }) : /* @__PURE__ */ jsx(Text, { size: "sm", className: "rounded-md bg-amber-50 p-2 !text-amber-700 dark:bg-amber-950/40 dark:!text-amber-400", children: "This chart's option is too custom for the visual editor. Edit it as JSON below." }),
2276
+ /* @__PURE__ */ jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2277
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: "Advanced \u2014 edit option JSON" }),
2278
+ /* @__PURE__ */ jsx("div", { className: "p-2 pt-0", children: /* @__PURE__ */ jsx(
2279
+ Textarea,
2280
+ {
2281
+ label: "ECharts option (JSON)",
2282
+ value: JSON.stringify(element.option, null, 2),
2283
+ onValueChange: (v) => {
2284
+ try {
2285
+ onPatch({ option: JSON.parse(v) });
2286
+ } catch {
2287
+ }
2288
+ },
2289
+ rows: 10
2290
+ }
2291
+ ) })
2292
+ ] })
2293
+ ] });
2294
+ }
2295
+ var CHART_TYPE_OPTIONS = [
2296
+ { value: "bar", label: "Bar" },
2297
+ { value: "line", label: "Line" },
2298
+ { value: "area", label: "Area" },
2299
+ { value: "pie", label: "Pie" },
2300
+ { value: "scatter", label: "Scatter" }
2301
+ ];
2302
+ function ChartModelEditor({ model, onChange }) {
2303
+ const setKind = (kind) => {
2304
+ if (kind === model.kind) return;
2305
+ if (kind === "pie") {
2306
+ const first = model.series[0];
2307
+ const slices = model.slices.length ? model.slices : model.categories.length ? model.categories.map((name, i) => ({ name, value: first?.values[i] ?? 0 })) : [{ name: "Slice 1", value: 1 }];
2308
+ onChange({ ...model, kind, slices });
2309
+ return;
2310
+ }
2311
+ if (model.kind === "pie") {
2312
+ const categories = model.slices.length ? model.slices.map((s) => s.name) : ["A", "B", "C"];
2313
+ const values = model.slices.length ? model.slices.map((s) => s.value) : [1, 2, 3];
2314
+ onChange({ ...model, kind, categories, series: [{ name: "Series 1", color: chartColorAt(0), values }] });
2315
+ return;
2316
+ }
2317
+ onChange({ ...model, kind });
2318
+ };
1698
2319
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1699
- /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "Chart option is JSON \u2014 paste any ECharts option here." }),
1700
2320
  /* @__PURE__ */ jsx(
1701
- Textarea,
2321
+ Select,
1702
2322
  {
1703
- label: "ECharts option (JSON)",
1704
- value: JSON.stringify(element.option, null, 2),
1705
- onValueChange: (v) => {
1706
- try {
1707
- onPatch({ option: JSON.parse(v) });
1708
- } catch {
1709
- }
1710
- },
1711
- rows: 10
2323
+ label: "Chart type",
2324
+ list: CHART_TYPE_OPTIONS,
2325
+ value: model.kind,
2326
+ onValueChange: (v) => setKind(v)
1712
2327
  }
1713
- )
2328
+ ),
2329
+ model.kind === "pie" ? /* @__PURE__ */ jsx(PieSliceEditor, { model, onChange }) : /* @__PURE__ */ jsx(CartesianChartEditor, { model, onChange })
2330
+ ] });
2331
+ }
2332
+ function PieSliceEditor({ model, onChange }) {
2333
+ const slices = model.slices;
2334
+ const update = (i, next) => {
2335
+ const copy = slices.map((s, idx) => idx === i ? { ...s, ...next } : s);
2336
+ onChange({ ...model, slices: copy });
2337
+ };
2338
+ const remove = (i) => onChange({ ...model, slices: slices.filter((_, idx) => idx !== i) });
2339
+ const add = () => onChange({ ...model, slices: [...slices, { name: `Slice ${slices.length + 1}`, value: 0 }] });
2340
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2341
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Slices" }),
2342
+ slices.map((s, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
2343
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { label: i === 0 ? "Name" : void 0, value: s.name, onChange: (e) => update(i, { name: e.target.value }) }) }),
2344
+ /* @__PURE__ */ jsx("div", { className: "w-20", children: /* @__PURE__ */ jsx(Input, { label: i === 0 ? "Value" : void 0, type: "number", value: String(s.value), onChange: (e) => update(i, { value: parseFloat(e.target.value) || 0 }) }) }),
2345
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => remove(i), "aria-label": "Remove slice" })
2346
+ ] }, i)),
2347
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: add, children: "Add slice" })
2348
+ ] });
2349
+ }
2350
+ function CartesianChartEditor({ model, onChange }) {
2351
+ const { categories, series } = model;
2352
+ const updateCategory = (i, label) => {
2353
+ onChange({ ...model, categories: categories.map((c, idx) => idx === i ? label : c) });
2354
+ };
2355
+ const removeCategory = (i) => {
2356
+ onChange({
2357
+ ...model,
2358
+ categories: categories.filter((_, idx) => idx !== i),
2359
+ series: series.map((s) => ({ ...s, values: s.values.filter((_, idx) => idx !== i) }))
2360
+ });
2361
+ };
2362
+ const addCategory = () => {
2363
+ onChange({
2364
+ ...model,
2365
+ categories: [...categories, `Cat ${categories.length + 1}`],
2366
+ series: series.map((s) => ({ ...s, values: [...s.values, 0] }))
2367
+ });
2368
+ };
2369
+ const updateSeries = (si, next) => {
2370
+ onChange({ ...model, series: series.map((s, idx) => idx === si ? { ...s, ...next } : s) });
2371
+ };
2372
+ const updateValue = (si, ci, value) => {
2373
+ onChange({
2374
+ ...model,
2375
+ series: series.map(
2376
+ (s, idx) => idx === si ? { ...s, values: s.values.map((v, vi) => vi === ci ? value : v) } : s
2377
+ )
2378
+ });
2379
+ };
2380
+ const removeSeries = (si) => onChange({ ...model, series: series.filter((_, idx) => idx !== si) });
2381
+ const addSeries = () => onChange({
2382
+ ...model,
2383
+ series: [
2384
+ ...series,
2385
+ { name: `Series ${series.length + 1}`, color: chartColorAt(series.length), values: categories.map(() => 0) }
2386
+ ]
2387
+ });
2388
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2389
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2390
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Categories" }),
2391
+ categories.map((c, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2392
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { value: c, onChange: (e) => updateCategory(i, e.target.value) }) }),
2393
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeCategory(i), "aria-label": "Remove category" })
2394
+ ] }, i)),
2395
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addCategory, children: "Add category" })
2396
+ ] }),
2397
+ /* @__PURE__ */ jsx(Separator, {}),
2398
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
2399
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Series" }),
2400
+ series.map((s, si) => /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-md border border-zinc-200 p-2 dark:border-zinc-800", children: [
2401
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
2402
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { label: "Name", value: s.name, onChange: (e) => updateSeries(si, { name: e.target.value }) }) }),
2403
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsx(ColorPicker, { value: s.color ?? chartColorAt(si), onChange: (c) => updateSeries(si, { color: c }) }) }),
2404
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeSeries(si), "aria-label": "Remove series" })
2405
+ ] }),
2406
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: categories.map((c, ci) => /* @__PURE__ */ jsx(
2407
+ Input,
2408
+ {
2409
+ label: c,
2410
+ type: "number",
2411
+ value: String(s.values[ci] ?? 0),
2412
+ onChange: (e) => updateValue(si, ci, parseFloat(e.target.value) || 0)
2413
+ },
2414
+ ci
2415
+ )) })
2416
+ ] }, si)),
2417
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addSeries, children: "Add series" })
2418
+ ] })
1714
2419
  ] });
1715
2420
  }
1716
2421
  function TableStyleControls({ element, onPatch }) {
2422
+ const columns = element.columns;
2423
+ const rows = element.rows;
2424
+ const nextColKey = () => {
2425
+ const existing = new Set(columns.map((c) => c.key));
2426
+ let n = columns.length + 1;
2427
+ while (existing.has(`col${n}`)) n++;
2428
+ return `col${n}`;
2429
+ };
2430
+ const setColumnLabel = (i, label) => {
2431
+ onPatch({ columns: columns.map((c, idx) => idx === i ? { ...c, label } : c) });
2432
+ };
2433
+ const removeColumn = (i) => {
2434
+ const key = columns[i]?.key;
2435
+ const nextCols = columns.filter((_, idx) => idx !== i);
2436
+ const nextRows = key ? rows.map((r) => {
2437
+ const { [key]: _drop, ...rest } = r;
2438
+ return rest;
2439
+ }) : rows;
2440
+ onPatch({ columns: nextCols, rows: nextRows });
2441
+ };
2442
+ const addColumn = () => {
2443
+ const key = nextColKey();
2444
+ onPatch({
2445
+ columns: [...columns, { key, label: `Column ${columns.length + 1}` }],
2446
+ rows: rows.map((r) => ({ ...r, [key]: "" }))
2447
+ });
2448
+ };
2449
+ const setCell = (rowIdx, key, value) => {
2450
+ onPatch({ rows: rows.map((r, idx) => idx === rowIdx ? { ...r, [key]: value } : r) });
2451
+ };
2452
+ const removeRow = (rowIdx) => onPatch({ rows: rows.filter((_, idx) => idx !== rowIdx) });
2453
+ const addRow = () => {
2454
+ const blank = {};
2455
+ for (const c of columns) blank[c.key] = "";
2456
+ onPatch({ rows: [...rows, blank] });
2457
+ };
1717
2458
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1718
- /* @__PURE__ */ jsx(
1719
- Textarea,
1720
- {
1721
- label: "Columns (JSON)",
1722
- value: JSON.stringify(element.columns, null, 2),
1723
- onValueChange: (v) => {
1724
- try {
1725
- onPatch({ columns: JSON.parse(v) });
1726
- } catch {
2459
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2460
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Columns" }),
2461
+ columns.map((c, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2462
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { value: c.label, onChange: (e) => setColumnLabel(i, e.target.value), "aria-label": `Column ${i + 1} label` }) }),
2463
+ /* @__PURE__ */ jsx(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: c.key }),
2464
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeColumn(i), "aria-label": "Remove column" })
2465
+ ] }, c.key)),
2466
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addColumn, children: "Add column" })
2467
+ ] }),
2468
+ /* @__PURE__ */ jsx(Separator, {}),
2469
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2470
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Rows" }),
2471
+ columns.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", className: "!text-zinc-500", children: "Add a column to start adding rows." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2472
+ rows.map((r, rowIdx) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2 border-b border-zinc-100 pb-2 dark:border-zinc-800", children: [
2473
+ /* @__PURE__ */ jsx("div", { className: "grid flex-1 grid-cols-1 gap-1", children: columns.map((c) => /* @__PURE__ */ jsx(
2474
+ Input,
2475
+ {
2476
+ label: c.label,
2477
+ value: r[c.key] == null ? "" : String(r[c.key]),
2478
+ onChange: (e) => setCell(rowIdx, c.key, e.target.value)
2479
+ },
2480
+ c.key
2481
+ )) }),
2482
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeRow(rowIdx), "aria-label": "Remove row" })
2483
+ ] }, rowIdx)),
2484
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addRow, children: "Add row" })
2485
+ ] })
2486
+ ] }),
2487
+ /* @__PURE__ */ jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2488
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer select-none px-2 py-1.5 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: "Edit as JSON" }),
2489
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 p-2 pt-0", children: [
2490
+ /* @__PURE__ */ jsx(
2491
+ Textarea,
2492
+ {
2493
+ label: "Columns (JSON)",
2494
+ value: JSON.stringify(columns, null, 2),
2495
+ onValueChange: (v) => {
2496
+ try {
2497
+ onPatch({ columns: JSON.parse(v) });
2498
+ } catch {
2499
+ }
2500
+ },
2501
+ rows: 5
1727
2502
  }
1728
- },
1729
- rows: 5
1730
- }
1731
- ),
1732
- /* @__PURE__ */ jsx(
1733
- Textarea,
1734
- {
1735
- label: "Rows (JSON)",
1736
- value: JSON.stringify(element.rows, null, 2),
1737
- onValueChange: (v) => {
1738
- try {
1739
- onPatch({ rows: JSON.parse(v) });
1740
- } catch {
2503
+ ),
2504
+ /* @__PURE__ */ jsx(
2505
+ Textarea,
2506
+ {
2507
+ label: "Rows (JSON)",
2508
+ value: JSON.stringify(rows, null, 2),
2509
+ onValueChange: (v) => {
2510
+ try {
2511
+ onPatch({ rows: JSON.parse(v) });
2512
+ } catch {
2513
+ }
2514
+ },
2515
+ rows: 8
1741
2516
  }
1742
- },
1743
- rows: 8
1744
- }
1745
- )
2517
+ )
2518
+ ] })
2519
+ ] })
1746
2520
  ] });
1747
2521
  }
1748
2522
  function EmbedStyleControls({ element, onPatch }) {
@@ -1997,7 +2771,9 @@ function DeckEditor({
1997
2771
  },
1998
2772
  onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
1999
2773
  onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2000
- 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)
2001
2777
  }
2002
2778
  ) })
2003
2779
  ] }),
@@ -2007,6 +2783,6 @@ function DeckEditor({
2007
2783
  );
2008
2784
  }
2009
2785
 
2010
- 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 };
2011
2787
  //# sourceMappingURL=index.js.map
2012
2788
  //# sourceMappingURL=index.js.map