@bpmn-nova/studio 0.3.4-preview → 0.3.6-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
@@ -5,6 +5,15 @@ import { BpmnViewer } from './modules/viewer/index.js';
5
5
  import { ThemeController } from './modules/theme/index.js';
6
6
  import { openSvgExportPreview } from './modules/export-svg/index.js';
7
7
  import { BpmnCanvas } from './canvas.js';
8
+ import { StudioSidebars } from './sidebars.js';
9
+ import { createPanelSelection, samePanelSelection } from './panel-selection.js';
10
+ import {
11
+ applyStudioControlMetrics,
12
+ createStudioUiContext,
13
+ normalizeStudioConfig,
14
+ normalizeStudioRegions,
15
+ resolveStudioControlMetrics,
16
+ } from './config.js';
8
17
  import { createDefaultContextMenuRegistry } from './context-menu.js';
9
18
  import { createInteractionController, createTemplateRegistry } from './interactions.js';
10
19
 
@@ -24,10 +33,25 @@ function iconNode(id, className = 'nova-icon nova-icon-sm') {
24
33
  function mountSlot(slot, container, context) {
25
34
  if (!slot) return null;
26
35
  if (slot instanceof HTMLElement) { container.appendChild(slot); return null; }
27
- if (typeof slot === 'function') return slot({ container, ...context });
36
+ if (typeof slot === 'function') return slot(Object.defineProperties({ container }, Object.getOwnPropertyDescriptors(context)));
28
37
  return null;
29
38
  }
30
39
 
40
+ function sameConfigValue(left, right) {
41
+ if (Object.is(left, right)) return true;
42
+ if (!left || !right || typeof left !== 'object' || typeof right !== 'object') return false;
43
+ if (Array.isArray(left) || Array.isArray(right)) {
44
+ return Array.isArray(left)
45
+ && Array.isArray(right)
46
+ && left.length === right.length
47
+ && left.every((value, index) => sameConfigValue(value, right[index]));
48
+ }
49
+ const leftKeys = Object.keys(left);
50
+ const rightKeys = Object.keys(right);
51
+ return leftKeys.length === rightKeys.length
52
+ && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(right, key) && sameConfigValue(left[key], right[key]));
53
+ }
54
+
31
55
  const STUDIO_MODES = ['design', 'viewer', 'instance'];
32
56
  const STUDIO_MODE_OPTIONS = Object.freeze([
33
57
  Object.freeze({ value: 'design', label: '流程设计', iconId: 'ui.design' }),
@@ -35,12 +59,6 @@ const STUDIO_MODE_OPTIONS = Object.freeze([
35
59
  Object.freeze({ value: 'instance', label: '审批轨迹', iconId: 'ui.trace' }),
36
60
  ]);
37
61
  const STUDIO_SHELL_REGIONS = ['header', 'left', 'right', 'footer'];
38
- const DEFAULT_STUDIO_SHELL_REGIONS = Object.freeze({
39
- header: 'default',
40
- left: 'default',
41
- right: 'default',
42
- footer: 'default',
43
- });
44
62
 
45
63
  function normalizeAllowedModes(allowedModes, { allowDefault = true } = {}) {
46
64
  if (allowDefault && (allowedModes === undefined || allowedModes === null)) return [...STUDIO_MODES];
@@ -51,54 +69,62 @@ function normalizeAllowedModes(allowedModes, { allowDefault = true } = {}) {
51
69
  return [...allowedModes];
52
70
  }
53
71
 
54
- function normalizeRegions(regions, current = DEFAULT_STUDIO_SHELL_REGIONS) {
55
- if (regions === undefined || regions === null) return { ...current };
56
- if (!regions || typeof regions !== 'object' || Array.isArray(regions)) {
57
- throw new Error('regions must be an object containing header, left, right, or footer modes.');
58
- }
59
- for (const name of Object.keys(regions)) {
60
- if (!STUDIO_SHELL_REGIONS.includes(name)) throw new Error(`Unknown Studio Shell region: ${name}.`);
61
- if (!['default', 'hidden'].includes(regions[name])) {
62
- throw new Error(`Studio Shell region "${name}" must be "default" or "hidden".`);
63
- }
64
- }
65
- return { ...current, ...regions };
66
- }
67
-
68
72
  export class BpmnStudioShell {
69
- constructor({
70
- container,
71
- studio,
72
- iconRegistry = createDefaultIconRegistry(),
73
- paletteRegistry = createDefaultPaletteRegistry(),
74
- propertiesRegistry = null,
75
- templateRegistry = createTemplateRegistry(),
76
- contextMenuRegistry = createDefaultContextMenuRegistry(),
77
- rendererOptions = {},
78
- nodeSubtitleResolver = rendererOptions.nodeSubtitleResolver,
79
- slots = {},
80
- layout = null,
81
- regions = null,
82
- runtime = null,
83
- mode = 'design',
84
- allowedModes = null,
85
- projection = undefined,
86
- responsive = false,
87
- projectionOptions = null,
88
- leftWidth = 244,
89
- rightWidth = 360,
90
- theme = null,
91
- runtimeAppearance = null,
92
- svgExport = null,
93
- onThemeChange = null,
94
- } = {}) {
73
+ constructor(options = {}) {
74
+ const {
75
+ container,
76
+ studio,
77
+ config,
78
+ iconRegistry = createDefaultIconRegistry(),
79
+ paletteRegistry = createDefaultPaletteRegistry(),
80
+ propertiesRegistry = null,
81
+ templateRegistry = createTemplateRegistry(),
82
+ contextMenuRegistry = createDefaultContextMenuRegistry(),
83
+ rendererOptions = {},
84
+ nodeSubtitleResolver = rendererOptions.nodeSubtitleResolver,
85
+ slots = {},
86
+ layout = null,
87
+ regions = null,
88
+ runtime = null,
89
+ mode = 'design',
90
+ allowedModes = null,
91
+ projection = undefined,
92
+ responsive = false,
93
+ projectionOptions = null,
94
+ leftWidth = 244,
95
+ rightWidth = 360,
96
+ timeline,
97
+ runtimeDetails,
98
+ runtimeTraceOptions,
99
+ theme = null,
100
+ runtimeAppearance = null,
101
+ svgExport = null,
102
+ onThemeChange = null,
103
+ } = options;
95
104
  if (!container || !studio) throw new Error('BpmnStudioShell requires container and studio.');
96
- if (typeof layout === 'function' && regions !== null && regions !== undefined) {
105
+ const configuredRegions = regions !== null && regions !== undefined
106
+ || config?.ui?.regions !== null && config?.ui?.regions !== undefined;
107
+ if (typeof layout === 'function' && configuredRegions) {
97
108
  throw new Error('BpmnStudioShell options "layout" and "regions" cannot be used together.');
98
109
  }
110
+ const fallbackConfig = {
111
+ viewer: {
112
+ timeline: rendererOptions.timeline,
113
+ runtimeDetails: rendererOptions.runtimeDetails,
114
+ runtimeTraceOptions: rendererOptions.runtimeTraceOptions,
115
+ },
116
+ export: rendererOptions.svgExport,
117
+ };
118
+ const legacyConfig = {};
119
+ for (const name of ['regions', 'leftWidth', 'rightWidth', 'responsive', 'projectionOptions', 'timeline', 'runtimeDetails', 'runtimeTraceOptions', 'svgExport']) {
120
+ if (Object.prototype.hasOwnProperty.call(options, name)) legacyConfig[name] = options[name];
121
+ }
122
+ const normalizedConfig = normalizeStudioConfig(config, { fallback: fallbackConfig, legacy: legacyConfig });
123
+ const resolvedUi = normalizedConfig.ui;
124
+ const resolvedViewer = normalizedConfig.viewer;
99
125
  const resolvedAllowedModes = normalizeAllowedModes(allowedModes);
100
126
  const resolvedMode = resolvedAllowedModes.includes(mode) ? mode : resolvedAllowedModes[0];
101
- const instanceProjection = projection || (responsive ? 'auto' : 'approval');
127
+ const instanceProjection = projection || (resolvedViewer.responsive ? 'auto' : 'approval');
102
128
  const activeProjection = resolvedMode === 'instance' ? instanceProjection : 'standard';
103
129
  Object.assign(this, {
104
130
  container,
@@ -107,26 +133,40 @@ export class BpmnStudioShell {
107
133
  paletteRegistry,
108
134
  templateRegistry,
109
135
  contextMenuRegistry,
110
- rendererOptions: { ...rendererOptions, nodeSubtitleResolver },
136
+ rendererOptions: {
137
+ ...rendererOptions,
138
+ nodeSubtitleResolver,
139
+ timeline: resolvedViewer.timeline,
140
+ runtimeDetails: resolvedViewer.runtimeDetails,
141
+ runtimeTraceOptions: resolvedViewer.runtimeTraceOptions,
142
+ },
111
143
  slots,
112
144
  runtime,
113
145
  mode: resolvedMode,
114
146
  allowedModes: resolvedAllowedModes,
115
147
  projection: activeProjection,
116
148
  _instanceProjection: instanceProjection,
117
- responsive,
118
- projectionOptions,
149
+ _projectionExplicit: projection !== undefined,
150
+ responsive: resolvedViewer.responsive,
151
+ projectionOptions: resolvedViewer.projectionOptions || null,
119
152
  runtimeAppearance,
120
- svgExport: svgExport || rendererOptions.svgExport || null,
153
+ svgExport: normalizedConfig.export || null,
121
154
  _usesCustomLayout: typeof layout === 'function',
122
- _regions: normalizeRegions(regions),
155
+ _regions: normalizeStudioRegions(resolvedUi.regions),
156
+ _config: normalizedConfig,
157
+ _configFallback: fallbackConfig,
158
+ _legacyConfig: legacyConfig,
159
+ _controlMetrics: resolveStudioControlMetrics(resolvedUi.controlSize),
123
160
  });
161
+ this.ui = createStudioUiContext(() => this._controlMetrics);
124
162
  this.propertiesRegistry = propertiesRegistry || createDefaultPropertiesRegistry({ studio });
125
163
  this.interactions = createInteractionController({ studio, templates: templateRegistry });
126
164
  this._cleanups = [];
127
165
  this._instances = [];
128
166
  this._modeListeners = new Set();
129
167
  this._validationListeners = new Set();
168
+ this._panelSelectionListeners = new Set();
169
+ this._panelSelection = Object.freeze({ selection: null, selectedElement: null, trace: null });
130
170
  this._modeViewports = new Map();
131
171
  this._destroyed = false;
132
172
  this._svgExportPreview = null;
@@ -150,18 +190,25 @@ export class BpmnStudioShell {
150
190
  });
151
191
  this.container.classList.add('nova-studio-shell');
152
192
  this.themeController = new ThemeController({ root: this.container, theme, onChange: onThemeChange });
153
- this.container.style.setProperty('--nova-left-width', `${leftWidth}px`);
154
- this.container.style.setProperty('--nova-right-width', `${rightWidth}px`);
193
+ applyStudioControlMetrics(this.container, this._controlMetrics);
194
+ this.container.style.setProperty('--nova-left-width', `${resolvedUi.sidebarWidth.left}px`);
195
+ this.container.style.setProperty('--nova-right-width', `${resolvedUi.sidebarWidth.right}px`);
155
196
  const context = {
156
197
  studio,
157
198
  shell: this,
158
199
  actions: this.actions,
200
+ ui: this.ui,
159
201
  getState: () => this.studio.getState(),
160
202
  subscribe: (listener) => this.studio.subscribe(listener),
161
203
  getMode: () => this.getMode(),
162
204
  getAllowedModes: () => this.getAllowedModes(),
163
205
  subscribeMode: (listener) => this.subscribeMode(listener),
164
206
  subscribeValidation: (listener) => this.subscribeValidation(listener),
207
+ getSidebarState: () => this.getSidebarState(),
208
+ setSidebarCollapsed: (side, collapsed) => this.setSidebarCollapsed(side, collapsed),
209
+ subscribeSidebarChange: (listener) => this.subscribeSidebarChange(listener),
210
+ getPanelSelection: () => this.getPanelSelection(),
211
+ subscribePanelSelection: (listener) => this.subscribePanelSelection(listener),
165
212
  iconRegistry,
166
213
  paletteRegistry,
167
214
  propertiesRegistry: this.propertiesRegistry,
@@ -178,6 +225,12 @@ export class BpmnStudioShell {
178
225
  const cleanup = layout({ container, ...context, mount });
179
226
  if (typeof cleanup === 'function') this._cleanups.push(cleanup);
180
227
  } else this._buildDefault(context, mount);
228
+ if (!this._usesCustomLayout) {
229
+ this._updatePanelSelection();
230
+ this._recordCleanup(this.studio.subscribe((event) => {
231
+ if (['selectionChanged', 'modelChanged', 'scopeChanged'].includes(event.type)) this._updatePanelSelection();
232
+ }));
233
+ }
181
234
  }
182
235
 
183
236
  _buildDefault(context, mount) {
@@ -194,18 +247,6 @@ export class BpmnStudioShell {
194
247
  const projectionSwitch = node('nav', 'nova-studio-projection-switch is-hidden');
195
248
  projectionSwitch.setAttribute('aria-label', '审批轨迹视图');
196
249
  const projectionButtons = new Map();
197
- const projectionOptions = this.projectionOptions || [
198
- { value: this.responsive ? 'auto' : 'approval', label: '实际路径' },
199
- { value: 'standard', label: '完整 BPMN' },
200
- ];
201
- for (const item of projectionOptions) {
202
- const button = node('button', '', item.label);
203
- button.type = 'button';
204
- button.dataset.projection = item.value;
205
- button.addEventListener('click', () => this.setProjection(item.value));
206
- projectionButtons.set(item.value, button);
207
- projectionSwitch.appendChild(button);
208
- }
209
250
  const centerControls = node('div', 'nova-studio-center-controls');
210
251
  centerControls.append(modeSwitch, projectionSwitch);
211
252
  const tools = node('div', 'nova-studio-tools');
@@ -370,6 +411,10 @@ export class BpmnStudioShell {
370
411
  statusbar.append(statusLeft, viewportTools);
371
412
  canvasColumn.append(canvasStage, statusbar);
372
413
  const right = node('aside', 'nova-studio-right');
414
+ const leftContent = node('div', 'nova-studio-sidebar-content nova-studio-left-content');
415
+ const rightContent = node('div', 'nova-studio-sidebar-content nova-studio-right-content');
416
+ left.appendChild(leftContent);
417
+ right.appendChild(rightContent);
373
418
  body.append(left, canvasColumn, right);
374
419
  this.container.append(header, body);
375
420
  hydrateIcons(this.container, this.iconRegistry);
@@ -380,6 +425,8 @@ export class BpmnStudioShell {
380
425
  _body: body,
381
426
  _left: left,
382
427
  _right: right,
428
+ _leftContent: leftContent,
429
+ _rightContent: rightContent,
383
430
  _canvasColumn: canvasColumn,
384
431
  _footer: statusbar,
385
432
  _canvasHost: canvasHost,
@@ -399,7 +446,9 @@ export class BpmnStudioShell {
399
446
  _defaultMount: mount,
400
447
  _usesDefaultProperties: !this.slots.right,
401
448
  });
449
+ this._sidebars = new StudioSidebars(this, { body, left, right, leftContent, rightContent });
402
450
  this._rebuildModeButtons();
451
+ this._rebuildProjectionButtons();
403
452
  this.canvas = mount.canvas(canvasHost, {
404
453
  rendererOptions: {
405
454
  ...this.rendererOptions,
@@ -410,10 +459,9 @@ export class BpmnStudioShell {
410
459
  },
411
460
  });
412
461
  this._renderScopePath();
413
- if (this.slots.left) this._recordCleanup(mountSlot(this.slots.left, left, { ...context, canvas: this.canvas }));
414
- else this.palette = mount.palette(left, { canvas: this.canvas });
415
- if (this.slots.right) this._recordCleanup(mountSlot(this.slots.right, right, { ...context, canvas: this.canvas }));
416
- else this.properties = mount.properties(right, { canvas: this.canvas });
462
+ if (this.slots.left) this._recordCleanup(mountSlot(this.slots.left, leftContent, { ...context, canvas: this.canvas }));
463
+ else this.palette = mount.palette(leftContent, { canvas: this.canvas });
464
+ this._setRightSlot(this.slots.right);
417
465
  const mountHeaderActions = () => {
418
466
  if (this.slots.headerActions) {
419
467
  this._recordCleanup(mountSlot(this.slots.headerActions, headerActions, { ...context, canvas: this.canvas }));
@@ -532,6 +580,26 @@ export class BpmnStudioShell {
532
580
  hydrateIcons(this._modeSwitch, this.iconRegistry);
533
581
  }
534
582
 
583
+ _rebuildProjectionButtons() {
584
+ if (!this._projectionSwitch) return;
585
+ this._projectionSwitch.replaceChildren();
586
+ this._projectionButtons = new Map();
587
+ const options = this.projectionOptions || [
588
+ { value: this.responsive ? 'auto' : 'approval', label: '实际路径' },
589
+ { value: 'standard', label: '完整 BPMN' },
590
+ ];
591
+ for (const item of options) {
592
+ const button = node('button', '', item.label);
593
+ button.type = 'button';
594
+ button.dataset.projection = item.value;
595
+ button.addEventListener('click', () => this.setProjection(item.value));
596
+ this._projectionButtons.set(item.value, button);
597
+ this._projectionSwitch.appendChild(button);
598
+ }
599
+ hydrateIcons(this._projectionSwitch, this.iconRegistry);
600
+ this._syncProjectionButtons();
601
+ }
602
+
535
603
  _syncModeChrome() {
536
604
  this._modeButtons?.forEach((button, value) => {
537
605
  const active = value === this.mode;
@@ -619,10 +687,11 @@ export class BpmnStudioShell {
619
687
  : this.rendererOptions.runtimeTransitionDetailsRenderer,
620
688
  onRuntimeTransitionDetailsOpen: this.rendererOptions.onRuntimeTransitionDetailsOpen,
621
689
  onTraceClick: (payload) => {
622
- const kind = payload.targetType === 'visit' ? 'node' : payload.targetType === 'transition' ? 'node' : payload.targetType;
623
- this._renderReadonlyDetails({ kind, element: payload.element, presentation: payload.presentation, transition: payload.transition });
624
690
  this.rendererOptions.onTraceClick?.(payload);
625
691
  },
692
+ _onPanelSelection: (trace) => {
693
+ if (viewer && this.viewer === viewer) this._updatePanelSelection(trace);
694
+ },
626
695
  onProjectionChange: () => {
627
696
  if (this.viewer === viewer) this._syncProjectionChrome();
628
697
  },
@@ -630,7 +699,6 @@ export class BpmnStudioShell {
630
699
  if (this.viewer === viewer) this._handleViewport(viewport);
631
700
  },
632
701
  onElementClick: (selection) => {
633
- if (!selection) this._renderReadonlyDetails(null);
634
702
  this.rendererOptions.onElementClick?.(selection);
635
703
  },
636
704
  });
@@ -717,17 +785,19 @@ export class BpmnStudioShell {
717
785
  this.projection = mode === 'instance' ? this._instanceProjection : 'standard';
718
786
  if (nextCanvas) this._instances.push(nextCanvas);
719
787
  this._releaseInstance(previousCanvas);
788
+ if (this._sidebars?.motionViewer === previousViewer) this._sidebars.finishMotion({ resume: false });
720
789
  previousViewer?.destroy?.();
721
790
 
722
791
  if (this.palette) this.palette.canvas = this.canvas;
723
792
  if (this._usesDefaultProperties) {
724
793
  this._releaseInstance(this.properties);
725
794
  this.properties = null;
726
- if (this.canvas) this.properties = this._mountProperties(this._right, { canvas: this.canvas });
795
+ if (this.canvas) this.properties = this._mountProperties(this._rightContent, { canvas: this.canvas });
727
796
  else this._renderReadonlyDetails(null);
728
797
  }
729
798
  this._rebuildModeButtons();
730
799
  this._syncModeChrome();
800
+ this._updatePanelSelection(null);
731
801
  this._restoreModeViewport(mode);
732
802
  if (emit && previousMode !== mode) this._emitModeChange(previousMode, source);
733
803
  return true;
@@ -738,11 +808,56 @@ export class BpmnStudioShell {
738
808
  this.rendererOptions.onViewportChange?.(viewport);
739
809
  }
740
810
 
811
+ // Adapter-only seam: replace right content without remounting the workbench.
812
+ _setRightSlot(slot) {
813
+ if (this._destroyed || !this._rightContent) return;
814
+ const next = slot || null;
815
+ if (this._rightSlot === next) return;
816
+ this._rightSlotCleanup?.();
817
+ this._rightSlotCleanup = null;
818
+ this._releaseInstance(this.properties);
819
+ this.properties = null;
820
+ this._rightContent.replaceChildren();
821
+ this._rightSlot = next;
822
+ this._usesDefaultProperties = !next;
823
+ if (next) {
824
+ const shell = this;
825
+ const context = { ...this._defaultContext, get canvas() { return shell.canvas; } };
826
+ this._rightSlotCleanup = mountSlot(next, this._rightContent, context);
827
+ } else if (this.canvas) this.properties = this._mountProperties(this._rightContent, { canvas: this.canvas });
828
+ else this._renderReadonlyDetails(this._readonlyPanelDetails());
829
+ }
830
+
831
+ getPanelSelection() { return this._panelSelection; }
832
+ subscribePanelSelection(listener) {
833
+ if (typeof listener !== 'function') throw new TypeError('subscribePanelSelection() requires a listener function.');
834
+ if (this._destroyed) return () => {};
835
+ this._panelSelectionListeners.add(listener);
836
+ return () => this._panelSelectionListeners.delete(listener);
837
+ }
838
+ _readonlyPanelDetails() {
839
+ const { selection, selectedElement, trace } = this._panelSelection;
840
+ return selectedElement ? { kind: selection?.kind, element: selectedElement, presentation: trace?.presentation, transition: trace?.transition } : null;
841
+ }
842
+ _updatePanelSelection(trace = this._panelSelection.trace) {
843
+ if (this._destroyed) return;
844
+ const next = createPanelSelection(this.studio, this.mode === 'design' ? null : this.viewer, trace);
845
+ if (samePanelSelection(next, this._panelSelection)) {
846
+ if (this.mode !== 'design' && !next.selectedElement) this._renderReadonlyDetails(null);
847
+ return;
848
+ }
849
+ this._panelSelection = next;
850
+ if (this.mode !== 'design') this._renderReadonlyDetails(this._readonlyPanelDetails());
851
+ for (const listener of [...this._panelSelectionListeners]) {
852
+ try { listener(next); } catch (error) { console.error('BpmnStudioShell panel selection listener failed.', error); }
853
+ }
854
+ }
855
+
741
856
  _renderReadonlyDetails(selection) {
742
- if (!this._usesDefaultProperties || !this._right) return;
857
+ if (!this._usesDefaultProperties || !this._rightContent) return;
743
858
  const model = this.studio.model;
744
859
  const element = selection?.element;
745
- this._right.innerHTML = '';
860
+ this._rightContent.innerHTML = '';
746
861
  const panel = node('div', 'nova-studio-readonly-panel');
747
862
  const header = node('header', 'nova-studio-readonly-header');
748
863
  header.append(node('strong', '', element?.name || (selection?.kind === 'edge' ? '流程连线' : model.name)), node('span', '', element?.id || model.id));
@@ -769,7 +884,7 @@ export class BpmnStudioShell {
769
884
  body.appendChild(row);
770
885
  }
771
886
  panel.appendChild(body);
772
- this._right.appendChild(panel);
887
+ this._rightContent.appendChild(panel);
773
888
  }
774
889
 
775
890
  _zoomActive(factor) { (this.canvas || this.viewer)?.zoomBy?.(factor); }
@@ -781,6 +896,91 @@ export class BpmnStudioShell {
781
896
  if (this.canvas) this.canvas.fitView(72);
782
897
  else this.viewer?.fitView?.();
783
898
  }
899
+ getConfig() {
900
+ const state = this.studio.getState();
901
+ const modeling = Object.freeze({
902
+ propertiesProfile: state.propertiesProfile,
903
+ ...(state.allowedNodeTypes ? { allowedNodeTypes: Object.freeze([...state.allowedNodeTypes]) } : {}),
904
+ ...(state.allowedEdgeTypes ? { allowedEdgeTypes: Object.freeze([...state.allowedEdgeTypes]) } : {}),
905
+ });
906
+ return Object.freeze({
907
+ modeling,
908
+ ui: this._config.ui,
909
+ viewer: this._config.viewer,
910
+ ...(this._config.export ? { export: this._config.export } : {}),
911
+ });
912
+ }
913
+ setConfig(config) {
914
+ if (this._destroyed) return this.getConfig();
915
+ const next = normalizeStudioConfig(config, {
916
+ fallback: this._configFallback,
917
+ legacy: this._legacyConfig,
918
+ });
919
+ const hasRegions = config?.ui
920
+ && Object.prototype.hasOwnProperty.call(config.ui, 'regions')
921
+ && config.ui.regions !== undefined
922
+ && config.ui.regions !== null;
923
+ if (this._usesCustomLayout && hasRegions) {
924
+ throw new Error('BpmnStudioShell config.ui.regions is unavailable when a custom layout is active.');
925
+ }
926
+
927
+ const previous = this._config;
928
+ const previousSidebars = this._sidebars?.getState();
929
+ const nextMetrics = resolveStudioControlMetrics(next.ui.controlSize);
930
+ const regionsChanged = !sameConfigValue(previous.ui.regions, next.ui.regions);
931
+ const projectionOptionsChanged = !sameConfigValue(previous.viewer.projectionOptions, next.viewer.projectionOptions);
932
+ const responsiveChanged = previous.viewer.responsive !== next.viewer.responsive;
933
+ const displayChanged = ['timeline', 'runtimeDetails', 'runtimeTraceOptions']
934
+ .some((name) => !sameConfigValue(previous.viewer[name], next.viewer[name]));
935
+ const exportChanged = !sameConfigValue(previous.export, next.export);
936
+ const nextDefaultProjection = next.viewer.responsive ? 'auto' : 'approval';
937
+ const projectionChanged = responsiveChanged
938
+ && !this._projectionExplicit
939
+ && this._instanceProjection !== nextDefaultProjection;
940
+ const viewport = this.viewer?.activeProjection === 'compact'
941
+ ? null
942
+ : (this.canvas?.renderer || this.viewer?.renderer)?.getViewportState?.();
943
+
944
+ this._config = next;
945
+ this._controlMetrics = nextMetrics;
946
+ applyStudioControlMetrics(this.container, nextMetrics);
947
+ this.container.style.setProperty('--nova-left-width', `${next.ui.sidebarWidth.left}px`);
948
+ this.container.style.setProperty('--nova-right-width', `${next.ui.sidebarWidth.right}px`);
949
+ this._regions = normalizeStudioRegions(next.ui.regions);
950
+ this.responsive = next.viewer.responsive;
951
+ this.projectionOptions = next.viewer.projectionOptions || null;
952
+ this.rendererOptions.timeline = next.viewer.timeline;
953
+ this.rendererOptions.runtimeDetails = next.viewer.runtimeDetails;
954
+ this.rendererOptions.runtimeTraceOptions = next.viewer.runtimeTraceOptions;
955
+ this.svgExport = next.export || null;
956
+ this.rendererOptions.svgExport = this.svgExport;
957
+ if (projectionChanged) {
958
+ this._instanceProjection = nextDefaultProjection;
959
+ if (this.mode === 'instance') this.projection = nextDefaultProjection;
960
+ }
961
+
962
+ if (regionsChanged) this._applyRegions({ fit: false, sidebars: false });
963
+ if (previousSidebars) this._sidebars.updateConfig(previousSidebars);
964
+ if (projectionOptionsChanged || responsiveChanged) this._rebuildProjectionButtons();
965
+ if (this.canvas?.renderer && exportChanged) this.canvas.renderer.svgExportOptions = this.svgExport || {};
966
+ if (this.viewer) {
967
+ this.viewer.responsive = this.responsive;
968
+ this.viewer.options.responsive = this.responsive;
969
+ if (exportChanged) this.viewer.svgExportOptions = this.svgExport || {};
970
+ if (projectionChanged && this.mode === 'instance') this.viewer.projection = this._instanceProjection;
971
+ if (displayChanged) {
972
+ this.viewer.setDisplayOptions({
973
+ timeline: next.viewer.timeline,
974
+ runtimeDetails: next.viewer.runtimeDetails,
975
+ runtimeTraceOptions: next.viewer.runtimeTraceOptions,
976
+ replace: true,
977
+ });
978
+ } else if (projectionChanged && this.mode === 'instance') this.viewer.refresh();
979
+ }
980
+ if (viewport) (this.canvas?.renderer || this.viewer?.renderer)?.setViewportState?.(viewport);
981
+ this._syncProjectionButtons();
982
+ return this.getConfig();
983
+ }
784
984
  getMode() { return this.mode; }
785
985
  getAllowedModes() { return Object.freeze([...this.allowedModes]); }
786
986
  subscribeMode(listener) {
@@ -814,37 +1014,61 @@ export class BpmnStudioShell {
814
1014
  if (this.mode === 'instance') this.viewer?.setRuntime(runtime);
815
1015
  }
816
1016
  setProjection(projection) {
1017
+ this._projectionExplicit = true;
817
1018
  this._instanceProjection = projection;
818
1019
  if (this.mode === 'instance') this.projection = projection;
819
1020
  this._syncProjectionButtons();
820
1021
  if (this.mode === 'instance') this.viewer?.setProjection?.(projection);
821
1022
  }
822
- getRegions() { return Object.freeze({ ...this._regions }); }
1023
+ getRegions() { return Object.freeze({ ...this._regions, left: this._isSidebarHidden('left') ? 'hidden' : 'default', right: this._isSidebarHidden('right') ? 'hidden' : 'default' }); }
823
1024
  setRegions(regions) {
824
1025
  if (this._usesCustomLayout) {
825
1026
  throw new Error('BpmnStudioShell setRegions() is unavailable when a custom layout is active.');
826
1027
  }
827
- const next = normalizeRegions(regions, this._regions);
1028
+ const next = normalizeStudioRegions(regions, this._regions);
828
1029
  const changed = STUDIO_SHELL_REGIONS.some((name) => next[name] !== this._regions[name]);
829
1030
  this._regions = next;
830
- if (changed) this._applyRegions();
1031
+ if (this._config) {
1032
+ this._config = Object.freeze({
1033
+ ...this._config,
1034
+ ui: Object.freeze({ ...this._config.ui, regions: Object.freeze(normalizeStudioRegions(regions, this._config.ui.regions)) }),
1035
+ });
1036
+ }
1037
+ if (changed || regions?.right === 'default') this._applyRegions({ fit: false });
831
1038
  return this.getRegions();
832
1039
  }
833
- _applyRegions({ fit = true } = {}) {
1040
+ _isSidebarHidden(side) {
1041
+ if (this._regions[side] === 'hidden') return true;
1042
+ if (side === 'left') return this.mode !== 'design';
1043
+ return this.mode === 'instance' && this._config?.ui.regions.right !== 'default';
1044
+ }
1045
+ getSidebarState() {
1046
+ if (this._sidebars) return this._sidebars.getState();
1047
+ return Object.freeze(Object.fromEntries(['left', 'right'].map((side) => [side, Object.freeze({ collapsed: false, hidden: true, collapsible: false })])));
1048
+ }
1049
+ setSidebarCollapsed(side, collapsed) { return this._sidebars?.setCollapsed(side, collapsed) || false; }
1050
+ subscribeSidebarChange(listener) {
1051
+ if (typeof listener !== 'function') throw new TypeError('subscribeSidebarChange() requires a listener function.');
1052
+ return this._sidebars?.subscribe(listener) || (() => {});
1053
+ }
1054
+ _applyRegions({ fit = true, sidebars = true } = {}) {
834
1055
  if (this._usesCustomLayout || !this._body) return;
835
1056
  const readonly = this.mode !== 'design';
836
1057
  const headerHidden = this._regions.header === 'hidden';
837
- const leftHidden = readonly || this._regions.left === 'hidden';
838
- const rightHidden = this._regions.right === 'hidden';
1058
+ const leftHidden = this._isSidebarHidden('left');
1059
+ const rightHidden = this._isSidebarHidden('right');
839
1060
  const footerHidden = this._regions.footer === 'hidden';
840
1061
  this.container.classList.toggle('is-header-hidden', headerHidden);
841
1062
  this._body.classList.toggle('is-left-hidden', leftHidden);
842
1063
  this._body.classList.toggle('is-right-hidden', rightHidden);
843
1064
  this._canvasColumn?.classList.toggle('is-footer-hidden', footerHidden);
844
1065
  if (this._header) this._header.hidden = headerHidden;
845
- if (this._left) this._left.hidden = leftHidden;
846
- if (this._right) this._right.hidden = rightHidden;
1066
+ if (!this._sidebars) {
1067
+ if (this._left) this._left.hidden = leftHidden;
1068
+ if (this._right) this._right.hidden = rightHidden;
1069
+ }
847
1070
  if (this._footer) this._footer.hidden = footerHidden;
1071
+ if (sidebars) this._sidebars?.apply({ animate: false });
848
1072
  if (fit) requestAnimationFrame(() => this._fitActive());
849
1073
  }
850
1074
  setTheme(theme) { return this.themeController.setTheme(theme); }
@@ -987,6 +1211,10 @@ export class BpmnStudioShell {
987
1211
  this._destroyed = true;
988
1212
  this._modeListeners.clear();
989
1213
  this._validationListeners.clear();
1214
+ this._panelSelectionListeners.clear();
1215
+ this._sidebars?.destroy();
1216
+ this._rightSlotCleanup?.();
1217
+ this._rightSlotCleanup = null;
990
1218
  this._svgExportPreview?.close?.({ immediate: true });
991
1219
  this._svgExportPreview = null;
992
1220
  this._cleanups.splice(0).reverse().forEach((cleanup) => cleanup?.());