mail-editor-pancake 0.2.1 → 0.2.3

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.
@@ -32,7 +32,7 @@ import {
32
32
  resolveGlobalListIndentPx,
33
33
  } from '../utils/emailListStyles';
34
34
  import { FONT_WEIGHT_STEP_OPTIONS, normalizeFontWeightStep } from '../utils/fontWeightSteps';
35
- import type { ImageAssetsHandlers, ImageFieldContext } from './imageAssets';
35
+ import { resolveAssetPermission, type ImageAssetsHandlers, type ImageFieldContext } from './imageAssets';
36
36
  import { FocusBreadcrumb } from './FocusBreadcrumb';
37
37
  import { openImageGalleryModal } from './ImageGalleryModal';
38
38
  import {
@@ -81,7 +81,7 @@ export interface RightPanelOptions {
81
81
  /** 点击路径中的文档级层级(等同 Esc 从 Section 上浮) */
82
82
  onFocusDocument: () => void;
83
83
  /**
84
- * 可选。配置后,schema 中 `type: 'image'` 的字段会显示「上传 / 图床」按钮并调用对应回调。
84
+ * 可选。配置后,schema 中 `type: 'image'` 的字段会显示「上传 / 图库」按钮并调用对应回调。
85
85
  */
86
86
  imageAssets?: ImageAssetsHandlers;
87
87
  /** 与 MailEditor.opts.ui 对齐 */
@@ -562,6 +562,24 @@ export class RightPanel {
562
562
  `section:${section.id}:attrs.columnGap`,
563
563
  )
564
564
  : null,
565
+ section.layout !== '1' && !a.preserveColumnsOnMobile
566
+ ? this._numberField(
567
+ this.opts.t('rightPanel.section.stackedGap'),
568
+ a.columnStackedGap ?? 0,
569
+ 0,
570
+ 64,
571
+ (v) => {
572
+ const g = Math.max(0, Math.min(64, Math.round(v)));
573
+ this.opts.store.update((d) => {
574
+ const s = findSection(d, section.id);
575
+ if (s) s.attrs.columnStackedGap = g > 0 ? g : undefined;
576
+ });
577
+ },
578
+ 1,
579
+ `section:${section.id}:attrs.stackedGap`,
580
+ this.opts.t('rightPanel.section.stackedGapHelp'),
581
+ )
582
+ : null,
565
583
  section.layout !== '1'
566
584
  ? this._switchField(
567
585
  this.opts.t('rightPanel.section.preserveMobile'),
@@ -939,7 +957,7 @@ export class RightPanel {
939
957
  ]);
940
958
  }
941
959
 
942
- /** `type: 'image'`:URL 输入 + 可选上传、图床(由 `imageAssets` 注入) */
960
+ /** `type: 'image'`:URL 输入 + 可选上传、图库(由 `imageAssets` 注入) */
943
961
  private _imageField(
944
962
  label: string,
945
963
  value: string,
@@ -1005,19 +1023,33 @@ export class RightPanel {
1005
1023
  );
1006
1024
  },
1007
1025
  });
1008
- row.append(
1009
- h(
1010
- 'button',
1011
- {
1012
- class: 'sm-btn sm-btn--secondary sm-image-field__btn',
1013
- type: 'button',
1014
- title: this.opts.t('rightPanel.image.uploadTitle'),
1015
- onclick: () => fileInp.click(),
1016
- },
1017
- [this.opts.t('rightPanel.image.upload')],
1018
- ),
1026
+ const uploadPerm = resolveAssetPermission(
1027
+ assets?.permissions,
1028
+ 'upload',
1029
+ this.opts.t('common.noPermissionUpload'),
1019
1030
  );
1020
- row.append(fileInp);
1031
+ if (!uploadPerm.allowed && uploadPerm.mode === 'hide') {
1032
+ // 无权限且配置为隐藏:不渲染上传按钮(fileInp 同样不渲染)
1033
+ } else {
1034
+ row.append(
1035
+ h(
1036
+ 'button',
1037
+ {
1038
+ class: 'sm-btn sm-btn--secondary sm-image-field__btn',
1039
+ type: 'button',
1040
+ title: uploadPerm.allowed
1041
+ ? this.opts.t('rightPanel.image.uploadTitle')
1042
+ : uploadPerm.tip,
1043
+ disabled: !uploadPerm.allowed,
1044
+ ...(uploadPerm.allowed
1045
+ ? { onclick: () => fileInp.click() }
1046
+ : {}),
1047
+ },
1048
+ [this.opts.t('rightPanel.image.upload')],
1049
+ ),
1050
+ );
1051
+ row.append(fileInp);
1052
+ }
1021
1053
  }
1022
1054
 
1023
1055
  if (showGalleryBtn) {
@@ -1032,6 +1064,7 @@ export class RightPanel {
1032
1064
  if (assets.imageGallery) {
1033
1065
  openImageGalleryModal({
1034
1066
  adapter: assets.imageGallery,
1067
+ permissions: assets?.permissions,
1035
1068
  onPick: (url) => applyUrl(url),
1036
1069
  t: this.opts.t,
1037
1070
  parent: this.el.closest('.sm-root') as HTMLElement | undefined,
@@ -1122,6 +1155,24 @@ export class RightPanel {
1122
1155
  onChange: (v: number) => void,
1123
1156
  step = 1,
1124
1157
  focusToken?: string,
1158
+ help?: string,
1159
+ ): HTMLElement {
1160
+ const field = this._buildNumberField(label, value, min, max, onChange, step, focusToken);
1161
+ if (!help) return field;
1162
+ return h('div', { class: 'sm-field', style: 'flex-direction:column;align-items:flex-start;gap:6px;' }, [
1163
+ field,
1164
+ h('div', { class: 'sm-field__help' }, [help]),
1165
+ ]);
1166
+ }
1167
+
1168
+ private _buildNumberField(
1169
+ label: string,
1170
+ value: number,
1171
+ min: number,
1172
+ max: number,
1173
+ onChange: (v: number) => void,
1174
+ step = 1,
1175
+ focusToken?: string,
1125
1176
  ) {
1126
1177
  const snap = (n: number) => {
1127
1178
  const c = Math.min(max, Math.max(min, n));
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 右侧「图片地址」类字段:宿主可选手输 URL、本地上传、内置图库弹层或完全自管图床回调。
2
+ * 右侧「图片地址」类字段:宿主可选手输 URL、本地上传、内置图库弹层或完全自管图库回调。
3
3
  * 编辑器最终只把 URL 写入 props。
4
4
  */
5
5
 
@@ -26,6 +26,62 @@ export interface ImageGalleryListResult {
26
26
  hasMore: boolean;
27
27
  }
28
28
 
29
+ /**
30
+ * 资源动作权限的语义标识。宿主在 `ImageAssetPermissions.check` 中据此返回是否有权限:
31
+ * - `'upload'`:右栏「上传」按钮、图库弹层「上传」按钮(对应创建/上传权限)
32
+ * - `'addUrl'`:图库弹层「添加链接」按钮(对应创建权限)
33
+ * - `'delete'`:图库弹层缩略图删除按钮(对应删除权限)
34
+ */
35
+ export type AssetPermissionAction = 'upload' | 'addUrl' | 'delete';
36
+
37
+ /**
38
+ * 图片资源动作的权限管控配置。未配置 `check` 时视为全部有权限(不做管控)。
39
+ */
40
+ export interface ImageAssetPermissions {
41
+ /**
42
+ * 权限校验:返回 `false` 表示该动作无权限。
43
+ * 未配置时视为全部有权限。
44
+ */
45
+ check?: (action: AssetPermissionAction) => boolean;
46
+ /**
47
+ * 无权限时的表现:
48
+ * - `'disable'`(默认):按钮置灰禁用,hover 提示无权限。
49
+ * - `'hide'`:直接隐藏对应按钮。
50
+ */
51
+ noPermissionMode?: 'disable' | 'hide';
52
+ /**
53
+ * 无权限禁用时 hover 提示文案;未配置或为空时使用编辑器默认文案(`common.noPermission`)。
54
+ */
55
+ noPermissionTip?: string;
56
+ }
57
+
58
+ /** `resolveAssetPermission` 的结果。 */
59
+ export interface ResolvedAssetPermission {
60
+ /** `true` 表示有权限 */
61
+ allowed: boolean;
62
+ /** 无权限时的表现(仅在 `allowed === false` 时有意义) */
63
+ mode: 'disable' | 'hide';
64
+ /** 无权限禁用时的 hover 提示文案(仅在 `allowed === false` 且 `mode === 'disable'` 时有意义) */
65
+ tip: string;
66
+ }
67
+
68
+ /**
69
+ * 解析某个动作的权限状态。未配置 `permissions` 或 `check` 时视为有权限。
70
+ * @param defaultNoPermissionTip 编辑器默认的无权限提示文案(由调用方从 i18n 传入)
71
+ */
72
+ export function resolveAssetPermission(
73
+ permissions: ImageAssetPermissions | undefined,
74
+ action: AssetPermissionAction,
75
+ defaultNoPermissionTip: string,
76
+ ): ResolvedAssetPermission {
77
+ if (!permissions?.check) return { allowed: true, mode: 'disable', tip: '' };
78
+ const allowed = permissions.check(action);
79
+ if (allowed) return { allowed: true, mode: 'disable', tip: '' };
80
+ const mode: 'disable' | 'hide' = permissions.noPermissionMode === 'hide' ? 'hide' : 'disable';
81
+ const tip = (permissions.noPermissionTip ?? '').trim() || defaultNoPermissionTip;
82
+ return { allowed: false, mode, tip };
83
+ }
84
+
29
85
  /**
30
86
  * 内置图库弹层的数据与扩展能力。实现 `listItems` 即可;上传、链接添加、删除等仅在提供对应方法时显示。
31
87
  */
@@ -47,7 +103,7 @@ export interface ImageAssetsHandlers {
47
103
  uploadImage?: (file: File, ctx: ImageFieldContext) => Promise<string>;
48
104
 
49
105
  /**
50
- * 完全自管图床:与内置 `imageGallery` 可并存;
106
+ * 完全自管图库:与内置 `imageGallery` 可并存;
51
107
  * **同时配置时优先使用内置 `imageGallery` 弹层**。
52
108
  */
53
109
  pickImageFromGallery?: (ctx: ImageFieldContext) => Promise<string | null>;
@@ -65,8 +121,14 @@ export interface ImageAssetsHandlers {
65
121
  showUpload?: boolean;
66
122
 
67
123
  /**
68
- * 是否显示「图床」按钮。在配置了 `pickImageFromGallery` 或 `imageGallery` 时有效。
124
+ * 是否显示「图库」按钮。在配置了 `pickImageFromGallery` 或 `imageGallery` 时有效。
69
125
  * 默认 `false`;需显式设为 `true` 才展示入口。
70
126
  */
71
127
  showGallery?: boolean;
128
+
129
+ /**
130
+ * 资源动作权限管控。配置后,「上传 / 添加链接 / 删除」按钮将按 `check` 结果禁用或隐藏。
131
+ * 默认无权限时禁用并 hover 提示。右栏「上传」与图库弹层内按钮共用同一份配置。
132
+ */
133
+ permissions?: ImageAssetPermissions;
72
134
  }
@@ -1294,6 +1294,14 @@
1294
1294
  border-color: var(--sm-primary);
1295
1295
  }
1296
1296
 
1297
+ .sm-input[disabled],
1298
+ .sm-textarea[disabled],
1299
+ .sm-select[disabled] {
1300
+ opacity: 0.55;
1301
+ cursor: not-allowed;
1302
+ background: var(--sm-surface-disabled, #f3f4f6);
1303
+ }
1304
+
1297
1305
  .sm-spacing-grid {
1298
1306
  display: grid;
1299
1307
  grid-template-columns: repeat(4, 1fr);
@@ -2148,6 +2156,13 @@
2148
2156
  .sm-gallery-modal__cell-delete:hover {
2149
2157
  background: var(--sm-danger, #dc2626);
2150
2158
  }
2159
+ .sm-gallery-modal__cell-delete[disabled] {
2160
+ opacity: 0.45;
2161
+ cursor: not-allowed;
2162
+ }
2163
+ .sm-gallery-modal__cell-delete[disabled]:hover {
2164
+ background: rgba(15, 23, 42, 0.82);
2165
+ }
2151
2166
 
2152
2167
  .sm-gallery-modal__cell {
2153
2168
  display: flex;
@@ -31,6 +31,10 @@ export const zhCNMessages: SimpleMailMessages = {
31
31
  'common.insert': '插入',
32
32
  'common.copy': '复制',
33
33
  'common.image': '图片',
34
+ 'common.noPermission': '无权限',
35
+ 'common.noPermissionUpload': '无上传权限',
36
+ 'common.noPermissionAdd': '无添加权限',
37
+ 'common.noPermissionDelete': '无删除权限',
34
38
  'common.left': '左',
35
39
  'common.center': '居中',
36
40
  'common.right': '右',
@@ -136,6 +140,8 @@ export const zhCNMessages: SimpleMailMessages = {
136
140
  'rightPanel.section.widthHelp': '留空=与邮件同宽;自适应等价于清空宽度',
137
141
  'rightPanel.section.padding': '内边距',
138
142
  'rightPanel.section.columnGap': '列间距 (px)',
143
+ 'rightPanel.section.stackedGap': '堆叠纵向间距 (px)',
144
+ 'rightPanel.section.stackedGapHelp': '小屏堆叠为单列后,相邻列之间的额外纵向间距;0 表示沿用块自身内边距。',
139
145
  'rightPanel.section.preserveMobile': '小屏仍并排显示多列(可能字很窄)',
140
146
  'rightPanel.section.preserveMobileHelp': '开启后 MJML 会生成 mj-group,移动端预览/导出与默认「小屏堆叠列」行为不同。',
141
147
  'rightPanel.section.columnLayout': '列布局',
@@ -145,10 +151,10 @@ export const zhCNMessages: SimpleMailMessages = {
145
151
  'rightPanel.image.uploadTitle': '上传本地图片',
146
152
  'rightPanel.image.upload': '上传',
147
153
  'rightPanel.image.galleryTitle': '从图库选择',
148
- 'rightPanel.image.gallery': '图床',
154
+ 'rightPanel.image.gallery': '图库',
149
155
  'rightPanel.image.helpManual': '可手输 URL',
150
156
  'rightPanel.image.helpUpload': '或使用「上传」',
151
- 'rightPanel.image.helpGallery': '或使用「图床」',
157
+ 'rightPanel.image.helpGallery': '或使用「图库」',
152
158
  'rightPanel.image.helpSeparator': ';',
153
159
  'rightPanel.image.helpEnd': '。',
154
160
  'rightPanel.image.helpAssetsConfigured': '已传入 imageAssets:上传默认显示(提供 uploadImage 且未设 showUpload:false);图库需 showGallery:true,可使用内置 imageGallery 或自管 pickImageFromGallery。仍可手输 URL。',
@@ -334,6 +340,10 @@ export const enUSMessages: SimpleMailMessages = {
334
340
  'common.insert': 'Insert',
335
341
  'common.copy': 'Copy',
336
342
  'common.image': 'Image',
343
+ 'common.noPermission': 'No permission',
344
+ 'common.noPermissionUpload': 'No upload permission',
345
+ 'common.noPermissionAdd': 'No add permission',
346
+ 'common.noPermissionDelete': 'No delete permission',
337
347
  'common.left': 'Left',
338
348
  'common.center': 'Center',
339
349
  'common.right': 'Right',
@@ -439,6 +449,8 @@ export const enUSMessages: SimpleMailMessages = {
439
449
  'rightPanel.section.widthHelp': 'Empty = email width. Auto is equivalent to clearing the width.',
440
450
  'rightPanel.section.padding': 'Padding',
441
451
  'rightPanel.section.columnGap': 'Column gap (px)',
452
+ 'rightPanel.section.stackedGap': 'Stacked vertical gap (px)',
453
+ 'rightPanel.section.stackedGapHelp': 'Extra vertical space between columns once they stack on small screens. 0 keeps each block own padding.',
442
454
  'rightPanel.section.preserveMobile': 'Keep columns side by side on small screens',
443
455
  'rightPanel.section.preserveMobileHelp': 'When enabled, MJML outputs mj-group. Mobile preview/export differs from the default stacked-column behavior.',
444
456
  'rightPanel.section.columnLayout': 'Column layout',
@@ -86,12 +86,16 @@ export type {
86
86
  } from './types';
87
87
  export type { EditorTheme } from './editor/theme';
88
88
  export type {
89
+ AssetPermissionAction,
89
90
  GalleryItem,
90
91
  ImageAssetsHandlers,
92
+ ImageAssetPermissions,
91
93
  ImageFieldContext,
92
94
  ImageGalleryAdapter,
93
95
  ImageGalleryListResult,
96
+ ResolvedAssetPermission,
94
97
  } from './editor/imageAssets';
98
+ export { resolveAssetPermission } from './editor/imageAssets';
95
99
  export type { OpenImageGalleryModalOptions } from './editor/ImageGalleryModal';
96
100
  export { openImageGalleryModal } from './editor/ImageGalleryModal';
97
101
  export {
@@ -34,6 +34,7 @@ export function docToMjml(doc: EmailDoc, registry: Registry): string {
34
34
  <mj-style>
35
35
  a { color: ${escapeAttr(attrs.linkColor)}; }
36
36
  ${sectionWidthConstraintCss(doc)}
37
+ ${columnGapMobileCss(doc)}
37
38
  </mj-style>
38
39
  </mj-head>
39
40
  <mj-body background-color="${escapeAttr(attrs.backgroundColor)}" width="${escapeAttr(docContentWidthCss(doc.meta.width))}">
@@ -55,6 +56,65 @@ function sectionWidthConstraintCss(doc: EmailDoc): string {
55
56
  .join('\n');
56
57
  }
57
58
 
59
+ /**
60
+ * 列间距相关的小屏规则。
61
+ *
62
+ * 1) 水平间距(columnGap)用 mj-column 对称 padding 实现,小屏堆叠后 padding 不消失会导致
63
+ * 相邻列内容纵向错位(首列右缩、次列左缩),需要在堆叠时清零。
64
+ * 2) 堆叠纵向间距(columnStackedGap)只挂在非末列上,作为堆叠后相邻列之间的额外间距。
65
+ *
66
+ * 共同注意点:mj-column 的 padding 不落在带 css-class 的外层 div 上,编译结果是
67
+ * `div[class] > table > tbody > tr > td{padding:...}`,所以规则必须命中这一层 td,
68
+ * 用直接子代链避免波及块自身的 padding。
69
+ * 断点取 MJML 默认堆叠断点 480px;!important 用于覆盖行内样式。
70
+ * 仅对会小屏堆叠的列生效;mj-group(preserveColumnsOnMobile)不堆叠,不注入。
71
+ */
72
+ const COLUMN_GAP_MOBILE_BREAKPOINT = 480;
73
+
74
+ function columnGapClassName(sectionId: string): string {
75
+ return `${sectionMjClassName(sectionId)}-cg`;
76
+ }
77
+
78
+ function columnStackedGapClassName(sectionId: string): string {
79
+ return `${sectionMjClassName(sectionId)}-svg`;
80
+ }
81
+
82
+ /** 该 Section 的列是否会在小屏堆叠(未包 mj-group 的多列) */
83
+ function stacksOnMobile(section: Section): boolean {
84
+ return section.columns.length > 1 && section.attrs.preserveColumnsOnMobile !== true;
85
+ }
86
+
87
+ function columnGapMobileCss(doc: EmailDoc): string {
88
+ const lines: string[] = [];
89
+
90
+ for (const s of doc.sections) {
91
+ if (!stacksOnMobile(s)) continue;
92
+ const gap = Math.max(0, s.attrs.columnGap ?? 0);
93
+ const stackedGap = Math.max(0, Math.round(s.attrs.columnStackedGap ?? 0));
94
+ if (gap <= 0 && stackedGap <= 0) continue;
95
+
96
+ if (gap > 0) {
97
+ const cls = columnGapClassName(s.id);
98
+ lines.push(
99
+ ` .${cls},`,
100
+ ` .${cls} > table > tbody > tr > td { padding-left: 0 !important; padding-right: 0 !important; }`,
101
+ );
102
+ }
103
+ if (stackedGap > 0) {
104
+ lines.push(
105
+ ` .${columnStackedGapClassName(
106
+ s.id,
107
+ )} > table > tbody > tr > td { padding-bottom: ${stackedGap}px !important; }`,
108
+ );
109
+ }
110
+ }
111
+
112
+ if (!lines.length) return '';
113
+ return ` @media only screen and (max-width:${COLUMN_GAP_MOBILE_BREAKPOINT}px) {
114
+ ${lines.join('\n')}
115
+ }`;
116
+ }
117
+
58
118
  function sectionToMjml(section: Section, registry: Registry, ctx: RenderContext): string {
59
119
  const a = section.attrs;
60
120
  const padding = [a.paddingTop, a.paddingRight, a.paddingBottom, a.paddingLeft]
@@ -67,10 +127,27 @@ function sectionToMjml(section: Section, registry: Registry, ctx: RenderContext)
67
127
  secW || dvKey ? ` css-class="${escapeAttr(sectionMjClassName(section.id))}"` : '';
68
128
  const widths = layoutWidths(section.layout);
69
129
  const gapPx = Math.max(0, section.attrs.columnGap ?? 0);
130
+ /** 仅小屏堆叠时才有水平复位与纵向间距;与下方 grouped 判定保持一致 */
131
+ const stacked = stacksOnMobile(section);
132
+ const hCls = stacked && gapPx > 0 ? columnGapClassName(section.id) : '';
133
+ const vCls =
134
+ stacked && Math.max(0, Math.round(section.attrs.columnStackedGap ?? 0)) > 0
135
+ ? columnStackedGapClassName(section.id)
136
+ : '';
137
+ const lastIndex = section.columns.length - 1;
70
138
 
71
139
  const columns = section.columns
72
140
  .map((col, i) =>
73
- columnToMjml(col, widths[i], i, section.columns.length, gapPx, registry, ctx),
141
+ columnToMjml(
142
+ col,
143
+ widths[i],
144
+ i,
145
+ section.columns.length,
146
+ gapPx,
147
+ [hCls, vCls && i < lastIndex ? vCls : ''].filter(Boolean).join(' '),
148
+ registry,
149
+ ctx,
150
+ ),
74
151
  )
75
152
  .join('\n');
76
153
 
@@ -92,6 +169,7 @@ function columnToMjml(
92
169
  columnIndex: number,
93
170
  columnCount: number,
94
171
  columnGapPx: number,
172
+ gapClassNames: string,
95
173
  registry: Registry,
96
174
  ctx: RenderContext,
97
175
  ): string {
@@ -106,6 +184,7 @@ function columnToMjml(
106
184
  const pr = columnIndex < columnCount - 1 ? `${half}px` : '0px';
107
185
  gapPad = ` padding="0px ${pr} 0px ${pl}"`;
108
186
  }
187
+ const clsAttr = gapClassNames ? ` css-class="${escapeAttr(gapClassNames)}"` : '';
109
188
 
110
189
  const rawTypo = mjRawCellTypographyFromStyles(ctx.doc.styles);
111
190
  const blocks = column.blocks
@@ -129,7 +208,7 @@ function columnToMjml(
129
208
  })
130
209
  .join('\n');
131
210
 
132
- return ` <mj-column width="${width}"${va}${bg}${gapPad}>
211
+ return ` <mj-column width="${width}"${va}${bg}${gapPad}${clsAttr}>
133
212
  ${blocks || ' <!-- empty column -->'}
134
213
  </mj-column>`;
135
214
  }
@@ -77,6 +77,13 @@ export interface SectionAttrs {
77
77
  * MJML 通过相邻 `mj-column` 对称内边距实现;画布用 flex `gap` 对齐观感。
78
78
  */
79
79
  columnGap?: number;
80
+ /**
81
+ * 多列在小屏堆叠为单列后,相邻列之间的纵向间距(px)。
82
+ * 仅在多列且未开启 `preserveColumnsOnMobile` 时生效;未设置/0 表示不额外加间距
83
+ * (沿用块自身 padding 形成的间距),避免改变已有邮件的移动端排版。
84
+ * 通过小屏媒体查询实现,不会影响桌面端行高。
85
+ */
86
+ columnStackedGap?: number;
80
87
  /**
81
88
  * 本节内容区最大宽度(窄于邮件 `meta.width` 时居中)。支持 `480`、`480px`、`90%`;留空则与邮件同宽。
82
89
  */
@@ -195,10 +202,21 @@ export interface BlockDefinition<P extends object = Record<string, unknown>> {
195
202
  ) => Block[] | PaletteDropResult;
196
203
  }
197
204
 
198
- /** 左栏组合拖入:可附带新建 Section 的 attrs(如 meta 扩展袋) */
205
+ /** 左栏组合拖入:可附带新建 Section 的 attrs、布局和按列分发的 Block。 */
199
206
  export interface PaletteDropResult {
207
+ /** 单列组合或落入既有列时插入的 Block。 */
200
208
  blocks: Block[];
201
209
  sectionAttrs?: Partial<SectionAttrs>;
210
+ /**
211
+ * 组合入口可声明要新建的 Section 布局;多列布局会始终创建独立 Section,
212
+ * 不会被插入到用户当前选中的单列中。
213
+ */
214
+ sectionLayout?: SectionLayout;
215
+ /**
216
+ * 仅创建新 Section 时使用;每个数组对应一个 Column,索引与 Section 列顺序一致。
217
+ * 超出布局列数的数组会被忽略,缺少的列保持为空。
218
+ */
219
+ columnBlocks?: Block[][];
202
220
  }
203
221
 
204
222
  export interface InlineEditableConfig<P extends object = Record<string, unknown>> {