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,8 +1,9 @@
1
1
  import {
2
- MailEditor,
2
+ type EmailDoc,
3
3
  type GalleryItem,
4
4
  type ImageGalleryAdapter,
5
5
  type ImageGalleryListResult,
6
+ MailEditor,
6
7
  } from '@simple-mail/core';
7
8
  import '@simple-mail/core/style.css';
8
9
  import { createAllBlocks } from '@simple-mail/blocks';
@@ -14,136 +15,164 @@ function getDemoLocale(): SimpleMailLocale {
14
15
  return raw === 'zh-CN' ? 'zh-CN' : 'en-US';
15
16
  }
16
17
 
17
- const locale = getDemoLocale();
18
- const isEn = locale === 'en-US';
19
-
20
- const demoItems: GalleryItem[] = [
21
- {
22
- id: 'demo-1',
23
- url: 'https://picsum.photos/seed/simplemail-a/640/320',
24
- thumbnailUrl: 'https://picsum.photos/seed/simplemail-a/240/160',
25
- title: isEn ? 'Demo asset A' : '示例素材 A',
26
- },
27
- {
28
- id: 'demo-2',
29
- url: 'https://picsum.photos/seed/simplemail-b/640/320',
30
- thumbnailUrl: 'https://picsum.photos/seed/simplemail-b/240/160',
31
- title: isEn ? 'Demo asset B' : '示例素材 B',
32
- },
33
- ];
18
+ const container = document.getElementById('app')!;
19
+ const localeSwitchButton = document.getElementById('locale-switch')!;
34
20
 
21
+ let editor: MailEditor | null = null;
22
+ let currentLocale: SimpleMailLocale = getDemoLocale();
35
23
  let demoNextId = 100;
36
24
 
37
- const demoGalleryAdapter: ImageGalleryAdapter = {
38
- async listItems({
39
- query,
40
- page,
41
- }: {
42
- query: string;
43
- page: number;
44
- }): Promise<ImageGalleryListResult> {
45
- const q = query.trim().toLowerCase();
46
- const filtered = demoItems.filter(
47
- (x) => !q || `${x.title ?? ''} ${x.url}`.toLowerCase().includes(q),
48
- );
49
- const pageSize = 6;
50
- const start = page * pageSize;
51
- const items = filtered.slice(start, start + pageSize);
52
- const hasMore = start + pageSize < filtered.length;
53
- await new Promise((r) => setTimeout(r, page === 0 ? 120 : 60));
54
- return { items, hasMore };
55
- },
56
- async uploadFile(file: File): Promise<void> {
57
- const seed =
58
- encodeURIComponent(file.name.replace(/\W/g, '_').slice(0, 24) || 'up') +
59
- '-' +
60
- Date.now();
61
- const url = `https://picsum.photos/seed/${seed}/640/280`;
62
- demoItems.unshift({
63
- id: `up-${demoNextId++}`,
64
- url,
65
- thumbnailUrl: url,
66
- title: file.name,
67
- });
68
- },
69
- async addByUrl(raw: string): Promise<void> {
70
- const u = raw.trim();
71
- if (!/^https?:\/\/.+/i.test(u)) {
72
- throw new Error(isEn ? 'Enter a valid http(s) URL' : '请输入有效的 http(s) 链接');
73
- }
74
- demoItems.unshift({
75
- id: `url-${demoNextId++}`,
76
- url: u,
77
- thumbnailUrl: u,
78
- title: isEn ? 'Linked image' : '外链图片',
79
- });
80
- },
81
- async deleteItem(id: string): Promise<void> {
82
- const i = demoItems.findIndex((x) => x.id === id);
83
- if (i >= 0) demoItems.splice(i, 1);
84
- },
85
- };
25
+ function mount(locale: SimpleMailLocale, initialDoc?: Partial<EmailDoc>) {
26
+ const isEn = locale === 'en-US';
86
27
 
87
- const container = document.getElementById('app')!;
28
+ const demoItems: GalleryItem[] = [
29
+ {
30
+ id: 'demo-1',
31
+ url: 'https://picsum.photos/seed/simplemail-a/640/320',
32
+ thumbnailUrl: 'https://picsum.photos/seed/simplemail-a/240/160',
33
+ title: isEn ? 'Demo asset A' : '示例素材 A',
34
+ },
35
+ {
36
+ id: 'demo-2',
37
+ url: 'https://picsum.photos/seed/simplemail-b/640/320',
38
+ thumbnailUrl: 'https://picsum.photos/seed/simplemail-b/240/160',
39
+ title: isEn ? 'Demo asset B' : '示例素材 B',
40
+ },
41
+ ];
88
42
 
89
- const editor = new MailEditor({
90
- container,
91
- locale,
92
- blocks: createAllBlocks({ locale }),
93
- initialDoc: {
94
- meta: {
95
- subject: isEn ? 'Welcome to Simple Mail!' : '欢迎加入 Simple Mail!',
96
- preheader: isEn ? 'A sample email' : '一封示例邮件',
97
- width: 600,
43
+ const demoGalleryAdapter: ImageGalleryAdapter = {
44
+ async listItems({
45
+ query,
46
+ page,
47
+ }: {
48
+ query: string;
49
+ page: number;
50
+ }): Promise<ImageGalleryListResult> {
51
+ const q = query.trim().toLowerCase();
52
+ const filtered = demoItems.filter(
53
+ (x) => !q || `${x.title ?? ''} ${x.url}`.toLowerCase().includes(q),
54
+ );
55
+ const pageSize = 6;
56
+ const start = page * pageSize;
57
+ const items = filtered.slice(start, start + pageSize);
58
+ const hasMore = start + pageSize < filtered.length;
59
+ await new Promise((r) => setTimeout(r, page === 0 ? 120 : 60));
60
+ return { items, hasMore };
98
61
  },
99
- variables: [
100
- { key: 'user.name', label: isEn ? 'User name' : '用户名', sample: isEn ? 'Alex' : '张三' },
101
- { key: 'user.email', label: isEn ? 'Email' : '邮箱', sample: 'alex@example.com' },
102
- {
103
- key: 'unsubscribeUrl',
104
- label: isEn ? 'Unsubscribe URL' : '退订地址',
105
- kind: 'link',
106
- sample: 'https://example.com/unsubscribe',
107
- },
108
- {
109
- key: 'couponLink',
110
- label: isEn ? 'Coupon link' : '优惠券链接',
111
- kind: 'link',
112
- sample: 'https://example.com/shop',
113
- },
114
- {
115
- key: 'couponImage',
116
- label: isEn ? 'Coupon image' : '优惠券图片',
117
- kind: 'image',
118
- sample: 'https://picsum.photos/seed/simplemail-coupon/320/160',
62
+ async uploadFile(file: File): Promise<void> {
63
+ const seed = `${encodeURIComponent(file.name.replace(/\W/g, '_').slice(0, 24) || 'up')}-${Date.now()}`;
64
+ const url = `https://picsum.photos/seed/${seed}/640/280`;
65
+ demoItems.unshift({
66
+ id: `up-${demoNextId++}`,
67
+ url,
68
+ thumbnailUrl: url,
69
+ title: file.name,
70
+ });
71
+ },
72
+ async addByUrl(raw: string): Promise<void> {
73
+ const u = raw.trim();
74
+ if (!/^https?:\/\/.+/i.test(u)) {
75
+ throw new Error(isEn ? 'Enter a valid http(s) URL' : '请输入有效的 http(s) 链接');
76
+ }
77
+ demoItems.unshift({
78
+ id: `url-${demoNextId++}`,
79
+ url: u,
80
+ thumbnailUrl: u,
81
+ title: isEn ? 'Linked image' : '外链图片',
82
+ });
83
+ },
84
+ async deleteItem(id: string): Promise<void> {
85
+ const i = demoItems.findIndex((x) => x.id === id);
86
+ if (i >= 0) demoItems.splice(i, 1);
87
+ },
88
+ };
89
+
90
+ editor = new MailEditor({
91
+ container,
92
+ locale,
93
+ blocks: createAllBlocks({ locale }),
94
+ // 切换语言时传入当前文档以保留画布内容;首次进入使用下方示例文档
95
+ initialDoc: initialDoc ?? {
96
+ meta: {
97
+ subject: isEn ? 'Welcome to Simple Mail!' : '欢迎加入 Simple Mail!',
98
+ preheader: isEn ? 'A sample email' : '一封示例邮件',
99
+ width: 600,
119
100
  },
120
- {
121
- key: 'productImage',
122
- label: isEn ? 'Product image' : '产品图片',
123
- kind: 'image',
124
- sample: 'https://picsum.photos/seed/simplemail-product/400/240',
101
+ variables: [
102
+ {
103
+ key: 'user.name',
104
+ label: isEn ? 'User name' : '用户名',
105
+ sample: isEn ? 'Alex' : '张三',
106
+ },
107
+ { key: 'user.email', label: isEn ? 'Email' : '邮箱', sample: 'alex@example.com' },
108
+ {
109
+ key: 'unsubscribeUrl',
110
+ label: isEn ? 'Unsubscribe URL' : '退订地址',
111
+ kind: 'link',
112
+ sample: 'https://example.com/unsubscribe',
113
+ },
114
+ {
115
+ key: 'couponLink',
116
+ label: isEn ? 'Coupon link' : '优惠券链接',
117
+ kind: 'link',
118
+ sample: 'https://example.com/shop',
119
+ },
120
+ {
121
+ key: 'couponImage',
122
+ label: isEn ? 'Coupon image' : '优惠券图片',
123
+ kind: 'image',
124
+ sample: 'https://picsum.photos/seed/simplemail-coupon/320/160',
125
+ },
126
+ {
127
+ key: 'productImage',
128
+ label: isEn ? 'Product image' : '产品图片',
129
+ kind: 'image',
130
+ sample: 'https://picsum.photos/seed/simplemail-product/400/240',
131
+ },
132
+ ],
133
+ sections: [],
134
+ },
135
+ /** 演示:侧边栏上传 + 内置图库(列表 / 搜索一行工具栏 / 上传与添加 / 删除) */
136
+ imageAssets: {
137
+ showGallery: true,
138
+ async uploadImage(file: File) {
139
+ const seed = encodeURIComponent(file.name.replace(/\W/g, '_').slice(0, 40) || 'up');
140
+ return `https://picsum.photos/seed/${seed}/600/240`;
125
141
  },
126
- ],
127
- sections: [],
128
- },
129
- /** 演示:侧边栏上传 + 内置图库(列表 / 搜索一行工具栏 / 上传与添加 / 删除) */
130
- imageAssets: {
131
- showGallery: true,
132
- async uploadImage(file: File) {
133
- const seed = encodeURIComponent(file.name.replace(/\W/g, '_').slice(0, 40) || 'up');
134
- return `https://picsum.photos/seed/${seed}/600/240`;
142
+ imageGallery: demoGalleryAdapter,
143
+ },
144
+ onChange: (doc) => {
145
+ console.debug('[doc changed]', doc);
135
146
  },
136
- imageGallery: demoGalleryAdapter,
137
- },
138
- onChange: (doc) => {
139
- console.debug('[doc changed]', doc);
140
- },
141
- /** 体验:右栏数字 / 内边距 / 字号滑块等(字重恒为五档平铺) */
142
- ui: { preferSliderControls: true },
143
- /** 点击画布灰色衬底或白底留白时取消选中 */
144
- clearSelectionOnCanvasMargin: true,
145
- /** 顶栏主题色拾色器;未设 accentColor 时使用样式表随 light/dark/system 的默认紫/靛 */
146
- showAccentColorPicker: true,
147
+ /** 体验:右栏数字 / 内边距 / 字号滑块等(字重恒为五档平铺) */
148
+ ui: { preferSliderControls: true },
149
+ /** 点击画布灰色衬底或白底留白时取消选中 */
150
+ clearSelectionOnCanvasMargin: true,
151
+ /** 顶栏主题色拾色器;未设 accentColor 时使用样式表随 light/dark/system 的默认紫/靛 */
152
+ showAccentColorPicker: true,
153
+ });
154
+
155
+ (window as unknown as { editor: MailEditor | null }).editor = editor;
156
+ document.documentElement.lang = locale;
157
+ localeSwitchButton.textContent = isEn ? '中文' : 'EN';
158
+ localeSwitchButton.title = isEn ? '切换到中文' : 'Switch to English';
159
+ }
160
+
161
+ /** 宿主语言切换:保存当前文档 → 销毁旧实例 → 新语言重建(与 README 推荐做法一致) */
162
+ function switchLocale(next: SimpleMailLocale) {
163
+ if (next === currentLocale) return;
164
+ const currentDoc = editor?.getValue();
165
+ editor?.destroy();
166
+ editor = null;
167
+ currentLocale = next;
168
+ mount(next, currentDoc);
169
+ const url = new URL(window.location.href);
170
+ url.searchParams.set('locale', next);
171
+ window.history.replaceState(null, '', url);
172
+ }
173
+
174
+ localeSwitchButton.addEventListener('click', () => {
175
+ switchLocale(currentLocale === 'zh-CN' ? 'en-US' : 'zh-CN');
147
176
  });
148
177
 
149
- (window as unknown as { editor: MailEditor }).editor = editor;
178
+ mount(currentLocale);
@@ -1,12 +1,15 @@
1
- import { defineConfig } from 'vite';
2
1
  import { resolve } from 'node:path';
2
+ import { defineConfig } from 'vite';
3
3
 
4
4
  export default defineConfig({
5
5
  server: { port: 5173, open: true },
6
6
  resolve: {
7
7
  // 直接消费 monorepo 包的源码,省去每次手动 build
8
8
  alias: {
9
- '@simple-mail/core/style.css': resolve(__dirname, '../../packages/core/src/editor/styles.css'),
9
+ '@simple-mail/core/style.css': resolve(
10
+ __dirname,
11
+ '../../packages/core/src/editor/styles.css',
12
+ ),
10
13
  '@simple-mail/core': resolve(__dirname, '../../packages/core/src/index.ts'),
11
14
  '@simple-mail/blocks': resolve(__dirname, '../../packages/blocks/src/index.ts'),
12
15
  },