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
@@ -1,10 +1,14 @@
1
- import { defineBlock } from '@simple-mail/core';
1
+ import { defineBlock, FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '@simple-mail/core';
2
2
  import { icons } from './icons';
3
3
  import {
4
4
  BIZ_SOCIAL_NETWORK_OPTIONS,
5
5
  escAttr,
6
+ flexJustifyFromAlign,
6
7
  mjSocialElementsLines,
7
8
  paddingQuad,
9
+ socialIconBorderRadiusCss,
10
+ socialIconBorderRadiusMjml,
11
+ socialIconSpacingPx,
8
12
  socialMeta,
9
13
  type SocialLinkItem,
10
14
  } from './socialShared';
@@ -14,6 +18,8 @@ interface SocialBizProps {
14
18
  elements: SocialLinkItem[];
15
19
  align: 'left' | 'center' | 'right';
16
20
  iconSize: number;
21
+ iconBorderRadius: number;
22
+ iconSpacing: number;
17
23
  labelFontSize: number;
18
24
  labelFontWeight: string;
19
25
  labelColor: string;
@@ -53,8 +59,10 @@ export const socialBlock = defineBlock<SocialBizProps>({
53
59
  elements: DEFAULT_BIZ_ELEMENTS,
54
60
  align: 'center',
55
61
  iconSize: 28,
62
+ iconBorderRadius: 999,
63
+ iconSpacing: 10,
56
64
  labelFontSize: 13,
57
- labelFontWeight: 'normal',
65
+ labelFontWeight: '400',
58
66
  labelColor: '#333333',
59
67
  paddingTop: 8,
60
68
  paddingRight: 0,
@@ -81,22 +89,15 @@ export const socialBlock = defineBlock<SocialBizProps>({
81
89
  key: 'labelFontWeight',
82
90
  label: '标签字重',
83
91
  type: 'select',
84
- options: [
85
- { label: '较细 (300)', value: '300' },
86
- { label: '常规', value: 'normal' },
87
- { label: '400', value: '400' },
88
- { label: '500', value: '500' },
89
- { label: '600', value: '600' },
90
- { label: '700', value: '700' },
91
- { label: '粗体', value: 'bold' },
92
- { label: '更粗 (800)', value: '800' },
93
- ],
92
+ selectVariant: 'segmented',
93
+ options: [...FONT_WEIGHT_STEP_OPTIONS],
94
94
  },
95
95
  { key: 'labelColor', label: '标签颜色', type: 'color' },
96
96
  {
97
97
  key: 'align',
98
98
  label: '对齐',
99
99
  type: 'select',
100
+ selectVariant: 'segmented',
100
101
  options: [
101
102
  { label: '左', value: 'left' },
102
103
  { label: '居中', value: 'center' },
@@ -104,29 +105,53 @@ export const socialBlock = defineBlock<SocialBizProps>({
104
105
  ],
105
106
  },
106
107
  { key: 'iconSize', label: '图标尺寸 (px)', type: 'number', min: 16, max: 64 },
108
+ {
109
+ key: 'iconBorderRadius',
110
+ label: '图标圆角 (px)',
111
+ type: 'number',
112
+ min: 0,
113
+ max: 999,
114
+ step: 1,
115
+ help: '0 为方形直角;较大值(如 999)为圆形(默认)。',
116
+ },
117
+ {
118
+ key: 'iconSpacing',
119
+ label: '图标间距 (px)',
120
+ type: 'number',
121
+ min: 0,
122
+ max: 64,
123
+ step: 1,
124
+ help: '图标之间的间隔(MJML inner-padding)。',
125
+ },
107
126
  { key: 'paddingTop', label: '外边距', type: 'spacing' },
108
127
  ],
109
128
  toMjml: (p) => {
110
129
  const pad = paddingQuad(p);
130
+ const br = socialIconBorderRadiusMjml(p.iconSize, p.iconBorderRadius ?? 999);
131
+ const gap = socialIconSpacingPx(p.iconSpacing, 10);
132
+ const socAttrs = `align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" border-radius="${br}" inner-padding="${gap}px" padding="${pad}"`;
111
133
  const els = normalizeElements(p).filter((e) => e.href?.trim());
112
134
  const labelStyle = {
113
135
  labelFontSize: Number(p.labelFontSize ?? 13),
114
- labelFontWeight: String(p.labelFontWeight ?? 'normal'),
136
+ labelFontWeight: normalizeFontWeightStep(String(p.labelFontWeight ?? '400')),
115
137
  labelColor: String(p.labelColor ?? '#333333'),
116
138
  };
117
139
  const inner = mjSocialElementsLines(els, labelStyle);
118
140
  if (!inner.trim()) {
119
- return `<mj-social align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" padding="${pad}"></mj-social>`;
141
+ return `<mj-social ${socAttrs}></mj-social>`;
120
142
  }
121
- return `<mj-social align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" padding="${pad}">
143
+ return `<mj-social ${socAttrs}>
122
144
  ${inner}
123
145
  </mj-social>`;
124
146
  },
125
147
  renderPreview: (p) => {
126
148
  const fsRaw = p.labelFontSize != null && p.labelFontSize > 0 ? p.labelFontSize : 13;
127
149
  const fs = Math.max(8, Math.min(48, fsRaw));
128
- const fw = p.labelFontWeight?.trim() || 'normal';
150
+ const fw = normalizeFontWeightStep(p.labelFontWeight);
129
151
  const textColor = p.labelColor?.trim() || '#333333';
152
+ const radiusCss = socialIconBorderRadiusCss(p.iconSize, p.iconBorderRadius ?? 999);
153
+ const gap = socialIconSpacingPx(p.iconSpacing, 10);
154
+ const jc = flexJustifyFromAlign(p.align);
130
155
  const items = normalizeElements(p)
131
156
  .filter((l) => l.href?.trim())
132
157
  .map((l) => {
@@ -135,21 +160,21 @@ ${inner}
135
160
  const label = l.label?.trim() ?? '';
136
161
  const iconSrc = l.iconSrc?.trim();
137
162
  const iconHtml = iconSrc
138
- ? `<img src="${escAttr(iconSrc)}" alt="" style="width:${p.iconSize}px;height:${p.iconSize}px;object-fit:contain;border-radius:3px;flex-shrink:0;" />`
139
- : `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius:50%;background:${bg};color:#fff;align-items:center;justify-content:center;font-size:${Math.round(
163
+ ? `<img src="${escAttr(iconSrc)}" alt="" style="width:${p.iconSize}px;height:${p.iconSize}px;object-fit:cover;border-radius:${radiusCss};flex-shrink:0;" />`
164
+ : `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius:${radiusCss};background:${bg};color:#fff;align-items:center;justify-content:center;font-size:${Math.round(
140
165
  p.iconSize / 2,
141
- )}px;font-weight:600;flex-shrink:0;">${meta.preview}</span>`;
166
+ )}px;font-weight:600;flex-shrink:0;overflow:hidden;">${meta.preview}</span>`;
142
167
  const textHtml = label
143
- ? `<span style="margin-left:6px;font-size:${fs}px !important;font-weight:${fw};color:${escAttr(
168
+ ? `<span style="font-size:${fs}px !important;font-weight:${fw};color:${escAttr(
144
169
  textColor,
145
170
  )} !important;line-height:1.2;">${escAttr(label)}</span>`
146
171
  : '';
147
- return `<span style="display:inline-flex;align-items:center;margin:0 6px;vertical-align:middle;"><a href="${escAttr(
172
+ return `<span style="display:inline-flex;align-items:center;gap:6px;"><a href="${escAttr(
148
173
  l.href,
149
174
  )}" style="display:inline-flex;align-items:center;text-decoration:none;line-height:0;">${iconHtml}</a>${textHtml}</span>`;
150
175
  })
151
176
  .join('');
152
177
  const pad = paddingQuad(p);
153
- return `<div style="padding:${pad};text-align:${p.align};">${items || '<span style="color:#9ca3af;font-size:12px;">请填写链接 URL</span>'}</div>`;
178
+ return `<div style="display:flex;flex-wrap:wrap;justify-content:${jc};align-items:center;gap:${gap}px;padding:${pad};">${items || '<span style="color:#9ca3af;font-size:12px;">请填写链接 URL</span>'}</div>`;
154
179
  },
155
180
  });
@@ -1,9 +1,13 @@
1
- import { defineBlock } from '@simple-mail/core';
1
+ import { defineBlock, FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '@simple-mail/core';
2
2
  import { icons } from './icons';
3
3
  import {
4
4
  escAttr,
5
+ flexJustifyFromAlign,
5
6
  mjSocialElementsLines,
6
7
  paddingQuad,
8
+ socialIconBorderRadiusCss,
9
+ socialIconBorderRadiusMjml,
10
+ socialIconSpacingPx,
7
11
  socialMeta,
8
12
  SOCIAL_NETWORK_OPTIONS,
9
13
  type SocialLinkItem,
@@ -13,6 +17,10 @@ interface SocialGroupProps {
13
17
  elements: SocialLinkItem[];
14
18
  align: 'left' | 'center' | 'right';
15
19
  iconSize: number;
20
+ /** 图标圆角 (px)。0 为方形;≥999 或不少于图标半边时为圆形(默认 999) */
21
+ iconBorderRadius: number;
22
+ /** 图标之间的间距 (px),对应 mj-social inner-padding */
23
+ iconSpacing: number;
16
24
  /** 标签文字字号(整组统一) */
17
25
  labelFontSize: number;
18
26
  labelFontWeight: string;
@@ -35,8 +43,10 @@ export const socialGroupBlock = defineBlock<SocialGroupProps>({
35
43
  ],
36
44
  align: 'center',
37
45
  iconSize: 28,
46
+ iconBorderRadius: 999,
47
+ iconSpacing: 10,
38
48
  labelFontSize: 13,
39
- labelFontWeight: 'normal',
49
+ labelFontWeight: '400',
40
50
  labelColor: '#333333',
41
51
  paddingTop: 8,
42
52
  paddingRight: 0,
@@ -63,22 +73,15 @@ export const socialGroupBlock = defineBlock<SocialGroupProps>({
63
73
  key: 'labelFontWeight',
64
74
  label: '标签字重',
65
75
  type: 'select',
66
- options: [
67
- { label: '较细 (300)', value: '300' },
68
- { label: '常规', value: 'normal' },
69
- { label: '400', value: '400' },
70
- { label: '500', value: '500' },
71
- { label: '600', value: '600' },
72
- { label: '700', value: '700' },
73
- { label: '粗体', value: 'bold' },
74
- { label: '更粗 (800)', value: '800' },
75
- ],
76
+ selectVariant: 'segmented',
77
+ options: [...FONT_WEIGHT_STEP_OPTIONS],
76
78
  },
77
79
  { key: 'labelColor', label: '标签颜色', type: 'color' },
78
80
  {
79
81
  key: 'align',
80
82
  label: '对齐',
81
83
  type: 'select',
84
+ selectVariant: 'segmented',
82
85
  options: [
83
86
  { label: '左', value: 'left' },
84
87
  { label: '居中', value: 'center' },
@@ -86,29 +89,53 @@ export const socialGroupBlock = defineBlock<SocialGroupProps>({
86
89
  ],
87
90
  },
88
91
  { key: 'iconSize', label: '图标尺寸 (px)', type: 'number', min: 16, max: 64 },
92
+ {
93
+ key: 'iconBorderRadius',
94
+ label: '图标圆角 (px)',
95
+ type: 'number',
96
+ min: 0,
97
+ max: 999,
98
+ step: 1,
99
+ help: '0 为方形直角;填较大值(如 999)或与尺寸一半相当时为圆形(默认圆形)。',
100
+ },
101
+ {
102
+ key: 'iconSpacing',
103
+ label: '图标间距 (px)',
104
+ type: 'number',
105
+ min: 0,
106
+ max: 64,
107
+ step: 1,
108
+ help: '每个图标周围的留白,横向即图标之间的间隔(MJML inner-padding)。',
109
+ },
89
110
  { key: 'paddingTop', label: '外边距', type: 'spacing' },
90
111
  ],
91
112
  toMjml: (p) => {
92
113
  const pad = paddingQuad(p);
114
+ const br = socialIconBorderRadiusMjml(p.iconSize, p.iconBorderRadius ?? 999);
115
+ const gap = socialIconSpacingPx(p.iconSpacing, 10);
116
+ const socAttrs = `align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" border-radius="${br}" inner-padding="${gap}px" padding="${pad}"`;
93
117
  const filtered = p.elements.filter((e) => e.href?.trim());
94
118
  const labelStyle = {
95
119
  labelFontSize: Number(p.labelFontSize ?? 13),
96
- labelFontWeight: String(p.labelFontWeight ?? 'normal'),
120
+ labelFontWeight: normalizeFontWeightStep(String(p.labelFontWeight ?? '400')),
97
121
  labelColor: String(p.labelColor ?? '#333333'),
98
122
  };
99
123
  const inner = mjSocialElementsLines(filtered, labelStyle);
100
124
  if (!inner.trim()) {
101
- return `<mj-social align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" padding="${pad}"></mj-social>`;
125
+ return `<mj-social ${socAttrs}></mj-social>`;
102
126
  }
103
- return `<mj-social align="${p.align}" icon-size="${p.iconSize}px" mode="horizontal" padding="${pad}">
127
+ return `<mj-social ${socAttrs}>
104
128
  ${inner}
105
129
  </mj-social>`;
106
130
  },
107
131
  renderPreview: (p) => {
108
132
  const fsRaw = p.labelFontSize != null && p.labelFontSize > 0 ? p.labelFontSize : 13;
109
133
  const fs = Math.max(8, Math.min(48, fsRaw));
110
- const fw = p.labelFontWeight?.trim() || 'normal';
134
+ const fw = normalizeFontWeightStep(p.labelFontWeight);
111
135
  const textColor = p.labelColor?.trim() || '#333333';
136
+ const radiusCss = socialIconBorderRadiusCss(p.iconSize, p.iconBorderRadius ?? 999);
137
+ const gap = socialIconSpacingPx(p.iconSpacing, 10);
138
+ const jc = flexJustifyFromAlign(p.align);
112
139
  const items = p.elements
113
140
  .filter((l) => l.href?.trim())
114
141
  .map((l) => {
@@ -117,21 +144,21 @@ ${inner}
117
144
  const label = l.label?.trim() ?? '';
118
145
  const iconSrc = l.iconSrc?.trim();
119
146
  const iconHtml = iconSrc
120
- ? `<img src="${escAttr(iconSrc)}" alt="" style="width:${p.iconSize}px;height:${p.iconSize}px;object-fit:contain;border-radius:3px;flex-shrink:0;" />`
121
- : `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius:50%;background:${bg};color:#fff;align-items:center;justify-content:center;font-size:${Math.round(
147
+ ? `<img src="${escAttr(iconSrc)}" alt="" style="width:${p.iconSize}px;height:${p.iconSize}px;object-fit:cover;border-radius:${radiusCss};flex-shrink:0;" />`
148
+ : `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius:${radiusCss};background:${bg};color:#fff;align-items:center;justify-content:center;font-size:${Math.round(
122
149
  p.iconSize / 2,
123
- )}px;font-weight:600;flex-shrink:0;">${meta.preview}</span>`;
150
+ )}px;font-weight:600;flex-shrink:0;overflow:hidden;">${meta.preview}</span>`;
124
151
  const textHtml = label
125
- ? `<span style="margin-left:6px;font-size:${fs}px !important;font-weight:${fw};color:${escAttr(
152
+ ? `<span style="font-size:${fs}px !important;font-weight:${fw};color:${escAttr(
126
153
  textColor,
127
154
  )} !important;line-height:1.2;">${escAttr(label)}</span>`
128
155
  : '';
129
- return `<span style="display:inline-flex;align-items:center;margin:0 6px;vertical-align:middle;"><a href="${escAttr(
156
+ return `<span style="display:inline-flex;align-items:center;gap:6px;"><a href="${escAttr(
130
157
  l.href,
131
158
  )}" style="display:inline-flex;align-items:center;text-decoration:none;line-height:0;">${iconHtml}</a>${textHtml}</span>`;
132
159
  })
133
160
  .join('');
134
161
  const pad = paddingQuad(p);
135
- return `<div style="padding:${pad};text-align:${p.align};">${items || '<span style="color:#9ca3af;font-size:12px;">请添加社交链接</span>'}</div>`;
162
+ return `<div style="display:flex;flex-wrap:wrap;justify-content:${jc};align-items:center;gap:${gap}px;padding:${pad};">${items || '<span style="color:#9ca3af;font-size:12px;">请添加社交链接</span>'}</div>`;
136
163
  },
137
164
  });
@@ -1,8 +1,10 @@
1
+ import { normalizeFontWeightStep } from '@simple-mail/core';
2
+
1
3
  /** 生成 mj-social-element 的 name 属性(与 MJML 内置图标对齐;未知平台退化为 web)。 */
2
4
  export function mjSocialElementName(network: string): string {
3
5
  const n = network.trim();
4
6
  if (n === 'ig') return 'instagram';
5
- if (n === 'rabbit' || n === 'tiktok') return 'web';
7
+ if (n === 'rabbit' || n === 'tiktok' || n === 'reddit') return 'web';
6
8
  return n;
7
9
  }
8
10
 
@@ -25,6 +27,7 @@ export const SOCIAL_NETWORK_OPTIONS: { label: string; value: string }[] = [
25
27
  { label: 'Facebook', value: 'facebook' },
26
28
  { label: 'Instagram', value: 'instagram' },
27
29
  { label: 'YouTube', value: 'youtube' },
30
+ { label: 'Reddit', value: 'reddit' },
28
31
  { label: '网页', value: 'web' },
29
32
  ];
30
33
 
@@ -38,6 +41,7 @@ const NETWORK_META: Record<string, { color: string; preview: string }> = {
38
41
  instagram: { color: '#E4405F', preview: '◎' },
39
42
  ig: { color: '#E4405F', preview: '◎' },
40
43
  youtube: { color: '#FF0000', preview: '▶' },
44
+ reddit: { color: '#FF4500', preview: 'r' },
41
45
  tiktok: { color: '#000000', preview: '♪' },
42
46
  rabbit: { color: '#FF2442', preview: 'R' },
43
47
  website: { color: '#6b7280', preview: '◇' },
@@ -82,13 +86,46 @@ export function paddingQuad(p: {
82
86
  return `${p.paddingTop}px ${p.paddingRight}px ${p.paddingBottom}px ${p.paddingLeft}px`;
83
87
  }
84
88
 
89
+ /** mj-social `border-radius`:0 直角;≥999 或 ≥图标半边 → 圆形裁切(999px) */
90
+ export function socialIconBorderRadiusMjml(iconSize: number, radiusPx: unknown): string {
91
+ const size = Math.max(1, Number(iconSize) || 28);
92
+ const r = Number(radiusPx);
93
+ if (!Number.isFinite(r) || r <= 0) return '0px';
94
+ const half = size / 2;
95
+ if (r >= 999 || r >= half) return '999px';
96
+ return `${Math.min(r, size)}px`;
97
+ }
98
+
99
+ /** 画布预览用图标圆角 CSS(与 {@link socialIconBorderRadiusMjml} 语义一致) */
100
+ export function socialIconBorderRadiusCss(iconSize: number, radiusPx: unknown): string {
101
+ const size = Math.max(1, Number(iconSize) || 28);
102
+ const r = Number(radiusPx);
103
+ if (!Number.isFinite(r) || r <= 0) return '0';
104
+ const half = size / 2;
105
+ if (r >= 999 || r >= half) return '50%';
106
+ return `${Math.min(r, size)}px`;
107
+ }
108
+
109
+ /** mj-social `inner-padding`,控制图标之间的可视间距 */
110
+ export function socialIconSpacingPx(spacing: unknown, fallback = 10): number {
111
+ const n = Number(spacing);
112
+ if (!Number.isFinite(n) || n < 0) return fallback;
113
+ return Math.min(64, Math.round(n));
114
+ }
115
+
116
+ export function flexJustifyFromAlign(align: 'left' | 'center' | 'right'): string {
117
+ if (align === 'right') return 'flex-end';
118
+ if (align === 'left') return 'flex-start';
119
+ return 'center';
120
+ }
121
+
85
122
  export function mjSocialElementsLines(
86
123
  links: SocialLinkItem[],
87
124
  labelStyle: SocialGroupLabelStyle,
88
125
  ): string {
89
126
  const fontSizeRaw = labelStyle.labelFontSize > 0 ? labelStyle.labelFontSize : 13;
90
127
  const fontSize = Math.max(8, Math.min(48, fontSizeRaw));
91
- const fontWeight = labelStyle.labelFontWeight?.trim() || 'normal';
128
+ const fontWeight = normalizeFontWeightStep(labelStyle.labelFontWeight);
92
129
  const textColor = labelStyle.labelColor?.trim() || '#333333';
93
130
  const fwEsc = escAttr(fontWeight);
94
131
  return links
@@ -1,4 +1,4 @@
1
- import { defineBlock } from '@simple-mail/core';
1
+ import { defineBlock, FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '@simple-mail/core';
2
2
  import { icons } from './icons';
3
3
 
4
4
  interface TextProps {
@@ -28,10 +28,10 @@ export const textBlock = defineBlock<TextProps>({
28
28
  content:
29
29
  '<p style="margin:0;">在此处输入文本。<strong>双击</strong>进入编辑模式,<em>选中文字</em>会出现工具条;支持变量 {{user.name}}</p>',
30
30
  align: 'left',
31
- color: '#433f3f',
32
- fontSize: '16px',
31
+ color: '',
32
+ fontSize: '',
33
33
  fontFamily: '',
34
- fontWeight: 'normal',
34
+ fontWeight: '',
35
35
  lineHeight: '',
36
36
  paddingTop: 8,
37
37
  paddingRight: 0,
@@ -41,27 +41,25 @@ export const textBlock = defineBlock<TextProps>({
41
41
  schema: [
42
42
  {
43
43
  key: 'align',
44
- label: '默认对齐',
44
+ label: '对齐',
45
45
  type: 'select',
46
+ selectVariant: 'segmented',
46
47
  options: [
47
48
  { label: '左', value: 'left' },
48
49
  { label: '居中', value: 'center' },
49
50
  { label: '右', value: 'right' },
50
51
  ],
51
52
  },
52
- { key: 'color', label: '默认颜色', type: 'color' },
53
- { key: 'fontSize', label: '默认字号', type: 'text', placeholder: '14px' },
53
+ { key: 'color', label: '颜色', type: 'color', placeholder: '继承全局' },
54
+ { key: 'fontSize', label: '字号', type: 'text', inheritGlobal: true },
54
55
  { key: 'fontFamily', label: '字体', type: 'text', placeholder: '继承全局' },
55
56
  {
56
57
  key: 'fontWeight',
57
58
  label: '字重',
58
59
  type: 'select',
59
- options: [
60
- { label: '常规 400', value: 'normal' },
61
- { label: '中等 500', value: '500' },
62
- { label: '半粗 600', value: '600' },
63
- { label: '加粗 700', value: 'bold' },
64
- ],
60
+ selectVariant: 'segmented',
61
+ inheritGlobal: true,
62
+ options: [...FONT_WEIGHT_STEP_OPTIONS],
65
63
  },
66
64
  {
67
65
  key: 'lineHeight',
@@ -101,18 +99,26 @@ export const textBlock = defineBlock<TextProps>({
101
99
  toMjml: (p) => {
102
100
  const padding = `${p.paddingTop}px ${p.paddingRight}px ${p.paddingBottom}px ${p.paddingLeft}px`;
103
101
  const ff = p.fontFamily ? ` font-family="${escapeAttr(p.fontFamily)}"` : '';
104
- const fw = p.fontWeight && p.fontWeight !== 'normal' ? ` font-weight="${p.fontWeight}"` : '';
102
+ const fwRaw = String(p.fontWeight ?? '').trim();
103
+ const fw = fwRaw ? ` font-weight="${escapeAttr(normalizeFontWeightStep(fwRaw))}"` : '';
105
104
  const lh = String(p.lineHeight ?? '').trim();
106
105
  const lhAttr = lh ? ` line-height="${escapeAttr(lh)}"` : '';
107
- return `<mj-text align="${p.align}" color="${escapeAttr(p.color)}" font-size="${escapeAttr(
108
- p.fontSize,
109
- )}"${lhAttr}${ff}${fw} padding="${padding}">${p.content}</mj-text>`;
106
+ const colorRaw = String(p.color ?? '').trim();
107
+ const colorAttr = colorRaw ? ` color="${escapeAttr(colorRaw)}"` : '';
108
+ const fsRaw = String(p.fontSize ?? '').trim();
109
+ const fsAttr = fsRaw ? ` font-size="${escapeAttr(fsRaw)}"` : '';
110
+ return `<mj-text align="${p.align}"${colorAttr}${fsAttr}${lhAttr}${ff}${fw} padding="${padding}">${p.content}</mj-text>`;
110
111
  },
111
112
  renderPreview: (p) => {
112
113
  const ff = p.fontFamily ? `font-family:${p.fontFamily};` : '';
113
- const fw = p.fontWeight && p.fontWeight !== 'normal' ? `font-weight:${p.fontWeight};` : '';
114
+ const fwRaw = String(p.fontWeight ?? '').trim();
115
+ const fwCss = fwRaw ? `font-weight:${normalizeFontWeightStep(fwRaw)};` : '';
114
116
  const lhRaw = String(p.lineHeight ?? '').trim();
115
117
  const lh = lhRaw ? `line-height:${lhRaw};` : 'line-height:normal;';
116
- return `<div class="sm-text-content" style="text-align:${p.align};color:${p.color};font-size:${p.fontSize};${lh}${ff}${fw}padding:${p.paddingTop}px ${p.paddingRight}px ${p.paddingBottom}px ${p.paddingLeft}px;">${p.content}</div>`;
118
+ const colorRaw = String(p.color ?? '').trim();
119
+ const colorCss = colorRaw ? `color:${colorRaw};` : '';
120
+ const fsRaw = String(p.fontSize ?? '').trim();
121
+ const fsCss = fsRaw ? `font-size:${fsRaw};` : '';
122
+ return `<div class="sm-text-content" style="text-align:${p.align};${colorCss}${fsCss}${fwCss}${lh}${ff}padding:${p.paddingTop}px ${p.paddingRight}px ${p.paddingBottom}px ${p.paddingLeft}px;">${p.content}</div>`;
117
123
  },
118
124
  });
@@ -4,9 +4,6 @@
4
4
  "outDir": "dist",
5
5
  "noEmit": true,
6
6
  "baseUrl": ".",
7
- "paths": {
8
- "@simple-mail/core": ["../core/src/index.ts"]
9
- },
10
7
  "types": []
11
8
  },
12
9
  "include": ["src", "../core/src/shims.d.ts"]
@@ -7,6 +7,8 @@ export default defineConfig({
7
7
  dts({
8
8
  rollupTypes: true,
9
9
  tsconfigPath: resolve(__dirname, 'tsconfig.dts.json'),
10
+ /** 与 JS 打包一致,避免 .d.ts rollup 把 core 打进 blocks */
11
+ external: ['@simple-mail/core'],
10
12
  }),
11
13
  ],
12
14
  build: {