@standhigher/react-email-editor 0.1.0 → 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # 更新日志
2
+
3
+ ## 0.2.0 — 2026-10-27
4
+
5
+ ### 不兼容变更
6
+
7
+ - 文档格式由 `version: 1` 升为 `version: 2`,旧版本会被拒绝,不提供自动迁移。请使用 `createEmailDocument()` 创建当前版本文档;升级已有数据时,需同时调整区块字段和样式,不能只修改版本号。
8
+ - Heading、Text、Button 的文字格式统一存于 `fields.html`;Button 不再读取 `fields.label` 和 `style.element` 中的文字样式。`EmailElementStyle` 不再继承 `EmailTypographyStyle`。
9
+ - 内置区块按各自能力声明 `styleFields`,不再读取未启用的样式;移除内置区块的 `dimension.height` 和区块级 `typography` 配置。自定义区块仍可显式启用这些字段,`exactStyleFields` 同时约束默认样式、画布和邮件输出。
10
+ - HTML 区块保留源码内部的段落、列表、链接和显式样式,不再注入正文排版;未声明的属性仍可继承全局样式。
11
+
12
+ ### 新增与改进
13
+
14
+ - Button 支持画布内富文本编辑,采用单段内容,粘贴多段时以空格连接;邮件输出保留基础行内格式,并只生成一个按钮外层链接。
15
+ - 新增 `BlockInspectorSection`、`inspectorSections`、`RichTextProfile` 和 `inlineEditProfile`,支持自定义检查器分组与富文本编辑能力;独立 `RichText` 支持 `profile`。
16
+ - 统一颜色面板、HEX 输入和颜色重置;链接编辑改为居中弹窗,支持颜色、下划线和打开方式。
17
+ - 优化区块操作栏、拖拽反馈、三栏布局、工具栏换行和预览高度;预览跟随文档内容展示。
18
+ - 按钮默认填充改为黑色,按钮和图片的链接默认为空,空白链接不生成跳转;全局内容、区块、按钮和图片边框默认透明。
19
+ - 精简中英文 README,补充 npm keywords,发布包包含中文 README 和更新日志。
20
+
21
+ ### 修复与验证
22
+
23
+ - 修复富文本格式操作后的光标、选区和格式保留,以及列表切换时的间距和缩进保留。
24
+ - 修复按钮导出对模板表达式中引号、比较符、注释和空格的处理。
25
+ - 接入 Oxlint,补充区块配置、富文本、列表、按钮和颜色转换回归测试。
26
+
27
+ ## 0.1.0 — 2026-10-27
28
+
29
+ 首个正式发布版本(对应提交 `48011d9`)。
30
+
31
+ - 提供 React 18 编辑器、拖拽区块、富文本编辑、检查器及桌面/移动预览。
32
+ - 支持 JSON 文档、全局和区块样式、HTML 源码编辑、自定义区块与 Go template 预览。
33
+ - 提供组件 ref 数据读取、独立 HTML 渲染、ESM、TypeScript 类型和独立 CSS 入口。
34
+ - 提供 Vite playground、构建和本地 link 联调流程。
35
+
36
+ ## 0.0.0-stage — 2026-10-27
37
+
38
+ - 发布前的包分发验证版本。
package/README.md CHANGED
@@ -2,244 +2,131 @@
2
2
 
3
3
  English | [简体中文](./README.zh-CN.md)
4
4
 
5
- A React email editor with drag-and-drop blocks, inline rich text, and desktop/mobile previews. Store content as JSON and export email HTML through a component ref.
5
+ A React email template editor with drag-and-drop blocks, inline rich text, desktop/mobile previews, JSON storage, and HTML export.
6
6
 
7
- ## Installation and integration
7
+ ## Installation
8
8
 
9
- Requires React 18 and React DOM 18. The package provides an ESM entry and TypeScript declarations.
9
+ Requires React 18 and React DOM 18. Includes ESM and TypeScript declarations.
10
10
 
11
11
  ```bash
12
12
  pnpm add @standhigher/react-email-editor
13
13
  ```
14
14
 
15
- Import the stylesheet once in your application. This example renders an empty editor and reads its current data:
15
+ ## Quick start
16
+
17
+ Import the stylesheet once in your application and use a ref to read the document and HTML. Your application handles persistence.
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.fields.html = "<p>Hello, {{ store_name }}</p>";
33
+ const initialDocument = createEmailDocument(text ? [text] : []);
34
+ const variables: Variable[] = [
35
+ { name: "{{ store_name }}", description: "Store name", value: "Example Store" },
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
- // Pass data.document, data.json, data.html, or data.renderedHtml
32
- // to your application's save or export handler.
44
+ // Persist data.document in your app; use data.html to export the template.
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}>Read data</button>
50
+ <EmailEditor
51
+ ref={editorRef}
52
+ initialDocument={initialDocument}
53
+ variables={variables}
54
+ height={720}
55
+ />
56
+ <button type="button" onClick={save}>Save</button>
40
57
  </>
41
58
  );
42
59
  }
43
60
  ```
44
61
 
45
- Saving is handled by your application. In Next.js App Router, put this integration in a Client Component with `"use client"`. When using `height="100%"`, give its parent a definite height.
62
+ Omit `initialDocument` to start empty. In Next.js App Router, add `"use client"` to the integration component and import CSS from an allowed global stylesheet entry. When using `height="100%"`, give the parent a definite height.
46
63
 
47
- ## Local development
48
-
49
- Use pnpm and a current Node.js LTS release; Node.js 22.18+ is recommended for the repository's tooling and TypeScript tests.
50
-
51
- ```bash
52
- pnpm install --frozen-lockfile
53
- pnpm dev
54
- ```
55
-
56
- Open [http://localhost:5173](http://localhost:5173). The playground loads `src/` directly with hot reload; no initial build is needed. It is not included in the published package. Reuse an existing development server when one is already running.
57
-
58
- | Command | Purpose |
59
- | --- | --- |
60
- | `pnpm dev` | Start the local playground on port 5173. |
61
- | `pnpm build` | Generate ESM, type declarations, and CSS in `dist/`. |
62
- | `pnpm build:watch` | Rebuild `dist/` when files in `src/` change; does not start a preview server. |
63
- | `pnpm typecheck` | Type-check the library and playground. |
64
+ ## Props
64
65
 
65
- To integrate with another local project, set its dependency to the library's root directory, adjusting the relative path as needed:
66
+ All props are optional.
66
67
 
67
- ```json
68
- {
69
- "dependencies": {
70
- "@standhigher/react-email-editor": "link:../react-email-editor"
71
- }
72
- }
73
- ```
74
-
75
- Run `pnpm build` in this repository first, then keep `pnpm build:watch` running during development. In the consuming project, run `pnpm install` and use its development server. Package imports, including CSS, resolve to `dist/`.
76
-
77
- Both projects must resolve the same React instance. For Vite consumers, use `resolve.dedupe: ["react", "react-dom"]` if needed. Next.js consumers can use `transpilePackages: ["@standhigher/react-email-editor"]`; when an external link is used with Turbopack, `turbopack.root` must include both projects.
78
-
79
- ## Component props
80
-
81
- All props are optional. Types are exported from `@standhigher/react-email-editor`.
82
-
83
- | Prop | Type | Default / behavior |
68
+ | Prop | Type | Behavior |
84
69
  | --- | --- | --- |
85
- | `initialDocument` | `EmailDocument` | Empty document. Read only on the first mount. |
86
- | `global` | `Partial<EmailGlobalSettings>` | No external overrides. Supplied fields override the initial document's global settings; later updates merge into the current document. |
87
- | `blocks` | `BlockDefinition[]` | `[]`. Adds custom block types; matching types replace built-in definitions. Other built-in blocks remain available. |
88
- | `variables` | `Variable[]` | `[]`. Variables available for insertion and preview substitution. |
89
- | `height` | `number \| string` | `"100%"`. A number is interpreted as pixels; a string is a CSS height. |
90
- | `onChange` | `(document: EmailDocument) => void` | Called when the document changes, including edits, reset, and external global updates that change its values. |
91
- | `onGlobalChange` | `(global: EmailGlobalSettings) => void` | Called when global settings are edited in the editor. External `global` updates and reset do not call it. |
92
-
93
- Reset restores the initial document snapshot and reapplies the current `global` overrides. To load a different document, remount with a new React `key`, for example `<EmailEditor key={documentId} initialDocument={document} />`. Changing `initialDocument` alone does not replace the open document. Omitting a previously supplied `global` field does not restore its default.
70
+ | `initialDocument` | `EmailDocument` | Read on first mount only. Remount with a new React `key` to switch documents. |
71
+ | `height` | `number \| string` | Defaults to `"100%"`; numbers are pixels. |
72
+ | `global` | `Partial<EmailGlobalSettings>` | Overrides global styles; later changes merge into the current document. Omitting a field does not restore its default. |
73
+ | `variables` | `Variable[]` | Variables for insertion and preview. Defaults to `[]`; Personalization controls are hidden when empty. |
74
+ | `blocks` | `BlockDefinition[]` | Adds custom blocks; matching types replace built-in definitions. |
75
+ | `onChange` | `(document: EmailDocument) => void` | Called on document changes, including external `global` updates that change document values. |
76
+ | `onGlobalChange` | `(global: EmailGlobalSettings) => void` | Called for global style edits inside the editor, excluding external `global` updates. |
94
77
 
95
- ## Configuration
78
+ Global style example: `global={{ contentWidth: 640, outerPadding: "24px" }}`. Defaults are exported as `DEFAULT_EMAIL_GLOBAL`.
96
79
 
97
- ### Document
80
+ Built-in blocks: `heading`, `text`, `button`, `image`, `divider`, `spacer`, and `html`, available through `BLOCK_TYPES`.
98
81
 
99
- Use `createEmailDocument()` and `createBlockNode()` to populate defaults:
100
-
101
- ```tsx
102
- import {
103
- BLOCK_TYPES,
104
- EmailEditor,
105
- createBlockNode,
106
- createEmailDocument,
107
- type Variable,
108
- } from "@standhigher/react-email-editor";
82
+ ## Save and export
109
83
 
110
- const text = createBlockNode(BLOCK_TYPES.text);
111
- if (text) text.fields.html = "<p>Hello {{ store_name }}</p>";
112
-
113
- export const initialDocument = createEmailDocument(text ? [text] : []);
114
- initialDocument.subject = "Welcome";
115
-
116
- const variables: Variable[] = [
117
- { name: "{{ store_name }}", description: "Store name", value: "Demo Store" },
118
- ];
119
-
120
- export function TemplateEmailPage() {
121
- return (
122
- <EmailEditor
123
- initialDocument={initialDocument}
124
- variables={variables}
125
- height={720}
126
- />
127
- );
128
- }
129
- ```
130
-
131
- Pass both `initialDocument` and `variables` as shown so the token has a preview value and can gain its missing dot on export. `createBlockNode()` returns `null` for an unknown block type. Built-in types are `heading`, `text`, `button`, `image`, `divider`, `spacer`, and `html`, also available through `BLOCK_TYPES`.
132
-
133
- | Document field | Type | Description |
134
- | --- | --- | --- |
135
- | `version` | `typeof DOCUMENT_VERSION` | Current version: `1`. Set by `createEmailDocument()`. |
136
- | `subject` | `string` (optional) | Email subject. The editor input is limited to 128 characters. |
137
- | `global` | `EmailGlobalSettings` | Global email appearance; defaults are populated by the helper. |
138
- | `blocks` | `EmailBlockNode[]` | Ordered nodes, each with a unique `id`, a `type`, and `fields`, plus optional `style` and preview `defaultData`. |
139
-
140
- The document format is under development; older block fields are not migrated automatically. See the [document types](./src/types/document.ts) for the full model.
141
-
142
- ### Global styles
143
-
144
- Pass partial settings through `global`, for example `<EmailEditor global={{ contentWidth: 640, outerPadding: "24px" }} />`. `DEFAULT_EMAIL_GLOBAL` exposes all defaults; `resolveEmailGlobal()` fills missing values and normalizes input.
145
-
146
- These settings control email content. Editor selection outlines and toolbars are not included in exported HTML.
147
-
148
- | Parameter | Type / values | Default | Description |
149
- | --- | --- | --- | --- |
150
- | `contentWidth` | `number` | `600` | Content width in pixels, rounded and clamped to 320–900. |
151
- | `contentAlign` | `"left" \| "center" \| "right"` | `"center"` | Horizontal position of the content. |
152
- | `backgroundColor` | `string` | `"#ffffff"` | Outer email background. |
153
- | `contentBackgroundColor` | `string` | `"#ffffff"` | Content background. |
154
- | `contentPadding` | `string` | `"0px"` | Content inner spacing. |
155
- | `outerPadding` | `string` | `"0px"` | Spacing outside the content. |
156
- | `blockGap` | `string` | `"4px"` | Gap between blocks. |
157
- | `textColor` | `string` | `"#000000"` | Default text color. |
158
- | `textAlign` | `"left" \| "center" \| "right" \| "justify"` | `"left"` | Default text alignment. |
159
- | `fontFamily` | `string` | `"Arial, Helvetica, sans-serif"` | Default font stack. |
160
- | `fontWeight` | `"normal" \| "bold"` | `"normal"` | Default font weight. |
161
- | `defaultFontSize` | `string` | `"16px"` | Default font size. |
162
- | `defaultLineHeight` | `string` | `"1.5"` | Default line height. |
163
- | `paragraphSpacing` | `string` | `"6px"` | Gap between adjacent top-level paragraphs. |
164
- | `listSpacing` | `string` | `"4px"` | Top and bottom margins of the entire list (`ul` / `ol`). |
165
- | `listIndent` | `string` | `"22px"` | List indentation. |
166
- | `linkColor` | `string` | `"#005bd3"` | Default link color. |
167
- | `linkDecoration` | `"underline" \| "none"` | `"underline"` | Default link decoration. |
168
- | `borderWidth` | `string` | `"0px"` | Content border width. |
169
- | `borderStyle` | `"none" \| "solid" \| "dashed" \| "dotted"` | `"solid"` | Content border style. |
170
- | `borderColor` | `string` | `"#000000"` | Content border color. |
171
- | `borderRadius` | `string` | `"0px"` | One radius or four, ordered top-left, top-right, bottom-right, bottom-left. |
172
-
173
- Use CSS length strings such as `"16px"` for spacing and sizes. Padding accepts shorthand such as `"12px 24px"`; line height also accepts a unitless value. Desktop canvas and preview follow `contentWidth`; mobile uses a 360px viewport.
174
-
175
- Global spacing and length settings accept non-negative values in `px`, `em`, `rem`, or `%`; unitless lengths are normalized to px, except for line height. Unsupported values such as negative lengths, `calc()`, `var()`, `vh`, or `vw` fall back to the corresponding global default.
176
-
177
- ### Variables and template data
178
-
179
- ```tsx
180
- import type { Variable } from "@standhigher/react-email-editor";
181
-
182
- export const variables: Variable[] = [
183
- { name: "{{ store_name }}", description: "Store name", value: "Demo Store" },
184
- ];
84
+ | Ref method | Returns |
85
+ | --- | --- |
86
+ | `getData()` | `{ document, json, html, renderedHtml }` |
87
+ | `getDocument()` | An independent `EmailDocument` snapshot to persist and pass back as `initialDocument`. |
88
+ | `getJson()` | A formatted document JSON string. |
89
+ | `getHtml()` | A complete email HTML document preserving template expressions. |
90
+ | `getRenderedHtml()` | A complete email HTML document with templates resolved using preview data. |
91
+
92
+ Render a document without mounting the editor:
93
+
94
+ ```ts
95
+ import { documentToHtml } from "@standhigher/react-email-editor";
96
+
97
+ const html = documentToHtml(initialDocument, undefined, { variables });
98
+ const previewHtml = documentToHtml(initialDocument, undefined, {
99
+ variables,
100
+ resolveHtmlTemplates: true,
101
+ });
185
102
  ```
186
103
 
187
- Pass this array through `variables` on `EmailEditor`.
104
+ A variable's `name` is inserted verbatim; `description` and preview `value` are optional. Export adds a missing dot only to tokens matching `variables[].name` exactly, preserving whitespace: `{{ store_name }}` → `{{ .store_name }}`. Other Go template expressions remain unchanged. Preview supports a subset of Go template syntax; data precedence is block `defaultData` > `templateData` (`documentToHtml` option) > `variables`.
188
105
 
189
- | Field | Type | Description |
190
- | --- | --- | --- |
191
- | `name` | `string` | Required. Token displayed and inserted as written, including spaces. Example: `{{ store_name }}`. |
192
- | `description` | `string` (optional) | Secondary text in the variable menu; omitted when empty or absent. |
193
- | `value` | `string` (optional) | Replacement text used in previews. |
194
-
195
- Tokens may include a leading dot. Template HTML export preserves spaces and adds a missing dot only when the token exactly matches `variables[].name`, including spaces: `{{ store_name }}` becomes `{{ .store_name }}`. Other Go template actions are left unchanged. Export does not substitute preview values by default.
196
-
197
- Use a block node's `defaultData: Record<string, unknown>` for structured preview data, including arrays for `{{range .products}}`. Block data overrides variables with the same name. Preview resolution applies to every block's rendered HTML, including custom blocks. Write template expressions directly in an HTML block's `fields.html`; the preview renderer supports a subset of Go template syntax.
106
+ The document format is under development. Only `version: 2` is accepted, with no automatic migration of older documents. Create data with `createEmailDocument()` and `createBlockNode()`. See [CHANGELOG](./CHANGELOG.md) for upgrade notes.
198
107
 
199
- ### Block configuration
108
+ ## Custom blocks
200
109
 
201
- Block node styles are grouped by purpose:
202
-
203
- | Style group | Purpose |
204
- | --- | --- |
205
- | `style.dimension` | Container `padding`, outer spacing via `margin`, and optional `height` in px. Height acts as a minimum including padding. |
206
- | `style.decorations` | Container background, border, and corner radius. |
207
- | `style.typography` | Block text styles overriding global typography. |
208
- | `style.element` | Element-specific styles for buttons, images, and dividers, such as size and alignment. |
110
+ Pass `BlockDefinition[]` through `blocks`. Required fields are `type`, `title`, and `render(ctx)`, which returns HTML. Use `fields` / `styleFields` for inspector controls and `defaultFields` / `defaultStyle` / `defaultData` for initial values. The render context includes `mode: "canvas" | "email"`.
209
111
 
210
- Heading and Text blocks start with `16px` padding. HTML blocks retain explicitly authored inline styles. See [EmailBlockStyle](./src/types/document.ts) for individual fields.
112
+ Built-in text formatting lives in `fields.html`, container styles in `style.dimension` / `style.decorations`, and button, image, and divider element styles in `style.element`. Rendering uses only enabled style fields. See [BlockDefinition](https://github.com/standhigher/react-email-editor/blob/main/src/types/block.ts) and [EmailDocument](https://github.com/standhigher/react-email-editor/blob/main/src/types/document.ts) for full types. When calling `documentToHtml` directly, pass the complete definition list including custom blocks as its second argument.
211
113
 
212
- To add a custom block, pass a stable `BlockDefinition[]` through `blocks`. `type`, `title`, and `render(ctx)` are required. Optional `fields` and `styleFields` configure the inspector; `defaultFields`, `defaultStyle`, and `defaultData` initialize new nodes. `render(ctx)` returns HTML and receives `mode: "canvas" | "email"`.
213
-
214
- ```tsx
215
- import { EmailEditor, type BlockDefinition } from "@standhigher/react-email-editor";
114
+ ## Local development
216
115
 
217
- const customBlocks: BlockDefinition[] = [{
218
- type: "notice",
219
- title: "Notice",
220
- defaultStyle: { dimension: { padding: "16px" } },
221
- render: () => '<p style="margin:0;color:#555555;">Thanks for subscribing.</p>',
222
- }];
116
+ Node.js 22.18+ and pnpm are recommended:
223
117
 
224
- export function CustomEmailPage() {
225
- return <EmailEditor blocks={customBlocks} height={720} />;
226
- }
118
+ ```bash
119
+ pnpm install --frozen-lockfile
120
+ pnpm dev
227
121
  ```
228
122
 
229
- By default, common container spacing and decoration controls are added to custom blocks. Set `exactStyleFields: true` to show only the supplied style fields; existing node styles still apply when rendering. Full definitions: [BlockDefinition, FieldConfig, and StyleFieldConfig](./src/types/block.ts).
230
-
231
- ## Reading and exporting data
232
-
233
- Call these methods through `useRef<EmailEditorHandle>(null)`:
123
+ Open [http://localhost:5173](http://localhost:5173). The playground loads `src/` directly; reuse an existing server. Validation:
234
124
 
235
- | Method | Return type | Description |
236
- | --- | --- | --- |
237
- | `getData()` | `EmailEditorData` | Returns `{ document, json, html, renderedHtml }`. |
238
- | `getDocument()` | `EmailDocument` | Independent document snapshot for storage or later editing. |
239
- | `getJson()` | `string` | Formatted document JSON. |
240
- | `getHtml()` | `string` | Complete email HTML. Template actions stay in place; only tokens from `variables` gain a missing dot. |
241
- | `getRenderedHtml()` | `string` | Complete email HTML with templates resolved using variables and block preview data. |
242
-
243
- Mutating a returned document snapshot does not change the editor. Use `onChange` to subscribe to document changes.
125
+ ```bash
126
+ pnpm typecheck
127
+ pnpm lint
128
+ pnpm build
129
+ node --test tests/*.test.mjs tests/*.test.ts
130
+ ```
244
131
 
245
- For rendering without mounting the editor, use `documentToHtml(document, definitions?, options?)`. Definitions default to `DEFAULT_BLOCKS`; include custom definitions when the document uses them. `options` accepts `resolveHtmlTemplates` (default `false`), `variables`, and `templateData`. When templates stay unresolved, `variables` supplies the only tokens that gain a missing dot. When resolving, data precedence is block `defaultData` over `templateData` over `variables`.
132
+ For local consumers, use `link:../react-email-editor`, run `pnpm build` first, and keep `pnpm build:watch` running 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's `root` covering the linked directory.