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
@@ -5,6 +5,7 @@ import type { Registry } from '../registry/registry';
5
5
  import { renderDoc } from '../renderer';
6
6
  import type { Store } from '../store/store';
7
7
  import { clear, h } from '../utils/dom';
8
+ import { richTextExecCommand } from '../utils/richTextCommand';
8
9
 
9
10
  export interface SourceViewOptions {
10
11
  store: Store;
@@ -110,7 +111,7 @@ export class SourceView {
110
111
  ta.value = text;
111
112
  document.body.appendChild(ta);
112
113
  ta.select();
113
- document.execCommand('copy');
114
+ richTextExecCommand('copy');
114
115
  document.body.removeChild(ta);
115
116
  }
116
117
  }
@@ -1,4 +1,5 @@
1
1
  import type { Store } from '../store/store';
2
+ import { bindColorPickerInput } from './ColorPickerPopover';
2
3
  import { normalizeAccentHex, rgbCssToHex } from '../utils/accentColor';
3
4
  import { h, clear } from '../utils/dom';
4
5
  import type { EditorTheme } from './theme';
@@ -23,8 +24,22 @@ export interface TopbarOptions {
23
24
  onInsertVariable: (anchor: HTMLElement) => void;
24
25
  onPreview: () => void;
25
26
  onExport: () => void;
26
- /** 为 false 时不展示「邮件设置」按钮(与 `ui.hideMailMeta` 配套) */
27
+ /** 切换编辑器根节点浏览器全屏(由 Editor 实现 Fullscreen API) */
28
+ onFullscreenToggle?: () => void;
29
+ /** 切换画布 Section / Block 布局虚线边框 */
30
+ onLayoutBordersToggle?: () => void;
31
+ /** 为 false 时不展示全屏按钮(也可用 `ui.hideTopbarFullscreen`) */
32
+ showFullscreenButton?: boolean;
33
+ /** 为 false 时不展示「邮件设置」按钮(也可用 `ui.hideTopbarMailSettings`) */
27
34
  showMailSettingsButton?: boolean;
35
+ /** 为 false 时不渲染左侧产品标题 */
36
+ showTitle?: boolean;
37
+ /** 为 false 时不展示「清空画布」 */
38
+ showClearCanvasButton?: boolean;
39
+ onClearCanvas?: () => void;
40
+ /** 为 false 时不展示「重置内容」 */
41
+ showResetContentButton?: boolean;
42
+ onResetContent?: () => void;
28
43
  }
29
44
 
30
45
  export class Topbar {
@@ -35,6 +50,8 @@ export class Topbar {
35
50
  private themeButtons!: Record<EditorTheme, HTMLButtonElement>;
36
51
  private modeButtons: Record<EditorMode, HTMLButtonElement> = {} as any;
37
52
  private accentInput?: HTMLInputElement;
53
+ private fullscreenBtn?: HTMLButtonElement;
54
+ private layoutBordersBtn?: HTMLButtonElement;
38
55
 
39
56
  constructor(opts: TopbarOptions) {
40
57
  this.opts = opts;
@@ -72,11 +89,28 @@ export class Topbar {
72
89
  if (this.accentInput.value.toLowerCase() !== forInput) this.accentInput.value = forInput;
73
90
  }
74
91
 
92
+ /** 与 document fullscreen 状态同步图标与 aria(按钮不存在时忽略) */
93
+ setFullscreenActive(active: boolean) {
94
+ const btn = this.fullscreenBtn;
95
+ if (!btn) return;
96
+ btn.replaceChildren(active ? iconFullscreenExit() : iconFullscreenEnter());
97
+ btn.title = active ? '退出全屏' : '全屏';
98
+ btn.setAttribute('aria-label', active ? '退出全屏' : '全屏');
99
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
100
+ }
101
+
102
+ /** 与画布布局边框开关同步按钮高亮(按钮不存在时忽略) */
103
+ setLayoutBordersActive(active: boolean) {
104
+ const btn = this.layoutBordersBtn;
105
+ if (!btn) return;
106
+ btn.setAttribute('aria-pressed', active ? 'true' : 'false');
107
+ btn.title = active ? '隐藏 Section / Block 边框' : '显示 Section / Block 边框';
108
+ btn.setAttribute('aria-label', active ? '隐藏 Section / Block 边框' : '显示 Section / Block 边框');
109
+ }
110
+
75
111
  private _render() {
76
112
  clear(this.el);
77
113
 
78
- const title = h('div', { class: 'sm-topbar__title' }, ['Simple Mail Editor']);
79
-
80
114
  const segmented = h('div', { class: 'sm-segmented' });
81
115
  const designBtn = h(
82
116
  'button',
@@ -132,7 +166,11 @@ export class Topbar {
132
166
  title: '主题色',
133
167
  value: '#4f46e5',
134
168
  }) as HTMLInputElement;
135
- inputEl.addEventListener('input', () => this.opts.onAccentChange!(inputEl.value));
169
+ bindColorPickerInput(inputEl, {
170
+ layerRoot: this.opts.accentPickerRoot,
171
+ liveCommit: true,
172
+ onCommit: (hex) => this.opts.onAccentChange!(hex),
173
+ });
136
174
  this.accentInput = inputEl;
137
175
  themeGroupKids.push(h('div', { class: 'sm-topbar__accent' }, [inputEl]));
138
176
  } else {
@@ -160,6 +198,41 @@ export class Topbar {
160
198
  [iconRedo(), '重做'],
161
199
  );
162
200
 
201
+ const contentActionKids: HTMLElement[] = [];
202
+ if (this.opts.showClearCanvasButton !== false && typeof this.opts.onClearCanvas === 'function') {
203
+ contentActionKids.push(
204
+ h(
205
+ 'button',
206
+ {
207
+ class: 'sm-btn sm-btn--ghost',
208
+ type: 'button',
209
+ title: '移除画布上所有 Section 与组件',
210
+ onclick: () => this.opts.onClearCanvas!(),
211
+ },
212
+ ['清空画布'],
213
+ ),
214
+ );
215
+ }
216
+ if (
217
+ this.opts.showResetContentButton !== false &&
218
+ typeof this.opts.onResetContent === 'function'
219
+ ) {
220
+ contentActionKids.push(
221
+ h(
222
+ 'button',
223
+ {
224
+ class: 'sm-btn sm-btn--ghost',
225
+ type: 'button',
226
+ title: '恢复为预置邮件结构',
227
+ onclick: () => this.opts.onResetContent!(),
228
+ },
229
+ ['重置内容'],
230
+ ),
231
+ );
232
+ }
233
+ const contentActionsGroup =
234
+ contentActionKids.length > 0 && h('div', { class: 'sm-topbar__group' }, contentActionKids);
235
+
163
236
  const insertVar = h(
164
237
  'button',
165
238
  {
@@ -200,11 +273,64 @@ export class Topbar {
200
273
  }
201
274
  trailingGroup.push(insertVar, previewBtn, exportBtn);
202
275
 
276
+ const canvasToolKids: HTMLElement[] = [];
277
+
278
+ if (typeof this.opts.onLayoutBordersToggle === 'function') {
279
+ canvasToolKids.push(
280
+ (this.layoutBordersBtn = h(
281
+ 'button',
282
+ {
283
+ class: 'sm-btn sm-btn--ghost sm-topbar__icon-btn sm-topbar__toggle-btn',
284
+ type: 'button',
285
+ title: '显示 Section / Block 边框',
286
+ 'aria-label': '显示 Section / Block 边框',
287
+ 'aria-pressed': 'false',
288
+ onclick: () => this.opts.onLayoutBordersToggle!(),
289
+ },
290
+ [iconLayoutBorders()],
291
+ ) as HTMLButtonElement),
292
+ );
293
+ } else {
294
+ this.layoutBordersBtn = undefined;
295
+ }
296
+
297
+ const showFs =
298
+ this.opts.showFullscreenButton !== false &&
299
+ typeof this.opts.onFullscreenToggle === 'function';
300
+ if (showFs) {
301
+ canvasToolKids.push(
302
+ (this.fullscreenBtn = h(
303
+ 'button',
304
+ {
305
+ class: 'sm-btn sm-btn--ghost sm-topbar__icon-btn',
306
+ type: 'button',
307
+ title: '全屏',
308
+ 'aria-label': '全屏',
309
+ 'aria-pressed': 'false',
310
+ onclick: () => this.opts.onFullscreenToggle!(),
311
+ },
312
+ [iconFullscreenEnter()],
313
+ ) as HTMLButtonElement),
314
+ );
315
+ } else {
316
+ this.fullscreenBtn = undefined;
317
+ }
318
+
319
+ const canvasToolsGroup =
320
+ canvasToolKids.length > 0 && h('div', { class: 'sm-topbar__group' }, canvasToolKids);
321
+
322
+ const headKids: HTMLElement[] = [];
323
+ if (this.opts.showTitle !== false) {
324
+ headKids.push(h('div', { class: 'sm-topbar__title' }, ['Simple Mail Editor']));
325
+ }
326
+
203
327
  this.el.append(
204
- title,
328
+ ...headKids,
205
329
  h('div', { class: 'sm-topbar__group' }, [segmented]),
206
330
  h('div', { class: 'sm-topbar__group sm-topbar__theme-group' }, themeGroupKids),
207
331
  h('div', { class: 'sm-topbar__group' }, [this.undoBtn, this.redoBtn]),
332
+ ...(contentActionsGroup ? [contentActionsGroup] : []),
333
+ ...(canvasToolsGroup ? [canvasToolsGroup] : []),
208
334
  h('div', { class: 'sm-topbar__spacer' }),
209
335
  h('div', { class: 'sm-topbar__group' }, trailingGroup),
210
336
  );
@@ -234,6 +360,25 @@ function iconEye(): SVGElement {
234
360
  '<path d="M2 10s3-5 8-5 8 5 8 5-3 5-8 5-8-5-8-5z" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="10" cy="10" r="2.4" stroke="currentColor" stroke-width="1.5" fill="none"/>',
235
361
  );
236
362
  }
363
+ /** 布局边框:虚线框 + 勾选(点亮时显示勾) */
364
+ function iconLayoutBorders(): SVGElement {
365
+ const el = svg(
366
+ '<rect x="4.5" y="4.5" width="11" height="11" rx="1.5" stroke="currentColor" stroke-width="1.5" fill="none" stroke-dasharray="2.5 2"/><path d="M7.2 10.1l1.8 1.8 3.8-3.9" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" class="sm-layout-borders-check"/>',
367
+ );
368
+ return el;
369
+ }
370
+ /** 进入全屏:四角向外 */
371
+ function iconFullscreenEnter(): SVGElement {
372
+ return svg(
373
+ '<path d="M6 4H4v2M14 4h2v2M6 16H4v-2M14 16h2v-2M4 6V4h2M16 6V4h-2M4 14v2h2M16 14v2h-2" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/>',
374
+ );
375
+ }
376
+ /** 退出全屏:四角向内 */
377
+ function iconFullscreenExit(): SVGElement {
378
+ return svg(
379
+ '<path d="M8 4v4H4M12 4v4h4M8 16v-4H4M12 16v-4h4M4 8h4v4H4M16 8h-4v4h4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/>',
380
+ );
381
+ }
237
382
  function iconSun(): SVGElement {
238
383
  return svgTheme(
239
384
  '<circle cx="10" cy="10" r="3.2" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M10 3.5V5M10 15v1.5M16.5 10H15M5 10H3.5M14.1 5.9 13 7M7 13 5.9 14.1M14.1 14.1 13 13M7 7 5.9 5.9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',