@particle-academy/fancy-slides 0.1.7 → 0.3.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
@@ -161,6 +161,22 @@ function weight(w) {
161
161
  return void 0;
162
162
  }
163
163
  function ImageElementRenderer({ element }) {
164
+ const crop = element.crop;
165
+ const fit = element.fit ?? "contain";
166
+ if (crop && crop.w > 0 && crop.h > 0) {
167
+ const inner = {
168
+ position: "absolute",
169
+ left: 0,
170
+ top: 0,
171
+ width: `${1 / crop.w * 100}%`,
172
+ height: `${1 / crop.h * 100}%`,
173
+ transform: `translate(${-crop.x / crop.w * 100}%, ${-crop.y / crop.h * 100}%)`,
174
+ transformOrigin: "top left",
175
+ objectFit: fit,
176
+ display: "block"
177
+ };
178
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", width: "100%", height: "100%", overflow: "hidden" }, children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: element.src, alt: element.alt ?? "", style: inner, draggable: false }) });
179
+ }
164
180
  return /* @__PURE__ */ jsxRuntime.jsx(
165
181
  "img",
166
182
  {
@@ -169,7 +185,7 @@ function ImageElementRenderer({ element }) {
169
185
  style: {
170
186
  width: "100%",
171
187
  height: "100%",
172
- objectFit: element.fit ?? "contain",
188
+ objectFit: fit,
173
189
  display: "block"
174
190
  },
175
191
  draggable: false
@@ -653,6 +669,11 @@ function SlideViewer({
653
669
  );
654
670
  const [blanked, setBlanked] = react.useState(false);
655
671
  const containerRef = react.useRef(null);
672
+ const prevIndexRef = react.useRef(index);
673
+ const forward = index >= prevIndexRef.current;
674
+ react.useEffect(() => {
675
+ prevIndexRef.current = index;
676
+ }, [index]);
656
677
  useSlideKeyboard({
657
678
  total: deck.slides.length,
658
679
  index,
@@ -676,6 +697,8 @@ function SlideViewer({
676
697
  const slide = deck.slides[index];
677
698
  const theme = resolveTheme(deck.theme);
678
699
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
700
+ const transition = slide?.transition ?? theme.defaultTransition;
701
+ const enterStyle = transitionEnterStyle(transition, forward);
679
702
  return /* @__PURE__ */ jsxRuntime.jsxs(
680
703
  "div",
681
704
  {
@@ -694,6 +717,7 @@ function SlideViewer({
694
717
  tabIndex: 0,
695
718
  "data-fancy-slides-viewer": deck.id,
696
719
  children: [
720
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: TRANSITION_KEYFRAMES }),
697
721
  !blanked && slide && /* @__PURE__ */ jsxRuntime.jsx(
698
722
  "div",
699
723
  {
@@ -705,7 +729,7 @@ function SlideViewer({
705
729
  ["--fs-ratio"]: aspectRatio.toString(),
706
730
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
707
731
  },
708
- children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement })
732
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Slide, { slide, theme, renderElement }) }, index)
709
733
  }
710
734
  ),
711
735
  !hideChrome && !blanked && /* @__PURE__ */ jsxRuntime.jsxs(
@@ -735,6 +759,68 @@ function SlideViewer({
735
759
  }
736
760
  );
737
761
  }
762
+ var DEFAULT_DURATION = 400;
763
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
764
+ function transitionEnterStyle(transition, forward) {
765
+ const kind = transition?.kind ?? "none";
766
+ if (kind === "none") return { width: "100%", height: "100%" };
767
+ const duration = transition?.duration ?? DEFAULT_DURATION;
768
+ let name;
769
+ switch (kind) {
770
+ case "fade":
771
+ name = "fs-fade-in";
772
+ break;
773
+ case "zoom":
774
+ name = "fs-zoom-in";
775
+ break;
776
+ case "slide": {
777
+ const dir = transition?.direction ?? (forward ? "right" : "left");
778
+ name = `fs-slide-in-${dir}`;
779
+ break;
780
+ }
781
+ default:
782
+ return { width: "100%", height: "100%" };
783
+ }
784
+ return {
785
+ width: "100%",
786
+ height: "100%",
787
+ animationName: name,
788
+ animationDuration: `${duration}ms`,
789
+ animationTimingFunction: EASE,
790
+ animationFillMode: "both"
791
+ };
792
+ }
793
+ var TRANSITION_KEYFRAMES = `
794
+ @media (prefers-reduced-motion: reduce) {
795
+ .fs-slide-enter { animation: none !important; }
796
+ }
797
+ @media (prefers-reduced-motion: no-preference) {
798
+ @keyframes fs-fade-in {
799
+ from { opacity: 0; }
800
+ to { opacity: 1; }
801
+ }
802
+ @keyframes fs-zoom-in {
803
+ from { opacity: 0; transform: scale(0.92); }
804
+ to { opacity: 1; transform: scale(1); }
805
+ }
806
+ @keyframes fs-slide-in-right {
807
+ from { opacity: 0; transform: translateX(8%); }
808
+ to { opacity: 1; transform: translateX(0); }
809
+ }
810
+ @keyframes fs-slide-in-left {
811
+ from { opacity: 0; transform: translateX(-8%); }
812
+ to { opacity: 1; transform: translateX(0); }
813
+ }
814
+ @keyframes fs-slide-in-up {
815
+ from { opacity: 0; transform: translateY(8%); }
816
+ to { opacity: 1; transform: translateY(0); }
817
+ }
818
+ @keyframes fs-slide-in-down {
819
+ from { opacity: 0; transform: translateY(-8%); }
820
+ to { opacity: 1; transform: translateY(0); }
821
+ }
822
+ }
823
+ `;
738
824
  function PresenterView({
739
825
  deck,
740
826
  index: controlledIndex,
@@ -1088,6 +1174,7 @@ function useDeckState({ value, onChange, onOp }) {
1088
1174
  setLayout: (id, layout) => apply({ kind: "slide_set_layout", id, layout }),
1089
1175
  setNotes: (id, notes) => apply({ kind: "slide_set_notes", id, notes }),
1090
1176
  setBackground: (id, background) => apply({ kind: "slide_set_background", id, background }),
1177
+ setTransition: (id, transition) => apply({ kind: "slide_set_transition", id, transition }),
1091
1178
  addElement: (slideId2, element) => {
1092
1179
  const id = element.id ?? elementId();
1093
1180
  apply({ kind: "element_add", slideId: slideId2, element: { ...element, id } });
@@ -1129,6 +1216,8 @@ function reduce(deck, op) {
1129
1216
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, notes: op.notes } : s) };
1130
1217
  case "slide_set_background":
1131
1218
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, background: op.background } : s) };
1219
+ case "slide_set_transition":
1220
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, transition: op.transition } : s) };
1132
1221
  case "element_add":
1133
1222
  return {
1134
1223
  ...deck,
@@ -1248,6 +1337,113 @@ function chartStarterOption(kind) {
1248
1337
  };
1249
1338
  }
1250
1339
  }
1340
+ var CHART_PALETTE = [
1341
+ "#8b5cf6",
1342
+ "#3b82f6",
1343
+ "#10b981",
1344
+ "#f59e0b",
1345
+ "#ef4444",
1346
+ "#ec4899",
1347
+ "#14b8a6",
1348
+ "#6366f1"
1349
+ ];
1350
+ function chartColorAt(index) {
1351
+ return CHART_PALETTE[index % CHART_PALETTE.length];
1352
+ }
1353
+ function isPlainObject(v) {
1354
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1355
+ }
1356
+ function toNumber(v) {
1357
+ const n = typeof v === "number" ? v : parseFloat(String(v));
1358
+ return Number.isFinite(n) ? n : 0;
1359
+ }
1360
+ function chartModelFromOption(option) {
1361
+ if (!isPlainObject(option)) return null;
1362
+ const seriesRaw = option.series;
1363
+ if (!Array.isArray(seriesRaw) || seriesRaw.length === 0) return null;
1364
+ if (!seriesRaw.every(isPlainObject)) return null;
1365
+ const types = seriesRaw.map((s) => String(s.type ?? ""));
1366
+ if (types[0] === "pie") {
1367
+ if (seriesRaw.length !== 1) return null;
1368
+ const data = seriesRaw[0].data;
1369
+ if (!Array.isArray(data)) return null;
1370
+ const slices = [];
1371
+ for (const d of data) {
1372
+ if (!isPlainObject(d)) return null;
1373
+ slices.push({ name: String(d.name ?? ""), value: toNumber(d.value) });
1374
+ }
1375
+ return { kind: "pie", categories: [], series: [], slices };
1376
+ }
1377
+ const cartesian = /* @__PURE__ */ new Set(["bar", "line", "scatter"]);
1378
+ if (!types.every((t) => cartesian.has(t))) return null;
1379
+ if (new Set(types).size !== 1) return null;
1380
+ const baseType = types[0];
1381
+ const isArea = baseType === "line" && seriesRaw.every((s) => isPlainObject(s.areaStyle) || s.areaStyle != null);
1382
+ const kind = baseType === "line" ? isArea ? "area" : "line" : baseType;
1383
+ const xAxis = option.xAxis;
1384
+ const axisData = isPlainObject(xAxis) ? xAxis.data : void 0;
1385
+ const categories = Array.isArray(axisData) ? axisData.map((c) => String(c)) : [];
1386
+ const firstData = seriesRaw[0].data;
1387
+ const valueCount = Array.isArray(firstData) ? firstData.length : 0;
1388
+ const cats = categories.length > 0 ? categories : Array.from({ length: valueCount }, (_, i) => String(i + 1));
1389
+ const series = [];
1390
+ for (const s of seriesRaw) {
1391
+ const data = s.data;
1392
+ if (!Array.isArray(data)) return null;
1393
+ const values = data.map((d) => {
1394
+ if (Array.isArray(d)) return toNumber(d[1]);
1395
+ if (isPlainObject(d)) return toNumber(d.value);
1396
+ return toNumber(d);
1397
+ });
1398
+ 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 });
1399
+ }
1400
+ return { kind, categories: cats, series, slices: [] };
1401
+ }
1402
+ function chartOptionFromModel(model) {
1403
+ if (model.kind === "pie") {
1404
+ return {
1405
+ tooltip: { trigger: "item" },
1406
+ legend: { bottom: 0 },
1407
+ color: model.slices.map((_, i) => chartColorAt(i)),
1408
+ series: [
1409
+ {
1410
+ type: "pie",
1411
+ radius: ["40%", "70%"],
1412
+ name: "Segment",
1413
+ data: model.slices.map((s) => ({ name: s.name, value: s.value }))
1414
+ }
1415
+ ]
1416
+ };
1417
+ }
1418
+ const isScatter = model.kind === "scatter";
1419
+ const isArea = model.kind === "area";
1420
+ const seriesType = model.kind === "bar" ? "bar" : model.kind === "scatter" ? "scatter" : "line";
1421
+ const series = model.series.map((s, i) => {
1422
+ const color = s.color ?? chartColorAt(i);
1423
+ const base = {
1424
+ type: seriesType,
1425
+ name: s.name,
1426
+ itemStyle: { color }
1427
+ };
1428
+ if (isScatter) {
1429
+ base.symbolSize = 12;
1430
+ base.data = s.values.map((v, idx) => [idx, v]);
1431
+ } else {
1432
+ base.data = s.values;
1433
+ }
1434
+ if (seriesType === "line") base.smooth = true;
1435
+ if (isArea) base.areaStyle = { color };
1436
+ return base;
1437
+ });
1438
+ return {
1439
+ grid: { top: 24, left: 56, right: 16, bottom: isScatter ? 32 : 40 },
1440
+ tooltip: { trigger: isScatter ? "item" : "axis" },
1441
+ legend: model.series.length > 1 ? { bottom: 0 } : void 0,
1442
+ xAxis: isScatter ? { type: "value" } : { type: "category", data: [...model.categories] },
1443
+ yAxis: { type: "value" },
1444
+ series
1445
+ };
1446
+ }
1251
1447
  function SlideRail({
1252
1448
  slides,
1253
1449
  selectedId,
@@ -1396,8 +1592,11 @@ function EditorToolbar({
1396
1592
  /* @__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
1593
  ] });
1398
1594
  }
1399
- function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1595
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1400
1596
  if (!element) {
1597
+ if (slide) {
1598
+ return /* @__PURE__ */ jsxRuntime.jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1599
+ }
1401
1600
  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
1601
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
1403
1602
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "mt-2 !text-zinc-500", children: "Select an element to edit its properties." })
@@ -1431,6 +1630,80 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1431
1630
  ] }) })
1432
1631
  ] });
1433
1632
  }
1633
+ function SlideSettings({
1634
+ slide,
1635
+ onSetTransition,
1636
+ onSetBackground
1637
+ }) {
1638
+ const transition = slide.transition;
1639
+ const kind = transition?.kind ?? "none";
1640
+ const setTransition = (next) => {
1641
+ const merged = { kind, duration: transition?.duration, direction: transition?.direction, ...next };
1642
+ onSetTransition?.(merged.kind === "none" ? { kind: "none" } : merged);
1643
+ };
1644
+ 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: [
1645
+ /* @__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: [
1646
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h3", size: "xs", className: "!font-mono !uppercase !tracking-wider !text-zinc-500", children: "slide" }),
1647
+ /* @__PURE__ */ jsxRuntime.jsxs(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
1648
+ "#",
1649
+ slide.id.slice(-6)
1650
+ ] })
1651
+ ] }) }),
1652
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-y-auto p-3", children: [
1653
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1654
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Transition" }),
1655
+ /* @__PURE__ */ jsxRuntime.jsx(
1656
+ reactFancy.Select,
1657
+ {
1658
+ label: "Kind",
1659
+ list: [
1660
+ { value: "none", label: "None" },
1661
+ { value: "fade", label: "Fade" },
1662
+ { value: "slide", label: "Slide" },
1663
+ { value: "zoom", label: "Zoom" }
1664
+ ],
1665
+ value: kind,
1666
+ onValueChange: (v) => setTransition({ kind: v })
1667
+ }
1668
+ ),
1669
+ kind === "slide" && /* @__PURE__ */ jsxRuntime.jsx(
1670
+ reactFancy.Select,
1671
+ {
1672
+ label: "Direction",
1673
+ list: [
1674
+ { value: "left", label: "From left" },
1675
+ { value: "right", label: "From right" },
1676
+ { value: "up", label: "From bottom" },
1677
+ { value: "down", label: "From top" }
1678
+ ],
1679
+ value: transition?.direction ?? "right",
1680
+ onValueChange: (v) => setTransition({ direction: v })
1681
+ }
1682
+ ),
1683
+ kind !== "none" && /* @__PURE__ */ jsxRuntime.jsx(
1684
+ reactFancy.Input,
1685
+ {
1686
+ label: "Duration (ms)",
1687
+ type: "number",
1688
+ value: String(transition?.duration ?? 400),
1689
+ onChange: (e) => setTransition({ duration: parseInt(e.target.value, 10) || 400 })
1690
+ }
1691
+ ),
1692
+ /* @__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." })
1693
+ ] }) }),
1694
+ 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: [
1695
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Background" }),
1696
+ /* @__PURE__ */ jsxRuntime.jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(
1697
+ reactFancy.ColorPicker,
1698
+ {
1699
+ value: slide.background?.color ?? "#ffffff",
1700
+ onChange: (c) => onSetBackground({ ...slide.background, color: c })
1701
+ }
1702
+ ) })
1703
+ ] }) })
1704
+ ] })
1705
+ ] });
1706
+ }
1434
1707
  function LayoutSection({ element, onPatch }) {
1435
1708
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1436
1709
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1524,7 +1797,35 @@ function TextStyleControls({ element, onPatch }) {
1524
1797
  ] });
1525
1798
  }
1526
1799
  function ImageStyleControls({ element, onPatch }) {
1800
+ const fileRef = react.useRef(null);
1801
+ const crop = element.crop;
1802
+ const onFile = (file) => {
1803
+ if (!file) return;
1804
+ const reader = new FileReader();
1805
+ reader.onload = () => {
1806
+ if (typeof reader.result === "string") onPatch({ src: reader.result });
1807
+ };
1808
+ reader.readAsDataURL(file);
1809
+ };
1810
+ const setCrop = (next) => {
1811
+ const base = crop ?? { x: 0, y: 0, w: 1, h: 1 };
1812
+ onPatch({ crop: { ...base, ...next } });
1813
+ };
1527
1814
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1815
+ /* @__PURE__ */ jsxRuntime.jsx(
1816
+ "input",
1817
+ {
1818
+ ref: fileRef,
1819
+ type: "file",
1820
+ accept: "image/*",
1821
+ className: "hidden",
1822
+ onChange: (e) => {
1823
+ onFile(e.target.files?.[0]);
1824
+ e.target.value = "";
1825
+ }
1826
+ }
1827
+ ),
1828
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "sm", variant: "ghost", icon: "upload", onClick: () => fileRef.current?.click(), children: "Upload image" }),
1528
1829
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1529
1830
  /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1530
1831
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -1540,7 +1841,17 @@ function ImageStyleControls({ element, onPatch }) {
1540
1841
  value: element.fit ?? "contain",
1541
1842
  onValueChange: (v) => onPatch({ fit: v })
1542
1843
  }
1543
- )
1844
+ ),
1845
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
1846
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
1847
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Crop" }),
1848
+ crop && /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", onClick: () => onPatch({ crop: void 0 }), children: "Clear crop" })
1849
+ ] }),
1850
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "X", value: crop?.x ?? 0, onValueChange: (v) => setCrop({ x: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
1851
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Y", value: crop?.y ?? 0, onValueChange: (v) => setCrop({ y: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
1852
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Width", value: crop?.w ?? 1, onValueChange: (v) => setCrop({ w: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
1853
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Slider, { label: "Height", value: crop?.h ?? 1, onValueChange: (v) => setCrop({ h: Number(v) }), min: 0.01, max: 1, step: 0.01, showValue: true }),
1854
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.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." })
1544
1855
  ] });
1545
1856
  }
1546
1857
  function ShapeStyleControls({ element, onPatch }) {
@@ -1587,54 +1898,254 @@ function CodeStyleControls({ element, onPatch }) {
1587
1898
  ] });
1588
1899
  }
1589
1900
  function ChartStyleControls({ element, onPatch }) {
1901
+ const model = chartModelFromOption(element.option);
1902
+ const writeModel = (m) => onPatch({ option: chartOptionFromModel(m) });
1903
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1904
+ model ? /* @__PURE__ */ jsxRuntime.jsx(ChartModelEditor, { model, onChange: writeModel }) : /* @__PURE__ */ jsxRuntime.jsx(reactFancy.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." }),
1905
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
1906
+ /* @__PURE__ */ jsxRuntime.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" }),
1907
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-2 pt-0", children: /* @__PURE__ */ jsxRuntime.jsx(
1908
+ reactFancy.Textarea,
1909
+ {
1910
+ label: "ECharts option (JSON)",
1911
+ value: JSON.stringify(element.option, null, 2),
1912
+ onValueChange: (v) => {
1913
+ try {
1914
+ onPatch({ option: JSON.parse(v) });
1915
+ } catch {
1916
+ }
1917
+ },
1918
+ rows: 10
1919
+ }
1920
+ ) })
1921
+ ] })
1922
+ ] });
1923
+ }
1924
+ var CHART_TYPE_OPTIONS = [
1925
+ { value: "bar", label: "Bar" },
1926
+ { value: "line", label: "Line" },
1927
+ { value: "area", label: "Area" },
1928
+ { value: "pie", label: "Pie" },
1929
+ { value: "scatter", label: "Scatter" }
1930
+ ];
1931
+ function ChartModelEditor({ model, onChange }) {
1932
+ const setKind = (kind) => {
1933
+ if (kind === model.kind) return;
1934
+ if (kind === "pie") {
1935
+ const first = model.series[0];
1936
+ 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 }];
1937
+ onChange({ ...model, kind, slices });
1938
+ return;
1939
+ }
1940
+ if (model.kind === "pie") {
1941
+ const categories = model.slices.length ? model.slices.map((s) => s.name) : ["A", "B", "C"];
1942
+ const values = model.slices.length ? model.slices.map((s) => s.value) : [1, 2, 3];
1943
+ onChange({ ...model, kind, categories, series: [{ name: "Series 1", color: chartColorAt(0), values }] });
1944
+ return;
1945
+ }
1946
+ onChange({ ...model, kind });
1947
+ };
1590
1948
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1591
- /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "sm", className: "!text-zinc-500", children: "Chart option is JSON \u2014 paste any ECharts option here." }),
1592
1949
  /* @__PURE__ */ jsxRuntime.jsx(
1593
- reactFancy.Textarea,
1950
+ reactFancy.Select,
1594
1951
  {
1595
- label: "ECharts option (JSON)",
1596
- value: JSON.stringify(element.option, null, 2),
1597
- onValueChange: (v) => {
1598
- try {
1599
- onPatch({ option: JSON.parse(v) });
1600
- } catch {
1601
- }
1602
- },
1603
- rows: 10
1952
+ label: "Chart type",
1953
+ list: CHART_TYPE_OPTIONS,
1954
+ value: model.kind,
1955
+ onValueChange: (v) => setKind(v)
1604
1956
  }
1605
- )
1957
+ ),
1958
+ model.kind === "pie" ? /* @__PURE__ */ jsxRuntime.jsx(PieSliceEditor, { model, onChange }) : /* @__PURE__ */ jsxRuntime.jsx(CartesianChartEditor, { model, onChange })
1959
+ ] });
1960
+ }
1961
+ function PieSliceEditor({ model, onChange }) {
1962
+ const slices = model.slices;
1963
+ const update = (i, next) => {
1964
+ const copy = slices.map((s, idx) => idx === i ? { ...s, ...next } : s);
1965
+ onChange({ ...model, slices: copy });
1966
+ };
1967
+ const remove = (i) => onChange({ ...model, slices: slices.filter((_, idx) => idx !== i) });
1968
+ const add = () => onChange({ ...model, slices: [...slices, { name: `Slice ${slices.length + 1}`, value: 0 }] });
1969
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
1970
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Slices" }),
1971
+ slices.map((s, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
1972
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: i === 0 ? "Name" : void 0, value: s.name, onChange: (e) => update(i, { name: e.target.value }) }) }),
1973
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-20", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: i === 0 ? "Value" : void 0, type: "number", value: String(s.value), onChange: (e) => update(i, { value: parseFloat(e.target.value) || 0 }) }) }),
1974
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => remove(i), "aria-label": "Remove slice" })
1975
+ ] }, i)),
1976
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: add, children: "Add slice" })
1977
+ ] });
1978
+ }
1979
+ function CartesianChartEditor({ model, onChange }) {
1980
+ const { categories, series } = model;
1981
+ const updateCategory = (i, label) => {
1982
+ onChange({ ...model, categories: categories.map((c, idx) => idx === i ? label : c) });
1983
+ };
1984
+ const removeCategory = (i) => {
1985
+ onChange({
1986
+ ...model,
1987
+ categories: categories.filter((_, idx) => idx !== i),
1988
+ series: series.map((s) => ({ ...s, values: s.values.filter((_, idx) => idx !== i) }))
1989
+ });
1990
+ };
1991
+ const addCategory = () => {
1992
+ onChange({
1993
+ ...model,
1994
+ categories: [...categories, `Cat ${categories.length + 1}`],
1995
+ series: series.map((s) => ({ ...s, values: [...s.values, 0] }))
1996
+ });
1997
+ };
1998
+ const updateSeries = (si, next) => {
1999
+ onChange({ ...model, series: series.map((s, idx) => idx === si ? { ...s, ...next } : s) });
2000
+ };
2001
+ const updateValue = (si, ci, value) => {
2002
+ onChange({
2003
+ ...model,
2004
+ series: series.map(
2005
+ (s, idx) => idx === si ? { ...s, values: s.values.map((v, vi) => vi === ci ? value : v) } : s
2006
+ )
2007
+ });
2008
+ };
2009
+ const removeSeries = (si) => onChange({ ...model, series: series.filter((_, idx) => idx !== si) });
2010
+ const addSeries = () => onChange({
2011
+ ...model,
2012
+ series: [
2013
+ ...series,
2014
+ { name: `Series ${series.length + 1}`, color: chartColorAt(series.length), values: categories.map(() => 0) }
2015
+ ]
2016
+ });
2017
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2018
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2019
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Categories" }),
2020
+ categories.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2021
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { value: c, onChange: (e) => updateCategory(i, e.target.value) }) }),
2022
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeCategory(i), "aria-label": "Remove category" })
2023
+ ] }, i)),
2024
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addCategory, children: "Add category" })
2025
+ ] }),
2026
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
2027
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
2028
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Series" }),
2029
+ series.map((s, si) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2 rounded-md border border-zinc-200 p-2 dark:border-zinc-800", children: [
2030
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-end gap-2", children: [
2031
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { label: "Name", value: s.name, onChange: (e) => updateSeries(si, { name: e.target.value }) }) }),
2032
+ /* @__PURE__ */ jsxRuntime.jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ColorPicker, { value: s.color ?? chartColorAt(si), onChange: (c) => updateSeries(si, { color: c }) }) }),
2033
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeSeries(si), "aria-label": "Remove series" })
2034
+ ] }),
2035
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2", children: categories.map((c, ci) => /* @__PURE__ */ jsxRuntime.jsx(
2036
+ reactFancy.Input,
2037
+ {
2038
+ label: c,
2039
+ type: "number",
2040
+ value: String(s.values[ci] ?? 0),
2041
+ onChange: (e) => updateValue(si, ci, parseFloat(e.target.value) || 0)
2042
+ },
2043
+ ci
2044
+ )) })
2045
+ ] }, si)),
2046
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addSeries, children: "Add series" })
2047
+ ] })
1606
2048
  ] });
1607
2049
  }
1608
2050
  function TableStyleControls({ element, onPatch }) {
2051
+ const columns = element.columns;
2052
+ const rows = element.rows;
2053
+ const nextColKey = () => {
2054
+ const existing = new Set(columns.map((c) => c.key));
2055
+ let n = columns.length + 1;
2056
+ while (existing.has(`col${n}`)) n++;
2057
+ return `col${n}`;
2058
+ };
2059
+ const setColumnLabel = (i, label) => {
2060
+ onPatch({ columns: columns.map((c, idx) => idx === i ? { ...c, label } : c) });
2061
+ };
2062
+ const removeColumn = (i) => {
2063
+ const key = columns[i]?.key;
2064
+ const nextCols = columns.filter((_, idx) => idx !== i);
2065
+ const nextRows = key ? rows.map((r) => {
2066
+ const { [key]: _drop, ...rest } = r;
2067
+ return rest;
2068
+ }) : rows;
2069
+ onPatch({ columns: nextCols, rows: nextRows });
2070
+ };
2071
+ const addColumn = () => {
2072
+ const key = nextColKey();
2073
+ onPatch({
2074
+ columns: [...columns, { key, label: `Column ${columns.length + 1}` }],
2075
+ rows: rows.map((r) => ({ ...r, [key]: "" }))
2076
+ });
2077
+ };
2078
+ const setCell = (rowIdx, key, value) => {
2079
+ onPatch({ rows: rows.map((r, idx) => idx === rowIdx ? { ...r, [key]: value } : r) });
2080
+ };
2081
+ const removeRow = (rowIdx) => onPatch({ rows: rows.filter((_, idx) => idx !== rowIdx) });
2082
+ const addRow = () => {
2083
+ const blank = {};
2084
+ for (const c of columns) blank[c.key] = "";
2085
+ onPatch({ rows: [...rows, blank] });
2086
+ };
1609
2087
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
1610
- /* @__PURE__ */ jsxRuntime.jsx(
1611
- reactFancy.Textarea,
1612
- {
1613
- label: "Columns (JSON)",
1614
- value: JSON.stringify(element.columns, null, 2),
1615
- onValueChange: (v) => {
1616
- try {
1617
- onPatch({ columns: JSON.parse(v) });
1618
- } catch {
2088
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2089
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Columns" }),
2090
+ columns.map((c, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
2091
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Input, { value: c.label, onChange: (e) => setColumnLabel(i, e.target.value), "aria-label": `Column ${i + 1} label` }) }),
2092
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!font-mono !text-zinc-400", children: c.key }),
2093
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeColumn(i), "aria-label": "Remove column" })
2094
+ ] }, c.key)),
2095
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addColumn, children: "Add column" })
2096
+ ] }),
2097
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Separator, {}),
2098
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
2099
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Rows" }),
2100
+ columns.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Text, { size: "xs", className: "!text-zinc-500", children: "Add a column to start adding rows." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2101
+ rows.map((r, rowIdx) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start gap-2 border-b border-zinc-100 pb-2 dark:border-zinc-800", children: [
2102
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid flex-1 grid-cols-1 gap-1", children: columns.map((c) => /* @__PURE__ */ jsxRuntime.jsx(
2103
+ reactFancy.Input,
2104
+ {
2105
+ label: c.label,
2106
+ value: r[c.key] == null ? "" : String(r[c.key]),
2107
+ onChange: (e) => setCell(rowIdx, c.key, e.target.value)
2108
+ },
2109
+ c.key
2110
+ )) }),
2111
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeRow(rowIdx), "aria-label": "Remove row" })
2112
+ ] }, rowIdx)),
2113
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addRow, children: "Add row" })
2114
+ ] })
2115
+ ] }),
2116
+ /* @__PURE__ */ jsxRuntime.jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2117
+ /* @__PURE__ */ jsxRuntime.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" }),
2118
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 p-2 pt-0", children: [
2119
+ /* @__PURE__ */ jsxRuntime.jsx(
2120
+ reactFancy.Textarea,
2121
+ {
2122
+ label: "Columns (JSON)",
2123
+ value: JSON.stringify(columns, null, 2),
2124
+ onValueChange: (v) => {
2125
+ try {
2126
+ onPatch({ columns: JSON.parse(v) });
2127
+ } catch {
2128
+ }
2129
+ },
2130
+ rows: 5
1619
2131
  }
1620
- },
1621
- rows: 5
1622
- }
1623
- ),
1624
- /* @__PURE__ */ jsxRuntime.jsx(
1625
- reactFancy.Textarea,
1626
- {
1627
- label: "Rows (JSON)",
1628
- value: JSON.stringify(element.rows, null, 2),
1629
- onValueChange: (v) => {
1630
- try {
1631
- onPatch({ rows: JSON.parse(v) });
1632
- } catch {
2132
+ ),
2133
+ /* @__PURE__ */ jsxRuntime.jsx(
2134
+ reactFancy.Textarea,
2135
+ {
2136
+ label: "Rows (JSON)",
2137
+ value: JSON.stringify(rows, null, 2),
2138
+ onValueChange: (v) => {
2139
+ try {
2140
+ onPatch({ rows: JSON.parse(v) });
2141
+ } catch {
2142
+ }
2143
+ },
2144
+ rows: 8
1633
2145
  }
1634
- },
1635
- rows: 8
1636
- }
1637
- )
2146
+ )
2147
+ ] })
2148
+ ] })
1638
2149
  ] });
1639
2150
  }
1640
2151
  function EmbedStyleControls({ element, onPatch }) {
@@ -1880,13 +2391,16 @@ function DeckEditor({
1880
2391
  ElementInspector,
1881
2392
  {
1882
2393
  element: selectedElement,
2394
+ slide: slide ?? null,
1883
2395
  onPatch: (patch) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, patch),
1884
2396
  onDelete: () => {
1885
2397
  if (!slide || !elementIdSelected) return;
1886
2398
  ops.removeElement(slide.id, elementIdSelected);
1887
2399
  setElementIdSelected(null);
1888
2400
  },
1889
- onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked })
2401
+ onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2402
+ onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2403
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
1890
2404
  }
1891
2405
  ) })
1892
2406
  ] }),