mail-editor-pancake 0.0.8 → 0.0.9

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.
@@ -1469,29 +1469,70 @@
1469
1469
  .sm-popover {
1470
1470
  position: absolute;
1471
1471
  z-index: 100;
1472
- min-width: 220px;
1473
- max-height: 280px;
1474
- overflow: auto;
1472
+ min-width: 260px;
1475
1473
  background: var(--sm-surface);
1476
1474
  border: 1px solid var(--sm-border);
1477
1475
  border-radius: var(--sm-radius-sm);
1478
1476
  box-shadow: var(--sm-shadow-md);
1479
1477
  }
1478
+
1479
+ .sm-popover--variables {
1480
+ max-height: min(480px, 65vh);
1481
+ overflow: auto;
1482
+ }
1483
+
1484
+ .sm-popover__row {
1485
+ display: flex;
1486
+ align-items: stretch;
1487
+ border-bottom: 1px solid var(--sm-border);
1488
+ }
1489
+ .sm-popover__row:last-child {
1490
+ border-bottom: 0;
1491
+ }
1492
+
1480
1493
  .sm-popover__item {
1481
1494
  display: flex;
1482
1495
  flex-direction: column;
1483
1496
  gap: 2px;
1484
- padding: 8px 12px;
1497
+ flex: 1;
1498
+ min-width: 0;
1499
+ padding: 10px 12px;
1485
1500
  cursor: pointer;
1486
1501
  border: 0;
1487
- width: 100%;
1488
1502
  text-align: left;
1489
1503
  background: transparent;
1490
1504
  font: inherit;
1491
1505
  }
1492
- .sm-popover__item:hover { background: var(--sm-primary-soft); }
1493
- .sm-popover__key { font-family: monospace; color: var(--sm-primary); }
1494
- .sm-popover__label { font-size: 11px; color: var(--sm-text-muted); }
1506
+ .sm-popover__item:hover { background: var(--sm-surface-hover); }
1507
+ .sm-popover__label { font-size: 13px; color: var(--sm-text); }
1508
+ .sm-popover__key {
1509
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1510
+ font-size: 12px;
1511
+ color: var(--sm-text-muted);
1512
+ }
1513
+
1514
+ .sm-popover__actions {
1515
+ display: flex;
1516
+ flex-shrink: 0;
1517
+ align-items: center;
1518
+ gap: 6px;
1519
+ margin-right: 8px;
1520
+ }
1521
+
1522
+ .sm-popover__action {
1523
+ padding: 4px 10px;
1524
+ border: 1px solid var(--sm-border);
1525
+ border-radius: var(--sm-radius-sm);
1526
+ background: var(--sm-surface);
1527
+ color: var(--sm-text-muted);
1528
+ font-size: 12px;
1529
+ cursor: pointer;
1530
+ white-space: nowrap;
1531
+ }
1532
+ .sm-popover__action:hover {
1533
+ background: var(--sm-surface-hover);
1534
+ color: var(--sm-text);
1535
+ }
1495
1536
 
1496
1537
  /* Modal -------------------------------------------------------------------- */
1497
1538
  /* 弹层常挂在 document.body,无 .sm-root 祖先时 CSS 变量不存在;凡 var(...) 须带浅色兜底,避免卡片「透明」与页面叠在一起 */
@@ -2,6 +2,17 @@ export { MailEditor } from './editor/Editor';
2
2
  export { defineBlock, Registry } from './registry/registry';
3
3
  export { mjRawCellTypographyFromStyles, mjRawEmailTableWrap } from './utils/lockedMjml';
4
4
  export { renderDoc, replaceVariables } from './renderer';
5
+ export {
6
+ buildBodyVariableElementInsert,
7
+ buildBodyVariableInsert,
8
+ buildBodyVariableKeyInsert,
9
+ buildLinkVariableHtml,
10
+ defaultVariableSample,
11
+ normalizeVariable,
12
+ tokenToVariableKey,
13
+ variablePlaceholder,
14
+ } from './variables';
15
+ export type { BodyVariableInsert, VariableKind } from './variables';
5
16
  export { docToMjml } from './renderer/mjml';
6
17
  export {
7
18
  Store,
@@ -42,6 +42,8 @@ export interface Variable {
42
42
  label: string;
43
43
  /** 预览数据,仅设计态预览使用 */
44
44
  sample?: string;
45
+ /** 插入语义:纯文本占位 / 链接片段 / 图片块 */
46
+ kind?: 'text' | 'link' | 'image';
45
47
  }
46
48
 
47
49
  export type SectionLayout = '1' | '1-1' | '1-2' | '2-1' | '1-1-1';
@@ -0,0 +1,16 @@
1
+ /**
2
+ * 向富文本 HTML 末尾「行内」追加内容,避免落在 `</p>` 外产生换行。
3
+ * 无块级标签时包一层 `<p style="margin:0;">`。
4
+ */
5
+ export function appendInlineToRichHtml(html: string, insert: string): string {
6
+ const trimmed = String(html ?? '').trim();
7
+ if (!insert) return trimmed;
8
+ if (!trimmed) return `<p style="margin:0;">${insert}</p>`;
9
+
10
+ const blockClose = trimmed.match(/<\/(p|div|h[1-6]|li)>\s*$/i);
11
+ if (blockClose && blockClose.index != null) {
12
+ const idx = blockClose.index;
13
+ return trimmed.slice(0, idx) + insert + trimmed.slice(idx);
14
+ }
15
+ return trimmed + insert;
16
+ }
@@ -0,0 +1,91 @@
1
+ import type { Variable } from '../types';
2
+
3
+ export type VariableKind = 'text' | 'link' | 'image';
4
+
5
+ /** 从 Mustache token 提取 key,如 `{{username}}` → `username` */
6
+ export function tokenToVariableKey(token: string): string {
7
+ const t = String(token || '').trim();
8
+ const m = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
9
+ return (m?.[1] || t.replace(/^\{\{\s*/, '').replace(/\s*\}\}$/, '')).trim();
10
+ }
11
+
12
+ export function variablePlaceholder(key: string): string {
13
+ return `{{${key}}}`;
14
+ }
15
+
16
+ /** 无后端 sample 时按 kind / label 生成预览用示例值(仅预览弹窗等场景) */
17
+ export function defaultVariableSample(v: Pick<Variable, 'key' | 'label' | 'kind' | 'sample'>): string {
18
+ if (v.sample != null && String(v.sample).trim() !== '') return String(v.sample);
19
+ if (v.kind === 'image') return 'https://via.placeholder.com/120x80/e8e8e8/666666?text=Image';
20
+ if (v.kind === 'link') return '#';
21
+ return v.label?.trim() || v.key;
22
+ }
23
+
24
+ /** 规范化 Variable(补 kind、sample) */
25
+ export function normalizeVariable(v: Variable): Variable {
26
+ const key = v.key?.trim() || tokenToVariableKey(String(v.key));
27
+ const kind = v.kind ?? 'text';
28
+ return {
29
+ ...v,
30
+ key,
31
+ kind,
32
+ sample: defaultVariableSample({ ...v, key, kind }),
33
+ };
34
+ }
35
+
36
+ export function buildLinkVariableHtml(token: string, linkColor: string): string {
37
+ const safeHref = token.replace(/"/g, '&quot;');
38
+ const safeText = token
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/</g, '&lt;')
41
+ .replace(/>/g, '&gt;');
42
+ return `<a href="${safeHref}" style="color:${linkColor} !important;text-decoration:underline;">${safeText}</a>`;
43
+ }
44
+
45
+ export interface BodyVariableInsert {
46
+ content: string;
47
+ asHtml: boolean;
48
+ }
49
+
50
+ /** 插入变量 key(`{{key}}`) */
51
+ export function buildBodyVariableKeyInsert(v: Variable): BodyVariableInsert {
52
+ const normalized = normalizeVariable(v);
53
+ return { content: variablePlaceholder(normalized.key), asHtml: false };
54
+ }
55
+
56
+ /** 插入链接 / 图片等元素片段 */
57
+ export function buildBodyVariableElementInsert(
58
+ v: Variable,
59
+ options?: { linkColor?: string; token?: string },
60
+ ): BodyVariableInsert {
61
+ const normalized = normalizeVariable(v);
62
+ const token = options?.token?.trim() || variablePlaceholder(normalized.key);
63
+ const linkColor = options?.linkColor?.trim() || '#ff5a00';
64
+
65
+ if (normalized.kind === 'link') {
66
+ return {
67
+ content: buildLinkVariableHtml(token, linkColor),
68
+ asHtml: true,
69
+ };
70
+ }
71
+ if (normalized.kind === 'image') {
72
+ const alt = tokenToVariableKey(token);
73
+ return {
74
+ content: `<img src="${token.replace(/"/g, '&quot;')}" alt="${alt.replace(/"/g, '&quot;')}" width="100" style="max-width:100%;height:auto;display:block;border:0;" />`,
75
+ asHtml: true,
76
+ };
77
+ }
78
+ return buildBodyVariableKeyInsert(normalized);
79
+ }
80
+
81
+ /** @deprecated 使用 buildBodyVariableKeyInsert / buildBodyVariableElementInsert */
82
+ export function buildBodyVariableInsert(
83
+ v: Variable,
84
+ options?: { linkColor?: string; token?: string },
85
+ ): BodyVariableInsert {
86
+ const normalized = normalizeVariable(v);
87
+ if (normalized.kind === 'link' || normalized.kind === 'image') {
88
+ return buildBodyVariableElementInsert(normalized, options);
89
+ }
90
+ return buildBodyVariableKeyInsert(normalized);
91
+ }