mail-editor-pancake 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +37 -4
  2. package/package.json +1 -1
  3. package/packages/blocks/dist/index.js +275 -263
  4. package/packages/blocks/dist/index.js.map +1 -1
  5. package/packages/blocks/package.json +1 -3
  6. package/packages/blocks/src/button.ts +1 -1
  7. package/packages/blocks/src/hero.ts +6 -3
  8. package/packages/blocks/src/i18n.ts +10 -11
  9. package/packages/blocks/src/image.ts +2 -1
  10. package/packages/blocks/src/index.ts +5 -3
  11. package/packages/blocks/src/social.ts +3 -3
  12. package/packages/blocks/src/socialGroup.ts +4 -4
  13. package/packages/blocks/src/socialShared.ts +23 -3
  14. package/packages/blocks/src/text.ts +1 -2
  15. package/packages/blocks/vite.config.ts +1 -1
  16. package/packages/core/dist/index.d.ts +96 -1
  17. package/packages/core/dist/index.js +4346 -3591
  18. package/packages/core/dist/index.js.map +1 -1
  19. package/packages/core/dist/style.css +1 -1
  20. package/packages/core/package.json +1 -3
  21. package/packages/core/src/editor/BlockCodeModal.ts +6 -7
  22. package/packages/core/src/editor/Canvas.ts +558 -20
  23. package/packages/core/src/editor/ColorPickerPopover.ts +2 -2
  24. package/packages/core/src/editor/Editor.ts +379 -39
  25. package/packages/core/src/editor/ExportModal.ts +7 -9
  26. package/packages/core/src/editor/FocusBreadcrumb.ts +3 -3
  27. package/packages/core/src/editor/ImportDocModal.ts +50 -7
  28. package/packages/core/src/editor/InlineEditor.ts +43 -39
  29. package/packages/core/src/editor/LeftPanel.ts +2 -4
  30. package/packages/core/src/editor/Modal.ts +1 -1
  31. package/packages/core/src/editor/PreviewModal.ts +3 -8
  32. package/packages/core/src/editor/RichTextToolbar.ts +26 -15
  33. package/packages/core/src/editor/RightPanel.ts +141 -125
  34. package/packages/core/src/editor/SourceView.ts +5 -3
  35. package/packages/core/src/editor/Topbar.ts +79 -34
  36. package/packages/core/src/editor/VariablePickerPanel.ts +3 -5
  37. package/packages/core/src/editor/imageAssets.ts +1 -1
  38. package/packages/core/src/editor/styles.css +258 -86
  39. package/packages/core/src/i18n/index.ts +139 -41
  40. package/packages/core/src/index.ts +11 -1
  41. package/packages/core/src/renderer/index.ts +3 -6
  42. package/packages/core/src/renderer/mjml.ts +4 -9
  43. package/packages/core/src/store/store.ts +2 -6
  44. package/packages/core/src/types.ts +3 -3
  45. package/packages/core/src/utils/accentColor.ts +3 -3
  46. package/packages/core/src/utils/docClipboard.ts +107 -2
  47. package/packages/core/src/utils/dynamicVariantHtml.ts +1 -1
  48. package/packages/core/src/utils/emailListStyles.ts +1 -1
  49. package/packages/core/src/utils/fontWeightSteps.ts +7 -6
  50. package/packages/core/src/utils/html.ts +3 -2
  51. package/packages/core/src/utils/inlineListEditing.ts +19 -35
  52. package/packages/core/src/utils/link.ts +3 -6
  53. package/packages/core/src/utils/lockedMjml.ts +1 -1
  54. package/packages/core/src/utils/modalSize.ts +4 -3
  55. package/packages/core/src/utils/richHtmlEmpty.ts +3 -1
  56. package/packages/core/src/variables/index.ts +4 -5
  57. package/packages/core/vite.config.ts +1 -1
  58. package/playground/vanilla/index.html +17 -0
  59. package/playground/vanilla/src/main.ts +152 -123
  60. package/playground/vanilla/vite.config.ts +5 -2
@@ -73,6 +73,8 @@ export const zhCNMessages: SimpleMailMessages = {
73
73
  'topbar.hideLayoutBorders': '隐藏 Section / Block 边框',
74
74
  'topbar.fullscreen': '全屏',
75
75
  'topbar.exitFullscreen': '退出全屏',
76
+ 'topbar.sectionSelect': '选择节/块:批量复制 / 导出 / 删除(⌘/Ctrl+点击直达,Shift+点击区间选)',
77
+ 'topbar.sectionSelectActive': '退出选择模式(Shift+点击可区间勾选)',
76
78
  'leftPanel.layout': '布局',
77
79
  'leftPanel.components': '组件',
78
80
  'leftPanel.sectionBlocks': '组合布局',
@@ -88,6 +90,20 @@ export const zhCNMessages: SimpleMailMessages = {
88
90
  'canvas.unknownBlock': '未知组件: {type}',
89
91
  'canvas.emptyColumnHint': '拖入组件 · 双击插入文本',
90
92
  'canvas.lockedCode': '已锁定为代码',
93
+ 'sectionSelect.count': '已选 {count} 节',
94
+ 'sectionSelect.countBlocks': '已选 {count} 块',
95
+ 'sectionSelect.countMixed': '已选 {sections} 节 · {blocks} 块',
96
+ 'sectionSelect.selectAll': '全选',
97
+ 'sectionSelect.unselectAll': '取消全选',
98
+ 'sectionSelect.copyDesign': '复制设计稿',
99
+ 'sectionSelect.exportJson': '导出 JSON',
100
+ 'sectionSelect.done': '完成',
101
+ 'sectionSelect.deleteConfirm': '确定删除选中的 {count} 个节?可通过撤销整体恢复。',
102
+ 'sectionSelect.deleteConfirmBlocks': '确定删除选中的 {count} 个块?可通过撤销整体恢复。',
103
+ 'sectionSelect.deleteConfirmMixed':
104
+ '确定删除选中的 {sections} 个节与 {blocks} 个块?可通过撤销整体恢复。',
105
+ 'canvas.copySectionDesignTitle': '复制节设计稿:将此 Section JSON 写入剪贴板,可在其他画布中追加',
106
+ 'canvas.copyBlockDesignTitle': '复制组件设计稿:将此组件 JSON 写入剪贴板,可在其他画布中追加',
91
107
  'rightPanel.docRoot.mail': '邮件',
92
108
  'rightPanel.docRoot.layout': '版式',
93
109
  'rightPanel.breadcrumb.selected': '当前选中',
@@ -100,8 +116,10 @@ export const zhCNMessages: SimpleMailMessages = {
100
116
  'rightPanel.block.locked': '该组件已被代码模式锁定。',
101
117
  'rightPanel.block.restoreProps': '恢复为属性模式',
102
118
  'rightPanel.block.unregistered': '未注册的组件: {type}',
103
- 'rightPanel.block.mjmlWarning': '检测到 MJML 标签(如 <mj-text>)。此处应只写 HTML,否则预览会显示源码。简单链接请用「文本」块。',
104
- 'rightPanel.block.invalidLink': '该链接无法跳转(如 1234、#、缺少 https://)。Outlook 网页端会清除此类链接,导致按钮/图标不可见。',
119
+ 'rightPanel.block.mjmlWarning':
120
+ '检测到 MJML 标签(如 <mj-text>)。此处应只写 HTML,否则预览会显示源码。简单链接请用「文本」块。',
121
+ 'rightPanel.block.invalidLink':
122
+ '该链接无法跳转(如 1234、#、缺少 https://)。Outlook 网页端会清除此类链接,导致按钮/图标不可见。',
105
123
  'rightPanel.doc.title': '邮件设置',
106
124
  'rightPanel.doc.layoutTitle': '版式',
107
125
  'rightPanel.doc.subject': '主题',
@@ -142,9 +160,11 @@ export const zhCNMessages: SimpleMailMessages = {
142
160
  'rightPanel.section.padding': '内边距',
143
161
  'rightPanel.section.columnGap': '列间距 (px)',
144
162
  'rightPanel.section.stackedGap': '堆叠纵向间距 (px)',
145
- 'rightPanel.section.stackedGapHelp': '小屏堆叠为单列后,相邻列之间的额外纵向间距;0 表示沿用块自身内边距。',
163
+ 'rightPanel.section.stackedGapHelp':
164
+ '小屏堆叠为单列后,相邻列之间的额外纵向间距;0 表示沿用块自身内边距。',
146
165
  'rightPanel.section.preserveMobile': '小屏仍并排显示多列(可能字很窄)',
147
- 'rightPanel.section.preserveMobileHelp': '开启后 MJML 会生成 mj-group,移动端预览/导出与默认「小屏堆叠列」行为不同。',
166
+ 'rightPanel.section.preserveMobileHelp':
167
+ '开启后 MJML 会生成 mj-group,移动端预览/导出与默认「小屏堆叠列」行为不同。',
148
168
  'rightPanel.section.columnLayout': '列布局',
149
169
  'rightPanel.social.labelPlaceholder': '标签文字(可选)',
150
170
  'rightPanel.social.iconUrlPlaceholder': '图标 URL(可选)',
@@ -158,14 +178,17 @@ export const zhCNMessages: SimpleMailMessages = {
158
178
  'rightPanel.image.helpGallery': '或使用「图库」',
159
179
  'rightPanel.image.helpSeparator': ';',
160
180
  'rightPanel.image.helpEnd': '。',
161
- 'rightPanel.image.helpAssetsConfigured': '已传入 imageAssets:上传默认显示(提供 uploadImage 且未设 showUpload:false);图库需 showGallery:true,可使用内置 imageGallery 或自管 pickImageFromGallery。仍可手输 URL。',
162
- 'rightPanel.image.helpNoAssets': '在 MailEditor 的 imageAssets 中配置 uploadImage、内置 imageGallery 或 pickImageFromGallery 等;未配置时仅支持手输 URL。',
181
+ 'rightPanel.image.helpAssetsConfigured':
182
+ '已传入 imageAssets:上传默认显示(提供 uploadImage 且未设 showUpload:false);图库需 showGallery:true,可使用内置 imageGallery 或自管 pickImageFromGallery。仍可手输 URL。',
183
+ 'rightPanel.image.helpNoAssets':
184
+ '在 MailEditor 的 imageAssets 中配置 uploadImage、内置 imageGallery 或 pickImageFromGallery 等;未配置时仅支持手输 URL。',
163
185
  'rightPanel.color.unset': '未设置',
164
186
  'rightPanel.fontSize.placeholder': '如 16px',
165
187
  'rightPanel.code.title': '编辑组件 MJML 源码',
166
188
  'rightPanel.code.restoreDefault': '恢复默认',
167
189
  'rightPanel.code.saveLocked': '保存为锁定 MJML',
168
- 'rightPanel.code.help': '保存后该组件将以这段 MJML 直接输出,属性面板会被禁用,可点"恢复默认"取消锁定。',
190
+ 'rightPanel.code.help':
191
+ '保存后该组件将以这段 MJML 直接输出,属性面板会被禁用,可点"恢复默认"取消锁定。',
169
192
  'toolbar.fontDefault': '默认',
170
193
  'toolbar.fontPingFang': '苹方/PingFang',
171
194
  'toolbar.fontMicrosoftYaHei': '微软雅黑',
@@ -226,6 +249,9 @@ export const zhCNMessages: SimpleMailMessages = {
226
249
  'importDoc.placeholder': '在此粘贴设计稿 JSON…',
227
250
  'importDoc.pasteFromClipboard': '从剪贴板填入',
228
251
  'importDoc.apply': '应用并覆盖画布',
252
+ 'importDoc.applyAppend': '追加到画布',
253
+ 'importDoc.hintSelection':
254
+ '检测到局部设计稿,将追加到当前画布(不覆盖现有内容);未注册的组件类型会被跳过。',
229
255
  'importDoc.clipboardEmpty': '剪贴板为空或无法读取,请手动粘贴 JSON。',
230
256
  'importDoc.clipboardFilled': '已从剪贴板填入,请确认后点击「应用并覆盖画布」。',
231
257
  'importDoc.empty': '请先粘贴设计稿 JSON。',
@@ -235,6 +261,14 @@ export const zhCNMessages: SimpleMailMessages = {
235
261
  'toast.copyDesignOk': '设计稿已复制到剪贴板',
236
262
  'toast.copyDesignFailed': '复制失败,请检查浏览器剪贴板权限',
237
263
  'toast.importDesignOk': '设计稿已导入',
264
+ 'toast.copySelectionOk': '局部设计稿已复制到剪贴板',
265
+ 'toast.copySelectionNoTarget': '请先选中要复制的 Section 或组件',
266
+ 'toast.pasteSelectionOk': '已追加到当前画布',
267
+ 'toast.pasteSelectionEmpty': '局部设计稿内容为空',
268
+ 'toast.pasteSelectionSkipped': '已跳过未注册的组件类型:{types}',
269
+ 'toast.exportSelectionOk': '已导出所选内容的设计稿 JSON',
270
+ 'toast.removeSectionsOk': '已删除 {count} 个节',
271
+ 'toast.removeCheckedOk': '已删除 {sections} 个节、{blocks} 个块',
238
272
  'toast.noVariables': '暂无可用变量',
239
273
  'toast.variableCopied': '已复制变量',
240
274
  'toast.copiedToClipboard': '已复制到剪贴板',
@@ -251,9 +285,11 @@ export const zhCNMessages: SimpleMailMessages = {
251
285
  'blocks.social.name': '社交链接',
252
286
  'blocks.footer.name': '页脚',
253
287
  'blocks.button.defaultText': '立即查看',
254
- 'blocks.text.defaultContent': '<p style="margin:0;">在此处输入文本。<strong>双击</strong>进入编辑模式,<em>选中文字</em>会出现工具条;支持变量 {{user.name}}</p>',
255
- 'blocks.html.defaultHtml': '<p style="margin:0;">在此写入任意 HTML,将经 <strong>mj-raw</strong> 原样输出到邮件。支持变量 {{user.name}}</p>',
256
- 'blocks.hero.defaultContent': '<p style="margin:0;font-size:28px;font-weight:700;line-height:1.25;">主标题落在大图上</p><p style="margin:12px 0 0;font-size:16px;font-weight:400;opacity:0.95;line-height:1.5;">支援变量与 <strong>双击编辑</strong>,与正文组件一致。</p>',
288
+ 'blocks.text.defaultContent': '<p>双击进入编辑...</p>',
289
+ 'blocks.html.defaultHtml':
290
+ '<p style="margin:0;">在此写入任意 HTML,将经 <strong>mj-raw</strong> 原样输出到邮件。支持变量 {{user.name}}</p>',
291
+ 'blocks.hero.defaultContent':
292
+ '<p style="margin:0;font-size:28px;font-weight:700;line-height:1.25;">主标题落在大图上</p><p style="margin:12px 0 0;font-size:16px;font-weight:400;opacity:0.95;line-height:1.5;">支援变量与 <strong>双击编辑</strong>,与正文组件一致。</p>',
257
293
  'blocks.footer.defaultCompanyName': '你的公司',
258
294
  'blocks.footer.defaultAddress': '上海市某区某路某号',
259
295
  'blocks.footer.defaultUnsubscribeText': '退订邮件',
@@ -312,13 +348,18 @@ export const zhCNMessages: SimpleMailMessages = {
312
348
  'blocks.help.globalLineHeight': '留空则使用邮件设置里的全局行距',
313
349
  'blocks.help.canvasEditRecommended': '建议直接在画布双击编辑;这里保留为高级编辑入口。',
314
350
  'blocks.help.canvasEditRecommendedShort': '建议在画布内双击编辑;此处供高级修改。',
315
- 'blocks.help.htmlOnly': '只写 HTML(如 <a>、<table>),不要写 <mj-text> 等 MJML 标签;MJML 标签在 mj-raw 内不会被编译。简单链接建议用「文本」块。',
351
+ 'blocks.help.htmlOnly':
352
+ '只写 HTML(如 <a>、<table>),不要写 <mj-text> 等 MJML 标签;MJML 标签在 mj-raw 内不会被编译。简单链接建议用「文本」块。',
316
353
  'blocks.help.imageFallbackColor': '图片加载失败时的底色',
317
- 'blocks.help.imageAutoHeight': '0 为自适应高度;填写固定值时画布与 MJML 会限制显示高度(预览可用 object-fit: cover)',
354
+ 'blocks.help.imageAutoHeight':
355
+ '0 为自适应高度;填写固定值时画布与 MJML 会限制显示高度(预览可用 object-fit: cover)',
318
356
  'blocks.help.buttonWidth': '支持纯数字(按 px)、带 px 或百分比',
319
- 'blocks.help.socialLinks': '每行:平台、链接、可选标签文字、自定义图标 URL、图标背景色。标签的字号/字重/颜色在下方统一设置。',
320
- 'blocks.help.bizSocialLinks': '平台:X、Rabbit、Facebook、TikTok、Instagram (ig)。可填标签、自定义图标 URL。MJML 侧 Rabbit/TikTok 使用 web 占位图标,需要品牌图时请填「图标 URL」。',
321
- 'blocks.help.iconRadius': '0 为方形直角;填较大值(如 999)或与尺寸一半相当时为圆形(默认圆形)。',
357
+ 'blocks.help.socialLinks':
358
+ '每行:平台、链接、可选标签文字、自定义图标 URL、图标背景色。标签的字号/字重/颜色在下方统一设置。',
359
+ 'blocks.help.bizSocialLinks':
360
+ '平台:X、Rabbit、Facebook、TikTok、Instagram (ig)。可填标签、自定义图标 URL。MJML 侧 Rabbit/TikTok 使用 web 占位图标,需要品牌图时请填「图标 URL」。',
361
+ 'blocks.help.iconRadius':
362
+ '0 为方形直角;填较大值(如 999)或与尺寸一半相当时为圆形(默认圆形)。',
322
363
  'blocks.help.iconSpacing': '每个图标周围的留白,横向即图标之间的间隔(MJML inner-padding)。',
323
364
  'blocks.help.bizIconSpacing': '图标之间的间隔(MJML inner-padding)。',
324
365
  'blocks.placeholder.buttonWidth': '留空=自适应,如 280px、100%',
@@ -372,7 +413,8 @@ export const enUSMessages: SimpleMailMessages = {
372
413
  'topbar.resetContent': 'Reset content',
373
414
  'topbar.copyDesignTitle': 'Copy design: write the canvas JSON to the clipboard for reuse',
374
415
  'topbar.copyDesign': 'Copy design',
375
- 'topbar.importDesignTitle': 'Import design: paste JSON or read from the clipboard to replace the canvas',
416
+ 'topbar.importDesignTitle':
417
+ 'Import design: paste JSON or read from the clipboard to replace the canvas',
376
418
  'topbar.importDesign': 'Import design',
377
419
  'topbar.preview': 'Preview',
378
420
  'topbar.exportHtml': 'Export HTML',
@@ -383,6 +425,9 @@ export const enUSMessages: SimpleMailMessages = {
383
425
  'topbar.hideLayoutBorders': 'Hide section/block borders',
384
426
  'topbar.fullscreen': 'Fullscreen',
385
427
  'topbar.exitFullscreen': 'Exit fullscreen',
428
+ 'topbar.sectionSelect':
429
+ 'Select sections / blocks: batch copy / export / delete (⌘/Ctrl+click, Shift+click for range)',
430
+ 'topbar.sectionSelectActive': 'Exit selection mode (Shift+click to select a range)',
386
431
  'leftPanel.layout': 'Layout',
387
432
  'leftPanel.components': 'Components',
388
433
  'leftPanel.sectionBlocks': 'Layout Blocks',
@@ -394,10 +439,29 @@ export const enUSMessages: SimpleMailMessages = {
394
439
  'canvas.addSection': '+ Add section:',
395
440
  'canvas.dynamicSectionLabel': 'Dynamic variable {index} · {token}',
396
441
  'canvas.sectionLabel': 'Section {index} · {layout}',
397
- 'canvas.sectionTitle': '{chip}. When child blocks fill the column, press Esc or Alt-click a block to select this section.',
442
+ 'canvas.sectionTitle':
443
+ '{chip}. When child blocks fill the column, press Esc or Alt-click a block to select this section.',
398
444
  'canvas.unknownBlock': 'Unknown block: {type}',
399
445
  'canvas.emptyColumnHint': 'Drag in a block · Double-click to insert text',
400
446
  'canvas.lockedCode': 'Locked as code',
447
+ 'sectionSelect.count': '{count} section(s) selected',
448
+ 'sectionSelect.countBlocks': '{count} block(s) selected',
449
+ 'sectionSelect.countMixed': '{sections} section(s) · {blocks} block(s) selected',
450
+ 'sectionSelect.selectAll': 'Select all',
451
+ 'sectionSelect.unselectAll': 'Deselect all',
452
+ 'sectionSelect.copyDesign': 'Copy design',
453
+ 'sectionSelect.exportJson': 'Export JSON',
454
+ 'sectionSelect.done': 'Done',
455
+ 'sectionSelect.deleteConfirm':
456
+ 'Delete the {count} selected section(s)? You can undo to restore them.',
457
+ 'sectionSelect.deleteConfirmBlocks':
458
+ 'Delete the {count} selected block(s)? You can undo to restore them.',
459
+ 'sectionSelect.deleteConfirmMixed':
460
+ 'Delete the {sections} selected section(s) and {blocks} block(s)? You can undo to restore them.',
461
+ 'canvas.copySectionDesignTitle':
462
+ 'Copy section design: write this section as JSON to clipboard, appendable on another canvas',
463
+ 'canvas.copyBlockDesignTitle':
464
+ 'Copy block design: write this block as JSON to clipboard, appendable on another canvas',
401
465
  'rightPanel.docRoot.mail': 'Email',
402
466
  'rightPanel.docRoot.layout': 'Layout',
403
467
  'rightPanel.breadcrumb.selected': 'Current selection',
@@ -406,12 +470,15 @@ export const enUSMessages: SimpleMailMessages = {
406
470
  'rightPanel.layout.columns': 'Columns',
407
471
  'rightPanel.tabs.props': 'Properties',
408
472
  'rightPanel.tabs.code': 'Code',
409
- 'rightPanel.block.deleted': 'This block may have been deleted. Select a block on the canvas again.',
473
+ 'rightPanel.block.deleted':
474
+ 'This block may have been deleted. Select a block on the canvas again.',
410
475
  'rightPanel.block.locked': 'This block is locked in code mode.',
411
476
  'rightPanel.block.restoreProps': 'Restore property mode',
412
477
  'rightPanel.block.unregistered': 'Unregistered block: {type}',
413
- 'rightPanel.block.mjmlWarning': 'MJML tags were detected, such as <mj-text>. Write HTML only here, otherwise the preview will show source code. Use a Text block for simple links.',
414
- 'rightPanel.block.invalidLink': 'This link is not navigable (e.g. 1234, #, or a URL without https://). Outlook on the web removes such links, which makes the button or icon invisible.',
478
+ 'rightPanel.block.mjmlWarning':
479
+ 'MJML tags were detected, such as <mj-text>. Write HTML only here, otherwise the preview will show source code. Use a Text block for simple links.',
480
+ 'rightPanel.block.invalidLink':
481
+ 'This link is not navigable (e.g. 1234, #, or a URL without https://). Outlook on the web removes such links, which makes the button or icon invisible.',
415
482
  'rightPanel.doc.title': 'Email settings',
416
483
  'rightPanel.doc.layoutTitle': 'Layout',
417
484
  'rightPanel.doc.subject': 'Subject',
@@ -431,7 +498,8 @@ export const enUSMessages: SimpleMailMessages = {
431
498
  'rightPanel.doc.listIndent': 'Default list indent',
432
499
  'rightPanel.doc.listIndentHelp': 'Lists without their own indent use this value.',
433
500
  'rightPanel.width.auto': 'Auto',
434
- 'rightPanel.width.customHelp': 'This value uses custom syntax. Edit it directly, or clear it to choose Auto and use the slider.',
501
+ 'rightPanel.width.customHelp':
502
+ 'This value uses custom syntax. Edit it directly, or clear it to choose Auto and use the slider.',
435
503
  'rightPanel.width.customPlaceholder': 'e.g. 480px or 90%',
436
504
  'rightPanel.inheritGlobal': 'Inherit global',
437
505
  'rightPanel.spacing.suffix': ' (top/right/bottom/left)',
@@ -443,8 +511,10 @@ export const enUSMessages: SimpleMailMessages = {
443
511
  'rightPanel.section.title': 'Section settings',
444
512
  'rightPanel.section.dynamicTitle': 'Dynamic variable section',
445
513
  'rightPanel.section.dynamicKey': 'Dynamic variable name',
446
- 'rightPanel.section.dynamicKeyPlaceholder': 'Use with care. Confirm the business flow supports it.',
447
- 'rightPanel.section.dynamicKeyHelp': 'This whole section will be replaced by the dynamic variable name.',
514
+ 'rightPanel.section.dynamicKeyPlaceholder':
515
+ 'Use with care. Confirm the business flow supports it.',
516
+ 'rightPanel.section.dynamicKeyHelp':
517
+ 'This whole section will be replaced by the dynamic variable name.',
448
518
  'rightPanel.section.backgroundColor': 'Background color',
449
519
  'rightPanel.section.transparentPlaceholder': 'Empty = transparent',
450
520
  'rightPanel.section.width': 'Section width',
@@ -452,9 +522,11 @@ export const enUSMessages: SimpleMailMessages = {
452
522
  'rightPanel.section.padding': 'Padding',
453
523
  'rightPanel.section.columnGap': 'Column gap (px)',
454
524
  'rightPanel.section.stackedGap': 'Stacked vertical gap (px)',
455
- 'rightPanel.section.stackedGapHelp': 'Extra vertical space between columns once they stack on small screens. 0 keeps each block own padding.',
525
+ 'rightPanel.section.stackedGapHelp':
526
+ 'Extra vertical space between columns once they stack on small screens. 0 keeps each block own padding.',
456
527
  'rightPanel.section.preserveMobile': 'Keep columns side by side on small screens',
457
- 'rightPanel.section.preserveMobileHelp': 'When enabled, MJML outputs mj-group. Mobile preview/export differs from the default stacked-column behavior.',
528
+ 'rightPanel.section.preserveMobileHelp':
529
+ 'When enabled, MJML outputs mj-group. Mobile preview/export differs from the default stacked-column behavior.',
458
530
  'rightPanel.section.columnLayout': 'Column layout',
459
531
  'rightPanel.social.labelPlaceholder': 'Label text (optional)',
460
532
  'rightPanel.social.iconUrlPlaceholder': 'Icon URL (optional)',
@@ -468,14 +540,17 @@ export const enUSMessages: SimpleMailMessages = {
468
540
  'rightPanel.image.helpGallery': 'or use Gallery',
469
541
  'rightPanel.image.helpSeparator': '; ',
470
542
  'rightPanel.image.helpEnd': '.',
471
- 'rightPanel.image.helpAssetsConfigured': 'imageAssets is configured: upload is shown by default when uploadImage exists and showUpload is not false; gallery requires showGallery:true and either imageGallery or pickImageFromGallery. Manual URLs are still supported.',
472
- 'rightPanel.image.helpNoAssets': 'Configure uploadImage, imageGallery, or pickImageFromGallery in MailEditor imageAssets. Without them, only manual URLs are supported.',
543
+ 'rightPanel.image.helpAssetsConfigured':
544
+ 'imageAssets is configured: upload is shown by default when uploadImage exists and showUpload is not false; gallery requires showGallery:true and either imageGallery or pickImageFromGallery. Manual URLs are still supported.',
545
+ 'rightPanel.image.helpNoAssets':
546
+ 'Configure uploadImage, imageGallery, or pickImageFromGallery in MailEditor imageAssets. Without them, only manual URLs are supported.',
473
547
  'rightPanel.color.unset': 'Not set',
474
548
  'rightPanel.fontSize.placeholder': 'e.g. 16px',
475
549
  'rightPanel.code.title': 'Edit block MJML source',
476
550
  'rightPanel.code.restoreDefault': 'Restore default',
477
551
  'rightPanel.code.saveLocked': 'Save as locked MJML',
478
- 'rightPanel.code.help': 'After saving, this block will output this MJML directly, and the properties panel will be disabled. Use "Restore default" to unlock it.',
552
+ 'rightPanel.code.help':
553
+ 'After saving, this block will output this MJML directly, and the properties panel will be disabled. Use "Restore default" to unlock it.',
479
554
  'toolbar.fontDefault': 'Default',
480
555
  'toolbar.fontPingFang': 'PingFang SC',
481
556
  'toolbar.fontMicrosoftYaHei': 'Microsoft YaHei',
@@ -530,14 +605,19 @@ export const enUSMessages: SimpleMailMessages = {
530
605
  'colorPicker.doubleClickConfirm': '{hex} (double-click to confirm)',
531
606
  'colorPicker.openTitle': 'Click to open the palette. Double-click a swatch to confirm.',
532
607
  'importDoc.title': 'Import design',
533
- 'importDoc.hint1': 'This will replace all sections and blocks on the current canvas, including global styles.',
608
+ 'importDoc.hint1':
609
+ 'This will replace all sections and blocks on the current canvas, including global styles.',
534
610
  'importDoc.hint2': 'If the host manages subject or width, those fields may remain unchanged.',
535
611
  'importDoc.hint3': 'Paste JSON generated by "Copy design", or a raw EmailDoc with version: "1".',
536
612
  'importDoc.placeholder': 'Paste design JSON here...',
537
613
  'importDoc.pasteFromClipboard': 'Paste from clipboard',
538
614
  'importDoc.apply': 'Apply and replace canvas',
615
+ 'importDoc.applyAppend': 'Append to canvas',
616
+ 'importDoc.hintSelection':
617
+ 'Partial design detected. It will be appended to the current canvas (existing content is kept); unregistered block types are skipped.',
539
618
  'importDoc.clipboardEmpty': 'Clipboard is empty or unreadable. Paste JSON manually.',
540
- 'importDoc.clipboardFilled': 'Filled from clipboard. Review it, then apply and replace the canvas.',
619
+ 'importDoc.clipboardFilled':
620
+ 'Filled from clipboard. Review it, then apply and replace the canvas.',
541
621
  'importDoc.empty': 'Paste design JSON first.',
542
622
  'importDoc.invalid': 'Invalid JSON or not a simple-mail design.',
543
623
  'blockCode.title': 'Edit block code',
@@ -545,6 +625,14 @@ export const enUSMessages: SimpleMailMessages = {
545
625
  'toast.copyDesignOk': 'Design copied to clipboard',
546
626
  'toast.copyDesignFailed': 'Copy failed. Check browser clipboard permissions.',
547
627
  'toast.importDesignOk': 'Design imported',
628
+ 'toast.copySelectionOk': 'Partial design copied to clipboard',
629
+ 'toast.copySelectionNoTarget': 'Select a section or block to copy first',
630
+ 'toast.pasteSelectionOk': 'Appended to the current canvas',
631
+ 'toast.pasteSelectionEmpty': 'The partial design is empty',
632
+ 'toast.pasteSelectionSkipped': 'Skipped unregistered block types: {types}',
633
+ 'toast.exportSelectionOk': 'Selected content exported as design JSON',
634
+ 'toast.removeSectionsOk': 'Deleted {count} section(s)',
635
+ 'toast.removeCheckedOk': 'Deleted {sections} section(s) and {blocks} block(s)',
548
636
  'toast.noVariables': 'No variables available',
549
637
  'toast.variableCopied': 'Variable copied',
550
638
  'toast.copiedToClipboard': 'Copied to clipboard',
@@ -561,9 +649,11 @@ export const enUSMessages: SimpleMailMessages = {
561
649
  'blocks.social.name': 'Social card',
562
650
  'blocks.footer.name': 'Footer',
563
651
  'blocks.button.defaultText': 'View now',
564
- 'blocks.text.defaultContent': '<p style="margin:0;">Enter text here. <strong>Double-click</strong> to edit, select text to show the toolbar, and use variables like {{user.name}}.</p>',
565
- 'blocks.html.defaultHtml': '<p style="margin:0;">Write any HTML here. It will be output through <strong>mj-raw</strong>. Variables like {{user.name}} are supported.</p>',
566
- 'blocks.hero.defaultContent': '<p style="margin:0;font-size:28px;font-weight:700;line-height:1.25;">Main headline on the hero image</p><p style="margin:12px 0 0;font-size:16px;font-weight:400;opacity:0.95;line-height:1.5;">Supports variables and <strong>double-click editing</strong>, just like text blocks.</p>',
652
+ 'blocks.text.defaultContent': '<p>Double-click to edit...</p>',
653
+ 'blocks.html.defaultHtml':
654
+ '<p style="margin:0;">Write any HTML here. It will be output through <strong>mj-raw</strong>. Variables like {{user.name}} are supported.</p>',
655
+ 'blocks.hero.defaultContent':
656
+ '<p style="margin:0;font-size:28px;font-weight:700;line-height:1.25;">Main headline on the hero image</p><p style="margin:12px 0 0;font-size:16px;font-weight:400;opacity:0.95;line-height:1.5;">Supports variables and <strong>double-click editing</strong>, just like text blocks.</p>',
567
657
  'blocks.footer.defaultCompanyName': 'Your company',
568
658
  'blocks.footer.defaultAddress': '123 Example Street, Shanghai',
569
659
  'blocks.footer.defaultUnsubscribeText': 'Unsubscribe',
@@ -620,16 +710,24 @@ export const enUSMessages: SimpleMailMessages = {
620
710
  'blocks.field.htmlPlaceholder': 'Enter HTML...',
621
711
  'blocks.field.heroPlaceholder': 'Write copy over the image...',
622
712
  'blocks.help.globalLineHeight': 'Leave empty to use the global line height from email settings.',
623
- 'blocks.help.canvasEditRecommended': 'Double-click on the canvas to edit directly. This field remains as an advanced fallback.',
624
- 'blocks.help.canvasEditRecommendedShort': 'Double-click on the canvas to edit. Use this field for advanced edits.',
625
- 'blocks.help.htmlOnly': 'Write HTML only, such as <a> or <table>. Do not use MJML tags like <mj-text>; MJML tags inside mj-raw will not compile. Use a Text block for simple links.',
713
+ 'blocks.help.canvasEditRecommended':
714
+ 'Double-click on the canvas to edit directly. This field remains as an advanced fallback.',
715
+ 'blocks.help.canvasEditRecommendedShort':
716
+ 'Double-click on the canvas to edit. Use this field for advanced edits.',
717
+ 'blocks.help.htmlOnly':
718
+ 'Write HTML only, such as <a> or <table>. Do not use MJML tags like <mj-text>; MJML tags inside mj-raw will not compile. Use a Text block for simple links.',
626
719
  'blocks.help.imageFallbackColor': 'Background color used if the image fails to load.',
627
- 'blocks.help.imageAutoHeight': '0 means automatic height. Fixed values limit both canvas and MJML display height; preview uses object-fit: cover.',
720
+ 'blocks.help.imageAutoHeight':
721
+ '0 means automatic height. Fixed values limit both canvas and MJML display height; preview uses object-fit: cover.',
628
722
  'blocks.help.buttonWidth': 'Supports plain numbers as px, px values, or percentages.',
629
- 'blocks.help.socialLinks': 'Each row contains a network, link, optional label text, custom icon URL, and icon background color. Label font size, weight, and color are configured below.',
630
- 'blocks.help.bizSocialLinks': 'Networks: X, Rabbit, Facebook, TikTok, and Instagram (ig). Labels and custom icon URLs are optional. Rabbit/TikTok use the web placeholder icon in MJML unless you provide an icon URL.',
631
- 'blocks.help.iconRadius': '0 gives square corners. Large values such as 999, or about half the icon size, make it circular.',
632
- 'blocks.help.iconSpacing': 'Space around each icon; horizontally this is the gap between icons (MJML inner-padding).',
723
+ 'blocks.help.socialLinks':
724
+ 'Each row contains a network, link, optional label text, custom icon URL, and icon background color. Label font size, weight, and color are configured below.',
725
+ 'blocks.help.bizSocialLinks':
726
+ 'Networks: X, Rabbit, Facebook, TikTok, and Instagram (ig). Labels and custom icon URLs are optional. Rabbit/TikTok use the web placeholder icon in MJML unless you provide an icon URL.',
727
+ 'blocks.help.iconRadius':
728
+ '0 gives square corners. Large values such as 999, or about half the icon size, make it circular.',
729
+ 'blocks.help.iconSpacing':
730
+ 'Space around each icon; horizontally this is the gap between icons (MJML inner-padding).',
633
731
  'blocks.help.bizIconSpacing': 'Gap between icons (MJML inner-padding).',
634
732
  'blocks.placeholder.buttonWidth': 'Empty = auto, e.g. 280px or 100%',
635
733
  'blocks.option.top': 'Top',
@@ -105,13 +105,23 @@ export { resolveAssetPermission } from './editor/imageAssets';
105
105
  export type { OpenImageGalleryModalOptions } from './editor/ImageGalleryModal';
106
106
  export { openImageGalleryModal } from './editor/ImageGalleryModal';
107
107
  export {
108
+ collectSelectionVariableKeys,
108
109
  DOC_CLIPBOARD_FORMAT_VERSION,
109
110
  DOC_CLIPBOARD_KIND,
110
111
  parseDocClipboard,
112
+ parseSelectionClipboard,
113
+ regenerateBlockId,
111
114
  regenerateDocIds,
115
+ remapSectionIds,
116
+ SELECTION_CLIPBOARD_FORMAT_VERSION,
117
+ SELECTION_CLIPBOARD_KIND,
112
118
  serializeDocClipboard,
119
+ serializeSelectionClipboard,
120
+ } from './utils/docClipboard';
121
+ export type {
122
+ DocClipboardEnvelope,
123
+ SelectionClipboardEnvelope,
113
124
  } from './utils/docClipboard';
114
- export type { DocClipboardEnvelope } from './utils/docClipboard';
115
125
  export {
116
126
  blockButtonWidthCss,
117
127
  docContentWidthCss,
@@ -1,12 +1,9 @@
1
1
  import mjmlCompile from 'mjml-browser';
2
- import type { EmailDoc, RenderEngine, Variable } from '../types';
3
2
  import type { Registry } from '../registry/registry';
4
- import { annotateDynamicVariantHtmlAttributes } from '../utils/dynamicVariantHtml';
5
- import {
6
- normalizeEmailListsInHtml,
7
- resolveGlobalListIndentPx,
8
- } from '../utils/emailListStyles';
3
+ import type { EmailDoc, RenderEngine, Variable } from '../types';
9
4
  import { inlineColumnWidthsInHtml } from '../utils/columnWidths';
5
+ import { annotateDynamicVariantHtmlAttributes } from '../utils/dynamicVariantHtml';
6
+ import { normalizeEmailListsInHtml, resolveGlobalListIndentPx } from '../utils/emailListStyles';
10
7
  import { docToMjml } from './mjml';
11
8
 
12
9
  export interface RenderResult {
@@ -1,9 +1,9 @@
1
1
  import type { Registry } from '../registry/registry';
2
2
  import type { Column, EmailDoc, RenderContext, Section, SectionLayout } from '../types';
3
- import { getSectionDynamicVariantKey, sectionMjClassName } from '../utils/dynamicVariantKey';
4
- import { blockButtonWidthCss, docContentWidthCss } from '../utils/contentWidth';
5
3
  import { COLUMN_NO_STACK_CLASS } from '../utils/columnWidths';
4
+ import { blockButtonWidthCss, docContentWidthCss } from '../utils/contentWidth';
6
5
  import { escapeAttr } from '../utils/dom';
6
+ import { getSectionDynamicVariantKey, sectionMjClassName } from '../utils/dynamicVariantKey';
7
7
  import { normalizeFontWeightStep } from '../utils/fontWeightSteps';
8
8
  import {
9
9
  mjRawCellTypographyFromStyles,
@@ -124,8 +124,7 @@ function sectionToMjml(section: Section, registry: Registry, ctx: RenderContext)
124
124
  const bg = a.backgroundColor ? ` background-color="${escapeAttr(a.backgroundColor)}"` : '';
125
125
  const secW = blockButtonWidthCss(a.width);
126
126
  const dvKey = getSectionDynamicVariantKey(section);
127
- const secCls =
128
- secW || dvKey ? ` css-class="${escapeAttr(sectionMjClassName(section.id))}"` : '';
127
+ const secCls = secW || dvKey ? ` css-class="${escapeAttr(sectionMjClassName(section.id))}"` : '';
129
128
  const widths = layoutWidths(section.layout);
130
129
  const gapPx = Math.max(0, section.attrs.columnGap ?? 0);
131
130
  /** 仅小屏堆叠时才有水平复位与纵向间距;与下方 grouped 判定保持一致 */
@@ -149,11 +148,7 @@ function sectionToMjml(section: Section, registry: Registry, ctx: RenderContext)
149
148
  i,
150
149
  section.columns.length,
151
150
  gapPx,
152
- [
153
- hCls,
154
- vCls && i < lastIndex ? vCls : '',
155
- preserveColumns ? COLUMN_NO_STACK_CLASS : '',
156
- ]
151
+ [hCls, vCls && i < lastIndex ? vCls : '', preserveColumns ? COLUMN_NO_STACK_CLASS : '']
157
152
  .filter(Boolean)
158
153
  .join(' '),
159
154
  registry,
@@ -142,9 +142,7 @@ export function pruneSectionIfEmpty(draft: EmailDoc, sectionId: string) {
142
142
 
143
143
  /** @deprecated 请改用 {@link pruneSectionIfEmpty},避免误删多个空 Section */
144
144
  export function pruneEmptySections(draft: EmailDoc) {
145
- draft.sections = draft.sections.filter((sec) =>
146
- sec.columns.some((col) => col.blocks.length > 0),
147
- );
145
+ draft.sections = draft.sections.filter((sec) => sec.columns.some((col) => col.blocks.length > 0));
148
146
  }
149
147
 
150
148
  /** 多列 Section 的列布局(左栏拖入默认 `1-1`,右栏可切换列数/比例) */
@@ -156,9 +154,7 @@ export const TWO_COLUMN_LAYOUTS = ['1-1', '1-2', '2-1'] as const;
156
154
  export type TwoColumnLayout = (typeof TWO_COLUMN_LAYOUTS)[number];
157
155
 
158
156
  export function isMultiColumnLayout(layout: SectionLayout): layout is MultiColumnLayout {
159
- return (
160
- layout === '1-1' || layout === '1-2' || layout === '2-1' || layout === '1-1-1'
161
- );
157
+ return layout === '1-1' || layout === '1-2' || layout === '2-1' || layout === '1-1-1';
162
158
  }
163
159
 
164
160
  /** @deprecated 使用 isMultiColumnLayout */
@@ -215,9 +215,7 @@ export interface BlockDefinition<P extends object = Record<string, unknown>> {
215
215
  * 用于「组合模板」:画布内仅为通用 image/text 等,便于逐项修改。
216
216
  * 文档里不会出现本定义的 `type`,仅作左栏入口;若需提供请返回内置块的实例。
217
217
  */
218
- expandPaletteDrop?: (
219
- createBlock: (type: string) => Block,
220
- ) => Block[] | PaletteDropResult;
218
+ expandPaletteDrop?: (createBlock: (type: string) => Block) => Block[] | PaletteDropResult;
221
219
  }
222
220
 
223
221
  /** 左栏组合拖入:可附带新建 Section 的 attrs、布局和按列分发的 Block。 */
@@ -295,6 +293,8 @@ export interface EditorUiOptions {
295
293
  hideTopbarDocClipboard?: boolean;
296
294
  /** 为 true 时隐藏顶栏「插入变量」按钮(由宿主在编辑器外提供时可用)。 */
297
295
  hideTopbarInsertVariable?: boolean;
296
+ /** 为 true 时隐藏顶栏「选择节」入口(Section 批量复制 / 导出 / 删除的选择模式)。 */
297
+ hideTopbarSectionSelect?: boolean;
298
298
  /**
299
299
  * 为 true 时顶栏默认可切换为仅图标(文案收入 `title` / `aria-label`),适合窄屏或嵌入宿主。
300
300
  * 默认 false;嵌入场景可在宿主侧设为 true。
@@ -30,9 +30,9 @@ export function rgbCssToHex(rgb: string): string | null {
30
30
  export function accentPrimarySoftRgba(primaryHex: string, isDarkChrome: boolean): string {
31
31
  const hex = normalizeAccentHex(primaryHex);
32
32
  if (!hex) return isDarkChrome ? 'rgba(129, 140, 248, 0.18)' : 'rgba(79, 70, 229, 0.12)';
33
- const r = parseInt(hex.slice(1, 3), 16);
34
- const g = parseInt(hex.slice(3, 5), 16);
35
- const b = parseInt(hex.slice(5, 7), 16);
33
+ const r = Number.parseInt(hex.slice(1, 3), 16);
34
+ const g = Number.parseInt(hex.slice(3, 5), 16);
35
+ const b = Number.parseInt(hex.slice(5, 7), 16);
36
36
  const a = isDarkChrome ? 0.22 : 0.14;
37
37
  return `rgba(${r},${g},${b},${a})`;
38
38
  }