@standhigher/react-email-editor 0.5.1 → 0.5.2
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/README.md +0 -4
- package/README.zh-CN.md +0 -4
- package/dist/index.js +347 -173
- package/dist/styles.css +44 -0
- package/docs/changelog.md +16 -0
- package/docs/changelog.zh-CN.md +16 -0
- package/docs/template-engines.md +8 -0
- package/docs/template-engines.zh-CN.md +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,10 +28,6 @@ export function EmailPage() {
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
With Next.js App Router, add `"use client"` to the integration component and import CSS from an allowed global stylesheet entry. Your app implements persistence, resource uploads and email delivery.
|
|
32
|
-
|
|
33
|
-
Custom blocks that omit `icon` use the first character of their trimmed `title` as the icon: `Coupon` displays `C` and `优惠券` displays `优`. An explicit `icon` takes precedence.
|
|
34
|
-
|
|
35
31
|
## Run locally
|
|
36
32
|
|
|
37
33
|
Use Node.js 24 LTS (`.nvmrc`; at least 22.18 for the complete test suite):
|
package/README.zh-CN.md
CHANGED
|
@@ -28,10 +28,6 @@ export function EmailPage() {
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Next.js App Router 的接入组件需声明 `"use client"`;样式在应用允许的全局样式入口引入。保存、上传资源与发送邮件由你的应用实现。
|
|
32
|
-
|
|
33
|
-
自定义区块省略 `icon` 时,默认使用 `title` 去除首尾空白后的第一个字符作为图标,例如 `Coupon` 显示 `C`、`优惠券` 显示 `优`;传入的 `icon` 优先使用。
|
|
34
|
-
|
|
35
31
|
## 本地启动
|
|
36
32
|
|
|
37
33
|
推荐 Node.js 24 LTS(`.nvmrc`;完整测试最低需要 22.18):
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ function usePluginEditor() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// src/shell/EmailEditorShell.tsx
|
|
47
|
-
import { useState as
|
|
47
|
+
import { useState as useState20 } from "react";
|
|
48
48
|
|
|
49
49
|
// src/editor-dnd/EditorDnd.tsx
|
|
50
50
|
import { useLayoutEffect as useLayoutEffect5, useMemo, useRef as useRef7, useState as useState7 } from "react";
|
|
@@ -4615,14 +4615,10 @@ function Inspector({
|
|
|
4615
4615
|
}
|
|
4616
4616
|
|
|
4617
4617
|
// src/stage/Stage.tsx
|
|
4618
|
-
import { useMemo as
|
|
4618
|
+
import { useMemo as useMemo4, useState as useState18 } from "react";
|
|
4619
4619
|
|
|
4620
|
-
// src/stage/
|
|
4621
|
-
import {
|
|
4622
|
-
import {
|
|
4623
|
-
SortableContext,
|
|
4624
|
-
verticalListSortingStrategy
|
|
4625
|
-
} from "@dnd-kit/sortable";
|
|
4620
|
+
// src/stage/preview/usePreviewBuffer.ts
|
|
4621
|
+
import { useCallback, useEffect as useEffect6, useLayoutEffect as useLayoutEffect9, useReducer, useRef as useRef13, useState as useState16 } from "react";
|
|
4626
4622
|
|
|
4627
4623
|
// src/render/layout.ts
|
|
4628
4624
|
function surfaceStyle(body) {
|
|
@@ -4639,8 +4635,126 @@ function viewportWidth(device, body) {
|
|
|
4639
4635
|
return `${emailViewportWidth(device, body.contentWidth)}px`;
|
|
4640
4636
|
}
|
|
4641
4637
|
|
|
4638
|
+
// src/stage/preview/buffer.ts
|
|
4639
|
+
function createPreviewBuffer() {
|
|
4640
|
+
return { frames: [null, null], front: null, failure: null };
|
|
4641
|
+
}
|
|
4642
|
+
function previewBufferReducer(state, action) {
|
|
4643
|
+
var _a, _b, _c;
|
|
4644
|
+
switch (action.type) {
|
|
4645
|
+
case "generated": {
|
|
4646
|
+
const match = action.force ? -1 : state.frames.findIndex((frame) => frame && frame.status !== "failed" && frame.html === action.snapshot.html && frame.width === action.snapshot.width);
|
|
4647
|
+
const target = match < 0 ? state.front === 0 ? 1 : 0 : match;
|
|
4648
|
+
const frames = [...state.frames];
|
|
4649
|
+
frames[target] = match < 0 ? action.snapshot : __spreadProps(__spreadValues({}, frames[target]), { source: action.snapshot.source });
|
|
4650
|
+
const ready = ((_a = frames[target]) == null ? void 0 : _a.status) === "ready";
|
|
4651
|
+
return { frames, front: ready ? target : state.front, failure: ready ? null : state.failure };
|
|
4652
|
+
}
|
|
4653
|
+
case "ready": {
|
|
4654
|
+
const index = state.frames.findIndex((frame) => frame && frame.id === action.id && frame.status === "loading");
|
|
4655
|
+
if (index < 0) return state;
|
|
4656
|
+
const frames = [...state.frames];
|
|
4657
|
+
frames[index] = __spreadProps(__spreadValues({}, frames[index]), { status: "ready" });
|
|
4658
|
+
const current = ((_b = frames[index]) == null ? void 0 : _b.source) === action.source;
|
|
4659
|
+
return { frames, front: current ? index : state.front, failure: current ? null : state.failure };
|
|
4660
|
+
}
|
|
4661
|
+
case "failed": {
|
|
4662
|
+
if (action.id === void 0) {
|
|
4663
|
+
const frames2 = [state.frames[0], state.frames[1]];
|
|
4664
|
+
for (const index2 of [0, 1]) {
|
|
4665
|
+
const frame = frames2[index2];
|
|
4666
|
+
if ((frame == null ? void 0 : frame.status) === "loading") frames2[index2] = __spreadProps(__spreadValues({}, frame), { status: "failed" });
|
|
4667
|
+
}
|
|
4668
|
+
return __spreadProps(__spreadValues({}, state), { frames: frames2, failure: action.failure });
|
|
4669
|
+
}
|
|
4670
|
+
const index = state.frames.findIndex((frame) => frame && frame.id === action.id && frame.status === "loading");
|
|
4671
|
+
if (index < 0) return state;
|
|
4672
|
+
const frames = [...state.frames];
|
|
4673
|
+
frames[index] = __spreadProps(__spreadValues({}, frames[index]), { status: "failed" });
|
|
4674
|
+
return __spreadProps(__spreadValues({}, state), { frames, failure: ((_c = frames[index]) == null ? void 0 : _c.source) === action.failure.source ? action.failure : state.failure });
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
}
|
|
4678
|
+
|
|
4679
|
+
// src/stage/preview/constants.ts
|
|
4680
|
+
var PREVIEW_DEBOUNCE_MS = 400;
|
|
4681
|
+
var PREVIEW_MAX_WAIT_MS = 2e3;
|
|
4682
|
+
var PREVIEW_LOAD_TIMEOUT_MS = 15e3;
|
|
4683
|
+
var PREVIEW_BUFFER_SLOTS = [0, 1];
|
|
4684
|
+
|
|
4685
|
+
// src/stage/preview/usePreviewBuffer.ts
|
|
4686
|
+
function usePreviewBuffer(source, requested) {
|
|
4687
|
+
var _a;
|
|
4688
|
+
const [state, dispatch] = useReducer(previewBufferReducer, void 0, createPreviewBuffer);
|
|
4689
|
+
const [request, setRequest] = useState16({ key: requested ? 1 : 0, force: false });
|
|
4690
|
+
const previousRequested = useRef13(requested);
|
|
4691
|
+
const sourceRef = useRef13(source);
|
|
4692
|
+
const pendingSince = useRef13(null);
|
|
4693
|
+
const attempted = useRef13(null);
|
|
4694
|
+
const revision = useRef13(0);
|
|
4695
|
+
useLayoutEffect9(() => {
|
|
4696
|
+
sourceRef.current = source;
|
|
4697
|
+
}, [source]);
|
|
4698
|
+
const flush = useCallback(() => setRequest((value) => __spreadProps(__spreadValues({}, value), { key: value.key + 1, force: false })), []);
|
|
4699
|
+
useEffect6(() => {
|
|
4700
|
+
var _a2, _b, _c;
|
|
4701
|
+
const enteringPreview = requested && !previousRequested.current;
|
|
4702
|
+
previousRequested.current = requested;
|
|
4703
|
+
const newRequest = request.key > 0 && ((_a2 = attempted.current) == null ? void 0 : _a2.requestKey) !== request.key;
|
|
4704
|
+
const force = newRequest && request.force;
|
|
4705
|
+
if (((_b = attempted.current) == null ? void 0 : _b.source) === source && !force) {
|
|
4706
|
+
attempted.current.requestKey = request.key;
|
|
4707
|
+
return;
|
|
4708
|
+
}
|
|
4709
|
+
const immediate = enteringPreview || newRequest;
|
|
4710
|
+
(_c = pendingSince.current) != null ? _c : pendingSince.current = Date.now();
|
|
4711
|
+
const remaining = Math.max(0, PREVIEW_MAX_WAIT_MS - (Date.now() - pendingSince.current));
|
|
4712
|
+
let idle;
|
|
4713
|
+
let cancelled = false;
|
|
4714
|
+
const generate = () => {
|
|
4715
|
+
if (cancelled || sourceRef.current !== source) return;
|
|
4716
|
+
attempted.current = { source, requestKey: request.key };
|
|
4717
|
+
pendingSince.current = null;
|
|
4718
|
+
try {
|
|
4719
|
+
const html = source.config.toHtml(source.document, { resolveHtmlTemplates: true, variables: source.variables });
|
|
4720
|
+
dispatch({ type: "generated", snapshot: { id: ++revision.current, source, html, width: viewportWidth(source.device, source.document.body), status: "loading" }, force });
|
|
4721
|
+
} catch (error) {
|
|
4722
|
+
dispatch({ type: "failed", failure: { source, message: error instanceof Error ? error.message : String(error) || "Email preview failed." } });
|
|
4723
|
+
}
|
|
4724
|
+
};
|
|
4725
|
+
const delay = immediate ? 0 : Math.min(PREVIEW_DEBOUNCE_MS, remaining);
|
|
4726
|
+
const timer = setTimeout(() => {
|
|
4727
|
+
if (immediate || typeof requestIdleCallback !== "function") generate();
|
|
4728
|
+
else idle = requestIdleCallback(generate, { timeout: Math.max(1, remaining - delay) });
|
|
4729
|
+
}, delay);
|
|
4730
|
+
return () => {
|
|
4731
|
+
cancelled = true;
|
|
4732
|
+
clearTimeout(timer);
|
|
4733
|
+
if (idle !== void 0) cancelIdleCallback(idle);
|
|
4734
|
+
};
|
|
4735
|
+
}, [source, requested, request]);
|
|
4736
|
+
const markReady = useCallback((id) => {
|
|
4737
|
+
dispatch({ type: "ready", id, source: sourceRef.current });
|
|
4738
|
+
}, []);
|
|
4739
|
+
const markFailed = useCallback((id, message) => {
|
|
4740
|
+
dispatch({ type: "failed", id, failure: { source: sourceRef.current, message } });
|
|
4741
|
+
}, []);
|
|
4742
|
+
const retry = useCallback(() => setRequest((value) => __spreadProps(__spreadValues({}, value), { key: value.key + 1, force: true })), []);
|
|
4743
|
+
const front = state.front === null ? null : state.frames[state.front];
|
|
4744
|
+
const failure = ((_a = state.failure) == null ? void 0 : _a.source) === source ? state.failure : null;
|
|
4745
|
+
const displayFailure = failure != null ? failure : state.front === null ? state.failure : null;
|
|
4746
|
+
return __spreadProps(__spreadValues({}, state), { failure, displayFailure, ready: (front == null ? void 0 : front.source) === source, markReady, markFailed, flush, retry });
|
|
4747
|
+
}
|
|
4748
|
+
|
|
4749
|
+
// src/stage/canvas/Canvas.tsx
|
|
4750
|
+
import { useDroppable } from "@dnd-kit/core";
|
|
4751
|
+
import {
|
|
4752
|
+
SortableContext,
|
|
4753
|
+
verticalListSortingStrategy
|
|
4754
|
+
} from "@dnd-kit/sortable";
|
|
4755
|
+
|
|
4642
4756
|
// src/stage/canvas/CanvasBlock.tsx
|
|
4643
|
-
import { useId as useId13, useMemo as useMemo3, useState as
|
|
4757
|
+
import { useId as useId13, useMemo as useMemo3, useState as useState17 } from "react";
|
|
4644
4758
|
import { useSortable } from "@dnd-kit/sortable";
|
|
4645
4759
|
import { CSS } from "@dnd-kit/utilities";
|
|
4646
4760
|
|
|
@@ -4996,7 +5110,7 @@ function blockToHtml(node, definitions = DEFAULT_BLOCKS, mode = "email", options
|
|
|
4996
5110
|
}
|
|
4997
5111
|
|
|
4998
5112
|
// src/stage/canvas/components/HtmlCanvas.tsx
|
|
4999
|
-
import { useCallback, useEffect as
|
|
5113
|
+
import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef14 } from "react";
|
|
5000
5114
|
|
|
5001
5115
|
// src/render/head-styles.ts
|
|
5002
5116
|
function extractHeadStyles(html) {
|
|
@@ -5017,10 +5131,10 @@ function frameHtml(html, baseCss) {
|
|
|
5017
5131
|
}
|
|
5018
5132
|
function HtmlCanvas({ html, baseCss = "", onSelect }) {
|
|
5019
5133
|
const hasHtmlTag = /<\/?[a-z][\w:-]*(?:\s[^>]*)?>/i.test(html);
|
|
5020
|
-
const containerRef =
|
|
5021
|
-
const frameRef =
|
|
5022
|
-
const cleanupRef =
|
|
5023
|
-
const observeContent =
|
|
5134
|
+
const containerRef = useRef14(null);
|
|
5135
|
+
const frameRef = useRef14(null);
|
|
5136
|
+
const cleanupRef = useRef14();
|
|
5137
|
+
const observeContent = useCallback2(() => {
|
|
5024
5138
|
var _a;
|
|
5025
5139
|
(_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
|
|
5026
5140
|
const container = containerRef.current;
|
|
@@ -5061,7 +5175,7 @@ function HtmlCanvas({ html, baseCss = "", onSelect }) {
|
|
|
5061
5175
|
window2.cancelAnimationFrame(animationFrame);
|
|
5062
5176
|
};
|
|
5063
5177
|
}, []);
|
|
5064
|
-
|
|
5178
|
+
useEffect7(() => () => {
|
|
5065
5179
|
var _a;
|
|
5066
5180
|
return (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
|
|
5067
5181
|
}, [hasHtmlTag, html]);
|
|
@@ -5233,6 +5347,7 @@ function canvasBlockStyle(block, body) {
|
|
|
5233
5347
|
// src/stage/canvas/CanvasBlock.tsx
|
|
5234
5348
|
import { jsx as jsx51, jsxs as jsxs40 } from "react/jsx-runtime";
|
|
5235
5349
|
function CanvasBlock({
|
|
5350
|
+
active = true,
|
|
5236
5351
|
block: sourceBlock,
|
|
5237
5352
|
body,
|
|
5238
5353
|
definition,
|
|
@@ -5251,9 +5366,9 @@ function CanvasBlock({
|
|
|
5251
5366
|
const block = useMemo3(() => __spreadProps(__spreadValues({}, sourceBlock), { style: resolveBlockStyle(sourceBlock.style, definition) }), [sourceBlock, definition]);
|
|
5252
5367
|
const renderKey = useMemo3(() => ({ definition, templateEngine, variables }), [definition, templateEngine, variables]);
|
|
5253
5368
|
const toolbarId = useId13();
|
|
5254
|
-
const [toolbarCollapsed, setToolbarCollapsed] =
|
|
5369
|
+
const [toolbarCollapsed, setToolbarCollapsed] = useState17(false);
|
|
5255
5370
|
const dragData = { kind: "canvas" };
|
|
5256
|
-
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData });
|
|
5371
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData, disabled: !active });
|
|
5257
5372
|
const select = (event) => {
|
|
5258
5373
|
event == null ? void 0 : event.stopPropagation();
|
|
5259
5374
|
onSelect();
|
|
@@ -5323,6 +5438,7 @@ function CanvasBlock({
|
|
|
5323
5438
|
// src/stage/canvas/Canvas.tsx
|
|
5324
5439
|
import { jsx as jsx52 } from "react/jsx-runtime";
|
|
5325
5440
|
function Canvas({
|
|
5441
|
+
active = true,
|
|
5326
5442
|
blocks,
|
|
5327
5443
|
body,
|
|
5328
5444
|
definitions,
|
|
@@ -5341,6 +5457,7 @@ function Canvas({
|
|
|
5341
5457
|
}) {
|
|
5342
5458
|
const { setNodeRef, isOver } = useDroppable({
|
|
5343
5459
|
id: CANVAS_DROP_ID,
|
|
5460
|
+
disabled: !active,
|
|
5344
5461
|
data: { kind: "canvas" }
|
|
5345
5462
|
});
|
|
5346
5463
|
return /* @__PURE__ */ jsx52(
|
|
@@ -5355,9 +5472,9 @@ function Canvas({
|
|
|
5355
5472
|
if (!ownerWindow || !(target instanceof ownerWindow.Element)) return;
|
|
5356
5473
|
if (target.closest(".ree-block")) return;
|
|
5357
5474
|
onSelect(null);
|
|
5358
|
-
const
|
|
5359
|
-
if (
|
|
5360
|
-
|
|
5475
|
+
const active2 = ownerDocument.activeElement;
|
|
5476
|
+
if (active2 instanceof ownerWindow.HTMLElement && active2.closest(".ree-rte")) {
|
|
5477
|
+
active2.blur();
|
|
5361
5478
|
}
|
|
5362
5479
|
},
|
|
5363
5480
|
children: /* @__PURE__ */ jsx52(
|
|
@@ -5392,6 +5509,7 @@ function Canvas({
|
|
|
5392
5509
|
CanvasBlock,
|
|
5393
5510
|
{
|
|
5394
5511
|
block,
|
|
5512
|
+
active,
|
|
5395
5513
|
body,
|
|
5396
5514
|
definition: getBlockDefinition(block.type, definitions),
|
|
5397
5515
|
selected: block.id === selectedId,
|
|
@@ -5420,84 +5538,134 @@ function Canvas({
|
|
|
5420
5538
|
}
|
|
5421
5539
|
|
|
5422
5540
|
// src/stage/preview/Preview.tsx
|
|
5423
|
-
import {
|
|
5541
|
+
import { useRef as useRef16 } from "react";
|
|
5542
|
+
|
|
5543
|
+
// src/stage/preview/PreviewFrame.tsx
|
|
5544
|
+
import { useLayoutEffect as useLayoutEffect10, useRef as useRef15 } from "react";
|
|
5424
5545
|
import { jsx as jsx53 } from "react/jsx-runtime";
|
|
5425
|
-
function
|
|
5426
|
-
const frameRef =
|
|
5427
|
-
const
|
|
5428
|
-
|
|
5429
|
-
() => config.toHtml(document, {
|
|
5430
|
-
resolveHtmlTemplates: true,
|
|
5431
|
-
variables
|
|
5432
|
-
}),
|
|
5433
|
-
[document, config, variables]
|
|
5434
|
-
);
|
|
5435
|
-
const observeContent = useCallback2(() => {
|
|
5436
|
-
var _a;
|
|
5437
|
-
(_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
|
|
5546
|
+
function PreviewFrame({ snapshot, onReady, onFailed }) {
|
|
5547
|
+
const frameRef = useRef15(null);
|
|
5548
|
+
const { id, html } = snapshot;
|
|
5549
|
+
useLayoutEffect10(() => {
|
|
5438
5550
|
const frame = frameRef.current;
|
|
5439
|
-
const
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
let
|
|
5443
|
-
const
|
|
5444
|
-
|
|
5551
|
+
const ownerWindow = frame == null ? void 0 : frame.ownerDocument.defaultView;
|
|
5552
|
+
if (!frame || !ownerWindow) return;
|
|
5553
|
+
let status = "loading";
|
|
5554
|
+
let stopObserving;
|
|
5555
|
+
const fail = (message) => {
|
|
5556
|
+
if (status !== "loading") return;
|
|
5557
|
+
status = "failed";
|
|
5558
|
+
clearTimeout(timer);
|
|
5559
|
+
stopObserving == null ? void 0 : stopObserving();
|
|
5560
|
+
stopObserving = void 0;
|
|
5561
|
+
onFailed(id, message);
|
|
5445
5562
|
};
|
|
5446
|
-
const
|
|
5447
|
-
|
|
5448
|
-
|
|
5563
|
+
const timer = setTimeout(() => fail("Email preview timed out. Please retry."), PREVIEW_LOAD_TIMEOUT_MS);
|
|
5564
|
+
const observeContent = () => {
|
|
5565
|
+
if (status !== "loading") return;
|
|
5566
|
+
stopObserving == null ? void 0 : stopObserving();
|
|
5567
|
+
stopObserving = void 0;
|
|
5568
|
+
try {
|
|
5569
|
+
const frameDocument = frame.contentDocument;
|
|
5570
|
+
const frameWindow = frameDocument == null ? void 0 : frameDocument.defaultView;
|
|
5571
|
+
if (!(frameDocument == null ? void 0 : frameDocument.body) || !frameWindow) throw new Error("Email preview could not be measured. Please retry.");
|
|
5572
|
+
let animationFrame = 0;
|
|
5573
|
+
let readyFrame = 0;
|
|
5574
|
+
const fit = () => {
|
|
5575
|
+
try {
|
|
5576
|
+
const height = frameDocument.body.getBoundingClientRect().height;
|
|
5577
|
+
if (!Number.isFinite(height) || height < 0) throw new Error("Email preview returned an invalid height. Please retry.");
|
|
5578
|
+
frame.style.height = `${Math.max(1, height)}px`;
|
|
5579
|
+
return true;
|
|
5580
|
+
} catch (error) {
|
|
5581
|
+
fail(error instanceof Error ? error.message : "Email preview could not be measured. Please retry.");
|
|
5582
|
+
return false;
|
|
5583
|
+
}
|
|
5584
|
+
};
|
|
5585
|
+
const scheduleFit = () => {
|
|
5586
|
+
ownerWindow.cancelAnimationFrame(animationFrame);
|
|
5587
|
+
animationFrame = ownerWindow.requestAnimationFrame(fit);
|
|
5588
|
+
};
|
|
5589
|
+
const observer = new frameWindow.ResizeObserver(scheduleFit);
|
|
5590
|
+
stopObserving = () => {
|
|
5591
|
+
observer.disconnect();
|
|
5592
|
+
frameDocument.removeEventListener("load", scheduleFit, true);
|
|
5593
|
+
ownerWindow.cancelAnimationFrame(animationFrame);
|
|
5594
|
+
ownerWindow.cancelAnimationFrame(readyFrame);
|
|
5595
|
+
};
|
|
5596
|
+
observer.observe(frameDocument.body);
|
|
5597
|
+
frameDocument.addEventListener("load", scheduleFit, true);
|
|
5598
|
+
if (!fit()) return;
|
|
5599
|
+
readyFrame = ownerWindow.requestAnimationFrame(() => {
|
|
5600
|
+
if (status !== "loading" || !fit()) return;
|
|
5601
|
+
status = "ready";
|
|
5602
|
+
clearTimeout(timer);
|
|
5603
|
+
onReady(id);
|
|
5604
|
+
});
|
|
5605
|
+
} catch (error) {
|
|
5606
|
+
fail(error instanceof Error ? error.message : "Email preview could not be loaded. Please retry.");
|
|
5607
|
+
}
|
|
5449
5608
|
};
|
|
5450
|
-
const
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5609
|
+
const loadError = () => fail("Email preview could not be loaded. Please retry.");
|
|
5610
|
+
frame.addEventListener("load", observeContent);
|
|
5611
|
+
frame.addEventListener("error", loadError);
|
|
5612
|
+
return () => {
|
|
5613
|
+
status = "disposed";
|
|
5614
|
+
clearTimeout(timer);
|
|
5615
|
+
stopObserving == null ? void 0 : stopObserving();
|
|
5616
|
+
frame.removeEventListener("load", observeContent);
|
|
5617
|
+
frame.removeEventListener("error", loadError);
|
|
5458
5618
|
};
|
|
5459
|
-
}, []);
|
|
5460
|
-
|
|
5461
|
-
var _a;
|
|
5462
|
-
return (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
|
|
5463
|
-
}, [html]);
|
|
5464
|
-
return /* @__PURE__ */ jsx53("div", { className: "ree-preview-stage", children: /* @__PURE__ */ jsx53(
|
|
5465
|
-
"div",
|
|
5466
|
-
{
|
|
5467
|
-
className: "ree-email-viewport",
|
|
5468
|
-
style: {
|
|
5469
|
-
width: viewportWidth(device, document.body),
|
|
5470
|
-
marginLeft: "auto",
|
|
5471
|
-
marginRight: "auto"
|
|
5472
|
-
},
|
|
5473
|
-
children: /* @__PURE__ */ jsx53(
|
|
5474
|
-
"iframe",
|
|
5475
|
-
{
|
|
5476
|
-
ref: frameRef,
|
|
5477
|
-
className: "ree-preview-frame",
|
|
5478
|
-
title: "Email preview",
|
|
5479
|
-
srcDoc: html,
|
|
5480
|
-
onLoad: observeContent,
|
|
5481
|
-
style: {
|
|
5482
|
-
backgroundColor: "#ffffff"
|
|
5483
|
-
}
|
|
5484
|
-
}
|
|
5485
|
-
)
|
|
5486
|
-
}
|
|
5487
|
-
) });
|
|
5619
|
+
}, [id, onReady, onFailed]);
|
|
5620
|
+
return /* @__PURE__ */ jsx53("iframe", { ref: frameRef, className: "ree-preview-frame", title: "Email preview", sandbox: "allow-same-origin", srcDoc: html });
|
|
5488
5621
|
}
|
|
5489
5622
|
|
|
5490
|
-
// src/stage/
|
|
5623
|
+
// src/stage/preview/Preview.tsx
|
|
5491
5624
|
import { jsx as jsx54, jsxs as jsxs41 } from "react/jsx-runtime";
|
|
5625
|
+
function Preview({ frames, front, failure, onReady, onFailed, onRetry }) {
|
|
5626
|
+
const scrollRef = useRef16({ top: 0, left: 0 });
|
|
5627
|
+
return /* @__PURE__ */ jsxs41("div", { className: "ree-preview-buffer", children: [
|
|
5628
|
+
PREVIEW_BUFFER_SLOTS.map((index) => {
|
|
5629
|
+
const snapshot = frames[index];
|
|
5630
|
+
const visible = front === index;
|
|
5631
|
+
return /* @__PURE__ */ jsx54(
|
|
5632
|
+
"div",
|
|
5633
|
+
__spreadProps(__spreadValues({
|
|
5634
|
+
className: `ree-preview-stage${visible ? "" : " is-buffered"}`,
|
|
5635
|
+
"aria-hidden": !visible
|
|
5636
|
+
}, !visible ? { inert: "" } : {}), {
|
|
5637
|
+
ref: (node) => {
|
|
5638
|
+
if (node && visible) {
|
|
5639
|
+
node.scrollTop = scrollRef.current.top;
|
|
5640
|
+
node.scrollLeft = scrollRef.current.left;
|
|
5641
|
+
}
|
|
5642
|
+
},
|
|
5643
|
+
onScroll: (event) => {
|
|
5644
|
+
if (visible) scrollRef.current = { top: event.currentTarget.scrollTop, left: event.currentTarget.scrollLeft };
|
|
5645
|
+
},
|
|
5646
|
+
children: /* @__PURE__ */ jsx54("div", { className: "ree-email-viewport", style: { width: snapshot == null ? void 0 : snapshot.width, marginLeft: "auto", marginRight: "auto" }, children: snapshot ? /* @__PURE__ */ jsx54(PreviewFrame, { snapshot, onReady, onFailed }, snapshot.id) : null })
|
|
5647
|
+
}),
|
|
5648
|
+
index
|
|
5649
|
+
);
|
|
5650
|
+
}),
|
|
5651
|
+
failure ? /* @__PURE__ */ jsxs41("div", { className: "ree-plugin-error ree-preview-error", role: "alert", children: [
|
|
5652
|
+
/* @__PURE__ */ jsx54("p", { children: failure.message }),
|
|
5653
|
+
/* @__PURE__ */ jsx54(Button, { onClick: onRetry, children: "Retry" })
|
|
5654
|
+
] }) : null
|
|
5655
|
+
] });
|
|
5656
|
+
}
|
|
5657
|
+
|
|
5658
|
+
// src/stage/StageHeader.tsx
|
|
5659
|
+
import { jsx as jsx55, jsxs as jsxs42 } from "react/jsx-runtime";
|
|
5492
5660
|
function StageHeader({
|
|
5493
5661
|
viewMode,
|
|
5494
5662
|
device,
|
|
5495
5663
|
onViewModeChange,
|
|
5496
5664
|
onDeviceChange
|
|
5497
5665
|
}) {
|
|
5498
|
-
return /* @__PURE__ */
|
|
5499
|
-
/* @__PURE__ */
|
|
5500
|
-
/* @__PURE__ */
|
|
5666
|
+
return /* @__PURE__ */ jsxs42("div", { className: "ree-col-head ree-stage-head", children: [
|
|
5667
|
+
/* @__PURE__ */ jsxs42("div", { className: "ree-segment", role: "group", "aria-label": "Editor mode", children: [
|
|
5668
|
+
/* @__PURE__ */ jsx55(
|
|
5501
5669
|
"button",
|
|
5502
5670
|
{
|
|
5503
5671
|
type: "button",
|
|
@@ -5506,14 +5674,14 @@ function StageHeader({
|
|
|
5506
5674
|
"aria-label": "Edit",
|
|
5507
5675
|
title: "Edit",
|
|
5508
5676
|
onClick: () => onViewModeChange("edit"),
|
|
5509
|
-
children: /* @__PURE__ */
|
|
5510
|
-
/* @__PURE__ */
|
|
5511
|
-
/* @__PURE__ */
|
|
5677
|
+
children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
5678
|
+
/* @__PURE__ */ jsx55("path", { d: "M12 20h9" }),
|
|
5679
|
+
/* @__PURE__ */ jsx55("path", { d: "M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4 11.5-11.5Z" })
|
|
5512
5680
|
] })
|
|
5513
5681
|
}
|
|
5514
5682
|
),
|
|
5515
|
-
/* @__PURE__ */
|
|
5516
|
-
/* @__PURE__ */
|
|
5683
|
+
/* @__PURE__ */ jsx55("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
|
|
5684
|
+
/* @__PURE__ */ jsx55(
|
|
5517
5685
|
"button",
|
|
5518
5686
|
{
|
|
5519
5687
|
type: "button",
|
|
@@ -5522,15 +5690,15 @@ function StageHeader({
|
|
|
5522
5690
|
"aria-label": "Preview",
|
|
5523
5691
|
title: "Preview",
|
|
5524
5692
|
onClick: () => onViewModeChange("preview"),
|
|
5525
|
-
children: /* @__PURE__ */
|
|
5526
|
-
/* @__PURE__ */
|
|
5527
|
-
/* @__PURE__ */
|
|
5693
|
+
children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
5694
|
+
/* @__PURE__ */ jsx55("path", { d: "M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" }),
|
|
5695
|
+
/* @__PURE__ */ jsx55("circle", { cx: "12", cy: "12", r: "2.5" })
|
|
5528
5696
|
] })
|
|
5529
5697
|
}
|
|
5530
5698
|
)
|
|
5531
5699
|
] }),
|
|
5532
|
-
/* @__PURE__ */
|
|
5533
|
-
/* @__PURE__ */
|
|
5700
|
+
/* @__PURE__ */ jsxs42("div", { className: "ree-segment", role: "group", "aria-label": "Canvas device", children: [
|
|
5701
|
+
/* @__PURE__ */ jsx55(
|
|
5534
5702
|
"button",
|
|
5535
5703
|
{
|
|
5536
5704
|
type: "button",
|
|
@@ -5539,15 +5707,15 @@ function StageHeader({
|
|
|
5539
5707
|
"aria-label": "Desktop",
|
|
5540
5708
|
title: "Desktop",
|
|
5541
5709
|
onClick: () => onDeviceChange("desktop"),
|
|
5542
|
-
children: /* @__PURE__ */
|
|
5543
|
-
/* @__PURE__ */
|
|
5544
|
-
/* @__PURE__ */
|
|
5545
|
-
/* @__PURE__ */
|
|
5710
|
+
children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
5711
|
+
/* @__PURE__ */ jsx55("rect", { x: "3", y: "4", width: "18", height: "12", rx: "1.5" }),
|
|
5712
|
+
/* @__PURE__ */ jsx55("path", { d: "M8 20h8" }),
|
|
5713
|
+
/* @__PURE__ */ jsx55("path", { d: "M12 16v4" })
|
|
5546
5714
|
] })
|
|
5547
5715
|
}
|
|
5548
5716
|
),
|
|
5549
|
-
/* @__PURE__ */
|
|
5550
|
-
/* @__PURE__ */
|
|
5717
|
+
/* @__PURE__ */ jsx55("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
|
|
5718
|
+
/* @__PURE__ */ jsx55(
|
|
5551
5719
|
"button",
|
|
5552
5720
|
{
|
|
5553
5721
|
type: "button",
|
|
@@ -5556,9 +5724,9 @@ function StageHeader({
|
|
|
5556
5724
|
"aria-label": "Mobile",
|
|
5557
5725
|
title: "Mobile",
|
|
5558
5726
|
onClick: () => onDeviceChange("mobile"),
|
|
5559
|
-
children: /* @__PURE__ */
|
|
5560
|
-
/* @__PURE__ */
|
|
5561
|
-
/* @__PURE__ */
|
|
5727
|
+
children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
5728
|
+
/* @__PURE__ */ jsx55("rect", { x: "7", y: "2.5", width: "10", height: "19", rx: "2" }),
|
|
5729
|
+
/* @__PURE__ */ jsx55("path", { d: "M11 18.5h2" })
|
|
5562
5730
|
] })
|
|
5563
5731
|
}
|
|
5564
5732
|
)
|
|
@@ -5567,7 +5735,7 @@ function StageHeader({
|
|
|
5567
5735
|
}
|
|
5568
5736
|
|
|
5569
5737
|
// src/stage/Stage.tsx
|
|
5570
|
-
import { jsx as
|
|
5738
|
+
import { jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
|
|
5571
5739
|
function Stage({
|
|
5572
5740
|
session,
|
|
5573
5741
|
viewMode,
|
|
@@ -5576,64 +5744,70 @@ function Stage({
|
|
|
5576
5744
|
onDeviceChange,
|
|
5577
5745
|
onSelectBlock
|
|
5578
5746
|
}) {
|
|
5579
|
-
const
|
|
5747
|
+
const source = useMemo4(() => ({ document: session.document, config: session.config, variables: session.variables, device }), [session.document, session.config, session.variables, device]);
|
|
5748
|
+
const requested = viewMode === "preview";
|
|
5749
|
+
const preview = usePreviewBuffer(source, requested);
|
|
5750
|
+
const [previewVisible, setPreviewVisible] = useState18(false);
|
|
5751
|
+
if (!requested && previewVisible) setPreviewVisible(false);
|
|
5752
|
+
if (requested && !previewVisible && (preview.ready || preview.failure)) setPreviewVisible(true);
|
|
5753
|
+
const editing = !requested;
|
|
5580
5754
|
const { targetId, dropEdge } = useEditorDragState();
|
|
5581
|
-
return /* @__PURE__ */
|
|
5582
|
-
/* @__PURE__ */
|
|
5755
|
+
return /* @__PURE__ */ jsxs43("div", { className: "ree-col ree-col-center", children: [
|
|
5756
|
+
/* @__PURE__ */ jsx56(
|
|
5583
5757
|
StageHeader,
|
|
5584
5758
|
{
|
|
5585
5759
|
viewMode,
|
|
5586
5760
|
device,
|
|
5587
|
-
onViewModeChange
|
|
5761
|
+
onViewModeChange: (mode) => {
|
|
5762
|
+
if (mode === "preview") preview.flush();
|
|
5763
|
+
onViewModeChange(mode);
|
|
5764
|
+
},
|
|
5588
5765
|
onDeviceChange
|
|
5589
5766
|
}
|
|
5590
5767
|
),
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
device
|
|
5617
|
-
}
|
|
5618
|
-
) }) })
|
|
5768
|
+
/* @__PURE__ */ jsxs43("div", { className: "ree-stage-content", "aria-busy": requested && !preview.ready && !preview.failure, children: [
|
|
5769
|
+
/* @__PURE__ */ jsx56("div", __spreadProps(__spreadValues({ className: `ree-stage-view${previewVisible ? " is-buffered" : ""}`, "aria-hidden": previewVisible }, !editing ? { inert: "" } : {}), { children: /* @__PURE__ */ jsx56(
|
|
5770
|
+
Canvas,
|
|
5771
|
+
{
|
|
5772
|
+
blocks: session.document.blocks,
|
|
5773
|
+
body: session.document.body,
|
|
5774
|
+
definitions: session.definitions,
|
|
5775
|
+
active: editing,
|
|
5776
|
+
selectedId: editing ? session.selectedId : null,
|
|
5777
|
+
dropTargetId: targetId,
|
|
5778
|
+
dropEdge,
|
|
5779
|
+
variables: session.variables,
|
|
5780
|
+
templateEngine: session.config.templateEngine,
|
|
5781
|
+
device,
|
|
5782
|
+
onSelect: onSelectBlock,
|
|
5783
|
+
onCommitControl: session.updateControl,
|
|
5784
|
+
onCommitControls: session.updateControls,
|
|
5785
|
+
onCommitStyle: session.updateStyle,
|
|
5786
|
+
onDuplicate: session.duplicateBlock,
|
|
5787
|
+
onRemove: session.removeBlock
|
|
5788
|
+
}
|
|
5789
|
+
) })),
|
|
5790
|
+
/* @__PURE__ */ jsx56("div", __spreadProps(__spreadValues({ className: `ree-stage-view${previewVisible ? "" : " is-buffered"}`, "aria-hidden": !previewVisible }, !previewVisible ? { inert: "" } : {}), { children: /* @__PURE__ */ jsx56(Preview, { frames: preview.frames, front: preview.front, failure: preview.displayFailure, onReady: preview.markReady, onFailed: preview.markFailed, onRetry: preview.retry }) })),
|
|
5791
|
+
requested && !preview.ready && !preview.failure ? /* @__PURE__ */ jsx56("div", { className: "ree-preview-pending", role: "status", children: "Preparing preview\u2026" }) : null
|
|
5792
|
+
] })
|
|
5619
5793
|
] });
|
|
5620
5794
|
}
|
|
5621
5795
|
|
|
5622
5796
|
// src/shell/SubjectBar.tsx
|
|
5623
|
-
import { useId as useId14, useRef as
|
|
5797
|
+
import { useId as useId14, useRef as useRef17 } from "react";
|
|
5624
5798
|
|
|
5625
5799
|
// src/shell/constants.ts
|
|
5626
5800
|
var EMPTY_VARIABLES = [];
|
|
5627
5801
|
var PERSONALIZATION_HINT = "Personalization fields will appear blank if no data is available.";
|
|
5628
5802
|
|
|
5629
5803
|
// src/shell/SubjectBar.tsx
|
|
5630
|
-
import { Fragment as Fragment5, jsx as
|
|
5804
|
+
import { Fragment as Fragment5, jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
|
|
5631
5805
|
function SubjectBar({
|
|
5632
5806
|
subject,
|
|
5633
5807
|
variables,
|
|
5634
5808
|
onSubjectChange
|
|
5635
5809
|
}) {
|
|
5636
|
-
const inputRef =
|
|
5810
|
+
const inputRef = useRef17(null);
|
|
5637
5811
|
const subjectId = useId14();
|
|
5638
5812
|
const countId = useId14();
|
|
5639
5813
|
const insertVariable = (token) => {
|
|
@@ -5653,10 +5827,10 @@ function SubjectBar({
|
|
|
5653
5827
|
(_b2 = inputRef.current) == null ? void 0 : _b2.setSelectionRange(cursor, cursor);
|
|
5654
5828
|
});
|
|
5655
5829
|
};
|
|
5656
|
-
return /* @__PURE__ */
|
|
5657
|
-
/* @__PURE__ */
|
|
5658
|
-
/* @__PURE__ */
|
|
5659
|
-
/* @__PURE__ */
|
|
5830
|
+
return /* @__PURE__ */ jsxs44("div", { className: "ree-subject-bar", children: [
|
|
5831
|
+
/* @__PURE__ */ jsx57("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
|
|
5832
|
+
/* @__PURE__ */ jsxs44("div", { className: "ree-subject-field", children: [
|
|
5833
|
+
/* @__PURE__ */ jsx57(
|
|
5660
5834
|
TextInput,
|
|
5661
5835
|
{
|
|
5662
5836
|
id: subjectId,
|
|
@@ -5668,21 +5842,21 @@ function SubjectBar({
|
|
|
5668
5842
|
onChange: (event) => onSubjectChange(event.target.value)
|
|
5669
5843
|
}
|
|
5670
5844
|
),
|
|
5671
|
-
/* @__PURE__ */
|
|
5845
|
+
/* @__PURE__ */ jsxs44("span", { id: countId, className: "ree-subject-count", children: [
|
|
5672
5846
|
subject.length,
|
|
5673
5847
|
"/",
|
|
5674
5848
|
SUBJECT_MAX_LENGTH
|
|
5675
5849
|
] })
|
|
5676
5850
|
] }),
|
|
5677
|
-
variables.length > 0 ? /* @__PURE__ */
|
|
5678
|
-
/* @__PURE__ */
|
|
5679
|
-
/* @__PURE__ */
|
|
5851
|
+
variables.length > 0 ? /* @__PURE__ */ jsxs44(Fragment5, { children: [
|
|
5852
|
+
/* @__PURE__ */ jsx57(PersonalizationMenu, { variables, onInsert: insertVariable }),
|
|
5853
|
+
/* @__PURE__ */ jsx57("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
|
|
5680
5854
|
] }) : null
|
|
5681
5855
|
] });
|
|
5682
5856
|
}
|
|
5683
5857
|
|
|
5684
5858
|
// src/shell/useEditorSession.ts
|
|
5685
|
-
import { useCallback as useCallback3, useState as
|
|
5859
|
+
import { useCallback as useCallback3, useState as useState19, useSyncExternalStore } from "react";
|
|
5686
5860
|
|
|
5687
5861
|
// src/utils/id.ts
|
|
5688
5862
|
function generateId(prefix = "block") {
|
|
@@ -5997,8 +6171,8 @@ function useEditorSession(editor) {
|
|
|
5997
6171
|
const store = getEditorStore(editor);
|
|
5998
6172
|
const { document, config, variables, resetKey } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
5999
6173
|
const definitions = config.definitions;
|
|
6000
|
-
const [selectedId, setSelectedId] =
|
|
6001
|
-
const [previousReset, setPreviousReset] =
|
|
6174
|
+
const [selectedId, setSelectedId] = useState19(null);
|
|
6175
|
+
const [previousReset, setPreviousReset] = useState19({ editor, resetKey });
|
|
6002
6176
|
if (previousReset.editor !== editor || previousReset.resetKey !== resetKey) {
|
|
6003
6177
|
setPreviousReset({ editor, resetKey });
|
|
6004
6178
|
setSelectedId(null);
|
|
@@ -6099,21 +6273,21 @@ function useEditorSession(editor) {
|
|
|
6099
6273
|
}
|
|
6100
6274
|
|
|
6101
6275
|
// src/shell/EmailEditorShell.tsx
|
|
6102
|
-
import { jsx as
|
|
6276
|
+
import { jsx as jsx58, jsxs as jsxs45 } from "react/jsx-runtime";
|
|
6103
6277
|
function EmailEditorShell({ editor, height = "100%" }) {
|
|
6104
6278
|
var _a;
|
|
6105
6279
|
const session = useEditorSession(editor);
|
|
6106
|
-
const [activeRichText, setActiveRichText] =
|
|
6107
|
-
const [viewMode, setViewMode] =
|
|
6108
|
-
const [device, setDevice] =
|
|
6109
|
-
const [inspectorPanel, setInspectorPanel] =
|
|
6110
|
-
const [previousReset, setPreviousReset] =
|
|
6280
|
+
const [activeRichText, setActiveRichText] = useState20(null);
|
|
6281
|
+
const [viewMode, setViewMode] = useState20("edit");
|
|
6282
|
+
const [device, setDevice] = useState20("desktop");
|
|
6283
|
+
const [inspectorPanel, setInspectorPanel] = useState20("body");
|
|
6284
|
+
const [previousReset, setPreviousReset] = useState20({ editor, key: session.resetKey });
|
|
6111
6285
|
if (previousReset.editor !== editor || previousReset.key !== session.resetKey) {
|
|
6112
6286
|
setPreviousReset({ editor, key: session.resetKey });
|
|
6113
6287
|
setActiveRichText(null);
|
|
6114
6288
|
setInspectorPanel("body");
|
|
6115
6289
|
}
|
|
6116
|
-
const [previousSelectedId, setPreviousSelectedId] =
|
|
6290
|
+
const [previousSelectedId, setPreviousSelectedId] = useState20(session.selectedId);
|
|
6117
6291
|
if (previousSelectedId !== session.selectedId) {
|
|
6118
6292
|
setPreviousSelectedId(session.selectedId);
|
|
6119
6293
|
if (!session.selectedId) setInspectorPanel("body");
|
|
@@ -6137,8 +6311,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6137
6311
|
selectBlock(null);
|
|
6138
6312
|
};
|
|
6139
6313
|
const showCanvas = () => setViewMode("edit");
|
|
6140
|
-
return /* @__PURE__ */
|
|
6141
|
-
/* @__PURE__ */
|
|
6314
|
+
return /* @__PURE__ */ jsx58(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx58(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs45("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
|
|
6315
|
+
/* @__PURE__ */ jsx58(
|
|
6142
6316
|
SubjectBar,
|
|
6143
6317
|
{
|
|
6144
6318
|
subject: (_a = session.document.subject) != null ? _a : "",
|
|
@@ -6146,8 +6320,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6146
6320
|
onSubjectChange: session.setSubject
|
|
6147
6321
|
}
|
|
6148
6322
|
),
|
|
6149
|
-
/* @__PURE__ */
|
|
6150
|
-
/* @__PURE__ */
|
|
6323
|
+
/* @__PURE__ */ jsxs45("div", { className: "ree-body", children: [
|
|
6324
|
+
/* @__PURE__ */ jsxs45(
|
|
6151
6325
|
EditorDnd,
|
|
6152
6326
|
{
|
|
6153
6327
|
blocks: session.document.blocks,
|
|
@@ -6159,7 +6333,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6159
6333
|
onReorder: session.reorderBlocks,
|
|
6160
6334
|
onLibraryDragStart: showCanvas,
|
|
6161
6335
|
children: [
|
|
6162
|
-
/* @__PURE__ */
|
|
6336
|
+
/* @__PURE__ */ jsx58(
|
|
6163
6337
|
BlockLibrary,
|
|
6164
6338
|
{
|
|
6165
6339
|
definitions: session.config.library,
|
|
@@ -6169,7 +6343,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6169
6343
|
}
|
|
6170
6344
|
}
|
|
6171
6345
|
),
|
|
6172
|
-
/* @__PURE__ */
|
|
6346
|
+
/* @__PURE__ */ jsx58(
|
|
6173
6347
|
Stage,
|
|
6174
6348
|
{
|
|
6175
6349
|
session,
|
|
@@ -6183,7 +6357,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6183
6357
|
]
|
|
6184
6358
|
}
|
|
6185
6359
|
),
|
|
6186
|
-
/* @__PURE__ */
|
|
6360
|
+
/* @__PURE__ */ jsx58(
|
|
6187
6361
|
Inspector,
|
|
6188
6362
|
{
|
|
6189
6363
|
panel: inspectorPanel,
|
|
@@ -6203,23 +6377,23 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6203
6377
|
}
|
|
6204
6378
|
|
|
6205
6379
|
// src/EmailEditor.tsx
|
|
6206
|
-
import { jsx as
|
|
6380
|
+
import { jsx as jsx59 } from "react/jsx-runtime";
|
|
6207
6381
|
function EmailEditor(props) {
|
|
6208
|
-
return /* @__PURE__ */
|
|
6382
|
+
return /* @__PURE__ */ jsx59(EmailEditorShell, __spreadValues({}, props));
|
|
6209
6383
|
}
|
|
6210
6384
|
|
|
6211
6385
|
// src/plugins/useEmailEditorConfig.ts
|
|
6212
|
-
import { useMemo as
|
|
6386
|
+
import { useMemo as useMemo5 } from "react";
|
|
6213
6387
|
function useEmailEditorConfig(options = {}) {
|
|
6214
6388
|
const { plugins, builtins, pickImage, templateEngine } = options;
|
|
6215
|
-
return
|
|
6389
|
+
return useMemo5(() => buildEmailEditorConfig({ plugins, builtins, pickImage, templateEngine }), [plugins, builtins, pickImage, templateEngine]);
|
|
6216
6390
|
}
|
|
6217
6391
|
|
|
6218
6392
|
// src/shell/useEmailEditor.ts
|
|
6219
|
-
import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as
|
|
6220
|
-
var useOptionsEffect = typeof window === "undefined" ? useEffect8 :
|
|
6393
|
+
import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as useLayoutEffect11, useState as useState21 } from "react";
|
|
6394
|
+
var useOptionsEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect11;
|
|
6221
6395
|
function useEmailEditor(options = {}) {
|
|
6222
|
-
const [store] =
|
|
6396
|
+
const [store] = useState21(() => createEditorStore(options));
|
|
6223
6397
|
useInsertionEffect(() => {
|
|
6224
6398
|
store.updateOptions(options);
|
|
6225
6399
|
});
|
|
@@ -6235,12 +6409,12 @@ function useEmailEditorState(editor) {
|
|
|
6235
6409
|
}
|
|
6236
6410
|
|
|
6237
6411
|
// src/plugins/components/ControlEditor.tsx
|
|
6238
|
-
import { jsx as
|
|
6412
|
+
import { jsx as jsx60 } from "react/jsx-runtime";
|
|
6239
6413
|
function ControlEditor({ control }) {
|
|
6240
6414
|
const context = usePluginEditor();
|
|
6241
6415
|
const definition = context.controlDefinitions.find((item) => item.name === control);
|
|
6242
6416
|
if (!definition) throw new Error(`Unknown plugin control: ${control}`);
|
|
6243
|
-
return /* @__PURE__ */
|
|
6417
|
+
return /* @__PURE__ */ jsx60(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
|
|
6244
6418
|
}
|
|
6245
6419
|
export {
|
|
6246
6420
|
BLOCK_TYPES,
|
package/dist/styles.css
CHANGED
|
@@ -1007,6 +1007,38 @@
|
|
|
1007
1007
|
|
|
1008
1008
|
/* ---------- 画布 ---------- */
|
|
1009
1009
|
|
|
1010
|
+
.ree-stage-content,
|
|
1011
|
+
.ree-preview-buffer {
|
|
1012
|
+
position: relative;
|
|
1013
|
+
display: flex;
|
|
1014
|
+
flex: 1;
|
|
1015
|
+
min-width: 0;
|
|
1016
|
+
min-height: 0;
|
|
1017
|
+
isolation: isolate;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.ree-stage-view.is-buffered,
|
|
1021
|
+
.ree-preview-stage.is-buffered {
|
|
1022
|
+
position: absolute;
|
|
1023
|
+
inset: 0;
|
|
1024
|
+
visibility: hidden;
|
|
1025
|
+
pointer-events: none;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.ree-preview-pending {
|
|
1029
|
+
position: absolute;
|
|
1030
|
+
top: 12px;
|
|
1031
|
+
left: 50%;
|
|
1032
|
+
transform: translateX(-50%);
|
|
1033
|
+
padding: 6px 12px;
|
|
1034
|
+
border: 1px solid var(--ree-border);
|
|
1035
|
+
border-radius: 6px;
|
|
1036
|
+
background: var(--ree-panel);
|
|
1037
|
+
color: var(--ree-muted);
|
|
1038
|
+
font-size: 12px;
|
|
1039
|
+
pointer-events: none;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1010
1042
|
.ree-stage-view {
|
|
1011
1043
|
display: flex;
|
|
1012
1044
|
flex: 1;
|
|
@@ -1090,6 +1122,18 @@
|
|
|
1090
1122
|
box-shadow: var(--ree-shadow-md);
|
|
1091
1123
|
}
|
|
1092
1124
|
|
|
1125
|
+
.ree-preview-error {
|
|
1126
|
+
position: absolute;
|
|
1127
|
+
top: 12px;
|
|
1128
|
+
left: 50%;
|
|
1129
|
+
transform: translateX(-50%);
|
|
1130
|
+
z-index: 1;
|
|
1131
|
+
max-width: calc(100% - 24px);
|
|
1132
|
+
border: 1px solid var(--ree-border);
|
|
1133
|
+
border-radius: 6px;
|
|
1134
|
+
background: var(--ree-panel);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1093
1137
|
/* ---------- 区块 ---------- */
|
|
1094
1138
|
|
|
1095
1139
|
.ree-block {
|
package/docs/changelog.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](./changelog.zh-CN.md)
|
|
4
4
|
|
|
5
|
+
## 0.5.2 — 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Fixes and improvements
|
|
8
|
+
|
|
9
|
+
- Fix canvas recreation and flicker when switching between editing and preview. Keep the editing canvas mounted and swap two iframe buffers after standby loading and height measurement, preserving separate editing and preview scroll positions. Exclude the hidden canvas from focus, keyboard and drag interactions while previewing.
|
|
10
|
+
- Debounce document, configuration, variable and device-width changes for 400ms with a maximum wait of 2 seconds and idle scheduling support. Process Preview and Retry clicks immediately while continuing to debounce subsequent changes in preview mode. Reuse ready or loading buffers with identical HTML and width to avoid remounts.
|
|
11
|
+
- Use `sandbox="allow-same-origin"` on preview iframes to block scripts and event handlers while retaining height measurement. Preserve exported HTML; this restriction is not complete HTML sanitization.
|
|
12
|
+
- Handle generation, load and measurement errors, including a 15-second load/measurement timeout, with error messages and Retry. Retain existing previews on failure and force a new load on retry. Prevent callbacks from stale, replaced or failed buffers from overwriting current content, and clean up timers, observers, listeners and animation frames on unmount.
|
|
13
|
+
- Update both template-engine guides. Bump the npm package from `0.5.1` to `0.5.2` as a patch, preserving the public API, email export and persisted document format. Preview no longer executes scripts in email HTML; custom engines and `renderEmail` should be pure and allow repeated background calls.
|
|
14
|
+
|
|
15
|
+
### Validation
|
|
16
|
+
|
|
17
|
+
- `npm run check` passes type checking, Oxlint, the build and all 144 Node tests, covering debounce, maximum wait, buffer reuse, stale callbacks, timeouts, exceptions, retry and cleanup.
|
|
18
|
+
- Reuse the existing Chrome page to verify blocked scripts and event handlers, unchanged iframe nodes after subject-only edits, one load for five consecutive changes in preview mode, and a 15-second timeout that retains the old preview and recovers through Retry.
|
|
19
|
+
- Verify desktop 600px and mobile 360px previews and five consecutive editing/preview round trips. Canvas, rich-text and iframe nodes remain mounted, with no visible blank or unmeasured frame in sampled states. Restore the test document and remove temporary probes. `git diff --check` passes.
|
|
20
|
+
|
|
5
21
|
## 0.5.1 — 2026-10-29
|
|
6
22
|
|
|
7
23
|
### Fixes and improvements
|
package/docs/changelog.zh-CN.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./changelog.md) | 简体中文
|
|
4
4
|
|
|
5
|
+
## 0.5.2 — 2026-10-29
|
|
6
|
+
|
|
7
|
+
### 修复与改进
|
|
8
|
+
|
|
9
|
+
- 修复编辑与预览切换时画布重建和闪动:编辑画布常驻,两个 iframe 在备用层完成加载与高度测量后交换显示,分别保留编辑区和预览区的滚动位置;预览时禁用隐藏画布的焦点、键盘和拖拽交互。
|
|
10
|
+
- 文档、配置、变量和设备宽度变化使用 400ms 防抖与最长 2 秒等待,支持空闲调度;点击 Preview 或 Retry 立即处理,预览模式中的后续修改仍防抖。HTML 和宽度一致时复用已就绪或正在加载的缓冲,避免重复挂载。
|
|
11
|
+
- 预览 iframe 使用 `sandbox="allow-same-origin"` 阻止脚本和事件属性执行,保持高度测量能力;导出 HTML 保持原有内容,此约束不等同于完整 HTML 消毒。
|
|
12
|
+
- 补齐生成、加载和测量错误,以及 15 秒加载测量超时提示与 Retry。已有预览在失败时继续显示,重试强制新加载;过期、被替换或已失败缓冲的回调不能覆盖当前内容,卸载时清理计时器、观察器、监听器和动画帧。
|
|
13
|
+
- 同步中英文模板引擎文档。本次按 patch 将 npm 包版本从 `0.5.1` 升至 `0.5.2`,公开 API、邮件导出与持久化文档格式不变;预览不再执行邮件 HTML 中的脚本,自定义引擎与 `renderEmail` 应允许后台重复调用并保持纯函数。
|
|
14
|
+
|
|
15
|
+
### 验证
|
|
16
|
+
|
|
17
|
+
- `npm run check` 通过类型检查、Oxlint、构建与全部 144 项 Node 测试,覆盖防抖、最长等待、缓冲复用、过期回调、超时、异常、重试与清理。
|
|
18
|
+
- 复用现有 Chrome 页面验证脚本和事件属性被阻止、仅修改主题时 iframe 节点不变、预览内连续 5 次修改只加载 1 次,以及 15 秒超时保留旧预览并在 Retry 后恢复。
|
|
19
|
+
- 验证桌面 600px 与移动 360px 预览及连续五轮编辑/预览切换,画布、富文本和 iframe 节点保持不变,采样未出现可见空白或未测量帧;测试文档已还原,临时探针已清理。`git diff --check` 通过。
|
|
20
|
+
|
|
5
21
|
## 0.5.1 — 2026-10-29
|
|
6
22
|
|
|
7
23
|
### 修复与改进
|
package/docs/template-engines.md
CHANGED
|
@@ -36,6 +36,14 @@ Field names support Unicode letters, digits and underscores, starting with a let
|
|
|
36
36
|
|
|
37
37
|
Functions (including `eq/ne/and/or/not/len/index/printf/html`), pipelines, parentheses, local variables, template definitions/calls, comments and other full Go features require an external engine. Unsupported syntax or incomplete control structures preserve the **entire input for that render**, without partial evaluation. In the editor this preserves that block's HTML template.
|
|
38
38
|
|
|
39
|
+
## Switching between editing and preview
|
|
40
|
+
|
|
41
|
+
The editing canvas stays mounted, so switching modes preserves rich-text editors and HTML block iframes. Editing and preview retain their own scroll positions. While preview is requested, the canvas is excluded from focus, keyboard and drag interactions, and rich-text toolbars are hidden.
|
|
42
|
+
|
|
43
|
+
Changes to the document, configuration, variables or device width schedule a complete email preview after a 400ms debounce, using an idle callback with a maximum wait of 2 seconds during continuous changes. Browsers without `requestIdleCallback` use a timer. Clicking Preview or Retry bypasses the wait and prepares the latest revision; subsequent changes in preview mode still use the same debounce. HTML generation still runs synchronously on the main thread; custom engines and `renderEmail` should be pure and allow repeated background calls.
|
|
44
|
+
|
|
45
|
+
Two iframe buffers keep loading and measurement in the standby layer. A buffer becomes visible only after it has loaded and its height has been measured. Until the latest revision is ready, the current view remains visible with a preparation status. Stale revisions cannot replace newer content. Identical HTML and width reuse a ready or loading buffer to avoid unnecessary reloads. Generation, load or measurement failures, including a 15-second load/measurement timeout, show an error and Retry while retaining any existing preview. Retry forces a new load. Preview iframes use `sandbox="allow-same-origin"` to block scripts and event handlers while allowing the host to measure height; this is not complete HTML sanitization and does not rewrite exported HTML. This cache only serves the UI; explicit export APIs synchronously generate from the latest document at call time.
|
|
46
|
+
|
|
39
47
|
## Integration example
|
|
40
48
|
|
|
41
49
|
Wrap your template library as TemplateEngine and pass the same adapter to editing and standalone preview. The caller provides templateEngine in this example:
|
|
@@ -36,6 +36,14 @@ interface TemplateEngineOptions {
|
|
|
36
36
|
|
|
37
37
|
默认引擎不支持函数调用(含 `eq/ne/and/or/not/len/index/printf/html`)、管道、括号表达式、局部变量、模板定义/调用、注释和其他完整 Go 语法。输入包含不支持的语法或不完整的控制结构时,**保留该次渲染的完整输入,不执行部分替换**;编辑器中意味着保留对应区块的 HTML 模板。需要完整 Go 兼容性时接入外部引擎。
|
|
38
38
|
|
|
39
|
+
## 编辑与预览切换
|
|
40
|
+
|
|
41
|
+
编辑画布保持挂载,切换预览不会重建富文本编辑器或 HTML 区块 iframe。编辑区与预览区分别保留滚动位置;预览时编辑画布不参与焦点、键盘和拖拽交互,富文本工具栏也会隐藏。
|
|
42
|
+
|
|
43
|
+
文档、配置、变量或设备宽度变化后,完整邮件预览在停止修改 400ms 后排入空闲任务,连续修改时最长等待 2 秒;不支持 `requestIdleCallback` 时使用定时器。点击 Preview 或 Retry 会跳过等待并准备最新版本;进入预览后的后续修改仍使用相同防抖规则。HTML 生成仍在主线程同步执行,自定义引擎和 `renderEmail` 应保持纯函数,允许后台重复调用。
|
|
44
|
+
|
|
45
|
+
预览使用两个 iframe 缓冲:最新内容在备用层加载并测量高度,完成后才交换显示。首次预览或最新内容尚未就绪时保留当前画面并显示准备提示;过期版本不会覆盖新内容。HTML 和宽度相同时复用已就绪或正在加载的缓冲,避免无意义重载。生成、加载或测量失败,以及加载测量超过 15 秒时显示错误和 Retry;已有预览仍保留,Retry 强制创建新加载。预览 iframe 使用 `sandbox="allow-same-origin"`,阻止脚本及事件属性执行,同时允许宿主测量高度;这不等同于完整 HTML 消毒,也不改写导出的 HTML。预览缓存仅服务于界面,显式导出 API 仍按调用时的最新文档同步生成。
|
|
46
|
+
|
|
39
47
|
## 接入示例
|
|
40
48
|
|
|
41
49
|
业务侧把实际模板库封装为 `TemplateEngine`,再把同一个适配器用于编辑器和独立预览。下面示例的 `templateEngine` 由调用方提供:
|
package/package.json
CHANGED