@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 +75 -6
- package/dist/config.js +285 -0
- package/dist/controller.js +19 -4
- package/dist/index.d.ts +101 -2
- package/dist/modules/export-svg/render.js +82 -62
- package/dist/modules/node-geometry/index.d.ts +19 -0
- package/dist/modules/node-geometry/index.js +147 -0
- package/dist/modules/renderer-svg/index.js +63 -25
- package/dist/modules/viewer/index.d.ts +1 -1
- package/dist/modules/viewer/index.js +86 -35
- package/dist/panel-selection.js +60 -0
- package/dist/shell.js +315 -87
- package/dist/sidebars.js +195 -0
- package/dist/styles.css +72 -12
- package/llms-full.txt +1614 -93
- package/llms.txt +76 -15
- package/package.json +1 -1
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.
|
|
7
|
+
> 当前版本为 `0.3.5-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
|
|
8
8
|
|
|
9
9
|

|
|
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
|
-
|
|
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`
|
|
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
|
+
}
|
package/dist/controller.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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>
|