mail-editor-pancake 0.0.7 → 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.
@@ -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';
@@ -25,12 +26,20 @@ export interface TopbarOptions {
25
26
  onExport: () => void;
26
27
  /** 切换编辑器根节点浏览器全屏(由 Editor 实现 Fullscreen API) */
27
28
  onFullscreenToggle?: () => void;
29
+ /** 切换画布 Section / Block 布局虚线边框 */
30
+ onLayoutBordersToggle?: () => void;
28
31
  /** 为 false 时不展示全屏按钮(也可用 `ui.hideTopbarFullscreen`) */
29
32
  showFullscreenButton?: boolean;
30
33
  /** 为 false 时不展示「邮件设置」按钮(也可用 `ui.hideTopbarMailSettings`) */
31
34
  showMailSettingsButton?: boolean;
32
35
  /** 为 false 时不渲染左侧产品标题 */
33
36
  showTitle?: boolean;
37
+ /** 为 false 时不展示「清空画布」 */
38
+ showClearCanvasButton?: boolean;
39
+ onClearCanvas?: () => void;
40
+ /** 为 false 时不展示「重置内容」 */
41
+ showResetContentButton?: boolean;
42
+ onResetContent?: () => void;
34
43
  }
35
44
 
36
45
  export class Topbar {
@@ -42,6 +51,7 @@ export class Topbar {
42
51
  private modeButtons: Record<EditorMode, HTMLButtonElement> = {} as any;
43
52
  private accentInput?: HTMLInputElement;
44
53
  private fullscreenBtn?: HTMLButtonElement;
54
+ private layoutBordersBtn?: HTMLButtonElement;
45
55
 
46
56
  constructor(opts: TopbarOptions) {
47
57
  this.opts = opts;
@@ -89,6 +99,15 @@ export class Topbar {
89
99
  btn.setAttribute('aria-pressed', active ? 'true' : 'false');
90
100
  }
91
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
+
92
111
  private _render() {
93
112
  clear(this.el);
94
113
 
@@ -147,7 +166,11 @@ export class Topbar {
147
166
  title: '主题色',
148
167
  value: '#4f46e5',
149
168
  }) as HTMLInputElement;
150
- 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
+ });
151
174
  this.accentInput = inputEl;
152
175
  themeGroupKids.push(h('div', { class: 'sm-topbar__accent' }, [inputEl]));
153
176
  } else {
@@ -175,6 +198,41 @@ export class Topbar {
175
198
  [iconRedo(), '重做'],
176
199
  );
177
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
+
178
236
  const insertVar = h(
179
237
  'button',
180
238
  {
@@ -215,12 +273,32 @@ export class Topbar {
215
273
  }
216
274
  trailingGroup.push(insertVar, previewBtn, exportBtn);
217
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
+
218
297
  const showFs =
219
298
  this.opts.showFullscreenButton !== false &&
220
299
  typeof this.opts.onFullscreenToggle === 'function';
221
- const fsGroup =
222
- showFs &&
223
- h('div', { class: 'sm-topbar__group' }, [
300
+ if (showFs) {
301
+ canvasToolKids.push(
224
302
  (this.fullscreenBtn = h(
225
303
  'button',
226
304
  {
@@ -233,8 +311,13 @@ export class Topbar {
233
311
  },
234
312
  [iconFullscreenEnter()],
235
313
  ) as HTMLButtonElement),
236
- ]);
237
- if (!showFs) this.fullscreenBtn = undefined;
314
+ );
315
+ } else {
316
+ this.fullscreenBtn = undefined;
317
+ }
318
+
319
+ const canvasToolsGroup =
320
+ canvasToolKids.length > 0 && h('div', { class: 'sm-topbar__group' }, canvasToolKids);
238
321
 
239
322
  const headKids: HTMLElement[] = [];
240
323
  if (this.opts.showTitle !== false) {
@@ -246,7 +329,8 @@ export class Topbar {
246
329
  h('div', { class: 'sm-topbar__group' }, [segmented]),
247
330
  h('div', { class: 'sm-topbar__group sm-topbar__theme-group' }, themeGroupKids),
248
331
  h('div', { class: 'sm-topbar__group' }, [this.undoBtn, this.redoBtn]),
249
- ...(fsGroup ? [fsGroup] : []),
332
+ ...(contentActionsGroup ? [contentActionsGroup] : []),
333
+ ...(canvasToolsGroup ? [canvasToolsGroup] : []),
250
334
  h('div', { class: 'sm-topbar__spacer' }),
251
335
  h('div', { class: 'sm-topbar__group' }, trailingGroup),
252
336
  );
@@ -276,6 +360,13 @@ function iconEye(): SVGElement {
276
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"/>',
277
361
  );
278
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
+ }
279
370
  /** 进入全屏:四角向外 */
280
371
  function iconFullscreenEnter(): SVGElement {
281
372
  return svg(