mail-editor-pancake 0.0.7 → 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.
@@ -151,6 +151,55 @@
151
151
  flex: 1;
152
152
  }
153
153
 
154
+ .sm-focus-crumb {
155
+ display: flex;
156
+ align-items: center;
157
+ flex-wrap: wrap;
158
+ gap: 2px;
159
+ width: 100%;
160
+ min-width: 0;
161
+ font-size: 12px;
162
+ line-height: 1.35;
163
+ color: var(--sm-text-muted);
164
+ }
165
+ .sm-focus-crumb__sep {
166
+ flex-shrink: 0;
167
+ padding: 0 2px;
168
+ color: var(--sm-text-muted);
169
+ opacity: 0.55;
170
+ user-select: none;
171
+ }
172
+ .sm-focus-crumb__item {
173
+ display: inline-block;
174
+ max-width: 9em;
175
+ overflow: hidden;
176
+ text-overflow: ellipsis;
177
+ white-space: nowrap;
178
+ border: 0;
179
+ background: transparent;
180
+ padding: 2px 6px;
181
+ margin: 0;
182
+ border-radius: var(--sm-radius-sm);
183
+ font: inherit;
184
+ font-size: 12px;
185
+ color: var(--sm-text-muted);
186
+ cursor: pointer;
187
+ vertical-align: middle;
188
+ }
189
+ .sm-focus-crumb__item:hover {
190
+ background: var(--sm-ghost-hover-bg);
191
+ color: var(--sm-text);
192
+ }
193
+ .sm-focus-crumb__item--current {
194
+ color: var(--sm-text);
195
+ font-weight: 600;
196
+ cursor: default;
197
+ }
198
+ .sm-focus-crumb__item--current:hover {
199
+ background: transparent;
200
+ color: var(--sm-text);
201
+ }
202
+
154
203
  .sm-topbar__theme-seg {
155
204
  flex-shrink: 0;
156
205
  }
@@ -205,6 +254,17 @@
205
254
  display: block;
206
255
  flex-shrink: 0;
207
256
  }
257
+ .sm-topbar__toggle-btn[aria-pressed="true"] {
258
+ background: var(--sm-primary-soft);
259
+ color: var(--sm-primary);
260
+ }
261
+ .sm-topbar__toggle-btn .sm-layout-borders-check {
262
+ opacity: 0;
263
+ transition: opacity 0.12s ease;
264
+ }
265
+ .sm-topbar__toggle-btn[aria-pressed="true"] .sm-layout-borders-check {
266
+ opacity: 1;
267
+ }
208
268
 
209
269
  .sm-btn {
210
270
  display: inline-flex;
@@ -309,7 +369,30 @@
309
369
  overflow: auto;
310
370
  min-width: 0;
311
371
  }
312
- .sm-panel--right { border-right: 0; border-left: 1px solid var(--sm-border); }
372
+ .sm-panel--right {
373
+ border-right: 0;
374
+ border-left: 1px solid var(--sm-border);
375
+ display: flex;
376
+ flex-direction: column;
377
+ overflow: hidden;
378
+ }
379
+
380
+ .sm-panel__head {
381
+ flex-shrink: 0;
382
+ padding: 10px 12px 8px;
383
+ border-bottom: 1px solid var(--sm-border);
384
+ background: var(--sm-panel-bg);
385
+ }
386
+
387
+ .sm-panel__head[hidden] {
388
+ display: none;
389
+ }
390
+
391
+ .sm-panel__body {
392
+ flex: 1;
393
+ min-height: 0;
394
+ overflow: auto;
395
+ }
313
396
 
314
397
  .sm-panel__title {
315
398
  font-size: 12px;
@@ -403,19 +486,46 @@
403
486
  /* 由 Canvas 在运行时设为 doc.styles.linkColor;兜底与默认邮件 link 橙一致 */
404
487
  --sm-editor-link-color: #ff5a00;
405
488
  }
406
- /* 用户代理对 <a> 的默认色会盖过继承的 color,需与预览/MJML 全局链接色对齐 */
407
- .sm-canvas a,
408
- .sm-canvas a:link,
409
- .sm-canvas a:visited,
410
- .sm-canvas a:hover,
411
- .sm-canvas a:active {
412
- color: var(--sm-editor-link-color) !important;
489
+ /*
490
+ * 无 inline color 时使用文档级链接色;局部 style 优先于全局(不用 !important,避免盖过 foreColor)。
491
+ * 属性选择器排除 background-color 等误匹配(仅匹配 style 中的 color: 声明)。
492
+ */
493
+ .sm-canvas a:not([style^="color:"]):not([style*=";color:"]):not([style*="; color:"]),
494
+ .sm-canvas a:not([style^="color:"]):not([style*=";color:"]):not([style*="; color:"]):link,
495
+ .sm-canvas a:not([style^="color:"]):not([style*=";color:"]):not([style*="; color:"]):visited,
496
+ .sm-canvas a:not([style^="color:"]):not([style*=";color:"]):not([style*="; color:"]):hover,
497
+ .sm-canvas a:not([style^="color:"]):not([style*=";color:"]):not([style*="; color:"]):active {
498
+ color: var(--sm-editor-link-color);
413
499
  }
414
500
 
415
501
  .sm-canvas .sm-text-content :where(p) {
416
502
  margin: 0;
417
503
  }
418
504
 
505
+ /* 宿主全局 reset(如 p/strong { font-weight:400 })会盖过块级字重配置,子节点须继承容器 */
506
+ .sm-canvas .sm-text-content :where(
507
+ p,
508
+ li,
509
+ td,
510
+ th,
511
+ span,
512
+ div,
513
+ h1,
514
+ h2,
515
+ h3,
516
+ h4,
517
+ h5,
518
+ h6,
519
+ address,
520
+ caption
521
+ ) {
522
+ font-weight: inherit;
523
+ }
524
+
525
+ .sm-canvas .sm-text-content :where(strong, b) {
526
+ font-weight: bolder;
527
+ }
528
+
419
529
  .sm-canvas .sm-text-content :where(ul, ol) {
420
530
  margin: 0.5em 0;
421
531
  padding-left: 1.5em;
@@ -430,15 +540,31 @@
430
540
  list-style-type: decimal;
431
541
  }
432
542
 
433
- /* Section 在画布中的呈现层(包了一层壳便于选中/排序) */
543
+ /* Section 外层:Sortable 拖拽壳,与列内 Sortable 分离避免嵌套冲突 */
544
+ .sm-section-host {
545
+ position: relative;
546
+ }
547
+ .sm-section-host:hover,
548
+ .sm-section-host.is-selected,
549
+ .sm-section-host.sm-chosen,
550
+ .sm-section-host.sm-drag {
551
+ z-index: 5;
552
+ }
553
+
554
+ /* Section 在画布中的呈现层 */
434
555
  .sm-section {
435
556
  position: relative;
436
557
  outline: 1px dashed transparent;
437
558
  outline-offset: -1px;
438
559
  transition: outline-color 0.12s;
439
560
  }
440
- .sm-section:hover { outline-color: var(--sm-outline-muted); }
441
- .sm-section.is-selected {
561
+ .sm-root.sm-show-layout-borders .sm-section {
562
+ outline-color: var(--sm-outline-muted);
563
+ }
564
+ .sm-section-host:hover .sm-section {
565
+ outline-color: var(--sm-outline-muted);
566
+ }
567
+ .sm-section-host.is-selected .sm-section {
442
568
  outline: 2px solid var(--sm-primary);
443
569
  }
444
570
  .sm-section__toolbar {
@@ -446,19 +572,37 @@
446
572
  top: auto;
447
573
  bottom: 100%;
448
574
  right: 8px;
575
+ left: auto;
449
576
  margin-bottom: 10px;
450
- z-index: 1;
577
+ z-index: 10;
451
578
  display: none;
452
579
  align-items: center;
453
580
  gap: 6px;
454
581
  flex-direction: row;
455
582
  flex-wrap: nowrap;
456
583
  max-width: calc(100% - 16px);
457
- /* 贴在 Section 上沿之上,避免压住白纸圆角与选中描边(translateY(-100%) 在部分布局下仍会叠在内容上) */
584
+ pointer-events: auto;
585
+ transition: margin-bottom 0.12s ease;
586
+ /* 贴在 Section 上沿之上,避免压住白纸圆角与选中描边 */
458
587
  transform: none;
459
588
  }
460
- .sm-section.is-selected .sm-section__toolbar,
461
- .sm-section:hover .sm-section__toolbar {
589
+ /* 内部 Block 工具条可见时,Section 工具条上移让位(尤其多列第一列 Block 条在左侧,避免叠层抢点击) */
590
+ .sm-section-host:has(.sm-block:hover) .sm-section__toolbar,
591
+ .sm-section-host:has(.sm-block.is-selected) .sm-section__toolbar,
592
+ .sm-section-host:has(.sm-block.is-editing) .sm-section__toolbar {
593
+ /* margin-bottom: calc(10px + 44px); */
594
+ }
595
+ /* Block 交互时抬高内容层,保证列内工具条(z-index:30)在 Section 工具条之上可点 */
596
+ .sm-section-host:has(.sm-block:hover) .sm-section,
597
+ .sm-section-host:has(.sm-block.is-selected) .sm-section,
598
+ .sm-section-host:has(.sm-block.is-editing) .sm-section {
599
+ position: relative;
600
+ z-index: 20;
601
+ }
602
+ .sm-section-host.is-selected .sm-section__toolbar,
603
+ .sm-section-host:hover .sm-section__toolbar,
604
+ .sm-section-host.sm-chosen .sm-section__toolbar,
605
+ .sm-section-host.sm-drag .sm-section__toolbar {
462
606
  display: inline-flex;
463
607
  }
464
608
  .sm-section__toolbar-label {
@@ -474,7 +618,7 @@
474
618
  user-select: none;
475
619
  flex-shrink: 0;
476
620
  }
477
- .sm-section.is-selected .sm-section__toolbar-label {
621
+ .sm-section-host.is-selected .sm-section__toolbar-label {
478
622
  border-color: var(--sm-primary-soft);
479
623
  color: var(--sm-primary);
480
624
  }
@@ -484,9 +628,28 @@
484
628
  gap: 4px;
485
629
  flex-shrink: 0;
486
630
  }
487
- .sm-section.is-selected .sm-section__toolbar-actions {
631
+ .sm-section-host.is-selected .sm-section__toolbar-actions,
632
+ .sm-section-host.sm-chosen .sm-section__toolbar-actions,
633
+ .sm-section-host.sm-drag .sm-section__toolbar-actions {
488
634
  display: inline-flex;
489
635
  }
636
+ .sm-section-host.is-selected .sm-section__toolbar,
637
+ .sm-section-host.sm-chosen .sm-section__toolbar,
638
+ .sm-section-host.sm-drag .sm-section__toolbar {
639
+ background: rgba(31, 35, 40, 0.92);
640
+ padding: 4px 6px;
641
+ border-radius: 6px;
642
+ box-shadow: 0 4px 14px rgba(16, 24, 40, 0.14);
643
+ }
644
+ .sm-section-host.is-selected .sm-section__toolbar-label,
645
+ .sm-section-host.sm-chosen .sm-section__toolbar-label,
646
+ .sm-section-host.sm-drag .sm-section__toolbar-label {
647
+ background: transparent;
648
+ border: 0;
649
+ box-shadow: none;
650
+ padding: 0;
651
+ color: rgba(255, 255, 255, 0.88);
652
+ }
490
653
  .sm-tool-btn {
491
654
  width: 24px;
492
655
  height: 24px;
@@ -501,13 +664,18 @@
501
664
  }
502
665
  .sm-tool-btn:hover { filter: brightness(1.1); }
503
666
  .sm-tool-btn--danger { background: var(--sm-danger); }
667
+ .sm-section__handle { cursor: grab; }
668
+ .sm-section__handle:active { cursor: grabbing; }
504
669
 
505
670
  .sm-section__columns {
506
671
  position: relative;
507
- z-index: 2;
508
672
  display: flex;
509
673
  width: 100%;
510
674
  }
675
+ .sm-section-host.sm-chosen .sm-section__columns,
676
+ .sm-section-host.sm-drag .sm-section__columns {
677
+ pointer-events: none;
678
+ }
511
679
 
512
680
  .sm-column {
513
681
  flex: 1 1 auto;
@@ -540,13 +708,30 @@
540
708
  .sm-block {
541
709
  position: relative;
542
710
  outline: 1px dashed transparent;
543
- outline-offset: -2px;
711
+ /* 不用负 offset,避免行首 caret 被描边盖住 */
712
+ outline-offset: 0;
544
713
  }
545
- .sm-block:hover { outline-color: var(--sm-outline-muted); }
546
- .sm-block.is-selected { outline: 2px solid var(--sm-primary); }
547
- .sm-block.is-editing {
714
+ .sm-root.sm-show-layout-borders .sm-block:not(.is-selected):not(.is-editing) {
715
+ outline-color: var(--sm-outline-muted);
716
+ }
717
+ .sm-block:hover:not(.is-selected):not(.is-editing) {
718
+ outline-color: var(--sm-outline-muted);
719
+ }
720
+ .sm-block.is-selected:not(.is-editing) {
548
721
  outline: 2px solid var(--sm-primary);
722
+ }
723
+ .sm-block.is-editing {
724
+ /* 编辑中:块级虚线框(偏外),与内层 focus 实线区分 */
725
+ outline: 1px dashed color-mix(in srgb, var(--sm-primary) 55%, var(--sm-border));
726
+ outline-offset: 3px;
549
727
  cursor: text;
728
+ /* 列内 Block 按 DOM 顺序绘制;下方块(如 padding-top:0 的分割线)会盖住 offset 描边 */
729
+ z-index: 3;
730
+ }
731
+ /* hover / 选中态同样抬升,避免 2px 实线描边被相邻块遮挡(编辑态 z-index 更高,见 .is-editing) */
732
+ .sm-block:hover:not(.is-editing),
733
+ .sm-block.is-selected:not(.is-editing) {
734
+ z-index: 2;
550
735
  }
551
736
  .sm-block__content { position: relative; }
552
737
 
@@ -556,7 +741,7 @@
556
741
  bottom: 100%;
557
742
  right: 0;
558
743
  margin-bottom: 8px;
559
- z-index: 20;
744
+ z-index: 30;
560
745
  display: none;
561
746
  align-items: center;
562
747
  gap: 6px;
@@ -733,6 +918,7 @@
733
918
  align-items: center;
734
919
  }
735
920
  .sm-social-link-list__color {
921
+ cursor: pointer;
736
922
  width: 100%;
737
923
  height: 32px;
738
924
  padding: 2px;
@@ -853,6 +1039,16 @@
853
1039
  text-align: center;
854
1040
  line-height: 1.2;
855
1041
  }
1042
+ .sm-spacing-row input[type='range'],
1043
+ .sm-field__slider-row input[type='range'],
1044
+ .sm-width-field__slider-row input[type='range'] {
1045
+ cursor: pointer;
1046
+ }
1047
+ .sm-spacing-row input[type='range']::-webkit-slider-thumb,
1048
+ .sm-field__slider-row input[type='range']::-webkit-slider-thumb,
1049
+ .sm-width-field__slider-row input[type='range']::-webkit-slider-thumb {
1050
+ cursor: pointer;
1051
+ }
856
1052
  .sm-spacing-row input[type='range'] {
857
1053
  flex: 1 1 auto;
858
1054
  min-width: 0;
@@ -880,11 +1076,67 @@
880
1076
  font-size: 11px;
881
1077
  }
882
1078
 
1079
+ .sm-field__label-row {
1080
+ display: flex;
1081
+ align-items: center;
1082
+ justify-content: space-between;
1083
+ gap: 8px;
1084
+ }
1085
+
1086
+ .sm-inherit-switch {
1087
+ display: flex;
1088
+ align-items: center;
1089
+ justify-content: space-between;
1090
+ gap: 8px;
1091
+ }
1092
+ .sm-inherit-switch__label {
1093
+ display: inline-flex;
1094
+ align-items: center;
1095
+ gap: 6px;
1096
+ cursor: pointer;
1097
+ user-select: none;
1098
+ }
1099
+ .sm-inherit-switch__text {
1100
+ font-size: 12px;
1101
+ color: var(--sm-text-muted);
1102
+ }
1103
+ .sm-inherit-switch__input {
1104
+ width: 28px;
1105
+ height: 16px;
1106
+ margin: 0;
1107
+ accent-color: var(--sm-primary);
1108
+ cursor: pointer;
1109
+ }
1110
+ .sm-inherit-switch__value {
1111
+ font-size: 12px;
1112
+ color: var(--sm-text-muted);
1113
+ font-variant-numeric: tabular-nums;
1114
+ }
1115
+ .sm-inherit-switch__value[hidden] {
1116
+ display: none;
1117
+ }
1118
+ .sm-segmented--disabled {
1119
+ opacity: 0.45;
1120
+ }
1121
+
883
1122
  .sm-field__slider-row {
884
1123
  display: flex;
885
1124
  align-items: center;
886
1125
  gap: 8px;
887
1126
  }
1127
+ .sm-field__slider-row--inherit {
1128
+ opacity: 0.55;
1129
+ }
1130
+ .sm-field__slider-row--inherit input[type='range'] {
1131
+ accent-color: var(--sm-outline-muted);
1132
+ }
1133
+ .sm-field__slider-row--inherit input[type='number']:disabled {
1134
+ background: var(--sm-surface-hover);
1135
+ color: var(--sm-text-muted);
1136
+ }
1137
+ .sm-input--inherit-disabled {
1138
+ opacity: 0.55;
1139
+ }
888
1140
  .sm-field__slider-row input[type='range'] {
889
1141
  flex: 1;
890
1142
  min-width: 0;
@@ -937,6 +1189,11 @@
937
1189
  border: 1px solid var(--sm-border);
938
1190
  border-radius: 6px;
939
1191
  background: none;
1192
+ cursor: pointer;
1193
+ }
1194
+ .sm-color-row input[type='color'].sm-color-row__pick--inherit {
1195
+ opacity: 0.55;
1196
+ border-style: dashed;
940
1197
  }
941
1198
 
942
1199
  .sm-empty-form {
@@ -1009,13 +1266,26 @@
1009
1266
 
1010
1267
  /* 内联编辑 + 富文本浮动工具条 -------------------------------------------- */
1011
1268
  .sm-inline-editing {
1012
- outline: 2px dashed var(--sm-primary) !important;
1269
+ /* 编辑中未聚焦(如点浮动工具条):虚线 + 淡底;正 offset 不挡行首 caret */
1270
+ outline: 2px dashed color-mix(in srgb, var(--sm-primary) 70%, var(--sm-outline-muted)) !important;
1013
1271
  outline-offset: 2px;
1272
+ box-shadow: none;
1273
+ border-radius: 2px;
1274
+ background-color: color-mix(in srgb, var(--sm-primary-soft) 55%, transparent);
1014
1275
  cursor: text;
1015
1276
  min-height: 1em;
1016
1277
  caret-color: var(--sm-primary);
1278
+ transition:
1279
+ outline-color 0.12s ease,
1280
+ box-shadow 0.12s ease,
1281
+ background-color 0.12s ease;
1282
+ }
1283
+ .sm-inline-editing:focus {
1284
+ outline: 2px solid var(--sm-primary) !important;
1285
+ outline-offset: 2px;
1286
+ box-shadow: 0 0 0 4px var(--sm-primary-soft);
1287
+ background-color: color-mix(in srgb, var(--sm-primary-soft) 35%, transparent);
1017
1288
  }
1018
- .sm-inline-editing:focus { outline-style: solid !important; }
1019
1289
  .sm-inline-editing:empty::before {
1020
1290
  content: attr(data-placeholder);
1021
1291
  color: var(--sm-placeholder);
@@ -1112,6 +1382,89 @@
1112
1382
  .sm-floating-toolbar__link-panel .sm-input::placeholder { color: rgba(255,255,255,0.5); }
1113
1383
  .sm-floating-toolbar__link-panel .sm-btn { height: 26px; }
1114
1384
 
1385
+ /* 自定义色板(右栏 / 富文本色卡) ---------------------------------------- */
1386
+ .sm-color-picker-layer {
1387
+ position: absolute;
1388
+ z-index: 210;
1389
+ }
1390
+ .sm-color-picker {
1391
+ width: 220px;
1392
+ padding: 10px;
1393
+ background: var(--sm-surface, #fff);
1394
+ border: 1px solid var(--sm-border, #e6e8ec);
1395
+ border-radius: var(--sm-radius-sm, 6px);
1396
+ box-shadow: var(--sm-shadow-md, 0 4px 16px rgba(16, 24, 40, 0.08));
1397
+ }
1398
+ .sm-color-picker__head {
1399
+ display: flex;
1400
+ align-items: center;
1401
+ justify-content: space-between;
1402
+ gap: 8px;
1403
+ margin-bottom: 8px;
1404
+ }
1405
+ .sm-color-picker__title {
1406
+ font-size: 12px;
1407
+ font-weight: 600;
1408
+ color: var(--sm-text, #1f2328);
1409
+ }
1410
+ .sm-color-picker__close {
1411
+ flex-shrink: 0;
1412
+ width: 24px;
1413
+ height: 24px;
1414
+ padding: 0;
1415
+ border: 0;
1416
+ border-radius: var(--sm-radius-sm, 6px);
1417
+ background: transparent;
1418
+ color: var(--sm-text-muted, #6b7280);
1419
+ font-size: 18px;
1420
+ line-height: 1;
1421
+ cursor: pointer;
1422
+ }
1423
+ .sm-color-picker__close:hover {
1424
+ background: var(--sm-ghost-hover-bg, #f0f1f4);
1425
+ color: var(--sm-text, #1f2328);
1426
+ }
1427
+ .sm-color-picker__swatches {
1428
+ display: grid;
1429
+ grid-template-columns: repeat(8, 1fr);
1430
+ gap: 4px;
1431
+ margin-bottom: 8px;
1432
+ }
1433
+ .sm-color-picker__swatch {
1434
+ width: 100%;
1435
+ aspect-ratio: 1;
1436
+ padding: 0;
1437
+ border: 1px solid rgba(0, 0, 0, 0.08);
1438
+ border-radius: 4px;
1439
+ cursor: pointer;
1440
+ }
1441
+ .sm-color-picker__swatch:hover {
1442
+ outline: 2px solid var(--sm-primary, #4f46e5);
1443
+ outline-offset: 1px;
1444
+ }
1445
+ .sm-color-picker__native {
1446
+ width: 100%;
1447
+ height: 32px;
1448
+ margin-bottom: 6px;
1449
+ padding: 0;
1450
+ border: 1px solid var(--sm-border, #e6e8ec);
1451
+ border-radius: 6px;
1452
+ cursor: pointer;
1453
+ }
1454
+ .sm-color-picker__hex {
1455
+ width: 100%;
1456
+ text-align: center;
1457
+ }
1458
+ .sm-color-picker__foot {
1459
+ display: flex;
1460
+ justify-content: flex-end;
1461
+ margin-top: 8px;
1462
+ }
1463
+ .sm-color-picker__clear {
1464
+ height: 26px;
1465
+ font-size: 12px;
1466
+ }
1467
+
1115
1468
  /* 弹层(变量插入) ------------------------------------------------------- */
1116
1469
  .sm-popover {
1117
1470
  position: absolute;
@@ -3,7 +3,18 @@ export { defineBlock, Registry } from './registry/registry';
3
3
  export { mjRawCellTypographyFromStyles, mjRawEmailTableWrap } from './utils/lockedMjml';
4
4
  export { renderDoc, replaceVariables } from './renderer';
5
5
  export { docToMjml } from './renderer/mjml';
6
- export { Store, createSection, findSection, findBlockLocation, pruneEmptySections } from './store/store';
6
+ export {
7
+ Store,
8
+ createSection,
9
+ findSection,
10
+ findBlockLocation,
11
+ pruneEmptySections,
12
+ pruneSectionIfEmpty,
13
+ isTwoColumnLayout,
14
+ setSectionLayout,
15
+ TWO_COLUMN_LAYOUTS,
16
+ } from './store/store';
17
+ export type { TwoColumnLayout } from './store/store';
7
18
  export type { MjRawCellTypography } from './utils/lockedMjml';
8
19
  export {
9
20
  FONT_WEIGHT_STEP_OPTIONS,
@@ -126,16 +126,57 @@ export function findBlockLocation(
126
126
  }
127
127
 
128
128
  /**
129
- * 从文档中移除「每一列都没有任何 Block」的 Section。
130
- * 在内容块被删除或拖走后调用,可与 `autoWrapSection` 搭配,避免唯一块离场后仍残留空壳;
131
- * 不会在用户仅点击「添加 Section」时单独触发,因此未拖入内容的空布局仍会保留在画布上。
129
+ * 若指定 Section 的每一列都没有 Block,则从文档中移除该 Section。
130
+ * 在内容块被删除或拖走后调用,可与 `autoWrapSection` 搭配,避免单列 Section 在唯一块离场后仍残留空壳;
131
+ * 多列 Section 始终保留,便于继续向各列拖入内容。
132
+ * 仅处理刚变空的 Section,不会误删用户事先添加、尚未拖入内容的空布局。
132
133
  */
134
+ export function pruneSectionIfEmpty(draft: EmailDoc, sectionId: string) {
135
+ const sec = draft.sections.find((s) => s.id === sectionId);
136
+ if (!sec) return;
137
+ if (sec.columns.length > 1) return;
138
+ const isEmpty = sec.columns.every((col) => col.blocks.length === 0);
139
+ if (!isEmpty) return;
140
+ draft.sections = draft.sections.filter((s) => s.id !== sectionId);
141
+ }
142
+
143
+ /** @deprecated 请改用 {@link pruneSectionIfEmpty},避免误删多个空 Section */
133
144
  export function pruneEmptySections(draft: EmailDoc) {
134
145
  draft.sections = draft.sections.filter((sec) =>
135
146
  sec.columns.some((col) => col.blocks.length > 0),
136
147
  );
137
148
  }
138
149
 
150
+ /** 两列 Section 的列宽比例(左栏拖入默认 `1-1`,右栏可改为 `1-2` / `2-1`) */
151
+ export const TWO_COLUMN_LAYOUTS = ['1-1', '1-2', '2-1'] as const;
152
+ export type TwoColumnLayout = (typeof TWO_COLUMN_LAYOUTS)[number];
153
+
154
+ export function isTwoColumnLayout(layout: SectionLayout): layout is TwoColumnLayout {
155
+ return layout === '1-1' || layout === '1-2' || layout === '2-1';
156
+ }
157
+
158
+ /** 仅更新 layout 字段;两列间切换时列数不变,列内块按列下标保留 */
159
+ export function setSectionLayout(section: Section, layout: SectionLayout): void {
160
+ const nextCols = layout.split('-').length;
161
+ const curCols = section.columns.length;
162
+ if (nextCols === curCols) {
163
+ section.layout = layout;
164
+ return;
165
+ }
166
+ if (nextCols > curCols) {
167
+ for (let i = curCols; i < nextCols; i++) {
168
+ section.columns.push({ id: uid('col'), attrs: {}, blocks: [] });
169
+ }
170
+ } else {
171
+ const removed = section.columns.splice(nextCols);
172
+ const last = section.columns[nextCols - 1];
173
+ if (last) {
174
+ for (const col of removed) last.blocks.push(...col.blocks);
175
+ }
176
+ }
177
+ section.layout = layout;
178
+ }
179
+
139
180
  export function createSection(layout: SectionLayout): Section {
140
181
  const colCount = layout.split('-').length;
141
182
  const columns: Column[] = Array.from({ length: colCount }, () => ({
@@ -136,6 +136,8 @@ export interface BlockSchemaField {
136
136
  step?: number;
137
137
  placeholder?: string;
138
138
  help?: string;
139
+ /** 为 true 时右栏显示「继承全局」开关:开=留空跟随邮件全局,关=写入当前全局值为块级固定值 */
140
+ inheritGlobal?: boolean;
139
141
  }
140
142
 
141
143
  export interface BlockDefinition<P extends object = Record<string, unknown>> {
@@ -223,6 +225,10 @@ export interface EditorUiOptions {
223
225
  * 为 true 时隐藏顶栏「全屏」图标按钮(浏览器原生全屏 API,作用于编辑器根节点)。
224
226
  */
225
227
  hideTopbarFullscreen?: boolean;
228
+ /** 为 true 时隐藏顶栏「清空画布」按钮。 */
229
+ hideTopbarClearCanvas?: boolean;
230
+ /** 为 true 时隐藏顶栏「重置内容」按钮。 */
231
+ hideTopbarResetContent?: boolean;
226
232
  }
227
233
 
228
234
  export interface EditorEvents {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * contenteditable 富文本仍依赖 Document.execCommand / queryCommand* 系列 API。
3
+ * 浏览器与 TypeScript lib.dom 虽已标记 deprecated,但目前无等价替代;集中封装便于 IDE 消警与日后迁移。
4
+ */
5
+ export interface RichTextDocument extends Document {
6
+ execCommand(commandId: string, showUI?: boolean, value?: string): boolean;
7
+ queryCommandState(commandId: string): boolean;
8
+ queryCommandValue(commandId: string): string;
9
+ }
10
+
11
+ function asRichTextDoc(doc: Document = document): RichTextDocument {
12
+ return doc as RichTextDocument;
13
+ }
14
+
15
+ export function richTextExecCommand(
16
+ commandId: string,
17
+ showUI = false,
18
+ value?: string,
19
+ doc: Document = document,
20
+ ): boolean {
21
+ return asRichTextDoc(doc).execCommand(commandId, showUI, value);
22
+ }
23
+
24
+ export function richTextQueryCommandState(commandId: string, doc: Document = document): boolean {
25
+ return asRichTextDoc(doc).queryCommandState(commandId);
26
+ }
27
+
28
+ export function richTextQueryCommandValue(commandId: string, doc: Document = document): string {
29
+ return asRichTextDoc(doc).queryCommandValue(commandId);
30
+ }
@@ -0,0 +1,13 @@
1
+ import type { SectionLayout } from '../types';
2
+
3
+ const LAYOUT_LABELS: Record<SectionLayout, string> = {
4
+ '1': '一列',
5
+ '1-1': '两列',
6
+ '1-2': '1:2',
7
+ '2-1': '2:1',
8
+ '1-1-1': '三列',
9
+ };
10
+
11
+ export function layoutHumanLabel(layout: SectionLayout): string {
12
+ return LAYOUT_LABELS[layout] ?? layout;
13
+ }