@standhigher/react-email-editor 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.zh-CN.md CHANGED
@@ -2,244 +2,158 @@
2
2
 
3
3
  [English](./README.md) | 简体中文
4
4
 
5
- React 邮件编辑器,支持区块拖拽、富文本编辑和桌面/移动预览。内容以 JSON 保存,通过组件 ref 导出邮件 HTML。
5
+ React 邮件模板编辑器,支持拖拽区块、画布富文本编辑、桌面/移动预览、JSON 保存和 HTML 导出。
6
6
 
7
- ## 安装与接入
7
+ ## 安装
8
8
 
9
- 需要 React 18 和 React DOM 18。组件库提供 ESM 入口和 TypeScript 类型声明。
9
+ 需要 React 18 和 React DOM 18。提供 ESM 和 TypeScript 类型。
10
10
 
11
11
  ```bash
12
12
  pnpm add @standhigher/react-email-editor
13
13
  ```
14
14
 
15
- 在应用中引入一次样式。以下示例展示空编辑器,并读取当前数据:
15
+ ## 基本接入
16
+
17
+ 在应用入口引入一次样式,通过 `ref` 读取文档和 HTML;保存逻辑由应用实现。
16
18
 
17
19
  ```tsx
18
20
  import { useRef } from "react";
19
21
  import {
22
+ BLOCK_TYPES,
20
23
  EmailEditor,
24
+ createBlockNode,
25
+ createEmailDocument,
21
26
  type EmailEditorHandle,
27
+ type Variable,
22
28
  } from "@standhigher/react-email-editor";
23
29
  import "@standhigher/react-email-editor/styles.css";
24
30
 
31
+ const text = createBlockNode(BLOCK_TYPES.text);
32
+ if (text) text.controls.html = "<p>你好,{{ store_name }}</p>";
33
+ const initialDocument = createEmailDocument(text ? [text] : []);
34
+ const variables: Variable[] = [
35
+ { name: "{{ store_name }}", description: "店铺名称", value: "示例店铺" },
36
+ ];
37
+
25
38
  export function EmailPage() {
26
39
  const editorRef = useRef<EmailEditorHandle>(null);
27
40
 
28
- function readData() {
41
+ function save() {
29
42
  const data = editorRef.current?.getData();
30
43
  if (!data) return;
31
- // 将 data.document、data.json、data.html 或 data.renderedHtml
32
- // 交给应用自己的保存或导出逻辑。
44
+ // 将 data.document 保存到应用,data.html 用于邮件模板导出。
33
45
  console.log(data);
34
46
  }
35
47
 
36
48
  return (
37
49
  <>
38
- <EmailEditor ref={editorRef} height={720} />
39
- <button type="button" onClick={readData}>读取数据</button>
50
+ <EmailEditor
51
+ ref={editorRef}
52
+ initialDocument={initialDocument}
53
+ variables={variables}
54
+ height={720}
55
+ />
56
+ <button type="button" onClick={save}>保存</button>
40
57
  </>
41
58
  );
42
59
  }
43
60
  ```
44
61
 
45
- 保存逻辑由接入方实现。Next.js App Router 中,接入编辑器的组件需使用 `"use client"`。使用 `height="100%"` 时,父容器需要有明确高度。
62
+ 省略 `initialDocument` 即为空编辑器。Next.js App Router 中,接入组件需声明 `"use client"`,CSS 在应用允许的全局样式入口引入。使用 `height="100%"` 时,父容器需要有明确高度。
46
63
 
47
- ## 本地启动
64
+ ## 常用参数
48
65
 
49
- 使用 pnpm 和当前受支持的 Node.js LTS;建议使用 Node.js 22.18 或更新版本,以兼容仓库工具链和 TypeScript 测试。
50
-
51
- ```bash
52
- pnpm install --frozen-lockfile
53
- pnpm dev
54
- ```
55
-
56
- 打开 [http://localhost:5173](http://localhost:5173)。playground 直接加载 `src/` 并支持热更新,无需提前构建,也不进入发布包。已有开发服务时直接复用。
57
-
58
- | 命令 | 用途 |
59
- | --- | --- |
60
- | `pnpm dev` | 启动本地 playground,端口为 5173。 |
61
- | `pnpm build` | 在 `dist/` 生成 ESM、类型声明和 CSS。 |
62
- | `pnpm build:watch` | 监听 `src/` 改动并重新构建 `dist/`,不启动预览服务。 |
63
- | `pnpm typecheck` | 检查组件库和 playground 的类型。 |
66
+ 所有 Props 均可选。
64
67
 
65
- 在另一个本地项目联调时,将其依赖指向组件库根目录,按实际位置调整相对路径:
66
-
67
- ```json
68
- {
69
- "dependencies": {
70
- "@standhigher/react-email-editor": "link:../react-email-editor"
71
- }
72
- }
73
- ```
74
-
75
- 先在本仓库执行 `pnpm build`,持续开发时保持 `pnpm build:watch` 运行。消费方执行 `pnpm install`,并使用自己的开发服务。包入口与 CSS 均从 `dist/` 读取。
76
-
77
- 两个项目应使用同一个 React 实例。Vite 消费方可按需设置 `resolve.dedupe: ["react", "react-dom"]`。Next.js 消费方可设置 `transpilePackages: ["@standhigher/react-email-editor"]`;使用 Turbopack 且链接目录在项目外时,`turbopack.root` 需覆盖两个项目。
78
-
79
- ## 组件参数
80
-
81
- 所有 Props 均为可选项,相关类型从 `@standhigher/react-email-editor` 导出。
82
-
83
- | 参数 | 类型 | 默认值 / 行为 |
68
+ | 参数 | 类型 | 行为 |
84
69
  | --- | --- | --- |
85
- | `initialDocument` | `EmailDocument` | 默认空文档,仅在首次挂载时读取。 |
86
- | `global` | `Partial<EmailGlobalSettings>` | 默认无外部覆盖。传入字段覆盖初始文档的全局设置;后续更新会合并到当前文档。 |
87
- | `blocks` | `BlockDefinition[]` | `[]`。新增自定义区块;同名 `type` 覆盖内置定义,其余内置区块保留。 |
88
- | `variables` | `Variable[]` | `[]`。用于插入和预览替换的变量列表。 |
89
- | `height` | `number \| string` | `"100%"`。数字按 px 处理,字符串使用 CSS 高度值。 |
90
- | `onChange` | `(document: EmailDocument) => void` | 文档变化时触发,包括编辑、重置,以及实际改变文档值的外部全局设置更新。 |
91
- | `onGlobalChange` | `(global: EmailGlobalSettings) => void` | 在编辑器中修改全局设置时触发;外部 `global` 更新和重置不触发此回调。 |
70
+ | `initialDocument` | `EmailDocument` | 仅首次挂载读取;切换文档时通过新的 React `key` 重新挂载。 |
71
+ | `height` | `number \| string` | 默认 `"100%"`;数字为 px。 |
72
+ | `body` | `Partial<EmailBodySettings>` | 覆盖邮件主体的布局、外观和默认排版,后续更新合并到当前文档;省略字段不会恢复默认值。 |
73
+ | `variables` | `Variable[]` | 插入和预览变量;默认 `[]`,为空时隐藏 Personalization 入口。 |
74
+ | `config` | `EmailEditorConfig` | 通过 `createEmailEditorConfig({ plugins })` 注册区块、资源适配器和区块库顺序。 |
75
+ | `onChange` | `(document: EmailDocument) => void` | 文档变化时触发,包括实际改变文档值的外部 `body` 更新。 |
76
+ | `onBodyChange` | `(body: EmailBodySettings) => void` | 编辑器内修改邮件主体配置时触发,外部 `body` 更新不触发。 |
92
77
 
93
- Reset 恢复首次初始化的文档快照,并重新应用当前 `global` 覆盖项。切换文档时使用新的 React `key` 重新挂载,例如 `<EmailEditor key={documentId} initialDocument={document} />`;仅修改 `initialDocument` 不会替换已打开的文档。省略之前传入的某个 `global` 字段,不会将其恢复为默认值。
78
+ 邮件主体示例:`body={{ contentWidth: 640, contentPadding: "24px" }}`。默认值通过 `DEFAULT_EMAIL_BODY` 导出;可使用 `resolveEmailBody()` 归一化部分配置。
94
79
 
95
- ## 配置说明
80
+ **BODY** 面板的配置存于 `EmailDocument.body`,同步用于画布、预览和最终邮件 HTML:
96
81
 
97
- ### 文档配置
82
+ - **General**:文字颜色、链接颜色、字体、字重和链接下划线开关。
83
+ - **Content**:Background、内容宽度、Padding(`contentPadding`)和区块间距。默认折叠的 **Advanced: Border** 包含边框颜色、宽度和样式。Background 是唯一的邮件主体内容容器背景色,外侧区域为白色,主体固定居中。Padding 作用于内容边框内部,是唯一的主体内边距配置;已移除 `outerPadding`。
98
84
 
99
- 通过 `createEmailDocument()` 和 `createBlockNode()` 补齐默认值:
85
+ 文字和链接默认继承 BODY 配置,富文本工具栏中单独设置的格式优先。仅修改链接 URL 或打开方式会保留显式链接样式;重置链接样式后恢复继承 BODY。基础字号(16px)、行高(1.5)以及段落、列表间距使用内部默认值。HTML 区块保留源码格式,外层继承主体排版,不改写内部链接和列表。BODY 不提供对齐和圆角配置。
100
86
 
101
- ```tsx
102
- import {
103
- BLOCK_TYPES,
104
- EmailEditor,
105
- createBlockNode,
106
- createEmailDocument,
107
- type Variable,
108
- } from "@standhigher/react-email-editor";
87
+ 内置区块:`heading`、`text`、`button`、`image`、`divider`、`spacer`、`html`,均可通过 `BLOCK_TYPES` 引用。
109
88
 
110
- const text = createBlockNode(BLOCK_TYPES.text);
111
- if (text) text.fields.html = "<p>你好,{{ store_name }}</p>";
89
+ ## 保存与导出
112
90
 
113
- export const initialDocument = createEmailDocument(text ? [text] : []);
114
- initialDocument.subject = "欢迎订阅";
91
+ | Ref 方法 | 返回值 |
92
+ | --- | --- |
93
+ | `getData()` | `{ document, json, html, renderedHtml }` |
94
+ | `getDocument()` | 独立的 `EmailDocument` 快照,可存储后传回 `initialDocument`。 |
95
+ | `getJson()` | 格式化的文档 JSON 字符串。 |
96
+ | `getHtml()` | 保留模板表达式的完整邮件 HTML。 |
97
+ | `getRenderedHtml()` | 使用预览数据解析模板后的完整邮件 HTML。 |
98
+
99
+ 不挂载编辑器时,也可直接渲染文档:
100
+
101
+ ```ts
102
+ import { documentToHtml } from "@standhigher/react-email-editor";
103
+
104
+ const html = documentToHtml(initialDocument, undefined, { variables });
105
+ const previewHtml = documentToHtml(initialDocument, undefined, {
106
+ variables,
107
+ resolveHtmlTemplates: true,
108
+ });
109
+ ```
115
110
 
116
- const variables: Variable[] = [
117
- { name: "{{ store_name }}", description: "店铺名称", value: "示例店铺" },
118
- ];
111
+ 变量的 `name` 原样插入,`description` 和预览值 `value` 可选。导出时仅对与 `variables[].name` 原文精确匹配的 token 补点并保留空格,如 `{{ store_name }}` → `{{ .store_name }}`;其他 Go template 表达式保持原样。预览支持部分 Go template 语法,数据优先级为区块 `defaultData` > `templateData`(`documentToHtml` 选项)> `variables`。
119
112
 
120
- export function TemplateEmailPage() {
121
- return (
122
- <EmailEditor
123
- initialDocument={initialDocument}
124
- variables={variables}
125
- height={720}
126
- />
127
- );
128
- }
129
- ```
113
+ 当前文档格式仍处于开发阶段,仅接受 `version: 4`,不自动迁移旧文档。使用 `createEmailDocument()` 和 `createBlockNode()` 创建数据。升级差异见 [CHANGELOG](./CHANGELOG.md)。
130
114
 
131
- 如示例所示,同时传入 `initialDocument` 和 `variables`,使变量拥有预览值,并能在导出时补点。`createBlockNode()` 遇到未知类型时返回 `null`。内置区块有 `heading`、`text`、`button`、`image`、`divider`、`spacer` 和 `html`,可通过 `BLOCK_TYPES` 引用。
115
+ ## 自定义区块
132
116
 
133
- | 文档字段 | 类型 | 说明 |
134
- | --- | --- | --- |
135
- | `version` | `typeof DOCUMENT_VERSION` | 当前版本为 `1`,由 `createEmailDocument()` 填充。 |
136
- | `subject` | `string`,可选 | 邮件主题,编辑器输入上限为 128 个字符。 |
137
- | `global` | `EmailGlobalSettings` | 邮件全局样式,创建函数会补齐默认值。 |
138
- | `blocks` | `EmailBlockNode[]` | 按展示顺序排列的区块。每项包含唯一 `id`、`type`、`fields`,以及可选的 `style` 和预览数据 `defaultData`。 |
139
-
140
- 文档结构仍处于开发阶段,不会自动迁移旧版区块字段。完整模型见[文档类型定义](./src/types/document.ts)。
141
-
142
- ### 全局样式
143
-
144
- 通过 `global` 传入部分设置,例如 `<EmailEditor global={{ contentWidth: 640, outerPadding: "24px" }} />`。`DEFAULT_EMAIL_GLOBAL` 导出完整默认值;`resolveEmailGlobal()` 用于补齐和归一化配置。
145
-
146
- 这些设置控制邮件内容。编辑器的选中边框、工具栏等辅助界面不会进入导出的 HTML。
147
-
148
- | 参数 | 类型 / 可选值 | 默认值 | 说明 |
149
- | --- | --- | --- | --- |
150
- | `contentWidth` | `number` | `600` | 内容宽度,单位 px;取整并限制在 320–900。 |
151
- | `contentAlign` | `"left" \| "center" \| "right"` | `"center"` | 内容整体的水平位置。 |
152
- | `backgroundColor` | `string` | `"#ffffff"` | 邮件外层背景色。 |
153
- | `contentBackgroundColor` | `string` | `"#ffffff"` | 内容背景色。 |
154
- | `contentPadding` | `string` | `"0px"` | 内容内边距。 |
155
- | `outerPadding` | `string` | `"0px"` | 内容外侧留白。 |
156
- | `blockGap` | `string` | `"4px"` | 区块间距。 |
157
- | `textColor` | `string` | `"#000000"` | 默认文字颜色。 |
158
- | `textAlign` | `"left" \| "center" \| "right" \| "justify"` | `"left"` | 默认文字对齐。 |
159
- | `fontFamily` | `string` | `"Arial, Helvetica, sans-serif"` | 默认字体列表。 |
160
- | `fontWeight` | `"normal" \| "bold"` | `"normal"` | 默认字重。 |
161
- | `defaultFontSize` | `string` | `"16px"` | 默认字号。 |
162
- | `defaultLineHeight` | `string` | `"1.5"` | 默认行高。 |
163
- | `paragraphSpacing` | `string` | `"6px"` | 相邻顶层段落的间距。 |
164
- | `listSpacing` | `string` | `"4px"` | 列表整体(`ul` / `ol`)的上下外边距。 |
165
- | `listIndent` | `string` | `"22px"` | 列表缩进。 |
166
- | `linkColor` | `string` | `"#005bd3"` | 默认链接颜色。 |
167
- | `linkDecoration` | `"underline" \| "none"` | `"underline"` | 默认链接装饰。 |
168
- | `borderWidth` | `string` | `"0px"` | 内容边框宽度。 |
169
- | `borderStyle` | `"none" \| "solid" \| "dashed" \| "dotted"` | `"solid"` | 内容边框样式。 |
170
- | `borderColor` | `string` | `"#000000"` | 内容边框颜色。 |
171
- | `borderRadius` | `string` | `"0px"` | 单个圆角值,或按左上、右上、右下、左下排列的四个值。 |
172
-
173
- 间距和尺寸使用 `"16px"` 等 CSS 长度字符串;内边距支持 `"12px 24px"` 等简写,行高也支持无单位数值。桌面画布和预览跟随 `contentWidth`,移动预览的视口宽度为 360px。
174
-
175
- 全局间距和长度配置支持非负的 `px`、`em`、`rem`、`%` 值;除行高外,无单位长度会补为 px。不支持的值(如负长度、`calc()`、`var()`、`vh`、`vw`)会回退为对应全局参数的默认值。
176
-
177
- ### 变量与模板数据
117
+ 使用 `defineBlock()` 声明控件及 `renderEmail()`,通过 `createEmailEditorConfig()` 注册:
178
118
 
179
119
  ```tsx
180
- import type { Variable } from "@standhigher/react-email-editor";
181
-
182
- export const variables: Variable[] = [
183
- { name: "{{ store_name }}", description: "店铺名称", value: "示例店铺" },
184
- ];
120
+ const coupon = defineBlock({
121
+ type: "my-app.coupon",
122
+ title: "优惠券",
123
+ controls: {
124
+ code: EmailControls.text({ label: "优惠码", defaultValue: "SAVE20" }),
125
+ },
126
+ renderEmail: ({ controls, email }) => email.html`<p>${controls.code}</p>`,
127
+ });
128
+ const config = createEmailEditorConfig({ plugins: [coupon] });
129
+
130
+ <EmailEditor config={config} />;
131
+ const block = config.createBlock("my-app.coupon");
132
+ const html = config.toHtml(createEmailDocument([block]));
185
133
  ```
186
134
 
187
- 通过 `EmailEditor` 的 `variables` 参数传入此数组。
188
-
189
- | 字段 | 类型 | 说明 |
190
- | --- | --- | --- |
191
- | `name` | `string` | 必填。下拉展示并原样插入的变量,保留空格,例如 `{{ store_name }}`。 |
192
- | `description` | `string`,可选 | 下拉选项副文案;空字符串或不传时不显示。 |
193
- | `value` | `string`,可选 | 预览时使用的替换文本。 |
194
-
195
- 变量可以带前导点。导出模板 HTML 时,仅对与 `variables[].name` 原文精确匹配(包括空格)的 token 补齐缺失的点,并保留空格,例如 `{{ store_name }}` 变为 `{{ .store_name }}`。其他 Go template 表达式保持原样,默认不会替换为预览值。
135
+ 从包入口导入 `defineBlock`、`EmailControls`、`createEmailEditorConfig`。控件值自动推导,默认生成检查器和画布,支持富文本、列表、图片资源适配器、自定义控件组件及 React `Inspector` / `Canvas`。`EditableRichText` 支持多区域编辑,`ControlEditor` 可在自定义检查器复用声明的控件。
196
136
 
197
- 区块节点的 `defaultData: Record<string, unknown>` 可提供结构化预览数据,例如供 `{{range .products}}` 使用的数组。同名数据以区块 `defaultData` 为准,覆盖全局变量。预览解析适用于所有区块渲染出的 HTML,也包括自定义区块。HTML 区块的模板表达式直接写在 `fields.html` 中;预览渲染器支持部分 Go template 语法。
137
+ `config.validate(document)` 返回带区块 ID 和控件路径的 `ValidationIssue[]`,用于导出前预检;数字范围、选项、URL/图片协议、列表数量和业务校验也用于检查器及邮件导出。自定义 Canvas/Inspector 可用 `setControls(patch)` 一次更新多个关联控件值并触发一次 `onChange`。声明元数据采用隔离冻结快照,控件值解析结果在每次上下文中复用,`onChange` / `onBodyChange` 返回独立数据快照。
198
138
 
199
- ### 区块配置
139
+ 编辑器与独立导出共用配置;`builtins` 可筛选和排序内置区块,覆盖内置区块必须显式声明 `override: true`。声明、运行时和持久化节点统一使用 `controls`;邮件文档为 v4,不兼容旧数据,不保留旧 `blocks`、`defaultFields`、`inlineEdit` 或 `render()` 接入方式。完整接入、类型、渲染工具和异常规则见[自定义区块 API 文档](./docs/custom-blocks.zh-CN.md)([English](./docs/custom-blocks.md)),可运行示例见 `playground/plugins/`。
200
140
 
201
- 区块节点的样式按用途分组:
141
+ ## 本地开发
202
142
 
203
- | 样式分组 | 用途 |
204
- | --- | --- |
205
- | `style.dimension` | 容器内边距 `padding`、外侧留白 `margin`,以及可选的 px 高度 `height`;高度包含内边距,作为最小高度。 |
206
- | `style.decorations` | 容器背景、边框和圆角。 |
207
- | `style.typography` | 区块文字样式,覆盖全局排版设置。 |
208
- | `style.element` | 按钮、图片和分割线等元素自身的样式,例如尺寸和对齐。 |
209
-
210
- Heading 和 Text 默认包含 `16px` 内边距。HTML 区块保留作者显式编写的内联样式。各字段见 [EmailBlockStyle](./src/types/document.ts)。
211
-
212
- 通过 `blocks` 传入稳定的 `BlockDefinition[]` 添加自定义区块。必填项为 `type`、`title` 和 `render(ctx)`;可选的 `fields`、`styleFields` 配置检查器,`defaultFields`、`defaultStyle`、`defaultData` 设置新建节点的初始值。`render(ctx)` 返回 HTML 字符串,接收 `mode: "canvas" | "email"`。
213
-
214
- ```tsx
215
- import { EmailEditor, type BlockDefinition } from "@standhigher/react-email-editor";
216
-
217
- const customBlocks: BlockDefinition[] = [{
218
- type: "notice",
219
- title: "Notice",
220
- defaultStyle: { dimension: { padding: "16px" } },
221
- render: () => '<p style="margin:0;color:#555555;">感谢订阅。</p>',
222
- }];
143
+ 建议 Node.js 22.18+,使用 pnpm:
223
144
 
224
- export function CustomEmailPage() {
225
- return <EmailEditor blocks={customBlocks} height={720} />;
226
- }
145
+ ```bash
146
+ pnpm install --frozen-lockfile
147
+ pnpm dev
227
148
  ```
228
149
 
229
- 默认会为自定义区块补充通用容器间距和装饰控件。设置 `exactStyleFields: true` 后,只展示传入的样式字段;节点上已有样式仍会参与渲染。完整定义见 [BlockDefinition、FieldConfig 和 StyleFieldConfig](./src/types/block.ts)。
230
-
231
- ## 读取与导出数据
232
-
233
- 通过 `useRef<EmailEditorHandle>(null)` 调用以下方法:
150
+ 打开 [http://localhost:5173](http://localhost:5173)。playground 直接加载 `src/`,已有服务时复用。验证命令:
234
151
 
235
- | 方法 | 返回类型 | 说明 |
236
- | --- | --- | --- |
237
- | `getData()` | `EmailEditorData` | 返回 `{ document, json, html, renderedHtml }`。 |
238
- | `getDocument()` | `EmailDocument` | 返回独立文档快照,可保存或用于下次编辑。 |
239
- | `getJson()` | `string` | 返回格式化的文档 JSON。 |
240
- | `getHtml()` | `string` | 返回完整邮件 HTML,保留模板表达式,仅为 `variables` 中的 token 补齐缺失的点。 |
241
- | `getRenderedHtml()` | `string` | 使用全局变量和区块预览数据解析模板,返回完整邮件 HTML。 |
242
-
243
- 修改返回的文档快照不会改变编辑器内部状态。可使用 `onChange` 订阅文档变化。
152
+ ```bash
153
+ pnpm typecheck
154
+ pnpm lint
155
+ pnpm build
156
+ node --test tests/*.test.mjs tests/*.test.ts
157
+ ```
244
158
 
245
- 不挂载编辑器时,可调用 `documentToHtml(document, definitions?, options?)`。`definitions` 默认为 `DEFAULT_BLOCKS`,文档使用自定义区块时需包含对应定义。`options` 支持 `resolveHtmlTemplates`(默认 `false`)、`variables` 和 `templateData`。未开启解析时,仅对传入 `variables` 中的 token 补点。开启解析时,数据优先级为区块 `defaultData` 高于 `templateData`,高于 `variables`。
159
+ 本地消费方可使用 `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` 需覆盖链接目录。