@bpmn-nova/react 0.3.2-preview → 0.3.4-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 的 React 18+ Adapter,提供 Studio、Designer、Viewer、审批轨
4
4
 
5
5
  > **English summary:** React 18+ components and refs for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
6
6
 
7
- > 当前版本为 `0.3.2-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
7
+ > 当前版本为 `0.3.4-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
8
8
 
9
9
  ![BPMN Nova React 流程工作台](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -20,6 +20,20 @@ import '@bpmn-nova/react/styles.css'
20
20
 
21
21
  只安装本包;`@bpmn-nova/studio` 会作为内部依赖自动解析。容器必须有明确高度。
22
22
 
23
+ TypeScript 类型也从 React 入口导入,不要为了模型类型额外声明 Studio 直依赖:
24
+
25
+ ```ts
26
+ import type {
27
+ BpmnNode,
28
+ EdgeType,
29
+ ElementSelection,
30
+ NodeType,
31
+ ProcessModel,
32
+ } from '@bpmn-nova/react'
33
+ ```
34
+
35
+ 公共类型门面还包含 `BpmnEdge`、`EngineId` 和 `LayoutOptions`。
36
+
23
37
  使用代码生成助手或 IDE Agent 接入时,从随包的 `llms.txt` 开始;需要完整接口和定制资料时再读取 `llms-full.txt`。两份文件都包含在 npm tarball 中,不依赖源码仓库可见性。
24
38
 
25
39
  ## 流程设计
@@ -50,7 +64,7 @@ export function WorkflowEditor({ initialXml }) {
50
64
  }
51
65
  ```
52
66
 
53
- Ref 提供 `actions`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
67
+ Ref 提供 `actions`、`validate()`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()` 和 `openSvgExportPreview()`。
54
68
 
55
69
  `xml`/`model` 是外部替换输入,`onChange` 的 XML 是草稿输出。宿主回写完全相同的导出 XML 不会重新导入或清空撤销历史;切换服务器 revision 或流程时传入不同 XML 会替换模型。Activiti 宿主应显式使用 `engine="activiti"`。
56
70
 
@@ -72,17 +86,70 @@ Ref 提供 `actions`、`exportXml()`、`fitView()`、`setTheme()`、`exportSvg()
72
86
  headerStart={({ state, actions }) => (
73
87
  <HostProcessIdentity onBack={back} />
74
88
  )}
89
+ headerActions={({ actions, mode }) => (
90
+ <HostWorkflowActions
91
+ disabled={mode !== 'design'}
92
+ onValidate={() => actions.validate()}
93
+ onSave={() => saveDraft(actions.exportXml())}
94
+ onPublish={() => publishProcess(actions)}
95
+ />
96
+ )}
75
97
  onChange={handleChange}
98
+ onValidation={(event) => handleValidation(event)}
76
99
  onSelectionChange={(selection, element) => {
77
100
  setSelectedElement(selection ? element : null)
78
101
  }}
79
102
  />
80
103
  ```
81
104
 
82
- `headerStart` 只替换 Nova Brand,默认模式区与工具区保留;`header` 可完整替换 Header。两者都通过 React Portal 在当前组件树内渲染,保留 Context 和生命周期。完整替换时使用 Render Context 的 `state.canUndo/canRedo` 和 `actions` 重建所需编辑按钮。
105
+ `headerStart` 只替换 Nova Brand;`headerActions` 只替换默认“校验 / 导入 / 导出”动作组,适合组合“校验 / 保存 / 发布”;`header` 可完整替换 Header。它们都通过 React Portal 在当前组件树内渲染,保留 Context 和生命周期。默认 Header 的最佳视图只保留在底部缩放区。
106
+
107
+ `actions.validate()` 以 `toolbar` 来源先更新 Nova 默认状态栏,再调用 `onValidation` 并返回 issues;Ref `validate()` 使用 `api` 来源。事件的 `valid` 仅由 error 决定,warning 会保留在 `issues` 中但不默认阻止发布。保存与发布仍由宿主负责。
108
+
109
+ ```js
110
+ async function publishProcess(actions) {
111
+ const issues = actions.validate()
112
+ if (issues.some((issue) => issue.level === 'error')) return
113
+ await publishXml(actions.exportXml())
114
+ }
115
+ ```
83
116
 
84
117
  宿主属性面板以 `onSelectionChange` 为选择状态来源;它覆盖节点、连线、多选、清空与键盘选择。`onElementClick` 只是点击观察事件,不能代替选择状态。宿主通过稳定 BPMN Element ID 关联业务配置,并自行负责保存、发布、权限与服务端事务。
85
118
 
119
+ ## Mode 双向同步与副标题刷新
120
+
121
+ ```jsx
122
+ import { useCallback, useRef, useState } from 'react'
123
+
124
+ const [mode, setMode] = useState('design')
125
+ const summaries = useRef(new Map())
126
+ const resolveSubtitle = useCallback(
127
+ ({ node }) => summaries.current.has(node.id)
128
+ ? summaries.current.get(node.id)
129
+ : undefined,
130
+ [],
131
+ )
132
+
133
+ <BpmnStudio
134
+ ref={studioRef}
135
+ xml={xml}
136
+ mode={mode}
137
+ allowedModes={['design', 'viewer']}
138
+ onModeChange={(event) => setMode(event.mode)}
139
+ nodeSubtitleResolver={resolveSubtitle}
140
+ headerStart={({ mode: actualMode }) => (
141
+ <HostHeader mode={actualMode} />
142
+ )}
143
+ />
144
+
145
+ function updateArchiveSummary() {
146
+ summaries.current.set('ServiceTask_Archive', '归档到采购系统')
147
+ studioRef.current?.refreshPresentation()
148
+ }
149
+ ```
150
+
151
+ Ref 的 `mode` Getter 与 `setMode()` 始终反映 Shell 实际状态。Resolver 返回 `undefined` 保留默认值、`null` 移除副标题行、字符串替换显示;函数引用变化会自动刷新,稳定闭包内部数据变化需显式调用 `refreshPresentation()`。Resolver 只影响 Design/Viewer 标准卡片和 SVG,不覆盖 Instance Runtime Presentation,也不修改 XML、History、Selection 或 Viewport。
152
+
86
153
  ## 只读展示与审批轨迹
87
154
 
88
155
  ```jsx
package/dist/index.d.ts CHANGED
@@ -29,8 +29,13 @@ import type {
29
29
  BpmnStudioShell,
30
30
  ContextMenuRegistry,
31
31
  InteractionController,
32
+ NodeSubtitleResolver,
32
33
  StudioCommands,
33
34
  StudioShellActions,
35
+ StudioMode,
36
+ StudioModeChangeEvent,
37
+ StudioValidationEvent,
38
+ StudioValidationIssue,
34
39
  StudioShellOptions,
35
40
  StudioShellRegions,
36
41
  StudioShellSlots,
@@ -48,6 +53,16 @@ import type {
48
53
  ViewerProjection,
49
54
  } from '@bpmn-nova/studio/viewer'
50
55
 
56
+ export type {
57
+ BpmnEdge,
58
+ BpmnNode,
59
+ EdgeType,
60
+ ElementSelection,
61
+ EngineId,
62
+ LayoutOptions,
63
+ NodeType,
64
+ ProcessModel,
65
+ } from '@bpmn-nova/studio'
51
66
  export {
52
67
  createContextMenuRegistry,
53
68
  createDefaultContextMenuRegistry,
@@ -67,6 +82,13 @@ export {
67
82
  registerBpmnProperties,
68
83
  registerFlowableProperties,
69
84
  } from '@bpmn-nova/studio'
85
+ export type {
86
+ NodeSubtitleResolver,
87
+ NodeSubtitleResolverContext,
88
+ StudioValidationEvent,
89
+ StudioValidationIssue,
90
+ StudioValidationSource,
91
+ } from '@bpmn-nova/studio'
70
92
 
71
93
  export interface BpmnNovaVisualProps {
72
94
  theme?: NovaThemeInput
@@ -96,11 +118,13 @@ export interface BpmnCanvasProps extends BpmnNovaVisualProps {
96
118
  iconRegistry?: IconRegistry
97
119
  nodeRenderers?: Record<string, Function>
98
120
  nodeRenderer?: Function
121
+ nodeSubtitleResolver?: NodeSubtitleResolver
99
122
  svgExport?: ViewerOptions['svgExport']
100
123
  }
101
124
  export interface BpmnCanvasHandle {
102
125
  readonly instance: CanvasInstance | null
103
126
  fitView(padding?: number, options?: Record<string, unknown>): void
127
+ refreshPresentation(): void
104
128
  clientToWorld(x: number, y: number): { x: number; y: number } | undefined
105
129
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
106
130
  exportSvg(options?: SvgExportOptions): Promise<SvgExportArtifact> | undefined
@@ -138,14 +162,16 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
138
162
  contextMenuRegistry?: ContextMenuRegistry
139
163
  nodeRenderers?: Record<string, Function>
140
164
  nodeRenderer?: Function
165
+ nodeSubtitleResolver?: NodeSubtitleResolver
141
166
  slots?: StudioShellSlots
142
167
  layout?: StudioShellOptions['layout']
143
168
  regions?: StudioShellRegions
144
169
  headerStart?: BpmnStudioRenderSlot
170
+ headerActions?: BpmnStudioRenderSlot
145
171
  header?: BpmnStudioRenderSlot
146
172
  runtime?: ProcessInstanceSnapshot | null
147
- mode?: 'design' | 'viewer' | 'instance'
148
- allowedModes?: Array<'design' | 'viewer' | 'instance'>
173
+ mode?: StudioMode
174
+ allowedModes?: readonly StudioMode[]
149
175
  projection?: ViewerProjection
150
176
  responsive?: boolean
151
177
  projectionOptions?: Array<{ value: ViewerProjection; label: string }>
@@ -169,18 +195,28 @@ export interface BpmnStudioProps extends BpmnNovaVisualProps, BpmnModelProps {
169
195
  onChange?: (model: ProcessModel, reason: string, xml: string) => void
170
196
  onSelectionChange?: (selection: ElementSelection | null, element: StudioState['selectedElement']) => void
171
197
  onScopeChange?: (activeScopeId: string, scopePath: StudioState['scopePath'], state: StudioState) => void
198
+ onModeChange?: (event: StudioModeChangeEvent) => void
199
+ onValidation?: (event: StudioValidationEvent) => void
172
200
  }
173
201
  export interface BpmnStudioRenderContext {
174
202
  readonly studio: BpmnStudioController
175
203
  readonly shell: BpmnStudioShell
176
204
  readonly actions: StudioShellActions
177
205
  readonly state: Readonly<StudioState>
206
+ readonly mode: StudioMode
178
207
  }
179
208
  export type BpmnStudioRenderSlot = ReactNode | ((context: BpmnStudioRenderContext) => ReactNode)
180
209
  export interface BpmnStudioHandle {
181
210
  readonly studio: BpmnStudioController
182
211
  readonly shell: BpmnStudioShell | null
183
212
  readonly actions: StudioShellActions | null
213
+ readonly mode: StudioMode | undefined
214
+ getMode(): StudioMode | undefined
215
+ getAllowedModes(): readonly StudioMode[]
216
+ setMode(mode: StudioMode): boolean
217
+ setAllowedModes(modes: readonly StudioMode[]): readonly StudioMode[]
218
+ refreshPresentation(): void
219
+ validate(): StudioValidationIssue[]
184
220
  exportXml(engine?: EngineId): string
185
221
  fitView(): void
186
222
  setTheme(theme: NovaThemeInput): NovaThemeState | undefined
@@ -212,6 +248,7 @@ export interface BpmnViewerProps extends BpmnNovaVisualProps, BpmnModelProps, Om
212
248
  export interface BpmnViewerHandle {
213
249
  readonly instance: ViewerInstance | null
214
250
  fitView(): void
251
+ refreshPresentation(): void
215
252
  setProjection(projection: ViewerProjection): void
216
253
  setRuntime(runtime: ProcessInstanceSnapshot | null): void
217
254
  setDisplayOptions(options: Parameters<ViewerInstance['setDisplayOptions']>[0]): void
package/dist/index.js CHANGED
@@ -62,9 +62,13 @@ export function useBpmnStudio(options = {}) {
62
62
  export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
63
63
  const hostRef = useRef(null);
64
64
  const instanceRef = useRef(null);
65
+ const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
66
+ subtitleResolverRef.current = props.nodeSubtitleResolver;
67
+ const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
65
68
  useImperativeHandle(ref, () => ({
66
69
  get instance() { return instanceRef.current; },
67
70
  fitView(padding, options) { instanceRef.current?.fitView(padding, options); },
71
+ refreshPresentation() { instanceRef.current?.refreshPresentation(); },
68
72
  clientToWorld(x, y) { return instanceRef.current?.clientToWorld(x, y); },
69
73
  setTheme(theme) { return instanceRef.current?.setTheme(theme); },
70
74
  exportSvg(options) { return instanceRef.current?.exportSvg(options); },
@@ -73,12 +77,13 @@ export const BpmnCanvas = forwardRef(function BpmnCanvas(props, ref) {
73
77
  useEffect(() => {
74
78
  if (!hostRef.current || !props.studio) return undefined;
75
79
  const interactions = props.interactions || createInteractionController({ studio: props.studio, templates: props.templates || createTemplateRegistry() });
76
- const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
80
+ const instance = new CoreCanvas({ container: hostRef.current, studio: props.studio, interactions, theme: props.theme, onThemeChange: props.onThemeChange, nodeSubtitleResolver: stableSubtitleResolverRef.current, rendererOptions: { iconRegistry: props.iconRegistry, nodeRenderers: props.nodeRenderers, nodeRenderer: props.nodeRenderer, svgExport: props.svgExport } });
77
81
  instanceRef.current = instance;
78
82
  requestAnimationFrame(() => instance.fitView());
79
83
  return () => { instance.destroy(); instanceRef.current = null; };
80
84
  }, [props.studio]);
81
85
  useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
86
+ useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
82
87
  return React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 320, ...props.style } });
83
88
  });
84
89
 
@@ -105,15 +110,27 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
105
110
  const mountedModelSyncRef = useRef(false);
106
111
  const [headerPortal, setHeaderPortal] = useState(null);
107
112
  const [headerStartPortal, setHeaderStartPortal] = useState(null);
113
+ const [headerActionsPortal, setHeaderActionsPortal] = useState(null);
108
114
  const callbacksRef = useRef({});
109
115
  callbacksRef.current = props;
110
116
  if (!props.studio && !ownedStudioRef.current) ownedStudioRef.current = createStudioController({ model: resolveModel(props), propertiesProfile: props.propertiesProfile, allowedNodeTypes: props.allowedNodeTypes, allowedEdgeTypes: props.allowedEdgeTypes });
111
117
  const studio = props.studio || ownedStudioRef.current;
112
118
  const [studioState, setStudioState] = useState(() => studio.getState());
119
+ const [actualMode, setActualMode] = useState(() => props.mode || props.allowedModes?.[0] || 'design');
120
+ const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
121
+ subtitleResolverRef.current = props.nodeSubtitleResolver;
122
+ const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
113
123
  useImperativeHandle(ref, () => ({
114
124
  studio,
115
125
  get shell() { return shellRef.current; },
116
126
  get actions() { return shellRef.current?.actions || null; },
127
+ get mode() { return shellRef.current?.getMode(); },
128
+ getMode: () => shellRef.current?.getMode(),
129
+ getAllowedModes: () => shellRef.current?.getAllowedModes() || [],
130
+ setMode: (mode) => shellRef.current?.setMode(mode) || false,
131
+ setAllowedModes: (modes) => shellRef.current?.setAllowedModes(modes) || [],
132
+ refreshPresentation: () => shellRef.current?.refreshPresentation(),
133
+ validate: () => shellRef.current?.validate() || [],
117
134
  exportXml: (engine) => studio.exportXml(engine),
118
135
  exportSvg: (options) => shellRef.current?.exportSvg(options),
119
136
  openSvgExportPreview: (options) => shellRef.current?.openSvgExportPreview(options),
@@ -122,6 +139,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
122
139
  }), [studio]);
123
140
  const hasNativeHeader = props.header !== undefined && props.header !== null;
124
141
  const hasNativeHeaderStart = props.headerStart !== undefined && props.headerStart !== null;
142
+ const hasNativeHeaderActions = props.headerActions !== undefined && props.headerActions !== null;
125
143
  useEffect(() => {
126
144
  if (!hostRef.current) return undefined;
127
145
  setStudioState(studio.getState());
@@ -132,7 +150,10 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
132
150
  };
133
151
  const shellSlots = { ...(props.slots || {}) };
134
152
  if (hasNativeHeader) shellSlots.header = createPortalSlot(setHeaderPortal);
135
- else if (!shellSlots.header && hasNativeHeaderStart) shellSlots.headerStart = createPortalSlot(setHeaderStartPortal);
153
+ else if (!shellSlots.header) {
154
+ if (hasNativeHeaderStart) shellSlots.headerStart = createPortalSlot(setHeaderStartPortal);
155
+ if (hasNativeHeaderActions) shellSlots.headerActions = createPortalSlot(setHeaderActionsPortal);
156
+ }
136
157
  const shell = new CoreStudioShell({
137
158
  container: hostRef.current,
138
159
  studio,
@@ -144,6 +165,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
144
165
  rendererOptions: {
145
166
  nodeRenderers: props.nodeRenderers,
146
167
  nodeRenderer: props.nodeRenderer,
168
+ nodeSubtitleResolver: stableSubtitleResolverRef.current,
147
169
  runtimePresenter: props.runtimePresenter,
148
170
  runtimeTraceProjector: props.runtimeTraceProjector,
149
171
  runtimeAssetResolver: props.runtimeAssetResolver,
@@ -176,14 +198,20 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
176
198
  onThemeChange: (state) => callbacksRef.current.onThemeChange?.(state),
177
199
  });
178
200
  shellRef.current = shell;
201
+ setActualMode(shell.getMode());
202
+ const offMode = shell.subscribeMode((event) => {
203
+ setActualMode(event.mode);
204
+ callbacksRef.current.onModeChange?.(event);
205
+ });
206
+ const offValidation = shell.subscribeValidation((event) => callbacksRef.current.onValidation?.(event));
179
207
  const off = studio.subscribe((event) => {
180
208
  setStudioState(event.state);
181
209
  if (event.type === 'modelChanged') callbacksRef.current.onChange?.(studio.model, event.reason, studio.exportXml());
182
210
  if (event.type === 'selectionChanged') callbacksRef.current.onSelectionChange?.(studio.selection, studio.getSelectedElement());
183
211
  if (event.type === 'scopeChanged') callbacksRef.current.onScopeChange?.(event.activeScopeId, event.scopePath, studio.getState());
184
212
  });
185
- return () => { off(); shell.destroy(); shellRef.current = null; };
186
- }, [studio, hasNativeHeader, hasNativeHeaderStart]);
213
+ return () => { offMode(); offValidation(); off(); shell.destroy(); shellRef.current = null; };
214
+ }, [studio, hasNativeHeader, hasNativeHeaderStart, hasNativeHeaderActions]);
187
215
  useEffect(() => () => {
188
216
  if (!props.studio && ownedStudioRef.current === studio) {
189
217
  studio.destroy();
@@ -199,11 +227,17 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
199
227
  else if (props.xml && props.xml !== studio.exportXml(props.engine)) studio.importXml(props.xml, props.engine);
200
228
  }, [studio, props.model, props.xml, props.engine]);
201
229
  useEffect(() => { if (props.runtime !== undefined) shellRef.current?.setRuntime(props.runtime); }, [props.runtime]);
202
- useEffect(() => { if (props.mode) shellRef.current?.setMode(props.mode); }, [props.mode]);
230
+ useEffect(() => {
231
+ const shell = shellRef.current;
232
+ if (!shell) return;
233
+ if (props.allowedModes !== undefined) shell.setAllowedModes(props.allowedModes);
234
+ if (props.mode) shell.setMode(props.mode);
235
+ }, [props.allowedModes, props.mode]);
203
236
  useEffect(() => { if (props.projection) shellRef.current?.setProjection(props.projection); }, [props.projection]);
204
237
  useEffect(() => { if (props.regions !== undefined) shellRef.current?.setRegions(props.regions); }, [props.regions]);
205
238
  useEffect(() => { if (props.theme !== undefined) shellRef.current?.setTheme(props.theme); }, [props.theme]);
206
239
  useEffect(() => { if (props.runtimeAppearance !== undefined) shellRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
240
+ useEffect(() => { shellRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
207
241
  useEffect(() => {
208
242
  const shell = shellRef.current;
209
243
  if (!shell) return;
@@ -219,6 +253,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
219
253
  shell: portal.context.shell,
220
254
  actions: portal.context.actions,
221
255
  state: studioState,
256
+ mode: actualMode,
222
257
  };
223
258
  return createPortal(typeof slot === 'function' ? slot(context) : slot, portal.target, key);
224
259
  };
@@ -226,6 +261,7 @@ export const BpmnStudio = forwardRef(function BpmnStudio(props, ref) {
226
261
  React.createElement('div', { ref: hostRef, className: props.className, style: { width: '100%', height: '100%', minHeight: 520, ...props.style } }),
227
262
  renderSlot(props.header, headerPortal, 'bpmn-nova-header'),
228
263
  renderSlot(props.headerStart, headerStartPortal, 'bpmn-nova-header-start'),
264
+ renderSlot(props.headerActions, headerActionsPortal, 'bpmn-nova-header-actions'),
229
265
  );
230
266
  });
231
267
 
@@ -290,6 +326,9 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
290
326
  const hostRef = useRef(null);
291
327
  const instanceRef = useRef(null);
292
328
  const callbacksRef = useRef({});
329
+ const subtitleResolverRef = useRef(props.nodeSubtitleResolver);
330
+ subtitleResolverRef.current = props.nodeSubtitleResolver;
331
+ const stableSubtitleResolverRef = useRef((context) => subtitleResolverRef.current?.(context));
293
332
  callbacksRef.current = {
294
333
  onTraceClick: props.onTraceClick,
295
334
  onElementClick: props.onElementClick,
@@ -303,6 +342,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
303
342
  useImperativeHandle(ref, () => ({
304
343
  get instance() { return instanceRef.current; },
305
344
  fitView() { instanceRef.current?.fitView(); },
345
+ refreshPresentation() { instanceRef.current?.refreshPresentation(); },
306
346
  setProjection(value) { instanceRef.current?.setProjection(value); },
307
347
  setRuntime(value) { instanceRef.current?.setRuntime(value); },
308
348
  setDisplayOptions(value) { instanceRef.current?.setDisplayOptions(value); },
@@ -325,6 +365,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
325
365
  iconRegistry: props.iconRegistry,
326
366
  nodeRenderers: props.nodeRenderers,
327
367
  nodeRenderer: props.nodeRenderer,
368
+ nodeSubtitleResolver: stableSubtitleResolverRef.current,
328
369
  runtimePresenter: props.runtimePresenter,
329
370
  runtimeTraceProjector: props.runtimeTraceProjector,
330
371
  runtimeAssetResolver: props.runtimeAssetResolver,
@@ -354,6 +395,7 @@ export const BpmnViewer = forwardRef(function BpmnViewer(props, ref) {
354
395
  useEffect(() => { if (instanceRef.current && props.projection) instanceRef.current.setProjection(props.projection); }, [props.projection]);
355
396
  useEffect(() => { if (props.theme !== undefined) instanceRef.current?.setTheme(props.theme); }, [props.theme]);
356
397
  useEffect(() => { if (props.runtimeAppearance !== undefined) instanceRef.current?.setRuntimeAppearance(props.runtimeAppearance); }, [props.runtimeAppearance]);
398
+ useEffect(() => { instanceRef.current?.refreshPresentation(); }, [props.nodeSubtitleResolver]);
357
399
  useEffect(() => { instanceRef.current?.setDisplayOptions({ timeline: props.timeline, runtimeDetails: props.runtimeDetails, runtimeTraceOptions: props.runtimeTraceOptions, runtimeAssetResolver: props.runtimeAssetResolver }); }, [props.timeline, props.runtimeDetails, props.runtimeTraceOptions, props.runtimeAssetResolver]);
358
400
  useEffect(() => {
359
401
  if (!instanceRef.current) return;
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; }