@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isDarkColor, SlideContext } from './chunk-WIUXPQAK.js';
2
2
  export { useIsDarkSlide, useSlideContext, useSlideTheme } from './chunk-WIUXPQAK.js';
3
3
  import { useId, useRef, useState, useEffect, useMemo, useCallback } from 'react';
4
- import { ContentRenderer, Text, Action, ContextMenu, Separator, Tooltip, Dropdown, Badge, Heading, Tabs, Card, Input, Slider, Textarea, Select, ColorPicker } from '@particle-academy/react-fancy';
4
+ import { ContentRenderer, Text, Action, ContextMenu, Separator, Tooltip, Dropdown, Badge, Heading, Tabs, Card, Select, Input, ColorPicker, Slider, Textarea } from '@particle-academy/react-fancy';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
 
7
7
  // src/theme/default-theme.ts
@@ -159,6 +159,22 @@ function weight(w) {
159
159
  return void 0;
160
160
  }
161
161
  function ImageElementRenderer({ element }) {
162
+ const crop = element.crop;
163
+ const fit = element.fit ?? "contain";
164
+ if (crop && crop.w > 0 && crop.h > 0) {
165
+ const inner = {
166
+ position: "absolute",
167
+ left: 0,
168
+ top: 0,
169
+ width: `${1 / crop.w * 100}%`,
170
+ height: `${1 / crop.h * 100}%`,
171
+ transform: `translate(${-crop.x / crop.w * 100}%, ${-crop.y / crop.h * 100}%)`,
172
+ transformOrigin: "top left",
173
+ objectFit: fit,
174
+ display: "block"
175
+ };
176
+ 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 }) });
177
+ }
162
178
  return /* @__PURE__ */ jsx(
163
179
  "img",
164
180
  {
@@ -167,7 +183,7 @@ function ImageElementRenderer({ element }) {
167
183
  style: {
168
184
  width: "100%",
169
185
  height: "100%",
170
- objectFit: element.fit ?? "contain",
186
+ objectFit: fit,
171
187
  display: "block"
172
188
  },
173
189
  draggable: false
@@ -611,6 +627,11 @@ function SlideViewer({
611
627
  );
612
628
  const [blanked, setBlanked] = useState(false);
613
629
  const containerRef = useRef(null);
630
+ const prevIndexRef = useRef(index);
631
+ const forward = index >= prevIndexRef.current;
632
+ useEffect(() => {
633
+ prevIndexRef.current = index;
634
+ }, [index]);
614
635
  useSlideKeyboard({
615
636
  total: deck.slides.length,
616
637
  index,
@@ -634,6 +655,8 @@ function SlideViewer({
634
655
  const slide = deck.slides[index];
635
656
  const theme = resolveTheme(deck.theme);
636
657
  const aspectRatio = theme.aspectRatio ?? 16 / 9;
658
+ const transition = slide?.transition ?? theme.defaultTransition;
659
+ const enterStyle = transitionEnterStyle(transition, forward);
637
660
  return /* @__PURE__ */ jsxs(
638
661
  "div",
639
662
  {
@@ -652,6 +675,7 @@ function SlideViewer({
652
675
  tabIndex: 0,
653
676
  "data-fancy-slides-viewer": deck.id,
654
677
  children: [
678
+ /* @__PURE__ */ jsx("style", { children: TRANSITION_KEYFRAMES }),
655
679
  !blanked && slide && /* @__PURE__ */ jsx(
656
680
  "div",
657
681
  {
@@ -663,7 +687,7 @@ function SlideViewer({
663
687
  ["--fs-ratio"]: aspectRatio.toString(),
664
688
  boxShadow: "0 8px 30px rgba(0,0,0,0.35)"
665
689
  },
666
- children: /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement })
690
+ children: /* @__PURE__ */ jsx("div", { className: "fs-slide-enter", style: enterStyle, children: /* @__PURE__ */ jsx(Slide, { slide, theme, renderElement }) }, index)
667
691
  }
668
692
  ),
669
693
  !hideChrome && !blanked && /* @__PURE__ */ jsxs(
@@ -693,6 +717,68 @@ function SlideViewer({
693
717
  }
694
718
  );
695
719
  }
720
+ var DEFAULT_DURATION = 400;
721
+ var EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
722
+ function transitionEnterStyle(transition, forward) {
723
+ const kind = transition?.kind ?? "none";
724
+ if (kind === "none") return { width: "100%", height: "100%" };
725
+ const duration = transition?.duration ?? DEFAULT_DURATION;
726
+ let name;
727
+ switch (kind) {
728
+ case "fade":
729
+ name = "fs-fade-in";
730
+ break;
731
+ case "zoom":
732
+ name = "fs-zoom-in";
733
+ break;
734
+ case "slide": {
735
+ const dir = transition?.direction ?? (forward ? "right" : "left");
736
+ name = `fs-slide-in-${dir}`;
737
+ break;
738
+ }
739
+ default:
740
+ return { width: "100%", height: "100%" };
741
+ }
742
+ return {
743
+ width: "100%",
744
+ height: "100%",
745
+ animationName: name,
746
+ animationDuration: `${duration}ms`,
747
+ animationTimingFunction: EASE,
748
+ animationFillMode: "both"
749
+ };
750
+ }
751
+ var TRANSITION_KEYFRAMES = `
752
+ @media (prefers-reduced-motion: reduce) {
753
+ .fs-slide-enter { animation: none !important; }
754
+ }
755
+ @media (prefers-reduced-motion: no-preference) {
756
+ @keyframes fs-fade-in {
757
+ from { opacity: 0; }
758
+ to { opacity: 1; }
759
+ }
760
+ @keyframes fs-zoom-in {
761
+ from { opacity: 0; transform: scale(0.92); }
762
+ to { opacity: 1; transform: scale(1); }
763
+ }
764
+ @keyframes fs-slide-in-right {
765
+ from { opacity: 0; transform: translateX(8%); }
766
+ to { opacity: 1; transform: translateX(0); }
767
+ }
768
+ @keyframes fs-slide-in-left {
769
+ from { opacity: 0; transform: translateX(-8%); }
770
+ to { opacity: 1; transform: translateX(0); }
771
+ }
772
+ @keyframes fs-slide-in-up {
773
+ from { opacity: 0; transform: translateY(8%); }
774
+ to { opacity: 1; transform: translateY(0); }
775
+ }
776
+ @keyframes fs-slide-in-down {
777
+ from { opacity: 0; transform: translateY(-8%); }
778
+ to { opacity: 1; transform: translateY(0); }
779
+ }
780
+ }
781
+ `;
696
782
  function PresenterView({
697
783
  deck,
698
784
  index: controlledIndex,
@@ -1046,6 +1132,7 @@ function useDeckState({ value, onChange, onOp }) {
1046
1132
  setLayout: (id, layout) => apply({ kind: "slide_set_layout", id, layout }),
1047
1133
  setNotes: (id, notes) => apply({ kind: "slide_set_notes", id, notes }),
1048
1134
  setBackground: (id, background) => apply({ kind: "slide_set_background", id, background }),
1135
+ setTransition: (id, transition) => apply({ kind: "slide_set_transition", id, transition }),
1049
1136
  addElement: (slideId2, element) => {
1050
1137
  const id = element.id ?? elementId();
1051
1138
  apply({ kind: "element_add", slideId: slideId2, element: { ...element, id } });
@@ -1087,6 +1174,8 @@ function reduce(deck, op) {
1087
1174
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, notes: op.notes } : s) };
1088
1175
  case "slide_set_background":
1089
1176
  return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, background: op.background } : s) };
1177
+ case "slide_set_transition":
1178
+ return { ...deck, slides: deck.slides.map((s) => s.id === op.id ? { ...s, transition: op.transition } : s) };
1090
1179
  case "element_add":
1091
1180
  return {
1092
1181
  ...deck,
@@ -1206,6 +1295,113 @@ function chartStarterOption(kind) {
1206
1295
  };
1207
1296
  }
1208
1297
  }
1298
+ var CHART_PALETTE = [
1299
+ "#8b5cf6",
1300
+ "#3b82f6",
1301
+ "#10b981",
1302
+ "#f59e0b",
1303
+ "#ef4444",
1304
+ "#ec4899",
1305
+ "#14b8a6",
1306
+ "#6366f1"
1307
+ ];
1308
+ function chartColorAt(index) {
1309
+ return CHART_PALETTE[index % CHART_PALETTE.length];
1310
+ }
1311
+ function isPlainObject(v) {
1312
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1313
+ }
1314
+ function toNumber(v) {
1315
+ const n = typeof v === "number" ? v : parseFloat(String(v));
1316
+ return Number.isFinite(n) ? n : 0;
1317
+ }
1318
+ function chartModelFromOption(option) {
1319
+ if (!isPlainObject(option)) return null;
1320
+ const seriesRaw = option.series;
1321
+ if (!Array.isArray(seriesRaw) || seriesRaw.length === 0) return null;
1322
+ if (!seriesRaw.every(isPlainObject)) return null;
1323
+ const types = seriesRaw.map((s) => String(s.type ?? ""));
1324
+ if (types[0] === "pie") {
1325
+ if (seriesRaw.length !== 1) return null;
1326
+ const data = seriesRaw[0].data;
1327
+ if (!Array.isArray(data)) return null;
1328
+ const slices = [];
1329
+ for (const d of data) {
1330
+ if (!isPlainObject(d)) return null;
1331
+ slices.push({ name: String(d.name ?? ""), value: toNumber(d.value) });
1332
+ }
1333
+ return { kind: "pie", categories: [], series: [], slices };
1334
+ }
1335
+ const cartesian = /* @__PURE__ */ new Set(["bar", "line", "scatter"]);
1336
+ if (!types.every((t) => cartesian.has(t))) return null;
1337
+ if (new Set(types).size !== 1) return null;
1338
+ const baseType = types[0];
1339
+ const isArea = baseType === "line" && seriesRaw.every((s) => isPlainObject(s.areaStyle) || s.areaStyle != null);
1340
+ const kind = baseType === "line" ? isArea ? "area" : "line" : baseType;
1341
+ const xAxis = option.xAxis;
1342
+ const axisData = isPlainObject(xAxis) ? xAxis.data : void 0;
1343
+ const categories = Array.isArray(axisData) ? axisData.map((c) => String(c)) : [];
1344
+ const firstData = seriesRaw[0].data;
1345
+ const valueCount = Array.isArray(firstData) ? firstData.length : 0;
1346
+ const cats = categories.length > 0 ? categories : Array.from({ length: valueCount }, (_, i) => String(i + 1));
1347
+ const series = [];
1348
+ for (const s of seriesRaw) {
1349
+ const data = s.data;
1350
+ if (!Array.isArray(data)) return null;
1351
+ const values = data.map((d) => {
1352
+ if (Array.isArray(d)) return toNumber(d[1]);
1353
+ if (isPlainObject(d)) return toNumber(d.value);
1354
+ return toNumber(d);
1355
+ });
1356
+ 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 });
1357
+ }
1358
+ return { kind, categories: cats, series, slices: [] };
1359
+ }
1360
+ function chartOptionFromModel(model) {
1361
+ if (model.kind === "pie") {
1362
+ return {
1363
+ tooltip: { trigger: "item" },
1364
+ legend: { bottom: 0 },
1365
+ color: model.slices.map((_, i) => chartColorAt(i)),
1366
+ series: [
1367
+ {
1368
+ type: "pie",
1369
+ radius: ["40%", "70%"],
1370
+ name: "Segment",
1371
+ data: model.slices.map((s) => ({ name: s.name, value: s.value }))
1372
+ }
1373
+ ]
1374
+ };
1375
+ }
1376
+ const isScatter = model.kind === "scatter";
1377
+ const isArea = model.kind === "area";
1378
+ const seriesType = model.kind === "bar" ? "bar" : model.kind === "scatter" ? "scatter" : "line";
1379
+ const series = model.series.map((s, i) => {
1380
+ const color = s.color ?? chartColorAt(i);
1381
+ const base = {
1382
+ type: seriesType,
1383
+ name: s.name,
1384
+ itemStyle: { color }
1385
+ };
1386
+ if (isScatter) {
1387
+ base.symbolSize = 12;
1388
+ base.data = s.values.map((v, idx) => [idx, v]);
1389
+ } else {
1390
+ base.data = s.values;
1391
+ }
1392
+ if (seriesType === "line") base.smooth = true;
1393
+ if (isArea) base.areaStyle = { color };
1394
+ return base;
1395
+ });
1396
+ return {
1397
+ grid: { top: 24, left: 56, right: 16, bottom: isScatter ? 32 : 40 },
1398
+ tooltip: { trigger: isScatter ? "item" : "axis" },
1399
+ legend: model.series.length > 1 ? { bottom: 0 } : void 0,
1400
+ xAxis: isScatter ? { type: "value" } : { type: "category", data: [...model.categories] },
1401
+ yAxis: { type: "value" },
1402
+ series
1403
+ };
1404
+ }
1209
1405
  function SlideRail({
1210
1406
  slides,
1211
1407
  selectedId,
@@ -1354,8 +1550,11 @@ function EditorToolbar({
1354
1550
  /* @__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" }) }) })
1355
1551
  ] });
1356
1552
  }
1357
- function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1553
+ function ElementInspector({ element, onPatch, onDelete, onLockToggle, slide, onSetTransition, onSetBackground }) {
1358
1554
  if (!element) {
1555
+ if (slide) {
1556
+ return /* @__PURE__ */ jsx(SlideSettings, { slide, onSetTransition, onSetBackground });
1557
+ }
1359
1558
  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: [
1360
1559
  /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Inspector" }),
1361
1560
  /* @__PURE__ */ jsx(Text, { size: "sm", className: "mt-2 !text-zinc-500", children: "Select an element to edit its properties." })
@@ -1389,6 +1588,80 @@ function ElementInspector({ element, onPatch, onDelete, onLockToggle }) {
1389
1588
  ] }) })
1390
1589
  ] });
1391
1590
  }
1591
+ function SlideSettings({
1592
+ slide,
1593
+ onSetTransition,
1594
+ onSetBackground
1595
+ }) {
1596
+ const transition = slide.transition;
1597
+ const kind = transition?.kind ?? "none";
1598
+ const setTransition = (next) => {
1599
+ const merged = { kind, duration: transition?.duration, direction: transition?.direction, ...next };
1600
+ onSetTransition?.(merged.kind === "none" ? { kind: "none" } : merged);
1601
+ };
1602
+ return /* @__PURE__ */ 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: [
1603
+ /* @__PURE__ */ jsx("div", { className: "flex items-center justify-between border-b border-zinc-200 px-3 py-2 dark:border-zinc-800", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1604
+ /* @__PURE__ */ jsx(Heading, { as: "h3", size: "xs", className: "!font-mono !uppercase !tracking-wider !text-zinc-500", children: "slide" }),
1605
+ /* @__PURE__ */ jsxs(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: [
1606
+ "#",
1607
+ slide.id.slice(-6)
1608
+ ] })
1609
+ ] }) }),
1610
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-3", children: [
1611
+ /* @__PURE__ */ jsx(Card, { padding: "md", className: "!bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1612
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Transition" }),
1613
+ /* @__PURE__ */ jsx(
1614
+ Select,
1615
+ {
1616
+ label: "Kind",
1617
+ list: [
1618
+ { value: "none", label: "None" },
1619
+ { value: "fade", label: "Fade" },
1620
+ { value: "slide", label: "Slide" },
1621
+ { value: "zoom", label: "Zoom" }
1622
+ ],
1623
+ value: kind,
1624
+ onValueChange: (v) => setTransition({ kind: v })
1625
+ }
1626
+ ),
1627
+ kind === "slide" && /* @__PURE__ */ jsx(
1628
+ Select,
1629
+ {
1630
+ label: "Direction",
1631
+ list: [
1632
+ { value: "left", label: "From left" },
1633
+ { value: "right", label: "From right" },
1634
+ { value: "up", label: "From bottom" },
1635
+ { value: "down", label: "From top" }
1636
+ ],
1637
+ value: transition?.direction ?? "right",
1638
+ onValueChange: (v) => setTransition({ direction: v })
1639
+ }
1640
+ ),
1641
+ kind !== "none" && /* @__PURE__ */ jsx(
1642
+ Input,
1643
+ {
1644
+ label: "Duration (ms)",
1645
+ type: "number",
1646
+ value: String(transition?.duration ?? 400),
1647
+ onChange: (e) => setTransition({ duration: parseInt(e.target.value, 10) || 400 })
1648
+ }
1649
+ ),
1650
+ /* @__PURE__ */ jsx(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." })
1651
+ ] }) }),
1652
+ onSetBackground && /* @__PURE__ */ jsx(Card, { padding: "md", className: "mt-3 !bg-white dark:!bg-zinc-950", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1653
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Background" }),
1654
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsx(
1655
+ ColorPicker,
1656
+ {
1657
+ value: slide.background?.color ?? "#ffffff",
1658
+ onChange: (c) => onSetBackground({ ...slide.background, color: c })
1659
+ }
1660
+ ) })
1661
+ ] }) })
1662
+ ] })
1663
+ ] });
1664
+ }
1392
1665
  function LayoutSection({ element, onPatch }) {
1393
1666
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1394
1667
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
@@ -1482,7 +1755,35 @@ function TextStyleControls({ element, onPatch }) {
1482
1755
  ] });
1483
1756
  }
1484
1757
  function ImageStyleControls({ element, onPatch }) {
1758
+ const fileRef = useRef(null);
1759
+ const crop = element.crop;
1760
+ const onFile = (file) => {
1761
+ if (!file) return;
1762
+ const reader = new FileReader();
1763
+ reader.onload = () => {
1764
+ if (typeof reader.result === "string") onPatch({ src: reader.result });
1765
+ };
1766
+ reader.readAsDataURL(file);
1767
+ };
1768
+ const setCrop = (next) => {
1769
+ const base = crop ?? { x: 0, y: 0, w: 1, h: 1 };
1770
+ onPatch({ crop: { ...base, ...next } });
1771
+ };
1485
1772
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1773
+ /* @__PURE__ */ jsx(
1774
+ "input",
1775
+ {
1776
+ ref: fileRef,
1777
+ type: "file",
1778
+ accept: "image/*",
1779
+ className: "hidden",
1780
+ onChange: (e) => {
1781
+ onFile(e.target.files?.[0]);
1782
+ e.target.value = "";
1783
+ }
1784
+ }
1785
+ ),
1786
+ /* @__PURE__ */ jsx(Action, { size: "sm", variant: "ghost", icon: "upload", onClick: () => fileRef.current?.click(), children: "Upload image" }),
1486
1787
  /* @__PURE__ */ jsx(Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1487
1788
  /* @__PURE__ */ jsx(Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1488
1789
  /* @__PURE__ */ jsx(
@@ -1498,7 +1799,17 @@ function ImageStyleControls({ element, onPatch }) {
1498
1799
  value: element.fit ?? "contain",
1499
1800
  onValueChange: (v) => onPatch({ fit: v })
1500
1801
  }
1501
- )
1802
+ ),
1803
+ /* @__PURE__ */ jsx(Separator, {}),
1804
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
1805
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Crop" }),
1806
+ crop && /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", onClick: () => onPatch({ crop: void 0 }), children: "Clear crop" })
1807
+ ] }),
1808
+ /* @__PURE__ */ jsx(Slider, { label: "X", value: crop?.x ?? 0, onValueChange: (v) => setCrop({ x: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
1809
+ /* @__PURE__ */ jsx(Slider, { label: "Y", value: crop?.y ?? 0, onValueChange: (v) => setCrop({ y: Number(v) }), min: 0, max: 1, step: 0.01, showValue: true }),
1810
+ /* @__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 }),
1811
+ /* @__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 }),
1812
+ /* @__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." })
1502
1813
  ] });
1503
1814
  }
1504
1815
  function ShapeStyleControls({ element, onPatch }) {
@@ -1545,54 +1856,254 @@ function CodeStyleControls({ element, onPatch }) {
1545
1856
  ] });
1546
1857
  }
1547
1858
  function ChartStyleControls({ element, onPatch }) {
1859
+ const model = chartModelFromOption(element.option);
1860
+ const writeModel = (m) => onPatch({ option: chartOptionFromModel(m) });
1861
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1862
+ 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." }),
1863
+ /* @__PURE__ */ jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
1864
+ /* @__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" }),
1865
+ /* @__PURE__ */ jsx("div", { className: "p-2 pt-0", children: /* @__PURE__ */ jsx(
1866
+ Textarea,
1867
+ {
1868
+ label: "ECharts option (JSON)",
1869
+ value: JSON.stringify(element.option, null, 2),
1870
+ onValueChange: (v) => {
1871
+ try {
1872
+ onPatch({ option: JSON.parse(v) });
1873
+ } catch {
1874
+ }
1875
+ },
1876
+ rows: 10
1877
+ }
1878
+ ) })
1879
+ ] })
1880
+ ] });
1881
+ }
1882
+ var CHART_TYPE_OPTIONS = [
1883
+ { value: "bar", label: "Bar" },
1884
+ { value: "line", label: "Line" },
1885
+ { value: "area", label: "Area" },
1886
+ { value: "pie", label: "Pie" },
1887
+ { value: "scatter", label: "Scatter" }
1888
+ ];
1889
+ function ChartModelEditor({ model, onChange }) {
1890
+ const setKind = (kind) => {
1891
+ if (kind === model.kind) return;
1892
+ if (kind === "pie") {
1893
+ const first = model.series[0];
1894
+ 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 }];
1895
+ onChange({ ...model, kind, slices });
1896
+ return;
1897
+ }
1898
+ if (model.kind === "pie") {
1899
+ const categories = model.slices.length ? model.slices.map((s) => s.name) : ["A", "B", "C"];
1900
+ const values = model.slices.length ? model.slices.map((s) => s.value) : [1, 2, 3];
1901
+ onChange({ ...model, kind, categories, series: [{ name: "Series 1", color: chartColorAt(0), values }] });
1902
+ return;
1903
+ }
1904
+ onChange({ ...model, kind });
1905
+ };
1548
1906
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1549
- /* @__PURE__ */ jsx(Text, { size: "sm", className: "!text-zinc-500", children: "Chart option is JSON \u2014 paste any ECharts option here." }),
1550
1907
  /* @__PURE__ */ jsx(
1551
- Textarea,
1908
+ Select,
1552
1909
  {
1553
- label: "ECharts option (JSON)",
1554
- value: JSON.stringify(element.option, null, 2),
1555
- onValueChange: (v) => {
1556
- try {
1557
- onPatch({ option: JSON.parse(v) });
1558
- } catch {
1559
- }
1560
- },
1561
- rows: 10
1910
+ label: "Chart type",
1911
+ list: CHART_TYPE_OPTIONS,
1912
+ value: model.kind,
1913
+ onValueChange: (v) => setKind(v)
1562
1914
  }
1563
- )
1915
+ ),
1916
+ model.kind === "pie" ? /* @__PURE__ */ jsx(PieSliceEditor, { model, onChange }) : /* @__PURE__ */ jsx(CartesianChartEditor, { model, onChange })
1917
+ ] });
1918
+ }
1919
+ function PieSliceEditor({ model, onChange }) {
1920
+ const slices = model.slices;
1921
+ const update = (i, next) => {
1922
+ const copy = slices.map((s, idx) => idx === i ? { ...s, ...next } : s);
1923
+ onChange({ ...model, slices: copy });
1924
+ };
1925
+ const remove = (i) => onChange({ ...model, slices: slices.filter((_, idx) => idx !== i) });
1926
+ const add = () => onChange({ ...model, slices: [...slices, { name: `Slice ${slices.length + 1}`, value: 0 }] });
1927
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1928
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Slices" }),
1929
+ slices.map((s, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
1930
+ /* @__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 }) }) }),
1931
+ /* @__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 }) }) }),
1932
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => remove(i), "aria-label": "Remove slice" })
1933
+ ] }, i)),
1934
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: add, children: "Add slice" })
1935
+ ] });
1936
+ }
1937
+ function CartesianChartEditor({ model, onChange }) {
1938
+ const { categories, series } = model;
1939
+ const updateCategory = (i, label) => {
1940
+ onChange({ ...model, categories: categories.map((c, idx) => idx === i ? label : c) });
1941
+ };
1942
+ const removeCategory = (i) => {
1943
+ onChange({
1944
+ ...model,
1945
+ categories: categories.filter((_, idx) => idx !== i),
1946
+ series: series.map((s) => ({ ...s, values: s.values.filter((_, idx) => idx !== i) }))
1947
+ });
1948
+ };
1949
+ const addCategory = () => {
1950
+ onChange({
1951
+ ...model,
1952
+ categories: [...categories, `Cat ${categories.length + 1}`],
1953
+ series: series.map((s) => ({ ...s, values: [...s.values, 0] }))
1954
+ });
1955
+ };
1956
+ const updateSeries = (si, next) => {
1957
+ onChange({ ...model, series: series.map((s, idx) => idx === si ? { ...s, ...next } : s) });
1958
+ };
1959
+ const updateValue = (si, ci, value) => {
1960
+ onChange({
1961
+ ...model,
1962
+ series: series.map(
1963
+ (s, idx) => idx === si ? { ...s, values: s.values.map((v, vi) => vi === ci ? value : v) } : s
1964
+ )
1965
+ });
1966
+ };
1967
+ const removeSeries = (si) => onChange({ ...model, series: series.filter((_, idx) => idx !== si) });
1968
+ const addSeries = () => onChange({
1969
+ ...model,
1970
+ series: [
1971
+ ...series,
1972
+ { name: `Series ${series.length + 1}`, color: chartColorAt(series.length), values: categories.map(() => 0) }
1973
+ ]
1974
+ });
1975
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1976
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1977
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Categories" }),
1978
+ categories.map((c, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1979
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { value: c, onChange: (e) => updateCategory(i, e.target.value) }) }),
1980
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeCategory(i), "aria-label": "Remove category" })
1981
+ ] }, i)),
1982
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addCategory, children: "Add category" })
1983
+ ] }),
1984
+ /* @__PURE__ */ jsx(Separator, {}),
1985
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1986
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Series" }),
1987
+ series.map((s, si) => /* @__PURE__ */ jsxs("div", { className: "space-y-2 rounded-md border border-zinc-200 p-2 dark:border-zinc-800", children: [
1988
+ /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-2", children: [
1989
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(Input, { label: "Name", value: s.name, onChange: (e) => updateSeries(si, { name: e.target.value }) }) }),
1990
+ /* @__PURE__ */ jsx(FieldLabel, { label: "Color", children: /* @__PURE__ */ jsx(ColorPicker, { value: s.color ?? chartColorAt(si), onChange: (c) => updateSeries(si, { color: c }) }) }),
1991
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeSeries(si), "aria-label": "Remove series" })
1992
+ ] }),
1993
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2", children: categories.map((c, ci) => /* @__PURE__ */ jsx(
1994
+ Input,
1995
+ {
1996
+ label: c,
1997
+ type: "number",
1998
+ value: String(s.values[ci] ?? 0),
1999
+ onChange: (e) => updateValue(si, ci, parseFloat(e.target.value) || 0)
2000
+ },
2001
+ ci
2002
+ )) })
2003
+ ] }, si)),
2004
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addSeries, children: "Add series" })
2005
+ ] })
1564
2006
  ] });
1565
2007
  }
1566
2008
  function TableStyleControls({ element, onPatch }) {
2009
+ const columns = element.columns;
2010
+ const rows = element.rows;
2011
+ const nextColKey = () => {
2012
+ const existing = new Set(columns.map((c) => c.key));
2013
+ let n = columns.length + 1;
2014
+ while (existing.has(`col${n}`)) n++;
2015
+ return `col${n}`;
2016
+ };
2017
+ const setColumnLabel = (i, label) => {
2018
+ onPatch({ columns: columns.map((c, idx) => idx === i ? { ...c, label } : c) });
2019
+ };
2020
+ const removeColumn = (i) => {
2021
+ const key = columns[i]?.key;
2022
+ const nextCols = columns.filter((_, idx) => idx !== i);
2023
+ const nextRows = key ? rows.map((r) => {
2024
+ const { [key]: _drop, ...rest } = r;
2025
+ return rest;
2026
+ }) : rows;
2027
+ onPatch({ columns: nextCols, rows: nextRows });
2028
+ };
2029
+ const addColumn = () => {
2030
+ const key = nextColKey();
2031
+ onPatch({
2032
+ columns: [...columns, { key, label: `Column ${columns.length + 1}` }],
2033
+ rows: rows.map((r) => ({ ...r, [key]: "" }))
2034
+ });
2035
+ };
2036
+ const setCell = (rowIdx, key, value) => {
2037
+ onPatch({ rows: rows.map((r, idx) => idx === rowIdx ? { ...r, [key]: value } : r) });
2038
+ };
2039
+ const removeRow = (rowIdx) => onPatch({ rows: rows.filter((_, idx) => idx !== rowIdx) });
2040
+ const addRow = () => {
2041
+ const blank = {};
2042
+ for (const c of columns) blank[c.key] = "";
2043
+ onPatch({ rows: [...rows, blank] });
2044
+ };
1567
2045
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1568
- /* @__PURE__ */ jsx(
1569
- Textarea,
1570
- {
1571
- label: "Columns (JSON)",
1572
- value: JSON.stringify(element.columns, null, 2),
1573
- onValueChange: (v) => {
1574
- try {
1575
- onPatch({ columns: JSON.parse(v) });
1576
- } catch {
2046
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2047
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Columns" }),
2048
+ columns.map((c, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
2049
+ /* @__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` }) }),
2050
+ /* @__PURE__ */ jsx(Text, { size: "xs", className: "!font-mono !text-zinc-400", children: c.key }),
2051
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeColumn(i), "aria-label": "Remove column" })
2052
+ ] }, c.key)),
2053
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addColumn, children: "Add column" })
2054
+ ] }),
2055
+ /* @__PURE__ */ jsx(Separator, {}),
2056
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
2057
+ /* @__PURE__ */ jsx(Heading, { as: "h4", size: "xs", className: "!uppercase !tracking-wider !text-zinc-500", children: "Rows" }),
2058
+ columns.length === 0 ? /* @__PURE__ */ jsx(Text, { size: "xs", className: "!text-zinc-500", children: "Add a column to start adding rows." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2059
+ 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: [
2060
+ /* @__PURE__ */ jsx("div", { className: "grid flex-1 grid-cols-1 gap-1", children: columns.map((c) => /* @__PURE__ */ jsx(
2061
+ Input,
2062
+ {
2063
+ label: c.label,
2064
+ value: r[c.key] == null ? "" : String(r[c.key]),
2065
+ onChange: (e) => setCell(rowIdx, c.key, e.target.value)
2066
+ },
2067
+ c.key
2068
+ )) }),
2069
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", color: "red", icon: "x", onClick: () => removeRow(rowIdx), "aria-label": "Remove row" })
2070
+ ] }, rowIdx)),
2071
+ /* @__PURE__ */ jsx(Action, { size: "xs", variant: "ghost", icon: "plus", onClick: addRow, children: "Add row" })
2072
+ ] })
2073
+ ] }),
2074
+ /* @__PURE__ */ jsxs("details", { className: "rounded-md border border-zinc-200 dark:border-zinc-800", children: [
2075
+ /* @__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" }),
2076
+ /* @__PURE__ */ jsxs("div", { className: "space-y-3 p-2 pt-0", children: [
2077
+ /* @__PURE__ */ jsx(
2078
+ Textarea,
2079
+ {
2080
+ label: "Columns (JSON)",
2081
+ value: JSON.stringify(columns, null, 2),
2082
+ onValueChange: (v) => {
2083
+ try {
2084
+ onPatch({ columns: JSON.parse(v) });
2085
+ } catch {
2086
+ }
2087
+ },
2088
+ rows: 5
1577
2089
  }
1578
- },
1579
- rows: 5
1580
- }
1581
- ),
1582
- /* @__PURE__ */ jsx(
1583
- Textarea,
1584
- {
1585
- label: "Rows (JSON)",
1586
- value: JSON.stringify(element.rows, null, 2),
1587
- onValueChange: (v) => {
1588
- try {
1589
- onPatch({ rows: JSON.parse(v) });
1590
- } catch {
2090
+ ),
2091
+ /* @__PURE__ */ jsx(
2092
+ Textarea,
2093
+ {
2094
+ label: "Rows (JSON)",
2095
+ value: JSON.stringify(rows, null, 2),
2096
+ onValueChange: (v) => {
2097
+ try {
2098
+ onPatch({ rows: JSON.parse(v) });
2099
+ } catch {
2100
+ }
2101
+ },
2102
+ rows: 8
1591
2103
  }
1592
- },
1593
- rows: 8
1594
- }
1595
- )
2104
+ )
2105
+ ] })
2106
+ ] })
1596
2107
  ] });
1597
2108
  }
1598
2109
  function EmbedStyleControls({ element, onPatch }) {
@@ -1838,13 +2349,16 @@ function DeckEditor({
1838
2349
  ElementInspector,
1839
2350
  {
1840
2351
  element: selectedElement,
2352
+ slide: slide ?? null,
1841
2353
  onPatch: (patch) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, patch),
1842
2354
  onDelete: () => {
1843
2355
  if (!slide || !elementIdSelected) return;
1844
2356
  ops.removeElement(slide.id, elementIdSelected);
1845
2357
  setElementIdSelected(null);
1846
2358
  },
1847
- onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked })
2359
+ onLockToggle: (locked) => slide && elementIdSelected && ops.updateElement(slide.id, elementIdSelected, { locked }),
2360
+ onSetTransition: (transition) => slide && ops.setTransition(slide.id, transition),
2361
+ onSetBackground: (background) => slide && ops.setBackground(slide.id, background)
1848
2362
  }
1849
2363
  ) })
1850
2364
  ] }),