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.
- package/README.md +79 -6
- package/package.json +1 -1
- package/packages/blocks/dist/index.d.ts +55 -3
- package/packages/blocks/dist/index.js +267 -208
- package/packages/blocks/dist/index.js.map +1 -1
- package/packages/blocks/node_modules/.bin/resolve +21 -0
- package/packages/blocks/package.json +1 -0
- package/packages/blocks/src/button.ts +1 -0
- package/packages/blocks/src/hero.ts +2 -0
- package/packages/blocks/src/image.ts +1 -0
- package/packages/blocks/src/index.ts +19 -2
- package/packages/blocks/src/logo.ts +1 -0
- package/packages/blocks/src/social.ts +47 -22
- package/packages/blocks/src/socialGroup.ts +49 -22
- package/packages/blocks/src/socialShared.ts +39 -2
- package/packages/blocks/src/text.ts +25 -19
- package/packages/blocks/tsconfig.json +0 -3
- package/packages/blocks/vite.config.ts +2 -0
- package/packages/core/dist/index.d.ts +90 -7
- package/packages/core/dist/index.js +2505 -1462
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/style.css +1 -1
- package/packages/core/src/editor/Canvas.ts +107 -53
- package/packages/core/src/editor/ColorPickerPopover.ts +291 -0
- package/packages/core/src/editor/Editor.ts +129 -8
- package/packages/core/src/editor/ExportModal.ts +2 -1
- package/packages/core/src/editor/FocusBreadcrumb.ts +120 -0
- package/packages/core/src/editor/ImageGalleryModal.ts +3 -3
- package/packages/core/src/editor/InlineEditor.ts +308 -66
- package/packages/core/src/editor/LeftPanel.ts +1 -2
- package/packages/core/src/editor/PreviewModal.ts +77 -7
- package/packages/core/src/editor/RichTextToolbar.ts +66 -44
- package/packages/core/src/editor/RightPanel.ts +773 -171
- package/packages/core/src/editor/SourceView.ts +2 -1
- package/packages/core/src/editor/Topbar.ts +150 -5
- package/packages/core/src/editor/styles.css +644 -81
- package/packages/core/src/index.ts +18 -1
- package/packages/core/src/renderer/mjml.ts +2 -1
- package/packages/core/src/store/store.ts +44 -3
- package/packages/core/src/types.ts +34 -3
- package/packages/core/src/utils/fontWeightSteps.ts +31 -0
- package/packages/core/src/utils/lockedMjml.ts +2 -1
- package/packages/core/src/utils/richTextCommand.ts +30 -0
- package/packages/core/src/utils/sectionLayout.ts +13 -0
- 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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
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"/>',
|