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
@@ -1,9 +1,9 @@
1
+ import type { SimpleMailT } from '../i18n';
1
2
  import type { Store } from '../store/store';
2
- import { bindColorPickerInput } from './ColorPickerPopover';
3
3
  import { normalizeAccentHex, rgbCssToHex } from '../utils/accentColor';
4
- import { h, clear } from '../utils/dom';
4
+ import { clear, h } from '../utils/dom';
5
+ import { bindColorPickerInput } from './ColorPickerPopover';
5
6
  import type { EditorTheme } from './theme';
6
- import type { SimpleMailT } from '../i18n';
7
7
 
8
8
  export type EditorMode = 'design' | 'source';
9
9
 
@@ -32,6 +32,8 @@ export interface TopbarOptions {
32
32
  onFullscreenToggle?: () => void;
33
33
  /** 切换画布 Section / Block 布局虚线边框 */
34
34
  onLayoutBordersToggle?: () => void;
35
+ /** 进入 / 退出「选择节」批量操作模式(批量复制 / 导出 / 删除 Section) */
36
+ onSectionSelectToggle?: () => void;
35
37
  /** 为 false 时不展示全屏按钮(也可用 `ui.hideTopbarFullscreen`) */
36
38
  showFullscreenButton?: boolean;
37
39
  /** 为 false 时不展示「邮件设置」按钮(也可用 `ui.hideTopbarMailSettings`) */
@@ -62,6 +64,7 @@ export class Topbar {
62
64
  private accentInput?: HTMLInputElement;
63
65
  private fullscreenBtn?: HTMLButtonElement;
64
66
  private layoutBordersBtn?: HTMLButtonElement;
67
+ private sectionSelectBtn?: HTMLButtonElement;
65
68
  private insertVariableBtn?: HTMLButtonElement;
66
69
 
67
70
  constructor(opts: TopbarOptions) {
@@ -97,7 +100,9 @@ export class Topbar {
97
100
  /** 根据 `.sm-root` 上计算后的 `--sm-primary` 更新拾取器显示(无拾取器或未挂 root 时忽略) */
98
101
  syncAccentPicker() {
99
102
  if (!this.accentInput || !this.opts.accentPickerRoot) return;
100
- const raw = getComputedStyle(this.opts.accentPickerRoot).getPropertyValue('--sm-primary').trim();
103
+ const raw = getComputedStyle(this.opts.accentPickerRoot)
104
+ .getPropertyValue('--sm-primary')
105
+ .trim();
101
106
  const base = normalizeAccentHex(raw) || rgbCssToHex(raw);
102
107
  if (!base) return;
103
108
  const norm = normalizeAccentHex(base);
@@ -117,12 +122,26 @@ export class Topbar {
117
122
  btn.setAttribute('aria-pressed', active ? 'true' : 'false');
118
123
  }
119
124
 
125
+ /** 「选择节」批量模式激活时高亮顶栏按钮(按钮不存在时忽略) */
126
+ setSectionSelectActive(active: boolean) {
127
+ const btn = this.sectionSelectBtn;
128
+ if (!btn) return;
129
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
130
+ const title = active
131
+ ? this.opts.t('topbar.sectionSelectActive')
132
+ : this.opts.t('topbar.sectionSelect');
133
+ btn.title = title;
134
+ btn.setAttribute('aria-label', title);
135
+ }
136
+
120
137
  /** 与画布布局边框开关同步按钮高亮(按钮不存在时忽略) */
121
138
  setLayoutBordersActive(active: boolean) {
122
139
  const btn = this.layoutBordersBtn;
123
140
  if (!btn) return;
124
141
  btn.setAttribute('aria-pressed', active ? 'true' : 'false');
125
- const title = active ? this.opts.t('topbar.hideLayoutBorders') : this.opts.t('topbar.showLayoutBorders');
142
+ const title = active
143
+ ? this.opts.t('topbar.hideLayoutBorders')
144
+ : this.opts.t('topbar.showLayoutBorders');
126
145
  btn.title = title;
127
146
  btn.setAttribute('aria-label', title);
128
147
  }
@@ -139,7 +158,11 @@ export class Topbar {
139
158
  clear(this.el);
140
159
  const t = this.opts.t;
141
160
 
142
- const segmented = h('div', { class: 'sm-segmented sm-topbar__mode-seg', role: 'group', 'aria-label': t('topbar.mode') });
161
+ const segmented = h('div', {
162
+ class: 'sm-segmented sm-topbar__mode-seg',
163
+ role: 'group',
164
+ 'aria-label': t('topbar.mode'),
165
+ });
143
166
  const designBtn = modeSegBtn({
144
167
  active: this.opts.mode === 'design',
145
168
  title: t('topbar.design'),
@@ -219,7 +242,10 @@ export class Topbar {
219
242
  });
220
243
 
221
244
  const contentActionKids: HTMLElement[] = [];
222
- if (this.opts.showClearCanvasButton !== false && typeof this.opts.onClearCanvas === 'function') {
245
+ if (
246
+ this.opts.showClearCanvasButton !== false &&
247
+ typeof this.opts.onClearCanvas === 'function'
248
+ ) {
223
249
  contentActionKids.push(
224
250
  actionBtn({
225
251
  class: 'sm-btn--ghost',
@@ -300,15 +326,15 @@ export class Topbar {
300
326
  );
301
327
  }
302
328
  if (this.opts.showInsertVariableButton !== false) {
303
- trailingGroup.push(
304
- (this.insertVariableBtn = actionBtn({
305
- class: 'sm-topbar__insert-var',
306
- title: t('topbar.insertVariable'),
307
- icon: iconVariable(),
308
- label: t('topbar.insertVariable'),
309
- onclick: (e: Event) => this.opts.onInsertVariable(e.currentTarget as HTMLElement),
310
- })),
311
- );
329
+ const insertVariableBtn = actionBtn({
330
+ class: 'sm-topbar__insert-var',
331
+ title: t('topbar.insertVariable'),
332
+ icon: iconVariable(),
333
+ label: t('topbar.insertVariable'),
334
+ onclick: (e: Event) => this.opts.onInsertVariable(e.currentTarget as HTMLElement),
335
+ });
336
+ this.insertVariableBtn = insertVariableBtn;
337
+ trailingGroup.push(insertVariableBtn);
312
338
  } else {
313
339
  this.insertVariableBtn = undefined;
314
340
  }
@@ -316,16 +342,29 @@ export class Topbar {
316
342
 
317
343
  const canvasToolKids: HTMLElement[] = [];
318
344
 
345
+ if (typeof this.opts.onSectionSelectToggle === 'function') {
346
+ this.sectionSelectBtn = iconOnlyBtn({
347
+ class: 'sm-btn--ghost sm-topbar__toggle-btn',
348
+ title: t('topbar.sectionSelect'),
349
+ icon: iconSectionSelect(),
350
+ pressed: false,
351
+ onclick: () => this.opts.onSectionSelectToggle!(),
352
+ });
353
+ canvasToolKids.push(this.sectionSelectBtn);
354
+ } else {
355
+ this.sectionSelectBtn = undefined;
356
+ }
357
+
319
358
  if (typeof this.opts.onLayoutBordersToggle === 'function') {
320
- canvasToolKids.push(
321
- (this.layoutBordersBtn = iconOnlyBtn({
322
- class: 'sm-btn--ghost sm-topbar__toggle-btn',
323
- title: t('topbar.showLayoutBorders'),
324
- icon: iconLayoutBorders(),
325
- pressed: false,
326
- onclick: () => this.opts.onLayoutBordersToggle!(),
327
- }) as HTMLButtonElement),
328
- );
359
+ const layoutBordersBtn = iconOnlyBtn({
360
+ class: 'sm-btn--ghost sm-topbar__toggle-btn',
361
+ title: t('topbar.showLayoutBorders'),
362
+ icon: iconLayoutBorders(),
363
+ pressed: false,
364
+ onclick: () => this.opts.onLayoutBordersToggle!(),
365
+ }) as HTMLButtonElement;
366
+ this.layoutBordersBtn = layoutBordersBtn;
367
+ canvasToolKids.push(layoutBordersBtn);
329
368
  } else {
330
369
  this.layoutBordersBtn = undefined;
331
370
  }
@@ -334,15 +373,15 @@ export class Topbar {
334
373
  this.opts.showFullscreenButton !== false &&
335
374
  typeof this.opts.onFullscreenToggle === 'function';
336
375
  if (showFs) {
337
- canvasToolKids.push(
338
- (this.fullscreenBtn = iconOnlyBtn({
339
- class: 'sm-btn--ghost sm-topbar__toggle-btn',
340
- title: t('topbar.fullscreen'),
341
- icon: iconFullscreenEnter(),
342
- pressed: false,
343
- onclick: () => this.opts.onFullscreenToggle!(),
344
- }) as HTMLButtonElement),
345
- );
376
+ const fullscreenBtn = iconOnlyBtn({
377
+ class: 'sm-btn--ghost sm-topbar__toggle-btn',
378
+ title: t('topbar.fullscreen'),
379
+ icon: iconFullscreenEnter(),
380
+ pressed: false,
381
+ onclick: () => this.opts.onFullscreenToggle!(),
382
+ }) as HTMLButtonElement;
383
+ this.fullscreenBtn = fullscreenBtn;
384
+ canvasToolKids.push(fullscreenBtn);
346
385
  } else {
347
386
  this.fullscreenBtn = undefined;
348
387
  }
@@ -508,6 +547,12 @@ function iconImportDoc(): SVGElement {
508
547
  '<path d="M10 4.5v6.5M7.2 9.2 10 12l2.8-2.8M5.5 14.5h9" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/><rect x="4" y="14" width="12" height="2.5" rx=".6" fill="currentColor"/>',
509
548
  );
510
549
  }
550
+ /** 选择节(批量操作):勾选列表 */
551
+ function iconSectionSelect(): SVGElement {
552
+ return svg(
553
+ '<rect x="3" y="3.5" width="14" height="13" rx="2" stroke="currentColor" stroke-width="1.4" fill="none"/><path d="M6.2 10l2.2 2.2 4.6-5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>',
554
+ );
555
+ }
511
556
  /** 显示边框:圆角矩形描边(开启时由 toggle 样式点亮 currentColor) */
512
557
  function iconLayoutBorders(): SVGElement {
513
558
  return svgPathFill(
@@ -1,7 +1,7 @@
1
+ import type { SimpleMailT } from '../i18n';
1
2
  import type { Variable } from '../types';
2
- import { variablePlaceholder } from '../variables';
3
3
  import { h } from '../utils/dom';
4
- import type { SimpleMailT } from '../i18n';
4
+ import { variablePlaceholder } from '../variables';
5
5
 
6
6
  export interface VariablePickerHandlers {
7
7
  onPickKey: (v: Variable) => void;
@@ -88,9 +88,7 @@ export function buildVariablePickerBody(
88
88
  ): HTMLElement {
89
89
  const wrap = h('div', { class: 'sm-var-picker' });
90
90
  if (!vars.length) {
91
- wrap.append(
92
- h('div', { class: 'sm-empty-form' }, [t('variablePicker.empty')]),
93
- );
91
+ wrap.append(h('div', { class: 'sm-empty-form' }, [t('variablePicker.empty')]));
94
92
  return wrap;
95
93
  }
96
94
  for (const v of vars) {
@@ -89,7 +89,7 @@ export interface ImageGalleryAdapter {
89
89
  /** 搜索 + 分页;`query` 为搜索框当前文本,`page` 从 0 起 */
90
90
  listItems: (args: { query: string; page: number }) => Promise<ImageGalleryListResult>;
91
91
  /** 在弹层内「上传到图库」:成功后编辑器会重新拉取第 0 页 */
92
- uploadFile?: (file: File) => Promise<void | string>;
92
+ uploadFile?: (file: File) => Promise<unknown>;
93
93
  /** 在弹层内「通过链接添加」:校验/落库后编辑器会重新拉取第 0 页 */
94
94
  addByUrl?: (url: string) => Promise<void>;
95
95
  /** 从图库删除一条;提供后每张缩略图右上角显示删除按钮 */