@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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @standhigher/react-email-editor
2
2
 
3
- English | [简体中文](./README.zh-CN.md)
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
- ![Project modules and features](./docs/assets/architecture.png)
9
+ ![Project modules and features](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.png)
10
10
 
11
- [Open full-size architecture diagram](./docs/assets/architecture.png) · [Editable SVG](./docs/assets/architecture.svg)
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 a supported Node.js LTS release (at least 22.18 for the complete test suite) and pnpm:
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](./docs/api.md#development-and-local-linking) for build and validation commands.
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](./docs/api.md): every public export, parameter, default and type.
47
- - [Basic examples](./docs/basic-examples.md): initial content, saving/loading, body styles, variables and preview.
48
- - [Advanced examples](./docs/advanced-examples.md): validated saving, multiple instances, custom blocks, multiple editing regions, resource adapters and standalone export.
49
- - [Custom block guide](./docs/custom-blocks.md): detailed controls, plugins and rendering helpers.
50
- - [Changelog](./CHANGELOG.md).
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](./README.md) | 简体中文
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
- ![项目模块架构图:8 个模块及对应功能](./docs/assets/architecture.zh-CN.png)
9
+ ![项目模块架构图:8 个模块及对应功能](https://cdn.jsdelivr.net/npm/@standhigher/react-email-editor@0.4.1/docs/assets/architecture.zh-CN.png)
10
10
 
11
- [查看高清架构图](./docs/assets/architecture.zh-CN.png) · [可编辑 SVG](./docs/assets/architecture.zh-CN.svg)
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
- 使用依赖支持的 Node.js LTS(运行全部测试至少需要 22.18)和 pnpm:
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 文档的开发说明](./docs/api.zh-CN.md#开发与本地联调)。
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 参考](./docs/api.zh-CN.md):所有公共导出、参数、默认值与类型。
47
- - [基础场景](./docs/basic-examples.zh-CN.md):初始内容、保存恢复、主体样式、变量及预览。
48
- - [复杂场景](./docs/advanced-examples.zh-CN.md):校验后保存、多实例、自定义区块、多区域编辑、资源适配与独立导出。
49
- - [自定义区块指南](./docs/custom-blocks.zh-CN.md):控件、插件和渲染工具的完整说明。
50
- - [更新记录](./CHANGELOG.md)。
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 useId10 } from "react";
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 useRef9, useId as useId6 } from "react";
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/inspector/VariablesDropdown.tsx
4083
- import { useState as useState12 } from "react";
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 VariablesDropdown({ variables, onInsert }) {
4086
- const [open, setOpen] = useState12(false);
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-var-wrap", children: [
4089
- /* @__PURE__ */ jsx40(
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-var-btn",
4094
- onClick: () => setOpen((current) => !current),
4095
- title: "Insert variable",
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
- children: "{ }"
4099
- }
4100
- ),
4101
- open ? /* @__PURE__ */ jsx40("div", { className: "ree-var-menu", children: variables.map((variable) => /* @__PURE__ */ jsxs29(
4102
- "button",
4103
- {
4104
- type: "button",
4105
- className: "ree-var-item",
4106
- onClick: () => {
4107
- onInsert(variable.name);
4108
- setOpen(false);
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
- /* @__PURE__ */ jsx40("span", { className: "ree-var-name", children: variable.name }),
4112
- variable.description ? /* @__PURE__ */ jsx40("span", { className: "ree-var-label", children: variable.description }) : null
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
- variable.name
4116
- )) }) : null
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 = useRef9(null);
4125
- const id = useId6();
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__ */ jsxs30("div", { className: "ree-field-label", children: [
4144
- /* @__PURE__ */ jsxs30("label", { htmlFor: id, children: [
4145
- control.label,
4146
- control.unit ? ` (${control.unit})` : ""
4147
- ] }),
4148
- textual ? /* @__PURE__ */ jsx41(VariablesDropdown, { variables, onInsert: insertVariable }) : null
4149
- ] }),
4150
- 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) => {
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 useId7 } from "react";
4272
+ import { useId as useId8 } from "react";
4160
4273
 
4161
4274
  // src/inspector/components/useListRows.ts
4162
- import { useLayoutEffect as useLayoutEffect7, useRef as useRef10, useState as useState13 } from "react";
4275
+ import { useLayoutEffect as useLayoutEffect8, useRef as useRef11, useState as useState14 } from "react";
4163
4276
  function useListRows(value, onChange) {
4164
- const [state, setState] = useState13({ source: void 0, rows: [], nextKey: 0 });
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 = useRef10(state);
4177
- useLayoutEffect7(() => {
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 = useId7();
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 useId8, useRef as useRef11, useState as useState14 } from "react";
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 = useRef11(null);
4248
- const id = useId8();
4249
- const [loading, setLoading] = useState14(false);
4250
- const [error, setError] = useState14("");
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 useId9 } from "react";
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 = useId9();
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 = useId10();
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 useId12, useMemo as useMemo3, useState as useState15 } from "react";
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 useRef12 } from "react";
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 = useRef12(null);
4778
- const frameRef = useRef12(null);
4779
- const cleanupRef = useRef12();
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 useId11 } from "react";
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 = useId11();
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 = useId12();
5007
- const [toolbarCollapsed, setToolbarCollapsed] = useState15(false);
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 useRef13 } from "react";
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 = useRef13(null);
5229
- const cleanupRef = useRef13();
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 useId13, useRef as useRef15 } from "react";
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 jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
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 = useId13();
5491
- const countId = useId13();
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__ */ jsxs44("div", { className: "ree-subject-bar", children: [
5510
- /* @__PURE__ */ jsx57("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
5511
- /* @__PURE__ */ jsxs44("div", { className: "ree-subject-field", children: [
5512
- /* @__PURE__ */ jsx57(
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__ */ jsxs44("span", { id: countId, className: "ree-subject-count", children: [
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__ */ jsxs44(Fragment5, { children: [
5531
- /* @__PURE__ */ jsx57(PersonalizationMenu, { variables, onInsert: insertVariable }),
5532
- /* @__PURE__ */ jsx57("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
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 jsx58, jsxs as jsxs45 } from "react/jsx-runtime";
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__ */ jsx58(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx58(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs45("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
5939
- /* @__PURE__ */ jsx58(
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__ */ jsxs45("div", { className: "ree-body", children: [
5948
- /* @__PURE__ */ jsxs45(
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__ */ jsx58(
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__ */ jsx58(
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__ */ jsx58(
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 jsx59 } from "react/jsx-runtime";
6064
+ import { jsx as jsx58 } from "react/jsx-runtime";
6005
6065
  function EmailEditor(props) {
6006
- return /* @__PURE__ */ jsx59(EmailEditorShell, __spreadValues({}, props));
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 useEffect9, useInsertionEffect, useLayoutEffect as useLayoutEffect8, useState as useState19 } from "react";
6018
- var useOptionsEffect = typeof window === "undefined" ? useEffect9 : useLayoutEffect8;
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 jsx60 } from "react/jsx-runtime";
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__ */ jsx60(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
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-var-wrap {
1668
+ .ree-input-with-variable {
1674
1669
  position: relative;
1675
1670
  }
1676
1671
 
1677
- .ree-var-btn {
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: 24px;
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-var-btn:hover {
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
  ### 文档与类型提示
@@ -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.1",
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