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.
Files changed (45) hide show
  1. package/README.md +79 -6
  2. package/package.json +1 -1
  3. package/packages/blocks/dist/index.d.ts +55 -3
  4. package/packages/blocks/dist/index.js +267 -208
  5. package/packages/blocks/dist/index.js.map +1 -1
  6. package/packages/blocks/node_modules/.bin/resolve +21 -0
  7. package/packages/blocks/package.json +1 -0
  8. package/packages/blocks/src/button.ts +1 -0
  9. package/packages/blocks/src/hero.ts +2 -0
  10. package/packages/blocks/src/image.ts +1 -0
  11. package/packages/blocks/src/index.ts +19 -2
  12. package/packages/blocks/src/logo.ts +1 -0
  13. package/packages/blocks/src/social.ts +47 -22
  14. package/packages/blocks/src/socialGroup.ts +49 -22
  15. package/packages/blocks/src/socialShared.ts +39 -2
  16. package/packages/blocks/src/text.ts +25 -19
  17. package/packages/blocks/tsconfig.json +0 -3
  18. package/packages/blocks/vite.config.ts +2 -0
  19. package/packages/core/dist/index.d.ts +90 -7
  20. package/packages/core/dist/index.js +2505 -1462
  21. package/packages/core/dist/index.js.map +1 -1
  22. package/packages/core/dist/style.css +1 -1
  23. package/packages/core/src/editor/Canvas.ts +107 -53
  24. package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
  25. package/packages/core/src/editor/Editor.ts +129 -8
  26. package/packages/core/src/editor/ExportModal.ts +2 -1
  27. package/packages/core/src/editor/FocusBreadcrumb.ts +120 -0
  28. package/packages/core/src/editor/ImageGalleryModal.ts +3 -3
  29. package/packages/core/src/editor/InlineEditor.ts +308 -66
  30. package/packages/core/src/editor/LeftPanel.ts +1 -2
  31. package/packages/core/src/editor/PreviewModal.ts +77 -7
  32. package/packages/core/src/editor/RichTextToolbar.ts +66 -44
  33. package/packages/core/src/editor/RightPanel.ts +773 -171
  34. package/packages/core/src/editor/SourceView.ts +2 -1
  35. package/packages/core/src/editor/Topbar.ts +150 -5
  36. package/packages/core/src/editor/styles.css +644 -81
  37. package/packages/core/src/index.ts +18 -1
  38. package/packages/core/src/renderer/mjml.ts +2 -1
  39. package/packages/core/src/store/store.ts +44 -3
  40. package/packages/core/src/types.ts +34 -3
  41. package/packages/core/src/utils/fontWeightSteps.ts +31 -0
  42. package/packages/core/src/utils/lockedMjml.ts +2 -1
  43. package/packages/core/src/utils/richTextCommand.ts +30 -0
  44. package/packages/core/src/utils/sectionLayout.ts +13 -0
  45. 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: allBlocks,
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。构造器中的 `initialDoc` 为 `Partial<EmailDoc>`,会与默认空邮件合并。**预置模板**可传入完整或部分 `sections`、`styles`、`variables` 等。运行时用 **`editor.setValue(doc)`** 可整份替换(例如切换模板),调用前会尽量失焦右栏输入,避免面板显示旧值。
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
  {
2
2
  "name": "mail-editor-pancake",
3
- "version": "0.0.0",
3
+ "version": "0.0.8",
4
4
  "private": false,
5
5
  "description": "面向运营的轻量邮件可视化编辑器(monorepo 根)",
6
6
  "devDependencies": {
@@ -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
- declare interface SocialLinkItem {
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 {