@bpmn-nova/studio 0.3.1-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.
package/dist/shell.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { createDefaultIconRegistry, hydrateIcons } from './modules/icons/index.js';
2
2
  import { createDefaultPaletteRegistry, PalettePanel } from './modules/palette/index.js';
3
3
  import { createDefaultPropertiesRegistry, PropertiesPanel } from './modules/properties/index.js';
4
- import { demoRuntime } from './modules/runtime/index.js';
5
4
  import { BpmnViewer } from './modules/viewer/index.js';
6
5
  import { ThemeController } from './modules/theme/index.js';
7
6
  import { openSvgExportPreview } from './modules/export-svg/index.js';
@@ -29,6 +28,39 @@ function mountSlot(slot, container, context) {
29
28
  return null;
30
29
  }
31
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
+
32
64
  export class BpmnStudioShell {
33
65
  constructor({
34
66
  container,
@@ -41,8 +73,10 @@ export class BpmnStudioShell {
41
73
  rendererOptions = {},
42
74
  slots = {},
43
75
  layout = null,
44
- runtime = demoRuntime(),
76
+ regions = null,
77
+ runtime = null,
45
78
  mode = 'design',
79
+ allowedModes = null,
46
80
  projection = undefined,
47
81
  responsive = false,
48
82
  projectionOptions = null,
@@ -54,8 +88,13 @@ export class BpmnStudioShell {
54
88
  onThemeChange = null,
55
89
  } = {}) {
56
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];
57
96
  const instanceProjection = projection || (responsive ? 'auto' : 'approval');
58
- const activeProjection = mode === 'instance' ? instanceProjection : 'standard';
97
+ const activeProjection = resolvedMode === 'instance' ? instanceProjection : 'standard';
59
98
  Object.assign(this, {
60
99
  container,
61
100
  studio,
@@ -66,24 +105,57 @@ export class BpmnStudioShell {
66
105
  rendererOptions,
67
106
  slots,
68
107
  runtime,
69
- mode,
108
+ mode: resolvedMode,
109
+ allowedModes: resolvedAllowedModes,
70
110
  projection: activeProjection,
71
111
  _instanceProjection: instanceProjection,
72
112
  responsive,
73
113
  projectionOptions,
74
114
  runtimeAppearance,
75
115
  svgExport: svgExport || rendererOptions.svgExport || null,
116
+ _usesCustomLayout: typeof layout === 'function',
117
+ _regions: normalizeRegions(regions),
76
118
  });
77
119
  this.propertiesRegistry = propertiesRegistry || createDefaultPropertiesRegistry({ studio });
78
120
  this.interactions = createInteractionController({ studio, templates: templateRegistry });
79
121
  this._cleanups = [];
80
122
  this._instances = [];
81
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
+ });
82
142
  this.container.classList.add('nova-studio-shell');
83
143
  this.themeController = new ThemeController({ root: this.container, theme, onChange: onThemeChange });
84
144
  this.container.style.setProperty('--nova-left-width', `${leftWidth}px`);
85
145
  this.container.style.setProperty('--nova-right-width', `${rightWidth}px`);
86
- 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
+ };
87
159
  const mount = {
88
160
  canvas: (host, options = {}) => this._mountCanvas(host, options),
89
161
  palette: (host, options = {}) => this._mountPalette(host, options),
@@ -109,7 +181,7 @@ export class BpmnStudioShell {
109
181
  { value: 'design', label: '流程设计', iconId: 'ui.design' },
110
182
  { value: 'viewer', label: '流程展示', iconId: 'ui.preview' },
111
183
  { value: 'instance', label: '审批轨迹', iconId: 'ui.trace' },
112
- ]) {
184
+ ].filter((item) => this.allowedModes.includes(item.value))) {
113
185
  const button = node('button');
114
186
  button.type = 'button';
115
187
  button.setAttribute('data-mode', item.value);
@@ -144,8 +216,8 @@ export class BpmnStudioShell {
144
216
  };
145
217
 
146
218
  const divider = () => tools.appendChild(node('span', 'nova-studio-tool-divider'));
147
- const undo = tool('', '撤销 Ctrl/⌘+Z', () => this.studio.undo(), 'is-icon', 'ui.undo');
148
- 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');
149
221
  divider();
150
222
  const beautifySplit = node('div', 'nova-studio-beautify-split');
151
223
  const beautifyButton = node('button', 'nova-studio-tool nova-studio-beautify is-magic');
@@ -200,7 +272,7 @@ export class BpmnStudioShell {
200
272
  });
201
273
  beautifySplit.append(beautifyButton, beautifyCaret, beautifyMenu);
202
274
  tools.appendChild(beautifySplit);
203
- const fit = tool('最佳视图', '适应画布内容', () => this._fitActive(), '', 'ui.fitView');
275
+ const fit = tool('最佳视图', '适应画布内容', () => this.actions.fitView(), '', 'ui.fitView');
204
276
 
205
277
  const importInput = node('input', 'nova-studio-import-input');
206
278
  importInput.type = 'file';
@@ -220,7 +292,7 @@ export class BpmnStudioShell {
220
292
  exportSvgButton.addEventListener('click', () => {
221
293
  exportPopover.classList.add('is-hidden');
222
294
  exportButton.focus({ preventScroll: true });
223
- this.openSvgExportPreview();
295
+ this.actions.openSvgExportPreview();
224
296
  });
225
297
  const exportBpmnButton = node('button', 'nova-studio-export-option');
226
298
  exportBpmnButton.type = 'button';
@@ -240,7 +312,7 @@ export class BpmnStudioShell {
240
312
  const file = importInput.files?.[0];
241
313
  if (!file) return;
242
314
  try {
243
- this.studio.importXml(await file.text(), this.studio.model.engine);
315
+ this.actions.importXml(await file.text(), this.studio.model.engine);
244
316
  this._setStatus('已导入 BPMN 文件', 'ok');
245
317
  requestAnimationFrame(() => this._fitActive());
246
318
  } catch (error) {
@@ -295,9 +367,13 @@ export class BpmnStudioShell {
295
367
  this.container.append(header, body);
296
368
  hydrateIcons(this.container, this.iconRegistry);
297
369
  Object.assign(this, {
370
+ _header: header,
371
+ _headerStart: brand,
298
372
  _body: body,
299
373
  _left: left,
300
374
  _right: right,
375
+ _canvasColumn: canvasColumn,
376
+ _footer: statusbar,
301
377
  _canvasHost: canvasHost,
302
378
  _floatingHead: floatingHead,
303
379
  _scopeBack: scopeBack,
@@ -328,8 +404,15 @@ export class BpmnStudioShell {
328
404
  else this.palette = mount.palette(left, { canvas: this.canvas });
329
405
  if (this.slots.right) this._recordCleanup(mountSlot(this.slots.right, right, { ...context, canvas: this.canvas }));
330
406
  else this.properties = mount.properties(right, { canvas: this.canvas });
331
- 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
+ }
332
414
  if (this.slots.footer) { statusbar.innerHTML = ''; this._recordCleanup(mountSlot(this.slots.footer, statusbar, { ...context, canvas: this.canvas })); }
415
+ hydrateIcons(this.container, this.iconRegistry);
333
416
  this._offState = this.studio.subscribe((event) => {
334
417
  if (event.type === 'historyChanged' || event.type === 'modelChanged') {
335
418
  undo.disabled = !this.studio.history.canUndo;
@@ -399,14 +482,13 @@ export class BpmnStudioShell {
399
482
 
400
483
  _runBeautify(action = null) {
401
484
  const settings = this.studio.model.settings || {};
402
- if (action === 'routing') this.studio.commands.rerouteEdges({ edgeStyle: 'rounded' });
403
- else if (action === 'smooth') this.studio.commands.rerouteEdges({ edgeStyle: 'smooth' });
404
- 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({
405
488
  direction: action || settings.direction || 'horizontal',
406
489
  density: settings.layoutDensity || 'balanced',
407
490
  edgeStyle: settings.edgeStyle || 'rounded',
408
491
  });
409
- requestAnimationFrame(() => this._fitActive());
410
492
  }
411
493
 
412
494
  _syncDensityButtons() {
@@ -416,13 +498,14 @@ export class BpmnStudioShell {
416
498
  }
417
499
 
418
500
  _setMode(mode, { force = false } = {}) {
419
- if (!['design', 'viewer', 'instance'].includes(mode)) return;
501
+ if (!this.allowedModes.includes(mode)) return;
420
502
  if (!force && this.mode === mode) return;
421
503
  this.mode = mode;
422
504
  this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
423
505
  this._modeButtons?.forEach((button, value) => button.classList.toggle('is-active', value === mode));
424
506
  const readonly = mode !== 'design';
425
507
  this._body?.classList.toggle('is-readonly', readonly);
508
+ this._applyRegions({ fit: false });
426
509
  this._floatingHead?.classList.toggle('is-hidden', readonly);
427
510
  this._designControls?.forEach((control) => control.classList.toggle('is-hidden-by-mode', readonly));
428
511
  this._projectionSwitch?.classList.toggle('is-hidden', mode !== 'instance');
@@ -563,6 +646,34 @@ export class BpmnStudioShell {
563
646
  this._syncProjectionButtons();
564
647
  if (this.mode === 'instance') this.viewer?.setProjection?.(projection);
565
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
+ }
566
677
  setTheme(theme) { return this.themeController.setTheme(theme); }
567
678
  setThemeMode(mode) { return this.themeController.setMode(mode); }
568
679
  getThemeState() { return this.themeController.getState(); }
@@ -618,7 +729,7 @@ export class BpmnStudioShell {
618
729
  }
619
730
 
620
731
  _showValidationStatus() {
621
- const issues = this.studio.validate();
732
+ const issues = this.actions.validate();
622
733
  if (!issues.length) this._setStatus('校验通过:流程结构正常', 'ok');
623
734
  else {
624
735
  const errors = issues.filter((issue) => issue.level === 'error').length;
@@ -628,7 +739,7 @@ export class BpmnStudioShell {
628
739
  }
629
740
 
630
741
  _exportBpmn() {
631
- 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' });
632
743
  const url = URL.createObjectURL(blob);
633
744
  const anchor = node('a');
634
745
  anchor.href = url;
package/dist/styles.css CHANGED
@@ -1141,6 +1141,8 @@ textarea.property-control { resize: vertical; }
1141
1141
 
1142
1142
  /* @bpmn-nova/internal/studio */
1143
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; }
1144
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; }
1145
1147
  .nova-studio-brand { min-width: 0; display: flex; align-items: center; gap: 10px; }
1146
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); }
@@ -1194,7 +1196,11 @@ textarea.property-control { resize: vertical; }
1194
1196
  .nova-studio-beautify-action-icon > .nova-icon-svg { width: 14px; height: 14px; }
1195
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; }
1196
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); }
1197
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); }
1198
1204
  .nova-studio-body.is-readonly .nova-studio-left { display: none; }
1199
1205
  .nova-studio-left, .nova-studio-right, .nova-studio-canvas-column, .nova-studio-canvas { min-width: 0; min-height: 0; overflow: hidden; }
1200
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; }
@@ -1206,6 +1212,7 @@ textarea.property-control { resize: vertical; }
1206
1212
  .nova-studio-left::-webkit-scrollbar-thumb:hover, .nova-studio-right::-webkit-scrollbar-thumb:hover { background: #c1c9d5; background: var(--nova-color-text-muted); }
1207
1213
  .nova-studio-left::-webkit-scrollbar-button, .nova-studio-right::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
1208
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); }
1209
1216
  .nova-studio-canvas-stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
1210
1217
  .nova-studio-canvas { position: relative; }
1211
1218
  .nova-studio-canvas-stage > .nova-studio-canvas { width: 100%; height: 100%; }
@@ -1422,6 +1429,7 @@ textarea.property-control { resize: vertical; }
1422
1429
  }
1423
1430
  @media (max-width: 720px) {
1424
1431
  .nova-studio-shell { grid-template-rows: 54px minmax(0,1fr); }
1432
+ .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1425
1433
  .nova-studio-header { grid-template-columns: auto minmax(0,1fr); padding-left: 9px; }
1426
1434
  .nova-studio-brand-copy { display: none; }
1427
1435
  .nova-studio-body { grid-template-columns: 1fr; }