react-email-studio 3.8.1 → 3.8.3

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
@@ -827,6 +827,65 @@ function countBlocksInDesign(rows) {
827
827
  }, 0);
828
828
  return rows.reduce((sum, r) => sum + r.cells.reduce((s, c) => s + colBlocks(c), 0), 0);
829
829
  }
830
+ function isSingleBlockInRow(row) {
831
+ if (!row?.cells) return false;
832
+ let count = 0;
833
+ for (const col of row.cells) {
834
+ if (!Array.isArray(col)) continue;
835
+ count += col.length;
836
+ if (count > 1) return false;
837
+ }
838
+ return count === 1;
839
+ }
840
+ function resolveBlockMovePlan(rows, rowId, cellIdx, blockIndex, inner) {
841
+ const rowIndex = rows.findIndex((r) => r.id === rowId);
842
+ const row = rowIndex >= 0 ? rows[rowIndex] : void 0;
843
+ if (!row) return null;
844
+ if (inner) {
845
+ const loc = toColumnLoc(rowId, cellIdx, {
846
+ parentBlockIdx: inner.parentBlockIdx,
847
+ innerCellIdx: inner.cellIdx
848
+ });
849
+ const col2 = getColumnBlocks(rows, loc);
850
+ const idx = inner.contentIdx;
851
+ return {
852
+ mode: "column",
853
+ blockIndex: idx,
854
+ canMoveUp: idx > 0,
855
+ canMoveDown: idx < col2.length - 1
856
+ };
857
+ }
858
+ const col = row.cells?.[cellIdx];
859
+ const colLen = Array.isArray(col) ? col.length : 0;
860
+ if (colLen > 1) {
861
+ return {
862
+ mode: "column",
863
+ blockIndex,
864
+ canMoveUp: blockIndex > 0,
865
+ canMoveDown: blockIndex < colLen - 1
866
+ };
867
+ }
868
+ if (colLen === 1 && (isRootContentRow(row) || isSingleBlockInRow(row))) {
869
+ return {
870
+ mode: "row",
871
+ canMoveUp: rowIndex > 0,
872
+ canMoveDown: rowIndex < rows.length - 1
873
+ };
874
+ }
875
+ if (colLen === 1) {
876
+ return {
877
+ mode: "row",
878
+ canMoveUp: rowIndex > 0,
879
+ canMoveDown: rowIndex < rows.length - 1
880
+ };
881
+ }
882
+ return {
883
+ mode: "column",
884
+ blockIndex,
885
+ canMoveUp: false,
886
+ canMoveDown: false
887
+ };
888
+ }
830
889
 
831
890
  // src/lib/htmlUtils.ts
832
891
  function escHtmlAttr(s) {
@@ -835,6 +894,11 @@ function escHtmlAttr(s) {
835
894
  function escHtml(s) {
836
895
  return String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
837
896
  }
897
+ function textBlockContentToHtml(content) {
898
+ const raw = typeof content === "string" ? content : "";
899
+ if (/^\s*<[^>]+>/.test(raw)) return raw;
900
+ return escHtml(raw).replace(/\r?\n/g, "<br/>");
901
+ }
838
902
  function utf8ToBase64(raw) {
839
903
  const bytes = new TextEncoder().encode(raw);
840
904
  let bin = "";
@@ -1175,8 +1239,7 @@ function blockToHtml(cb) {
1175
1239
  case "text": {
1176
1240
  const shell = emailSurfaceBgCss(p);
1177
1241
  const inner = `font-size:${lenPx(p.fontSize)};color:${p.color};text-align:${p.align};font-weight:${p.fontWeight || 400};font-style:${p.italic ? "italic" : "normal"};text-decoration:${p.underline ? "underline" : "none"};line-height:${lh(p.lineHeight)};letter-spacing:${lenPx(p.letterSpacing)};font-family:${p.fontFamily || "Georgia,serif"}`;
1178
- const content = typeof p.content === "string" ? p.content : "";
1179
- const body = /^\s*<[^>]+>/.test(content) ? content : escHtml(content).replace(/\r?\n/g, "<br/>");
1242
+ const body = textBlockContentToHtml(p.content);
1180
1243
  return `<div style="${pd(p.padding)};${marginCss()}${shell}"><div style="${inner}">${body}</div></div>`;
1181
1244
  }
1182
1245
  case "html": {
@@ -3021,7 +3084,7 @@ function ContentBlock({ block, selected, onClick, preview, C }) {
3021
3084
  "div",
3022
3085
  {
3023
3086
  style: { fontSize: p.fontSize, color: p.color, textAlign: p.align, fontWeight: p.fontWeight || (p.bold ? 700 : 400), fontStyle: p.italic ? "italic" : "normal", textDecoration: p.underline ? "underline" : "none", lineHeight: p.lineHeight || 1.65, letterSpacing: `${p.letterSpacing || 0}px`, fontFamily: p.fontFamily || "Georgia,serif" },
3024
- dangerouslySetInnerHTML: { __html: p.content }
3087
+ dangerouslySetInnerHTML: { __html: textBlockContentToHtml(p.content) }
3025
3088
  }
3026
3089
  )
3027
3090
  );
@@ -7440,50 +7503,64 @@ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
7440
7503
  function ContextMenu({ x, y, items, onClose, C }) {
7441
7504
  const ref = useRef4(null);
7442
7505
  useEffect5(() => {
7443
- const h = () => onClose();
7444
- window.addEventListener("mousedown", h);
7445
- window.addEventListener("keydown", h);
7506
+ const onPointerDown = (ev) => {
7507
+ if (ref.current?.contains(ev.target)) return;
7508
+ onClose();
7509
+ };
7510
+ const onKeyDown = (ev) => {
7511
+ if (ev.key === "Escape") onClose();
7512
+ };
7513
+ window.addEventListener("pointerdown", onPointerDown);
7514
+ window.addEventListener("keydown", onKeyDown);
7446
7515
  return () => {
7447
- window.removeEventListener("mousedown", h);
7448
- window.removeEventListener("keydown", h);
7516
+ window.removeEventListener("pointerdown", onPointerDown);
7517
+ window.removeEventListener("keydown", onKeyDown);
7449
7518
  };
7450
7519
  }, [onClose]);
7451
- return /* @__PURE__ */ jsx8("div", { ref, onMouseDown: (e) => e.stopPropagation(), style: { position: "fixed", left: x, top: y, zIndex: 9999, background: C.sidebar, border: `1px solid ${C.border}`, borderRadius: 7, padding: "4px 0", boxShadow: `0 8px 32px rgba(0,0,0,.4)`, minWidth: 160 }, children: items.map((item, i) => item.sep ? /* @__PURE__ */ jsx8("div", { style: { height: 1, background: C.border, margin: "3px 0" } }, i) : /* @__PURE__ */ jsxs6(
7452
- "button",
7520
+ return /* @__PURE__ */ jsx8(
7521
+ "div",
7453
7522
  {
7454
- type: "button",
7455
- disabled: !!item.disabled,
7456
- onClick: () => {
7457
- if (item.disabled) return;
7458
- item.action();
7459
- onClose();
7460
- },
7461
- style: {
7462
- display: "block",
7463
- width: "100%",
7464
- background: "none",
7465
- border: "none",
7466
- padding: "7px 14px",
7467
- textAlign: "left",
7468
- cursor: item.disabled ? "not-allowed" : "pointer",
7469
- fontSize: 12,
7470
- color: item.disabled ? C.muted : item.danger ? C.danger : C.text,
7471
- fontWeight: item.bold ? 700 : 400,
7472
- opacity: item.disabled ? 0.45 : 1
7473
- },
7474
- onMouseEnter: (e) => {
7475
- if (!item.disabled) e.currentTarget.style.background = C.surface;
7476
- },
7477
- onMouseLeave: (e) => {
7478
- e.currentTarget.style.background = "none";
7479
- },
7480
- children: [
7481
- item.icon && /* @__PURE__ */ jsx8("span", { style: { marginRight: 8 }, children: item.icon }),
7482
- item.label
7483
- ]
7484
- },
7485
- i
7486
- )) });
7523
+ ref,
7524
+ onPointerDown: (e) => e.stopPropagation(),
7525
+ style: { position: "fixed", left: x, top: y, zIndex: 9999, background: C.sidebar, border: `1px solid ${C.border}`, borderRadius: 7, padding: "4px 0", boxShadow: `0 8px 32px rgba(0,0,0,.4)`, minWidth: 160 },
7526
+ children: items.map((item, i) => item.sep ? /* @__PURE__ */ jsx8("div", { style: { height: 1, background: C.border, margin: "3px 0" } }, i) : /* @__PURE__ */ jsxs6(
7527
+ "button",
7528
+ {
7529
+ type: "button",
7530
+ disabled: !!item.disabled,
7531
+ onClick: () => {
7532
+ if (item.disabled) return;
7533
+ item.action();
7534
+ onClose();
7535
+ },
7536
+ style: {
7537
+ display: "block",
7538
+ width: "100%",
7539
+ background: "none",
7540
+ border: "none",
7541
+ padding: "7px 14px",
7542
+ textAlign: "left",
7543
+ cursor: item.disabled ? "not-allowed" : "pointer",
7544
+ fontSize: 12,
7545
+ color: item.disabled ? C.muted : item.danger ? C.danger : C.text,
7546
+ fontWeight: item.bold ? 700 : 400,
7547
+ opacity: item.disabled ? 0.45 : 1
7548
+ },
7549
+ onMouseEnter: (e) => {
7550
+ if (!item.disabled) e.currentTarget.style.background = C.surface;
7551
+ },
7552
+ onMouseLeave: (e) => {
7553
+ e.currentTarget.style.background = "none";
7554
+ },
7555
+ children: [
7556
+ item.icon && /* @__PURE__ */ jsx8("span", { style: { marginRight: 8 }, children: item.icon }),
7557
+ item.label
7558
+ ]
7559
+ },
7560
+ i
7561
+ ))
7562
+ }
7563
+ );
7487
7564
  }
7488
7565
 
7489
7566
  // src/editor/overlays/ConfirmDialog.tsx
@@ -8087,10 +8164,27 @@ var ReactEmailEditorComponent = forwardRef(
8087
8164
  else dropContent(rowId, cellIdx, { kind: "duplicate", fromIdx: contentIdx });
8088
8165
  }
8089
8166
  };
8167
+ const shiftBlockFromMenu = (rowId, cellIdx, ci, dir, inner = null) => {
8168
+ const blockIndex = inner ? inner.contentIdx : ci;
8169
+ const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
8170
+ if (!plan) return;
8171
+ if (dir < 0 && !plan.canMoveUp || dir > 0 && !plan.canMoveDown) return;
8172
+ if (plan.mode === "row") {
8173
+ moveRow(rowId, dir);
8174
+ return;
8175
+ }
8176
+ moveContent(rowId, cellIdx, plan.blockIndex, dir, inner);
8177
+ };
8090
8178
  const handleContextMenu = (e, rowId, cellIdx, ci, inner = null) => {
8179
+ e.preventDefault();
8180
+ e.stopPropagation();
8181
+ const blockIndex = inner ? inner.contentIdx : ci;
8182
+ const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
8183
+ const canMoveUp = plan?.canMoveUp ?? false;
8184
+ const canMoveDown = plan?.canMoveDown ?? false;
8091
8185
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
8092
- { icon: "\u2191", label: "Move Up", action: () => moveContent(rowId, cellIdx, ci, -1, inner) },
8093
- { icon: "\u2193", label: "Move Down", action: () => moveContent(rowId, cellIdx, ci, 1, inner) },
8186
+ { icon: "\u2191", label: "Move Up", disabled: !canMoveUp, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, -1, inner) },
8187
+ { icon: "\u2193", label: "Move Down", disabled: !canMoveDown, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, 1, inner) },
8094
8188
  { sep: true },
8095
8189
  { icon: "\u29C9", label: "Duplicate", action: () => dropContent(rowId, cellIdx, inner ? { kind: "duplicate", fromIdx: inner.contentIdx, fromNested: { parentBlockIdx: inner.parentBlockIdx, innerCellIdx: inner.cellIdx } } : { kind: "duplicate", fromIdx: ci }) },
8096
8190
  { sep: true },
@@ -8100,17 +8194,14 @@ var ReactEmailEditorComponent = forwardRef(
8100
8194
  const handleLayoutContextMenu = (e, rowId) => {
8101
8195
  e.preventDefault();
8102
8196
  e.stopPropagation();
8103
- const ri = rows.findIndex((r) => r.id === rowId);
8197
+ const currentRows = rowsRef.current;
8198
+ const ri = currentRows.findIndex((r) => r.id === rowId);
8104
8199
  setSelectedRowId(rowId);
8105
8200
  setSelContentId(null);
8106
8201
  setSelMeta(null);
8107
8202
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
8108
- { icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () => {
8109
- if (ri > 0) moveRow(rowId, -1);
8110
- } },
8111
- { icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= rows.length - 1, action: () => {
8112
- if (ri >= 0 && ri < rows.length - 1) moveRow(rowId, 1);
8113
- } },
8203
+ { icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () => moveRow(rowId, -1) },
8204
+ { icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= currentRows.length - 1, action: () => moveRow(rowId, 1) },
8114
8205
  { sep: true },
8115
8206
  { icon: "\u29C9", label: "Duplicate row", action: () => dupRow(rowId) },
8116
8207
  { sep: true },