@standhigher/react-email-editor 0.2.0 → 0.4.1

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.
@@ -0,0 +1,486 @@
1
+ # 自定义区块 API
2
+
3
+ [English](./custom-blocks.md) | 简体中文 · [返回 README](../README.zh-CN.md)
4
+
5
+ 本指南按“示例 → API 参数 → 返回值 → 行为规则”介绍三个核心入口:
6
+
7
+ | 入口 | 职责 |
8
+ | --- | --- |
9
+ | `EmailControls` | 声明控件、值类型、默认值、校验和编辑组件。 |
10
+ | `defineBlock()` | 组合控件、样式、编辑界面和邮件输出,形成一个区块插件。 |
11
+ | `useEmailEditorConfig()` | 注册插件,提供区块创建、文档校验和 HTML 导出。 |
12
+
13
+ 内置区块使用相同机制。配置不持有具体文档或选中状态,可以被多个编辑器复用。`EmailDocument` 是文档状态的事实源,React 编辑会话负责选择、拖拽和更新。
14
+
15
+ ## 完整接入示例
16
+
17
+ ```tsx
18
+ import { useState } from "react";
19
+ import {
20
+ EmailEditor,
21
+ EmailControls,
22
+ createEmailDocument,
23
+ useEmailEditorConfig,
24
+ useEmailEditor,
25
+ defineBlock,
26
+ } from "@standhigher/react-email-editor";
27
+ import "@standhigher/react-email-editor/styles.css";
28
+
29
+ const coupon = defineBlock({
30
+ type: "my-app.coupon",
31
+ title: "优惠券",
32
+ controls: {
33
+ code: EmailControls.text({ label: "优惠码", defaultValue: "SAVE20" }),
34
+ message: EmailControls.richText({
35
+ label: "说明",
36
+ defaultValue: "<p>感谢你的支持。</p>",
37
+ }),
38
+ },
39
+ renderEmail: ({ controls, email }) => email.html`
40
+ ${email.richText(controls.message)}
41
+ <p>优惠码:${controls.code}</p>
42
+ `,
43
+ });
44
+
45
+ export function EmailPage() {
46
+ const config = useEmailEditorConfig({ plugins: [coupon] });
47
+ const [initialDocument] = useState(() => createEmailDocument([config.createBlock(coupon.type)]));
48
+ const editor = useEmailEditor({ config, initialDocument });
49
+ return <EmailEditor editor={editor} />;
50
+ }
51
+ ```
52
+
53
+ `controls` 在声明阶段保存控件定义,在节点、Canvas/Inspector Props 和渲染上下文中保存对应的值:
54
+
55
+ ```ts
56
+ // defineBlock 的声明
57
+ controls: { code: EmailControls.text({ label: "优惠码", defaultValue: "SAVE20" }) }
58
+
59
+ // config.createBlock() 返回的节点数据
60
+ { id: "…", type: "my-app.coupon", controls: { code: "SAVE20" }, style: { /* … */ } }
61
+
62
+ // renderEmail 或 Canvas / Inspector 中读取当前值
63
+ controls.code // string
64
+ ```
65
+
66
+ ## 1. useEmailEditorConfig(options?)
67
+
68
+ ### 示例
69
+
70
+ ```tsx
71
+ function useCouponConfig() {
72
+ return useEmailEditorConfig({
73
+ plugins: [coupon],
74
+ builtins: ["heading", "text", "image", "button"],
75
+ pickImage: async ({ value, label, signal }) => {
76
+ // 调用应用自己的素材库;使用 value 初始化,label 作标题,signal 取消请求。
77
+ // 返回图片 URL;取消选择时返回 null。
78
+ return null;
79
+ },
80
+ });
81
+ }
82
+ ```
83
+
84
+ 配置 Hook 必须在 React 组件或自定义 Hook 顶层调用。保持 `plugins`、`builtins` 和 `pickImage` 引用稳定会保留配置引用;更新配置不会重置已有实例文档。移除仍被文档使用的插件后,校验报告缺失插件,原始数据保留。配置的 `toHtml(document)` 和 `validate(document)` 面向任意文档;实例的 `getHtml()` 和 `validate()` 面向当前文档。
85
+
86
+ ### 参数:UseEmailEditorConfigOptions
87
+
88
+ 所有参数均可选。
89
+
90
+ | 参数 | 类型 | 默认值与作用 |
91
+ | --- | --- | --- |
92
+ | `plugins` | `BlockPlugin[]` | 默认 `[]`。注册自定义区块;同一类型重复注册报错。 |
93
+ | `builtins` | `false \| string[]` | 省略时显示全部内置区块;数组筛选并排序;`false` 隐藏全部内置区块。 |
94
+ | `pickImage` | `(context: ImagePickerContext) => Promise<string \| null>` | 图片资源选择适配器;未提供时仍可手动输入图片 URL。 |
95
+
96
+ `builtins` 只影响区块库可见性,隐藏的内置区块仍可创建、加载和渲染。数组只接受内置类型,不允许重复。自定义类型按 `plugins` 顺序追加;覆盖内置类型的插件仍遵循该内置类型的显示位置和筛选规则。覆盖须在插件声明上设置 `override: true`。
97
+
98
+ 内置类型为 `heading`、`text`、`button`、`image`、`divider`、`spacer`、`html`,可通过 `BLOCK_TYPES` 引用。
99
+
100
+ ### 返回值:EmailEditorConfig
101
+
102
+ | 字段 / 方法 | 类型 | 含义和作用 |
103
+ | --- | --- | --- |
104
+ | `definitions` | `readonly BlockPlugin[]` | 全部注册定义,包含隐藏的内置区块,供加载、创建、校验和渲染使用。 |
105
+ | `library` | `readonly BlockPlugin[]` | 区块库显示列表,已经过内置筛选、排序和插件合并。 |
106
+ | `pickImage` | 同配置参数,可选 | 已注册的图片选择适配器。 |
107
+ | `createBlock(type)` | `(type: string) => EmailBlockNode` | 创建独立节点及 ID,深拷贝默认值、样式和模板样例;未知类型抛错。 |
108
+ | `toHtml(document, options?)` | `(...) => string` | 同步生成完整邮件 HTML;校验或渲染失败时抛错。 |
109
+ | `validate(document)` | `(...) => ValidationIssue[]` | 汇总控件校验问题与缺失插件;不调用渲染器、不修改输入文档。 |
110
+
111
+ `createBlock()` 只返回节点,不会把节点插入已挂载编辑器。配置本身及普通声明元数据采用复制、冻结快照;函数、React 元素和非普通对象保留身份,函数闭包中的状态不会被隔离。调整配置时重新创建插件和配置。
112
+
113
+ ### toHtml 的 options:DocumentHtmlOptions
114
+
115
+ | 参数 | 类型 | 作用 |
116
+ | --- | --- | --- |
117
+ | `resolveHtmlTemplates` | `boolean` | 默认不解析模板;`true` 使用预览数据解析受支持的 Go template 表达式。 |
118
+ | `variables` | `Variable[]` | 变量原文、可选描述和预览值;导出时仅给列表中精确匹配的变量原文补点。 |
119
+ | `templateData` | `Record<string, unknown>` | 本次模板预览使用的数据。 |
120
+
121
+ 模板预览优先级为节点 `defaultData` > `templateData` > `variables`,按顶层键合并。普通导出保留模板表达式,例如 `{{ store_name }}` 在变量列表中时写为 `{{ .store_name }}`,保留原空格;列表外的表达式不改写。
122
+
123
+ ### validate 的返回项:ValidationIssue
124
+
125
+ | 字段 | 类型 | 作用 |
126
+ | --- | --- | --- |
127
+ | `blockId` | `string` | 问题区块 ID。 |
128
+ | `blockType` | `string` | 问题区块类型。 |
129
+ | `controlPath` | `string` | 控件路径,例如 `terms[0].text`;插件缺失时为空字符串。 |
130
+ | `message` | `string` | 校验失败原因。 |
131
+
132
+ 不支持的文档版本会直接抛错。`validate()` 不检查渲染器能否成功运行,导出仍可能因渲染错误失败。
133
+
134
+ ## 2. defineBlock(options)
135
+
136
+ ### 示例:控件、容器样式和检查器分组
137
+
138
+ ```ts
139
+ const banner = defineBlock({
140
+ type: "my-app.banner",
141
+ title: "横幅",
142
+ description: "带链接的提示区块",
143
+ controls: {
144
+ title: EmailControls.text({ label: "标题", defaultValue: "限时活动" }),
145
+ href: EmailControls.url({ label: "链接" }),
146
+ },
147
+ containerStyles: { spacing: true, appearance: true },
148
+ defaultStyle: { dimension: { padding: "16px" } },
149
+ inspectorSections: [
150
+ { title: "内容", controlNames: ["title", "href"] },
151
+ { title: "间距", styleIds: ["dimension.padding", "dimension.margin"] },
152
+ ],
153
+ renderEmail: ({ controls, email }) => email.button({
154
+ href: controls.href,
155
+ label: controls.title,
156
+ }),
157
+ });
158
+ ```
159
+
160
+ ### 参数:BlockPluginOptions
161
+
162
+ | 字段 | 必填 | 含义和作用 |
163
+ | --- | --- | --- |
164
+ | `type` | 是 | 唯一且稳定的类型标识;建议业务命名空间,例如 `my-app.banner`。持久化后不能随意变更。 |
165
+ | `title` | 是 | 区块库和检查器名称。`type` / `title` 不允许为空白。 |
166
+ | `icon` | 否 | 字符串或 ReactNode 图标。 |
167
+ | `description` | 否 | 区块说明。 |
168
+ | `override` | 否 | 默认 `false`;允许在注册时替换同类型的内置区块。 |
169
+ | `controls` | 否 | `ControlSchema` 对象;按声明顺序定义内容控件,自动推导值类型,默认空对象。 |
170
+ | `containerStyles` | 否 | 默认开启间距及外观控件;`false` 全关,或 `{ spacing?: boolean; appearance?: boolean }` 单独控制。 |
171
+ | `styleControls` | 否 | `StyleControlConfig[]`,补充或覆盖同 `id` 的样式控件。 |
172
+ | `inspectorSections` | 否 | `BlockInspectorSection[]`,按顺序组织内容和样式控件;省略时自动分组。 |
173
+ | `defaultStyle` | 否 | `EmailBlockStyle`,只为已启用的样式提供默认值,优先于样式控件的 `defaultValue`。 |
174
+ | `defaultData` | 否 | 模板预览样例,创建节点时深拷贝到节点。与控件默认值用途不同。 |
175
+ | `Canvas` | 否 | 自定义 React 画布;省略时使用默认 HTML 画布或单富文本编辑区域。 |
176
+ | `Inspector` | 否 | 自定义 React 内容检查器;替换默认内容控件界面,样式面板继续保留。 |
177
+ | `renderEmail` | 是 | 同步函数,返回 HTML 字符串或 `EmailMarkup`;输出区块内部内容。 |
178
+
179
+ 默认容器控件包含 padding、margin、背景色及边框颜色、宽度、样式、圆角,不包含固定高度或文字排版。`containerStyles: false` 只关闭自动添加,仍可通过 `styleControls` 显式添加所需能力。
180
+
181
+ `inspectorSections` 显式设置后,只展示其中列出的默认内容和样式控件;未列出的控件不自动追加,但仍属于区块 schema。
182
+
183
+ ### 返回值:BlockPlugin
184
+
185
+ 通常直接把返回值放入 `plugins`,不手工构造运行时定义。
186
+
187
+ | 返回字段 | 作用 |
188
+ | --- | --- |
189
+ | `type/title/icon/description/override` | 区块标识、展示及注册元数据。 |
190
+ | `schema` | 冻结的控件声明对象,保留按名称查询和类型推导。 |
191
+ | `controls` | 归一化后的控件定义数组,每项带 `name`;这里是定义,节点的 `controls` 是值。 |
192
+ | `readControls(source)` | 读取并归一化控件值,补齐缺失值,保留未知业务属性。 |
193
+ | `styleControls/inspectorSections` | 合并后的样式能力和检查器分组。 |
194
+ | `defaultStyle/defaultData` | 默认样式与模板样例。 |
195
+ | `inlineControl` | 默认画布直接编辑的富文本控件名;有自定义 Canvas 时不使用。 |
196
+ | `canvas/inspector` | 适配后的 React 组件;与声明参数的大写 `Canvas/Inspector` 对应。 |
197
+ | `renderEmail(context)` | 包装后的渲染函数,邮件模式执行控件校验并将结果转换为字符串。 |
198
+
199
+ ### renderEmail 的上下文:PluginRenderContext
200
+
201
+ | 字段 | 含义和作用 |
202
+ | --- | --- |
203
+ | `block` | 完整节点快照,包含 ID、类型、`controls`、样式及模板样例。 |
204
+ | `controls` | 已归一化的控件值,类型由声明推导。 |
205
+ | `style` | 区块已启用的样式。 |
206
+ | `body` | 邮件主体布局、外观及默认文字配置。 |
207
+ | `mode` | `"canvas"` 或 `"email"`;预览使用邮件渲染后解析模板,不是第三种 mode。 |
208
+ | `email` | 邮件 HTML 工具,见下文。 |
209
+
210
+ 渲染函数应同步、无副作用。业务请求在应用或编辑组件中完成;导出不请求网络。只输出内部 HTML,外层表格、容器间距和完整文档由公共渲染器生成。自定义 Canvas 的 DOM 不会进入邮件,预览和导出始终使用 `renderEmail()`。
211
+
212
+ ## 3. EmailControls
213
+
214
+ ### 示例:文本、选项和列表
215
+
216
+ ```ts
217
+ const productControls = {
218
+ name: EmailControls.text({ label: "名称", defaultValue: "马克杯" }),
219
+ price: EmailControls.number({ label: "价格", min: 0, step: 0.01 }),
220
+ visible: EmailControls.boolean({ label: "显示", defaultValue: true }),
221
+ variant: EmailControls.select({
222
+ label: "规格",
223
+ defaultValue: "small",
224
+ options: [{ label: "小号", value: "small" }, { label: "大号", value: "large" }],
225
+ }),
226
+ terms: EmailControls.list({
227
+ label: "使用条件",
228
+ controls: {
229
+ text: EmailControls.text({ label: "条件", defaultValue: "每位客户限用一次" }),
230
+ },
231
+ }),
232
+ };
233
+ ```
234
+
235
+ `EmailControls` 是控件工厂集合,方法使用小写驼峰,调用结果是控件声明。它同时提供编辑界面、值类型、默认值和校验规则。
236
+
237
+ ### 方法
238
+
239
+ 每个方法接收一个选项对象,返回 `ControlDefinition<T>`。
240
+
241
+ | 方法 | 值类型 | 未声明 defaultValue 时 | 默认编辑方式 |
242
+ | --- | --- | --- | --- |
243
+ | `text(options)` | `string` | `""` | 单行输入,可插入变量。 |
244
+ | `textarea(options)` | `string` | `""` | 多行输入,可插入变量。 |
245
+ | `richText(options)` | `string` | `"<p></p>"` | 富文本 HTML,继承主体排版。 |
246
+ | `html(options)` | `string` | `""` | HTML 源码编辑器。 |
247
+ | `url(options)` | `string` | `""` | URL 输入、协议校验、变量插入。 |
248
+ | `image(options)` | `string` | `""` | 图片 URL、预览及可选资源选择器。 |
249
+ | `number(options)` | `number` | `0` | 数字输入。 |
250
+ | `color(options)` | `string` | `""` | 颜色选择。 |
251
+ | `select(options)` | `string` | `""` | 下拉选项,包含可清空的空选项;空选项文案使用 placeholder,默认 None。 |
252
+ | `boolean(options)` | `boolean` | `false` | 复选框。 |
253
+ | `list(options)` | 子控件值对象数组 | `[]` | 新增、删除、上移、下移,可嵌套。额外必填 `controls`。 |
254
+ | `custom(options)` | `read()` 返回类型 | 必须显式提供 | 自定义 React 控件。额外必填 `defaultValue/read/component`。 |
255
+
256
+ ### 参数:ControlOptions
257
+
258
+ | 参数 | 类型 | 用途 |
259
+ | --- | --- | --- |
260
+ | `label` | `string`,必填 | 控件标签。 |
261
+ | `defaultValue` | `T` | 新节点及缺失值的默认值;可变数据独立深拷贝。 |
262
+ | `placeholder` | `string` | 支持输入提示的控件使用。 |
263
+ | `options` | `{ label: string; value: string }[]` | select 选项。 |
264
+ | `rows` | `number` | textarea / HTML 编辑器行数;textarea 默认 4 行。 |
265
+ | `min/max` | `number` | number 数值范围或 list 条目数限制。 |
266
+ | `step` | `number` | number 输入步长;不作为独立校验约束。 |
267
+ | `unit` | `string` | 输入标签的单位提示;不写进控件值,不自动拼接 CSS 单位。 |
268
+ | `profile` | `"text" \| "heading" \| "button"` | 富文本编辑能力。 |
269
+ | `inline` | `boolean` | richText 在默认画布直接编辑;复合布局使用 Canvas。 |
270
+ | `component` | `ComponentType<ControlComponentProps<T>>` | 替换该控件的默认 React 编辑组件。 |
271
+ | `validate` | `(value: T) => string \| undefined` | 接收归一化值,返回非空错误文本或 `undefined`。 |
272
+
273
+ `list` 额外接收 `controls: ControlSchema`,声明每行的子控件。`custom` 额外接收纯函数 `read: (value: unknown) => T`,解析未知持久化值;值必须可 JSON 持久化,不存 React 节点、函数或编辑器实例。
274
+
275
+ ### 默认值、读取和校验
276
+
277
+ - 默认值只补 `undefined`,保留空字符串、`0`、`false`。字符串类将 `null` 归一化为 `""`;数字类将非法输入转换为 `NaN` 并校验失败;布尔类只接受真正的 boolean,其余归一化为 `false`;非数组列表归一化为 `[]`。
278
+ - 每次构建上下文时读取一次,渲染、组件和校验复用结果;`read()` 应为纯函数。列表行未声明的业务 ID、元数据会保留。
279
+ - number 必须有限且满足 `min/max`;非空 select 必须属于 `options`;URL/图片校验协议;list 的 `min/max` 限制条目数,递归校验子控件。
280
+ - 空 URL 和空 select 默认可选,必填使用业务 `validate()`。检查器显示错误,草稿可保留无效值,邮件导出拒绝输出;预览走邮件渲染,也会校验。
281
+
282
+ ### 返回值:ControlDefinition
283
+
284
+ | 字段 | 作用 |
285
+ | --- | --- |
286
+ | `kind` | 控件类型,例如 `text`、`richText`、`list`。 |
287
+ | `defaultValue` | 已解析的默认值。 |
288
+ | `read(value)` | 从未知值归一化为控件值类型。 |
289
+ | `validateValue(value)` | 对已归一化值执行内置及业务约束,不重复调用 read。 |
290
+ | `renderControl(props)` | 通过 component 生成的自定义控件渲染入口,可选。 |
291
+ | `itemControls` | list 的子控件 schema,可选。 |
292
+ | 其他元数据 | 保留 label、placeholder、options、profile 等声明配置。 |
293
+
294
+ ### 示例:自定义值类型
295
+
296
+ ```tsx
297
+ import { TextInput, type ControlComponentProps } from "@standhigher/react-email-editor";
298
+
299
+ function TagInput({ value, onChange, label }: ControlComponentProps<string[]>) {
300
+ return <label>{label}<TextInput value={value.join(", ")} onChange={(event) => onChange(event.target.value.split(",").map((tag) => tag.trim()))} /></label>;
301
+ }
302
+
303
+ const tags = EmailControls.custom<string[]>({
304
+ label: "标签",
305
+ defaultValue: [],
306
+ read: (value) => Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [],
307
+ component: TagInput,
308
+ });
309
+ ```
310
+
311
+ ## 4. 自定义 React 编辑组件
312
+
313
+ ### 单个控件:component
314
+
315
+ ```tsx
316
+ import { TextInput, type ControlComponentProps } from "@standhigher/react-email-editor";
317
+
318
+ function CouponInput({ value, onChange, label }: ControlComponentProps<string>) {
319
+ return <label>{label}<TextInput value={value} onChange={(event) => onChange(event.target.value.toUpperCase())} /></label>;
320
+ }
321
+
322
+ const code = EmailControls.text({ label: "优惠码", component: CouponInput });
323
+ ```
324
+
325
+ | Props | 作用 |
326
+ | --- | --- |
327
+ | `value` | 当前控件值。 |
328
+ | `onChange(value)` | 提交该控件新值。 |
329
+ | `label` | 控件名称。 |
330
+ | `body` | 主体配置。 |
331
+ | `variables` | 可用模板变量。 |
332
+
333
+ 组件接收独立的 value、body 和 variables 快照,可以使用 Hook;异步业务自行处理 loading、error/retry、empty 和卸载取消。图片使用统一 `pickImage` 适配器时已有相应状态处理:返回 null 取消、抛错显示重试、卸载时取消 signal 并忽略过期结果。列表异步更新按稳定行标识合并到最新数据,保留等待期间的其它编辑及排序,已删除的行不再接收结果。
334
+
335
+ ### Canvas / Inspector
336
+
337
+ ```tsx
338
+ import {
339
+ ControlEditor, EditableRichText,
340
+ type ControlValues, type PluginEditorProps,
341
+ } from "@standhigher/react-email-editor";
342
+
343
+ const cardControls = {
344
+ title: EmailControls.text({ label: "标题", defaultValue: "新品" }),
345
+ description: EmailControls.richText({ label: "说明" }),
346
+ footer: EmailControls.richText({ label: "页脚" }),
347
+ };
348
+ type CardValues = ControlValues<typeof cardControls>;
349
+
350
+ function CardCanvas({ controls }: PluginEditorProps<CardValues>) {
351
+ return <div>
352
+ <strong>{controls.title}</strong>
353
+ <EditableRichText control="description" />
354
+ <EditableRichText control="footer" />
355
+ </div>;
356
+ }
357
+
358
+ function CardInspector() {
359
+ return <>
360
+ <ControlEditor control="title" />
361
+ <ControlEditor control="description" />
362
+ <ControlEditor control="footer" />
363
+ </>;
364
+ }
365
+
366
+ const card = defineBlock({
367
+ type: "my-app.card", title: "卡片", controls: cardControls,
368
+ Canvas: CardCanvas, Inspector: CardInspector,
369
+ renderEmail: ({ controls, email }) => email.html`
370
+ <h2>${controls.title}</h2>
371
+ ${email.richText(controls.description)}
372
+ ${email.richText(controls.footer)}
373
+ `,
374
+ });
375
+ ```
376
+
377
+ `Canvas` / `Inspector` 接收 `PluginEditorProps<ControlValues<Schema>>`:
378
+
379
+ | Props | 含义和作用 |
380
+ | --- | --- |
381
+ | `block/controls/style/body` | 节点、归一化控件值、区块样式及主体配置的快照;通过更新方法提交变化。 |
382
+ | `variables` | 模板变量。 |
383
+ | `selected` | 当前区块是否选中;Inspector 中为 true。 |
384
+ | `renderedHtml` | Canvas 中为已解析模板的内部 HTML;Inspector 中为空字符串。 |
385
+ | `select()` | 请求选中区块;Inspector 中无需再次选择。 |
386
+ | `setControl(name, value)` | 类型安全地修改单个控件值。 |
387
+ | `setControls(patch)` | 浅合并多个控件值,一次文档更新、一次 onChange;嵌套对象提交完整新值。 |
388
+ | `setStyle(id, value)` | 按样式路径修改,例如 `setStyle("dimension.padding", "12px")`。 |
389
+
390
+ `ControlEditor` 只接收 `control: string`,复用已经声明的控件、校验和资源适配器。`EditableRichText` 接收必填 `control`、可选 `profile/placeholder`,自动接入控件更新、选区、工具栏、变量及主体排版,只能绑定 richText 控件。两者只在区块 Canvas / Inspector 内使用。
391
+
392
+ 单个 `richText({ inline: true })` 会让默认画布整体使用该编辑区域,并从默认检查器中移除该控件;它不会把富文本插入 renderEmail 布局中的某个位置。多个 inline 区域必须声明 Canvas,否则定义时报错。声明 Canvas 后编辑区域完全由该组件组合。
393
+
394
+ ## 5. 样式控件与检查器分组
395
+
396
+ ### 示例
397
+
398
+ ```ts
399
+ import { createDimensionControls } from "@standhigher/react-email-editor";
400
+
401
+ const spacing = createDimensionControls({ padding: { defaultValue: "12px" } });
402
+ // 下面的配置可传给 defineBlock:
403
+ const layoutBlock = defineBlock({
404
+ type: "my-app.layout",
405
+ title: "布局",
406
+ controls: { title: EmailControls.text({ label: "标题" }) },
407
+ containerStyles: false,
408
+ styleControls: [spacing.padding, spacing.margin],
409
+ inspectorSections: [{
410
+ title: "内容和间距",
411
+ controlNames: ["title"],
412
+ styleIds: ["dimension.padding", "dimension.margin"],
413
+ }],
414
+ renderEmail: ({ controls, email }) => email.html`<p>${controls.title}</p>`,
415
+ });
416
+ ```
417
+
418
+ `createDimensionControls()`、`createDecorationControls()`、`createTypographyControls()` 返回按能力名称索引的样式配置对象。可传入按名称索引的覆盖配置,再挑选需要的项放入 styleControls。
419
+
420
+ | StyleControlConfig 字段 | 作用 |
421
+ | --- | --- |
422
+ | `id` | 样式存取路径,例如 `dimension.padding`、`typography.fontSize`、`element.width`。 |
423
+ | `group` | `general/dimension/typography/decorations`,决定默认检查器分组。 |
424
+ | `label` | 控件名称。 |
425
+ | `control` | `text/color/alignment/select/number/range/spacing/corners`。 |
426
+ | `options` | 下拉选项。 |
427
+ | `unit/units` | 单位后缀或可选单位列表;单位应符合邮件 CSS 的限制。 |
428
+ | `min/max/step` | 数字、滑块等控件参数。 |
429
+ | `defaultValue` | 字符串默认样式值,低于 defaultStyle 优先级。 |
430
+ | `placeholder` | 输入提示。 |
431
+
432
+ `id/group/label/control` 必填,其余可选。
433
+
434
+ | BlockInspectorSection 字段 | 作用 |
435
+ | --- | --- |
436
+ | `title` | 必填分组标题。 |
437
+ | `controlNames` | 内容控件名称,按顺序显示。 |
438
+ | `styleIds` | 样式控件 ID,按顺序显示。 |
439
+ | `advancedStyleIds` | 放进折叠区的样式控件 ID。 |
440
+
441
+ 主体配置存于 body;容器样式存于 style.dimension/decorations;按钮、图片等元素样式存于 style.element;显式富文本格式保存在 controls 中的 HTML 字符串。只读取区块启用的样式。自定义元素的样式需要在 renderEmail 中实际应用。
442
+
443
+ ## 6. 邮件渲染工具:email
444
+
445
+ ```ts
446
+ renderEmail: ({ controls, email }) => email.html`
447
+ <p>${controls.title}</p>
448
+ ${email.richText(controls.description)}
449
+ ${email.button({ href: controls.url, label: "查看详情" })}
450
+ `
451
+ ```
452
+
453
+ | 方法 | 作用 |
454
+ | --- | --- |
455
+ | `email.html` 标签模板 | 转义普通插值,支持嵌入 EmailMarkup 片段及片段数组;Go template token 保留。 |
456
+ | `email.raw(html)` | 显式插入原始 HTML,不消毒。 |
457
+ | `email.richText(html)` | 保留富文本显式格式,补充主体默认段落、列表和链接样式。 |
458
+ | `email.table(rows)` | 由 EmailMarkup 二维数组生成邮件表格。 |
459
+ | `email.image({ src, alt?, width? })` | 图片输出,校验协议、归一化宽度,默认 100%。 |
460
+ | `email.button({ href, label })` | 邮件表格按钮,校验协议,空或无效地址不生成链接。 |
461
+ | `email.url(value, image?)` | 返回允许的 URL,无效时返回空字符串;image 默认为 false。 |
462
+
463
+ 链接允许 http/https/mailto/tel、锚点或模板;图片允许 http/https/cid 或模板。自行拼接属性时先用 email.url() 校验协议,再进行属性转义。返回普通字符串时转义由插件负责;原始 HTML、富文本和模板预览均不是任意不可信内容的安全沙箱。
464
+
465
+ ## 7. 文档、错误与公开类型
466
+
467
+ 节点使用 `EmailBlockNode`,包含必填 id、type、controls,以及可选 style、defaultData。文档使用 `EmailDocument`,包含 version、body、blocks 和可选 subject。保存 JSON 后重新加载时,仍需注册同 type 的插件,JSON 不保存组件或渲染函数。
468
+
469
+ 当前文档版本为 **4**。全链路使用 controls,不提供旧 API 别名、旧数据兼容或自动迁移;不支持的版本明确报错。请用 createEmailDocument() 和 config.createBlock() 创建新数据,不要只修改旧 JSON 的版本号。
470
+
471
+ 缺少插件时画布显示占位并保留原始节点,邮件导出报错。Canvas、Inspector 及画布渲染异常按区块隔离并提供重试;邮件渲染错误包含区块 type、id 和原因。getData()/getDocument() 和 onChange/onBodyChange 参数均为独立快照。
472
+
473
+ | 公开类型 | 用途 |
474
+ | --- | --- |
475
+ | `ControlKind/ControlDefinition<T>/ControlOptions<T>` | 控件类型、工厂返回值与参数。 |
476
+ | `ControlSchema/ControlValues<Schema>` | 控件 schema 与推导后的值对象。 |
477
+ | `ControlComponentProps<T>` | 单控件 React 编辑组件。 |
478
+ | `BlockPlugin/BlockPluginOptions` | 区块定义及声明参数。 |
479
+ | `PluginEditorProps/PluginRenderContext` | 编辑组件 Props / 邮件渲染上下文。 |
480
+ | `EmailEditorConfig/UseEmailEditorConfigOptions` | 注册配置及参数。 |
481
+ | `ImagePickerContext/ValidationIssue` | 图片资源适配器 / 校验结果。 |
482
+ | `ControlOption/StyleControlConfig/BlockInspectorSection` | 选项 / 样式控件 / 检查器分组。 |
483
+ | `ControlEditorProps/EditableRichTextProps` | 控件复用和富文本编辑组件 Props。 |
484
+ | `EmailMarkup/EmailTools` | 邮件片段及工具集。 |
485
+
486
+ 编辑器 Hooks、实例方法、body 和 variables 的完整接入见 [API 参考](./api.zh-CN.md),业务集成见[复杂场景](./advanced-examples.zh-CN.md)。可运行示例见 [优惠券](../playground/plugins/coupon.ts)、[商品卡片](../playground/plugins/product.ts)及[统一配置](../playground/plugins/config.ts)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@standhigher/react-email-editor",
3
- "version": "0.2.0",
3
+ "version": "0.4.1",
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": [
@@ -29,6 +29,7 @@
29
29
  "types": "./dist/index.d.ts",
30
30
  "files": [
31
31
  "dist",
32
+ "docs",
32
33
  "README.md",
33
34
  "README.zh-CN.md",
34
35
  "CHANGELOG.md"
@@ -57,6 +58,7 @@
57
58
  "oxlint": "^1.90.0",
58
59
  "react": "^18.3.1",
59
60
  "react-dom": "^18.3.1",
61
+ "react-test-renderer": "18.3.1",
60
62
  "tsup": "^8",
61
63
  "typescript": "^5",
62
64
  "vite": "^8.3.5"