@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview

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 (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
package/dist/shell.js CHANGED
@@ -1,9 +1,9 @@
1
- import { createDefaultIconRegistry, hydrateIcons } from '@bpmn-nova/icons';
2
- import { createDefaultPaletteRegistry, PalettePanel } from '@bpmn-nova/palette';
3
- import { createDefaultPropertiesRegistry, PropertiesPanel } from '@bpmn-nova/properties';
4
- import { demoRuntime } from '@bpmn-nova/runtime';
5
- import { BpmnViewer } from '@bpmn-nova/viewer';
6
- import { ThemeController } from '@bpmn-nova/theme';
1
+ import { createDefaultIconRegistry, hydrateIcons } from './modules/icons/index.js';
2
+ import { createDefaultPaletteRegistry, PalettePanel } from './modules/palette/index.js';
3
+ import { createDefaultPropertiesRegistry, PropertiesPanel } from './modules/properties/index.js';
4
+ import { BpmnViewer } from './modules/viewer/index.js';
5
+ import { ThemeController } from './modules/theme/index.js';
6
+ import { openSvgExportPreview } from './modules/export-svg/index.js';
7
7
  import { BpmnCanvas } from './canvas.js';
8
8
  import { createDefaultContextMenuRegistry } from './context-menu.js';
9
9
  import { createInteractionController, createTemplateRegistry } from './interactions.js';
@@ -28,6 +28,39 @@ function mountSlot(slot, container, context) {
28
28
  return null;
29
29
  }
30
30
 
31
+ const STUDIO_MODES = ['design', 'viewer', 'instance'];
32
+ const STUDIO_SHELL_REGIONS = ['header', 'left', 'right', 'footer'];
33
+ const DEFAULT_STUDIO_SHELL_REGIONS = Object.freeze({
34
+ header: 'default',
35
+ left: 'default',
36
+ right: 'default',
37
+ footer: 'default',
38
+ });
39
+
40
+ function normalizeAllowedModes(allowedModes) {
41
+ if (allowedModes === undefined || allowedModes === null) return [...STUDIO_MODES];
42
+ if (!Array.isArray(allowedModes)) throw new Error('allowedModes must be an array of Studio modes.');
43
+ const normalized = [...new Set(allowedModes.filter((mode) => STUDIO_MODES.includes(mode)))];
44
+ if (!normalized.length || normalized.length !== allowedModes.length) {
45
+ throw new Error('allowedModes must contain one or more unique design, viewer, or instance modes.');
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ function normalizeRegions(regions, current = DEFAULT_STUDIO_SHELL_REGIONS) {
51
+ if (regions === undefined || regions === null) return { ...current };
52
+ if (!regions || typeof regions !== 'object' || Array.isArray(regions)) {
53
+ throw new Error('regions must be an object containing header, left, right, or footer modes.');
54
+ }
55
+ for (const name of Object.keys(regions)) {
56
+ if (!STUDIO_SHELL_REGIONS.includes(name)) throw new Error(`Unknown Studio Shell region: ${name}.`);
57
+ if (!['default', 'hidden'].includes(regions[name])) {
58
+ throw new Error(`Studio Shell region "${name}" must be "default" or "hidden".`);
59
+ }
60
+ }
61
+ return { ...current, ...regions };
62
+ }
63
+
31
64
  export class BpmnStudioShell {
32
65
  constructor({
33
66
  container,
@@ -40,8 +73,10 @@ export class BpmnStudioShell {
40
73
  rendererOptions = {},
41
74
  slots = {},
42
75
  layout = null,
43
- runtime = demoRuntime(),
76
+ regions = null,
77
+ runtime = null,
44
78
  mode = 'design',
79
+ allowedModes = null,
45
80
  projection = undefined,
46
81
  responsive = false,
47
82
  projectionOptions = null,
@@ -49,11 +84,17 @@ export class BpmnStudioShell {
49
84
  rightWidth = 360,
50
85
  theme = null,
51
86
  runtimeAppearance = null,
87
+ svgExport = null,
52
88
  onThemeChange = null,
53
89
  } = {}) {
54
90
  if (!container || !studio) throw new Error('BpmnStudioShell requires container and studio.');
91
+ if (typeof layout === 'function' && regions !== null && regions !== undefined) {
92
+ throw new Error('BpmnStudioShell options "layout" and "regions" cannot be used together.');
93
+ }
94
+ const resolvedAllowedModes = normalizeAllowedModes(allowedModes);
95
+ const resolvedMode = resolvedAllowedModes.includes(mode) ? mode : resolvedAllowedModes[0];
55
96
  const instanceProjection = projection || (responsive ? 'auto' : 'approval');
56
- const activeProjection = mode === 'instance' ? instanceProjection : 'standard';
97
+ const activeProjection = resolvedMode === 'instance' ? instanceProjection : 'standard';
57
98
  Object.assign(this, {
58
99
  container,
59
100
  studio,
@@ -64,22 +105,57 @@ export class BpmnStudioShell {
64
105
  rendererOptions,
65
106
  slots,
66
107
  runtime,
67
- mode,
108
+ mode: resolvedMode,
109
+ allowedModes: resolvedAllowedModes,
68
110
  projection: activeProjection,
69
111
  _instanceProjection: instanceProjection,
70
112
  responsive,
71
113
  projectionOptions,
72
114
  runtimeAppearance,
115
+ svgExport: svgExport || rendererOptions.svgExport || null,
116
+ _usesCustomLayout: typeof layout === 'function',
117
+ _regions: normalizeRegions(regions),
73
118
  });
74
119
  this.propertiesRegistry = propertiesRegistry || createDefaultPropertiesRegistry({ studio });
75
120
  this.interactions = createInteractionController({ studio, templates: templateRegistry });
76
121
  this._cleanups = [];
77
122
  this._instances = [];
123
+ this._svgExportPreview = null;
124
+ this.actions = Object.freeze({
125
+ undo: () => this.studio.undo(),
126
+ redo: () => this.studio.redo(),
127
+ beautify: (options) => {
128
+ this.studio.commands.beautify(options);
129
+ requestAnimationFrame(() => this._fitActive());
130
+ },
131
+ rerouteEdges: (options) => {
132
+ this.studio.commands.rerouteEdges(options);
133
+ requestAnimationFrame(() => this._fitActive());
134
+ },
135
+ fitView: () => this.fitView(),
136
+ validate: () => this.studio.validate(),
137
+ importXml: (xml, engine) => this.studio.importXml(xml, engine),
138
+ exportXml: (engine) => this.studio.exportXml(engine),
139
+ exportSvg: (options) => this.exportSvg(options),
140
+ openSvgExportPreview: (options) => this.openSvgExportPreview(options),
141
+ });
78
142
  this.container.classList.add('nova-studio-shell');
79
143
  this.themeController = new ThemeController({ root: this.container, theme, onChange: onThemeChange });
80
144
  this.container.style.setProperty('--nova-left-width', `${leftWidth}px`);
81
145
  this.container.style.setProperty('--nova-right-width', `${rightWidth}px`);
82
- const context = { studio, iconRegistry, paletteRegistry, propertiesRegistry: this.propertiesRegistry, templateRegistry, contextMenuRegistry, interactions: this.interactions };
146
+ const context = {
147
+ studio,
148
+ shell: this,
149
+ actions: this.actions,
150
+ getState: () => this.studio.getState(),
151
+ subscribe: (listener) => this.studio.subscribe(listener),
152
+ iconRegistry,
153
+ paletteRegistry,
154
+ propertiesRegistry: this.propertiesRegistry,
155
+ templateRegistry,
156
+ contextMenuRegistry,
157
+ interactions: this.interactions,
158
+ };
83
159
  const mount = {
84
160
  canvas: (host, options = {}) => this._mountCanvas(host, options),
85
161
  palette: (host, options = {}) => this._mountPalette(host, options),
@@ -105,7 +181,7 @@ export class BpmnStudioShell {
105
181
  { value: 'design', label: '流程设计', iconId: 'ui.design' },
106
182
  { value: 'viewer', label: '流程展示', iconId: 'ui.preview' },
107
183
  { value: 'instance', label: '审批轨迹', iconId: 'ui.trace' },
108
- ]) {
184
+ ].filter((item) => this.allowedModes.includes(item.value))) {
109
185
  const button = node('button');
110
186
  button.type = 'button';
111
187
  button.setAttribute('data-mode', item.value);
@@ -140,8 +216,8 @@ export class BpmnStudioShell {
140
216
  };
141
217
 
142
218
  const divider = () => tools.appendChild(node('span', 'nova-studio-tool-divider'));
143
- const undo = tool('', '撤销 Ctrl/⌘+Z', () => this.studio.undo(), 'is-icon', 'ui.undo');
144
- const redo = tool('', '重做 Ctrl/⌘+Y', () => this.studio.redo(), 'is-icon', 'ui.redo');
219
+ const undo = tool('', '撤销 Ctrl/⌘+Z', () => this.actions.undo(), 'is-icon', 'ui.undo');
220
+ const redo = tool('', '重做 Ctrl/⌘+Y', () => this.actions.redo(), 'is-icon', 'ui.redo');
145
221
  divider();
146
222
  const beautifySplit = node('div', 'nova-studio-beautify-split');
147
223
  const beautifyButton = node('button', 'nova-studio-tool nova-studio-beautify is-magic');
@@ -196,7 +272,7 @@ export class BpmnStudioShell {
196
272
  });
197
273
  beautifySplit.append(beautifyButton, beautifyCaret, beautifyMenu);
198
274
  tools.appendChild(beautifySplit);
199
- const fit = tool('最佳视图', '适应画布内容', () => this._fitActive(), '', 'ui.fitView');
275
+ const fit = tool('最佳视图', '适应画布内容', () => this.actions.fitView(), '', 'ui.fitView');
200
276
 
201
277
  const importInput = node('input', 'nova-studio-import-input');
202
278
  importInput.type = 'file';
@@ -204,12 +280,39 @@ export class BpmnStudioShell {
204
280
  importInput.hidden = true;
205
281
  const validateButton = tool('校验', '校验流程结构', () => this._showValidationStatus(), 'nova-studio-validate');
206
282
  const importButton = tool('导入', '导入 BPMN XML', () => importInput.click(), 'nova-studio-import');
207
- const exportButton = tool('导出 BPMN', '导出当前流程 XML', () => this._exportBpmn(), 'nova-studio-export is-primary');
283
+ const exportMenu = node('div', 'nova-studio-export-menu');
284
+ const exportButton = node('button', 'nova-studio-tool nova-studio-export is-primary');
285
+ exportButton.type = 'button';
286
+ exportButton.title = '导出当前内容';
287
+ exportButton.append(node('span', '', '导出'), iconNode('ui.chevron', 'nova-icon nova-icon-xs'));
288
+ const exportPopover = node('div', 'nova-studio-export-popover is-hidden');
289
+ const exportSvgButton = node('button', 'nova-studio-export-option');
290
+ exportSvgButton.type = 'button';
291
+ exportSvgButton.append(iconNode('ui.preview'), node('strong', '', '导出 SVG'), node('small', '', '预览确认后下载'));
292
+ exportSvgButton.addEventListener('click', () => {
293
+ exportPopover.classList.add('is-hidden');
294
+ exportButton.focus({ preventScroll: true });
295
+ this.actions.openSvgExportPreview();
296
+ });
297
+ const exportBpmnButton = node('button', 'nova-studio-export-option');
298
+ exportBpmnButton.type = 'button';
299
+ exportBpmnButton.append(iconNode('ui.design'), node('strong', '', '导出 BPMN'), node('small', '', '下载流程 XML'));
300
+ exportBpmnButton.addEventListener('click', () => {
301
+ exportPopover.classList.add('is-hidden');
302
+ this._exportBpmn();
303
+ });
304
+ exportPopover.append(exportSvgButton, exportBpmnButton);
305
+ exportButton.addEventListener('click', (event) => {
306
+ event.stopPropagation();
307
+ exportPopover.classList.toggle('is-hidden');
308
+ });
309
+ exportMenu.append(exportButton, exportPopover);
310
+ tools.appendChild(exportMenu);
208
311
  importInput.addEventListener('change', async () => {
209
312
  const file = importInput.files?.[0];
210
313
  if (!file) return;
211
314
  try {
212
- this.studio.importXml(await file.text(), this.studio.model.engine);
315
+ this.actions.importXml(await file.text(), this.studio.model.engine);
213
316
  this._setStatus('已导入 BPMN 文件', 'ok');
214
317
  requestAnimationFrame(() => this._fitActive());
215
318
  } catch (error) {
@@ -264,9 +367,13 @@ export class BpmnStudioShell {
264
367
  this.container.append(header, body);
265
368
  hydrateIcons(this.container, this.iconRegistry);
266
369
  Object.assign(this, {
370
+ _header: header,
371
+ _headerStart: brand,
267
372
  _body: body,
268
373
  _left: left,
269
374
  _right: right,
375
+ _canvasColumn: canvasColumn,
376
+ _footer: statusbar,
270
377
  _canvasHost: canvasHost,
271
378
  _floatingHead: floatingHead,
272
379
  _scopeBack: scopeBack,
@@ -275,7 +382,10 @@ export class BpmnStudioShell {
275
382
  _projectionSwitch: projectionSwitch,
276
383
  _projectionButtons: projectionButtons,
277
384
  _viewportTools: viewportTools,
278
- _designControls: [undo, redo, beautifySplit, validateButton, importButton, exportButton],
385
+ _designControls: [undo, redo, beautifySplit, validateButton, importButton],
386
+ _exportMenu: exportMenu,
387
+ _exportPopover: exportPopover,
388
+ _exportBpmnButton: exportBpmnButton,
279
389
  _defaultContext: context,
280
390
  _defaultMount: mount,
281
391
  _usesDefaultProperties: !this.slots.right,
@@ -294,8 +404,15 @@ export class BpmnStudioShell {
294
404
  else this.palette = mount.palette(left, { canvas: this.canvas });
295
405
  if (this.slots.right) this._recordCleanup(mountSlot(this.slots.right, right, { ...context, canvas: this.canvas }));
296
406
  else this.properties = mount.properties(right, { canvas: this.canvas });
297
- if (this.slots.header) { header.innerHTML = ''; this._recordCleanup(mountSlot(this.slots.header, header, { ...context, canvas: this.canvas })); }
407
+ if (this.slots.header) {
408
+ header.innerHTML = '';
409
+ this._recordCleanup(mountSlot(this.slots.header, header, { ...context, canvas: this.canvas }));
410
+ } else if (this.slots.headerStart) {
411
+ brand.innerHTML = '';
412
+ this._recordCleanup(mountSlot(this.slots.headerStart, brand, { ...context, canvas: this.canvas }));
413
+ }
298
414
  if (this.slots.footer) { statusbar.innerHTML = ''; this._recordCleanup(mountSlot(this.slots.footer, statusbar, { ...context, canvas: this.canvas })); }
415
+ hydrateIcons(this.container, this.iconRegistry);
299
416
  this._offState = this.studio.subscribe((event) => {
300
417
  if (event.type === 'historyChanged' || event.type === 'modelChanged') {
301
418
  undo.disabled = !this.studio.history.canUndo;
@@ -319,6 +436,7 @@ export class BpmnStudioShell {
319
436
  this._setStatus(`${initialGraph.nodes.length} 节点 · ${initialGraph.edges.length} 连线`, 'ok');
320
437
  const closeBeautify = (event) => {
321
438
  if (!beautifySplit.contains(event.target)) beautifyMenu.classList.add('is-hidden');
439
+ if (!exportMenu.contains(event.target)) exportPopover.classList.add('is-hidden');
322
440
  };
323
441
  document.addEventListener('click', closeBeautify);
324
442
  this._cleanups.push(() => document.removeEventListener('click', closeBeautify));
@@ -364,14 +482,13 @@ export class BpmnStudioShell {
364
482
 
365
483
  _runBeautify(action = null) {
366
484
  const settings = this.studio.model.settings || {};
367
- if (action === 'routing') this.studio.commands.rerouteEdges({ edgeStyle: 'rounded' });
368
- else if (action === 'smooth') this.studio.commands.rerouteEdges({ edgeStyle: 'smooth' });
369
- else this.studio.commands.beautify({
485
+ if (action === 'routing') this.actions.rerouteEdges({ edgeStyle: 'rounded' });
486
+ else if (action === 'smooth') this.actions.rerouteEdges({ edgeStyle: 'smooth' });
487
+ else this.actions.beautify({
370
488
  direction: action || settings.direction || 'horizontal',
371
489
  density: settings.layoutDensity || 'balanced',
372
490
  edgeStyle: settings.edgeStyle || 'rounded',
373
491
  });
374
- requestAnimationFrame(() => this._fitActive());
375
492
  }
376
493
 
377
494
  _syncDensityButtons() {
@@ -381,16 +498,18 @@ export class BpmnStudioShell {
381
498
  }
382
499
 
383
500
  _setMode(mode, { force = false } = {}) {
384
- if (!['design', 'viewer', 'instance'].includes(mode)) return;
501
+ if (!this.allowedModes.includes(mode)) return;
385
502
  if (!force && this.mode === mode) return;
386
503
  this.mode = mode;
387
504
  this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
388
505
  this._modeButtons?.forEach((button, value) => button.classList.toggle('is-active', value === mode));
389
506
  const readonly = mode !== 'design';
390
507
  this._body?.classList.toggle('is-readonly', readonly);
508
+ this._applyRegions({ fit: false });
391
509
  this._floatingHead?.classList.toggle('is-hidden', readonly);
392
510
  this._designControls?.forEach((control) => control.classList.toggle('is-hidden-by-mode', readonly));
393
511
  this._projectionSwitch?.classList.toggle('is-hidden', mode !== 'instance');
512
+ this._exportBpmnButton?.classList.toggle('is-hidden', mode !== 'design');
394
513
  this._syncProjectionButtons();
395
514
  this._body?.classList.remove('is-timeline');
396
515
 
@@ -418,6 +537,7 @@ export class BpmnStudioShell {
418
537
  runtimePresenter: this.rendererOptions.runtimePresenter,
419
538
  runtimeTraceProjector: this.rendererOptions.runtimeTraceProjector,
420
539
  runtimeAssetResolver: this.rendererOptions.runtimeAssetResolver,
540
+ svgExport: this.svgExport,
421
541
  runtimeTimelineRenderer: this.slots.runtimeTimeline
422
542
  ? (timeline) => mountSlot(this.slots.runtimeTimeline, timeline.container, { ...timeline, studio: this.studio, shell: this })
423
543
  : this.rendererOptions.runtimeTimelineRenderer,
@@ -526,6 +646,34 @@ export class BpmnStudioShell {
526
646
  this._syncProjectionButtons();
527
647
  if (this.mode === 'instance') this.viewer?.setProjection?.(projection);
528
648
  }
649
+ getRegions() { return Object.freeze({ ...this._regions }); }
650
+ setRegions(regions) {
651
+ if (this._usesCustomLayout) {
652
+ throw new Error('BpmnStudioShell setRegions() is unavailable when a custom layout is active.');
653
+ }
654
+ const next = normalizeRegions(regions, this._regions);
655
+ const changed = STUDIO_SHELL_REGIONS.some((name) => next[name] !== this._regions[name]);
656
+ this._regions = next;
657
+ if (changed) this._applyRegions();
658
+ return this.getRegions();
659
+ }
660
+ _applyRegions({ fit = true } = {}) {
661
+ if (this._usesCustomLayout || !this._body) return;
662
+ const readonly = this.mode !== 'design';
663
+ const headerHidden = this._regions.header === 'hidden';
664
+ const leftHidden = readonly || this._regions.left === 'hidden';
665
+ const rightHidden = this._regions.right === 'hidden';
666
+ const footerHidden = this._regions.footer === 'hidden';
667
+ this.container.classList.toggle('is-header-hidden', headerHidden);
668
+ this._body.classList.toggle('is-left-hidden', leftHidden);
669
+ this._body.classList.toggle('is-right-hidden', rightHidden);
670
+ this._canvasColumn?.classList.toggle('is-footer-hidden', footerHidden);
671
+ if (this._header) this._header.hidden = headerHidden;
672
+ if (this._left) this._left.hidden = leftHidden;
673
+ if (this._right) this._right.hidden = rightHidden;
674
+ if (this._footer) this._footer.hidden = footerHidden;
675
+ if (fit) requestAnimationFrame(() => this._fitActive());
676
+ }
529
677
  setTheme(theme) { return this.themeController.setTheme(theme); }
530
678
  setThemeMode(mode) { return this.themeController.setMode(mode); }
531
679
  getThemeState() { return this.themeController.getState(); }
@@ -544,6 +692,35 @@ export class BpmnStudioShell {
544
692
  }
545
693
  fitView() { this._fitActive(); }
546
694
  zoomBy(factor) { this._zoomActive(factor); }
695
+ exportSvg(options) {
696
+ const active = this.canvas || this.viewer;
697
+ if (!active?.exportSvg) return Promise.reject(new Error('当前视图不支持 SVG 导出。'));
698
+ const label = this.mode === 'design' ? '流程设计' : this.mode === 'viewer' ? '流程展示' : this.viewer?.activeProjection === 'compact' ? '移动时间线' : this.viewer?.activeProjection === 'approval' ? '实际路径' : '完整 BPMN';
699
+ const filename = options?.filename || this.svgExport?.filename || `${this.studio.model.name || this.studio.model.id}-${label}`;
700
+ return active.exportSvg({ label, ...(options || {}), filename });
701
+ }
702
+ openSvgExportPreview(options = {}) {
703
+ if (this._svgExportPreview && !this._svgExportPreview.closed) {
704
+ this._svgExportPreview.focus();
705
+ return this._svgExportPreview;
706
+ }
707
+ const active = this.canvas || this.viewer;
708
+ if (!active?.exportSvg) return null;
709
+ const label = this.mode === 'design' ? '流程设计' : this.mode === 'viewer' ? '流程展示' : this.viewer?.activeProjection === 'compact' ? '移动时间线' : this.viewer?.activeProjection === 'approval' ? '实际路径' : '完整 BPMN';
710
+ const filename = options.filename || this.svgExport?.filename || `${this.studio.model.name || this.studio.model.id}-${label}`;
711
+ this._svgExportPreview = openSvgExportPreview({
712
+ container: this.container,
713
+ title: options.previewTitle || `导出 ${label} SVG`,
714
+ initialTheme: options.theme || 'current',
715
+ createArtifact: (previewOptions) => this.exportSvg({ label, ...options, ...previewOptions, filename }),
716
+ onClose: () => { this._svgExportPreview = null; },
717
+ onDownload: (artifact) => {
718
+ this._setStatus(`SVG 已导出:${artifact.filename}`, 'ok');
719
+ options.onDownload?.(artifact);
720
+ },
721
+ });
722
+ return this._svgExportPreview;
723
+ }
547
724
 
548
725
  _setStatus(message, tone = 'ok') {
549
726
  if (!this._statusText || !this._statusDot) return;
@@ -552,7 +729,7 @@ export class BpmnStudioShell {
552
729
  }
553
730
 
554
731
  _showValidationStatus() {
555
- const issues = this.studio.validate();
732
+ const issues = this.actions.validate();
556
733
  if (!issues.length) this._setStatus('校验通过:流程结构正常', 'ok');
557
734
  else {
558
735
  const errors = issues.filter((issue) => issue.level === 'error').length;
@@ -562,7 +739,7 @@ export class BpmnStudioShell {
562
739
  }
563
740
 
564
741
  _exportBpmn() {
565
- const blob = new Blob([this.studio.exportXml()], { type: 'application/xml;charset=utf-8' });
742
+ const blob = new Blob([this.actions.exportXml()], { type: 'application/xml;charset=utf-8' });
566
743
  const url = URL.createObjectURL(blob);
567
744
  const anchor = node('a');
568
745
  anchor.href = url;
@@ -583,7 +760,12 @@ export class BpmnStudioShell {
583
760
  selectionToolbar: options.selectionToolbar ?? this.slots.selectionToolbar ?? null,
584
761
  contextMenu: options.contextMenu ?? this.slots.contextMenu ?? null,
585
762
  contextMenuRegistry: options.contextMenuRegistry ?? this.contextMenuRegistry,
586
- rendererOptions: { iconRegistry: this.iconRegistry, ...this.rendererOptions, ...(options.rendererOptions || {}) },
763
+ rendererOptions: {
764
+ iconRegistry: this.iconRegistry,
765
+ ...this.rendererOptions,
766
+ svgExport: this.svgExport,
767
+ ...(options.rendererOptions || {}),
768
+ },
587
769
  themeController: this.themeController,
588
770
  });
589
771
  this._instances.push(instance); return instance;
@@ -597,6 +779,8 @@ export class BpmnStudioShell {
597
779
  instance.render(); this._instances.push(instance); return instance;
598
780
  }
599
781
  destroy() {
782
+ this._svgExportPreview?.close?.({ immediate: true });
783
+ this._svgExportPreview = null;
600
784
  this._cleanups.splice(0).reverse().forEach((cleanup) => cleanup?.());
601
785
  this._instances.splice(0).reverse().forEach((instance) => instance.destroy?.());
602
786
  this.container.innerHTML = '';
package/dist/styles.css CHANGED
@@ -1,4 +1,4 @@
1
- /* @bpmn-nova/theme */
1
+ /* @bpmn-nova/internal/theme */
2
2
  [data-nova-theme] {
3
3
  accent-color: var(--nova-color-primary);
4
4
  }
@@ -149,7 +149,7 @@
149
149
  --nova-tone-manual-task-strong: #dc876f;
150
150
  }
151
151
 
152
- /* @bpmn-nova/icons */
152
+ /* @bpmn-nova/internal/icons */
153
153
  .nova-icon { width: 16px; height: 16px; display: inline-grid; place-items: center; flex: none; color: currentColor; line-height: 0; transform-origin: 50% 50%; }
154
154
  .nova-icon.nova-icon-xs { width: 12px; height: 12px; }
155
155
  .nova-icon.nova-icon-sm { width: 14px; height: 14px; }
@@ -157,7 +157,97 @@
157
157
  .nova-icon > .nova-icon-svg { width: 100%; height: 100%; display: block; overflow: visible; fill: none; stroke: currentColor; vector-effect: non-scaling-stroke; }
158
158
  .nova-icon[data-icon-missing="true"] { visibility: hidden; }
159
159
 
160
- /* @bpmn-nova/renderer-svg */
160
+ /* @bpmn-nova/internal/export-svg */
161
+ .nova-svg-export-preview {
162
+ position: absolute;
163
+ inset: 0;
164
+ z-index: 140;
165
+ display: grid;
166
+ place-items: center;
167
+ padding: 24px;
168
+ box-sizing: border-box;
169
+ background: var(--nova-color-backdrop, rgba(20, 26, 39, .38));
170
+ color: var(--nova-color-text, #1f2430);
171
+ font-family: var(--nova-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", sans-serif);
172
+ opacity: 1;
173
+ transition: opacity .18s ease;
174
+ }
175
+ .nova-svg-export-preview.is-entering,
176
+ .nova-svg-export-preview.is-closing { opacity: 0; }
177
+ .nova-svg-export-dialog {
178
+ width: min(1120px, 100%);
179
+ height: min(820px, 100%);
180
+ min-height: 420px;
181
+ display: grid;
182
+ grid-template-rows: auto auto minmax(0, 1fr) auto auto;
183
+ overflow: hidden;
184
+ border: 1px solid var(--nova-color-border, #e1e5ec);
185
+ border-radius: 16px;
186
+ background: var(--nova-color-surface, #fff);
187
+ box-shadow: var(--nova-shadow-lg, 0 18px 52px rgba(22, 29, 48, .2));
188
+ transform: translateY(0) scale(1);
189
+ transition: transform .2s cubic-bezier(.2, .8, .2, 1);
190
+ }
191
+ .nova-svg-export-preview.is-entering .nova-svg-export-dialog,
192
+ .nova-svg-export-preview.is-closing .nova-svg-export-dialog { transform: translateY(10px) scale(.985); }
193
+ .nova-svg-export-header,
194
+ .nova-svg-export-toolbar,
195
+ .nova-svg-export-footer { display: flex; align-items: center; gap: 12px; padding: 14px 18px; }
196
+ .nova-svg-export-header { justify-content: space-between; border-bottom: 1px solid var(--nova-color-divider, #e9ecf2); }
197
+ .nova-svg-export-heading { display: grid; gap: 3px; }
198
+ .nova-svg-export-heading strong { font-size: 17px; }
199
+ .nova-svg-export-heading span { color: var(--nova-color-text-muted); font-size: 12px; }
200
+ .nova-svg-export-close { width: 34px; height: 34px; border: 0; border-radius: 9px; background: transparent; color: var(--nova-color-text-muted); font-size: 24px; line-height: 1; cursor: pointer; }
201
+ .nova-svg-export-close:hover { background: var(--nova-color-surface-muted); color: var(--nova-color-text); }
202
+ .nova-svg-export-toolbar { justify-content: space-between; flex-wrap: wrap; background: var(--nova-color-surface-subtle); border-bottom: 1px solid var(--nova-color-divider); }
203
+ .nova-svg-export-theme,
204
+ .nova-svg-export-viewport-tools { display: inline-flex; align-items: center; gap: 3px; padding: 3px; border-radius: 9px; background: var(--nova-color-surface); border: 1px solid var(--nova-color-border); }
205
+ .nova-svg-export-theme button,
206
+ .nova-svg-export-viewport-tools button { min-height: 28px; padding: 0 10px; border: 0; border-radius: 6px; background: transparent; color: var(--nova-color-text-secondary); cursor: pointer; }
207
+ .nova-svg-export-theme button.is-active { background: var(--nova-color-primary-soft); color: var(--nova-color-primary); font-weight: 650; }
208
+ .nova-svg-export-theme button:focus-visible,
209
+ .nova-svg-export-viewport-tools button:focus-visible,
210
+ .nova-svg-export-actions button:focus-visible,
211
+ .nova-svg-export-close:focus-visible,
212
+ .nova-svg-export-viewport:focus-visible { outline: 2px solid var(--nova-color-focus-ring); outline-offset: 2px; }
213
+ .nova-svg-export-transparent { display: inline-flex; align-items: center; gap: 7px; color: var(--nova-color-text-secondary); font-size: 12px; cursor: pointer; }
214
+ .nova-svg-export-viewport-tools span { min-width: 46px; text-align: center; color: var(--nova-color-text-muted); font-size: 11px; }
215
+ .nova-svg-export-body { position: relative; min-height: 0; overflow: hidden; background: var(--nova-color-canvas); }
216
+ .nova-svg-export-viewport { position: absolute; inset: 0; overflow: hidden; cursor: grab; touch-action: none; background-color: var(--nova-color-canvas); background-image: linear-gradient(45deg, color-mix(in srgb, var(--nova-color-border) 35%, transparent) 25%, transparent 25%), linear-gradient(-45deg, color-mix(in srgb, var(--nova-color-border) 35%, transparent) 25%, transparent 25%), linear-gradient(45deg, transparent 75%, color-mix(in srgb, var(--nova-color-border) 35%, transparent) 75%), linear-gradient(-45deg, transparent 75%, color-mix(in srgb, var(--nova-color-border) 35%, transparent) 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0; }
217
+ .nova-svg-export-viewport.is-dragging { cursor: grabbing; }
218
+ .nova-svg-export-stage { position: absolute; left: 0; top: 0; width: 1px; height: 1px; transform-origin: 0 0; will-change: transform; }
219
+ .nova-svg-export-image { display: block; max-width: none; user-select: none; pointer-events: none; box-shadow: 0 6px 24px rgba(0, 0, 0, .14); }
220
+ .nova-svg-export-state { position: absolute; inset: 0; display: grid; place-content: center; justify-items: center; gap: 12px; background: color-mix(in srgb, var(--nova-color-surface) 88%, transparent); color: var(--nova-color-text-secondary); }
221
+ .nova-svg-export-state[hidden] { display: none; }
222
+ .nova-svg-export-state.is-error { color: var(--nova-tone-danger-foreground); }
223
+ .nova-svg-export-spinner { width: 24px; height: 24px; border: 2px solid var(--nova-color-border); border-top-color: var(--nova-color-primary); border-radius: 50%; animation: nova-svg-export-spin .8s linear infinite; }
224
+ .nova-svg-export-state.is-error .nova-svg-export-spinner { display: none; }
225
+ @keyframes nova-svg-export-spin { to { transform: rotate(360deg); } }
226
+ .nova-svg-export-warnings { max-height: 110px; overflow: auto; padding: 10px 18px; border-top: 1px solid var(--nova-tone-warning-border); background: var(--nova-tone-warning-background); color: var(--nova-tone-warning-foreground); font-size: 11px; }
227
+ .nova-svg-export-warnings[hidden] { display: none; }
228
+ .nova-svg-export-warnings strong { display: block; margin-bottom: 4px; }
229
+ .nova-svg-export-warnings ul { margin: 0; padding-left: 18px; }
230
+ .nova-svg-export-footer { justify-content: space-between; border-top: 1px solid var(--nova-color-divider); }
231
+ .nova-svg-export-summary { min-width: 0; overflow: hidden; color: var(--nova-color-text-muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
232
+ .nova-svg-export-actions { display: flex; gap: 8px; }
233
+ .nova-svg-export-actions button { min-height: 34px; padding: 0 14px; border: 1px solid var(--nova-color-border); border-radius: 9px; background: var(--nova-color-surface); color: var(--nova-color-text-secondary); cursor: pointer; }
234
+ .nova-svg-export-actions button.is-primary { border-color: var(--nova-color-primary); background: var(--nova-color-primary); color: var(--nova-color-text-inverse, #fff); }
235
+ .nova-svg-export-actions button:disabled { opacity: .48; cursor: not-allowed; }
236
+ @media (max-width: 720px) {
237
+ .nova-svg-export-preview { padding: 10px; }
238
+ .nova-svg-export-dialog { width: 100%; height: 100%; min-height: 0; border-radius: 12px; }
239
+ .nova-svg-export-toolbar { align-items: flex-start; }
240
+ .nova-svg-export-viewport-tools { order: 3; width: 100%; justify-content: center; }
241
+ .nova-svg-export-summary { display: none; }
242
+ .nova-svg-export-footer { justify-content: flex-end; }
243
+ }
244
+ @media (prefers-reduced-motion: reduce) {
245
+ .nova-svg-export-preview,
246
+ .nova-svg-export-dialog { transition: none; }
247
+ .nova-svg-export-spinner { animation: none; }
248
+ }
249
+
250
+ /* @bpmn-nova/internal/renderer-svg */
161
251
  .mb-diagram-host,
162
252
  .mb-runtime-timeline-host {
163
253
  --mb-primary: var(--nova-color-primary, #635bff);
@@ -898,7 +988,7 @@ button.mb-runtime-action-summary:hover, button.mb-runtime-action-summary:focus-v
898
988
  height: 100%;
899
989
  }
900
990
 
901
- /* @bpmn-nova/palette */
991
+ /* @bpmn-nova/internal/palette */
902
992
  .nova-palette { height: 100%; overflow: auto; padding: 10px; color: var(--nova-color-text, #243047); background: var(--nova-color-surface, #fff); }
903
993
  .nova-palette-section { border-bottom: 1px solid var(--nova-color-divider, #edf0f5); padding: 3px 0 8px; }
904
994
  .nova-palette-section-toggle { width: 100%; min-height: 34px; border: 0; background: transparent; display: grid; grid-template-columns: 16px 1fr auto; align-items: center; gap: 6px; color: inherit; cursor: pointer; text-align: left; }
@@ -917,7 +1007,7 @@ button.mb-runtime-action-summary:hover, button.mb-runtime-action-summary:focus-v
917
1007
  .nova-palette-item-copy strong { font-size: var(--nova-font-size-sm, 12px); line-height: 18px; font-weight: var(--nova-font-weight-semibold, 600); }
918
1008
  .nova-palette-item-copy small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--nova-color-text-muted, #919aad); font-size: var(--nova-font-size-xs, 10px); line-height: 14px; }
919
1009
 
920
- /* @bpmn-nova/properties-renderer */
1010
+ /* @bpmn-nova/internal/properties-renderer */
921
1011
  .nova-properties { min-height: 100%; color: var(--nova-color-text); background: var(--nova-color-surface); color-scheme: inherit; }
922
1012
  .properties-head { position: sticky; top: 0; z-index: 5; padding: 15px 16px 12px; border-bottom: 1px solid var(--nova-color-divider, #e7eaf1); background: color-mix(in srgb, var(--nova-color-surface) 97%, transparent); backdrop-filter: blur(10px); }
923
1013
  .properties-title-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
@@ -1049,8 +1139,10 @@ textarea.property-control { resize: vertical; }
1049
1139
  .property-branch-target { min-width: 0; flex: 1; display: grid; grid-template-columns: auto minmax(0,1fr) auto; align-items: center; gap: 5px; }
1050
1140
  .property-branch-target small { color: var(--nova-color-text-muted); font-size: var(--nova-font-size-xs, 10px); line-height: 14px; }.property-branch-target strong { overflow: hidden; color: var(--nova-color-text-secondary); font-size: var(--nova-font-size-sm, 12px); line-height: 18px; font-weight: var(--nova-font-weight-semibold, 600); text-overflow: ellipsis; white-space: nowrap; }.property-branch-target > span { color: var(--nova-color-primary); font-size: var(--nova-font-size-sm, 12px); }
1051
1141
 
1052
- /* @bpmn-nova/studio */
1053
- .nova-studio-shell { --nova-left-width: 244px; --nova-right-width: 360px; --nova-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; --nova-font-size-xs: 10px; --nova-font-size-sm: 12px; --nova-font-size-md: 14px; --nova-font-size-lg: 16px; --nova-font-weight-regular: 400; --nova-font-weight-medium: 500; --nova-font-weight-semibold: 600; --nova-font-weight-bold: 700; width: 100%; height: 100%; min-height: 520px; overflow: hidden; display: grid; grid-template-rows: 58px minmax(0, 1fr); color: var(--nova-color-text, #263148); background: var(--nova-color-canvas, #f6f7fb); font-family: var(--nova-font-family); }
1142
+ /* @bpmn-nova/internal/studio */
1143
+ .nova-studio-shell { --nova-left-width: 244px; --nova-right-width: 360px; --nova-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; --nova-font-size-xs: 10px; --nova-font-size-sm: 12px; --nova-font-size-md: 14px; --nova-font-size-lg: 16px; --nova-font-weight-regular: 400; --nova-font-weight-medium: 500; --nova-font-weight-semibold: 600; --nova-font-weight-bold: 700; position: relative; isolation: isolate; width: 100%; height: 100%; min-height: 520px; overflow: hidden; display: grid; grid-template-rows: 58px minmax(0, 1fr); color: var(--nova-color-text, #263148); background: var(--nova-color-canvas, #f6f7fb); font-family: var(--nova-font-family); }
1144
+ .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1145
+ .nova-studio-header[hidden], .nova-studio-left[hidden], .nova-studio-right[hidden], .nova-studio-statusbar[hidden] { display: none !important; }
1054
1146
  .nova-studio-header { min-width: 0; border-bottom: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); display: grid; grid-template-columns: minmax(180px, auto) auto minmax(0, 1fr); align-items: center; gap: 18px; padding: 0 12px 0 14px; }
1055
1147
  .nova-studio-brand { min-width: 0; display: flex; align-items: center; gap: 10px; }
1056
1148
  .nova-studio-brand-mark { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--nova-color-text-inverse, #fff); background: linear-gradient(145deg, var(--nova-color-primary, #5362da), var(--nova-color-primary-hover)); font-size: var(--nova-font-size-md); font-weight: var(--nova-font-weight-bold); box-shadow: var(--nova-shadow-sm); }
@@ -1076,6 +1168,17 @@ textarea.property-control { resize: vertical; }
1076
1168
  .nova-studio-tool.is-primary { border-color: var(--nova-color-primary); color: var(--nova-color-text-inverse, #fff); background: var(--nova-color-primary, #5361d6); }
1077
1169
  .nova-studio-tool.is-primary:hover { border-color: var(--nova-color-primary-hover); color: var(--nova-color-text-inverse, #fff); background: var(--nova-color-primary-hover); }
1078
1170
  .nova-studio-tool:disabled { opacity: .4; cursor: default; }
1171
+ .nova-studio-export-menu { position: relative; flex: none; }
1172
+ .nova-studio-export-menu > .nova-studio-tool { display: inline-flex; align-items: center; gap: 7px; }
1173
+ .nova-studio-export-popover { position: absolute; z-index: 90; top: calc(100% + 8px); right: 0; width: 220px; padding: 6px; border: 1px solid var(--nova-color-border); border-radius: 11px; background: var(--nova-color-surface-raised); box-shadow: var(--nova-shadow-md); }
1174
+ .nova-studio-export-popover.is-hidden { display: none; }
1175
+ .nova-studio-export-option { width: 100%; display: grid; grid-template-columns: 28px minmax(0, 1fr); grid-template-rows: auto auto; column-gap: 9px; padding: 9px; border: 0; border-radius: 8px; background: transparent; color: var(--nova-color-text); text-align: left; cursor: pointer; }
1176
+ .nova-studio-export-option:hover { background: var(--nova-color-surface-muted); }
1177
+ .nova-studio-export-option:focus-visible { outline: 2px solid var(--nova-color-focus-ring); outline-offset: -2px; }
1178
+ .nova-studio-export-option > .nova-icon { grid-row: 1 / span 2; align-self: center; color: var(--nova-color-primary); }
1179
+ .nova-studio-export-option strong { font-size: var(--nova-font-size-sm, 12px); }
1180
+ .nova-studio-export-option small { color: var(--nova-color-text-muted); font-size: var(--nova-font-size-xs, 10px); }
1181
+ .nova-studio-export-option.is-hidden { display: none; }
1079
1182
  .nova-studio-import-input { display: none; }
1080
1183
  .nova-studio-beautify-split { position: relative; flex: none; display: flex; }
1081
1184
  .nova-studio-beautify-split .nova-studio-beautify { border-radius: 7px 0 0 7px; }
@@ -1093,7 +1196,11 @@ textarea.property-control { resize: vertical; }
1093
1196
  .nova-studio-beautify-action-icon > .nova-icon-svg { width: 14px; height: 14px; }
1094
1197
  .nova-studio-beautify-action strong { font-size: var(--nova-font-size-sm); font-weight: var(--nova-font-weight-semibold); line-height: 18px; }.nova-studio-beautify-action small { color: #778195; color: var(--nova-color-text-muted, #778195); font-size: var(--nova-font-size-xs); line-height: 14px; }
1095
1198
  .nova-studio-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--nova-left-width) minmax(320px, 1fr) var(--nova-right-width); }
1199
+ .nova-studio-body.is-left-hidden { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
1200
+ .nova-studio-body.is-right-hidden { grid-template-columns: var(--nova-left-width) minmax(320px, 1fr); }
1201
+ .nova-studio-body.is-left-hidden.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
1096
1202
  .nova-studio-body.is-readonly { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
1203
+ .nova-studio-body.is-readonly.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
1097
1204
  .nova-studio-body.is-readonly .nova-studio-left { display: none; }
1098
1205
  .nova-studio-left, .nova-studio-right, .nova-studio-canvas-column, .nova-studio-canvas { min-width: 0; min-height: 0; overflow: hidden; }
1099
1206
  .nova-studio-left { border-right: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
@@ -1105,6 +1212,7 @@ textarea.property-control { resize: vertical; }
1105
1212
  .nova-studio-left::-webkit-scrollbar-thumb:hover, .nova-studio-right::-webkit-scrollbar-thumb:hover { background: #c1c9d5; background: var(--nova-color-text-muted); }
1106
1213
  .nova-studio-left::-webkit-scrollbar-button, .nova-studio-right::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
1107
1214
  .nova-studio-canvas-column { display: grid; grid-template-rows: minmax(0, 1fr) 35px; background: var(--nova-color-canvas, #f7f8fc); }
1215
+ .nova-studio-canvas-column.is-footer-hidden { grid-template-rows: minmax(0, 1fr); }
1108
1216
  .nova-studio-canvas-stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
1109
1217
  .nova-studio-canvas { position: relative; }
1110
1218
  .nova-studio-canvas-stage > .nova-studio-canvas { width: 100%; height: 100%; }
@@ -1321,6 +1429,7 @@ textarea.property-control { resize: vertical; }
1321
1429
  }
1322
1430
  @media (max-width: 720px) {
1323
1431
  .nova-studio-shell { grid-template-rows: 54px minmax(0,1fr); }
1432
+ .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1324
1433
  .nova-studio-header { grid-template-columns: auto minmax(0,1fr); padding-left: 9px; }
1325
1434
  .nova-studio-brand-copy { display: none; }
1326
1435
  .nova-studio-body { grid-template-columns: 1fr; }