@bpmn-nova/vue 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
@@ -1,10 +1,10 @@
1
1
  # @bpmn-nova/vue
2
2
 
3
- BPMN Nova 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨迹、Properties 和 Expose 实例方法。
3
+ BPMN Nova 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨迹、Palette、Properties 和 Expose 实例方法。
4
4
 
5
5
  > **English summary:** Vue 3.3+ components and exposed instance methods for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
6
6
 
7
- > 当前版本为 `0.3.1-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
7
+ > 当前版本为 `0.3.2-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
8
8
 
9
9
  ![BPMN Nova Vue 流程工作台](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -20,6 +20,8 @@ import '@bpmn-nova/vue/styles.css'
20
20
 
21
21
  只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
22
22
 
23
+ 使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
24
+
23
25
  ## 流程设计
24
26
 
25
27
  ```vue
@@ -28,7 +30,7 @@ import { ref } from 'vue'
28
30
  import { BpmnStudio } from '@bpmn-nova/vue'
29
31
  import '@bpmn-nova/vue/styles.css'
30
32
 
31
- defineProps({ xml: String })
33
+ const props = defineProps({ initialXml: String })
32
34
  const studioRef = ref(null)
33
35
 
34
36
  function handleChange(model, reason, nextXml) {
@@ -40,9 +42,10 @@ function handleChange(model, reason, nextXml) {
40
42
  <div style="height: 720px">
41
43
  <BpmnStudio
42
44
  ref="studioRef"
43
- :xml="xml"
45
+ :xml="props.initialXml"
44
46
  engine="flowable"
45
47
  mode="design"
48
+ :allowed-modes="['design']"
46
49
  theme="auto"
47
50
  @change="handleChange"
48
51
  />
@@ -50,7 +53,63 @@ function handleChange(model, reason, nextXml) {
50
53
  </template>
51
54
  ```
52
55
 
53
- Expose 提供 `exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
56
+ Expose 提供 `getActions()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
57
+
58
+ `xml`/`model` 是外部替换输入,`change` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
59
+
60
+ ## 组合宿主头部与业务属性面板
61
+
62
+ 默认 `BpmnStudio` 是包含 Header、Palette、Canvas、Properties 和 Statusbar 的完整工作台。下面的接入显式隐藏 Nova Properties,保留 Nova Palette,并只替换 Header 左侧品牌区:
63
+
64
+ ```vue
65
+ <script setup>
66
+ import { ref } from 'vue'
67
+ import { BpmnStudio } from '@bpmn-nova/vue'
68
+ import '@bpmn-nova/vue/styles.css'
69
+
70
+ const studioRef = ref(null)
71
+ const selectedElement = ref(null)
72
+ const supportedNodeTypes = ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']
73
+
74
+ function handleSelectionChange(selection, element) {
75
+ selectedElement.value = selection ? element : null
76
+ }
77
+ </script>
78
+
79
+ <template>
80
+ <div class="process-design-workbench">
81
+ <BpmnStudio
82
+ ref="studioRef"
83
+ :xml="xml"
84
+ engine="activiti"
85
+ mode="design"
86
+ :allowed-modes="['design']"
87
+ :allowed-node-types="supportedNodeTypes"
88
+ :allowed-edge-types="['sequenceFlow']"
89
+ :regions="{ right: 'hidden' }"
90
+ theme="auto"
91
+ @change="handleChange"
92
+ @selection-change="handleSelectionChange"
93
+ >
94
+ <template #header-start="{ state, actions }">
95
+ <button type="button" @click="back">← 返回审批列表</button>
96
+ <ApprovalIcon :icon="icon" />
97
+ <strong>{{ processName }}</strong>
98
+ <span>{{ processType }}</span>
99
+ </template>
100
+ </BpmnStudio>
101
+
102
+ <DxProcessProperties
103
+ v-if="selectedElement"
104
+ :element-id="selectedElement.id"
105
+ />
106
+ </div>
107
+ </template>
108
+ ```
109
+
110
+ `header-start` 使用 Vue Teleport 在当前应用内渲染,因此保留 provide/inject、响应式状态与组件生命周期;`header` 可完整替换 Header。插槽内通过 `actions` 调用 Undo、Redo、美化、最佳视图、校验和导入导出。若整个 Header 都不需要,配置 `regions.header = 'hidden'`。
111
+
112
+ 编辑器属性面板应订阅 `selection-change`,该事件覆盖节点、连线、多选、清空和键盘选择。`element-click` 是点击观察事件,不能代替选择状态。宿主使用稳定 BPMN Element ID 关联业务配置,并自行拥有保存、发布、权限和服务端事务。
54
113
 
55
114
  ## 只读展示与审批轨迹
56
115
 
@@ -164,12 +223,16 @@ studioRef.value?.openSvgExportPreview({
164
223
 
165
224
  本包还导出:
166
225
 
167
- - `BpmnDesigner`、`BpmnCanvas`、`BpmnPropertiesPanel`
226
+ - `BpmnDesigner`、`BpmnCanvas`、`BpmnPalettePanel`、`BpmnPropertiesPanel`
168
227
  - `useBpmnStudio`
169
228
  - `createVuePropertyComponent`
170
229
  - `createVueRuntimeDetailsComponent`
171
230
  - `createVueRuntimeTransitionDetailsComponent`
172
231
  - `createVueRuntimeTimelineComponent`
232
+ - `createStudioController`、`createEmptyProcess`、`importBpmn`、`exportBpmn`
233
+ - Palette、Properties、Context Menu、Icon 和 Template Registry 的公开创建函数
234
+
235
+ 完整工作台可通过 `allowedNodeTypes` 与 `allowedEdgeTypes` 限制宿主支持的图元类型,通过 `allowedModes` 限制可切换的工作台模式。图元限制同时作用于 XML 导入、Palette、连接、快捷新增、模板和节点类型转换。
173
236
 
174
237
  ## 常见问题
175
238
 
@@ -181,11 +244,11 @@ studioRef.value?.openSvgExportPreview({
181
244
 
182
245
  ## 文档与 AI
183
246
 
247
+ - npm 包内 `llms.txt`:可执行安装入口
248
+ - npm 包内 `llms-full.txt`:完整 AI 上下文
184
249
  - [完整项目能力](https://github.com/daxiangme/bpmn-nova)
185
250
  - [React/Vue 组件说明](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/COMPONENTS.md)
186
251
  - [公开 Interface](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/API.md)
187
- - [AI 接入入口](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms.txt)
188
- - [AI 完整上下文](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/llms-full.txt)
189
252
 
190
253
  ## License
191
254
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
- import type { Component, DefineComponent } from 'vue'
2
- import type { BpmnEdge, BpmnNode, ElementSelection, EngineId, LayoutOptions, ProcessModel } from '@bpmn-nova/studio'
1
+ import type { Component, DefineComponent, VNodeChild } from 'vue'
2
+ import type { BpmnEdge, BpmnNode, EdgeType, ElementSelection, EngineId, LayoutOptions, NodeType, ProcessModel } from '@bpmn-nova/studio'
3
3
  import type { BpmnDesigner as DesignerInstance } from '@bpmn-nova/studio/designer'
4
4
  import type { IconRegistry } from '@bpmn-nova/studio'
5
- import type { PaletteRegistry } from '@bpmn-nova/studio'
5
+ import type {
6
+ PaletteItemRenderer,
7
+ PalettePanel as PalettePanelInstance,
8
+ PaletteProvider,
9
+ PaletteRegistry,
10
+ PaletteSectionRenderer,
11
+ } from '@bpmn-nova/studio'
6
12
  import type { PropertiesContext, PropertiesProvider, PropertiesRegistry, PropertyEntry } from '@bpmn-nova/studio'
7
13
  import type { ProcessInstanceSnapshot } from '@bpmn-nova/studio/runtime'
8
14
  import type { SvgExportArtifact, SvgExportOptions, SvgExportPreviewController } from '@bpmn-nova/studio/export-svg'
@@ -10,9 +16,12 @@ import type {
10
16
  BpmnCanvas as CanvasInstance,
11
17
  BpmnStudioController,
12
18
  BpmnStudioShell,
19
+ ContextMenuRegistry,
13
20
  InteractionController,
14
21
  StudioCommands,
22
+ StudioShellActions,
15
23
  StudioShellOptions,
24
+ StudioShellRegions,
16
25
  StudioShellSlots,
17
26
  StudioState,
18
27
  TemplateRegistry,
@@ -28,13 +37,33 @@ import type {
28
37
  ViewerProjection,
29
38
  } from '@bpmn-nova/studio/viewer'
30
39
 
40
+ export {
41
+ createContextMenuRegistry,
42
+ createDefaultContextMenuRegistry,
43
+ createDefaultIconRegistry,
44
+ createDefaultPaletteRegistry,
45
+ createDefaultPropertiesRegistry,
46
+ createEmptyProcess,
47
+ createPaletteRegistry,
48
+ createPropertiesRegistry,
49
+ createStudioController,
50
+ createTemplateRegistry,
51
+ exportBpmn,
52
+ importBpmn,
53
+ propertyEntry,
54
+ propertyGroup,
55
+ registerActivitiProperties,
56
+ registerBpmnProperties,
57
+ registerFlowableProperties,
58
+ } from '@bpmn-nova/studio'
59
+
31
60
  export interface BpmnNovaVisualProps {
32
61
  theme?: NovaThemeInput
33
62
  runtimeAppearance?: RuntimeAppearanceOptions
34
63
  onThemeChange?: (state: NovaThemeState) => void
35
64
  }
36
65
  export interface BpmnModelProps { model?: ProcessModel; xml?: string; engine?: EngineId }
37
- export interface UseBpmnStudioOptions extends BpmnModelProps { studio?: BpmnStudioController; propertiesProfile?: 'business' | 'developer' }
66
+ export interface UseBpmnStudioOptions extends BpmnModelProps { studio?: BpmnStudioController; propertiesProfile?: 'business' | 'developer'; allowedNodeTypes?: NodeType[]; allowedEdgeTypes?: EdgeType[] }
38
67
  export interface UseBpmnStudioResult { studio: BpmnStudioController; state: StudioState; commands: StudioCommands }
39
68
  export function useBpmnStudio(options?: UseBpmnStudioOptions): UseBpmnStudioResult
40
69
 
@@ -56,17 +85,39 @@ export interface BpmnCanvasExposed {
56
85
  }
57
86
  export const BpmnCanvas: DefineComponent<BpmnCanvasProps>
58
87
 
88
+ export interface BpmnPalettePanelProps extends BpmnNovaVisualProps {
89
+ studio: BpmnStudioController
90
+ interactions?: InteractionController
91
+ templates?: TemplateRegistry
92
+ registry?: PaletteRegistry
93
+ providers?: PaletteProvider[]
94
+ canvas?: CanvasInstance | null
95
+ iconRegistry?: IconRegistry
96
+ renderItem?: PaletteItemRenderer
97
+ renderSection?: PaletteSectionRenderer
98
+ }
99
+ export interface BpmnPalettePanelExposed {
100
+ getPanel(): PalettePanelInstance | null
101
+ render(): void
102
+ setTheme(theme: NovaThemeInput): NovaThemeState | undefined
103
+ }
104
+ export const BpmnPalettePanel: DefineComponent<BpmnPalettePanelProps>
105
+
59
106
  export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
60
107
  studio?: BpmnStudioController
61
108
  propertiesProfile?: 'business' | 'developer'
109
+ allowedNodeTypes?: NodeType[]
110
+ allowedEdgeTypes?: EdgeType[]
62
111
  iconRegistry?: IconRegistry
63
112
  paletteRegistry?: PaletteRegistry
64
113
  propertiesRegistry?: PropertiesRegistry
65
114
  templateRegistry?: TemplateRegistry
115
+ contextMenuRegistry?: ContextMenuRegistry
66
116
  nodeRenderers?: Record<string, Function>
67
117
  nodeRenderer?: Function
68
118
  slotsConfig?: StudioShellSlots
69
119
  layout?: StudioShellOptions['layout']
120
+ regions?: StudioShellRegions
70
121
  leftWidth?: number
71
122
  rightWidth?: number
72
123
  runtime?: ProcessInstanceSnapshot | null
@@ -86,6 +137,7 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
86
137
  onTraceClick?: (event: RuntimeTraceClickEvent) => void
87
138
  onElementClick?: ViewerOptions['onElementClick']
88
139
  mode?: 'design' | 'viewer' | 'instance'
140
+ allowedModes?: Array<'design' | 'viewer' | 'instance'>
89
141
  projection?: ViewerProjection
90
142
  responsive?: boolean
91
143
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
@@ -93,13 +145,26 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
93
145
  export interface BpmnStudioExposed {
94
146
  getStudio(): BpmnStudioController
95
147
  getShell(): BpmnStudioShell | null
148
+ getActions(): StudioShellActions | null
96
149
  exportXml(engine?: EngineId): string
97
150
  fitView(): void
98
151
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
99
152
  exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
100
153
  openSvgExportPreview(options?: SvgExportOptions & { previewTitle?: string; onDownload?: (artifact: SvgExportArtifact) => void }): SvgExportPreviewController | null | undefined
101
154
  }
102
- export const BpmnStudio: DefineComponent<BpmnStudioProps>
155
+ export interface BpmnStudioSlotContext {
156
+ readonly studio: BpmnStudioController
157
+ readonly shell: BpmnStudioShell
158
+ readonly actions: StudioShellActions
159
+ readonly state: Readonly<StudioState>
160
+ }
161
+ export interface BpmnStudioSlots {
162
+ 'header-start'?(context: BpmnStudioSlotContext): VNodeChild
163
+ header?(context: BpmnStudioSlotContext): VNodeChild
164
+ }
165
+ export const BpmnStudio: DefineComponent<BpmnStudioProps> & {
166
+ new(): { $slots: BpmnStudioSlots }
167
+ }
103
168
 
104
169
  export interface BpmnDesignerProps extends BpmnNovaVisualProps, BpmnModelProps { svgExport?: ViewerOptions['svgExport'] }
105
170
  export interface BpmnDesignerExposed {
package/dist/index.js CHANGED
@@ -1,9 +1,30 @@
1
- import { createApp, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
1
+ import { Fragment, Teleport, createApp, defineComponent, h, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
2
2
  import { BpmnDesigner as CoreDesigner } from '@bpmn-nova/studio/designer';
3
3
  import { BpmnViewer as CoreViewer } from '@bpmn-nova/studio/viewer';
4
4
  import { importBpmn, exportBpmn } from '@bpmn-nova/studio';
5
5
  import { createEmptyProcess } from '@bpmn-nova/studio';
6
6
  import { BpmnCanvas as CoreCanvas, BpmnStudioShell as CoreStudioShell, createInteractionController, createStudioController, createTemplateRegistry } from '@bpmn-nova/studio';
7
+ import { PalettePanel as CorePalettePanel, createDefaultPaletteRegistry as createCoreDefaultPaletteRegistry } from '@bpmn-nova/studio';
8
+
9
+ export {
10
+ createContextMenuRegistry,
11
+ createDefaultContextMenuRegistry,
12
+ createDefaultIconRegistry,
13
+ createDefaultPaletteRegistry,
14
+ createDefaultPropertiesRegistry,
15
+ createEmptyProcess,
16
+ createPaletteRegistry,
17
+ createPropertiesRegistry,
18
+ createStudioController,
19
+ createTemplateRegistry,
20
+ exportBpmn,
21
+ importBpmn,
22
+ propertyEntry,
23
+ propertyGroup,
24
+ registerActivitiProperties,
25
+ registerBpmnProperties,
26
+ registerFlowableProperties,
27
+ } from '@bpmn-nova/studio';
7
28
 
8
29
  export function createVueRuntimeDetailsComponent(Component) {
9
30
  return ({ container, ...context }) => {
@@ -28,7 +49,7 @@ function resolveModel(props) {
28
49
  }
29
50
 
30
51
  export function useBpmnStudio(options = {}) {
31
- const studio = options.studio || createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile });
52
+ const studio = options.studio || createStudioController({ model: resolveModel(options), propertiesProfile: options.propertiesProfile, allowedNodeTypes: options.allowedNodeTypes, allowedEdgeTypes: options.allowedEdgeTypes });
32
53
  const state = reactive(studio.getState());
33
54
  const off = studio.subscribe((event) => Object.assign(state, event.state));
34
55
  onBeforeUnmount(() => { off(); if (!options.studio) studio.destroy(); });
@@ -53,38 +74,107 @@ export const BpmnCanvas = defineComponent({
53
74
  },
54
75
  });
55
76
 
77
+ export const BpmnPalettePanel = defineComponent({
78
+ name: 'BpmnNovaPalettePanel',
79
+ props: {
80
+ studio: { type: Object, required: true },
81
+ interactions: Object,
82
+ templates: Object,
83
+ registry: Object,
84
+ providers: Array,
85
+ canvas: Object,
86
+ iconRegistry: Object,
87
+ renderItem: Function,
88
+ renderSection: Function,
89
+ theme: [Object, String],
90
+ onThemeChange: Function,
91
+ },
92
+ setup(props, { expose, attrs }) {
93
+ const host = ref(null);
94
+ let panel = null;
95
+
96
+ const mountPanel = () => {
97
+ if (!host.value || !props.studio) return;
98
+ panel?.destroy();
99
+ const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
100
+ const registry = props.registry || createCoreDefaultPaletteRegistry({ providers: props.providers || [] });
101
+ panel = new CorePalettePanel({
102
+ container: host.value,
103
+ registry,
104
+ studio: props.studio,
105
+ interactions,
106
+ canvas: props.canvas || null,
107
+ iconRegistry: props.iconRegistry,
108
+ renderItem: props.renderItem,
109
+ renderSection: props.renderSection,
110
+ theme: props.theme,
111
+ onThemeChange: props.onThemeChange,
112
+ });
113
+ };
114
+
115
+ onMounted(mountPanel);
116
+ watch(() => [props.studio, props.registry, props.interactions, props.templates, props.canvas, props.providers, props.iconRegistry, props.renderItem, props.renderSection], mountPanel);
117
+ watch(() => props.theme, (value) => { if (value !== undefined) panel?.setTheme(value); });
118
+ onBeforeUnmount(() => panel?.destroy());
119
+ expose({ getPanel: () => panel, render: () => panel?.render(), setTheme: (value) => panel?.setTheme(value) });
120
+ return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', overflow: 'auto', ...(attrs.style || {}) } });
121
+ },
122
+ });
123
+
56
124
  export const BpmnStudio = defineComponent({
57
125
  name: 'BpmnNovaStudio',
58
126
  props: {
59
- studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String,
60
- iconRegistry: Object, paletteRegistry: Object, propertiesRegistry: Object, templateRegistry: Object,
61
- nodeRenderers: Object, nodeRenderer: Function, slotsConfig: Object, layout: Function, leftWidth: Number, rightWidth: Number,
127
+ studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String, allowedNodeTypes: Array, allowedEdgeTypes: Array,
128
+ iconRegistry: Object, paletteRegistry: Object, propertiesRegistry: Object, templateRegistry: Object, contextMenuRegistry: Object,
129
+ nodeRenderers: Object, nodeRenderer: Function, slotsConfig: Object, layout: Function, regions: Object, leftWidth: Number, rightWidth: Number,
62
130
  runtime: Object, runtimePresenter: Function, runtimeTraceProjector: Function, runtimeAssetResolver: Function, runtimeTimelineRenderer: Function, onRuntimeTraceItemClick: Function, runtimeDetailsRenderer: Function, onRuntimeDetailsOpen: Function,
63
131
  runtimeTransitionDetailsRenderer: Function, onRuntimeTransitionDetailsOpen: Function,
64
132
  timeline: Object, runtimeDetails: Object, runtimeTraceOptions: Object, onTraceClick: Function, onElementClick: Function,
65
- mode: String, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
133
+ mode: String, allowedModes: Array, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
66
134
  theme: [Object, String], runtimeAppearance: Object, svgExport: Object, onThemeChange: Function,
67
135
  },
68
136
  emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click'],
69
- setup(props, { emit, expose, attrs }) {
137
+ setup(props, { emit, expose, attrs, slots }) {
70
138
  const host = ref(null);
139
+ const headerTarget = ref(null);
140
+ const headerStartTarget = ref(null);
141
+ const headerContext = ref(null);
142
+ const headerStartContext = ref(null);
71
143
  const owned = !props.studio;
72
- const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile });
144
+ const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
145
+ const state = reactive(studio.getState());
73
146
  let shell = null;
74
147
  let off = null;
148
+
149
+ const createTeleportSlot = (target, contextRef) => (context) => {
150
+ target.value = context.container;
151
+ contextRef.value = context;
152
+ return () => {
153
+ if (target.value === context.container) target.value = null;
154
+ if (contextRef.value === context) contextRef.value = null;
155
+ };
156
+ };
157
+
75
158
  onMounted(() => {
76
- shell = new CoreStudioShell({ container: host.value, studio, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: props.slotsConfig || {}, layout: props.layout, runtime: props.runtime, mode: props.mode, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
159
+ const shellSlots = { ...(props.slotsConfig || {}) };
160
+ if (slots.header) shellSlots.header = createTeleportSlot(headerTarget, headerContext);
161
+ else if (!shellSlots.header && slots['header-start']) shellSlots.headerStart = createTeleportSlot(headerStartTarget, headerStartContext);
162
+ shell = new CoreStudioShell({ container: host.value, studio, iconRegistry: props.iconRegistry, paletteRegistry: props.paletteRegistry, propertiesRegistry: props.propertiesRegistry, templateRegistry: props.templateRegistry, contextMenuRegistry: props.contextMenuRegistry, rendererOptions: { nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, runtimePresenter: props.runtimePresenter, runtimeTraceProjector: props.runtimeTraceProjector, runtimeAssetResolver: props.runtimeAssetResolver, runtimeTraceOptions: props.runtimeTraceOptions, runtimeTimelineRenderer: props.runtimeTimelineRenderer, timeline: props.timeline, runtimeDetails: props.runtimeDetails, onRuntimeTraceItemClick: (payload) => props.onRuntimeTraceItemClick?.(payload), runtimeDetailsRenderer: props.runtimeDetailsRenderer, onRuntimeDetailsOpen: (payload) => props.onRuntimeDetailsOpen?.(payload), runtimeTransitionDetailsRenderer: props.runtimeTransitionDetailsRenderer, onRuntimeTransitionDetailsOpen: (payload) => props.onRuntimeTransitionDetailsOpen?.(payload), onElementClick: (payload) => emit('element-click', payload), onTraceClick: (payload) => emit('trace-click', payload) }, slots: shellSlots, layout: props.layout, regions: props.regions, runtime: props.runtime, mode: props.mode, allowedModes: props.allowedModes, projection: props.projection, responsive: props.responsive, projectionOptions: props.projectionOptions, leftWidth: props.leftWidth, rightWidth: props.rightWidth, theme: props.theme, runtimeAppearance: props.runtimeAppearance, svgExport: props.svgExport, onThemeChange: props.onThemeChange });
77
163
  off = studio.subscribe((event) => {
164
+ Object.assign(state, event.state);
78
165
  if (event.type === 'modelChanged') emit('change', studio.model, event.reason, studio.exportXml());
79
166
  if (event.type === 'selectionChanged') emit('selection-change', studio.selection, studio.getSelectedElement());
80
167
  if (event.type === 'scopeChanged') emit('scope-change', event.activeScopeId, event.scopePath, studio.getState());
81
168
  });
82
169
  });
83
170
  watch(() => props.model, (value) => { if (value && value !== studio.model) studio.setModel(value); });
84
- watch(() => props.xml, (value) => { if (value) studio.importXml(value, props.engine); });
171
+ watch(() => props.xml, (value) => {
172
+ if (value && value !== studio.exportXml(props.engine)) studio.importXml(value, props.engine);
173
+ });
85
174
  watch(() => props.runtime, (value) => { if (value !== undefined) shell?.setRuntime(value); });
86
175
  watch(() => props.mode, (value) => { if (value) shell?.setMode(value); });
87
176
  watch(() => props.projection, (value) => { if (value) shell?.setProjection(value); });
177
+ watch(() => props.regions, (value) => { if (value !== undefined) shell?.setRegions(value); }, { deep: true });
88
178
  watch(() => props.theme, (value) => { if (value !== undefined) shell?.setTheme(value); });
89
179
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) shell?.setRuntimeAppearance(value); });
90
180
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions], ([timeline, runtimeDetails, runtimeTraceOptions]) => {
@@ -95,8 +185,21 @@ export const BpmnStudio = defineComponent({
95
185
  shell.viewer?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions });
96
186
  });
97
187
  onBeforeUnmount(() => { off?.(); shell?.destroy(); if (owned) studio.destroy(); });
98
- expose({ getStudio: () => studio, getShell: () => shell, exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.canvas?.fitView(), setTheme: (value) => shell?.setTheme(value) });
99
- return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '520px', ...(attrs.style || {}) } });
188
+ expose({ getStudio: () => studio, getShell: () => shell, getActions: () => shell?.actions || null, exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.fitView(), setTheme: (value) => shell?.setTheme(value) });
189
+ const renderSlot = (name, target, contextRef) => {
190
+ if (!slots[name] || !target.value || !contextRef.value) return null;
191
+ return h(Teleport, { to: target.value }, slots[name]({
192
+ studio: contextRef.value.studio,
193
+ shell: contextRef.value.shell,
194
+ actions: contextRef.value.actions,
195
+ state,
196
+ }));
197
+ };
198
+ return () => h(Fragment, null, [
199
+ h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '520px', ...(attrs.style || {}) } }),
200
+ renderSlot('header', headerTarget, headerContext),
201
+ renderSlot('header-start', headerStartTarget, headerStartContext),
202
+ ]);
100
203
  },
101
204
  });
102
205
 
package/dist/styles.css CHANGED
@@ -1141,6 +1141,8 @@ textarea.property-control { resize: vertical; }
1141
1141
 
1142
1142
  /* @bpmn-nova/internal/studio */
1143
1143
  .nova-studio-shell { --nova-left-width: 244px; --nova-right-width: 360px; --nova-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; --nova-font-size-xs: 10px; --nova-font-size-sm: 12px; --nova-font-size-md: 14px; --nova-font-size-lg: 16px; --nova-font-weight-regular: 400; --nova-font-weight-medium: 500; --nova-font-weight-semibold: 600; --nova-font-weight-bold: 700; position: relative; isolation: isolate; width: 100%; height: 100%; min-height: 520px; overflow: hidden; display: grid; grid-template-rows: 58px minmax(0, 1fr); color: var(--nova-color-text, #263148); background: var(--nova-color-canvas, #f6f7fb); font-family: var(--nova-font-family); }
1144
+ .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1145
+ .nova-studio-header[hidden], .nova-studio-left[hidden], .nova-studio-right[hidden], .nova-studio-statusbar[hidden] { display: none !important; }
1144
1146
  .nova-studio-header { min-width: 0; border-bottom: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); display: grid; grid-template-columns: minmax(180px, auto) auto minmax(0, 1fr); align-items: center; gap: 18px; padding: 0 12px 0 14px; }
1145
1147
  .nova-studio-brand { min-width: 0; display: flex; align-items: center; gap: 10px; }
1146
1148
  .nova-studio-brand-mark { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--nova-color-text-inverse, #fff); background: linear-gradient(145deg, var(--nova-color-primary, #5362da), var(--nova-color-primary-hover)); font-size: var(--nova-font-size-md); font-weight: var(--nova-font-weight-bold); box-shadow: var(--nova-shadow-sm); }
@@ -1194,7 +1196,11 @@ textarea.property-control { resize: vertical; }
1194
1196
  .nova-studio-beautify-action-icon > .nova-icon-svg { width: 14px; height: 14px; }
1195
1197
  .nova-studio-beautify-action strong { font-size: var(--nova-font-size-sm); font-weight: var(--nova-font-weight-semibold); line-height: 18px; }.nova-studio-beautify-action small { color: #778195; color: var(--nova-color-text-muted, #778195); font-size: var(--nova-font-size-xs); line-height: 14px; }
1196
1198
  .nova-studio-body { min-width: 0; min-height: 0; display: grid; grid-template-columns: var(--nova-left-width) minmax(320px, 1fr) var(--nova-right-width); }
1199
+ .nova-studio-body.is-left-hidden { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
1200
+ .nova-studio-body.is-right-hidden { grid-template-columns: var(--nova-left-width) minmax(320px, 1fr); }
1201
+ .nova-studio-body.is-left-hidden.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
1197
1202
  .nova-studio-body.is-readonly { grid-template-columns: minmax(320px, 1fr) var(--nova-right-width); }
1203
+ .nova-studio-body.is-readonly.is-right-hidden { grid-template-columns: minmax(320px, 1fr); }
1198
1204
  .nova-studio-body.is-readonly .nova-studio-left { display: none; }
1199
1205
  .nova-studio-left, .nova-studio-right, .nova-studio-canvas-column, .nova-studio-canvas { min-width: 0; min-height: 0; overflow: hidden; }
1200
1206
  .nova-studio-left { border-right: 1px solid var(--nova-color-divider, #e4e8f0); background: var(--nova-color-surface, #fff); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; }
@@ -1206,6 +1212,7 @@ textarea.property-control { resize: vertical; }
1206
1212
  .nova-studio-left::-webkit-scrollbar-thumb:hover, .nova-studio-right::-webkit-scrollbar-thumb:hover { background: #c1c9d5; background: var(--nova-color-text-muted); }
1207
1213
  .nova-studio-left::-webkit-scrollbar-button, .nova-studio-right::-webkit-scrollbar-button { display: none; width: 0; height: 0; }
1208
1214
  .nova-studio-canvas-column { display: grid; grid-template-rows: minmax(0, 1fr) 35px; background: var(--nova-color-canvas, #f7f8fc); }
1215
+ .nova-studio-canvas-column.is-footer-hidden { grid-template-rows: minmax(0, 1fr); }
1209
1216
  .nova-studio-canvas-stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; }
1210
1217
  .nova-studio-canvas { position: relative; }
1211
1218
  .nova-studio-canvas-stage > .nova-studio-canvas { width: 100%; height: 100%; }
@@ -1422,6 +1429,7 @@ textarea.property-control { resize: vertical; }
1422
1429
  }
1423
1430
  @media (max-width: 720px) {
1424
1431
  .nova-studio-shell { grid-template-rows: 54px minmax(0,1fr); }
1432
+ .nova-studio-shell.is-header-hidden { grid-template-rows: minmax(0, 1fr); }
1425
1433
  .nova-studio-header { grid-template-columns: auto minmax(0,1fr); padding-left: 9px; }
1426
1434
  .nova-studio-brand-copy { display: none; }
1427
1435
  .nova-studio-body { grid-template-columns: 1fr; }