@particle-academy/fancy-slides 0.2.0 → 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.d.cts CHANGED
@@ -349,6 +349,14 @@ declare function TextElementRenderer({ element, theme, slideWidthPx, editing, se
349
349
  interface ImageElementRendererProps {
350
350
  element: ImageElement;
351
351
  }
352
+ /**
353
+ * Renders an image element. When `element.crop` is set (a window in
354
+ * image-relative 0..1 coords `{x,y,w,h}`), the image is shown cropped to that
355
+ * window: an `overflow:hidden` box clips an inner `<img>` that's been scaled up
356
+ * by `1/w` × `1/h` and offset so the crop window exactly fills the box. Without
357
+ * a crop it's the plain `object-fit` image. The same renderer feeds both the
358
+ * editor canvas and the viewer, so a crop is visible everywhere.
359
+ */
352
360
  declare function ImageElementRenderer({ element }: ImageElementRendererProps): react_jsx_runtime.JSX.Element;
353
361
 
354
362
  interface ShapeElementRendererProps {
package/dist/index.d.ts CHANGED
@@ -349,6 +349,14 @@ declare function TextElementRenderer({ element, theme, slideWidthPx, editing, se
349
349
  interface ImageElementRendererProps {
350
350
  element: ImageElement;
351
351
  }
352
+ /**
353
+ * Renders an image element. When `element.crop` is set (a window in
354
+ * image-relative 0..1 coords `{x,y,w,h}`), the image is shown cropped to that
355
+ * window: an `overflow:hidden` box clips an inner `<img>` that's been scaled up
356
+ * by `1/w` × `1/h` and offset so the crop window exactly fills the box. Without
357
+ * a crop it's the plain `object-fit` image. The same renderer feeds both the
358
+ * editor canvas and the viewer, so a crop is visible everywhere.
359
+ */
352
360
  declare function ImageElementRenderer({ element }: ImageElementRendererProps): react_jsx_runtime.JSX.Element;
353
361
 
354
362
  interface ShapeElementRendererProps {
package/dist/index.js CHANGED
@@ -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
@@ -1279,6 +1295,113 @@ function chartStarterOption(kind) {
1279
1295
  };
1280
1296
  }
1281
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
+ }
1282
1405
  function SlideRail({
1283
1406
  slides,
1284
1407
  selectedId,
@@ -1632,7 +1755,35 @@ function TextStyleControls({ element, onPatch }) {
1632
1755
  ] });
1633
1756
  }
1634
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
+ };
1635
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" }),
1636
1787
  /* @__PURE__ */ jsx(Textarea, { label: "Image URL", value: element.src, onValueChange: (v) => onPatch({ src: v }), rows: 2 }),
1637
1788
  /* @__PURE__ */ jsx(Input, { label: "Alt text", value: element.alt ?? "", onChange: (e) => onPatch({ alt: e.target.value }) }),
1638
1789
  /* @__PURE__ */ jsx(
@@ -1648,7 +1799,17 @@ function ImageStyleControls({ element, onPatch }) {
1648
1799
  value: element.fit ?? "contain",
1649
1800
  onValueChange: (v) => onPatch({ fit: v })
1650
1801
  }
1651
- )
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." })
1652
1813
  ] });
1653
1814
  }
1654
1815
  function ShapeStyleControls({ element, onPatch }) {
@@ -1695,54 +1856,254 @@ function CodeStyleControls({ element, onPatch }) {
1695
1856
  ] });
1696
1857
  }
1697
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
+ };
1698
1906
  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
1907
  /* @__PURE__ */ jsx(
1701
- Textarea,
1908
+ Select,
1702
1909
  {
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
1910
+ label: "Chart type",
1911
+ list: CHART_TYPE_OPTIONS,
1912
+ value: model.kind,
1913
+ onValueChange: (v) => setKind(v)
1712
1914
  }
1713
- )
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
+ ] })
1714
2006
  ] });
1715
2007
  }
1716
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
+ };
1717
2045
  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 {
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
1727
2089
  }
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 {
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
1741
2103
  }
1742
- },
1743
- rows: 8
1744
- }
1745
- )
2104
+ )
2105
+ ] })
2106
+ ] })
1746
2107
  ] });
1747
2108
  }
1748
2109
  function EmbedStyleControls({ element, onPatch }) {