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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.1-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
7
+ > 当前版本为 `0.3.2-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
 
@@ -27,6 +27,8 @@ import '@bpmn-nova/studio/styles.css'
27
27
 
28
28
  BPMN Nova 不会在 JavaScript 中隐式注入 CSS。视觉容器必须有明确高度。
29
29
 
30
+ 使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
31
+
30
32
  ## 流程设计
31
33
 
32
34
  ```html
@@ -45,10 +47,16 @@ const model = createEmptyProcess('flowable')
45
47
  model.id = 'Process_PurchaseApproval'
46
48
  model.name = '采购申请审批流程'
47
49
 
48
- const studio = createStudioController({ model })
50
+ const studio = createStudioController({
51
+ model,
52
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
53
+ allowedEdgeTypes: ['sequenceFlow'],
54
+ })
49
55
  const shell = createStudioShell({
50
56
  container: document.querySelector('#studio'),
51
57
  studio,
58
+ mode: 'design',
59
+ allowedModes: ['design'],
52
60
  theme: 'auto',
53
61
  })
54
62
 
@@ -59,6 +67,40 @@ shell.destroy()
59
67
  studio.destroy()
60
68
  ```
61
69
 
70
+ `allowedNodeTypes` 与 `allowedEdgeTypes` 同时约束 XML 导入、Palette、连接、快捷新增、模板和节点类型转换;不传时开放 Nova 当前支持的全部图元。`allowedModes` 控制默认工作台显示和允许切换的模式。不传 Runtime 时实例模式保持空运行事实,不会注入演示审批数据。
71
+
72
+ ## 嵌入宿主工作台
73
+
74
+ 默认 `BpmnStudioShell` 仍是包含 Header、Palette、Canvas、Properties 和 Statusbar 的完整工作台。宿主要复用自己的业务属性面板时,应显式隐藏 Nova 右侧区域,并把业务面板作为 Nova 根节点的外部兄弟区域:
75
+
76
+ ```js
77
+ const shell = createStudioShell({
78
+ container: novaHost,
79
+ studio,
80
+ mode: 'design',
81
+ allowedModes: ['design'],
82
+ regions: { right: 'hidden' },
83
+ slots: {
84
+ headerStart({ container, actions, getState, subscribe }) {
85
+ // 在 Nova Header 左侧挂载宿主的返回入口、业务图标、流程名称和类型。
86
+ // 完整替换 Header 时改用 slots.header,并继续调用 actions。
87
+ },
88
+ },
89
+ theme: 'auto',
90
+ })
91
+
92
+ shell.setRegions({ right: 'default' })
93
+ shell.actions.undo()
94
+ shell.actions.redo()
95
+ shell.actions.fitView()
96
+ const issues = shell.actions.validate()
97
+ const xml = shell.actions.exportXml('flowable')
98
+ ```
99
+
100
+ `regions` 支持 `header`、`left`、`right`、`footer` 的 `default | hidden` 状态;隐藏区域不占布局轨道。`setRegions()` 原地更新默认 Shell,不重建 Controller 或 Canvas。`layout()` 是完整布局替换,不能与 `regions` 同时使用。
101
+
102
+ `shell.actions` 只封装撤销、重做、布局、视图、校验和 BPMN/SVG 导入导出。宿主的保存草稿、发布、权限、文件选择和服务端事务不属于 Nova Actions。
103
+
62
104
  ## 只读 Viewer
63
105
 
64
106
  ```js
@@ -227,12 +269,12 @@ import { activitiProfile } from '@bpmn-nova/studio/activiti'
227
269
 
228
270
  ## 文档与 AI
229
271
 
272
+ - npm 包内 `llms.txt`:可执行安装入口
273
+ - npm 包内 `llms-full.txt`:完整 AI 上下文
230
274
  - [项目首页](https://github.com/daxiangme/bpmn-nova)
231
275
  - [快速开始](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/GETTING-STARTED.md)
232
276
  - [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
233
277
  - [自定义指南](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/CUSTOMIZATION.md)
234
- - [AI 接入入口](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms.txt)
235
- - [AI 完整上下文](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms-full.txt)
236
278
 
237
279
  ## 能力边界
238
280
 
package/dist/canvas.js CHANGED
@@ -80,6 +80,7 @@ export class BpmnCanvas {
80
80
  },
81
81
  onStartConnect: (node) => studio.startConnect(node.id),
82
82
  canConnect: (source, target) => canCreateSequenceFlow(studio.model, source.id, target.id),
83
+ canCreateNodeType: (nodeType) => studio.allowsNodeType(nodeType),
83
84
  onQuickAdd: (source, type, preset) => studio.commands.quickAdd(source.id, type, preset),
84
85
  onDeleteNode: (node) => studio.commands.remove({ kind: 'node', id: node.id }),
85
86
  onUngroup: (node) => studio.commands.ungroup(node.id),
@@ -521,7 +522,7 @@ export class BpmnCanvas {
521
522
  { id: 'distribute-vertical', iconId: 'ui.distributeVertical', title: '垂直等距', action: () => actions.distribute('vertical'), disabled: nodes.length < 3 },
522
523
  ];
523
524
  this._selectionToolbar.append(
524
- button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
525
+ button('group', 'ui.layoutSelection', '创建分组', actions.group, { label: '创建分组', disabled: this.studio.allowsNodeType?.('group') === false || !groupableNodes.length || groupableNodes.length !== diagramNodes.length }),
525
526
  separator(),
526
527
  menu('align', 'ui.alignCenterHorizontal', '对齐选区', alignmentItems, nodes.length < 2),
527
528
  menu('distribute', 'ui.distributeHorizontal', '等距分布', distributeItems, nodes.length < 3),
@@ -104,7 +104,7 @@ function multiActions(context) {
104
104
  execute: ({ studio }) => studio.arrangeSelection({ type: 'align', alignment }),
105
105
  }));
106
106
  actions.push(
107
- action({ id: 'selection.group', label: '创建分组', iconId: 'ui.layoutSelection', group: 'layout', disabled: !groupableNodes.length || groupableNodes.length !== diagramNodes.length, execute: ({ studio }) => studio.commands.groupSelection() }),
107
+ action({ id: 'selection.group', label: '创建分组', iconId: 'ui.layoutSelection', group: 'layout', disabled: context.studio.allowsNodeType?.('group') === false || !groupableNodes.length || groupableNodes.length !== diagramNodes.length, execute: ({ studio }) => studio.commands.groupSelection() }),
108
108
  action({ id: 'selection.distribute.horizontal', label: '水平等距', iconId: 'ui.distributeHorizontal', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'horizontal' }) }),
109
109
  action({ id: 'selection.distribute.vertical', label: '垂直等距', iconId: 'ui.distributeVertical', group: 'distribute', disabled: nodes.length < 3, execute: ({ studio }) => studio.arrangeSelection({ type: 'distribute', axis: 'vertical' }) }),
110
110
  action({ id: 'selection.layout', label: '美化选区', iconId: 'ui.layoutSelection', group: 'layout', disabled: nodes.length < 2, execute: ({ studio }) => studio.arrangeSelection({ type: 'layout' }) }),
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  applyContainmentOperation,
3
3
  CONTAINMENT_LIMITS,
4
+ EDGE_DEFINITIONS,
4
5
  HistoryStack,
5
6
  LAYOUT_DENSITIES,
6
7
  NODE_DEFINITIONS,
@@ -58,9 +59,38 @@ function mergeNodePreset(preset = {}) {
58
59
  };
59
60
  }
60
61
 
62
+ function normalizeAllowedNodeTypes(allowedNodeTypes) {
63
+ if (allowedNodeTypes === undefined || allowedNodeTypes === null) return null;
64
+ if (typeof allowedNodeTypes === 'string' || typeof allowedNodeTypes[Symbol.iterator] !== 'function') {
65
+ throw new Error('allowedNodeTypes must be an iterable of BPMN node types.');
66
+ }
67
+ const normalized = new Set(allowedNodeTypes);
68
+ for (const nodeType of normalized) {
69
+ if (!NODE_DEFINITIONS[nodeType] || nodeType === 'generic') {
70
+ throw new Error(`Unknown allowed BPMN node type: ${nodeType}`);
71
+ }
72
+ }
73
+ return normalized;
74
+ }
75
+
76
+ function normalizeAllowedEdgeTypes(allowedEdgeTypes) {
77
+ if (allowedEdgeTypes === undefined || allowedEdgeTypes === null) return null;
78
+ if (typeof allowedEdgeTypes === 'string' || typeof allowedEdgeTypes[Symbol.iterator] !== 'function') {
79
+ throw new Error('allowedEdgeTypes must be an iterable of BPMN edge types.');
80
+ }
81
+ const normalized = new Set(allowedEdgeTypes);
82
+ for (const edgeType of normalized) {
83
+ if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown allowed BPMN edge type: ${edgeType}`);
84
+ }
85
+ return normalized;
86
+ }
87
+
61
88
  export class BpmnStudioController {
62
- constructor({ model, historyLimit = 80, extensions = [], propertiesProfile = 'business' } = {}) {
89
+ constructor({ model, historyLimit = 80, extensions = [], propertiesProfile = 'business', allowedNodeTypes = null, allowedEdgeTypes = null } = {}) {
63
90
  if (!model) throw new Error('BpmnStudioController requires a model.');
91
+ this._allowedNodeTypes = normalizeAllowedNodeTypes(allowedNodeTypes);
92
+ this._allowedEdgeTypes = normalizeAllowedEdgeTypes(allowedEdgeTypes);
93
+ this._assertAllowedModel(model);
64
94
  this._model = model;
65
95
  this.history = new HistoryStack(historyLimit);
66
96
  this.selection = { kind: 'process', id: model.id };
@@ -108,6 +138,32 @@ export class BpmnStudioController {
108
138
 
109
139
  get model() { return this._model; }
110
140
 
141
+ allowsNodeType(nodeType) {
142
+ return Boolean(NODE_DEFINITIONS[nodeType])
143
+ && nodeType !== 'generic'
144
+ && (!this._allowedNodeTypes || this._allowedNodeTypes.has(nodeType));
145
+ }
146
+
147
+ allowsEdgeType(edgeType) {
148
+ return Boolean(EDGE_DEFINITIONS[edgeType])
149
+ && (!this._allowedEdgeTypes || this._allowedEdgeTypes.has(edgeType));
150
+ }
151
+
152
+ _assertAllowedModel(model) {
153
+ if (this._allowedNodeTypes) {
154
+ const unsupportedNode = model?.nodes?.find((node) => !this._allowedNodeTypes.has(node.type));
155
+ if (unsupportedNode) {
156
+ throw new Error(`BPMN node type is not allowed: ${unsupportedNode.type} (${unsupportedNode.id})`);
157
+ }
158
+ }
159
+ if (this._allowedEdgeTypes) {
160
+ const unsupportedEdge = model?.edges?.find((edge) => !this._allowedEdgeTypes.has(edge.type || 'sequenceFlow'));
161
+ if (unsupportedEdge) {
162
+ throw new Error(`BPMN edge type is not allowed: ${unsupportedEdge.type || 'sequenceFlow'} (${unsupportedEdge.id})`);
163
+ }
164
+ }
165
+ }
166
+
111
167
  getState() {
112
168
  return {
113
169
  model: cloneModel(this._model),
@@ -118,6 +174,8 @@ export class BpmnStudioController {
118
174
  canRedo: this.history.canRedo,
119
175
  engine: this._model.engine,
120
176
  propertiesProfile: this.propertiesProfile,
177
+ allowedNodeTypes: this._allowedNodeTypes ? [...this._allowedNodeTypes] : null,
178
+ allowedEdgeTypes: this._allowedEdgeTypes ? [...this._allowedEdgeTypes] : null,
121
179
  activeScopeId: this.activeScopeId,
122
180
  scopePath: getScopePath(this._model, this.activeScopeId),
123
181
  };
@@ -143,6 +201,7 @@ export class BpmnStudioController {
143
201
  }
144
202
 
145
203
  setModel(model, { resetHistory = true } = {}) {
204
+ this._assertAllowedModel(model);
146
205
  const previousScopeId = this.activeScopeId;
147
206
  this._model = model;
148
207
  applyContainmentOperation(this._model, { type: 'reconcile' });
@@ -192,6 +251,7 @@ export class BpmnStudioController {
192
251
  }
193
252
 
194
253
  groupSelection() {
254
+ if (!this.allowsNodeType('group')) return null;
195
255
  const selected = selectedDiagramNodes(this._model, this.selection, this.activeScopeId);
196
256
  const groupable = selectedGroupableNodes(this._model, this.selection, this.activeScopeId);
197
257
  if (!groupable.length || groupable.length !== selected.length) return null;
@@ -303,6 +363,7 @@ export class BpmnStudioController {
303
363
  createNode({ nodeType, point, preset = {} } = {}) {
304
364
  const def = NODE_DEFINITIONS[nodeType];
305
365
  if (!def) throw new Error(`Unknown BPMN node type: ${nodeType}`);
366
+ if (!this.allowsNodeType(nodeType)) throw new Error(`BPMN node type is not allowed: ${nodeType}`);
306
367
  const position = point || { x: 100, y: 100 };
307
368
  let node;
308
369
  this._commit('create-node', () => {
@@ -331,9 +392,15 @@ export class BpmnStudioController {
331
392
  for (const spec of nodeSpecs) {
332
393
  if (!spec.key || keys.has(spec.key)) throw new Error('Template node keys must be unique.');
333
394
  if (!NODE_DEFINITIONS[spec.nodeType]) throw new Error(`Unknown BPMN node type: ${spec.nodeType}`);
395
+ if (!this.allowsNodeType(spec.nodeType)) throw new Error(`BPMN node type is not allowed: ${spec.nodeType}`);
334
396
  keys.add(spec.key);
335
397
  }
336
- for (const edge of template.edges || []) if (!keys.has(edge.source) || !keys.has(edge.target)) throw new Error('Template edge references an unknown node key.');
398
+ for (const edge of template.edges || []) {
399
+ if (!keys.has(edge.source) || !keys.has(edge.target)) throw new Error('Template edge references an unknown node key.');
400
+ const edgeType = edge.preset?.type || 'sequenceFlow';
401
+ if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown BPMN edge type: ${edgeType}`);
402
+ if (!this.allowsEdgeType(edgeType)) throw new Error(`BPMN edge type is not allowed: ${edgeType}`);
403
+ }
337
404
  const result = { nodes: [], edges: [] };
338
405
  const byKey = new Map();
339
406
  this._commit('create-template', () => {
@@ -355,6 +422,9 @@ export class BpmnStudioController {
355
422
  }
356
423
 
357
424
  connect(source, target, overrides = {}) {
425
+ const edgeType = overrides.type || 'sequenceFlow';
426
+ if (!EDGE_DEFINITIONS[edgeType]) throw new Error(`Unknown BPMN edge type: ${edgeType}`);
427
+ if (!this.allowsEdgeType(edgeType)) throw new Error(`BPMN edge type is not allowed: ${edgeType}`);
358
428
  if (!canCreateSequenceFlow(this._model, source, target)) return null;
359
429
  let edge;
360
430
  this._commit('connect', () => {
@@ -366,6 +436,7 @@ export class BpmnStudioController {
366
436
  }
367
437
 
368
438
  startConnect(sourceId) {
439
+ if (!this.allowsEdgeType('sequenceFlow')) return false;
369
440
  const source = getNode(this._model, sourceId);
370
441
  const definition = source && (NODE_DEFINITIONS[source.type] || NODE_DEFINITIONS.generic);
371
442
  if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !SEQUENCE_FLOW_KINDS.has(definition.kind) || definition.eventStage === 'end') return false;
@@ -407,6 +478,9 @@ export class BpmnStudioController {
407
478
  updateNode(id, patch = {}) {
408
479
  const node = getNode(this._model, id);
409
480
  if (!node) return false;
481
+ if (patch.type !== undefined && patch.type !== node.type) {
482
+ throw new Error('Node type changes must use changeNodeType().');
483
+ }
410
484
  const previousLabelPlacement = resolveSwimlaneLabelPlacement(this._model, node);
411
485
  this._commit('update-node', () => {
412
486
  if (patch.properties) node.properties = { ...node.properties, ...patch.properties };
@@ -431,6 +505,9 @@ export class BpmnStudioController {
431
505
  updateEdge(id, patch = {}) {
432
506
  const edge = getEdge(this._model, id);
433
507
  if (!edge) return false;
508
+ if (patch.type !== undefined && patch.type !== (edge.type || 'sequenceFlow')) {
509
+ throw new Error('Edge type cannot be changed through updateEdge().');
510
+ }
434
511
  this._commit('update-edge', () => {
435
512
  if (patch.properties) edge.properties = { ...(edge.properties || {}), ...patch.properties };
436
513
  const rest = { ...patch }; delete rest.properties;
@@ -500,7 +577,7 @@ export class BpmnStudioController {
500
577
  changeNodeType(id, type) {
501
578
  const node = getNode(this._model, id);
502
579
  const def = NODE_DEFINITIONS[type];
503
- if (!node || !def || node.type === type) return false;
580
+ if (!node || !def || !this.allowsNodeType(type) || node.type === type) return false;
504
581
  if (isEmbeddedSubProcess(node) && !isEmbeddedSubProcess(type) && hasScopeChildren(this._model, id)) return false;
505
582
  this._commit('change-node-type', () => {
506
583
  const next = createNode(type, node.x, node.y, { id: node.id, name: node.name });
@@ -514,9 +591,10 @@ export class BpmnStudioController {
514
591
  }
515
592
 
516
593
  quickAdd(sourceId, type = 'userTask', preset = {}) {
594
+ if (!this.allowsEdgeType('sequenceFlow')) return null;
517
595
  const source = getNode(this._model, sourceId);
518
596
  const def = NODE_DEFINITIONS[type];
519
- if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !def) return null;
597
+ if (!source || elementScopeId(this._model, source) !== this.activeScopeId || !def || !this.allowsNodeType(type)) return null;
520
598
  const direction = this._model.settings?.direction || 'horizontal';
521
599
  const spacing = LAYOUT_DENSITIES[this._model.settings?.layoutDensity || 'balanced'] || LAYOUT_DENSITIES.balanced;
522
600
  const outgoingCount = this._model.edges.filter((edge) => edge.source === sourceId).length;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ export * from './modules/palette/index.js'
14
14
  import type {
15
15
  BpmnEdge,
16
16
  BpmnNode,
17
+ EdgeType,
17
18
  ElementSelection,
18
19
  EngineId,
19
20
  LayoutOptions,
@@ -201,9 +202,16 @@ export interface StudioState {
201
202
  canRedo: boolean
202
203
  engine: EngineId
203
204
  propertiesProfile: 'business' | 'developer'
205
+ allowedNodeTypes: NodeType[] | null
206
+ allowedEdgeTypes: EdgeType[] | null
204
207
  activeScopeId: string
205
208
  scopePath: Array<{ id: string; name: string; kind: 'process' | 'subProcess' }>
206
209
  }
210
+ export interface StudioValidationIssue {
211
+ level: 'error' | 'warning'
212
+ elementId?: string
213
+ message: string
214
+ }
207
215
  export interface NodePreset extends Partial<Omit<BpmnNode, 'properties'>> { properties?: Record<string, unknown> }
208
216
  export interface TemplateDefinition {
209
217
  nodes: Array<{ key: string; nodeType: NodeType; offset?: Point; preset?: NodePreset }>
@@ -223,6 +231,8 @@ export interface StudioControllerOptions {
223
231
  historyLimit?: number
224
232
  extensions?: StudioExtension[]
225
233
  propertiesProfile?: 'business' | 'developer'
234
+ allowedNodeTypes?: Iterable<NodeType> | null
235
+ allowedEdgeTypes?: Iterable<EdgeType> | null
226
236
  }
227
237
  export interface StudioCommands {
228
238
  createNode(input: { nodeType: NodeType; point?: Point; preset?: NodePreset }): BpmnNode
@@ -289,6 +299,8 @@ export class BpmnStudioController {
289
299
  navigateToScope(scopeId: string): boolean
290
300
  setModel(model: ProcessModel, options?: { resetHistory?: boolean }): void
291
301
  setPropertiesProfile(profile: 'business' | 'developer'): void
302
+ allowsNodeType(nodeType: NodeType): boolean
303
+ allowsEdgeType(edgeType: EdgeType): boolean
292
304
  startConnect(sourceId: string): boolean
293
305
  cancelConnect(): void
294
306
  createNode(input: Parameters<StudioCommands['createNode']>[0]): BpmnNode
@@ -311,6 +323,7 @@ export class BpmnStudioController {
311
323
  redo(): boolean
312
324
  exportXml(engine?: EngineId): string
313
325
  importXml(xml: string, engine?: EngineId): ProcessModel
326
+ validate(): StudioValidationIssue[]
314
327
  registerExtension(extension: StudioExtension): () => void
315
328
  destroy(): void
316
329
  }
@@ -430,10 +443,38 @@ export class BpmnCanvas {
430
443
  }
431
444
  export function createBpmnCanvas(options: BpmnCanvasOptions): BpmnCanvas
432
445
 
446
+ export interface StudioShellActions {
447
+ undo(): boolean
448
+ redo(): boolean
449
+ beautify(options?: LayoutOptions): void
450
+ rerouteEdges(options?: LayoutOptions): void
451
+ fitView(): void
452
+ validate(): StudioValidationIssue[]
453
+ importXml(xml: string, engine?: EngineId): ProcessModel
454
+ exportXml(engine?: EngineId): string
455
+ exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact>
456
+ openSvgExportPreview(options?: SvgExportOptions & {
457
+ previewTitle?: string
458
+ onDownload?: (artifact: SvgExportArtifact) => void
459
+ }): SvgExportPreviewController | null
460
+ }
461
+ export interface StudioShellSlotContext {
462
+ studio: BpmnStudioController
463
+ shell: BpmnStudioShell
464
+ canvas: BpmnCanvas
465
+ actions: StudioShellActions
466
+ getState(): StudioState
467
+ subscribe(listener: (event: StudioEvent) => void): () => void
468
+ }
469
+ export type StudioShellDomSlot =
470
+ | HTMLElement
471
+ | ((context: StudioShellSlotContext & { container: HTMLElement }) => void | (() => void))
433
472
  export interface StudioShellSlots {
434
- left?: HTMLElement | ((context: Record<string, unknown>) => void | (() => void))
435
- right?: HTMLElement | ((context: Record<string, unknown>) => void | (() => void))
436
- header?: HTMLElement | ((context: Record<string, unknown>) => void | (() => void))
473
+ left?: StudioShellDomSlot
474
+ right?: StudioShellDomSlot
475
+ headerStart?: StudioShellDomSlot
476
+ header?: StudioShellDomSlot
477
+ footer?: StudioShellDomSlot
437
478
  contextMenu?: ContextMenuSlot
438
479
  selectionToolbar?: SelectionToolbarSlot
439
480
  runtimeDetails?: RuntimeDetailsRenderer
@@ -441,6 +482,19 @@ export interface StudioShellSlots {
441
482
  runtimeTimeline?: RuntimeTimelineRenderer
442
483
  [name: string]: HTMLElement | Function | undefined
443
484
  }
485
+ export type StudioShellRegionMode = 'default' | 'hidden'
486
+ export interface StudioShellRegions {
487
+ header?: StudioShellRegionMode
488
+ left?: StudioShellRegionMode
489
+ right?: StudioShellRegionMode
490
+ footer?: StudioShellRegionMode
491
+ }
492
+ export interface StudioShellRegionState {
493
+ header: StudioShellRegionMode
494
+ left: StudioShellRegionMode
495
+ right: StudioShellRegionMode
496
+ footer: StudioShellRegionMode
497
+ }
444
498
  export interface StudioShellOptions {
445
499
  container: HTMLElement
446
500
  studio: BpmnStudioController
@@ -454,9 +508,11 @@ export interface StudioShellOptions {
454
508
  timeline?: ViewerTimelineOptions
455
509
  }
456
510
  slots?: StudioShellSlots
511
+ regions?: StudioShellRegions
457
512
  layout?: (context: Record<string, unknown>) => void | (() => void)
458
513
  runtime?: ProcessInstanceSnapshot | null
459
514
  mode?: 'design' | 'viewer' | 'instance'
515
+ allowedModes?: Array<'design' | 'viewer' | 'instance'>
460
516
  projection?: ViewerProjection
461
517
  responsive?: boolean
462
518
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
@@ -469,8 +525,11 @@ export interface StudioShellOptions {
469
525
  }
470
526
  export class BpmnStudioShell {
471
527
  constructor(options: StudioShellOptions)
528
+ readonly actions: StudioShellActions
472
529
  canvas?: BpmnCanvas
473
530
  viewer?: BpmnViewer
531
+ getRegions(): Readonly<StudioShellRegionState>
532
+ setRegions(regions: StudioShellRegions): Readonly<StudioShellRegionState>
474
533
  setMode(mode: 'design' | 'viewer' | 'instance'): void
475
534
  setRuntime(runtime: ProcessInstanceSnapshot | null): void
476
535
  setProjection(projection: ViewerProjection): void
@@ -39,6 +39,18 @@ export const defaultBpmnPaletteProvider: PaletteProvider
39
39
  export function createPaletteRegistry(): PaletteRegistry
40
40
  export function createDefaultPaletteRegistry(options?: { providers?: PaletteProvider[] }): PaletteRegistry
41
41
 
42
+ export type PaletteItemRenderer = (context: {
43
+ container: HTMLElement
44
+ item: PaletteItem
45
+ panel: PalettePanel
46
+ beginCreate(): unknown
47
+ }) => void | (() => void)
48
+ export type PaletteSectionRenderer = (context: {
49
+ container: HTMLElement
50
+ section: PaletteSection
51
+ panel: PalettePanel
52
+ }) => void | (() => void)
53
+
42
54
  export interface PalettePanelOptions {
43
55
  container: HTMLElement
44
56
  registry: PaletteRegistry
@@ -46,8 +58,8 @@ export interface PalettePanelOptions {
46
58
  interactions: { beginCreate(intent: PaletteCreateIntent): unknown; serialize(session: unknown): string }
47
59
  canvas?: unknown
48
60
  iconRegistry?: IconRegistry
49
- renderItem?: ((context: { item: PaletteItem; panel: PalettePanel }) => HTMLElement | null) | null
50
- renderSection?: ((context: { section: PaletteSection; panel: PalettePanel }) => HTMLElement | null) | null
61
+ renderItem?: PaletteItemRenderer | null
62
+ renderSection?: PaletteSectionRenderer | null
51
63
  themeController?: ThemeController | null
52
64
  theme?: NovaThemeInput | null
53
65
  onThemeChange?: ((state: NovaThemeState) => void) | null
@@ -56,10 +56,10 @@ export class PaletteRegistry {
56
56
  export const defaultBpmnPaletteProvider = {
57
57
  id: 'bpmn-default-palette',
58
58
  priority: 100,
59
- getSections() {
59
+ getSections(context = {}) {
60
60
  return PALETTE_GROUPS.map((group) => {
61
61
  const items = [];
62
- for (const nodeType of group.types) {
62
+ for (const nodeType of group.types.filter((type) => context.studio?.allowsNodeType?.(type) !== false)) {
63
63
  if (nodeType === 'parallelGateway') {
64
64
  items.push({
65
65
  id: 'node-parallelGateway',
@@ -85,7 +85,7 @@ export const defaultBpmnPaletteProvider = {
85
85
  });
86
86
  }
87
87
  return { id: group.id, label: group.label, collapsed: group.id !== 'favorites', items };
88
- });
88
+ }).filter((section) => section.items.length);
89
89
  },
90
90
  };
91
91
 
@@ -17,7 +17,9 @@ function nodeTypeOptions(context) {
17
17
  ? ['task', 'container']
18
18
  : [currentKind];
19
19
  return Object.entries(NODE_DEFINITIONS)
20
- .filter(([type, def]) => type !== 'generic' && compatible.includes(def.kind))
20
+ .filter(([type, def]) => type !== 'generic'
21
+ && compatible.includes(def.kind)
22
+ && context.studio?.allowsNodeType?.(type) !== false)
21
23
  .map(([value, def]) => ({ value, label: def.label }));
22
24
  }
23
25
 
@@ -84,6 +84,7 @@ export interface DiagramRendererOptions {
84
84
  onEdgeClick?: (edge: BpmnEdge, event: Event) => void
85
85
  onCanvasClick?: (event: Event) => void
86
86
  onViewportChange?: (state: { zoom: number; pan: Point }) => void
87
+ canCreateNodeType?: (nodeType: string) => boolean
87
88
  [key: string]: unknown
88
89
  }
89
90
  export class DiagramRenderer {
@@ -1424,7 +1424,7 @@ export class DiagramRenderer {
1424
1424
  const type = quickItem.nodeType;
1425
1425
  if (type === 'startEvent') continue;
1426
1426
  const def = NODE_DEFINITIONS[type];
1427
- if (!def) continue;
1427
+ if (!def || this.options.canCreateNodeType?.(type) === false) continue;
1428
1428
  const item = el('button', 'mb-quick-menu-item');
1429
1429
  item.dataset.search = `${quickItem.id} ${type} ${quickItem.label}`.toLowerCase();
1430
1430
  const miniIcon = el('span', `mb-mini-type mb-mini-kind-${def.kind}`);