react-email-studio 3.8.2 → 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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.8.3] - 2026-05-26
9
+
10
+ ### Fixed
11
+
12
+ - **Context menu Move Up / Move Down** now works for all block types: root `blocks[]` sections (row reorder), stacked blocks in a `rows[]` column (in-column swap), nested layout columns, and single-block layout rows.
13
+ - **Context menu clicks** no longer dismiss the menu before the action runs (`pointerdown` handling on the menu overlay).
14
+
8
15
  ## [3.8.2] - 2026-05-22
9
16
 
10
17
  ### Added
package/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  ## Release notes
9
9
 
10
- **Latest: 3.8.2** — includes a single-file **`RELEASE.md`** npm tutorial and fixes plain text newline rendering parity between canvas preview and exported HTML.
10
+ **Latest: 3.8.3** — fixes context menu **Move Up / Move Down** for root sections, stacked column blocks, and nested layout blocks.
11
+
12
+ See **3.8.2** for the **`RELEASE.md`** npm tutorial and plain-text newline rendering fixes.
11
13
 
12
14
  Version history and migration hints: **[CHANGELOG.md](./CHANGELOG.md)** (also included in the published npm tarball under `node_modules/react-email-studio/CHANGELOG.md`).
13
15
 
package/dist/index.cjs CHANGED
@@ -864,6 +864,65 @@ function countBlocksInDesign(rows) {
864
864
  }, 0);
865
865
  return rows.reduce((sum, r) => sum + r.cells.reduce((s, c) => s + colBlocks(c), 0), 0);
866
866
  }
867
+ function isSingleBlockInRow(row) {
868
+ if (!row?.cells) return false;
869
+ let count = 0;
870
+ for (const col of row.cells) {
871
+ if (!Array.isArray(col)) continue;
872
+ count += col.length;
873
+ if (count > 1) return false;
874
+ }
875
+ return count === 1;
876
+ }
877
+ function resolveBlockMovePlan(rows, rowId, cellIdx, blockIndex, inner) {
878
+ const rowIndex = rows.findIndex((r) => r.id === rowId);
879
+ const row = rowIndex >= 0 ? rows[rowIndex] : void 0;
880
+ if (!row) return null;
881
+ if (inner) {
882
+ const loc = toColumnLoc(rowId, cellIdx, {
883
+ parentBlockIdx: inner.parentBlockIdx,
884
+ innerCellIdx: inner.cellIdx
885
+ });
886
+ const col2 = getColumnBlocks(rows, loc);
887
+ const idx = inner.contentIdx;
888
+ return {
889
+ mode: "column",
890
+ blockIndex: idx,
891
+ canMoveUp: idx > 0,
892
+ canMoveDown: idx < col2.length - 1
893
+ };
894
+ }
895
+ const col = row.cells?.[cellIdx];
896
+ const colLen = Array.isArray(col) ? col.length : 0;
897
+ if (colLen > 1) {
898
+ return {
899
+ mode: "column",
900
+ blockIndex,
901
+ canMoveUp: blockIndex > 0,
902
+ canMoveDown: blockIndex < colLen - 1
903
+ };
904
+ }
905
+ if (colLen === 1 && (isRootContentRow(row) || isSingleBlockInRow(row))) {
906
+ return {
907
+ mode: "row",
908
+ canMoveUp: rowIndex > 0,
909
+ canMoveDown: rowIndex < rows.length - 1
910
+ };
911
+ }
912
+ if (colLen === 1) {
913
+ return {
914
+ mode: "row",
915
+ canMoveUp: rowIndex > 0,
916
+ canMoveDown: rowIndex < rows.length - 1
917
+ };
918
+ }
919
+ return {
920
+ mode: "column",
921
+ blockIndex,
922
+ canMoveUp: false,
923
+ canMoveDown: false
924
+ };
925
+ }
867
926
 
868
927
  // src/lib/htmlUtils.ts
869
928
  function escHtmlAttr(s) {
@@ -7428,50 +7487,64 @@ var import_jsx_runtime8 = require("react/jsx-runtime");
7428
7487
  function ContextMenu({ x, y, items, onClose, C }) {
7429
7488
  const ref = (0, import_react6.useRef)(null);
7430
7489
  (0, import_react6.useEffect)(() => {
7431
- const h = () => onClose();
7432
- window.addEventListener("mousedown", h);
7433
- window.addEventListener("keydown", h);
7490
+ const onPointerDown = (ev) => {
7491
+ if (ref.current?.contains(ev.target)) return;
7492
+ onClose();
7493
+ };
7494
+ const onKeyDown = (ev) => {
7495
+ if (ev.key === "Escape") onClose();
7496
+ };
7497
+ window.addEventListener("pointerdown", onPointerDown);
7498
+ window.addEventListener("keydown", onKeyDown);
7434
7499
  return () => {
7435
- window.removeEventListener("mousedown", h);
7436
- window.removeEventListener("keydown", h);
7500
+ window.removeEventListener("pointerdown", onPointerDown);
7501
+ window.removeEventListener("keydown", onKeyDown);
7437
7502
  };
7438
7503
  }, [onClose]);
7439
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("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__ */ (0, import_jsx_runtime8.jsx)("div", { style: { height: 1, background: C.border, margin: "3px 0" } }, i) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
7440
- "button",
7504
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7505
+ "div",
7441
7506
  {
7442
- type: "button",
7443
- disabled: !!item.disabled,
7444
- onClick: () => {
7445
- if (item.disabled) return;
7446
- item.action();
7447
- onClose();
7448
- },
7449
- style: {
7450
- display: "block",
7451
- width: "100%",
7452
- background: "none",
7453
- border: "none",
7454
- padding: "7px 14px",
7455
- textAlign: "left",
7456
- cursor: item.disabled ? "not-allowed" : "pointer",
7457
- fontSize: 12,
7458
- color: item.disabled ? C.muted : item.danger ? C.danger : C.text,
7459
- fontWeight: item.bold ? 700 : 400,
7460
- opacity: item.disabled ? 0.45 : 1
7461
- },
7462
- onMouseEnter: (e) => {
7463
- if (!item.disabled) e.currentTarget.style.background = C.surface;
7464
- },
7465
- onMouseLeave: (e) => {
7466
- e.currentTarget.style.background = "none";
7467
- },
7468
- children: [
7469
- item.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { marginRight: 8 }, children: item.icon }),
7470
- item.label
7471
- ]
7472
- },
7473
- i
7474
- )) });
7507
+ ref,
7508
+ onPointerDown: (e) => e.stopPropagation(),
7509
+ 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 },
7510
+ children: items.map((item, i) => item.sep ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { style: { height: 1, background: C.border, margin: "3px 0" } }, i) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
7511
+ "button",
7512
+ {
7513
+ type: "button",
7514
+ disabled: !!item.disabled,
7515
+ onClick: () => {
7516
+ if (item.disabled) return;
7517
+ item.action();
7518
+ onClose();
7519
+ },
7520
+ style: {
7521
+ display: "block",
7522
+ width: "100%",
7523
+ background: "none",
7524
+ border: "none",
7525
+ padding: "7px 14px",
7526
+ textAlign: "left",
7527
+ cursor: item.disabled ? "not-allowed" : "pointer",
7528
+ fontSize: 12,
7529
+ color: item.disabled ? C.muted : item.danger ? C.danger : C.text,
7530
+ fontWeight: item.bold ? 700 : 400,
7531
+ opacity: item.disabled ? 0.45 : 1
7532
+ },
7533
+ onMouseEnter: (e) => {
7534
+ if (!item.disabled) e.currentTarget.style.background = C.surface;
7535
+ },
7536
+ onMouseLeave: (e) => {
7537
+ e.currentTarget.style.background = "none";
7538
+ },
7539
+ children: [
7540
+ item.icon && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { marginRight: 8 }, children: item.icon }),
7541
+ item.label
7542
+ ]
7543
+ },
7544
+ i
7545
+ ))
7546
+ }
7547
+ );
7475
7548
  }
7476
7549
 
7477
7550
  // src/editor/overlays/ConfirmDialog.tsx
@@ -8075,10 +8148,27 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
8075
8148
  else dropContent(rowId, cellIdx, { kind: "duplicate", fromIdx: contentIdx });
8076
8149
  }
8077
8150
  };
8151
+ const shiftBlockFromMenu = (rowId, cellIdx, ci, dir, inner = null) => {
8152
+ const blockIndex = inner ? inner.contentIdx : ci;
8153
+ const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
8154
+ if (!plan) return;
8155
+ if (dir < 0 && !plan.canMoveUp || dir > 0 && !plan.canMoveDown) return;
8156
+ if (plan.mode === "row") {
8157
+ moveRow(rowId, dir);
8158
+ return;
8159
+ }
8160
+ moveContent(rowId, cellIdx, plan.blockIndex, dir, inner);
8161
+ };
8078
8162
  const handleContextMenu = (e, rowId, cellIdx, ci, inner = null) => {
8163
+ e.preventDefault();
8164
+ e.stopPropagation();
8165
+ const blockIndex = inner ? inner.contentIdx : ci;
8166
+ const plan = resolveBlockMovePlan(rowsRef.current, rowId, cellIdx, blockIndex, inner);
8167
+ const canMoveUp = plan?.canMoveUp ?? false;
8168
+ const canMoveDown = plan?.canMoveDown ?? false;
8079
8169
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
8080
- { icon: "\u2191", label: "Move Up", action: () => moveContent(rowId, cellIdx, ci, -1, inner) },
8081
- { icon: "\u2193", label: "Move Down", action: () => moveContent(rowId, cellIdx, ci, 1, inner) },
8170
+ { icon: "\u2191", label: "Move Up", disabled: !canMoveUp, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, -1, inner) },
8171
+ { icon: "\u2193", label: "Move Down", disabled: !canMoveDown, action: () => shiftBlockFromMenu(rowId, cellIdx, ci, 1, inner) },
8082
8172
  { sep: true },
8083
8173
  { 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 }) },
8084
8174
  { sep: true },
@@ -8088,17 +8178,14 @@ var ReactEmailEditorComponent = (0, import_react8.forwardRef)(
8088
8178
  const handleLayoutContextMenu = (e, rowId) => {
8089
8179
  e.preventDefault();
8090
8180
  e.stopPropagation();
8091
- const ri = rows.findIndex((r) => r.id === rowId);
8181
+ const currentRows = rowsRef.current;
8182
+ const ri = currentRows.findIndex((r) => r.id === rowId);
8092
8183
  setSelectedRowId(rowId);
8093
8184
  setSelContentId(null);
8094
8185
  setSelMeta(null);
8095
8186
  setCtxMenu({ x: e.clientX, y: e.clientY, items: [
8096
- { icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () => {
8097
- if (ri > 0) moveRow(rowId, -1);
8098
- } },
8099
- { icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= rows.length - 1, action: () => {
8100
- if (ri >= 0 && ri < rows.length - 1) moveRow(rowId, 1);
8101
- } },
8187
+ { icon: "\u2191", label: "Move row up", disabled: ri <= 0, action: () => moveRow(rowId, -1) },
8188
+ { icon: "\u2193", label: "Move row down", disabled: ri < 0 || ri >= currentRows.length - 1, action: () => moveRow(rowId, 1) },
8102
8189
  { sep: true },
8103
8190
  { icon: "\u29C9", label: "Duplicate row", action: () => dupRow(rowId) },
8104
8191
  { sep: true },