@particle-academy/fancy-slides 0.1.6 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -653,6 +653,11 @@ function SlideViewer({
653
653
  );
654
654
  const [blanked, setBlanked] = react.useState(false);
655
655
  const containerRef = react.useRef(null);
656
+ const prevIndexRef = react.useRef(index);
657
+ const forward = index >= prevIndexRef.current;
658
+ react.useEffect(() => {
659
+ prevIndexRef.current = index;
660
+ }, [index]);
656
661
  useSlideKeyboard({
657
662
  total: deck.slides.length,
658
663
  index,
@@ -676,6 +681,8 @@ function SlideViewer({
676
681
  const slide = deck.slides[index];
677
682
  const theme = resolveTheme(deck.theme);
678
683
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
684
+ const transition = slide?.transition ?? theme.defaultTransition;
685
+ const enterStyle = transitionEnterStyle(transition, forward);
679
686
  return /* @__PURE__ */ jsxRuntime.jsxs(
680
687
  "div",
681
688
  {
@@ -694,6 +701,7 @@ function SlideViewer({
694
701
  tabIndex: 0,
695
702
  "data-fancy-slides-viewer": deck.id,
696
703
  children: [
704
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: TRANSITION_KEYFRAMES }),
697
705
  !blanked && slide && /* @__PURE__ */ jsxRuntime.jsx(
698
706
  "div",
699
707
  {
@@ -705,7 +713,7 @@ function SlideViewer({
705
713
  ["--fs-ratio"]: aspectRatio.toString(),
706
714
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
707
715
  },
708
- children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement })
716
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement }) }, index)
709
717
  }
710
718
  ),
711
719
  !hideChrome && !blanked && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -735,6 +743,68 @@ function SlideViewer({
735
743
  }
736
744
  );
737
745
  }
746
+ var DEFAULT_DURATION = 400;
747
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
748
+ function transitionEnterStyle(transition, forward) {
749
+ const kind = transition?.kind ?? "none";
750
+ if (kind === "none") return { width: "100%", height: "100%" };
751
+ const duration = transition?.duration ?? DEFAULT_DURATION;
752
+ let name;
753
+ switch (kind) {
754
+ case "fade":
755
+ name = "fs-fade-in";
756
+ break;
757
+ case "zoom":
758
+ name = "fs-zoom-in";
759
+ break;
760
+ case "slide": {
761
+ const dir = transition?.direction ?? (forward ? "right" : "left");
762
+ name = `fs-slide-in-${dir}`;
763
+ break;
764
+ }
765
+ default:
766
+ return { width: "100%", height: "100%" };
767
+ }
768
+ return {
769
+ width: "100%",
770
+ height: "100%",
771
+ animationName: name,
772
+ animationDuration: `${duration}ms`,
773
+ animationTimingFunction: EASE,
774
+ animationFillMode: "both"
775
+ };
776
+ }
777
+ var TRANSITION_KEYFRAMES = `
778
+ @media (prefers-reduced-motion: reduce) {
779
+ .fs-slide-enter { animation: none !important; }
780
+ }
781
+ @media (prefers-reduced-motion: no-preference) {
782
+ @keyframes fs-fade-in {
783
+ from { opacity: 0; }
784
+ to { opacity: 1; }
785
+ }
786
+ @keyframes fs-zoom-in {
787
+ from { opacity: 0; transform: scale(0.92); }
788
+ to { opacity: 1; transform: scale(1); }
789
+ }
790
+ @keyframes fs-slide-in-right {
791
+ from { opacity: 0; transform: translateX(8%); }
792
+ to { opacity: 1; transform: translateX(0); }
793
+ }
794
+ @keyframes fs-slide-in-left {
795
+ from { opacity: 0; transform: translateX(-8%); }
796
+ to { opacity: 1; transform: translateX(0); }
797
+ }
798
+ @keyframes fs-slide-in-up {
799
+ from { opacity: 0; transform: translateY(8%); }
800
+ to { opacity: 1; transform: translateY(0); }
801
+ }
802
+ @keyframes fs-slide-in-down {
803
+ from { opacity: 0; transform: translateY(-8%); }
804
+ to { opacity: 1; transform: translateY(0); }
805
+ }
806
+ }
807
+ `;
738
808
  function PresenterView({
739
809
  deck,
740
810
  index: controlledIndex,
@@ -1088,6 +1158,7 @@ function useDeckState({ value, onChange, onOp }) {
1088
1158
  setLayout: (id, layout) => apply({ kind: "slide_set_layout", id, layout }),
1089
1159
  setNotes: (id, notes) => apply({ kind: "slide_set_notes", id, notes }),
1090
1160
  setBackground: (id, background) => apply({ kind: "slide_set_background", id, background }),
1161
+ setTransition: (id, transition) => apply({ kind: "slide_set_transition", id, transition }),
1091
1162
  addElement: (slideId2, element) => {
1092
1163
  const id = element.id ?? elementId();
1093
1164
  apply({ kind: "element_add", slideId: slideId2, element: { ...element, id } });
@@ -1129,6 +1200,8 @@ function reduce(deck, op) {
1129
1200
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, notes: op.notes } : s) };
1130
1201
  case "slide_set_background":
1131
1202
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, background: op.background } : s) };
1203
+ case "slide_set_transition":
1204
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, transition: op.transition } : s) };
1132
1205
  case "element_add":
1133
1206
  return {
1134
1207
  ...deck,
@@ -1396,8 +1469,11 @@ function EditorToolbar({
1396
1469
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Tooltip, { content: "Present (F)", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { color: "violet", size: "sm", icon: "play", onClick: onPresent, children: "Present" }) }) })
1397
1470
  ] });
1398
1471
  }
1399
- function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1472
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1400
1473
  if (!element) {
1474
+ if (slide) {
1475
+ return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1476
+ }
1401
1477
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fs-inspector flex h-full flex-col border-l border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900", children: [
1402
1478
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
1403
1479
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "mt-2 !text-zinc-500", children: "Select an element to edit its properties." })
@@ -1431,6 +1507,80 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1431
1507
  ] }) })
1432
1508
  ] });
1433
1509
  }
1510
+ function SlideSettings({
1511
+ slide,
1512
+ onSetTransition,
1513
+ onSetBackground
1514
+ }) {
1515
+ const transition = slide.transition;
1516
+ const kind = transition?.kind ?? "none";
1517
+ const setTransition = (next) => {
1518
+ const merged = { kind, duration: transition?.duration, direction: transition?.direction, ...next };
1519
+ onSetTransition?.(merged.kind === "none" ? { kind: "none" } : merged);
1520
+ };
1521
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fs-inspector flex h-full w-full flex-col border-l border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900", children: [
1522
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between border-b border-zinc-200 px-3 py-2 dark:border-zinc-800", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1523
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!font-mono !uppercase !tracking-wider !text-zinc-500", children: "slide" }),
1524
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
1525
+ "#",
1526
+ slide.id.slice(-6)
1527
+ ] })
1528
+ ] }) }),
1529
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-3", children: [
1530
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1531
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Transition" }),
1532
+ /* @__PURE__ */ jsxRuntime.jsx(
1533
+ reactFancy.Select,
1534
+ {
1535
+ label: "Kind",
1536
+ list: [
1537
+ { value: "none", label: "None" },
1538
+ { value: "fade", label: "Fade" },
1539
+ { value: "slide", label: "Slide" },
1540
+ { value: "zoom", label: "Zoom" }
1541
+ ],
1542
+ value: kind,
1543
+ onValueChange: (v) => setTransition({ kind: v })
1544
+ }
1545
+ ),
1546
+ kind === "slide" && /* @__PURE__ */ jsxRuntime.jsx(
1547
+ reactFancy.Select,
1548
+ {
1549
+ label: "Direction",
1550
+ list: [
1551
+ { value: "left", label: "From left" },
1552
+ { value: "right", label: "From right" },
1553
+ { value: "up", label: "From bottom" },
1554
+ { value: "down", label: "From top" }
1555
+ ],
1556
+ value: transition?.direction ?? "right",
1557
+ onValueChange: (v) => setTransition({ direction: v })
1558
+ }
1559
+ ),
1560
+ kind !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
1561
+ reactFancy.Input,
1562
+ {
1563
+ label: "Duration (ms)",
1564
+ type: "number",
1565
+ value: String(transition?.duration ?? 400),
1566
+ onChange: (e) => setTransition({ duration: parseInt(e.target.value, 10) || 400 })
1567
+ }
1568
+ ),
1569
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: "Entrance animation played when this slide appears in the viewer. Falls back to the theme default. Honors prefers-reduced-motion." })
1570
+ ] }) }),
1571
+ onSetBackground && /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1572
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Background" }),
1573
+ /* @__PURE__ */ jsxRuntime.jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(
1574
+ reactFancy.ColorPicker,
1575
+ {
1576
+ value: slide.background?.color ?? "#ffffff",
1577
+ onChange: (c) => onSetBackground({ ...slide.background, color: c })
1578
+ }
1579
+ ) })
1580
+ ] }) })
1581
+ ] })
1582
+ ] });
1583
+ }
1434
1584
  function LayoutSection({ element, onPatch }) {
1435
1585
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1436
1586
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1880,13 +2030,16 @@ function DeckEditor({
1880
2030
  ElementInspector,
1881
2031
  {
1882
2032
  element: selectedElement,
2033
+ slide: slide ?? null,
1883
2034
  onPatch: (patch) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, patch),
1884
2035
  onDelete: () => {
1885
2036
  if (!slide || !elementIdSelected) return;
1886
2037
  ops.removeElement(slide.id, elementIdSelected);
1887
2038
  setElementIdSelected(null);
1888
2039
  },
1889
- onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked })
2040
+ onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2041
+ onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2042
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
1890
2043
  }
1891
2044
  ) })
1892
2045
  ] }),