react-email-studio 2.0.0 → 3.0.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
@@ -692,6 +692,10 @@ function makeContentBlock(type) {
692
692
  }
693
693
 
694
694
  // src/lib/columnPath.ts
695
+ var MAX_NESTED_LAYOUT_DEPTH = 1;
696
+ function nestedLayoutDepth(nested) {
697
+ return nested != null && nested.parentBlockIdx != null && nested.innerCellIdx != null ? 1 : 0;
698
+ }
695
699
  function isSplitLayoutBlock(b) {
696
700
  return b && b.type === "layout" && b.props && Array.isArray(b.props.cells);
697
701
  }
@@ -1283,8 +1287,18 @@ function normalizeEmailDocument(input) {
1283
1287
  settings: doc.settings && typeof doc.settings === "object" && !Array.isArray(doc.settings) ? doc.settings : {},
1284
1288
  rows: rows.map((r, ri) => {
1285
1289
  const columns = Array.isArray(r.columns) ? r.columns : [];
1286
- const colCount = typeof r.layout?.columns === "number" && r.layout.columns > 0 ? Math.floor(r.layout.columns) : Math.max(1, columns.length || 1);
1287
- const colsNormalized = columns.length > 0 ? columns : Array.from({ length: colCount }).map(() => ({}));
1290
+ const layoutColCount = typeof r.layout?.columns === "number" && r.layout.columns > 0 ? Math.floor(r.layout.columns) : 0;
1291
+ const colCount = Math.max(1, columns.length, layoutColCount);
1292
+ const colsNormalized = (() => {
1293
+ if (columns.length >= colCount) return columns;
1294
+ if (columns.length > 0) {
1295
+ return [
1296
+ ...columns,
1297
+ ...Array.from({ length: colCount - columns.length }).map(() => ({}))
1298
+ ];
1299
+ }
1300
+ return Array.from({ length: colCount }).map(() => ({}));
1301
+ })();
1288
1302
  return {
1289
1303
  id: ensureId(r.id, `row${ri + 1}`),
1290
1304
  type: "row",
@@ -1295,6 +1309,7 @@ function normalizeEmailDocument(input) {
1295
1309
  align: r.layout?.align || "center"
1296
1310
  },
1297
1311
  styles: r.styles && typeof r.styles === "object" && !Array.isArray(r.styles) ? r.styles : {},
1312
+ ...typeof r._reactEmailStudio === "object" && r._reactEmailStudio !== null && !Array.isArray(r._reactEmailStudio) ? { _reactEmailStudio: r._reactEmailStudio } : {},
1298
1313
  columns: colsNormalized.map((c, ci) => ({
1299
1314
  id: ensureId(c.id, `col${ri + 1}_${ci + 1}`),
1300
1315
  layout: c.layout && typeof c.layout === "object" && !Array.isArray(c.layout) ? c.layout : {},
@@ -1305,7 +1320,8 @@ function normalizeEmailDocument(input) {
1305
1320
  content: b?.content && typeof b.content === "object" && !Array.isArray(b.content) ? b.content : {},
1306
1321
  styles: b?.styles && typeof b.styles === "object" && !Array.isArray(b.styles) ? b.styles : {},
1307
1322
  behavior: b?.behavior && typeof b.behavior === "object" && !Array.isArray(b.behavior) ? b.behavior : {},
1308
- responsive: b?.responsive && typeof b.responsive === "object" && !Array.isArray(b.responsive) ? b.responsive : {}
1323
+ responsive: b?.responsive && typeof b.responsive === "object" && !Array.isArray(b.responsive) ? b.responsive : {},
1324
+ ...b?.props && typeof b.props === "object" && !Array.isArray(b.props) ? { props: b.props } : {}
1309
1325
  })) : []
1310
1326
  }))
1311
1327
  };
@@ -1316,6 +1332,14 @@ function normalizeEmailDocument(input) {
1316
1332
  }
1317
1333
 
1318
1334
  // src/lib/emailDesignJson.ts
1335
+ var MAX_LAYOUT_TREE_DEPTH = 32;
1336
+ function cloneJson(v) {
1337
+ try {
1338
+ return JSON.parse(JSON.stringify(v));
1339
+ } catch {
1340
+ return v;
1341
+ }
1342
+ }
1319
1343
  function layoutColumnPaddingForDoc(p) {
1320
1344
  if (typeof p === "number" && Number.isFinite(p)) {
1321
1345
  const n = Math.max(0, p);
@@ -1402,7 +1426,242 @@ function normalizeBoxStyles(props, t) {
1402
1426
  }
1403
1427
  props.padding = boxPad(props.padding, uniformPadDefault(def?.padding));
1404
1428
  }
1405
- function mapBlockToInternal(b) {
1429
+ function internalBlockToEmailDoc(b, depth = 0) {
1430
+ const p = b?.props && typeof b.props === "object" ? b.props : {};
1431
+ const id = typeof b?.id === "string" ? b.id : uid();
1432
+ const type = b?.type === "nestedRow" ? "layout" : b?.type;
1433
+ if (!isKnownBlockType(type)) {
1434
+ return { id, type: String(type || "text"), content: {}, styles: {} };
1435
+ }
1436
+ switch (type) {
1437
+ case "heading": {
1438
+ const fw = typeof p.fontWeight === "number" ? p.fontWeight : typeof p.fontWeight === "string" ? Number.parseInt(String(p.fontWeight), 10) || (p.bold ? 700 : 400) : p.bold ? 700 : 400;
1439
+ const padUniform = typeof p.padding === "number" && Number.isFinite(p.padding) ? p.padding : uniformPadDefault(p.padding);
1440
+ return {
1441
+ id,
1442
+ type,
1443
+ content: { text: typeof p.content === "string" ? p.content : "", tag: p.level || "h2" },
1444
+ styles: {
1445
+ fontSize: p.fontSize,
1446
+ fontWeight: fw,
1447
+ color: p.color,
1448
+ lineHeight: p.lineHeight,
1449
+ textAlign: p.align,
1450
+ letterSpacing: p.letterSpacing,
1451
+ marginBottom: padUniform
1452
+ }
1453
+ };
1454
+ }
1455
+ case "text":
1456
+ return {
1457
+ id,
1458
+ type,
1459
+ content: { html: typeof p.content === "string" ? p.content : "" },
1460
+ styles: {
1461
+ fontSize: p.fontSize,
1462
+ color: p.color,
1463
+ lineHeight: p.lineHeight,
1464
+ textAlign: p.align,
1465
+ ...p.padding && typeof p.padding === "object" && !Array.isArray(p.padding) ? { padding: layoutColumnPaddingForDoc(p.padding) } : {}
1466
+ }
1467
+ };
1468
+ case "html":
1469
+ return {
1470
+ id,
1471
+ type,
1472
+ content: { html: typeof p.content === "string" ? p.content : "" },
1473
+ styles: p.padding && typeof p.padding === "object" && !Array.isArray(p.padding) ? { padding: layoutColumnPaddingForDoc(p.padding) } : {}
1474
+ };
1475
+ case "image": {
1476
+ const br = p.borderRadius;
1477
+ const borderRadius = typeof br === "number" && Number.isFinite(br) ? br : br && typeof br === "object" && !Array.isArray(br) ? Math.round(
1478
+ (numOr(br.tl, 0) + numOr(br.tr, numOr(br.tl, 0)) + numOr(br.br, numOr(br.tl, 0)) + numOr(br.bl, numOr(br.tr, 0))) / 4
1479
+ ) : 0;
1480
+ return {
1481
+ id,
1482
+ type,
1483
+ content: {
1484
+ src: p.src || "",
1485
+ alt: p.alt || "",
1486
+ link: p.link || ""
1487
+ },
1488
+ styles: {
1489
+ width: p.width,
1490
+ align: p.align,
1491
+ borderRadius
1492
+ },
1493
+ ...p.linkTarget === "_blank" ? { behavior: { openInNewTab: true } } : {}
1494
+ };
1495
+ }
1496
+ case "button": {
1497
+ let borderRadiusOut;
1498
+ if (typeof p.borderRadius === "number" && Number.isFinite(p.borderRadius)) {
1499
+ borderRadiusOut = p.borderRadius;
1500
+ } else if (p.borderRadius && typeof p.borderRadius === "object" && !Array.isArray(p.borderRadius)) {
1501
+ const br = p.borderRadius;
1502
+ const tl = numOr(br.tl, 0);
1503
+ const tr2 = numOr(br.tr, tl);
1504
+ const brc = numOr(br.br, tl);
1505
+ const bl = numOr(br.bl, tr2);
1506
+ borderRadiusOut = tl === tr2 && tr2 === brc && brc === bl ? tl : Math.round((tl + tr2 + brc + bl) / 4);
1507
+ }
1508
+ return {
1509
+ id,
1510
+ type,
1511
+ content: { text: p.label || "", href: p.href || "#" },
1512
+ styles: {
1513
+ backgroundColor: p.bgColor,
1514
+ color: p.textColor,
1515
+ fontSize: p.fontSize,
1516
+ fontWeight: p.fontWeight,
1517
+ ...borderRadiusOut !== void 0 ? { borderRadius: borderRadiusOut } : {},
1518
+ textAlign: p.align,
1519
+ padding: {
1520
+ top: p.paddingV ?? 11,
1521
+ right: p.paddingH ?? 24,
1522
+ bottom: p.paddingV ?? 11,
1523
+ left: p.paddingH ?? 24
1524
+ }
1525
+ },
1526
+ ...p.linkTarget === "_blank" ? { behavior: { openInNewTab: true } } : {}
1527
+ };
1528
+ }
1529
+ case "divider": {
1530
+ const padY = typeof p.padding === "number" && Number.isFinite(p.padding) ? p.padding : p.padding && typeof p.padding === "object" && !Array.isArray(p.padding) ? numOr(p.padding.top, 12) : 12;
1531
+ return {
1532
+ id,
1533
+ type,
1534
+ content: {},
1535
+ styles: { color: p.color, thickness: p.thickness, paddingY: padY }
1536
+ };
1537
+ }
1538
+ case "spacer":
1539
+ return { id, type, content: {}, styles: { height: p.height } };
1540
+ case "social": {
1541
+ const urls = p.socialUrls && typeof p.socialUrls === "object" && !Array.isArray(p.socialUrls) ? p.socialUrls : {};
1542
+ const networks = {};
1543
+ const ordered = Array.isArray(p.networks) ? p.networks.filter((n) => typeof n === "string" && !!n) : [];
1544
+ const seen = /* @__PURE__ */ new Set();
1545
+ for (const n of ordered) {
1546
+ const u = urls[n];
1547
+ networks[n] = typeof u === "string" && u.trim() ? u : "";
1548
+ seen.add(n);
1549
+ }
1550
+ for (const key of Object.keys(urls)) {
1551
+ if (seen.has(key)) continue;
1552
+ const v = urls[key];
1553
+ if (typeof v === "string" && v.trim()) {
1554
+ networks[key] = v;
1555
+ seen.add(key);
1556
+ }
1557
+ }
1558
+ const padStyles = p.padding != null ? typeof p.padding === "number" && Number.isFinite(p.padding) ? { padding: layoutColumnPaddingForDoc(p.padding) } : p.padding && typeof p.padding === "object" && !Array.isArray(p.padding) ? { padding: layoutColumnPaddingForDoc(p.padding) } : {} : {};
1559
+ return {
1560
+ id,
1561
+ type,
1562
+ content: { networks },
1563
+ styles: {
1564
+ iconSize: p.iconSize,
1565
+ gap: p.gap,
1566
+ shape: p.shape,
1567
+ align: p.align,
1568
+ ...padStyles
1569
+ }
1570
+ };
1571
+ }
1572
+ case "table":
1573
+ return {
1574
+ id,
1575
+ type,
1576
+ content: { data: Array.isArray(p.data) ? p.data : [] },
1577
+ styles: {
1578
+ borderColor: p.cellBorder,
1579
+ cellPadding: p.cellPadding,
1580
+ headerBg: p.headerBg,
1581
+ striped: p.striped
1582
+ }
1583
+ };
1584
+ case "video":
1585
+ return {
1586
+ id,
1587
+ type,
1588
+ content: { url: p.src || "" },
1589
+ styles: { height: p.height }
1590
+ };
1591
+ case "timer": {
1592
+ let timerBr;
1593
+ if (typeof p.borderRadius === "number" && Number.isFinite(p.borderRadius)) {
1594
+ timerBr = p.borderRadius;
1595
+ } else if (p.borderRadius && typeof p.borderRadius === "object" && !Array.isArray(p.borderRadius)) {
1596
+ const br = p.borderRadius;
1597
+ const tl = numOr(br.tl, 0);
1598
+ const tr2 = numOr(br.tr, tl);
1599
+ const brc = numOr(br.br, tl);
1600
+ const bl = numOr(br.bl, tr2);
1601
+ timerBr = tl === tr2 && tr2 === brc && brc === bl ? tl : Math.round((tl + tr2 + brc + bl) / 4);
1602
+ }
1603
+ return {
1604
+ id,
1605
+ type,
1606
+ content: { endDate: p.endDate || "" },
1607
+ styles: {
1608
+ backgroundColor: p.bgColor,
1609
+ color: p.textColor,
1610
+ padding: typeof p.padding === "number" ? p.padding : uniformPadDefault(p.padding),
1611
+ ...timerBr !== void 0 ? { borderRadius: timerBr } : {},
1612
+ textAlign: p.align
1613
+ }
1614
+ };
1615
+ }
1616
+ case "menu":
1617
+ return {
1618
+ id,
1619
+ type,
1620
+ content: { items: Array.isArray(p.items) ? p.items : [] },
1621
+ styles: { fontSize: p.fontSize, color: p.color }
1622
+ };
1623
+ case "link":
1624
+ return {
1625
+ id,
1626
+ type,
1627
+ content: { text: p.label || "", href: p.href || "#" },
1628
+ styles: { color: p.color, fontSize: p.fontSize, textAlign: p.align },
1629
+ ...p.linkTarget === "_blank" ? { behavior: { openInNewTab: true } } : {}
1630
+ };
1631
+ case "layout": {
1632
+ const ratios = Array.isArray(p.ratios) ? [...p.ratios] : [1];
1633
+ const emptyCells = ratios.map(() => []);
1634
+ const rawCells = Array.isArray(p.cells) ? p.cells : [];
1635
+ const cellsOut = depth >= MAX_LAYOUT_TREE_DEPTH ? emptyCells : ratios.map((_, i) => {
1636
+ const col = rawCells[i];
1637
+ return Array.isArray(col) ? col.map((child) => internalBlockToEmailDoc(child, depth + 1)) : [];
1638
+ });
1639
+ return {
1640
+ id,
1641
+ type,
1642
+ content: {
1643
+ preset: p.preset,
1644
+ cols: p.cols,
1645
+ ratios,
1646
+ gap: p.gap,
1647
+ padding: p.padding,
1648
+ bgColor: p.bgColor,
1649
+ bgImage: p.bgImage,
1650
+ bgSize: p.bgSize,
1651
+ bgRepeat: p.bgRepeat,
1652
+ bgPosition: p.bgPosition,
1653
+ bgGradient: p.bgGradient ?? null,
1654
+ columnStyles: p.columnStyles,
1655
+ cells: cellsOut
1656
+ },
1657
+ styles: {}
1658
+ };
1659
+ }
1660
+ default:
1661
+ return { id, type, content: {}, styles: {} };
1662
+ }
1663
+ }
1664
+ function mapBlockToInternal(b, layoutDepth = 0) {
1406
1665
  const t = b.type;
1407
1666
  if (!isKnownBlockType(t)) return null;
1408
1667
  const block = makeContentBlock(t);
@@ -1473,8 +1732,28 @@ function mapBlockToInternal(b) {
1473
1732
  if (asNum(s.height) != null) block.props.height = s.height;
1474
1733
  break;
1475
1734
  case "social": {
1476
- const networksObj = c.networks && typeof c.networks === "object" ? c.networks : {};
1477
- const keys = Object.keys(networksObj || {});
1735
+ const networksRaw = c.networks;
1736
+ let networksObj = {};
1737
+ if (Array.isArray(networksRaw)) {
1738
+ const urlMap = c.socialUrls && typeof c.socialUrls === "object" && !Array.isArray(c.socialUrls) ? c.socialUrls : {};
1739
+ for (const n of networksRaw) {
1740
+ if (typeof n !== "string" || !n) continue;
1741
+ const u = urlMap[n];
1742
+ networksObj[n] = typeof u === "string" ? u : "";
1743
+ }
1744
+ for (const key of Object.keys(urlMap)) {
1745
+ if (key in networksObj) continue;
1746
+ const u = urlMap[key];
1747
+ if (typeof u === "string" && u.trim()) networksObj[key] = u;
1748
+ }
1749
+ } else if (networksRaw && typeof networksRaw === "object" && !Array.isArray(networksRaw)) {
1750
+ networksObj = { ...networksRaw };
1751
+ for (const k of Object.keys(networksObj)) {
1752
+ const v = networksObj[k];
1753
+ networksObj[k] = typeof v === "string" ? v : "";
1754
+ }
1755
+ }
1756
+ const keys = Object.keys(networksObj);
1478
1757
  if (keys.length) {
1479
1758
  block.props.networks = keys;
1480
1759
  block.props.socialUrls = networksObj;
@@ -1483,6 +1762,13 @@ function mapBlockToInternal(b) {
1483
1762
  if (asNum(s.gap) != null) block.props.gap = s.gap;
1484
1763
  if (asStr(s.shape)) block.props.shape = s.shape;
1485
1764
  if (asStr(s.align)) block.props.align = s.align;
1765
+ if (s.padding != null) {
1766
+ if (typeof s.padding === "number" && Number.isFinite(s.padding)) {
1767
+ block.props.padding = s.padding;
1768
+ } else if (s.padding && typeof s.padding === "object" && !Array.isArray(s.padding)) {
1769
+ block.props.padding = layoutColumnPaddingForDoc(s.padding);
1770
+ }
1771
+ }
1486
1772
  break;
1487
1773
  }
1488
1774
  case "table":
@@ -1523,15 +1809,81 @@ function mapBlockToInternal(b) {
1523
1809
  if (asNum(s.fontSize) != null) block.props.fontSize = s.fontSize;
1524
1810
  if (asStr(s.textAlign)) block.props.align = s.textAlign;
1525
1811
  break;
1526
- case "layout":
1812
+ case "layout": {
1813
+ if (Array.isArray(c.cells)) {
1814
+ const d = DEFAULT_BLOCK_PROPS.layout;
1815
+ block.props.preset = asStr(c.preset) ?? block.props.preset;
1816
+ if (typeof c.cols === "number") block.props.cols = c.cols;
1817
+ if (Array.isArray(c.ratios)) block.props.ratios = [...c.ratios];
1818
+ if (typeof c.gap === "number") block.props.gap = c.gap;
1819
+ if (c.padding !== void 0 && c.padding !== null) block.props.padding = c.padding;
1820
+ if (typeof c.bgColor === "string") block.props.bgColor = c.bgColor;
1821
+ if (typeof c.bgImage === "string") block.props.bgImage = c.bgImage;
1822
+ if (typeof c.bgSize === "string") block.props.bgSize = c.bgSize;
1823
+ if (typeof c.bgRepeat === "string") block.props.bgRepeat = c.bgRepeat;
1824
+ if (typeof c.bgPosition === "string") block.props.bgPosition = c.bgPosition;
1825
+ if (c.bgGradient !== void 0) block.props.bgGradient = c.bgGradient;
1826
+ if (c.columnStyles && typeof c.columnStyles === "object" && !Array.isArray(c.columnStyles)) {
1827
+ block.props.columnStyles = c.columnStyles;
1828
+ }
1829
+ block.props.cells = c.cells.map(
1830
+ (col) => Array.isArray(col) ? col.map((raw) => mapEmbeddedLayoutCellBlock(raw, layoutDepth + 1)).filter((x) => x != null) : []
1831
+ );
1832
+ } else {
1833
+ block.props.preset = asStr(c.preset) ?? block.props.preset;
1834
+ }
1527
1835
  break;
1836
+ }
1528
1837
  default:
1529
1838
  break;
1530
1839
  }
1840
+ const styleObj = s && typeof s === "object" && !Array.isArray(s) ? s : {};
1841
+ const hasDocStyles = Object.keys(styleObj).some((k) => {
1842
+ const v = styleObj[k];
1843
+ return v !== void 0 && v !== null && v !== "";
1844
+ });
1845
+ const skipContentMergeKeys = {
1846
+ heading: /* @__PURE__ */ new Set(["text", "tag"]),
1847
+ text: /* @__PURE__ */ new Set(["html", "text"]),
1848
+ html: /* @__PURE__ */ new Set(["html"]),
1849
+ image: /* @__PURE__ */ new Set(["src", "alt", "link"]),
1850
+ button: /* @__PURE__ */ new Set(["text", "href"]),
1851
+ link: /* @__PURE__ */ new Set(["text", "href"]),
1852
+ social: /* @__PURE__ */ new Set(["networks", "socialUrls"]),
1853
+ table: /* @__PURE__ */ new Set(["data"]),
1854
+ menu: /* @__PURE__ */ new Set(["items"]),
1855
+ video: /* @__PURE__ */ new Set(["url"]),
1856
+ timer: /* @__PURE__ */ new Set(["endDate"])
1857
+ };
1858
+ if (!hasDocStyles && c && typeof c === "object" && t !== "layout") {
1859
+ const known = DEFAULT_BLOCK_PROPS[t];
1860
+ const skip = skipContentMergeKeys[t];
1861
+ if (known && typeof known === "object" && !Array.isArray(known)) {
1862
+ for (const key of Object.keys(known)) {
1863
+ if (skip?.has(key)) continue;
1864
+ if (key in c && c[key] !== void 0) {
1865
+ block.props[key] = c[key];
1866
+ }
1867
+ }
1868
+ }
1869
+ }
1870
+ applyImportedEditorPropsFromDoc(block, t, b, layoutDepth);
1531
1871
  return block;
1532
1872
  }
1533
1873
  function rowToInternal(r) {
1534
- const cols = Math.max(1, r.layout?.columns || r.columns?.length || 1);
1874
+ const columns = Array.isArray(r.columns) ? r.columns : [];
1875
+ const layoutColCount = typeof r.layout?.columns === "number" && r.layout.columns > 0 ? Math.floor(r.layout.columns) : 0;
1876
+ const cols = Math.max(1, columns.length, layoutColCount);
1877
+ const colList = (() => {
1878
+ if (columns.length >= cols) return columns;
1879
+ if (columns.length > 0) {
1880
+ return [
1881
+ ...columns,
1882
+ ...Array.from({ length: cols - columns.length }).map(() => ({}))
1883
+ ];
1884
+ }
1885
+ return Array.from({ length: cols }).map(() => ({}));
1886
+ })();
1535
1887
  const preset = pickPresetByColCount(cols) || LAYOUT_PRESETS[0];
1536
1888
  const row = makeLayoutRow(preset);
1537
1889
  row.id = typeof r.id === "string" ? r.id : uid();
@@ -1544,14 +1896,14 @@ function rowToInternal(r) {
1544
1896
  row.bgSize = r.styles?.backgroundSize || row.bgSize;
1545
1897
  row.bgPosition = r.styles?.backgroundPosition || row.bgPosition || "center";
1546
1898
  row.bgGradient = r.styles?.backgroundGradient || row.bgGradient || null;
1547
- const columns = Array.isArray(r.columns) ? r.columns : [];
1548
- const colList = columns.length ? columns : Array.from({ length: cols }).map(() => ({}));
1549
- row.cells = colList.slice(0, cols).map((c) => {
1899
+ row.cells = colList.map((c) => {
1550
1900
  const blocks = Array.isArray(c.blocks) ? c.blocks : [];
1551
- return blocks.map(mapBlockToInternal).filter((x) => x != null);
1901
+ return blocks.map((blk) => mapBlockToInternal(blk, 0)).filter((x) => x != null);
1552
1902
  });
1553
- const columnStyles = {};
1554
- colList.slice(0, cols).forEach((c, i) => {
1903
+ const studio = r._reactEmailStudio;
1904
+ const snap = studio?.row;
1905
+ const columnStylesFromDoc = {};
1906
+ colList.forEach((c, i) => {
1555
1907
  const bgColor = typeof c.styles?.backgroundColor === "string" ? c.styles.backgroundColor : "";
1556
1908
  const padding = layoutColumnPaddingForDoc(c.styles?.padding);
1557
1909
  const borderRadius = layoutColumnBorderRadiusForDoc(c.styles?.borderRadius);
@@ -1563,17 +1915,30 @@ function rowToInternal(r) {
1563
1915
  const hasPad = columnPaddingNonZero(padding);
1564
1916
  const hasBr = columnRadiusNonZero(borderRadius);
1565
1917
  if (bgColor || hasPad || hasBr || bgImage || bgRepeat || bgSize || bgPosition || bgGradient) {
1566
- columnStyles[i] = { bgColor, padding, borderRadius, bgImage, bgRepeat, bgSize, bgPosition, bgGradient };
1918
+ columnStylesFromDoc[i] = { bgColor, padding, borderRadius, bgImage, bgRepeat, bgSize, bgPosition, bgGradient };
1567
1919
  }
1568
1920
  });
1569
- if (Object.keys(columnStyles).length) row.columnStyles = columnStyles;
1921
+ if (snap && typeof snap === "object" && !Array.isArray(snap)) {
1922
+ const { cells: _omitCells, columnStyles: snapCs, ...rest } = snap;
1923
+ Object.assign(row, rest);
1924
+ const snapColumn = snapCs && typeof snapCs === "object" && !Array.isArray(snapCs) ? snapCs : {};
1925
+ row.columnStyles = { ...snapColumn, ...columnStylesFromDoc };
1926
+ if (!Object.keys(row.columnStyles).length) delete row.columnStyles;
1927
+ } else if (Object.keys(columnStylesFromDoc).length) {
1928
+ row.columnStyles = columnStylesFromDoc;
1929
+ }
1570
1930
  return row;
1571
1931
  }
1572
1932
  function normalizeEmailDesignInput(input) {
1573
1933
  const doc = normalizeEmailDocument(input);
1574
1934
  if (!doc) return null;
1575
1935
  const s = doc.settings || {};
1576
- const settings = {
1936
+ const studioSettings = s._reactEmailStudio;
1937
+ const settings = {};
1938
+ if (studioSettings?.editorSettings && typeof studioSettings.editorSettings === "object" && !Array.isArray(studioSettings.editorSettings)) {
1939
+ Object.assign(settings, cloneJson(studioSettings.editorSettings));
1940
+ }
1941
+ Object.assign(settings, {
1577
1942
  bgColor: s.backgroundColor || "#f1f5f9",
1578
1943
  bgImage: s.backgroundImage || "",
1579
1944
  bgRepeat: s.backgroundRepeat || "no-repeat",
@@ -1587,15 +1952,15 @@ function normalizeEmailDesignInput(input) {
1587
1952
  contentBgPosition: s.contentBackgroundPosition || "center",
1588
1953
  contentBgGradient: s.contentBackgroundGradient || null,
1589
1954
  contentWidth: typeof s.width === "number" ? s.width : 600,
1590
- padding: 24,
1591
- borderRadius: 8,
1955
+ padding: typeof studioSettings?.contentPadding === "number" ? studioSettings.contentPadding : typeof settings.padding === "number" ? settings.padding : 24,
1956
+ borderRadius: typeof studioSettings?.contentBorderRadius === "number" ? studioSettings.contentBorderRadius : typeof settings.borderRadius === "number" ? settings.borderRadius : 8,
1592
1957
  pageFontFamily: s.fontFamily,
1593
1958
  pageTextColor: s.color,
1594
1959
  pageLineHeight: s.lineHeightBase != null ? String(s.lineHeightBase) : void 0,
1595
1960
  pageResponsive: s.responsive,
1596
1961
  pageRtl: s.rtl,
1597
1962
  fontFamily: s.fontFamily
1598
- };
1963
+ });
1599
1964
  const rows = (doc.rows || []).map(rowToInternal);
1600
1965
  return withHydratedRows({ rows, settings, __emailDocument: doc });
1601
1966
  }
@@ -1619,14 +1984,29 @@ function designToEmailDocument(rows, settings) {
1619
1984
  fontFamily: settings.fontFamily || settings.pageFontFamily || "Arial, Helvetica, sans-serif",
1620
1985
  lineHeightBase: settings.pageLineHeight ? Number(settings.pageLineHeight) : 1.6,
1621
1986
  color: settings.pageTextColor || "#111827",
1622
- responsive: true,
1623
- rtl: false
1987
+ responsive: typeof settings.pageResponsive === "boolean" ? settings.pageResponsive : true,
1988
+ rtl: !!settings.pageRtl,
1989
+ _reactEmailStudio: {
1990
+ contentPadding: settings.padding ?? 24,
1991
+ contentBorderRadius: settings.borderRadius ?? 8,
1992
+ editorSettings: cloneJson(settings)
1993
+ }
1624
1994
  },
1625
1995
  rows: rows.map((r, ri) => {
1626
- const cols = Math.max(1, r.cols || r.cells?.length || 1);
1996
+ const cellArrays = Array.isArray(r.cells) ? r.cells : [];
1997
+ const declaredCols = typeof r.cols === "number" && r.cols > 0 ? r.cols : 0;
1998
+ const cols = Math.max(1, cellArrays.length, declaredCols);
1999
+ const rowPad = r.padding && typeof r.padding === "object" && !Array.isArray(r.padding) ? layoutColumnPaddingForDoc(r.padding) : (() => {
2000
+ const n = typeof r.padding === "number" && Number.isFinite(r.padding) ? r.padding : 0;
2001
+ return { top: n, right: n, bottom: n, left: n };
2002
+ })();
2003
+ const { cells: _rowCells, ...rowEditorSnap } = r;
1627
2004
  return {
1628
2005
  id: r.id || `row_${ri + 1}`,
1629
2006
  type: "row",
2007
+ _reactEmailStudio: {
2008
+ row: cloneJson(rowEditorSnap)
2009
+ },
1630
2010
  layout: {
1631
2011
  columns: cols,
1632
2012
  gap: r.gap ?? 0,
@@ -1640,13 +2020,14 @@ function designToEmailDocument(rows, settings) {
1640
2020
  backgroundSize: r.bgSize || "cover",
1641
2021
  backgroundPosition: r.bgPosition || "center",
1642
2022
  backgroundGradient: r.bgGradient || null,
1643
- padding: { top: r.padding ?? 0, right: r.padding ?? 0, bottom: r.padding ?? 0, left: r.padding ?? 0 },
2023
+ padding: rowPad,
1644
2024
  borderRadius: 0,
1645
2025
  borderWidth: 0,
1646
2026
  borderColor: "#e5e7eb",
1647
2027
  textAlign: "left"
1648
2028
  },
1649
- columns: (r.cells || []).slice(0, cols).map((cellBlocks, ci) => {
2029
+ columns: Array.from({ length: cols }, (_, ci) => {
2030
+ const cellBlocks = cellArrays[ci] ?? [];
1650
2031
  const cs = r.columnStyles && r.columnStyles[ci] || {};
1651
2032
  return {
1652
2033
  id: `col_${ri + 1}_${ci + 1}`,
@@ -1661,12 +2042,7 @@ function designToEmailDocument(rows, settings) {
1661
2042
  backgroundGradient: cs.bgGradient || null,
1662
2043
  borderRadius: layoutColumnBorderRadiusForDoc(cs.borderRadius)
1663
2044
  },
1664
- blocks: (cellBlocks || []).map((b) => ({
1665
- id: b.id,
1666
- type: b.type,
1667
- content: b.props,
1668
- styles: {}
1669
- }))
2045
+ blocks: (cellBlocks || []).map((b) => exportEmailDocBlock(b))
1670
2046
  };
1671
2047
  })
1672
2048
  };
@@ -1674,14 +2050,29 @@ function designToEmailDocument(rows, settings) {
1674
2050
  };
1675
2051
  return doc;
1676
2052
  }
1677
- function hydrateBlock(b) {
2053
+ function hydrateBlock(b, depth = 0) {
1678
2054
  if (!b || typeof b !== "object") return null;
1679
2055
  const t = b.type === "nestedRow" ? "layout" : b.type;
1680
2056
  if (!isKnownBlockType(t)) return null;
1681
2057
  if (t === "layout" && b.props && Array.isArray(b.props.cells)) {
1682
2058
  const defaults2 = DEFAULT_BLOCK_PROPS.layout;
2059
+ if (depth >= MAX_LAYOUT_TREE_DEPTH) {
2060
+ const ratios = Array.isArray(b.props.ratios) && b.props.ratios.length ? [...b.props.ratios] : [1];
2061
+ return {
2062
+ ...b,
2063
+ type: "layout",
2064
+ id: typeof b.id === "string" && b.id ? b.id : uid(),
2065
+ props: {
2066
+ ...defaults2,
2067
+ ...b.props,
2068
+ bgSize: b.props.bgSize ?? defaults2.bgSize,
2069
+ bgRepeat: b.props.bgRepeat ?? defaults2.bgRepeat,
2070
+ cells: ratios.map(() => [])
2071
+ }
2072
+ };
2073
+ }
1683
2074
  const cells = b.props.cells.map(
1684
- (col) => Array.isArray(col) ? col.map(hydrateBlock).filter((x) => x != null) : []
2075
+ (col) => Array.isArray(col) ? col.map((x) => hydrateBlock(x, depth + 1)).filter((x) => x != null) : []
1685
2076
  );
1686
2077
  return {
1687
2078
  ...b,
@@ -1712,6 +2103,54 @@ function hydrateBlock(b) {
1712
2103
  props: merged
1713
2104
  };
1714
2105
  }
2106
+ function mapEmbeddedLayoutCellBlock(raw, layoutDepth = 0) {
2107
+ if (!raw || typeof raw !== "object") return null;
2108
+ const r = raw;
2109
+ if (r.props && typeof r.props === "object" && typeof r.type === "string" && !("styles" in r)) {
2110
+ return hydrateBlock(r, layoutDepth);
2111
+ }
2112
+ return mapBlockToInternal(r, layoutDepth);
2113
+ }
2114
+ function applyImportedEditorPropsFromDoc(block, t, b, layoutDepth = 0) {
2115
+ const exp = b.props;
2116
+ if (!exp || typeof exp !== "object" || Array.isArray(exp)) return;
2117
+ if (t === "layout" && Array.isArray(exp.cells)) {
2118
+ const hb = hydrateBlock({ type: "layout", id: block.id, props: exp }, layoutDepth);
2119
+ if (hb) {
2120
+ block.props = hb.props;
2121
+ if (typeof hb.id === "string" && hb.id) block.id = hb.id;
2122
+ }
2123
+ return;
2124
+ }
2125
+ Object.assign(block.props, exp);
2126
+ normalizeBoxStyles(block.props, t);
2127
+ if (t === "html" || t === "text") {
2128
+ block.props.content = normalizeRichHtmlForStorage(String(block.props.content ?? ""));
2129
+ }
2130
+ }
2131
+ function exportEmailDocBlock(b, depth = 0) {
2132
+ if (!b || typeof b !== "object") return { id: uid(), type: "text", content: {}, styles: {} };
2133
+ const doc = internalBlockToEmailDoc(b, depth);
2134
+ const out = { ...doc };
2135
+ if (b.props && typeof b.props === "object" && !Array.isArray(b.props)) {
2136
+ out.props = cloneJson(b.props);
2137
+ }
2138
+ if (b.type === "layout" && Array.isArray(b.props?.cells)) {
2139
+ const baseContent = typeof doc.content === "object" && doc.content ? doc.content : {};
2140
+ if (depth >= MAX_LAYOUT_TREE_DEPTH) {
2141
+ const ratios = Array.isArray(b.props.ratios) && b.props.ratios.length ? b.props.ratios : [1];
2142
+ out.content = { ...baseContent, cells: ratios.map(() => []) };
2143
+ } else {
2144
+ out.content = {
2145
+ ...baseContent,
2146
+ cells: b.props.cells.map(
2147
+ (col) => Array.isArray(col) ? col.map((child) => exportEmailDocBlock(child, depth + 1)) : []
2148
+ )
2149
+ };
2150
+ }
2151
+ }
2152
+ return out;
2153
+ }
1715
2154
  function hydrateLayoutRow(row) {
1716
2155
  if (!row || typeof row !== "object") return row;
1717
2156
  const cells = (row.cells || []).map(
@@ -4443,32 +4882,42 @@ function ContentBlockEditor({ block, onChange, mergeTags, onClose, onUpload, C }
4443
4882
  ] });
4444
4883
  case "text":
4445
4884
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
4446
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_react4.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4447
- TextRichEditor,
4885
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PR, { label: "Content (HTML)", C, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
4886
+ "textarea",
4448
4887
  {
4888
+ style: { ...IS, minHeight: 200, resize: "vertical", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", fontSize: 12, lineHeight: 1.45 },
4449
4889
  value: normalizeRichHtmlForStorage(p.content),
4450
- onChange: (html) => set("content", html),
4451
- typography: {
4452
- fontSize: p.fontSize,
4453
- color: p.color,
4454
- align: p.align,
4455
- fontFamily: p.fontFamily,
4456
- lineHeight: p.lineHeight,
4457
- letterSpacing: p.letterSpacing,
4458
- padding: p.padding
4890
+ onChange: (e) => set("content", normalizeRichHtmlForStorage(e.target.value)),
4891
+ spellCheck: false,
4892
+ placeholder: "e.g. <p>Your message</p>"
4893
+ },
4894
+ block.id
4895
+ ) }),
4896
+ mergeTags && mergeTags.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PR, { label: "Insert merge tag", C, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
4897
+ "select",
4898
+ {
4899
+ style: IS,
4900
+ defaultValue: "",
4901
+ onChange: (e) => {
4902
+ const v = e.target.value;
4903
+ if (v) {
4904
+ set("content", normalizeRichHtmlForStorage((p.content || "") + " " + v));
4905
+ e.target.value = "";
4906
+ }
4459
4907
  },
4460
- mergeTags,
4461
- onMergeTagInsert: (tag) => set("content", (p.content || "") + " " + tag),
4462
- placeholder: "Write your message\u2026",
4463
- headerTitle: "Rich text",
4464
- C
4908
+ children: [
4909
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "", children: "Choose\u2026" }),
4910
+ mergeTags.map((t) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: t.value, children: t.name }, t.name))
4911
+ ]
4465
4912
  }
4466
- ) }, block.id) }),
4913
+ ) }) : null,
4467
4914
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NumRangePx, { label: "Font size", value: p.fontSize ?? 15, onChange: (n) => set("fontSize", n), min: 8, max: 96, step: 1, C }),
4468
4915
  Col("color", "Color"),
4469
4916
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AlignButtons, { value: p.align, onChange: (v) => set("align", v), options: ["left", "center", "right", "justify"], C }),
4470
4917
  Sel("fontWeight", "Weight", ["400", "500", "600", "700", "800"]),
4471
4918
  Sel("fontFamily", "Font", FONTS2),
4919
+ Tog("italic", "Italic"),
4920
+ Tog("underline", "Underline"),
4472
4921
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PR, { label: "Line height", C, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "number", style: useIS(C).IS, min: 1, max: 4, step: 0.05, value: p.lineHeight ?? 1.65, onChange: (e) => set("lineHeight", +e.target.value) }) }),
4473
4922
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(NumRangePx, { label: "Letter spacing", value: p.letterSpacing ?? 0, onChange: (n) => set("letterSpacing", n), min: 0, max: 30, step: 0.5, C }),
4474
4923
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(BlockSurfaceBgInspector, { variant: "surface", p, set, C, onUpload, syncKey: block.id }),
@@ -6304,6 +6753,9 @@ var ReactEmailEditor = (0, import_react8.forwardRef)(
6304
6753
  const nested = action.nested;
6305
6754
  const toLoc = toColumnLoc(rowId, cellIdx, nested);
6306
6755
  if (action.kind === "new") {
6756
+ if ((action.contentType === "layout" || action.contentType === "nestedRow") && nestedLayoutDepth(nested) >= MAX_NESTED_LAYOUT_DEPTH) {
6757
+ return;
6758
+ }
6307
6759
  let nb;
6308
6760
  if (action.preset && (action.contentType === "layout" || action.contentType === "nestedRow")) {
6309
6761
  nb = makeNestedRowBlock(action.preset);