mail-editor-pancake 0.0.0 → 0.0.8
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 +79 -6
- package/package.json +1 -1
- package/packages/blocks/dist/index.d.ts +55 -3
- package/packages/blocks/dist/index.js +267 -208
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/node_modules/.bin/resolve +21 -0
- package/packages/blocks/package.json +1 -0
- package/packages/blocks/src/button.ts +1 -0
- package/packages/blocks/src/hero.ts +2 -0
- package/packages/blocks/src/image.ts +1 -0
- package/packages/blocks/src/index.ts +19 -2
- package/packages/blocks/src/logo.ts +1 -0
- package/packages/blocks/src/social.ts +47 -22
- package/packages/blocks/src/socialGroup.ts +49 -22
- package/packages/blocks/src/socialShared.ts +39 -2
- package/packages/blocks/src/text.ts +25 -19
- package/packages/blocks/tsconfig.json +0 -3
- package/packages/blocks/vite.config.ts +2 -0
- package/packages/core/dist/index.d.ts +90 -7
- package/packages/core/dist/index.js +2505 -1462
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/src/editor/Canvas.ts +107 -53
- package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
- package/packages/core/src/editor/Editor.ts +129 -8
- package/packages/core/src/editor/ExportModal.ts +2 -1
- package/packages/core/src/editor/FocusBreadcrumb.ts +120 -0
- package/packages/core/src/editor/ImageGalleryModal.ts +3 -3
- package/packages/core/src/editor/InlineEditor.ts +308 -66
- package/packages/core/src/editor/LeftPanel.ts +1 -2
- package/packages/core/src/editor/PreviewModal.ts +77 -7
- package/packages/core/src/editor/RichTextToolbar.ts +66 -44
- package/packages/core/src/editor/RightPanel.ts +773 -171
- package/packages/core/src/editor/SourceView.ts +2 -1
- package/packages/core/src/editor/Topbar.ts +150 -5
- package/packages/core/src/editor/styles.css +644 -81
- package/packages/core/src/index.ts +18 -1
- package/packages/core/src/renderer/mjml.ts +2 -1
- package/packages/core/src/store/store.ts +44 -3
- package/packages/core/src/types.ts +34 -3
- package/packages/core/src/utils/fontWeightSteps.ts +31 -0
- package/packages/core/src/utils/lockedMjml.ts +2 -1
- package/packages/core/src/utils/richTextCommand.ts +30 -0
- package/packages/core/src/utils/sectionLayout.ts +13 -0
- package/playground/vanilla/src/main.ts +1 -1
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Doc → Section → Column → Block // 仅四层,不允许 Block 再含
|
|
|
23
23
|
- **界面主题**:顶栏 **太阳 / 月亮 / 显示器** 图标切换浅色、深色、跟随系统(`prefers-color-scheme`);中间邮件画布仍为白纸以贴近成品
|
|
24
24
|
- **品牌色**:可选 `accentColor` / `setAccentColor`,覆盖强调色与选区色;可选顶栏拾色器
|
|
25
25
|
- **仅搭正文**:`ui.hideMailMeta` 隐藏主题 / Preheader 与顶栏「邮件设置」,保留版式宽度与全局样式
|
|
26
|
+
- **画布清空 / 重置**:顶栏「清空画布」「重置内容」;`presetDoc` 与 `initialDoc` 分离,编辑已保存邮件时重置仍回到业务预置模板
|
|
26
27
|
- 包体可控:核心 + MJML + CodeMirror + 富文本,gzip ≈ 586KB
|
|
27
28
|
|
|
28
29
|
## 仓库结构
|
|
@@ -67,6 +68,12 @@ const editor = new MailEditor({
|
|
|
67
68
|
variables: [{ key: 'user.name', label: '用户名', sample: '张三' }],
|
|
68
69
|
sections: [],
|
|
69
70
|
},
|
|
71
|
+
/**
|
|
72
|
+
* 顶栏「重置内容」恢复的目标(与 initialDoc 独立)。
|
|
73
|
+
* 编辑页打开已保存邮件时:initialDoc = 当前稿,presetDoc = 业务默认模板。
|
|
74
|
+
* 未传时与 initialDoc 合并结果一致。
|
|
75
|
+
*/
|
|
76
|
+
// presetDoc: defaultTemplatePartial,
|
|
70
77
|
/** 右栏控件形态等,见下文「UI 选项 ui」 */
|
|
71
78
|
// ui: { preferSliderControls: true, hideMailMeta: true },
|
|
72
79
|
// autoWrapSection: true(默认)— 把 Block 拖到 Section 之间空白处时
|
|
@@ -85,6 +92,36 @@ const { mjml, html } = editor.export({ withSampleVariables: true });
|
|
|
85
92
|
|
|
86
93
|
// 整份替换文档(例如切换模板);会先失焦右栏避免旧值残留
|
|
87
94
|
// editor.setValue(nextDoc);
|
|
95
|
+
|
|
96
|
+
// 清空画布(仅 sections 置空,保留 meta / styles / variables;可 ⌘Z 撤销)
|
|
97
|
+
// editor.clearCanvas();
|
|
98
|
+
|
|
99
|
+
// 恢复为 presetDoc(或构造时 initialDoc)快照
|
|
100
|
+
// editor.resetToPreset();
|
|
101
|
+
|
|
102
|
+
// 异步加载默认模板后更新重置目标
|
|
103
|
+
// editor.setPresetDoc(defaultTemplatePartial);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 画布清空与重置
|
|
107
|
+
|
|
108
|
+
顶栏位于 **撤销 / 重做** 右侧(可用 `ui` 隐藏,见下表):
|
|
109
|
+
|
|
110
|
+
| 按钮 | 行为 |
|
|
111
|
+
|------|------|
|
|
112
|
+
| **清空画布** | 移除所有 Section / Block;`meta`、`styles`、`variables` 不变。操作前浏览器 `confirm` 确认;记入撤销栈(⌘Z 可恢复)。 |
|
|
113
|
+
| **重置内容** | 整份替换为 **`presetDoc`** 快照(未传 `presetDoc` 时等同构造时的 `initialDoc` 合并结果)。操作前 `confirm` 确认;**不**走撤销栈(与 `setValue` 相同,会清空 history)。 |
|
|
114
|
+
|
|
115
|
+
**宿主常见写法**:新建页 `initialDoc` 与 `presetDoc` 同为默认模板;编辑页 `initialDoc` 为接口返回的 `jsonContent`,`presetDoc` 仍为业务预置结构,避免「重置」把用户带回打开时的草稿。
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const editor = new MailEditor({
|
|
119
|
+
container: el,
|
|
120
|
+
blocks: allBlocks,
|
|
121
|
+
initialDoc: loadedFromApi, // 当前画布
|
|
122
|
+
presetDoc: businessDefaultTemplate, // 顶栏「重置内容」
|
|
123
|
+
onChange: (doc) => save(doc),
|
|
124
|
+
});
|
|
88
125
|
```
|
|
89
126
|
|
|
90
127
|
### 界面主题
|
|
@@ -120,6 +157,11 @@ editor.getTheme();
|
|
|
120
157
|
|------|------|
|
|
121
158
|
| `preferSliderControls?: boolean` | 为 `true` 时右栏数值、内边距、全局/组件字号等使用滑块等增强控件。默认 `false`。 |
|
|
122
159
|
| `hideMailMeta?: boolean` | 为 `true` 时:**不展示**右栏「主题」「Preheader」以及顶栏「邮件设置」按钮;右栏文档级面板改为 **「版式」(内容宽度)+「全局样式」**。`doc.meta.subject` / `preheader` 仍在数据模型中,导出 MJML 仍会生成 `<mj-title>`(可为空)、有值时才生成 `<mj-preview>`,适合发件主题由宿主系统单独维护的场景。 |
|
|
160
|
+
| `hideTopbarTitle?: boolean` | 隐藏顶栏左侧产品标题(嵌入宿主页时常用)。 |
|
|
161
|
+
| `hideTopbarMailSettings?: boolean` | 隐藏顶栏「邮件设置」按钮(与 `hideMailMeta` 叠加使用)。 |
|
|
162
|
+
| `hideTopbarFullscreen?: boolean` | 隐藏顶栏全屏按钮。 |
|
|
163
|
+
| `hideTopbarClearCanvas?: boolean` | 隐藏顶栏「清空画布」。 |
|
|
164
|
+
| `hideTopbarResetContent?: boolean` | 隐藏顶栏「重置内容」。 |
|
|
123
165
|
|
|
124
166
|
### 图片资源 `imageAssets`
|
|
125
167
|
|
|
@@ -199,20 +241,23 @@ export function MailEditorView({ value, onChange }: {
|
|
|
199
241
|
|
|
200
242
|
```vue
|
|
201
243
|
<script setup lang="ts">
|
|
202
|
-
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
|
203
|
-
import { MailEditor, type EmailDoc } from '@simple-mail/core';
|
|
244
|
+
import { markRaw, onMounted, onBeforeUnmount, ref } from 'vue';
|
|
245
|
+
import { MailEditor, type BlockDefinition, type EmailDoc } from '@simple-mail/core';
|
|
204
246
|
import '@simple-mail/core/style.css';
|
|
205
247
|
import { allBlocks } from '@simple-mail/blocks';
|
|
206
248
|
|
|
207
|
-
const props = defineProps<{ modelValue?: EmailDoc }>();
|
|
249
|
+
const props = defineProps<{ modelValue?: EmailDoc; blocks?: BlockDefinition<any>[] }>();
|
|
208
250
|
const emit = defineEmits<{ 'update:modelValue': [EmailDoc] }>();
|
|
209
251
|
const el = ref<HTMLDivElement>();
|
|
210
252
|
let editor: MailEditor | null = null;
|
|
211
253
|
|
|
212
254
|
onMounted(() => {
|
|
255
|
+
const defs = props.blocks ?? allBlocks;
|
|
256
|
+
// expandPaletteDrop / schema 等依赖块定义上的函数;props 深度代理可能导致丢失,建议 markRaw
|
|
257
|
+
const stable = defs.map((d) => markRaw(d));
|
|
213
258
|
editor = new MailEditor({
|
|
214
259
|
container: el.value!,
|
|
215
|
-
blocks:
|
|
260
|
+
blocks: stable,
|
|
216
261
|
initialDoc: props.modelValue,
|
|
217
262
|
onChange: (doc) => emit('update:modelValue', doc),
|
|
218
263
|
});
|
|
@@ -265,7 +310,8 @@ export const couponBlock = defineBlock<{ title: string; code: string; expiresAt:
|
|
|
265
310
|
new MailEditor({ container, blocks: [...allBlocks, couponBlock] });
|
|
266
311
|
```
|
|
267
312
|
|
|
268
|
-
`schema` 字段类型支持:`text | textarea | number | color | select | switch | image | url | spacing | socialLinkList`。其中 **`image`** 渲染为「URL 输入 +(可选)上传 +(可选)图库」,由 `MailEditor` 的 **`imageAssets`** 控制,见上文「图片资源 imageAssets」。
|
|
313
|
+
`schema` 字段类型支持:`text | textarea | number | color | select | switch | image | url | spacing | socialLinkList`。其中 **`image`** 渲染为「URL 输入 +(可选)上传 +(可选)图库」,由 `MailEditor` 的 **`imageAssets`** 控制,见上文「图片资源 imageAssets」。
|
|
314
|
+
**社交组**(`social-group`)另有 **`iconBorderRadius`**(px,默认圆形)、**`iconSpacing`**(图标间距 px,对应 MJML `mj-social` 的 `inner-padding`),画布预览与导出共用同一套圆角/间距逻辑。
|
|
269
315
|
所有字段会在右栏自动渲染表单,change 事件回写 `block.props`。
|
|
270
316
|
|
|
271
317
|
### 内联编辑 inlineEditable
|
|
@@ -280,6 +326,26 @@ new MailEditor({ container, blocks: [...allBlocks, couponBlock] });
|
|
|
280
326
|
|
|
281
327
|
提交时机:失焦 / 单行 Enter / Esc 取消。富文本提交前会过一次白名单清洗(保留 `a/b/strong/i/em/u/s/span/p/div/ul/ol/li/h1-h6/br`,属性仅留 `href/target/rel/style/class`,`<font>` 自动转为 `<span style="...">`)。
|
|
282
328
|
|
|
329
|
+
### 左栏组合模板 `expandPaletteDrop`(可选)
|
|
330
|
+
|
|
331
|
+
用于「一次拖入、画布内仍是多个通用块」的预设(例如页脚 = 图片 + 若干文本),避免整段 raw MJML,又省去运营逐个从左侧拖组件。
|
|
332
|
+
|
|
333
|
+
在 `BlockDefinition` 上可选声明:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[];
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
- 从左栏拖入该条目时,引擎会在目标列(或拖到 Section 间隙时自动包裹的**单列 Section**)内 **`splice` 插入**回调返回的多个块;**文档 JSON 里不会出现该定义的 `type`**,仅在注册表中作为左栏卡片存在。
|
|
340
|
+
- 回调内请使用传入的 `createBlock('image' | 'text' | …)`,以便 ID、`defaultProps` 与内置块一致。
|
|
341
|
+
- `toMjml` / `renderPreview` 仍须在类型上满足 `BlockDefinition`;组合入口可选用占位的 `toMjml: () => ''`(正常不应出现在 `sections` 里)。
|
|
342
|
+
|
|
343
|
+
**宿主集成注意**
|
|
344
|
+
|
|
345
|
+
- 修改 `packages/core` 源码(含拖拽、`expandPaletteDrop` 等)后,请在 monorepo 根目录执行 **`pnpm build`** 或 **`pnpm --filter @simple-mail/core build`**,保证 npm/link 宿主的 **`dist`** 与类型声明同步。
|
|
346
|
+
- **Vue**:若把 `blocks` 数组作为 **props** 传入再交给 `MailEditor`,响应式代理可能导致块定义上的**函数字段**不可靠;应对每个 `BlockDefinition` 使用 **`markRaw`**(或与之一致的「非响应式」引用)后再传入构造器。参见上文「Vue 集成」示例注释。
|
|
347
|
+
- **Vite**:宿主若通过 **`link:` / workspace** 引用本仓库包,建议将 **`@simple-mail/core`、`@simple-mail/blocks`**(以及项目中实际 import 的等价路径,如指向本 monorepo 子包的 specifier)列入 **`optimizeDeps.exclude`**,避免 **依赖预构建缓存**与本地刚构建的 **`dist`** 不一致(常见现象:画布拖拽、`expandPaletteDrop` 等与当前源码不符)。仍异常时可删除宿主项目的 **`node_modules/.vite`** 后重启 dev。
|
|
348
|
+
|
|
283
349
|
## 操作手册
|
|
284
350
|
|
|
285
351
|
| 操作 | 触发方式 |
|
|
@@ -293,6 +359,8 @@ new MailEditor({ container, blocks: [...allBlocks, couponBlock] });
|
|
|
293
359
|
| 删除 | 选中后按 Delete/Backspace;或工具条 🗑 |
|
|
294
360
|
| 复制 | 工具条 ⎘ 图标 |
|
|
295
361
|
| 撤销 / 重做 | ⌘Z / ⌘⇧Z(顶栏按钮也行) |
|
|
362
|
+
| 清空画布 | 顶栏「清空画布」;移除全部 Section/Block,保留全局样式与变量(可撤销) |
|
|
363
|
+
| 重置内容 | 顶栏「重置内容」;恢复为 `presetDoc`(或构造时的 `initialDoc`) |
|
|
296
364
|
| 插入变量 | 顶栏 `{{ }}`:编辑中插到光标处;否则插到聚焦输入框 |
|
|
297
365
|
| 切换源码 / 设计 | 顶栏切换 |
|
|
298
366
|
| 导出 HTML | 顶栏右上 |
|
|
@@ -308,7 +376,12 @@ new MailEditor({ container, blocks: [...allBlocks, couponBlock] });
|
|
|
308
376
|
|
|
309
377
|
## 数据模型 速览
|
|
310
378
|
|
|
311
|
-
`EmailDoc` 为单一事实来源;设计 / 导出 / 宿主保存均围绕该 JSON
|
|
379
|
+
`EmailDoc` 为单一事实来源;设计 / 导出 / 宿主保存均围绕该 JSON。
|
|
380
|
+
|
|
381
|
+
- **`initialDoc`**:`Partial<EmailDoc>`,与默认空邮件合并,作为**首次进入画布**的内容。
|
|
382
|
+
- **`presetDoc`**:可选,与默认空邮件合并,作为顶栏 **「重置内容」** 的目标;未传时与 `initialDoc` 相同。编辑已保存邮件时应单独传入业务默认模板,勿与 `initialDoc` 混用。
|
|
383
|
+
- **`editor.setValue(doc)`**:运行期整份替换(如切换模板);会清空撤销栈;调用前会尽量失焦右栏输入。
|
|
384
|
+
- **`editor.clearCanvas()`** / **`editor.resetToPreset()`** / **`editor.setPresetDoc(partial)`**:与顶栏按钮等价,供宿主程序化调用。
|
|
312
385
|
|
|
313
386
|
```ts
|
|
314
387
|
interface EmailDoc {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BlockDefinition } from '@simple-mail/core';
|
|
2
2
|
|
|
3
|
-
/** 全部组件(内置 +
|
|
3
|
+
/** 全部组件(内置 + 示例自定义)。业务相关块请在使用侧与 `allBlocks` 合并后传入 MailEditor。 */
|
|
4
4
|
export declare const allBlocks: BlockDefinition<any>[];
|
|
5
5
|
|
|
6
6
|
/** 内置组件(content 类)。可直接传给 MailEditor.blocks。 */
|
|
@@ -36,9 +36,16 @@ declare interface DividerProps {
|
|
|
36
36
|
paddingBottom: number;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
export declare const escAttr: (s: string) => string;
|
|
40
|
+
|
|
41
|
+
/** mj-social-element 体内文本转义(非属性) */
|
|
42
|
+
export declare const escMjmlText: (s: string) => string;
|
|
43
|
+
|
|
44
|
+
/** 业务示例的“自定义”组件。可作为参考或直接使用;正式业务块应由宿主应用注册。 */
|
|
40
45
|
export declare const exampleCustomBlocks: BlockDefinition<any>[];
|
|
41
46
|
|
|
47
|
+
export declare function flexJustifyFromAlign(align: 'left' | 'center' | 'right'): string;
|
|
48
|
+
|
|
42
49
|
export declare const footerBlock: BlockDefinition<FooterProps>;
|
|
43
50
|
|
|
44
51
|
declare interface FooterProps {
|
|
@@ -101,11 +108,31 @@ declare interface LogoProps {
|
|
|
101
108
|
align: 'left' | 'center' | 'right';
|
|
102
109
|
}
|
|
103
110
|
|
|
111
|
+
/** 生成 mj-social-element 的 name 属性(与 MJML 内置图标对齐;未知平台退化为 web)。 */
|
|
112
|
+
export declare function mjSocialElementName(network: string): string;
|
|
113
|
+
|
|
114
|
+
export declare function mjSocialElementsLines(links: SocialLinkItem[], labelStyle: SocialGroupLabelStyle): string;
|
|
115
|
+
|
|
116
|
+
export declare function paddingQuad(p: {
|
|
117
|
+
paddingTop: number;
|
|
118
|
+
paddingRight: number;
|
|
119
|
+
paddingBottom: number;
|
|
120
|
+
paddingLeft: number;
|
|
121
|
+
}): string;
|
|
122
|
+
|
|
123
|
+
/** 社交平台与 MJML mj-social-element 的 name 对齐(含常用内置名)。 */
|
|
124
|
+
export declare const SOCIAL_NETWORK_OPTIONS: {
|
|
125
|
+
label: string;
|
|
126
|
+
value: string;
|
|
127
|
+
}[];
|
|
128
|
+
|
|
104
129
|
/** 与社交组一致,仅平台选项收窄为业务五件套 */
|
|
105
130
|
declare interface SocialBizProps {
|
|
106
131
|
elements: SocialLinkItem[];
|
|
107
132
|
align: 'left' | 'center' | 'right';
|
|
108
133
|
iconSize: number;
|
|
134
|
+
iconBorderRadius: number;
|
|
135
|
+
iconSpacing: number;
|
|
109
136
|
labelFontSize: number;
|
|
110
137
|
labelFontWeight: string;
|
|
111
138
|
labelColor: string;
|
|
@@ -124,10 +151,21 @@ export declare const socialBlock: BlockDefinition<SocialBizProps>;
|
|
|
124
151
|
|
|
125
152
|
export declare const socialGroupBlock: BlockDefinition<SocialGroupProps>;
|
|
126
153
|
|
|
154
|
+
/** 社交组块级:所有带标签的链接共用同一套标签文字样式 */
|
|
155
|
+
export declare interface SocialGroupLabelStyle {
|
|
156
|
+
labelFontSize: number;
|
|
157
|
+
labelFontWeight: string;
|
|
158
|
+
labelColor: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
declare interface SocialGroupProps {
|
|
128
162
|
elements: SocialLinkItem[];
|
|
129
163
|
align: 'left' | 'center' | 'right';
|
|
130
164
|
iconSize: number;
|
|
165
|
+
/** 图标圆角 (px)。0 为方形;≥999 或不少于图标半边时为圆形(默认 999) */
|
|
166
|
+
iconBorderRadius: number;
|
|
167
|
+
/** 图标之间的间距 (px),对应 mj-social inner-padding */
|
|
168
|
+
iconSpacing: number;
|
|
131
169
|
/** 标签文字字号(整组统一) */
|
|
132
170
|
labelFontSize: number;
|
|
133
171
|
labelFontWeight: string;
|
|
@@ -138,7 +176,16 @@ declare interface SocialGroupProps {
|
|
|
138
176
|
paddingLeft: number;
|
|
139
177
|
}
|
|
140
178
|
|
|
141
|
-
|
|
179
|
+
/** 画布预览用图标圆角 CSS(与 {@link socialIconBorderRadiusMjml} 语义一致) */
|
|
180
|
+
export declare function socialIconBorderRadiusCss(iconSize: number, radiusPx: unknown): string;
|
|
181
|
+
|
|
182
|
+
/** mj-social `border-radius`:0 直角;≥999 或 ≥图标半边 → 圆形裁切(999px) */
|
|
183
|
+
export declare function socialIconBorderRadiusMjml(iconSize: number, radiusPx: unknown): string;
|
|
184
|
+
|
|
185
|
+
/** mj-social `inner-padding`,控制图标之间的可视间距 */
|
|
186
|
+
export declare function socialIconSpacingPx(spacing: unknown, fallback?: number): number;
|
|
187
|
+
|
|
188
|
+
export declare interface SocialLinkItem {
|
|
142
189
|
network: string;
|
|
143
190
|
href: string;
|
|
144
191
|
/** 图标旁文字,对应 mj-social-element 子内容 */
|
|
@@ -149,6 +196,11 @@ declare interface SocialLinkItem {
|
|
|
149
196
|
backgroundColor?: string;
|
|
150
197
|
}
|
|
151
198
|
|
|
199
|
+
export declare function socialMeta(network: string): {
|
|
200
|
+
color: string;
|
|
201
|
+
preview: string;
|
|
202
|
+
};
|
|
203
|
+
|
|
152
204
|
export declare const spacerBlock: BlockDefinition<SpacerProps>;
|
|
153
205
|
|
|
154
206
|
declare interface SpacerProps {
|