@bpmn-nova/studio 0.3.3-preview → 0.3.5-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/README.md CHANGED
@@ -4,7 +4,7 @@ BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计
4
4
 
5
5
  > **English summary:** The complete framework-neutral BPMN Nova package for process design, viewing, approval traces, themes, properties, and pure SVG export.
6
6
 
7
- > 当前版本为 `0.3.3-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
7
+ > 当前版本为 `0.3.5-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
8
8
 
9
9
  ![BPMN Nova 流程设计器](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -47,14 +47,22 @@ const model = createEmptyProcess('flowable')
47
47
  model.id = 'Process_PurchaseApproval'
48
48
  model.name = '采购申请审批流程'
49
49
 
50
+ const config = {
51
+ modeling: {
52
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
53
+ allowedEdgeTypes: ['sequenceFlow'],
54
+ },
55
+ ui: { controlSize: 'medium' },
56
+ }
57
+
50
58
  const studio = createStudioController({
51
59
  model,
52
- allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
53
- allowedEdgeTypes: ['sequenceFlow'],
60
+ config,
54
61
  })
55
62
  const shell = createStudioShell({
56
63
  container: document.querySelector('#studio'),
57
64
  studio,
65
+ config,
58
66
  mode: 'design',
59
67
  allowedModes: ['design'],
60
68
  theme: 'auto',
@@ -77,15 +85,17 @@ studio.destroy()
77
85
  const shell = createStudioShell({
78
86
  container: novaHost,
79
87
  studio,
88
+ config: {
89
+ ui: { controlSize: 'medium', regions: { right: 'hidden' } },
90
+ },
80
91
  mode: 'design',
81
92
  allowedModes: ['design'],
82
- regions: { right: 'hidden' },
83
93
  slots: {
84
94
  headerStart({ container, actions, getState, subscribe }) {
85
95
  // 在 Nova Header 左侧挂载宿主的返回入口、业务图标、流程名称和类型。
86
96
  // 完整替换 Header 时改用 slots.header,并继续调用 actions。
87
97
  },
88
- headerActions({ container, actions, getMode, subscribeMode }) {
98
+ headerActions({ container, actions, getMode, subscribeMode, ui }) {
89
99
  // 替换默认“校验 / 导入 / 导出”,在这里挂载宿主的“校验 / 保存 / 发布”。
90
100
  // 校验按钮调用 actions.validate();保存和发布继续调用宿主服务。
91
101
  },
@@ -101,7 +111,7 @@ const issues = shell.actions.validate()
101
111
  const xml = shell.actions.exportXml('flowable')
102
112
  ```
103
113
 
104
- `regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
114
+ `regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道,也不保留折叠按钮。Design 默认显示左右,Viewer 仅右侧,Instance 默认隐藏两侧;显式 `regions.right: 'default'` 可在 Instance 开启右侧。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
105
115
 
106
116
  `headerActions` 只替换 Header 最右侧动作组,`headerStart` 可与其同时使用,完整 `header` 的优先级更高。未提供 `headerActions` 时仍显示默认“校验 / 导入 / 导出”。顶部不再重复显示最佳视图;底部缩放区和 `fitView()` Interface 保持不变。
107
117
 
@@ -128,6 +138,62 @@ async function publishProcess(actions) {
128
138
 
129
139
  `shell.actions` 只封装撤销、重做、布局、视图、校验和 BPMN/SVG 导入导出。宿主的保存草稿、发布、权限、文件选择和服务端事务不属于 Nova Actions。
130
140
 
141
+ ## 侧栏内容与平滑折叠
142
+
143
+ `slots.right` 只替换内容,Nova 保留宽度管理和分隔线中点折叠按钮;不要同时将 `regions.right` 设置为 `hidden`,否则自定义内容也隐藏。`config.ui.rightPanel.layout` 默认 `flex`,宿主安排固定头尾和 `flex: 1; min-height: 0; overflow: auto` 内容区;改为 `scroll` 时由 Nova 滚动整个内容。
144
+
145
+ ```js
146
+ const shell = createStudioShell({
147
+ container,
148
+ studio,
149
+ config: { ui: {
150
+ leftPanel: { collapsible: true, defaultCollapsed: false },
151
+ rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'scroll' },
152
+ } },
153
+ slots: {
154
+ right({ container, shell }) {
155
+ const text = document.createElement('p')
156
+ container.append(text)
157
+ const render = ({ selectedElement }) => { text.textContent = selectedElement?.id ?? '未选择元素' }
158
+ render(shell.getPanelSelection())
159
+ const off = shell.subscribePanelSelection(render)
160
+ return () => { off(); text.remove() }
161
+ },
162
+ },
163
+ })
164
+ const offSidebar = shell.subscribeSidebarChange(({ side, collapsed, source }) => console.log(side, collapsed, source))
165
+ shell.setSidebarCollapsed('left', true)
166
+ console.log(shell.getSidebarState().left)
167
+ // 卸载时 offSidebar(); shell.destroy(); studio.destroy()
168
+ ```
169
+
170
+ 默认左右可独立折叠;200ms 动画尊重减少动态效果,不卸载内容、不丢失表单和滚动状态、不自动 Fit。Shell 容器 ≤720px 时首次默认收起并保留按钮,展开挤压画布,宽窄屏分别记忆状态。`defaultCollapsed` 只用于初始化;隐藏不同于收起。`panelSelection` 包含 `{ selection, selectedElement, trace }`,Design 从 Controller 派生,Viewer/Instance 从实际点击派生而不回写设计选择。
171
+
172
+ 完整配置、事件契约、窄屏与 Flex 样式见仓库 [CUSTOMIZATION.md](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md#侧栏布局与平滑折叠),或包内 `llms-full.txt` 同名章节。上述新增能力自 `0.3.5-preview` 提供。
173
+
174
+ ## Config 与 Header 控件尺寸
175
+
176
+ `config` 统一承载静态配置:`modeling` 是 Controller 初始化约束,`ui` 是 Shell 区域和尺寸,`viewer` 是只读/实例展示选项,`export` 是 SVG 导出默认值。`config.ui.controlSize` 默认 `medium`,支持 `small`(28px)、`medium`(32px)、`large`(40px)和 `24px`–`48px` 的 CSS 像素字符串。
177
+
178
+ ```js
179
+ shell.setConfig({
180
+ ui: {
181
+ controlSize: '36px',
182
+ regions: { right: 'hidden' },
183
+ sidebarWidth: { left: 232, right: 344 },
184
+ },
185
+ viewer: { responsive: true },
186
+ export: { padding: 36 },
187
+ })
188
+
189
+ console.log(shell.getConfig())
190
+ console.log(shell.ui.controlSize, shell.ui.controlHeight) // 36px, 36px
191
+ ```
192
+
193
+ 尺寸只作用于 Header;Footer、Canvas 浮动工具、Palette 和 Properties 不随之缩放。Header Slot 可读取只读 `ui`,并继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 和 `--nova-control-radius`。Element Plus `default`、Ant Design `middle` 都对应 Nova `medium`。
194
+
195
+ 旧顶层配置仍兼容但已 Deprecated;同一字段同时出现时旧顶层显式值优先。`setConfig()` 原地应用 UI、Viewer 和 Export,不重建 Canvas;`config.modeling` 只在创建 Controller 时生效,运行中继续通过 `setPropertiesProfile()` 修改 Profile,节点/连线白名单保持不变量。
196
+
131
197
  ## Mode 与节点副标题投影
132
198
 
133
199
  `design`、`viewer`、`instance` 分别表示流程设计、流程展示和审批轨迹。Shell 是唯一 Mode 状态源:
@@ -188,6 +254,8 @@ viewer.destroy()
188
254
 
189
255
  ## 审批轨迹
190
256
 
257
+ 完整 XML、Snapshot、宿主转换器及 Core 生命周期示例见[审批轨迹接入指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/RUNTIME-INTEGRATION.md),正文也包含在包内 `llms-full.txt`。Studio 现有 Runtime 类型出口保持不变;Vue/React 自 `0.3.5-preview` 提供的新增类型出口和旧版兼容写法在指南中单独说明。
258
+
191
259
  Viewer 接收引擎中立的 Runtime Snapshot。三种投影使用相同数据:
192
260
 
193
261
  | 投影 | 配置 | 适用场景 |
@@ -337,6 +405,7 @@ import { activitiProfile } from '@bpmn-nova/studio/activiti'
337
405
  - [项目首页](https://github.com/daxiangme/bpmn-nova)
338
406
  - [快速开始](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/GETTING-STARTED.md)
339
407
  - [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
408
+ - [审批轨迹接入指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/RUNTIME-INTEGRATION.md)
340
409
  - [自定义指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md)
341
410
 
342
411
  ## 能力边界
package/dist/config.js ADDED
@@ -0,0 +1,285 @@
1
+ const CONTROL_SIZE_PRESETS = Object.freeze({
2
+ small: 28,
3
+ medium: 32,
4
+ large: 40,
5
+ });
6
+
7
+ const STUDIO_SHELL_REGIONS = Object.freeze(['header', 'left', 'right', 'footer']);
8
+ const STUDIO_PROJECTIONS = Object.freeze(['auto', 'standard', 'approval', 'compact']);
9
+
10
+ export const DEFAULT_STUDIO_SHELL_REGIONS = Object.freeze({
11
+ header: 'default',
12
+ left: 'default',
13
+ right: 'default',
14
+ footer: 'default',
15
+ });
16
+
17
+ function isObject(value) {
18
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ function assertObject(value, path) {
22
+ if (!isObject(value)) throw new TypeError(`${path} must be an object.`);
23
+ return value;
24
+ }
25
+
26
+ function hasOwn(object, key) {
27
+ return Object.prototype.hasOwnProperty.call(object, key);
28
+ }
29
+
30
+ function copyValue(value) {
31
+ if (Array.isArray(value)) return value.map(copyValue);
32
+ if (!isObject(value)) return value;
33
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyValue(item)]));
34
+ }
35
+
36
+ function freezeValue(value) {
37
+ if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
38
+ Object.values(value).forEach(freezeValue);
39
+ return Object.freeze(value);
40
+ }
41
+
42
+ function immutableCopy(value) {
43
+ return freezeValue(copyValue(value));
44
+ }
45
+
46
+ function optionalObject(value, path) {
47
+ if (value === undefined) return undefined;
48
+ return immutableCopy(assertObject(value, path));
49
+ }
50
+
51
+ function optionalTypeList(value, path) {
52
+ if (value === undefined || value === null) return undefined;
53
+ if (typeof value === 'string' || typeof value?.[Symbol.iterator] !== 'function') {
54
+ throw new TypeError(`${path} must be a readonly array or iterable.`);
55
+ }
56
+ const entries = [...value];
57
+ if (entries.some((item) => typeof item !== 'string')) throw new TypeError(`${path} must contain only strings.`);
58
+ return Object.freeze(entries);
59
+ }
60
+
61
+ function normalizePropertiesProfile(value) {
62
+ if (value === undefined) return 'business';
63
+ if (value !== 'business' && value !== 'developer') {
64
+ throw new TypeError('config.modeling.propertiesProfile must be "business" or "developer".');
65
+ }
66
+ return value;
67
+ }
68
+
69
+ function normalizeSidebarWidth(value, fallback, path) {
70
+ if (value === undefined) return fallback;
71
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) {
72
+ throw new TypeError(`${path} must be a finite non-negative number.`);
73
+ }
74
+ return value;
75
+ }
76
+
77
+ function normalizeSidebarConfig(value, side) {
78
+ const path = `config.ui.${side}Panel`;
79
+ const input = value === undefined ? {} : assertObject(value, path);
80
+ for (const key of ['collapsible', 'defaultCollapsed']) {
81
+ if (input[key] !== undefined && typeof input[key] !== 'boolean') {
82
+ throw new TypeError(`${path}.${key} must be a boolean.`);
83
+ }
84
+ }
85
+ const result = { collapsible: input.collapsible ?? true, defaultCollapsed: input.defaultCollapsed ?? false };
86
+ if (side === 'right') {
87
+ const layout = input.layout === undefined ? 'flex' : input.layout;
88
+ if (!['flex', 'scroll'].includes(layout)) throw new TypeError(`${path}.layout must be flex or scroll.`);
89
+ result.layout = layout;
90
+ }
91
+ return Object.freeze(result);
92
+ }
93
+
94
+ export function normalizeStudioControlSize(value) {
95
+ if (value === undefined) return 'medium';
96
+ if (typeof value !== 'string') {
97
+ throw new TypeError('config.ui.controlSize must be small, medium, large, or a 24px–48px CSS pixel value.');
98
+ }
99
+ if (hasOwn(CONTROL_SIZE_PRESETS, value)) return value;
100
+ if (!/^(?:\d+(?:\.\d+)?|\.\d+)px$/.test(value)) {
101
+ throw new TypeError('config.ui.controlSize must be small, medium, large, or a 24px–48px CSS pixel value.');
102
+ }
103
+ const pixels = Number(value.slice(0, -2));
104
+ if (!Number.isFinite(pixels) || pixels < 24 || pixels > 48) {
105
+ throw new RangeError('config.ui.controlSize must be between 24px and 48px.');
106
+ }
107
+ return `${pixels}px`;
108
+ }
109
+
110
+ export function resolveStudioControlMetrics(controlSize) {
111
+ const normalized = normalizeStudioControlSize(controlSize);
112
+ const height = CONTROL_SIZE_PRESETS[normalized] || Number(normalized.slice(0, -2));
113
+ const clamp = (value, minimum, maximum) => Math.min(maximum, Math.max(minimum, value));
114
+ const pixels = (value) => `${Number(value.toFixed(2))}px`;
115
+ return Object.freeze({
116
+ controlSize: normalized,
117
+ controlHeight: pixels(height),
118
+ headerHeight: pixels(height + 26),
119
+ modeHeight: pixels(height + 4),
120
+ modeButtonHeight: pixels(height - 4),
121
+ projectionButtonHeight: pixels(height - 6),
122
+ iconButtonSize: pixels(height - 1),
123
+ splitCaretWidth: pixels(height - 5),
124
+ fontSize: pixels(clamp(height * 0.25 + 4, 11, 14)),
125
+ paddingInline: pixels(clamp((height - 12) / 2, 8, 14)),
126
+ modePaddingInline: pixels(clamp(height / 2 - 4, 10, 14)),
127
+ iconSize: pixels(clamp(height / 2 - 2, 12, 18)),
128
+ radius: pixels(clamp(height / 4 - 1, 6, 9)),
129
+ });
130
+ }
131
+
132
+ export function normalizeStudioRegions(regions, current = DEFAULT_STUDIO_SHELL_REGIONS) {
133
+ if (regions === undefined || regions === null) return { ...current };
134
+ assertObject(regions, 'config.ui.regions');
135
+ for (const name of Object.keys(regions)) {
136
+ if (!STUDIO_SHELL_REGIONS.includes(name)) throw new Error(`Unknown Studio Shell region: ${name}.`);
137
+ if (!['default', 'hidden'].includes(regions[name])) {
138
+ throw new Error(`Studio Shell region "${name}" must be "default" or "hidden".`);
139
+ }
140
+ }
141
+ return { ...current, ...regions };
142
+ }
143
+
144
+ function normalizeProjectionOptions(value) {
145
+ if (value === undefined || value === null) return undefined;
146
+ if (!Array.isArray(value)) throw new TypeError('config.viewer.projectionOptions must be a readonly array.');
147
+ const seen = new Set();
148
+ const options = value.map((item, index) => {
149
+ assertObject(item, `config.viewer.projectionOptions[${index}]`);
150
+ if (!STUDIO_PROJECTIONS.includes(item.value)) {
151
+ throw new TypeError(`config.viewer.projectionOptions[${index}].value must be auto, standard, approval, or compact.`);
152
+ }
153
+ if (seen.has(item.value)) throw new TypeError(`config.viewer.projectionOptions contains duplicate value "${item.value}".`);
154
+ if (typeof item.label !== 'string' || !item.label.trim()) {
155
+ throw new TypeError(`config.viewer.projectionOptions[${index}].label must be a non-empty string.`);
156
+ }
157
+ seen.add(item.value);
158
+ return Object.freeze({ value: item.value, label: item.label });
159
+ });
160
+ return Object.freeze(options);
161
+ }
162
+
163
+ function normalizeStudioConfigInput(config) {
164
+ if (config === undefined) return {};
165
+ return assertObject(config, 'config');
166
+ }
167
+
168
+ function assignPath(target, section, key, value) {
169
+ if (!target[section]) target[section] = {};
170
+ target[section][key] = value;
171
+ }
172
+
173
+ function applyKnownConfig(target, source) {
174
+ if (!source) return;
175
+ const config = normalizeStudioConfigInput(source);
176
+ for (const section of ['modeling', 'ui', 'viewer']) {
177
+ if (config[section] !== undefined) target[section] = { ...(target[section] || {}), ...assertObject(config[section], `config.${section}`) };
178
+ }
179
+ if (hasOwn(config, 'export')) target.export = config.export;
180
+ }
181
+
182
+ function applyLegacyConfig(target, legacy) {
183
+ if (!legacy) return;
184
+ for (const [key, path] of Object.entries({
185
+ propertiesProfile: ['modeling', 'propertiesProfile'],
186
+ allowedNodeTypes: ['modeling', 'allowedNodeTypes'],
187
+ allowedEdgeTypes: ['modeling', 'allowedEdgeTypes'],
188
+ regions: ['ui', 'regions'],
189
+ leftWidth: ['ui', 'leftWidth'],
190
+ rightWidth: ['ui', 'rightWidth'],
191
+ responsive: ['viewer', 'responsive'],
192
+ projectionOptions: ['viewer', 'projectionOptions'],
193
+ timeline: ['viewer', 'timeline'],
194
+ runtimeDetails: ['viewer', 'runtimeDetails'],
195
+ runtimeTraceOptions: ['viewer', 'runtimeTraceOptions'],
196
+ })) {
197
+ if (hasOwn(legacy, key) && legacy[key] !== undefined) assignPath(target, path[0], path[1], legacy[key]);
198
+ }
199
+ if (hasOwn(legacy, 'svgExport') && legacy.svgExport !== undefined) target.export = legacy.svgExport;
200
+ }
201
+
202
+ export function normalizeStudioConfig(config, { fallback = null, legacy = null } = {}) {
203
+ const merged = {};
204
+ applyKnownConfig(merged, fallback);
205
+ applyKnownConfig(merged, config);
206
+ applyLegacyConfig(merged, legacy);
207
+
208
+ const modelingInput = merged.modeling === undefined ? {} : assertObject(merged.modeling, 'config.modeling');
209
+ const uiInput = merged.ui === undefined ? {} : assertObject(merged.ui, 'config.ui');
210
+ const viewerInput = merged.viewer === undefined ? {} : assertObject(merged.viewer, 'config.viewer');
211
+ const sidebarInput = uiInput.sidebarWidth === undefined
212
+ ? {}
213
+ : assertObject(uiInput.sidebarWidth, 'config.ui.sidebarWidth');
214
+ const leftWidth = hasOwn(uiInput, 'leftWidth') ? uiInput.leftWidth : sidebarInput.left;
215
+ const rightWidth = hasOwn(uiInput, 'rightWidth') ? uiInput.rightWidth : sidebarInput.right;
216
+ const controlSize = normalizeStudioControlSize(uiInput.controlSize);
217
+ const allowedNodeTypes = optionalTypeList(modelingInput.allowedNodeTypes, 'config.modeling.allowedNodeTypes');
218
+ const allowedEdgeTypes = optionalTypeList(modelingInput.allowedEdgeTypes, 'config.modeling.allowedEdgeTypes');
219
+ const responsive = viewerInput.responsive === undefined ? false : viewerInput.responsive;
220
+ if (typeof responsive !== 'boolean') throw new TypeError('config.viewer.responsive must be a boolean.');
221
+
222
+ const modeling = {
223
+ propertiesProfile: normalizePropertiesProfile(modelingInput.propertiesProfile),
224
+ ...(allowedNodeTypes ? { allowedNodeTypes } : {}),
225
+ ...(allowedEdgeTypes ? { allowedEdgeTypes } : {}),
226
+ };
227
+ const ui = {
228
+ controlSize,
229
+ // Keep omitted regions omitted: an explicit right='default' opts into the
230
+ // Instance sidebar, whereas the mode default must survive get/setConfig.
231
+ regions: Object.freeze(normalizeStudioRegions(uiInput.regions, {})),
232
+ leftPanel: normalizeSidebarConfig(uiInput.leftPanel, 'left'),
233
+ rightPanel: normalizeSidebarConfig(uiInput.rightPanel, 'right'),
234
+ sidebarWidth: Object.freeze({
235
+ left: normalizeSidebarWidth(leftWidth, 244, 'config.ui.sidebarWidth.left'),
236
+ right: normalizeSidebarWidth(rightWidth, 360, 'config.ui.sidebarWidth.right'),
237
+ }),
238
+ };
239
+ const projectionOptions = normalizeProjectionOptions(viewerInput.projectionOptions);
240
+ const timeline = optionalObject(viewerInput.timeline, 'config.viewer.timeline');
241
+ const runtimeDetails = optionalObject(viewerInput.runtimeDetails, 'config.viewer.runtimeDetails');
242
+ const runtimeTraceOptions = optionalObject(viewerInput.runtimeTraceOptions, 'config.viewer.runtimeTraceOptions');
243
+ const viewer = {
244
+ responsive,
245
+ ...(projectionOptions ? { projectionOptions } : {}),
246
+ ...(timeline ? { timeline } : {}),
247
+ ...(runtimeDetails ? { runtimeDetails } : {}),
248
+ ...(runtimeTraceOptions ? { runtimeTraceOptions } : {}),
249
+ };
250
+ const exportOptions = merged.export === undefined || merged.export === null
251
+ ? undefined
252
+ : optionalObject(merged.export, 'config.export');
253
+
254
+ return Object.freeze({
255
+ modeling: Object.freeze(modeling),
256
+ ui: Object.freeze(ui),
257
+ viewer: Object.freeze(viewer),
258
+ ...(exportOptions ? { export: exportOptions } : {}),
259
+ });
260
+ }
261
+
262
+ export function createStudioUiContext(getMetrics) {
263
+ return Object.freeze({
264
+ get controlSize() { return getMetrics().controlSize; },
265
+ get controlHeight() { return getMetrics().controlHeight; },
266
+ });
267
+ }
268
+
269
+ export function applyStudioControlMetrics(root, metrics) {
270
+ const properties = {
271
+ '--nova-control-height': metrics.controlHeight,
272
+ '--nova-header-height': metrics.headerHeight,
273
+ '--nova-control-font-size': metrics.fontSize,
274
+ '--nova-control-padding-inline': metrics.paddingInline,
275
+ '--nova-mode-control-padding-inline': metrics.modePaddingInline,
276
+ '--nova-control-icon-size': metrics.iconSize,
277
+ '--nova-control-radius': metrics.radius,
278
+ '--nova-mode-control-height': metrics.modeHeight,
279
+ '--nova-mode-button-height': metrics.modeButtonHeight,
280
+ '--nova-projection-button-height': metrics.projectionButtonHeight,
281
+ '--nova-icon-button-size': metrics.iconButtonSize,
282
+ '--nova-split-caret-width': metrics.splitCaretWidth,
283
+ };
284
+ for (const [name, value] of Object.entries(properties)) root.style.setProperty(name, value);
285
+ }
@@ -36,6 +36,7 @@ import {
36
36
  selectedLayoutNodes,
37
37
  selectionBounds,
38
38
  } from './selection-layout.js';
39
+ import { normalizeStudioConfig } from './config.js';
39
40
 
40
41
  const SEQUENCE_FLOW_KINDS = new Set(['event', 'boundary', 'task', 'container', 'gateway']);
41
42
 
@@ -86,17 +87,31 @@ function normalizeAllowedEdgeTypes(allowedEdgeTypes) {
86
87
  }
87
88
 
88
89
  export class BpmnStudioController {
89
- constructor({ model, historyLimit = 80, extensions = [], propertiesProfile = 'business', allowedNodeTypes = null, allowedEdgeTypes = null } = {}) {
90
+ constructor(options = {}) {
91
+ const {
92
+ model,
93
+ historyLimit = 80,
94
+ extensions = [],
95
+ propertiesProfile,
96
+ allowedNodeTypes,
97
+ allowedEdgeTypes,
98
+ config,
99
+ } = options;
90
100
  if (!model) throw new Error('BpmnStudioController requires a model.');
91
- this._allowedNodeTypes = normalizeAllowedNodeTypes(allowedNodeTypes);
92
- this._allowedEdgeTypes = normalizeAllowedEdgeTypes(allowedEdgeTypes);
101
+ const legacy = {};
102
+ if (Object.prototype.hasOwnProperty.call(options, 'propertiesProfile')) legacy.propertiesProfile = propertiesProfile;
103
+ if (Object.prototype.hasOwnProperty.call(options, 'allowedNodeTypes')) legacy.allowedNodeTypes = allowedNodeTypes;
104
+ if (Object.prototype.hasOwnProperty.call(options, 'allowedEdgeTypes')) legacy.allowedEdgeTypes = allowedEdgeTypes;
105
+ const normalizedConfig = normalizeStudioConfig(config, { legacy });
106
+ this._allowedNodeTypes = normalizeAllowedNodeTypes(normalizedConfig.modeling.allowedNodeTypes);
107
+ this._allowedEdgeTypes = normalizeAllowedEdgeTypes(normalizedConfig.modeling.allowedEdgeTypes);
93
108
  this._assertAllowedModel(model);
94
109
  this._model = model;
95
110
  this.history = new HistoryStack(historyLimit);
96
111
  this.selection = { kind: 'process', id: model.id };
97
112
  this.activeScopeId = model.id;
98
113
  this.connectingSource = null;
99
- this.propertiesProfile = propertiesProfile;
114
+ this.propertiesProfile = normalizedConfig.modeling.propertiesProfile;
100
115
  this._listeners = new Set();
101
116
  this._extensions = new Map();
102
117
  applyContainmentOperation(this._model, { type: 'reconcile' });
package/dist/index.d.ts CHANGED
@@ -178,7 +178,7 @@ export class BpmnViewer {
178
178
  setModel(model: ProcessModel): void
179
179
  setRuntime(runtime: ProcessInstanceSnapshot | null): void
180
180
  refreshPresentation(): void
181
- setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: import('./modules/viewer/index.js').RuntimeAssetResolver | null }): void
181
+ setDisplayOptions(options: { timeline?: ViewerTimelineOptions; runtimeDetails?: RuntimeDetailsOptions; runtimeTraceOptions?: RuntimeTraceProjectionOptions; runtimeAssetResolver?: import('./modules/viewer/index.js').RuntimeAssetResolver | null; replace?: boolean }): void
182
182
  setProjection(projection: ViewerProjection): void
183
183
  setTheme(theme: NovaThemeInput): NovaThemeState
184
184
  setThemeMode(mode: NovaThemeMode): NovaThemeState
@@ -231,12 +231,64 @@ export interface StudioExtension {
231
231
  id: string
232
232
  setup(context: { studio: BpmnStudioController }): void | (() => void)
233
233
  }
234
+ export type StudioControlSize = 'small' | 'medium' | 'large' | `${number}px`
235
+ export interface StudioModelingConfig {
236
+ propertiesProfile?: 'business' | 'developer'
237
+ allowedNodeTypes?: readonly NodeType[]
238
+ allowedEdgeTypes?: readonly EdgeType[]
239
+ }
240
+ export interface StudioSidebarConfig {
241
+ /** Defaults to true. Disabling collapse keeps an available sidebar expanded. */
242
+ collapsible?: boolean
243
+ /** Initial wide-container state only; later config updates do not reset user choices. */
244
+ defaultCollapsed?: boolean
245
+ }
246
+ export interface StudioRightPanelConfig extends StudioSidebarConfig {
247
+ /** Defaults to flex. In scroll mode Nova scrolls the entire supplied content. */
248
+ layout?: 'flex' | 'scroll'
249
+ }
250
+ export interface StudioUiConfig {
251
+ controlSize?: StudioControlSize
252
+ regions?: StudioShellRegions
253
+ leftPanel?: StudioSidebarConfig
254
+ rightPanel?: StudioRightPanelConfig
255
+ sidebarWidth?: {
256
+ left?: number
257
+ right?: number
258
+ }
259
+ }
260
+ export interface StudioViewerConfig {
261
+ responsive?: boolean
262
+ projectionOptions?: readonly { value: ViewerProjection; label: string }[]
263
+ timeline?: ViewerTimelineOptions
264
+ runtimeDetails?: RuntimeDetailsOptions
265
+ runtimeTraceOptions?: RuntimeTraceProjectionOptions
266
+ }
267
+ export type StudioSvgExportOptions = SvgExportOptions & {
268
+ label?: string
269
+ nodeRenderers?: Record<string, SvgNodeRenderer>
270
+ runtimeTimelineRenderer?: RuntimeTimelineSvgRenderer
271
+ }
272
+ export interface BpmnStudioConfig {
273
+ modeling?: StudioModelingConfig
274
+ ui?: StudioUiConfig
275
+ viewer?: StudioViewerConfig
276
+ export?: StudioSvgExportOptions
277
+ }
278
+ export interface StudioUiContext {
279
+ readonly controlSize: StudioControlSize
280
+ readonly controlHeight: `${number}px`
281
+ }
234
282
  export interface StudioControllerOptions {
235
283
  model: ProcessModel
236
284
  historyLimit?: number
237
285
  extensions?: StudioExtension[]
286
+ config?: BpmnStudioConfig
287
+ /** @deprecated Use config.modeling.propertiesProfile. */
238
288
  propertiesProfile?: 'business' | 'developer'
289
+ /** @deprecated Use config.modeling.allowedNodeTypes. */
239
290
  allowedNodeTypes?: Iterable<NodeType> | null
291
+ /** @deprecated Use config.modeling.allowedEdgeTypes. */
240
292
  allowedEdgeTypes?: Iterable<EdgeType> | null
241
293
  }
242
294
  export interface StudioCommands {
@@ -466,6 +518,25 @@ export interface StudioShellActions {
466
518
  }): SvgExportPreviewController | null
467
519
  }
468
520
  export type StudioMode = 'design' | 'viewer' | 'instance'
521
+ export type StudioSidebarSide = 'left' | 'right'
522
+ export type StudioSidebarState = Readonly<Record<StudioSidebarSide, {
523
+ readonly collapsed: boolean
524
+ readonly hidden: boolean
525
+ readonly collapsible: boolean
526
+ }>>
527
+ export type StudioSidebarChangeSource = 'button' | 'api' | 'responsive' | 'config'
528
+ export interface StudioSidebarChangeEvent {
529
+ readonly side: StudioSidebarSide
530
+ readonly collapsed: boolean
531
+ readonly previousCollapsed: boolean
532
+ readonly mode: StudioMode
533
+ readonly source: StudioSidebarChangeSource
534
+ }
535
+ export interface StudioPanelSelection {
536
+ readonly selection: ElementSelection | null
537
+ readonly selectedElement: StudioState['selectedElement']
538
+ readonly trace: RuntimeTraceClickEvent | null
539
+ }
469
540
  export type StudioModeChangeSource = 'toolbar' | 'api' | 'allowed-modes'
470
541
  export interface StudioModeChangeEvent {
471
542
  readonly mode: StudioMode
@@ -486,12 +557,18 @@ export interface StudioShellSlotContext {
486
557
  shell: BpmnStudioShell
487
558
  canvas: BpmnCanvas
488
559
  actions: StudioShellActions
560
+ readonly ui: StudioUiContext
489
561
  getState(): StudioState
490
562
  subscribe(listener: (event: StudioEvent) => void): () => void
491
563
  getMode(): StudioMode
492
564
  getAllowedModes(): readonly StudioMode[]
493
565
  subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
494
566
  subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
567
+ getSidebarState(): StudioSidebarState
568
+ setSidebarCollapsed(side: StudioSidebarSide, collapsed: boolean): boolean
569
+ subscribeSidebarChange(listener: (event: StudioSidebarChangeEvent) => void): () => void
570
+ getPanelSelection(): StudioPanelSelection
571
+ subscribePanelSelection(listener: (selection: StudioPanelSelection) => void): () => void
495
572
  }
496
573
  export type StudioShellDomSlot =
497
574
  | HTMLElement
@@ -526,6 +603,7 @@ export interface StudioShellRegionState {
526
603
  export interface StudioShellOptions {
527
604
  container: HTMLElement
528
605
  studio: BpmnStudioController
606
+ config?: BpmnStudioConfig
529
607
  iconRegistry?: IconRegistry
530
608
  paletteRegistry?: PaletteRegistry
531
609
  propertiesRegistry?: PropertiesRegistry
@@ -534,33 +612,54 @@ export interface StudioShellOptions {
534
612
  rendererOptions?: DiagramRendererOptions & {
535
613
  runtimeTraceOptions?: RuntimeTraceProjectionOptions
536
614
  timeline?: ViewerTimelineOptions
615
+ runtimeDetails?: RuntimeDetailsOptions
537
616
  }
538
617
  nodeSubtitleResolver?: NodeSubtitleResolver
539
618
  slots?: StudioShellSlots
619
+ /** @deprecated Use config.ui.regions. */
540
620
  regions?: StudioShellRegions
541
621
  layout?: (context: Record<string, unknown>) => void | (() => void)
542
622
  runtime?: ProcessInstanceSnapshot | null
543
623
  mode?: StudioMode
544
624
  allowedModes?: readonly StudioMode[]
545
625
  projection?: ViewerProjection
626
+ /** @deprecated Use config.viewer.responsive. */
546
627
  responsive?: boolean
628
+ /** @deprecated Use config.viewer.projectionOptions. */
547
629
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
630
+ /** @deprecated Use config.ui.sidebarWidth.left. */
548
631
  leftWidth?: number
632
+ /** @deprecated Use config.ui.sidebarWidth.right. */
549
633
  rightWidth?: number
634
+ /** @deprecated Use config.viewer.timeline. */
635
+ timeline?: ViewerTimelineOptions
636
+ /** @deprecated Use config.viewer.runtimeDetails. */
637
+ runtimeDetails?: RuntimeDetailsOptions
638
+ /** @deprecated Use config.viewer.runtimeTraceOptions. */
639
+ runtimeTraceOptions?: RuntimeTraceProjectionOptions
550
640
  theme?: NovaThemeInput
551
641
  runtimeAppearance?: RuntimeAppearanceOptions
552
- svgExport?: SvgExportOptions & { label?: string; nodeRenderers?: Record<string, SvgNodeRenderer>; runtimeTimelineRenderer?: RuntimeTimelineSvgRenderer }
642
+ /** @deprecated Use config.export. */
643
+ svgExport?: StudioSvgExportOptions
553
644
  onThemeChange?: (state: NovaThemeState) => void
554
645
  }
555
646
  export class BpmnStudioShell {
556
647
  constructor(options: StudioShellOptions)
557
648
  readonly actions: StudioShellActions
649
+ readonly ui: StudioUiContext
558
650
  canvas?: BpmnCanvas
559
651
  viewer?: BpmnViewer
560
652
  getMode(): StudioMode
561
653
  getAllowedModes(): readonly StudioMode[]
562
654
  subscribeMode(listener: (event: StudioModeChangeEvent) => void): () => void
563
655
  subscribeValidation(listener: (event: StudioValidationEvent) => void): () => void
656
+ getSidebarState(): StudioSidebarState
657
+ setSidebarCollapsed(side: StudioSidebarSide, collapsed: boolean): boolean
658
+ subscribeSidebarChange(listener: (event: StudioSidebarChangeEvent) => void): () => void
659
+ getPanelSelection(): StudioPanelSelection
660
+ subscribePanelSelection(listener: (selection: StudioPanelSelection) => void): () => void
661
+ getConfig(): Readonly<BpmnStudioConfig>
662
+ setConfig(config?: BpmnStudioConfig): Readonly<BpmnStudioConfig>
564
663
  setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
565
664
  getRegions(): Readonly<StudioShellRegionState>
566
665
  setRegions(regions: StudioShellRegions): Readonly<StudioShellRegionState>