@standhigher/react-email-editor 0.4.1 → 0.4.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/README.md +11 -10
- package/README.zh-CN.md +11 -10
- package/dist/index.js +207 -147
- package/dist/styles.css +41 -31
- package/docs/api.md +2 -0
- package/docs/api.zh-CN.md +2 -0
- package/docs/changelog.md +125 -0
- package/{CHANGELOG.md → docs/changelog.zh-CN.md} +27 -0
- package/docs/release.md +38 -0
- package/docs/release.zh-CN.md +38 -0
- package/package.json +20 -5
- package/docs/assets/architecture.png +0 -0
- package/docs/assets/architecture.zh-CN.png +0 -0
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @standhigher/react-email-editor
|
|
2
2
|
|
|
3
|
-
English | [简体中文](
|
|
3
|
+
English | [简体中文](https://github.com/standhigher/react-email-editor/blob/main/README.zh-CN.md)
|
|
4
4
|
|
|
5
5
|
Drag content onto a canvas, style your email, preview it, then save editable JSON or export email HTML.
|
|
6
6
|
|
|
7
7
|
## Modules and features
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
|
-
[Open full-size architecture diagram](
|
|
11
|
+
[Open full-size architecture diagram](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.png) · [Editable SVG](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.svg)
|
|
12
12
|
|
|
13
13
|
## Install and integrate
|
|
14
14
|
|
|
@@ -32,21 +32,22 @@ With Next.js App Router, add `"use client"` to the integration component and imp
|
|
|
32
32
|
|
|
33
33
|
## Run locally
|
|
34
34
|
|
|
35
|
-
Use
|
|
35
|
+
Use Node.js 24 LTS (`.nvmrc`; at least 22.18 for the complete test suite) and pnpm 10.16.1 (`packageManager`):
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
pnpm install --frozen-lockfile
|
|
39
39
|
pnpm dev
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Open [http://localhost:5173](http://localhost:5173); reuse an existing server when available. See [development and local linking](
|
|
42
|
+
Open [http://localhost:5173](http://localhost:5173); reuse an existing server when available. See [development and local linking](https://github.com/standhigher/react-email-editor/blob/main/docs/api.md#development-and-local-linking) for build and validation commands.
|
|
43
43
|
|
|
44
44
|
## Documentation
|
|
45
45
|
|
|
46
|
-
- [API reference](
|
|
47
|
-
- [Basic examples](
|
|
48
|
-
- [Advanced examples](
|
|
49
|
-
- [Custom block guide](
|
|
50
|
-
- [
|
|
46
|
+
- [API reference](https://github.com/standhigher/react-email-editor/blob/main/docs/api.md): every public export, parameter, default and type.
|
|
47
|
+
- [Basic examples](https://github.com/standhigher/react-email-editor/blob/main/docs/basic-examples.md): initial content, saving/loading, body styles, variables and preview.
|
|
48
|
+
- [Advanced examples](https://github.com/standhigher/react-email-editor/blob/main/docs/advanced-examples.md): validated saving, multiple instances, custom blocks, multiple editing regions, resource adapters and standalone export.
|
|
49
|
+
- [Custom block guide](https://github.com/standhigher/react-email-editor/blob/main/docs/custom-blocks.md): detailed controls, plugins and rendering helpers.
|
|
50
|
+
- [Release guide](https://github.com/standhigher/react-email-editor/blob/main/docs/release.md): package checks and manual npm publishing.
|
|
51
|
+
- [Changelog](https://github.com/standhigher/react-email-editor/blob/main/docs/changelog.md).
|
|
51
52
|
|
|
52
53
|
The document format is under development. Only `version: 4` is supported, without automatic migration of older documents.
|
package/README.zh-CN.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @standhigher/react-email-editor
|
|
2
2
|
|
|
3
|
-
[English](
|
|
3
|
+
[English](https://github.com/standhigher/react-email-editor/blob/main/README.md) | 简体中文
|
|
4
4
|
|
|
5
5
|
把内容拖进画布,调整邮件外观,预览后保存为可继续编辑的 JSON 或导出邮件 HTML。
|
|
6
6
|
|
|
7
7
|
## 模块架构图
|
|
8
8
|
|
|
9
|
-

|
|
10
10
|
|
|
11
|
-
[查看高清架构图](
|
|
11
|
+
[查看高清架构图](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.zh-CN.png) · [可编辑 SVG](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.zh-CN.svg)
|
|
12
12
|
|
|
13
13
|
## 安装与最小接入
|
|
14
14
|
|
|
@@ -32,21 +32,22 @@ Next.js App Router 的接入组件需声明 `"use client"`;样式在应用允
|
|
|
32
32
|
|
|
33
33
|
## 本地启动
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
推荐 Node.js 24 LTS(`.nvmrc`;完整测试最低需要 22.18)和 pnpm 10.16.1(`packageManager`):
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
pnpm install --frozen-lockfile
|
|
39
39
|
pnpm dev
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
打开 [http://localhost:5173](http://localhost:5173),已有服务时优先复用。构建与校验命令见 [API 文档的开发说明](
|
|
42
|
+
打开 [http://localhost:5173](http://localhost:5173),已有服务时优先复用。构建与校验命令见 [API 文档的开发说明](https://github.com/standhigher/react-email-editor/blob/main/docs/api.zh-CN.md#开发与本地联调)。
|
|
43
43
|
|
|
44
44
|
## 文档
|
|
45
45
|
|
|
46
|
-
- [API 参考](
|
|
47
|
-
- [基础场景](
|
|
48
|
-
- [复杂场景](
|
|
49
|
-
- [自定义区块指南](
|
|
50
|
-
- [
|
|
46
|
+
- [API 参考](https://github.com/standhigher/react-email-editor/blob/main/docs/api.zh-CN.md):所有公共导出、参数、默认值与类型。
|
|
47
|
+
- [基础场景](https://github.com/standhigher/react-email-editor/blob/main/docs/basic-examples.zh-CN.md):初始内容、保存恢复、主体样式、变量及预览。
|
|
48
|
+
- [复杂场景](https://github.com/standhigher/react-email-editor/blob/main/docs/advanced-examples.zh-CN.md):校验后保存、多实例、自定义区块、多区域编辑、资源适配与独立导出。
|
|
49
|
+
- [自定义区块指南](https://github.com/standhigher/react-email-editor/blob/main/docs/custom-blocks.zh-CN.md):控件、插件和渲染工具的完整说明。
|
|
50
|
+
- [发布指南](https://github.com/standhigher/react-email-editor/blob/main/docs/release.zh-CN.md):发布包校验与本地手动发布。
|
|
51
|
+
- [更新记录](https://github.com/standhigher/react-email-editor/blob/main/docs/changelog.zh-CN.md)。
|
|
51
52
|
|
|
52
53
|
文档格式仍处于开发阶段,当前只支持 `version: 4`,不自动迁移旧版文档。
|
package/dist/index.js
CHANGED
|
@@ -3840,7 +3840,7 @@ function buildRenderContext(node, mode, body = DEFAULT_EMAIL_BODY, definition =
|
|
|
3840
3840
|
}
|
|
3841
3841
|
|
|
3842
3842
|
// src/inspector/InspectorControls.tsx
|
|
3843
|
-
import { useId as
|
|
3843
|
+
import { useId as useId11 } from "react";
|
|
3844
3844
|
|
|
3845
3845
|
// src/constants/style-groups.ts
|
|
3846
3846
|
var STYLE_GROUPS = [
|
|
@@ -3885,7 +3885,7 @@ function inheritedStyleValue(id, body) {
|
|
|
3885
3885
|
}
|
|
3886
3886
|
|
|
3887
3887
|
// src/inspector/components/BasicControl.tsx
|
|
3888
|
-
import { useRef as
|
|
3888
|
+
import { useRef as useRef10, useId as useId7 } from "react";
|
|
3889
3889
|
|
|
3890
3890
|
// src/inspector/HtmlCodeEditor.tsx
|
|
3891
3891
|
import { useEffect as useEffect4, useLayoutEffect as useLayoutEffect6, useRef as useRef8 } from "react";
|
|
@@ -4079,41 +4079,147 @@ function HtmlCodeEditor({
|
|
|
4079
4079
|
);
|
|
4080
4080
|
}
|
|
4081
4081
|
|
|
4082
|
-
// src/
|
|
4083
|
-
import { useState as
|
|
4082
|
+
// src/components/PersonalizationMenu.tsx
|
|
4083
|
+
import { useId as useId6, useRef as useRef9, useState as useState13 } from "react";
|
|
4084
|
+
import { createPortal as createPortal2 } from "react-dom";
|
|
4085
|
+
|
|
4086
|
+
// src/components/usePersonalizationMenu.ts
|
|
4087
|
+
import { useLayoutEffect as useLayoutEffect7, useState as useState12 } from "react";
|
|
4088
|
+
function usePersonalizationMenu(open, rootRef, menuRef, focusOnOpen, setOpen) {
|
|
4089
|
+
const [position, setPosition] = useState12({ left: 0, top: 0, maxHeight: 240 });
|
|
4090
|
+
useLayoutEffect7(() => {
|
|
4091
|
+
var _a;
|
|
4092
|
+
const root = rootRef.current;
|
|
4093
|
+
const menu = menuRef.current;
|
|
4094
|
+
if (!open || !root || !menu) return;
|
|
4095
|
+
const ownerDocument = root.ownerDocument;
|
|
4096
|
+
const ownerWindow = ownerDocument.defaultView;
|
|
4097
|
+
if (!ownerWindow) return;
|
|
4098
|
+
const place = () => {
|
|
4099
|
+
var _a2, _b;
|
|
4100
|
+
const anchor = root.getBoundingClientRect();
|
|
4101
|
+
const inputBounds = (_b = (_a2 = root.closest(".ree-input-with-variable")) == null ? void 0 : _a2.getBoundingClientRect()) != null ? _b : anchor;
|
|
4102
|
+
const width = menu.offsetWidth;
|
|
4103
|
+
const height = Math.min(menu.scrollHeight + 2, 240);
|
|
4104
|
+
const below = Math.max(0, ownerWindow.innerHeight - inputBounds.bottom - 8);
|
|
4105
|
+
const above = Math.max(0, inputBounds.top - 8);
|
|
4106
|
+
const up = below < height && above > below;
|
|
4107
|
+
const maxHeight = Math.min(240, up ? above : below);
|
|
4108
|
+
const left = anchor.left + width > ownerWindow.innerWidth - 8 ? anchor.right - width : anchor.left;
|
|
4109
|
+
setPosition({
|
|
4110
|
+
left: Math.max(8, Math.min(left, ownerWindow.innerWidth - width - 8)),
|
|
4111
|
+
top: up ? inputBounds.top - Math.min(height, maxHeight) : inputBounds.bottom,
|
|
4112
|
+
maxHeight
|
|
4113
|
+
});
|
|
4114
|
+
};
|
|
4115
|
+
const onPointerDown = (event) => {
|
|
4116
|
+
if (!root.contains(event.target) && !menu.contains(event.target)) setOpen(false);
|
|
4117
|
+
};
|
|
4118
|
+
const onKeyDown = (event) => {
|
|
4119
|
+
var _a2, _b;
|
|
4120
|
+
const target = event.target;
|
|
4121
|
+
if (!root.contains(target) && !menu.contains(target)) return;
|
|
4122
|
+
if (event.key === "Tab" && menu.contains(target)) {
|
|
4123
|
+
const buttons = menu.querySelectorAll("button");
|
|
4124
|
+
if (target !== (event.shiftKey ? buttons[0] : buttons[buttons.length - 1])) return;
|
|
4125
|
+
if (event.shiftKey) event.preventDefault();
|
|
4126
|
+
setOpen(false);
|
|
4127
|
+
(_a2 = root.querySelector("button")) == null ? void 0 : _a2.focus({ preventScroll: true });
|
|
4128
|
+
return;
|
|
4129
|
+
}
|
|
4130
|
+
if (event.key !== "Escape") return;
|
|
4131
|
+
event.preventDefault();
|
|
4132
|
+
event.stopPropagation();
|
|
4133
|
+
setOpen(false);
|
|
4134
|
+
(_b = root.querySelector("button")) == null ? void 0 : _b.focus({ preventScroll: true });
|
|
4135
|
+
};
|
|
4136
|
+
menu.setAttribute("popover", "manual");
|
|
4137
|
+
menu.showPopover();
|
|
4138
|
+
place();
|
|
4139
|
+
if (focusOnOpen.current) (_a = menu.querySelector("button")) == null ? void 0 : _a.focus({ preventScroll: true });
|
|
4140
|
+
const observer = new ResizeObserver(place);
|
|
4141
|
+
observer.observe(root);
|
|
4142
|
+
observer.observe(menu);
|
|
4143
|
+
ownerWindow.addEventListener("resize", place);
|
|
4144
|
+
ownerDocument.addEventListener("scroll", place, true);
|
|
4145
|
+
ownerDocument.addEventListener("pointerdown", onPointerDown, true);
|
|
4146
|
+
ownerDocument.addEventListener("keydown", onKeyDown, true);
|
|
4147
|
+
return () => {
|
|
4148
|
+
menu.hidePopover();
|
|
4149
|
+
observer.disconnect();
|
|
4150
|
+
ownerWindow.removeEventListener("resize", place);
|
|
4151
|
+
ownerDocument.removeEventListener("scroll", place, true);
|
|
4152
|
+
ownerDocument.removeEventListener("pointerdown", onPointerDown, true);
|
|
4153
|
+
ownerDocument.removeEventListener("keydown", onKeyDown, true);
|
|
4154
|
+
};
|
|
4155
|
+
}, [open, rootRef, menuRef, focusOnOpen, setOpen]);
|
|
4156
|
+
return position;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
// src/components/PersonalizationMenu.tsx
|
|
4084
4160
|
import { jsx as jsx40, jsxs as jsxs29 } from "react/jsx-runtime";
|
|
4085
|
-
function
|
|
4086
|
-
|
|
4161
|
+
function PersonalizationMenu({
|
|
4162
|
+
variables,
|
|
4163
|
+
onInsert,
|
|
4164
|
+
suffix = false
|
|
4165
|
+
}) {
|
|
4166
|
+
const [open, setOpen] = useState13(false);
|
|
4167
|
+
const [host, setHost] = useState13(null);
|
|
4168
|
+
const id = useId6();
|
|
4169
|
+
const rootRef = useRef9(null);
|
|
4170
|
+
const menuRef = useRef9(null);
|
|
4171
|
+
const focusOnOpen = useRef9(false);
|
|
4172
|
+
const position = usePersonalizationMenu(open && variables.length > 0, rootRef, menuRef, focusOnOpen, setOpen);
|
|
4087
4173
|
if (variables.length === 0) return null;
|
|
4088
|
-
return /* @__PURE__ */ jsxs29("div", { className: "ree-
|
|
4089
|
-
/* @__PURE__ */
|
|
4174
|
+
return /* @__PURE__ */ jsxs29("div", { className: suffix ? "ree-personalization ree-personalization-suffix" : "ree-personalization", ref: rootRef, children: [
|
|
4175
|
+
/* @__PURE__ */ jsxs29(
|
|
4090
4176
|
"button",
|
|
4091
4177
|
{
|
|
4092
4178
|
type: "button",
|
|
4093
|
-
className: "ree-
|
|
4094
|
-
|
|
4095
|
-
title: "
|
|
4096
|
-
"aria-label": "Insert variable",
|
|
4179
|
+
className: suffix ? "ree-personalization-suffix-btn" : "ree-personalization-btn",
|
|
4180
|
+
"aria-label": suffix ? "Insert variable" : void 0,
|
|
4181
|
+
title: suffix ? "Personalization" : void 0,
|
|
4097
4182
|
"aria-expanded": open,
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4183
|
+
"aria-controls": open ? id : void 0,
|
|
4184
|
+
onClick: (event) => {
|
|
4185
|
+
var _a;
|
|
4186
|
+
focusOnOpen.current = event.detail === 0;
|
|
4187
|
+
const root = rootRef.current;
|
|
4188
|
+
if (root) setHost((_a = root.closest(".ree-root")) != null ? _a : root.ownerDocument.body);
|
|
4189
|
+
setOpen((current) => !current);
|
|
4190
|
+
},
|
|
4191
|
+
onKeyDown: (event) => {
|
|
4192
|
+
var _a, _b;
|
|
4193
|
+
if (event.key === "Tab" && open && !event.shiftKey) {
|
|
4194
|
+
event.preventDefault();
|
|
4195
|
+
(_b = (_a = menuRef.current) == null ? void 0 : _a.querySelector("button")) == null ? void 0 : _b.focus();
|
|
4196
|
+
}
|
|
4109
4197
|
},
|
|
4110
4198
|
children: [
|
|
4111
|
-
|
|
4112
|
-
|
|
4199
|
+
suffix ? "{ }" : "Personalization",
|
|
4200
|
+
suffix ? null : /* @__PURE__ */ jsx40("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx40("path", { d: "M5.2 7.5 10 12.3l4.8-4.8 1.2 1.2L10 14.7 4 8.7l1.2-1.2Z" }) })
|
|
4113
4201
|
]
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
|
|
4202
|
+
}
|
|
4203
|
+
),
|
|
4204
|
+
open && host ? createPortal2(
|
|
4205
|
+
/* @__PURE__ */ jsx40("div", { id, ref: menuRef, className: "ree-var-menu ree-personalization-menu", style: position, children: variables.map((variable) => /* @__PURE__ */ jsxs29(
|
|
4206
|
+
"button",
|
|
4207
|
+
{
|
|
4208
|
+
type: "button",
|
|
4209
|
+
className: "ree-var-item",
|
|
4210
|
+
onClick: () => {
|
|
4211
|
+
onInsert(variable.name);
|
|
4212
|
+
setOpen(false);
|
|
4213
|
+
},
|
|
4214
|
+
children: [
|
|
4215
|
+
/* @__PURE__ */ jsx40("span", { className: "ree-var-name", children: variable.name }),
|
|
4216
|
+
variable.description ? /* @__PURE__ */ jsx40("span", { className: "ree-var-label", children: variable.description }) : null
|
|
4217
|
+
]
|
|
4218
|
+
},
|
|
4219
|
+
variable.name
|
|
4220
|
+
)) }),
|
|
4221
|
+
host
|
|
4222
|
+
) : null
|
|
4117
4223
|
] });
|
|
4118
4224
|
}
|
|
4119
4225
|
|
|
@@ -4121,8 +4227,8 @@ function VariablesDropdown({ variables, onInsert }) {
|
|
|
4121
4227
|
import { jsx as jsx41, jsxs as jsxs30 } from "react/jsx-runtime";
|
|
4122
4228
|
function BasicControl({ control, value, variables, onChange }) {
|
|
4123
4229
|
var _a, _b, _c, _d, _e;
|
|
4124
|
-
const ref =
|
|
4125
|
-
const id =
|
|
4230
|
+
const ref = useRef10(null);
|
|
4231
|
+
const id = useId7();
|
|
4126
4232
|
const shown = value == null || typeof value === "number" && !Number.isFinite(value) ? "" : String(value);
|
|
4127
4233
|
const insertVariable = (token) => {
|
|
4128
4234
|
var _a2, _b2;
|
|
@@ -4130,6 +4236,11 @@ function BasicControl({ control, value, variables, onChange }) {
|
|
|
4130
4236
|
const start = (_a2 = element == null ? void 0 : element.selectionStart) != null ? _a2 : shown.length;
|
|
4131
4237
|
const end = (_b2 = element == null ? void 0 : element.selectionEnd) != null ? _b2 : shown.length;
|
|
4132
4238
|
onChange(shown.slice(0, start) + token + shown.slice(end));
|
|
4239
|
+
requestAnimationFrame(() => {
|
|
4240
|
+
var _a3, _b3;
|
|
4241
|
+
(_a3 = ref.current) == null ? void 0 : _a3.focus();
|
|
4242
|
+
(_b3 = ref.current) == null ? void 0 : _b3.setSelectionRange(start + token.length, start + token.length);
|
|
4243
|
+
});
|
|
4133
4244
|
};
|
|
4134
4245
|
if (control.kind === "boolean") return /* @__PURE__ */ jsx41(CheckboxControl, { label: control.label, checked: Boolean(value), onChange });
|
|
4135
4246
|
if (control.kind === "color") return /* @__PURE__ */ jsx41(ColorControl, { label: control.label, value: shown, resetTo: String((_a = control.defaultValue) != null ? _a : ""), onChange });
|
|
@@ -4138,30 +4249,32 @@ function BasicControl({ control, value, variables, onChange }) {
|
|
|
4138
4249
|
/* @__PURE__ */ jsx41(HtmlCodeEditor, { value: shown, placeholder: control.placeholder, rows: control.rows, onChange })
|
|
4139
4250
|
] });
|
|
4140
4251
|
const textual = control.kind === "text" || control.kind === "textarea" || control.kind === "url";
|
|
4252
|
+
const hasVariables = textual && variables.length > 0;
|
|
4141
4253
|
const invalidUrl = control.kind === "url" && shown.trim() && !safeEmailUrl(shown);
|
|
4254
|
+
const input = control.kind === "select" ? /* @__PURE__ */ jsx41(SelectControl, { id, label: control.label, value: shown, options: (_b = control.options) != null ? _b : [], emptyLabel: (_c = control.placeholder) != null ? _c : "None", onChange }) : control.kind === "textarea" ? /* @__PURE__ */ jsx41("textarea", { id, ref: (element) => {
|
|
4255
|
+
ref.current = element;
|
|
4256
|
+
}, className: "ree-input ree-textarea", value: shown, placeholder: control.placeholder, rows: (_d = control.rows) != null ? _d : 4, onChange: (event) => onChange(event.target.value) }) : /* @__PURE__ */ jsx41(TextInput, { id, ref: (element) => {
|
|
4257
|
+
ref.current = element;
|
|
4258
|
+
}, type: control.kind === "number" ? "number" : "text", value: shown, min: control.min, max: control.max, step: control.step, placeholder: control.placeholder, "aria-invalid": Boolean(invalidUrl) || Boolean((_e = control.validateValue) == null ? void 0 : _e.call(control, value)), onChange: (event) => onChange(control.kind === "number" && event.target.value !== "" ? Number(event.target.value) : event.target.value) });
|
|
4142
4259
|
return /* @__PURE__ */ jsxs30("div", { className: "ree-field", children: [
|
|
4143
|
-
/* @__PURE__ */
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
ref.current = element;
|
|
4152
|
-
}, className: "ree-input ree-textarea", value: shown, placeholder: control.placeholder, rows: (_d = control.rows) != null ? _d : 4, onChange: (event) => onChange(event.target.value) }) : /* @__PURE__ */ jsx41(TextInput, { id, ref: (element) => {
|
|
4153
|
-
ref.current = element;
|
|
4154
|
-
}, type: control.kind === "number" ? "number" : "text", value: shown, min: control.min, max: control.max, step: control.step, placeholder: control.placeholder, "aria-invalid": Boolean(invalidUrl) || Boolean((_e = control.validateValue) == null ? void 0 : _e.call(control, value)), onChange: (event) => onChange(control.kind === "number" && event.target.value !== "" ? Number(event.target.value) : event.target.value) })
|
|
4260
|
+
/* @__PURE__ */ jsx41("div", { className: "ree-field-label", children: /* @__PURE__ */ jsxs30("label", { htmlFor: id, children: [
|
|
4261
|
+
control.label,
|
|
4262
|
+
control.unit ? ` (${control.unit})` : ""
|
|
4263
|
+
] }) }),
|
|
4264
|
+
hasVariables ? /* @__PURE__ */ jsxs30("div", { className: "ree-input-with-variable", children: [
|
|
4265
|
+
input,
|
|
4266
|
+
/* @__PURE__ */ jsx41(PersonalizationMenu, { suffix: true, variables, onInsert: insertVariable })
|
|
4267
|
+
] }) : input
|
|
4155
4268
|
] });
|
|
4156
4269
|
}
|
|
4157
4270
|
|
|
4158
4271
|
// src/inspector/components/ListControl.tsx
|
|
4159
|
-
import { useId as
|
|
4272
|
+
import { useId as useId8 } from "react";
|
|
4160
4273
|
|
|
4161
4274
|
// src/inspector/components/useListRows.ts
|
|
4162
|
-
import { useLayoutEffect as
|
|
4275
|
+
import { useLayoutEffect as useLayoutEffect8, useRef as useRef11, useState as useState14 } from "react";
|
|
4163
4276
|
function useListRows(value, onChange) {
|
|
4164
|
-
const [state, setState] =
|
|
4277
|
+
const [state, setState] = useState14({ source: void 0, rows: [], nextKey: 0 });
|
|
4165
4278
|
if (state.source !== value) {
|
|
4166
4279
|
const available = [...state.rows];
|
|
4167
4280
|
let nextKey = state.nextKey;
|
|
@@ -4173,8 +4286,8 @@ function useListRows(value, onChange) {
|
|
|
4173
4286
|
});
|
|
4174
4287
|
setState({ source: value, rows, nextKey });
|
|
4175
4288
|
}
|
|
4176
|
-
const latest =
|
|
4177
|
-
|
|
4289
|
+
const latest = useRef11(state);
|
|
4290
|
+
useLayoutEffect8(() => {
|
|
4178
4291
|
latest.current = state;
|
|
4179
4292
|
return () => {
|
|
4180
4293
|
latest.current = null;
|
|
@@ -4219,7 +4332,7 @@ import { jsx as jsx42, jsxs as jsxs31 } from "react/jsx-runtime";
|
|
|
4219
4332
|
function ListControl(_a) {
|
|
4220
4333
|
var _b = _a, { control, value, onChange } = _b, props = __objRest(_b, ["control", "value", "onChange"]);
|
|
4221
4334
|
var _a2;
|
|
4222
|
-
const id =
|
|
4335
|
+
const id = useId8();
|
|
4223
4336
|
const schema = (_a2 = control.itemControls) != null ? _a2 : {};
|
|
4224
4337
|
const { rows, updateRow, moveRow, removeRow, addRow } = useListRows(value, onChange);
|
|
4225
4338
|
return /* @__PURE__ */ jsxs31("fieldset", { className: "ree-plugin-list", children: [
|
|
@@ -4239,15 +4352,15 @@ function ListControl(_a) {
|
|
|
4239
4352
|
}
|
|
4240
4353
|
|
|
4241
4354
|
// src/inspector/components/ImageControl.tsx
|
|
4242
|
-
import { useContext as useContext4, useEffect as useEffect5, useId as
|
|
4355
|
+
import { useContext as useContext4, useEffect as useEffect5, useId as useId9, useRef as useRef12, useState as useState15 } from "react";
|
|
4243
4356
|
import { jsx as jsx43, jsxs as jsxs32 } from "react/jsx-runtime";
|
|
4244
4357
|
function ImageControl({ control, value, onChange }) {
|
|
4245
4358
|
var _a;
|
|
4246
4359
|
const config = useContext4(EditorConfigContext);
|
|
4247
|
-
const controller =
|
|
4248
|
-
const id =
|
|
4249
|
-
const [loading, setLoading] =
|
|
4250
|
-
const [error, setError] =
|
|
4360
|
+
const controller = useRef12(null);
|
|
4361
|
+
const id = useId9();
|
|
4362
|
+
const [loading, setLoading] = useState15(false);
|
|
4363
|
+
const [error, setError] = useState15("");
|
|
4251
4364
|
const shown = String(value != null ? value : "");
|
|
4252
4365
|
const url = safeEmailUrl(shown, true);
|
|
4253
4366
|
useEffect5(() => () => {
|
|
@@ -4284,12 +4397,12 @@ function ImageControl({ control, value, onChange }) {
|
|
|
4284
4397
|
}
|
|
4285
4398
|
|
|
4286
4399
|
// src/inspector/components/RichTextControl.tsx
|
|
4287
|
-
import { useContext as useContext5, useId as
|
|
4400
|
+
import { useContext as useContext5, useId as useId10 } from "react";
|
|
4288
4401
|
import { jsx as jsx44, jsxs as jsxs33 } from "react/jsx-runtime";
|
|
4289
4402
|
function RichTextControl({ control, value, variables, body, onChange }) {
|
|
4290
4403
|
var _a, _b;
|
|
4291
4404
|
const parent = useContext5(PluginEditorContext);
|
|
4292
|
-
const id =
|
|
4405
|
+
const id = useId10();
|
|
4293
4406
|
const context = {
|
|
4294
4407
|
block: (_a = parent == null ? void 0 : parent.block) != null ? _a : { id, type: "control", controls: {} },
|
|
4295
4408
|
controls: { [control.name]: value },
|
|
@@ -4347,7 +4460,7 @@ function InspectorControls({
|
|
|
4347
4460
|
onCommitStyle
|
|
4348
4461
|
}) {
|
|
4349
4462
|
var _a, _b, _c, _d, _e;
|
|
4350
|
-
const toolbarId =
|
|
4463
|
+
const toolbarId = useId11();
|
|
4351
4464
|
const contentControls = ((_a = definition.controls) != null ? _a : []).filter((control) => control.name !== definition.inlineControl);
|
|
4352
4465
|
const CustomInspector = definition.inspector;
|
|
4353
4466
|
const context = buildRenderContext(block, "canvas", body, definition);
|
|
@@ -4499,7 +4612,7 @@ function viewportWidth(device, body) {
|
|
|
4499
4612
|
}
|
|
4500
4613
|
|
|
4501
4614
|
// src/stage/canvas/CanvasBlock.tsx
|
|
4502
|
-
import { useId as
|
|
4615
|
+
import { useId as useId13, useMemo as useMemo3, useState as useState16 } from "react";
|
|
4503
4616
|
import { useSortable } from "@dnd-kit/sortable";
|
|
4504
4617
|
import { CSS } from "@dnd-kit/utilities";
|
|
4505
4618
|
|
|
@@ -4753,7 +4866,7 @@ function blockToHtml(node, definitions = DEFAULT_BLOCKS, mode = "email", options
|
|
|
4753
4866
|
}
|
|
4754
4867
|
|
|
4755
4868
|
// src/stage/canvas/components/HtmlCanvas.tsx
|
|
4756
|
-
import { useCallback, useEffect as useEffect6, useRef as
|
|
4869
|
+
import { useCallback, useEffect as useEffect6, useRef as useRef13 } from "react";
|
|
4757
4870
|
|
|
4758
4871
|
// src/render/head-styles.ts
|
|
4759
4872
|
function extractHeadStyles(html) {
|
|
@@ -4774,9 +4887,9 @@ function frameHtml(html, baseCss) {
|
|
|
4774
4887
|
}
|
|
4775
4888
|
function HtmlCanvas({ html, baseCss = "", onSelect }) {
|
|
4776
4889
|
const hasHtmlTag = /<\/?[a-z][\w:-]*(?:\s[^>]*)?>/i.test(html);
|
|
4777
|
-
const containerRef =
|
|
4778
|
-
const frameRef =
|
|
4779
|
-
const cleanupRef =
|
|
4890
|
+
const containerRef = useRef13(null);
|
|
4891
|
+
const frameRef = useRef13(null);
|
|
4892
|
+
const cleanupRef = useRef13();
|
|
4780
4893
|
const observeContent = useCallback(() => {
|
|
4781
4894
|
var _a;
|
|
4782
4895
|
(_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
|
|
@@ -4897,7 +5010,7 @@ function PluginCanvasContent({ block, body, definition, variables, selected, too
|
|
|
4897
5010
|
}
|
|
4898
5011
|
|
|
4899
5012
|
// src/stage/canvas/components/BlockActions.tsx
|
|
4900
|
-
import { useId as
|
|
5013
|
+
import { useId as useId12 } from "react";
|
|
4901
5014
|
import { jsx as jsx50, jsxs as jsxs39 } from "react/jsx-runtime";
|
|
4902
5015
|
function BlockActions({
|
|
4903
5016
|
inlineEdit,
|
|
@@ -4907,7 +5020,7 @@ function BlockActions({
|
|
|
4907
5020
|
onDuplicate,
|
|
4908
5021
|
onRemove
|
|
4909
5022
|
}) {
|
|
4910
|
-
const tooltipId =
|
|
5023
|
+
const tooltipId = useId12();
|
|
4911
5024
|
const toggleLabel = toolbarCollapsed ? "Expand Toolbar" : "Collapse Toolbar";
|
|
4912
5025
|
return /* @__PURE__ */ jsxs39(
|
|
4913
5026
|
"div",
|
|
@@ -5003,8 +5116,8 @@ function CanvasBlock({
|
|
|
5003
5116
|
}) {
|
|
5004
5117
|
var _a, _b, _c;
|
|
5005
5118
|
const block = useMemo3(() => __spreadProps(__spreadValues({}, sourceBlock), { style: resolveBlockStyle(sourceBlock.style, definition) }), [sourceBlock, definition]);
|
|
5006
|
-
const toolbarId =
|
|
5007
|
-
const [toolbarCollapsed, setToolbarCollapsed] =
|
|
5119
|
+
const toolbarId = useId13();
|
|
5120
|
+
const [toolbarCollapsed, setToolbarCollapsed] = useState16(false);
|
|
5008
5121
|
const dragData = { kind: "canvas" };
|
|
5009
5122
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData });
|
|
5010
5123
|
const select = (event) => {
|
|
@@ -5171,7 +5284,7 @@ function Canvas({
|
|
|
5171
5284
|
}
|
|
5172
5285
|
|
|
5173
5286
|
// src/stage/preview/Preview.tsx
|
|
5174
|
-
import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as
|
|
5287
|
+
import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef14 } from "react";
|
|
5175
5288
|
|
|
5176
5289
|
// src/render/document-html.ts
|
|
5177
5290
|
function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
|
|
@@ -5225,8 +5338,8 @@ function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
|
|
|
5225
5338
|
// src/stage/preview/Preview.tsx
|
|
5226
5339
|
import { jsx as jsx53 } from "react/jsx-runtime";
|
|
5227
5340
|
function Preview({ document, definitions, variables, device }) {
|
|
5228
|
-
const frameRef =
|
|
5229
|
-
const cleanupRef =
|
|
5341
|
+
const frameRef = useRef14(null);
|
|
5342
|
+
const cleanupRef = useRef14();
|
|
5230
5343
|
const html = useMemo4(
|
|
5231
5344
|
() => documentToHtml(document, definitions, {
|
|
5232
5345
|
resolveHtmlTemplates: true,
|
|
@@ -5420,75 +5533,22 @@ function Stage({
|
|
|
5420
5533
|
}
|
|
5421
5534
|
|
|
5422
5535
|
// src/shell/SubjectBar.tsx
|
|
5423
|
-
import { useId as
|
|
5536
|
+
import { useId as useId14, useRef as useRef15 } from "react";
|
|
5424
5537
|
|
|
5425
5538
|
// src/shell/constants.ts
|
|
5426
5539
|
var EMPTY_VARIABLES = [];
|
|
5427
5540
|
var PERSONALIZATION_HINT = "Personalization fields will appear blank if no data is available.";
|
|
5428
5541
|
|
|
5429
|
-
// src/shell/PersonalizationMenu.tsx
|
|
5430
|
-
import { useEffect as useEffect8, useRef as useRef14, useState as useState16 } from "react";
|
|
5431
|
-
import { jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
|
|
5432
|
-
function PersonalizationMenu({
|
|
5433
|
-
variables,
|
|
5434
|
-
onInsert
|
|
5435
|
-
}) {
|
|
5436
|
-
const [open, setOpen] = useState16(false);
|
|
5437
|
-
const rootRef = useRef14(null);
|
|
5438
|
-
useEffect8(() => {
|
|
5439
|
-
const root = rootRef.current;
|
|
5440
|
-
if (!open || !root) return;
|
|
5441
|
-
const ownerDocument = root.ownerDocument;
|
|
5442
|
-
const onPointerDown = (event) => {
|
|
5443
|
-
if (!root.contains(event.target)) setOpen(false);
|
|
5444
|
-
};
|
|
5445
|
-
ownerDocument.addEventListener("pointerdown", onPointerDown, true);
|
|
5446
|
-
return () => ownerDocument.removeEventListener("pointerdown", onPointerDown, true);
|
|
5447
|
-
}, [open]);
|
|
5448
|
-
if (variables.length === 0) return null;
|
|
5449
|
-
return /* @__PURE__ */ jsxs43("div", { className: "ree-personalization", ref: rootRef, children: [
|
|
5450
|
-
/* @__PURE__ */ jsxs43(
|
|
5451
|
-
"button",
|
|
5452
|
-
{
|
|
5453
|
-
type: "button",
|
|
5454
|
-
className: "ree-personalization-btn",
|
|
5455
|
-
"aria-expanded": open,
|
|
5456
|
-
onClick: () => setOpen((current) => !current),
|
|
5457
|
-
children: [
|
|
5458
|
-
"Personalization",
|
|
5459
|
-
/* @__PURE__ */ jsx56("svg", { viewBox: "0 0 20 20", width: "16", height: "16", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx56("path", { d: "M5.2 7.5 10 12.3l4.8-4.8 1.2 1.2L10 14.7 4 8.7l1.2-1.2Z" }) })
|
|
5460
|
-
]
|
|
5461
|
-
}
|
|
5462
|
-
),
|
|
5463
|
-
open ? /* @__PURE__ */ jsx56("div", { className: "ree-var-menu ree-personalization-menu", children: variables.map((variable) => /* @__PURE__ */ jsxs43(
|
|
5464
|
-
"button",
|
|
5465
|
-
{
|
|
5466
|
-
type: "button",
|
|
5467
|
-
className: "ree-var-item",
|
|
5468
|
-
onClick: () => {
|
|
5469
|
-
onInsert(variable.name);
|
|
5470
|
-
setOpen(false);
|
|
5471
|
-
},
|
|
5472
|
-
children: [
|
|
5473
|
-
/* @__PURE__ */ jsx56("span", { className: "ree-var-name", children: variable.name }),
|
|
5474
|
-
variable.description ? /* @__PURE__ */ jsx56("span", { className: "ree-var-label", children: variable.description }) : null
|
|
5475
|
-
]
|
|
5476
|
-
},
|
|
5477
|
-
variable.name
|
|
5478
|
-
)) }) : null
|
|
5479
|
-
] });
|
|
5480
|
-
}
|
|
5481
|
-
|
|
5482
5542
|
// src/shell/SubjectBar.tsx
|
|
5483
|
-
import { Fragment as Fragment5, jsx as
|
|
5543
|
+
import { Fragment as Fragment5, jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
|
|
5484
5544
|
function SubjectBar({
|
|
5485
5545
|
subject,
|
|
5486
5546
|
variables,
|
|
5487
5547
|
onSubjectChange
|
|
5488
5548
|
}) {
|
|
5489
5549
|
const inputRef = useRef15(null);
|
|
5490
|
-
const subjectId =
|
|
5491
|
-
const countId =
|
|
5550
|
+
const subjectId = useId14();
|
|
5551
|
+
const countId = useId14();
|
|
5492
5552
|
const insertVariable = (token) => {
|
|
5493
5553
|
var _a, _b;
|
|
5494
5554
|
const input = inputRef.current;
|
|
@@ -5506,10 +5566,10 @@ function SubjectBar({
|
|
|
5506
5566
|
(_b2 = inputRef.current) == null ? void 0 : _b2.setSelectionRange(cursor, cursor);
|
|
5507
5567
|
});
|
|
5508
5568
|
};
|
|
5509
|
-
return /* @__PURE__ */
|
|
5510
|
-
/* @__PURE__ */
|
|
5511
|
-
/* @__PURE__ */
|
|
5512
|
-
/* @__PURE__ */
|
|
5569
|
+
return /* @__PURE__ */ jsxs43("div", { className: "ree-subject-bar", children: [
|
|
5570
|
+
/* @__PURE__ */ jsx56("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
|
|
5571
|
+
/* @__PURE__ */ jsxs43("div", { className: "ree-subject-field", children: [
|
|
5572
|
+
/* @__PURE__ */ jsx56(
|
|
5513
5573
|
TextInput,
|
|
5514
5574
|
{
|
|
5515
5575
|
id: subjectId,
|
|
@@ -5521,15 +5581,15 @@ function SubjectBar({
|
|
|
5521
5581
|
onChange: (event) => onSubjectChange(event.target.value)
|
|
5522
5582
|
}
|
|
5523
5583
|
),
|
|
5524
|
-
/* @__PURE__ */
|
|
5584
|
+
/* @__PURE__ */ jsxs43("span", { id: countId, className: "ree-subject-count", children: [
|
|
5525
5585
|
subject.length,
|
|
5526
5586
|
"/",
|
|
5527
5587
|
SUBJECT_MAX_LENGTH
|
|
5528
5588
|
] })
|
|
5529
5589
|
] }),
|
|
5530
|
-
variables.length > 0 ? /* @__PURE__ */
|
|
5531
|
-
/* @__PURE__ */
|
|
5532
|
-
/* @__PURE__ */
|
|
5590
|
+
variables.length > 0 ? /* @__PURE__ */ jsxs43(Fragment5, { children: [
|
|
5591
|
+
/* @__PURE__ */ jsx56(PersonalizationMenu, { variables, onInsert: insertVariable }),
|
|
5592
|
+
/* @__PURE__ */ jsx56("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
|
|
5533
5593
|
] }) : null
|
|
5534
5594
|
] });
|
|
5535
5595
|
}
|
|
@@ -5897,7 +5957,7 @@ function useEditorSession(editor) {
|
|
|
5897
5957
|
}
|
|
5898
5958
|
|
|
5899
5959
|
// src/shell/EmailEditorShell.tsx
|
|
5900
|
-
import { jsx as
|
|
5960
|
+
import { jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
|
|
5901
5961
|
function EmailEditorShell({ editor, height = "100%" }) {
|
|
5902
5962
|
var _a;
|
|
5903
5963
|
const session = useEditorSession(editor);
|
|
@@ -5935,8 +5995,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
5935
5995
|
selectBlock(null);
|
|
5936
5996
|
};
|
|
5937
5997
|
const showCanvas = () => setViewMode("edit");
|
|
5938
|
-
return /* @__PURE__ */
|
|
5939
|
-
/* @__PURE__ */
|
|
5998
|
+
return /* @__PURE__ */ jsx57(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx57(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs44("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
|
|
5999
|
+
/* @__PURE__ */ jsx57(
|
|
5940
6000
|
SubjectBar,
|
|
5941
6001
|
{
|
|
5942
6002
|
subject: (_a = session.document.subject) != null ? _a : "",
|
|
@@ -5944,8 +6004,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
5944
6004
|
onSubjectChange: session.setSubject
|
|
5945
6005
|
}
|
|
5946
6006
|
),
|
|
5947
|
-
/* @__PURE__ */
|
|
5948
|
-
/* @__PURE__ */
|
|
6007
|
+
/* @__PURE__ */ jsxs44("div", { className: "ree-body", children: [
|
|
6008
|
+
/* @__PURE__ */ jsxs44(
|
|
5949
6009
|
EditorDnd,
|
|
5950
6010
|
{
|
|
5951
6011
|
blocks: session.document.blocks,
|
|
@@ -5957,7 +6017,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
5957
6017
|
onReorder: session.reorderBlocks,
|
|
5958
6018
|
onLibraryDragStart: showCanvas,
|
|
5959
6019
|
children: [
|
|
5960
|
-
/* @__PURE__ */
|
|
6020
|
+
/* @__PURE__ */ jsx57(
|
|
5961
6021
|
BlockLibrary,
|
|
5962
6022
|
{
|
|
5963
6023
|
definitions: session.config.library,
|
|
@@ -5967,7 +6027,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
5967
6027
|
}
|
|
5968
6028
|
}
|
|
5969
6029
|
),
|
|
5970
|
-
/* @__PURE__ */
|
|
6030
|
+
/* @__PURE__ */ jsx57(
|
|
5971
6031
|
Stage,
|
|
5972
6032
|
{
|
|
5973
6033
|
session,
|
|
@@ -5981,7 +6041,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
5981
6041
|
]
|
|
5982
6042
|
}
|
|
5983
6043
|
),
|
|
5984
|
-
/* @__PURE__ */
|
|
6044
|
+
/* @__PURE__ */ jsx57(
|
|
5985
6045
|
Inspector,
|
|
5986
6046
|
{
|
|
5987
6047
|
panel: inspectorPanel,
|
|
@@ -6001,9 +6061,9 @@ function EmailEditorShell({ editor, height = "100%" }) {
|
|
|
6001
6061
|
}
|
|
6002
6062
|
|
|
6003
6063
|
// src/EmailEditor.tsx
|
|
6004
|
-
import { jsx as
|
|
6064
|
+
import { jsx as jsx58 } from "react/jsx-runtime";
|
|
6005
6065
|
function EmailEditor(props) {
|
|
6006
|
-
return /* @__PURE__ */
|
|
6066
|
+
return /* @__PURE__ */ jsx58(EmailEditorShell, __spreadValues({}, props));
|
|
6007
6067
|
}
|
|
6008
6068
|
|
|
6009
6069
|
// src/plugins/useEmailEditorConfig.ts
|
|
@@ -6014,8 +6074,8 @@ function useEmailEditorConfig(options = {}) {
|
|
|
6014
6074
|
}
|
|
6015
6075
|
|
|
6016
6076
|
// src/shell/useEmailEditor.ts
|
|
6017
|
-
import { useEffect as
|
|
6018
|
-
var useOptionsEffect = typeof window === "undefined" ?
|
|
6077
|
+
import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as useLayoutEffect9, useState as useState19 } from "react";
|
|
6078
|
+
var useOptionsEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect9;
|
|
6019
6079
|
function useEmailEditor(options = {}) {
|
|
6020
6080
|
const [store] = useState19(() => createEditorStore(options));
|
|
6021
6081
|
useInsertionEffect(() => {
|
|
@@ -6033,12 +6093,12 @@ function useEmailEditorState(editor) {
|
|
|
6033
6093
|
}
|
|
6034
6094
|
|
|
6035
6095
|
// src/plugins/components/ControlEditor.tsx
|
|
6036
|
-
import { jsx as
|
|
6096
|
+
import { jsx as jsx59 } from "react/jsx-runtime";
|
|
6037
6097
|
function ControlEditor({ control }) {
|
|
6038
6098
|
const context = usePluginEditor();
|
|
6039
6099
|
const definition = context.controlDefinitions.find((item) => item.name === control);
|
|
6040
6100
|
if (!definition) throw new Error(`Unknown plugin control: ${control}`);
|
|
6041
|
-
return /* @__PURE__ */
|
|
6101
|
+
return /* @__PURE__ */ jsx59(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
|
|
6042
6102
|
}
|
|
6043
6103
|
export {
|
|
6044
6104
|
BLOCK_TYPES,
|
package/dist/styles.css
CHANGED
|
@@ -693,11 +693,6 @@
|
|
|
693
693
|
background: var(--ree-panel-secondary);
|
|
694
694
|
}
|
|
695
695
|
|
|
696
|
-
.ree-personalization-menu {
|
|
697
|
-
left: 0;
|
|
698
|
-
min-width: 240px;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
696
|
.ree-subject-hint {
|
|
702
697
|
flex: 0 1 auto;
|
|
703
698
|
margin: 0;
|
|
@@ -1670,16 +1665,30 @@
|
|
|
1670
1665
|
|
|
1671
1666
|
/* ---------- 变量 ---------- */
|
|
1672
1667
|
|
|
1673
|
-
.ree-
|
|
1668
|
+
.ree-input-with-variable {
|
|
1674
1669
|
position: relative;
|
|
1675
1670
|
}
|
|
1676
1671
|
|
|
1677
|
-
.ree-
|
|
1672
|
+
.ree-input-with-variable > .ree-input {
|
|
1673
|
+
--ree-control-padding-end: 48px;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
.ree-input-with-variable > .ree-textarea {
|
|
1677
|
+
display: block;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
.ree-personalization-suffix {
|
|
1681
|
+
position: absolute;
|
|
1682
|
+
top: 6px;
|
|
1683
|
+
right: 6px;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.ree-personalization-suffix-btn {
|
|
1678
1687
|
appearance: none;
|
|
1679
1688
|
border: none;
|
|
1680
1689
|
background: transparent;
|
|
1681
1690
|
min-width: 30px;
|
|
1682
|
-
height:
|
|
1691
|
+
height: 26px;
|
|
1683
1692
|
padding: 0 6px;
|
|
1684
1693
|
border-radius: var(--ree-radius-sm);
|
|
1685
1694
|
cursor: pointer;
|
|
@@ -1687,10 +1696,16 @@
|
|
|
1687
1696
|
font-size: 12px;
|
|
1688
1697
|
}
|
|
1689
1698
|
|
|
1690
|
-
.ree-
|
|
1699
|
+
.ree-personalization-suffix-btn:hover {
|
|
1691
1700
|
background: var(--ree-panel-secondary);
|
|
1692
1701
|
}
|
|
1693
1702
|
|
|
1703
|
+
.ree-personalization-suffix-btn:focus-visible,
|
|
1704
|
+
.ree-personalization-btn:focus-visible {
|
|
1705
|
+
outline: 2px solid var(--ree-text);
|
|
1706
|
+
outline-offset: -2px;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1694
1709
|
.ree-var-menu {
|
|
1695
1710
|
position: absolute;
|
|
1696
1711
|
top: 100%;
|
|
@@ -1707,6 +1722,16 @@
|
|
|
1707
1722
|
color: var(--ree-text, #202223);
|
|
1708
1723
|
}
|
|
1709
1724
|
|
|
1725
|
+
.ree-var-menu.ree-personalization-menu {
|
|
1726
|
+
position: fixed;
|
|
1727
|
+
inset: auto;
|
|
1728
|
+
margin: 0;
|
|
1729
|
+
z-index: 1000;
|
|
1730
|
+
width: 240px;
|
|
1731
|
+
min-width: 0;
|
|
1732
|
+
max-width: calc(100vw - 16px);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1710
1735
|
.ree-var-item {
|
|
1711
1736
|
display: flex;
|
|
1712
1737
|
flex-direction: column;
|
|
@@ -1730,6 +1755,13 @@
|
|
|
1730
1755
|
color: var(--ree-primary);
|
|
1731
1756
|
}
|
|
1732
1757
|
|
|
1758
|
+
.ree-var-name,
|
|
1759
|
+
.ree-var-label {
|
|
1760
|
+
max-width: 100%;
|
|
1761
|
+
overflow-wrap: anywhere;
|
|
1762
|
+
white-space: pre-wrap;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1733
1765
|
.ree-var-label {
|
|
1734
1766
|
font-size: 12px;
|
|
1735
1767
|
color: var(--ree-muted);
|
|
@@ -1862,28 +1894,6 @@
|
|
|
1862
1894
|
margin-bottom: 14px;
|
|
1863
1895
|
}
|
|
1864
1896
|
|
|
1865
|
-
.ree-field-label:has(> .ree-var-wrap) {
|
|
1866
|
-
position: relative;
|
|
1867
|
-
justify-content: flex-start;
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
.ree-field-label > .ree-var-wrap {
|
|
1871
|
-
position: static;
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
.ree-field-label .ree-var-menu {
|
|
1875
|
-
left: 0;
|
|
1876
|
-
width: min(280px, 100%);
|
|
1877
|
-
min-width: 0;
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
.ree-field-label .ree-var-name,
|
|
1881
|
-
.ree-field-label .ree-var-label {
|
|
1882
|
-
max-width: 100%;
|
|
1883
|
-
overflow-wrap: anywhere;
|
|
1884
|
-
white-space: pre-wrap;
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
1897
|
/* ---------- 配置控件 ---------- */
|
|
1888
1898
|
|
|
1889
1899
|
.ree-textarea {
|
package/docs/api.md
CHANGED
|
@@ -270,6 +270,8 @@ pnpm build
|
|
|
270
270
|
node --test tests/*.test.mjs tests/*.test.ts
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
Use `pnpm check` to run all four checks in order, or `pnpm test` after building to run only the Node tests. Node 24 LTS and pnpm 10.16.1 are the repository defaults. See the [release guide](./release.md) for package verification and publishing.
|
|
274
|
+
|
|
273
275
|
Build before tests that depend on dist. Internal TypeScript tests require Node 22.18+. The package ships ESM, declarations and flattened CSS. React/React DOM are peer dependencies; TipTap, dnd-kit, CodeMirror and Lezer are dependencies, externalized during builds rather than bundled.
|
|
274
276
|
|
|
275
277
|
Local consumers can use link:../react-email-editor. Run pnpm build first and pnpm build:watch to update dist. Both projects must share one React instance: Vite can use resolve.dedupe: ["react", "react-dom"]; Next.js can use transpilePackages: ["@standhigher/react-email-editor"], with Turbopack root covering the linked directory.
|
package/docs/api.zh-CN.md
CHANGED
|
@@ -270,6 +270,8 @@ pnpm build
|
|
|
270
270
|
node --test tests/*.test.mjs tests/*.test.ts
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
+
`pnpm check` 按上述顺序执行全部检查;只运行 Node 测试时,构建后执行 `pnpm test`。本仓库默认使用 Node 24 LTS 和 pnpm 10.16.1。打包检查与发布流程见[发布指南](./release.zh-CN.md)。
|
|
274
|
+
|
|
273
275
|
先构建再运行依赖 dist 的测试;TypeScript 内部测试需要 Node 22.18+。包提供 ESM、类型与展开后的 CSS。React/React DOM 为 peer dependencies;TipTap、dnd-kit、CodeMirror、Lezer 等为 dependencies,构建时 external,不内联到发布包。
|
|
274
276
|
|
|
275
277
|
消费方可使用 `link:../react-email-editor`;先 `pnpm build`,再 `pnpm build:watch` 保持 dist 更新。两项目需共用一个 React 实例:Vite 可设置 `resolve.dedupe: ["react", "react-dom"]`;Next.js 可设置 `transpilePackages: ["@standhigher/react-email-editor"]`,Turbopack 的 root 需覆盖链接目录。
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
English | [简体中文](./changelog.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
## 0.4.3 — 2026-10-28
|
|
6
|
+
|
|
7
|
+
### Fixes and improvements
|
|
8
|
+
|
|
9
|
+
- Reuse the same Personalization menu in the subject header and configuration panel, with consistent variable colors, width, border radius, shadows and hover styles.
|
|
10
|
+
- Move variable triggers for text, multiline text and link controls to an input suffix on the right, reserving space for content; restore input focus and cursor position after inserting a variable.
|
|
11
|
+
- Open menus to the left or above according to available viewport space, using native popovers to avoid scroll clipping and positioning offsets in embedded containers with `transform`.
|
|
12
|
+
- Complete focus handling when opening from the keyboard, Tab/Shift+Tab exit, Escape dismissal and outside-click dismissal; preserve the selected block on close and clean up observers and event listeners.
|
|
13
|
+
- Keep the public API, email export and `version: 4` document format unchanged.
|
|
14
|
+
|
|
15
|
+
### Validation
|
|
16
|
+
|
|
17
|
+
- `pnpm check` passed, including type checking, Oxlint, build and all 103 Node tests; add regression coverage for menu positioning, focus, dismissal and lifecycle.
|
|
18
|
+
- Reuse the existing development server for browser interaction checks, covering suffix clicks, cursor insertion, keyboard navigation and positioning inside containers with `transform`.
|
|
19
|
+
|
|
20
|
+
## 0.4.2 — 2026-10-28
|
|
21
|
+
|
|
22
|
+
### npm publishing and documentation
|
|
23
|
+
|
|
24
|
+
- Add the MIT license, repository, homepage and issue tracker, pin the public npm registry and pnpm 10.16.1, and recommend Node 24 LTS through `.nvmrc`.
|
|
25
|
+
- Add `pnpm test`, `pnpm check` and complete validation before publishing; preserve CSS through `sideEffects` and verify that consumers can remove unused JavaScript imports.
|
|
26
|
+
- Fix broken npm README images by using fixed absolute CDN URLs for images and viewing links from the published 0.4.1 package; newer packages retain Markdown and SVG documentation without shipping PNG originals again.
|
|
27
|
+
- Use absolute GitHub URLs for language switches, API references, examples, release guides, changelogs and section navigation in both READMEs, and verify target documents and anchors; remote documents must be pushed with the commit.
|
|
28
|
+
- Add English and Chinese release guides, package-content checks and consumer CSS validation; keep the React 18 peer range, existing public API and document format unchanged.
|
|
29
|
+
|
|
30
|
+
## 0.4.1 — 2026-10-28
|
|
31
|
+
|
|
32
|
+
### Documentation and type hints
|
|
33
|
+
|
|
34
|
+
- Simplify both READMEs, add module architecture diagrams, and move detailed integration instructions into separate API reference, basic example and advanced example documents.
|
|
35
|
+
- Add examples for saving and restoring documents, body settings and variables, multiple-instance subscriptions, custom blocks, multiple rich-text regions, image resource pickers and individual block template previews; update navigation in the custom block guides.
|
|
36
|
+
- Add English JSDoc for editor instances, document models, plugins and controls, email rendering helpers, rich text and basic UI components, shipped with TypeScript declarations for IDE hover details on parameters, defaults and behavior boundaries.
|
|
37
|
+
- Update only documentation, type comments and the package version, without changing runtime behavior, the public API or the persisted document version; documents remain at `version: 4`.
|
|
38
|
+
|
|
39
|
+
### Validation
|
|
40
|
+
|
|
41
|
+
- Type checking, Oxlint, build and all 96 Node tests passed.
|
|
42
|
+
- All 34 TypeScript/TSX example files from both READMEs, API references and scenario documents passed type checking; all local documentation link targets exist.
|
|
43
|
+
|
|
44
|
+
## 0.4.0 — 2026-10-28
|
|
45
|
+
|
|
46
|
+
### Features and improvements
|
|
47
|
+
|
|
48
|
+
- Standardize React integration on `useEmailEditorConfig()`, `useEmailEditor()` and `<EmailEditor editor={editor}>`; add `useEmailEditorState(editor)` for on-demand subscriptions to cached, read-only document snapshots.
|
|
49
|
+
- Keep editor instance references stable and isolated, allowing reading, validation and export without mounting the UI; add `setDocument()` and `reset()`, clearing selection, focus and control state from the previous document on replacement or reset.
|
|
50
|
+
- Preserve the current document when configuration or variables change; merge external body input when its value changes, avoid initialization callbacks, and keep document reads and callback arguments as independent snapshots.
|
|
51
|
+
- Add export, new-document, reset and subscription examples to the playground, and update English and Chinese integration documentation.
|
|
52
|
+
|
|
53
|
+
### Fixes and validation
|
|
54
|
+
|
|
55
|
+
- Synchronize configuration, variables, body settings and callbacks during commit so child layout effects can read new values; defer subscription notifications and body callbacks to the layout phase, keeping interrupted concurrent renders from changing existing instances.
|
|
56
|
+
- Use instance-specific IDs for subject inputs so labels, inputs and character-count hints remain independently associated across multiple editors.
|
|
57
|
+
- Add coverage for configuration update timing, concurrent interruption, subject associations across multiple instances, negative type contracts for removed APIs and checks for compatibility entry points in source.
|
|
58
|
+
|
|
59
|
+
### Breaking changes
|
|
60
|
+
|
|
61
|
+
- Remove public `createEmailEditorConfig`, `EmailEditorConfigOptions`, `EmailEditorHandle`, component refs and previous state Props, without aliases or compatibility layers. Pass configuration and state to `useEmailEditor()`; the component accepts only `editor` and `height`.
|
|
62
|
+
- The fourth argument of `blockToHtml()` and `renderBlockContent()` accepts only a `BlockRenderOptions` object; remove boolean adaptation and explicitly reject booleans passed from JavaScript. Template previews use `{ resolveHtmlTemplate: true }`.
|
|
63
|
+
- Persisted documents remain at `version: 4`, without migration of older versions; this package version change affects only the package API and behavior.
|
|
64
|
+
|
|
65
|
+
## 0.3.0 — 2026-10-28
|
|
66
|
+
|
|
67
|
+
### Features and improvements
|
|
68
|
+
|
|
69
|
+
- Add `defineBlock()`, `EmailControls` and `createEmailEditorConfig()`, sharing plugin configuration between the editor and standalone email export; support filtering, sorting and explicit overrides in the built-in block library.
|
|
70
|
+
- Support custom control components, React Canvas/Inspector components, multiple `EditableRichText` regions, `ControlEditor`, declarative nested lists, image resource pickers and shared email HTML helpers in block plugins.
|
|
71
|
+
- Add `config.validate()` to collect issues with numeric ranges, options, URL/image protocols, list counts and business validation; support updating several related controls at once through `setControls()`.
|
|
72
|
+
- Add English and Chinese custom block API documentation, coupon and product card examples, and component regression tests.
|
|
73
|
+
|
|
74
|
+
### Fixes
|
|
75
|
+
|
|
76
|
+
- Preserve explicit colors and underlines when changing link URLs or opening behavior, and restore BODY inheritance on reset.
|
|
77
|
+
- Use the same base typography for the canvas body, preview and email export, fixing differences in calculations for `em` padding, borders and other dimensions.
|
|
78
|
+
- Fix asynchronous list updates overwriting subsequent edits or sorting; merge the latest values by stable row identifiers, ignore stale results after deletion or unmount, and avoid reusing deleted row identifiers for new rows.
|
|
79
|
+
- Give custom controls independent snapshots of values, body settings and variables to prevent parameter mutations from affecting the host; isolate variable synchronization for custom Canvas/Inspector components as well.
|
|
80
|
+
- Show an empty option in optional selects and support clearing them; associate control labels correctly with selects.
|
|
81
|
+
- Make inspector rich text inherit body settings and enabled block typography, paragraph, list and link styles, updating when body configuration changes.
|
|
82
|
+
- Report missing plugins and email export failures explicitly; isolate canvas and inspector errors per block and preserve original node data.
|
|
83
|
+
|
|
84
|
+
### Breaking changes
|
|
85
|
+
|
|
86
|
+
- Switch custom blocks to `defineBlock` + `EmailControls` + `createEmailEditorConfig`, with the component receiving `config`; remove the old `blocks` Prop, public `BlockDefinition/FieldConfig/FieldType` and `resolveBlockStyleFields`, without an adaptation layer. Control factories use `EmailControls.text(...)`, and blocks and lists declare controls through `controls` (without `field` / `fields` aliases); runtime and persisted nodes use `controls`, style declarations use `styleControls`, and update methods use `setControl/setControls`, with corresponding renames for types, `ControlEditor` and `controlPath`. Content defaults come from control declarations, rendering uses `renderEmail`, and the document format advances to v4, without compatibility or automatic migration for old data.
|
|
87
|
+
- Standardize email body settings on `body`: documents use `EmailDocument.body`, the component uses `body` / `onBodyChange`, and types and helpers use `EmailBodySettings`, `DEFAULT_EMAIL_BODY` and `resolveEmailBody`; block rendering contexts and `BlockRenderOptions` also use `body`. Remove the previous Global API without aliases or migration code.
|
|
88
|
+
- Advance the document version to `4`, without support for older versions. BODY General provides text color, link color, font, font weight and a link underline toggle; Content provides a single body Background, content width, Padding and block spacing, with border color, width and style grouped under Advanced: Border, collapsed by default. Apply configuration consistently to canvas, preview and export while preserving explicit content formatting. Keep only `contentPadding`, removing `outerPadding`, body alignment, outer background and border radius; center the body and keep the outer area white.
|
|
89
|
+
|
|
90
|
+
## 0.2.0 — 2026-10-27
|
|
91
|
+
|
|
92
|
+
### Breaking changes
|
|
93
|
+
|
|
94
|
+
- Advance the document format from `version: 1` to `version: 2`, rejecting older versions without automatic migration. Use `createEmailDocument()` to create current-version documents; upgrading existing data requires updating block fields and styles as well as the version number.
|
|
95
|
+
- Store text formatting for Heading, Text and Button in `fields.html`; Button no longer reads `fields.label` or typography from `style.element`. `EmailElementStyle` no longer extends `EmailTypographyStyle`.
|
|
96
|
+
- Make built-in blocks declare `styleFields` according to their capabilities and ignore disabled styles; remove `dimension.height` and block-level `typography` from built-in blocks. Custom blocks can still enable these fields explicitly, with `exactStyleFields` constraining defaults, canvas and email output.
|
|
97
|
+
- Preserve paragraphs, lists, links and explicit styles inside HTML block source without injecting body typography; undeclared properties can still inherit global styles.
|
|
98
|
+
|
|
99
|
+
### Features and improvements
|
|
100
|
+
|
|
101
|
+
- Support inline rich-text editing for Button with a single paragraph, joining multiple pasted paragraphs with spaces; preserve basic inline formatting in email output and generate only one outer button link.
|
|
102
|
+
- Add `BlockInspectorSection`, `inspectorSections`, `RichTextProfile` and `inlineEditProfile` for custom inspector groups and rich-text capabilities; standalone `RichText` supports `profile`.
|
|
103
|
+
- Unify color panels, HEX input and color reset; use a centered dialog for link editing, with color, underline and opening behavior options.
|
|
104
|
+
- Improve block action bars, drag feedback, the three-column layout, toolbar wrapping and preview height; let previews follow document content.
|
|
105
|
+
- Change the default button fill to black and default button/image links to empty, with no navigation for blank links; default global content, block, button and image borders to transparent.
|
|
106
|
+
- Simplify both READMEs, add npm keywords, and include the Chinese README and changelog in the published package.
|
|
107
|
+
|
|
108
|
+
### Fixes and validation
|
|
109
|
+
|
|
110
|
+
- Fix cursor, selection and formatting preservation after rich-text formatting operations, and preserve spacing and indentation when switching lists.
|
|
111
|
+
- Fix button export handling of quotes, comparison operators, comments and spaces inside template expressions.
|
|
112
|
+
- Add Oxlint and regression tests for block configuration, rich text, lists, buttons and color conversion.
|
|
113
|
+
|
|
114
|
+
## 0.1.0 — 2026-10-27
|
|
115
|
+
|
|
116
|
+
First official release (commit `48011d9`).
|
|
117
|
+
|
|
118
|
+
- Provide a React 18 editor, draggable blocks, rich-text editing, an inspector and desktop/mobile previews.
|
|
119
|
+
- Support JSON documents, global and block styles, HTML source editing, custom blocks and Go template previews.
|
|
120
|
+
- Provide data access through component refs, standalone HTML rendering, ESM, TypeScript declarations and a separate CSS entry point.
|
|
121
|
+
- Provide a Vite playground, build tooling and a local linking workflow.
|
|
122
|
+
|
|
123
|
+
## 0.0.0-stage — 2026-10-27
|
|
124
|
+
|
|
125
|
+
- Package distribution verification version before the first release.
|
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# 更新日志
|
|
2
2
|
|
|
3
|
+
[English](./changelog.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
## 0.4.3 — 2026-10-28
|
|
6
|
+
|
|
7
|
+
### 修复与改进
|
|
8
|
+
|
|
9
|
+
- 主题 header 与配置区复用同一个 Personalization 菜单,统一变量颜色、宽度、圆角、阴影和 hover 样式。
|
|
10
|
+
- 将配置区文本、多行文本与链接字段的变量入口移至输入框右侧后缀,预留内容空间;插入变量后恢复输入焦点和光标位置。
|
|
11
|
+
- 菜单按视口空间向左或向上展开,使用原生 popover 避免滚动裁切及带 `transform` 的嵌入容器引起的定位偏移。
|
|
12
|
+
- 补齐键盘打开时的焦点、Tab/Shift+Tab 退出、Escape 关闭与外部点击关闭;关闭菜单时保留当前区块选择,并清理观察器及事件监听器。
|
|
13
|
+
- 保持公开 API、邮件导出和 `version: 4` 文档格式不变。
|
|
14
|
+
|
|
15
|
+
### 验证
|
|
16
|
+
|
|
17
|
+
- `pnpm check` 通过,包含类型检查、Oxlint、构建及全部 103 项 Node 测试;新增菜单定位、焦点、关闭与生命周期回归测试。
|
|
18
|
+
- 复用现有开发服务完成浏览器交互验收,覆盖后缀点击、光标插入、键盘导航及带 `transform` 容器中的定位。
|
|
19
|
+
|
|
20
|
+
## 0.4.2 — 2026-10-28
|
|
21
|
+
|
|
22
|
+
### npm 发布与文档
|
|
23
|
+
|
|
24
|
+
- 补齐 MIT 许可证、仓库、主页和 Issue 地址,固定公开 npm registry 与 pnpm 10.16.1,并通过 `.nvmrc` 推荐 Node 24 LTS。
|
|
25
|
+
- 新增 `pnpm test`、`pnpm check` 和发布前完整校验;通过 `sideEffects` 保留 CSS,并验证消费方可以移除未使用的 JavaScript 导入。
|
|
26
|
+
- 修复 npm README 图片失效:中英文图片与查看链接改用已发布 0.4.1 资源的固定绝对 CDN 地址;新版包不重复携带 PNG 原图,保留 Markdown 和 SVG 文档。
|
|
27
|
+
- 中英文 README 的语言切换、API、示例、发布指南、更新日志及章节导航统一使用 GitHub 绝对地址,并验证目标文档和锚点存在;远端文档需随提交一并推送。
|
|
28
|
+
- 补充中英文发布指南、发布文件范围与 CSS 消费验证;保持 React 18 peer 范围及现有公开 API、文档格式不变。
|
|
29
|
+
|
|
3
30
|
## 0.4.1 — 2026-10-28
|
|
4
31
|
|
|
5
32
|
### 文档与类型提示
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# npm release guide
|
|
2
|
+
|
|
3
|
+
English | [简体中文](./release.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
## Toolchain and package configuration
|
|
6
|
+
|
|
7
|
+
Development uses Node 24 LTS (`.nvmrc`) and pnpm 10.16.1 (`packageManager`). The complete test suite needs Node 22.18+. These development requirements are not consumer requirements, so the package does not declare `engines`. React / React DOM 18 remain the supported peers; React 19 requires a separate compatibility validation.
|
|
8
|
+
|
|
9
|
+
The manifest declares MIT licensing, repository, homepage, issue tracker and the public npm registry. `sideEffects: ["**/*.css"]` preserves CSS imports while allowing unused JavaScript modules to be removed. Actual tree shaking depends on module contents and the consumer's bundler.
|
|
10
|
+
|
|
11
|
+
The package includes built assets, Markdown documentation and SVGs under `docs/` (including [English](./changelog.md) and [Chinese](./changelog.zh-CN.md) changelogs), plus both READMEs. npm automatically includes `package.json` and `LICENSE`. Source, playground, tests, caches and PNG originals are excluded.
|
|
12
|
+
|
|
13
|
+
## README images
|
|
14
|
+
|
|
15
|
+
Both READMEs use absolute CDN URLs pinned to the publicly published `0.4.1` package, which contains the English and Chinese PNG and SVG assets. This avoids relative-path resolution on npm, GitHub branch synchronization and shipping PNG copies in every release.
|
|
16
|
+
|
|
17
|
+
When updating a diagram, host the new image at a stable public URL before changing either README. Do not reference an unpublished version or a `latest` URL that may no longer contain PNGs. Verify an unauthenticated `200` response and the correct image Content-Type.
|
|
18
|
+
|
|
19
|
+
README language switches, documentation, changelog and section links use absolute GitHub `blob/main` URLs. Push the corresponding commit before publishing so the linked READMEs, `docs/` and changelog exist on GitHub; local-only files still return 404. Relative links inside documentation pages are resolved by GitHub against their directories.
|
|
20
|
+
|
|
21
|
+
## Local checks and publishing
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm install --frozen-lockfile
|
|
25
|
+
pnpm check
|
|
26
|
+
npm pack --dry-run
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`pnpm check` runs type checking, linting, build and Node tests in order. Tests cover package contents, README image URLs and consumer CSS preservation. `pnpm test` requires an existing build. `prepack` builds assets; `prepublishOnly` runs the complete check before local publishing.
|
|
30
|
+
|
|
31
|
+
To publish locally, first update both changelogs and select an unpublished version:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm login --registry=https://registry.npmjs.org/
|
|
35
|
+
npm publish
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The npm account needs permission for the `@standhigher` scope and any required 2FA. Published versions cannot be overwritten. Committing or pushing code does not automatically publish a package.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# npm 发布指南
|
|
2
|
+
|
|
3
|
+
[English](./release.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
## 工具链与包配置
|
|
6
|
+
|
|
7
|
+
开发默认使用 Node 24 LTS(`.nvmrc`)和 pnpm 10.16.1(`packageManager`)。完整测试最低需要 Node 22.18;这不是消费方安装要求,因此包没有添加 `engines` 限制。当前仍只声明 React 18 / React DOM 18 支持,React 19 需完成独立兼容性验证后再扩大 peer 范围。
|
|
8
|
+
|
|
9
|
+
`package.json` 声明 MIT 许可证、仓库、主页、Issue 地址及公开 npm registry。`sideEffects: ["**/*.css"]` 保留消费方的 CSS 导入,允许 bundler 丢弃未使用的 JavaScript 模块;实际裁剪效果取决于模块内容和消费方 bundler。
|
|
10
|
+
|
|
11
|
+
`files` 只发布构建产物、`docs/` 下的 Markdown 文档和 SVG(含[英文](./changelog.md)与[中文](./changelog.zh-CN.md)更新日志),以及中英文 README;npm 自动包含 `package.json` 和 `LICENSE`。源码、playground、测试、本地缓存和 PNG 原图不进入包。
|
|
12
|
+
|
|
13
|
+
## README 图片
|
|
14
|
+
|
|
15
|
+
npm README 使用固定版本的绝对 CDN 地址,图片来自已公开发布的 `0.4.1` 包。中英文 PNG 和 SVG 源文件均由该版本提供,不依赖本地相对路径或 GitHub 分支同步,也不需要在每个新版包中重复分发 PNG。
|
|
16
|
+
|
|
17
|
+
更新架构图时,先将新图片放到可公开读取的稳定地址,再更新两个 README 的图片与查看链接。不要把地址替换成尚未发布的版本,也不要使用会随 `latest` 变化且可能缺失 PNG 的路径。图片地址应无需登录,返回 `200` 和正确的图片 Content-Type。
|
|
18
|
+
|
|
19
|
+
README 的语言切换、文档、更新日志和章节导航使用 GitHub `blob/main` 绝对地址。发布前须先推送相应提交,确保 GitHub 上的 README、`docs/` 与更新日志均已同步;仅在本地存在的文档仍会返回 404。文档页内部的相对链接继续由 GitHub 按目录解析。
|
|
20
|
+
|
|
21
|
+
## 本地检查与发布
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm install --frozen-lockfile
|
|
25
|
+
pnpm check
|
|
26
|
+
npm pack --dry-run
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`pnpm check` 顺序执行类型检查、lint、构建和 Node 测试;测试包含发布文件范围、README 图片地址及消费方 CSS 保留验证。`pnpm test` 只执行测试,需先构建。`prepack` 负责构建;本地 `npm publish` 的 `prepublishOnly` 会执行完整校验。
|
|
30
|
+
|
|
31
|
+
需要本地手动发布时,在确认版本未发布、中英文更新日志已同步后执行:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm login --registry=https://registry.npmjs.org/
|
|
35
|
+
npm publish
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
发布需要 npm 账号对 `@standhigher` scope 的权限及账号要求的 2FA。已发布的相同版本不能覆盖。代码提交或推送不会自动发布 npm 包。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@standhigher/react-email-editor",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React email template editor with drag-and-drop blocks, rich text, JSON storage, and HTML export.",
|
|
6
6
|
"keywords": [
|
|
@@ -17,6 +17,16 @@
|
|
|
17
17
|
"go-template"
|
|
18
18
|
],
|
|
19
19
|
"author": "Nono",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/standhigher/react-email-editor.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/standhigher/react-email-editor#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/standhigher/react-email-editor/issues"
|
|
28
|
+
},
|
|
29
|
+
"packageManager": "pnpm@10.16.1",
|
|
20
30
|
"exports": {
|
|
21
31
|
".": {
|
|
22
32
|
"types": "./dist/index.d.ts",
|
|
@@ -27,21 +37,26 @@
|
|
|
27
37
|
"main": "./dist/index.js",
|
|
28
38
|
"module": "./dist/index.js",
|
|
29
39
|
"types": "./dist/index.d.ts",
|
|
40
|
+
"sideEffects": ["**/*.css"],
|
|
30
41
|
"files": [
|
|
31
42
|
"dist",
|
|
32
|
-
"docs",
|
|
43
|
+
"docs/**/*.md",
|
|
44
|
+
"docs/**/*.svg",
|
|
33
45
|
"README.md",
|
|
34
|
-
"README.zh-CN.md"
|
|
35
|
-
"CHANGELOG.md"
|
|
46
|
+
"README.zh-CN.md"
|
|
36
47
|
],
|
|
37
48
|
"publishConfig": {
|
|
38
|
-
"access": "public"
|
|
49
|
+
"access": "public",
|
|
50
|
+
"registry": "https://registry.npmjs.org/"
|
|
39
51
|
},
|
|
40
52
|
"scripts": {
|
|
41
53
|
"dev": "vite --config playground/vite.config.ts",
|
|
42
54
|
"build": "tsup",
|
|
43
55
|
"build:watch": "tsup --watch src",
|
|
56
|
+
"test": "node --test tests/*.test.mjs tests/*.test.ts",
|
|
57
|
+
"check": "pnpm typecheck && pnpm lint && pnpm build && pnpm test",
|
|
44
58
|
"prepack": "pnpm build",
|
|
59
|
+
"prepublishOnly": "pnpm check",
|
|
45
60
|
"typecheck": "tsc --noEmit && tsc --noEmit -p playground",
|
|
46
61
|
"lint": "oxlint . --deny-warnings --report-unused-disable-directives",
|
|
47
62
|
"lint:fix": "pnpm lint --fix"
|
|
Binary file
|
|
Binary file
|