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
|
@@ -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: '
|
|
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
|
-
|
|
85
|
-
|
|
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 ?? '
|
|
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
|
|
141
|
+
return `<mj-social ${socAttrs}></mj-social>`;
|
|
120
142
|
}
|
|
121
|
-
return `<mj-social
|
|
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
|
|
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:
|
|
139
|
-
: `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius
|
|
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="
|
|
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;
|
|
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="
|
|
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: '
|
|
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
|
-
|
|
67
|
-
|
|
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 ?? '
|
|
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
|
|
125
|
+
return `<mj-social ${socAttrs}></mj-social>`;
|
|
102
126
|
}
|
|
103
|
-
return `<mj-social
|
|
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
|
|
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:
|
|
121
|
-
: `<span style="display:inline-flex;width:${p.iconSize}px;height:${p.iconSize}px;border-radius
|
|
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="
|
|
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;
|
|
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="
|
|
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
|
|
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: '
|
|
32
|
-
fontSize: '
|
|
31
|
+
color: '',
|
|
32
|
+
fontSize: '',
|
|
33
33
|
fontFamily: '',
|
|
34
|
-
fontWeight: '
|
|
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: '
|
|
53
|
-
{ key: 'fontSize', label: '
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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
|
});
|