@bpmn-nova/vue 0.3.2-preview → 0.3.3-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 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨
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.2-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
7
+ > 当前版本为 `0.3.3-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
 
@@ -53,7 +53,7 @@ function handleChange(model, reason, nextXml) {
53
53
  </template>
54
54
  ```
55
55
 
56
- Expose 提供 `getActions()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
56
+ Expose 提供 `getActions()`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
57
57
 
58
58
  `xml`/`model` 是外部替换输入,`change` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
59
59
 
@@ -90,6 +90,7 @@ function handleSelectionChange(selection, element) {
90
90
  theme="auto"
91
91
  @change="handleChange"
92
92
  @selection-change="handleSelectionChange"
93
+ @validation="handleValidation"
93
94
  >
94
95
  <template #header-start="{ state, actions }">
95
96
  <button type="button" @click="back">← 返回审批列表</button>
@@ -97,6 +98,11 @@ function handleSelectionChange(selection, element) {
97
98
  <strong>{{ processName }}</strong>
98
99
  <span>{{ processType }}</span>
99
100
  </template>
101
+ <template #header-actions="{ actions, mode }">
102
+ <button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
103
+ <button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
104
+ <button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
105
+ </template>
100
106
  </BpmnStudio>
101
107
 
102
108
  <DxProcessProperties
@@ -107,10 +113,56 @@ function handleSelectionChange(selection, element) {
107
113
  </template>
108
114
  ```
109
115
 
110
- `header-start` 使用 Vue Teleport 在当前应用内渲染,因此保留 provide/inject、响应式状态与组件生命周期;`header` 可完整替换 Header。插槽内通过 `actions` 调用 Undo、Redo、美化、最佳视图、校验和导入导出。若整个 Header 都不需要,配置 `regions.header = 'hidden'`。
116
+ `header-start` 与 `header-actions` 使用 Vue Teleport 在当前应用内渲染,因此保留 provide/inject、响应式状态与组件生命周期。`header-actions` 只替换默认“校验 / 导入 / 导出”动作组,完整 `header` 的优先级更高。默认 Header 的最佳视图只保留在底部缩放区;若整个 Header 都不需要,配置 `regions.header = 'hidden'`。
117
+
118
+ `actions.validate()` 以 `toolbar` 来源先更新 Nova 默认状态栏,再发送 `validation` 事件并返回 issues;Expose `validate()` 使用 `api` 来源。事件的 `valid` 仅在存在 error 时为 `false`,warning 不默认阻止发布。保存与发布仍由宿主负责。
119
+
120
+ ```js
121
+ async function publishProcess(actions) {
122
+ const issues = actions.validate()
123
+ if (issues.some((issue) => issue.level === 'error')) return
124
+ await publishXml(actions.exportXml())
125
+ }
126
+ ```
111
127
 
112
128
  编辑器属性面板应订阅 `selection-change`,该事件覆盖节点、连线、多选、清空和键盘选择。`element-click` 是点击观察事件,不能代替选择状态。宿主使用稳定 BPMN Element ID 关联业务配置,并自行拥有保存、发布、权限和服务端事务。
113
129
 
130
+ ## Mode 双向同步与副标题刷新
131
+
132
+ ```vue
133
+ <script setup>
134
+ import { ref } from 'vue'
135
+
136
+ const studioRef = ref(null)
137
+ const mode = ref('design')
138
+ const summaries = new Map()
139
+ const resolveSubtitle = ({ node }) => summaries.has(node.id)
140
+ ? summaries.get(node.id)
141
+ : undefined
142
+
143
+ function updateSummary() {
144
+ summaries.set('ServiceTask_Archive', '归档到采购系统')
145
+ studioRef.value?.refreshPresentation()
146
+ }
147
+ </script>
148
+
149
+ <template>
150
+ <BpmnStudio
151
+ ref="studioRef"
152
+ v-model:mode="mode"
153
+ :allowed-modes="['design', 'viewer']"
154
+ :node-subtitle-resolver="resolveSubtitle"
155
+ @mode-change="event => console.log(event.source)"
156
+ >
157
+ <template #header-start="{ mode: actualMode }">
158
+ <HostHeader :mode="actualMode" />
159
+ </template>
160
+ </BpmnStudio>
161
+ </template>
162
+ ```
163
+
164
+ Expose 的 `getMode()` / `setMode()` 使用 Shell 实际状态。Resolver 返回 `undefined` 保留默认值、`null` 移除副标题行、字符串替换显示;函数引用变化会自动刷新,稳定闭包内部数据变化需显式调用 `refreshPresentation()`。Resolver 只影响 Design/Viewer 标准卡片和 SVG,不覆盖 Instance Runtime Presentation,也不修改 XML、History、Selection 或 Viewport。
165
+
114
166
  ## 只读展示与审批轨迹
115
167
 
116
168
  ```vue
package/dist/index.d.ts CHANGED
@@ -18,7 +18,12 @@ import type {
18
18
  BpmnStudioShell,
19
19
  ContextMenuRegistry,
20
20
  InteractionController,
21
+ NodeSubtitleResolver,
21
22
  StudioCommands,
23
+ StudioMode,
24
+ StudioModeChangeEvent,
25
+ StudioValidationEvent,
26
+ StudioValidationIssue,
22
27
  StudioShellActions,
23
28
  StudioShellOptions,
24
29
  StudioShellRegions,
@@ -37,6 +42,14 @@ import type {
37
42
  ViewerProjection,
38
43
  } from '@bpmn-nova/studio/viewer'
39
44
 
45
+ export type {
46
+ NodeSubtitleResolver,
47
+ NodeSubtitleResolverContext,
48
+ StudioValidationEvent,
49
+ StudioValidationIssue,
50
+ StudioValidationSource,
51
+ } from '@bpmn-nova/studio'
52
+
40
53
  export {
41
54
  createContextMenuRegistry,
42
55
  createDefaultContextMenuRegistry,
@@ -74,11 +87,13 @@ export interface BpmnCanvasProps extends BpmnNovaVisualProps {
74
87
  iconRegistry?: IconRegistry
75
88
  nodeRenderers?: Record<string, Function>
76
89
  nodeRenderer?: Function
90
+ nodeSubtitleResolver?: NodeSubtitleResolver
77
91
  svgExport?: ViewerOptions['svgExport']
78
92
  }
79
93
  export interface BpmnCanvasExposed {
80
94
  getCanvas(): CanvasInstance | null
81
95
  fitView(): void
96
+ refreshPresentation(): void
82
97
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
83
98
  exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
84
99
  openSvgExportPreview(options?: SvgExportOptions & { previewTitle?: string; onDownload?: (artifact: SvgExportArtifact) => void }): SvgExportPreviewController | undefined
@@ -115,6 +130,7 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
115
130
  contextMenuRegistry?: ContextMenuRegistry
116
131
  nodeRenderers?: Record<string, Function>
117
132
  nodeRenderer?: Function
133
+ nodeSubtitleResolver?: NodeSubtitleResolver
118
134
  slotsConfig?: StudioShellSlots
119
135
  layout?: StudioShellOptions['layout']
120
136
  regions?: StudioShellRegions
@@ -136,8 +152,8 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
136
152
  runtimeTraceOptions?: ViewerOptions['runtimeTraceOptions']
137
153
  onTraceClick?: (event: RuntimeTraceClickEvent) => void
138
154
  onElementClick?: ViewerOptions['onElementClick']
139
- mode?: 'design' | 'viewer' | 'instance'
140
- allowedModes?: Array<'design' | 'viewer' | 'instance'>
155
+ mode?: StudioMode
156
+ allowedModes?: readonly StudioMode[]
141
157
  projection?: ViewerProjection
142
158
  responsive?: boolean
143
159
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
@@ -146,6 +162,12 @@ export interface BpmnStudioExposed {
146
162
  getStudio(): BpmnStudioController
147
163
  getShell(): BpmnStudioShell | null
148
164
  getActions(): StudioShellActions | null
165
+ getMode(): StudioMode | undefined
166
+ getAllowedModes(): readonly StudioMode[]
167
+ setMode(mode: StudioMode): boolean
168
+ setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
169
+ refreshPresentation(): void
170
+ validate(): StudioValidationIssue[]
149
171
  exportXml(engine?: EngineId): string
150
172
  fitView(): void
151
173
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
@@ -157,9 +179,11 @@ export interface BpmnStudioSlotContext {
157
179
  readonly shell: BpmnStudioShell
158
180
  readonly actions: StudioShellActions
159
181
  readonly state: Readonly<StudioState>
182
+ readonly mode: StudioMode
160
183
  }
161
184
  export interface BpmnStudioSlots {
162
185
  'header-start'?(context: BpmnStudioSlotContext): VNodeChild
186
+ 'header-actions'?(context: BpmnStudioSlotContext): VNodeChild
163
187
  header?(context: BpmnStudioSlotContext): VNodeChild
164
188
  }
165
189
  export const BpmnStudio: DefineComponent<BpmnStudioProps> & {
@@ -184,6 +208,7 @@ export interface BpmnViewerProps extends BpmnNovaVisualProps, BpmnModelProps, Om
184
208
  export interface BpmnViewerExposed {
185
209
  getInstance(): ViewerInstance | null
186
210
  fitView(): void
211
+ refreshPresentation(): void
187
212
  setProjection(projection: ViewerProjection): void
188
213
  setDisplayOptions(options: Parameters<ViewerInstance['setDisplayOptions']>[0]): void
189
214
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
@@ -225,6 +250,9 @@ export interface BpmnStudioEmits {
225
250
  'scope-change': [activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState]
226
251
  'element-click': [payload: Parameters<NonNullable<ViewerOptions['onElementClick']>>[0]]
227
252
  'trace-click': [payload: RuntimeTraceClickEvent]
253
+ 'update:mode': [mode: StudioMode]
254
+ 'mode-change': [event: StudioModeChangeEvent]
255
+ validation: [event: StudioValidationEvent]
228
256
  }
229
257
  export interface BpmnDesignerEmits {
230
258
  change: [model: ProcessModel, reason: string, xml: string]
package/dist/index.js CHANGED
@@ -58,18 +58,20 @@ export function useBpmnStudio(options = {}) {
58
58
 
59
59
  export const BpmnCanvas = defineComponent({
60
60
  name: 'BpmnNovaCanvas',
61
- props: { studio: { type: Object, required: true }, interactions: Object, templates: Object, iconRegistry: Object, nodeRenderers: Object, nodeRenderer: Function, svgExport: Object, theme: [Object, String], onThemeChange: Function },
61
+ props: { studio: { type: Object, required: true }, interactions: Object, templates: Object, iconRegistry: Object, nodeRenderers: Object, nodeRenderer: Function, nodeSubtitleResolver: Function, svgExport: Object, theme: [Object, String], onThemeChange: Function },
62
62
  setup(props, { expose, attrs }) {
63
63
  const host = ref(null);
64
64
  let canvas = null;
65
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
65
66
  onMounted(() => {
66
67
  const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
67
- canvas = new CoreCanvas({ container: host.value, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
68
+ canvas = new CoreCanvas({ container: host.value, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, nodeSubtitleResolver, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
68
69
  requestAnimationFrame(() => canvas.fitView());
69
70
  });
70
71
  onBeforeUnmount(() => canvas?.destroy());
71
72
  watch(() => props.theme, (value) => { if (value !== undefined) canvas?.setTheme(value); });
72
- expose({ getCanvas: () => canvas, fitView: () => canvas?.fitView(), setTheme: (value) => canvas?.setTheme(value), exportSvg: (options) => canvas?.exportSvg(options), openSvgExportPreview: (options) => canvas?.openSvgExportPreview(options) });
73
+ watch(() => props.nodeSubtitleResolver, () => canvas?.refreshPresentation());
74
+ expose({ getCanvas: () => canvas, fitView: () => canvas?.fitView(), refreshPresentation: () => canvas?.refreshPresentation(), setTheme: (value) => canvas?.setTheme(value), exportSvg: (options) => canvas?.exportSvg(options), openSvgExportPreview: (options) => canvas?.openSvgExportPreview(options) });
73
75
  return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '320px', ...(attrs.style || {}) } });
74
76
  },
75
77
  });
@@ -126,25 +128,31 @@ export const BpmnStudio = defineComponent({
126
128
  props: {
127
129
  studio: Object, model: Object, xml: String, engine: { type: String, default: 'flowable' }, propertiesProfile: String, allowedNodeTypes: Array, allowedEdgeTypes: Array,
128
130
  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,
131
+ nodeRenderers: Object, nodeRenderer: Function, nodeSubtitleResolver: Function, slotsConfig: Object, layout: Function, regions: Object, leftWidth: Number, rightWidth: Number,
130
132
  runtime: Object, runtimePresenter: Function, runtimeTraceProjector: Function, runtimeAssetResolver: Function, runtimeTimelineRenderer: Function, onRuntimeTraceItemClick: Function, runtimeDetailsRenderer: Function, onRuntimeDetailsOpen: Function,
131
133
  runtimeTransitionDetailsRenderer: Function, onRuntimeTransitionDetailsOpen: Function,
132
134
  timeline: Object, runtimeDetails: Object, runtimeTraceOptions: Object, onTraceClick: Function, onElementClick: Function,
133
135
  mode: String, allowedModes: Array, projection: String, responsive: { type: Boolean, default: false }, projectionOptions: Array,
134
136
  theme: [Object, String], runtimeAppearance: Object, svgExport: Object, onThemeChange: Function,
135
137
  },
136
- emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click'],
138
+ emits: ['change', 'selection-change', 'scope-change', 'element-click', 'trace-click', 'update:mode', 'mode-change', 'validation'],
137
139
  setup(props, { emit, expose, attrs, slots }) {
138
140
  const host = ref(null);
139
141
  const headerTarget = ref(null);
140
142
  const headerStartTarget = ref(null);
143
+ const headerActionsTarget = ref(null);
141
144
  const headerContext = ref(null);
142
145
  const headerStartContext = ref(null);
146
+ const headerActionsContext = ref(null);
143
147
  const owned = !props.studio;
144
148
  const studio = props.studio || createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
145
149
  const state = reactive(studio.getState());
150
+ const actualMode = ref(props.mode || props.allowedModes?.[0] || 'design');
151
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
146
152
  let shell = null;
147
153
  let off = null;
154
+ let offMode = null;
155
+ let offValidation = null;
148
156
 
149
157
  const createTeleportSlot = (target, contextRef) => (context) => {
150
158
  target.value = context.container;
@@ -158,8 +166,18 @@ export const BpmnStudio = defineComponent({
158
166
  onMounted(() => {
159
167
  const shellSlots = { ...(props.slotsConfig || {}) };
160
168
  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 });
169
+ else if (!shellSlots.header) {
170
+ if (slots['header-start']) shellSlots.headerStart = createTeleportSlot(headerStartTarget, headerStartContext);
171
+ if (slots['header-actions']) shellSlots.headerActions = createTeleportSlot(headerActionsTarget, headerActionsContext);
172
+ }
173
+ 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, nodeSubtitleResolver, 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 });
174
+ actualMode.value = shell.getMode();
175
+ offMode = shell.subscribeMode((event) => {
176
+ actualMode.value = event.mode;
177
+ emit('update:mode', event.mode);
178
+ emit('mode-change', event);
179
+ });
180
+ offValidation = shell.subscribeValidation((event) => emit('validation', event));
163
181
  off = studio.subscribe((event) => {
164
182
  Object.assign(state, event.state);
165
183
  if (event.type === 'modelChanged') emit('change', studio.model, event.reason, studio.exportXml());
@@ -172,11 +190,16 @@ export const BpmnStudio = defineComponent({
172
190
  if (value && value !== studio.exportXml(props.engine)) studio.importXml(value, props.engine);
173
191
  });
174
192
  watch(() => props.runtime, (value) => { if (value !== undefined) shell?.setRuntime(value); });
175
- watch(() => props.mode, (value) => { if (value) shell?.setMode(value); });
193
+ watch(() => [props.allowedModes, props.mode], ([allowedModes, mode]) => {
194
+ if (!shell) return;
195
+ if (allowedModes !== undefined) shell.setAllowedModes(allowedModes);
196
+ if (mode) shell.setMode(mode);
197
+ }, { deep: true });
176
198
  watch(() => props.projection, (value) => { if (value) shell?.setProjection(value); });
177
199
  watch(() => props.regions, (value) => { if (value !== undefined) shell?.setRegions(value); }, { deep: true });
178
200
  watch(() => props.theme, (value) => { if (value !== undefined) shell?.setTheme(value); });
179
201
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) shell?.setRuntimeAppearance(value); });
202
+ watch(() => props.nodeSubtitleResolver, () => shell?.refreshPresentation());
180
203
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions], ([timeline, runtimeDetails, runtimeTraceOptions]) => {
181
204
  if (!shell) return;
182
205
  shell.rendererOptions.timeline = timeline;
@@ -184,8 +207,8 @@ export const BpmnStudio = defineComponent({
184
207
  shell.rendererOptions.runtimeTraceOptions = runtimeTraceOptions;
185
208
  shell.viewer?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions });
186
209
  });
187
- onBeforeUnmount(() => { off?.(); shell?.destroy(); if (owned) studio.destroy(); });
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) });
210
+ onBeforeUnmount(() => { offMode?.(); offValidation?.(); off?.(); shell?.destroy(); if (owned) studio.destroy(); });
211
+ expose({ getStudio: () => studio, getShell: () => shell, getActions: () => shell?.actions || null, getMode: () => shell?.getMode(), getAllowedModes: () => shell?.getAllowedModes() || [], setMode: (value) => shell?.setMode(value) || false, setAllowedModes: (value) => shell?.setAllowedModes(value) || [], refreshPresentation: () => shell?.refreshPresentation(), validate: () => shell?.validate() || [], exportXml: (engine) => studio.exportXml(engine), exportSvg: (options) => shell?.exportSvg(options), openSvgExportPreview: (options) => shell?.openSvgExportPreview(options), fitView: () => shell?.fitView(), setTheme: (value) => shell?.setTheme(value) });
189
212
  const renderSlot = (name, target, contextRef) => {
190
213
  if (!slots[name] || !target.value || !contextRef.value) return null;
191
214
  return h(Teleport, { to: target.value }, slots[name]({
@@ -193,12 +216,14 @@ export const BpmnStudio = defineComponent({
193
216
  shell: contextRef.value.shell,
194
217
  actions: contextRef.value.actions,
195
218
  state,
219
+ mode: actualMode.value,
196
220
  }));
197
221
  };
198
222
  return () => h(Fragment, null, [
199
223
  h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '520px', ...(attrs.style || {}) } }),
200
224
  renderSlot('header', headerTarget, headerContext),
201
225
  renderSlot('header-start', headerStartTarget, headerStartContext),
226
+ renderSlot('header-actions', headerActionsTarget, headerActionsContext),
202
227
  ]);
203
228
  },
204
229
  });
@@ -265,6 +290,7 @@ export const BpmnViewer = defineComponent({
265
290
  iconRegistry: Object,
266
291
  nodeRenderers: Object,
267
292
  nodeRenderer: Function,
293
+ nodeSubtitleResolver: Function,
268
294
  timeline: Object,
269
295
  runtimeDetails: Object,
270
296
  runtimePresenter: Function,
@@ -290,6 +316,7 @@ export const BpmnViewer = defineComponent({
290
316
  setup(props, { emit, expose, attrs }) {
291
317
  const host = ref(null);
292
318
  let instance = null;
319
+ const nodeSubtitleResolver = (context) => props.nodeSubtitleResolver?.(context);
293
320
  onMounted(() => {
294
321
  instance = new CoreViewer({
295
322
  container: host.value,
@@ -298,6 +325,7 @@ export const BpmnViewer = defineComponent({
298
325
  iconRegistry: props.iconRegistry,
299
326
  nodeRenderers: props.nodeRenderers,
300
327
  nodeRenderer: props.nodeRenderer,
328
+ nodeSubtitleResolver,
301
329
  timeline: props.timeline,
302
330
  runtimeDetails: props.runtimeDetails,
303
331
  runtimePresenter: props.runtimePresenter,
@@ -327,10 +355,11 @@ export const BpmnViewer = defineComponent({
327
355
  watch(() => props.projection, (value) => instance?.setProjection(value));
328
356
  watch(() => props.theme, (value) => { if (value !== undefined) instance?.setTheme(value); });
329
357
  watch(() => props.runtimeAppearance, (value) => { if (value !== undefined) instance?.setRuntimeAppearance(value); });
358
+ watch(() => props.nodeSubtitleResolver, () => instance?.refreshPresentation());
330
359
  watch(() => [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver], ([timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver]) => instance?.setDisplayOptions({ timeline, runtimeDetails, runtimeTraceOptions, runtimeAssetResolver }));
331
360
  watch(() => props.model, (value) => { if (value) instance?.setModel(value); });
332
361
  watch(() => props.xml, (value) => { if (value) instance?.setModel(importBpmn(value, props.engine)); });
333
- expose({ getInstance: () => instance, fitView: () => instance?.fitView(), setProjection: (p) => instance?.setProjection(p), setDisplayOptions: (value) => instance?.setDisplayOptions(value), setTheme: (value) => instance?.setTheme(value), exportSvg: (options) => instance?.exportSvg(options), openSvgExportPreview: (options) => instance?.openSvgExportPreview(options) });
362
+ expose({ getInstance: () => instance, fitView: () => instance?.fitView(), refreshPresentation: () => instance?.refreshPresentation(), setProjection: (p) => instance?.setProjection(p), setDisplayOptions: (value) => instance?.setDisplayOptions(value), setTheme: (value) => instance?.setTheme(value), exportSvg: (options) => instance?.exportSvg(options), openSvgExportPreview: (options) => instance?.openSvgExportPreview(options) });
334
363
  return () => h('div', { ...attrs, ref: host, style: { width: '100%', height: '100%', minHeight: '260px', ...(attrs.style || {}) } });
335
364
  },
336
365
  });
package/dist/styles.css CHANGED
@@ -511,8 +511,8 @@
511
511
  .mb-node-event,
512
512
  .mb-node-boundary { display: grid; place-items: center; z-index: 7; }
513
513
  .mb-event-shape {
514
- width: 46px;
515
- height: 46px;
514
+ width: var(--mb-event-shape-size, 46px);
515
+ height: var(--mb-event-shape-size, 46px);
516
516
  box-sizing: border-box;
517
517
  display: grid;
518
518
  place-items: center;
@@ -523,7 +523,7 @@
523
523
  box-shadow: var(--nova-shadow-sm);
524
524
  position: relative;
525
525
  }
526
- .mb-node-boundary .mb-event-shape { width: 40px; height: 40px; border-width: 2px; }
526
+ .mb-node-boundary .mb-event-shape { width: var(--mb-event-shape-size, 40px); height: var(--mb-event-shape-size, 40px); border-width: 2px; }
527
527
  .mb-event-shape[data-stage="intermediate"],
528
528
  .mb-node-boundary .mb-event-shape { box-shadow: inset 0 0 0 3px var(--nova-color-surface), inset 0 0 0 4.5px var(--nova-color-text-muted, #8796a9), var(--nova-shadow-sm); }
529
529
  .mb-event-shape[data-stage="end"] { border-width: 4px; border-color: var(--nova-color-text-secondary); }
@@ -1159,6 +1159,7 @@ textarea.property-control { resize: vertical; }
1159
1159
  .nova-studio-projection-switch button:hover { color: var(--nova-tone-primary-foreground, #4d59ca); background: var(--nova-color-primary-soft, #f4f5ff); }.nova-studio-projection-switch button.is-active { color: var(--nova-tone-primary-foreground, #4654c8); background: var(--nova-color-primary-soft, #eef0ff); font-weight: var(--nova-font-weight-semibold); }
1160
1160
  .nova-studio-tools { min-width: 0; overflow: visible; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
1161
1161
  .nova-studio-tools::-webkit-scrollbar { display: none; }
1162
+ .nova-studio-header-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 5px; }
1162
1163
  .nova-studio-tool-divider { flex: none; width: 1px; height: 20px; margin: 0 2px; background: var(--nova-color-divider, #e4e8f0); }
1163
1164
  .nova-studio-tool { flex: none; min-height: 32px; border: 1px solid var(--nova-color-border, #e2e6ef); border-radius: 7px; padding: 0 10px; display: inline-flex; align-items: center; justify-content: center; gap: 5px; color: var(--nova-color-text-secondary, #4c586d); background: var(--nova-color-surface, #fff); font-size: var(--nova-font-size-sm); line-height: 18px; white-space: nowrap; cursor: pointer; }
1164
1165
  .nova-studio-tool:focus { outline: none; }.nova-studio-tool:focus-visible { outline: 2px solid var(--nova-color-focus-ring, #aeb7ff); outline-offset: 1px; }