@bpmn-nova/react 0.3.4-preview → 0.3.5-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -75,6 +75,8 @@ Use `engine="flowable"` or `engine="activiti"` explicitly from the host workflow
75
75
 
76
76
  Studio modes are presentation state: `design` means editable process design, `viewer` means read-only process viewing, and `instance` means approval trace. Observe the actual Shell state through `subscribeMode()` or framework mode events; never store it in the BPMN model.
77
77
 
78
+ Node geometry is also presentation, not DI normalization. Loading server XML via the component parses its BPMN DI Bounds; XML export already writes model dimensions. The `0.3.5-preview` gateway/data fix scales standard shells to fit those bounds and docks display endpoints without rewriting model geometry or waypoints. Do not resize stored nodes, recreate them, or patch internal CSS to compensate. See [Node DI and visual geometry](docs/API.md#节点-di-与视觉尺寸) and the customization section in the co-located `llms-full.txt`; `node-geometry` is internal, not another installable package.
79
+
78
80
  ## 4. Mount a minimal integration
79
81
 
80
82
  Every branch must import its own public `styles.css` exactly once and provide a height through the complete parent layout chain.
@@ -90,6 +92,10 @@ import '@bpmn-nova/vue/styles.css'
90
92
  const props = defineProps({ initialXml: String })
91
93
  const emit = defineEmits(['change'])
92
94
  const studioRef = ref(null)
95
+ const config = {
96
+ modeling: { allowedEdgeTypes: ['sequenceFlow'] },
97
+ ui: { controlSize: 'medium' },
98
+ }
93
99
 
94
100
  function handleChange(model, reason, xml) {
95
101
  emit('change', xml)
@@ -102,8 +108,8 @@ function handleChange(model, reason, xml) {
102
108
  ref="studioRef"
103
109
  :xml="props.initialXml"
104
110
  engine="flowable"
111
+ :config="config"
105
112
  mode="design"
106
- :allowed-edge-types="['sequenceFlow']"
107
113
  :allowed-modes="['design', 'viewer']"
108
114
  theme="auto"
109
115
  @change="handleChange"
@@ -119,6 +125,11 @@ import { useRef } from 'react'
119
125
  import { BpmnStudio } from '@bpmn-nova/react'
120
126
  import '@bpmn-nova/react/styles.css'
121
127
 
128
+ const STUDIO_CONFIG = {
129
+ modeling: { allowedEdgeTypes: ['sequenceFlow'] },
130
+ ui: { controlSize: 'medium' },
131
+ }
132
+
122
133
  export function WorkflowEditor({ initialXml, saveDraft }) {
123
134
  const studioRef = useRef(null)
124
135
  return (
@@ -127,8 +138,8 @@ export function WorkflowEditor({ initialXml, saveDraft }) {
127
138
  ref={studioRef}
128
139
  xml={initialXml}
129
140
  engine="flowable"
141
+ config={STUDIO_CONFIG}
130
142
  mode="design"
131
- allowedEdgeTypes={['sequenceFlow']}
132
143
  allowedModes={['design', 'viewer']}
133
144
  theme="auto"
134
145
  onChange={(model, reason, xml) => saveDraft(xml)}
@@ -148,13 +159,19 @@ import {
148
159
  } from '@bpmn-nova/studio'
149
160
  import '@bpmn-nova/studio/styles.css'
150
161
 
162
+ const config = {
163
+ modeling: { allowedEdgeTypes: ['sequenceFlow'] },
164
+ ui: { controlSize: 'medium' },
165
+ }
166
+
151
167
  const studio = createStudioController({
152
168
  model: createEmptyProcess('flowable'),
153
- allowedEdgeTypes: ['sequenceFlow'],
169
+ config,
154
170
  })
155
171
  const shell = createStudioShell({
156
172
  container: document.querySelector('#workflow-studio'),
157
173
  studio,
174
+ config,
158
175
  mode: 'design',
159
176
  allowedModes: ['design', 'viewer'],
160
177
  theme: 'auto',
@@ -177,18 +194,16 @@ This step is complete when the workbench is visible, the initial XML is rendered
177
194
 
178
195
  ### 4.1 Embed the complete Studio in an existing business workbench
179
196
 
180
- `BpmnStudio` is a complete Header + Palette + Canvas + Properties + Statusbar workbench by default. Preserve that default unless the host already owns a region. To keep the Nova Palette and editing tools, replace only the Header start area and explicitly hide Nova Properties:
197
+ `BpmnStudio` is a complete Header + Palette + Canvas + Properties + Statusbar workbench by default. Preserve that default unless the host already owns a region. Define a stable `studioConfig` object in host code. To keep the Nova Palette and editing tools, replace only the Header start area and explicitly hide Nova Properties:
181
198
 
182
199
  ```vue
183
200
  <BpmnStudio
184
201
  ref="studioRef"
185
202
  :xml="xml"
186
203
  engine="activiti"
204
+ :config="studioConfig"
187
205
  mode="design"
188
206
  :allowed-modes="['design']"
189
- :allowed-node-types="supportedNodeTypes"
190
- :allowed-edge-types="['sequenceFlow']"
191
- :regions="{ right: 'hidden' }"
192
207
  theme="auto"
193
208
  @change="handleChange"
194
209
  @selection-change="handleSelectionChange"
@@ -196,23 +211,43 @@ This step is complete when the workbench is visible, the initial XML is rendered
196
211
  <template #header-start="{ state, actions }">
197
212
  <!-- Host back action, business icon, process name, and type -->
198
213
  </template>
199
- <template #header-actions="{ actions, mode }">
214
+ <template #header-actions="{ actions, mode, ui }">
200
215
  <!-- Host Validate / Save / Publish actions. Validation calls actions.validate(). -->
201
216
  </template>
202
217
  </BpmnStudio>
203
218
  ```
204
219
 
205
- - Vue uses native `#header-start` / `#header-actions` / `#header` slots. React uses `headerStart` / `headerActions` / `header` render props. Core uses matching DOM Slots.
220
+ - Vue uses native `#header-start` / `#header-actions` / `#header` / `#right` slots. React uses `headerStart` / `headerActions` / `header` / `right` render props. Core uses matching DOM Slots. Native Right support starts in `0.3.5-preview`; inspect the installed declarations before using it.
206
221
  - Vue supports `v-model:mode` and `mode-change`; React supports `mode` and `onModeChange`. Framework Header contexts expose the actual reactive `mode`. Core Slots use `getMode()` / `subscribeMode()`.
207
222
  - Header Start replaces only the Nova Brand. Header Actions replaces only the default Validate / Import / Export group, so a host can compose Validate / Save / Publish without rebuilding the editing tools. A complete Header replacement calls the public Actions Interface.
208
223
  - `actions.validate()` updates Nova's default status summary before emitting a read-only validation result with `source: 'toolbar'`; `shell.validate()` and framework Handle/Expose methods use `source: 'api'`. Vue receives `validation`, React receives `onValidation`, and Core uses `subscribeValidation()`. `valid` means there are no errors; warnings do not block publishing by default.
209
224
  - The default Header does not duplicate Fit; use the footer control or `fitView()`.
210
- - `regions` can hide `header`, `left`, `right`, or `footer` without leaving an empty layout track. Prop changes update the existing Shell; do not rebuild the Canvas.
211
- - Put a host-owned properties panel beside the Nova root. Use `selection-change` / `onSelectionChange` as its source of truth because it covers nodes, edges, multi-selection, clearing, and keyboard selection. Do not substitute `element-click` / `onElementClick`.
225
+ - `config.ui.regions` can hide `header`, `left`, `right`, or `footer` without leaving an empty layout track. Config changes update the existing Shell; do not rebuild the Canvas.
226
+ - A host panel can replace right content inside Nova, or live beside the Nova root after explicitly hiding the whole region. For an external design panel use `selection-change` / `onSelectionChange`, covering nodes, edges, multi-selection, clearing, and keyboard selection. Do not substitute `element-click` / `onElementClick`. Use the new `panelSelection` context for selection across read-only modes.
212
227
  - Join host business configuration by stable BPMN element ID. Saving, publishing, authorization, upload, and server transactions remain host responsibilities.
213
228
  - `theme="auto"` explicitly follows the system theme. The compatibility default remains `light`.
214
229
  - Vue and React both export standalone `BpmnPalettePanel` for a fully custom layout. Pass the same external Studio Controller to Canvas, Palette, and Properties.
215
230
 
231
+ Group static Studio options under `config`: modeling constraints in `config.modeling`, Header layout in `config.ui`, Viewer display defaults in `config.viewer`, and SVG defaults in `config.export`. Keep model/XML, mode/allowedModes/runtime/projection/theme, resolvers, renderers, registries, callbacks, and Slots at the top level. Deprecated top-level configuration aliases remain compatible and explicitly win when the same item is supplied in both places.
232
+
233
+ `config.ui.controlSize` defaults to `medium` (32px). `small`, `medium`, and `large` map to 28px, 32px, and 40px; Element Plus `default` and Ant Design `middle` both map to Nova `medium`. A host-specific value must be a strict `24px`–`48px` string such as `36px`; numbers, other units, CSS expressions, and out-of-range values are invalid. Header contexts expose read-only `ui.controlSize` and `ui.controlHeight`, while custom actions inherit `--nova-control-height`, `--nova-header-height`, `--nova-control-font-size`, `--nova-control-padding-inline`, `--nova-control-icon-size`, and `--nova-control-radius`. These sizes never change footer zoom, canvas floating controls, Palette, or Properties.
234
+
235
+ Core Shell and framework handles expose `getConfig()` and `setConfig()`. The setter validates first and updates UI, Viewer, and Export in place without changing Model, XML, History, Selection, Mode, or Viewport. Modeling allowlists are Controller creation invariants; use the existing `setPropertiesProfile()` only for a runtime Profile change.
236
+
237
+ ### 4.2 Sidebar layout and collapse (0.3.5-preview)
238
+
239
+ Read `SOURCE: docs/CUSTOMIZATION.md`, section `侧栏布局与平滑折叠`, in the co-located `llms-full.txt` before integrating the sidebar interfaces introduced in `0.3.5-preview`. They are not available in the older `0.3.4-preview` package. The host must still provide a computable container height.
240
+
241
+ - Configure `config.ui.leftPanel/rightPanel.collapsible` (default `true`) and `defaultCollapsed` (wide-layout initialization only, default `false`). `config.ui.rightPanel.layout` defaults to `flex`: Nova supplies a bounded column, the host arranges fixed regions and a `flex: 1; min-height: 0; overflow: auto` body. Use `scroll` for Nova-owned scrolling of the entire content; avoid nested scroll containers unless intentional.
242
+ - Core `slots.right`, Vue `#right`, and React `right` replace only content. Native slots preserve the host application tree and win over same-name Core DOM slots. Do not create another framework root or patch Nova classes. Header/Right contexts share `state`, `mode`, `ui`, and `panelSelection`, plus `studio`, `shell`, and `actions`.
243
+ - Design shows both panels by default; Viewer only the right; Instance hides both. An omitted right region uses mode defaults; explicit `regions.right: 'default'` opts into Instance, while `'hidden'` removes the panel and its toggle. A slot never overrides hidden. Preserve this distinction when reading/updating Config.
244
+ - Collapsed retains an edge toggle; hidden does not. Sidebars animate independently for 200ms and respect reduced motion. The first entry into a Shell container at or below 720px starts collapsible panels collapsed, with separate wide/narrow state memory. Expansion pushes the canvas, never opens a drawer. Content/form/scroll state survives; no automatic Fit occurs. Automatic Viewer projection responds once to the final size.
245
+ - Use `getSidebarState()`, `setSidebarCollapsed(side, collapsed)`, and `subscribeSidebarChange()`. The setter returns `true` only for actual changes; unavailable/redundant requests return `false`. Vue emits `sidebar-change`; React uses `onSidebarChange`. The read-only event contains `side`, `collapsed`, `previousCollapsed`, `mode`, and `source: button | api | responsive | config`.
246
+ - `panelSelection` contains `selection`, `selectedElement`, and `trace`. Core reads/subscribes with `getPanelSelection()` / `subscribePanelSelection()`. Design follows Controller selection; read-only clicks do not rewrite design selection. Do not substitute `state.selection` for the current approval-trace context. Hiding the sidebar does not disable trace clicks, details, or assets.
247
+ - These policies belong to the default Shell, not a complete custom `layout()` or standalone Designer/Viewer. Sidebar UI state must never enter BPMN XML, Model, or History.
248
+
249
+ ### 4.3 Definition-time subtitle presentation
250
+
216
251
  For a host-owned definition-time subtitle, pass `nodeSubtitleResolver` to Studio, Canvas, or Viewer. Return `undefined` for Nova's default subtitle, `null` to remove the row, or a string (including an empty string) as the override. After data inside a stable closure or Map changes, call `refreshPresentation()` on the component handle or Shell. This refresh must not be implemented by mutating a node, re-importing XML, or remounting the component. The resolver applies only to standard Design/Viewer task and container cards and standard SVG fallback; a complete custom renderer wins, and Instance runtime summaries are never overridden.
217
252
 
218
253
  ## 5. Keep model ownership deterministic
@@ -227,12 +262,16 @@ This step is complete when edit, undo, redo, save, reload, and intentional exter
227
262
 
228
263
  ## 6. Add host constraints before business use
229
264
 
230
- Production workflow applications usually support a subset of BPMN. Configure `allowedNodeTypes` and `allowedEdgeTypes` on the Studio Controller or framework `BpmnStudio`; these reject unsupported imported models and block node/edge creation outside the host contract. The node allowlist filters Palette entries, but one allowed node type may still have multiple Palette presets. Configure matching Palette and Properties registries when the host needs custom labels or business fields.
265
+ Production workflow applications usually support a subset of BPMN. Configure `config.modeling.allowedNodeTypes` and `config.modeling.allowedEdgeTypes` on the Studio Controller or framework `BpmnStudio`; these reject unsupported imported models and block node/edge creation outside the host contract. The node allowlist filters Palette entries, but one allowed node type may still have multiple Palette presets. Configure matching Palette and Properties registries when the host needs custom labels or business fields.
231
266
 
232
267
  Use `allowedModes` to expose only host-authorized workbench modes. Runtime changes use `setAllowedModes()` rather than rebuilding the Studio; invalid, empty, or duplicate lists are errors. Pass real Runtime data for instance mode; absence of Runtime data means no approval trace rather than demo business data.
233
268
 
234
269
  Runtime snapshots store asset references only. The host supplies `runtimeAssetResolver`. BPMN Nova does not execute workflow engines, submit approvals, upload/store attachments, issue permissions, or export PNG/PDF.
235
270
 
271
+ For approval-trace integration, read the complete `SOURCE: docs/RUNTIME-INTEGRATION.md` section in the co-located `llms-full.txt` before mapping backend records. It covers instance-bound XML, stable work-item/visit identities, Flowable/Activiti mapping limits, complete approval scenarios, request cancellation, and assets. Runtime data stays top-level; only Studio display configuration belongs in `config.viewer`. Finish this branch only after the guide's relevant integration checks pass; report missing history instead of inventing facts.
272
+
273
+ Named Runtime type re-exports from the Vue/React facade start in `0.3.5-preview`. Check the installed declarations before using them; the guide gives a Props-based type fallback for older releases. Keep the one-direct-package rule rather than adding Studio for these types.
274
+
236
275
  Read `llms-full.txt` before implementing custom Shell regions, Header composition, Actions, Palette, Properties, Context Menu, Renderer, Runtime, theme, or SVG export behavior. Use only public exports from the one selected package or supported Studio subpaths. `llms-full.txt` is generated by the Nova repository and must not be edited by hand.
237
276
 
238
277
  ## 7. Verify the integration
@@ -246,16 +285,24 @@ Run the target application's existing non-destructive quality commands, includin
246
285
  5. Intentional external XML replacement loads once and resets history once.
247
286
  6. Disallowed node types are unavailable and rejected by import/creation commands; disallowed modes are not rendered and `setMode()` returns `false` without changing state.
248
287
  7. Theme changes update the existing instance.
249
- 8. Unmount/remount leaves no duplicate listeners, overlays, or framework instances.
250
- 9. Browser Console has no errors.
251
- 10. If a subtitle resolver is used, verify override, `undefined`, `null`, SVG parity, and that `refreshPresentation()` preserves XML, history, selection, scope, zoom, pan, theme, and mode. Verify Instance summaries are unchanged.
288
+ 8. Default `medium`, presets, and a custom value such as `36px` resize only Header controls; invalid `config.ui.controlSize` leaves the previous DOM and configuration intact.
289
+ 9. Unmount/remount leaves no duplicate listeners, overlays, or framework instances.
290
+ 10. Browser Console has no errors.
291
+ 11. If a subtitle resolver is used, verify override, `undefined`, `null`, SVG parity, and that `refreshPresentation()` preserves XML, history, selection, scope, zoom, pan, theme, and mode. Verify Instance summaries are unchanged.
292
+ 12. If sidebars are used, verify independent collapse, narrow containers, reduced motion, keyboard focus, host form/scroll preservation, Flex/Scroll behavior, native slot updates, Instance defaults, and selection/event consistency. Keep XML, history, and viewport invariant during ordinary collapse; record automatic projection changes separately.
252
293
 
253
294
  The installation is complete only when the package is present, the production build succeeds, and the relevant browser checks pass. Report any unverified branch explicitly.
254
295
 
296
+ ## Maintainer release policy
297
+
298
+ Before changing Nova package versions, publishing, or recovering a partial release, read the complete `SOURCE: docs/NPM-PACKAGES.md` section in the co-located `llms-full.txt` (or that file in the source repository). It defines mandatory lockstep versions and full releases of all three public packages, including unchanged packages, plus Registry completion checks. This is a maintainer rule, not an instruction for host applications to install all three packages or authorization to publish without a user request.
299
+
255
300
  ## Reference
256
301
 
257
302
  - `README.md`: human-facing product overview and quick start.
258
303
  - `llms-full.txt`: complete generated AI context containing setup, component, Interface, customization, publishing, and architecture documentation.
304
+ - `docs/RUNTIME-INTEGRATION.md`: authoritative approval-trace guide in the source repository; the full content also ships inside every public package's `llms-full.txt`.
305
+ - `docs/NPM-PACKAGES.md`: authoritative global version and full-release policy, including partial-release recovery.
259
306
 
260
307
 
261
308
  <!-- SOURCE: README.md -->
@@ -269,7 +316,7 @@ The installation is complete only when the package is present, the production bu
269
316
  BPMN Nova 提供可嵌入的流程设计器、只读 Viewer、运行态审批轨迹、实例级主题、纯 SVG 导出,以及 React / Vue 适配。项目使用独立的 DOM / SVG 渲染实现,不依赖 `bpmn-js`,并为 Flowable 与 Activiti 提供 XML Profile 和属性扩展。
270
317
 
271
318
  > [!IMPORTANT]
272
- > 当前版本为 `0.3.4-preview`。它适合 SDK 评估、产品集成验证和企业审批原型;公开 Interface、BPMN XML round-trip 与引擎兼容能力仍在持续稳定中,建议使用 `@preview` 安装并在生产接入前完成目标流程验证。
319
+ > 当前版本为 `0.3.5-preview`。它适合 SDK 评估、产品集成验证和企业审批原型;公开 Interface、BPMN XML round-trip 与引擎兼容能力仍在持续稳定中,建议使用 `@preview` 安装并在生产接入前完成目标流程验证。
273
320
 
274
321
  AI 或代码生成工具接入必须从 [`llms.txt`](llms.txt) 的完整安装流程开始;需要全部接口与定制上下文时再读取 [`llms-full.txt`](llms-full.txt)。这两份文件也会随三个公开 npm 包发布。
275
322
 
@@ -397,16 +444,24 @@ const model = createEmptyProcess('flowable')
397
444
  model.id = 'Process_PurchaseApproval'
398
445
  model.name = '采购申请审批流程'
399
446
 
447
+ const config = {
448
+ modeling: {
449
+ propertiesProfile: 'business',
450
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
451
+ allowedEdgeTypes: ['sequenceFlow'],
452
+ },
453
+ ui: { controlSize: 'medium' },
454
+ }
455
+
400
456
  const studio = createStudioController({
401
457
  model,
402
- propertiesProfile: 'business',
403
- allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
404
- allowedEdgeTypes: ['sequenceFlow'],
458
+ config,
405
459
  })
406
460
 
407
461
  const shell = createStudioShell({
408
462
  container: document.querySelector('#bpmn-studio'),
409
463
  studio,
464
+ config,
410
465
  mode: 'design',
411
466
  allowedModes: ['design'],
412
467
  theme: 'auto',
@@ -430,6 +485,8 @@ const studio = createStudioController({ model })
430
485
 
431
486
  ### Viewer 与 Runtime
432
487
 
488
+ 真实后端接入请从[审批轨迹接入指南](docs/RUNTIME-INTEGRATION.md)开始:包含匹配的 XML、Flowable/Activiti 映射、完整 Snapshot、会签与驳回重入、刷新和附件。Vue/React 的具名 Runtime 类型出口自 `0.3.5-preview` 提供;指南也提供旧版的兼容类型写法。
489
+
433
490
  ```js
434
491
  import { BpmnViewer } from '@bpmn-nova/studio/viewer'
435
492
  import '@bpmn-nova/studio/styles.css'
@@ -457,15 +514,22 @@ npm install @bpmn-nova/react@preview
457
514
  import { BpmnStudio } from '@bpmn-nova/react'
458
515
  import '@bpmn-nova/react/styles.css'
459
516
 
517
+ const studioConfig = {
518
+ modeling: {
519
+ propertiesProfile: 'business',
520
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
521
+ allowedEdgeTypes: ['sequenceFlow'],
522
+ },
523
+ ui: { controlSize: 'medium' },
524
+ }
525
+
460
526
  export function WorkflowEditor({ initialXml }) {
461
527
  return (
462
528
  <div style={{ height: 720 }}>
463
529
  <BpmnStudio
464
530
  xml={initialXml}
465
531
  engine="flowable"
466
- propertiesProfile="business"
467
- allowedNodeTypes={['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']}
468
- allowedEdgeTypes={['sequenceFlow']}
532
+ config={studioConfig}
469
533
  mode="design"
470
534
  allowedModes={['design']}
471
535
  theme="auto"
@@ -492,6 +556,14 @@ import { BpmnStudio } from '@bpmn-nova/vue'
492
556
  import '@bpmn-nova/vue/styles.css'
493
557
 
494
558
  const props = defineProps({ initialXml: String })
559
+ const studioConfig = {
560
+ modeling: {
561
+ propertiesProfile: 'business',
562
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
563
+ allowedEdgeTypes: ['sequenceFlow'],
564
+ },
565
+ ui: { controlSize: 'medium' },
566
+ }
495
567
 
496
568
  function onChange(model, reason, nextXml) {
497
569
  console.log(reason, nextXml)
@@ -503,9 +575,7 @@ function onChange(model, reason, nextXml) {
503
575
  <BpmnStudio
504
576
  :xml="props.initialXml"
505
577
  engine="flowable"
506
- properties-profile="business"
507
- :allowed-node-types="['startEvent', 'userTask', 'exclusiveGateway', 'endEvent']"
508
- :allowed-edge-types="['sequenceFlow']"
578
+ :config="studioConfig"
509
579
  mode="design"
510
580
  :allowed-modes="['design']"
511
581
  theme="auto"
@@ -528,9 +598,10 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
528
598
  engine="activiti"
529
599
  mode="design"
530
600
  :allowed-modes="['design']"
531
- :allowed-node-types="supportedNodeTypes"
532
- :allowed-edge-types="['sequenceFlow']"
533
- :regions="{ right: 'hidden' }"
601
+ :config="{
602
+ modeling: { allowedNodeTypes: supportedNodeTypes, allowedEdgeTypes: ['sequenceFlow'] },
603
+ ui: { controlSize: 'medium', regions: { right: 'hidden' } },
604
+ }"
534
605
  theme="auto"
535
606
  @change="handleChange"
536
607
  @selection-change="handleSelectionChange"
@@ -539,7 +610,7 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
539
610
  <template #header-start="{ state, actions }">
540
611
  <!-- 返回、业务图标、流程名称和类型 -->
541
612
  </template>
542
- <template #header-actions="{ actions, mode }">
613
+ <template #header-actions="{ actions, mode, ui }">
543
614
  <button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
544
615
  <button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
545
616
  <button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
@@ -547,10 +618,26 @@ Vue Expose 提供与 Vanilla 实例对应的 Actions、校验、XML、视图、
547
618
  </BpmnStudio>
548
619
  ```
549
620
 
550
- `header-start` 只替换 Nova Brand;`header-actions` 只替换默认的“校验 / 导入 / 导出”动作组,适合宿主组合“校验 / 保存 / 发布”;`header` 可替换整个 Header。`regions` 隐藏区域后不会保留 Grid 空白,也不会重建 Canvas。React 提供等价的 `headerStart` / `headerActions` / `header` Render Prop,Core 提供同名 DOM Slots。默认 Header 的最佳视图只保留在底部缩放区。
621
+ `header-start` 只替换 Nova Brand;`header-actions` 只替换默认的“校验 / 导入 / 导出”动作组,适合宿主组合“校验 / 保存 / 发布”;`header` 可替换整个 Header。`config.ui.regions` 隐藏区域后不会保留 Grid 空白,也不会重建 Canvas。React 提供等价的 `headerStart` / `headerActions` / `header` Render Prop,Core 提供同名 DOM Slots。默认 Header 的最佳视图只保留在底部缩放区。
551
622
 
552
623
  `actions.validate()` 会先更新 Nova 默认状态栏,再发送 `validation` 事件并返回 issues;公开 `validate()` 使用相同流程。`valid` 只在存在 error 时为 `false`,warning 会展示和上报但不默认阻止发布。
553
624
 
625
+ 静态建模、Shell UI、Viewer 展示和 SVG 导出选项统一收敛在 `config`。顶部控件由 `config.ui.controlSize` 控制,默认 `medium`,可选 `small`、`medium`、`large` 或 `24px`–`48px`;该值只改变 Header,不改变底部缩放区、画布浮动工具、Palette 或 Properties。Header Slot 的 `ui.controlHeight` 可直接用于对齐宿主控件:Element Plus 的 `default`、Ant Design 的 `middle` 对应 Nova `medium`。自定义像素值时也可读取 `--nova-control-height` 等公开 CSS Variables。
626
+
627
+ ```js
628
+ shell.setConfig({
629
+ ui: {
630
+ controlSize: '36px',
631
+ regions: { right: 'hidden' },
632
+ sidebarWidth: { left: 232, right: 344 },
633
+ },
634
+ })
635
+
636
+ console.log(shell.getConfig(), shell.ui.controlHeight) // 36px
637
+ ```
638
+
639
+ 旧的 `propertiesProfile`、`allowedNodeTypes`、`regions`、`responsive`、`timeline`、`svgExport` 等顶层字段继续兼容,但已标记为 Deprecated;同一项同时出现时,显式旧字段优先。`config.modeling` 只在 Controller 创建时建立约束,运行中节点和连线白名单不会被 `setConfig()` 改写。
640
+
554
641
  发布事务由宿主实现,并可以直接复用同一个 Header Action 完成“校验后发布”:
555
642
 
556
643
  ```js
@@ -561,7 +648,24 @@ async function publishProcess(actions) {
561
648
  }
562
649
  ```
563
650
 
564
- 宿主右侧属性面板应位于 Nova 外部,并以 `selection-change` / `onSelectionChange` 为状态来源、以稳定 BPMN Element ID 关联业务配置。`element-click` 不能替代选择事件。Nova 不拥有宿主的保存、发布、权限或服务端事务。
651
+ 宿主右侧属性面板可以通过右侧 Slot 复用 Nova 布局,也可以在显式隐藏 Nova 右侧后放在外部。外部设计面板以 `selection-change` / `onSelectionChange` 为状态来源,以稳定 BPMN Element ID 关联业务配置;`element-click` 不能替代选择事件。Nova 不拥有宿主的保存、发布、权限或服务端事务。
652
+
653
+ ### 侧栏扩展、滚动与平滑折叠
654
+
655
+ Core `slots.right`、Vue `#right`、React `right` 可以替换右侧内容,Nova 继续管理宽度、高度和左右分隔线中点的折叠按钮。右侧默认 `flex`,适合固定标题/底部、中间内容滚动;设置 `scroll` 则由 Nova 滚动整个内容。
656
+
657
+ ```js
658
+ const config = {
659
+ ui: {
660
+ leftPanel: { collapsible: true, defaultCollapsed: false },
661
+ rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
662
+ },
663
+ }
664
+ ```
665
+
666
+ 左右独立收起,200ms 平滑过渡并尊重减少动态效果;收起保留展开按钮,不卸载内容、不丢失表单/滚动状态、不自动 Fit。Shell 容器 ≤720px 时首次默认收起,展开继续挤压画布,宽窄屏分别记忆状态。**隐藏不同于收起**:`regions.right: 'hidden'` 移除整个区域及按钮;Instance 默认隐藏右侧,显式 `'default'` 可开启。
667
+
668
+ Shell 与框架 Handle/Expose 提供 `getSidebarState()`、`setSidebarCollapsed()`、`subscribeSidebarChange()`;Vue `sidebar-change`、React `onSidebarChange` 接收同一事件。Header/Right Context 的 `panelSelection` 支持设计与只读轨迹选择,Core 使用 `getPanelSelection()` / `subscribePanelSelection()`。完整 Core/Vue/React 用法、Flex CSS 和兼容说明见[侧栏指南](docs/CUSTOMIZATION.md#侧栏布局与平滑折叠)。本节新接口自 `0.3.5-preview` 提供。
565
669
 
566
670
  `theme="auto"` 是显式启用系统主题适配;为了兼容已有接入,未传 Theme 时仍默认为 `light`。Vue 包同时提供独立的 `BpmnPalettePanel`,用于完全自定义布局。
567
671
 
@@ -760,6 +864,7 @@ shell.openSvgExportPreview()
760
864
 
761
865
  ## 浏览器接入注意事项
762
866
 
867
+ - 节点默认尺寸不等于导入尺寸:组件加载服务端 XML 也会解析 BPMN DI Bounds;保存/导出会写入模型宽高。自 `0.3.5-preview` 提供的网关与数据图元兼容修复只让内部图形按实际容器缩小居中,并校正显示连线,不扩大 DI、不改 waypoint。宿主无需配置节点尺寸、删除重建节点或覆盖内部 CSS,详见 [DI 与视觉尺寸](docs/API.md#节点-di-与视觉尺寸)。
763
868
  - 为 Canvas、Studio 和 Viewer 容器设置明确高度;隐藏容器恢复显示后可调用 `fitView()`。
764
869
  - CSS 必须由宿主显式导入;Studio、React、Vue 各自提供一个自包含样式入口。
765
870
  - 使用 ESM 与现代浏览器;Node.js 18+ 用于构建和包消费工具链。
@@ -772,6 +877,7 @@ shell.openSvgExportPreview()
772
877
  - [快速开始](docs/GETTING-STARTED.md):Studio、Designer、Viewer、React、Vue 与 XML。
773
878
  - [组件参数](docs/COMPONENTS.md):Options、Props、事件、Ref / Expose 和实例方法。
774
879
  - [API](docs/API.md):模型、命令、XML、Runtime Snapshot、主题与导出类型。
880
+ - [审批轨迹接入指南](docs/RUNTIME-INTEGRATION.md):宿主数据转换、组件接入、审批轮次、回退与资源解析。
775
881
  - [自定义指南](docs/CUSTOMIZATION.md):主题、Palette、图标、Renderer、Slot 和运行态扩展。
776
882
  - [Properties Panel](docs/PROPERTIES-PANEL.md):Provider、Group、Entry、Data Provider 和自定义字段。
777
883
  - [NPM Packages](docs/NPM-PACKAGES.md):三个公开包、Studio 子路径、构建验证与 Registry 清理说明。
@@ -893,16 +999,24 @@ const model = createEmptyProcess('flowable')
893
999
  model.id = 'Process_Approval'
894
1000
  model.name = '审批流程'
895
1001
 
1002
+ const config = {
1003
+ modeling: {
1004
+ propertiesProfile: 'business',
1005
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
1006
+ allowedEdgeTypes: ['sequenceFlow'],
1007
+ },
1008
+ ui: { controlSize: 'medium' },
1009
+ }
1010
+
896
1011
  const studio = createStudioController({
897
1012
  model,
898
- propertiesProfile: 'business',
899
- allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
900
- allowedEdgeTypes: ['sequenceFlow'],
1013
+ config,
901
1014
  })
902
1015
 
903
1016
  const shell = createStudioShell({
904
1017
  container: document.querySelector('#studio'),
905
1018
  studio,
1019
+ config,
906
1020
  mode: 'design',
907
1021
  allowedModes: ['design', 'viewer'],
908
1022
  theme: 'auto',
@@ -974,6 +1088,11 @@ import { useRef, useState } from 'react'
974
1088
  import { BpmnStudio } from '@bpmn-nova/react'
975
1089
  import '@bpmn-nova/react/styles.css'
976
1090
 
1091
+ const STUDIO_CONFIG = {
1092
+ modeling: { propertiesProfile: 'business' },
1093
+ ui: { controlSize: 'medium' },
1094
+ }
1095
+
977
1096
  export function WorkflowEditor({ initialXml }) {
978
1097
  const studioRef = useRef(null)
979
1098
  const [mode, setMode] = useState('design')
@@ -984,10 +1103,10 @@ export function WorkflowEditor({ initialXml }) {
984
1103
  ref={studioRef}
985
1104
  xml={initialXml}
986
1105
  engine="flowable"
1106
+ config={STUDIO_CONFIG}
987
1107
  mode={mode}
988
1108
  allowedModes={['design', 'viewer']}
989
1109
  theme="auto"
990
- propertiesProfile="business"
991
1110
  onChange={(model, reason, nextXml) => saveDraft(nextXml)}
992
1111
  onModeChange={(event) => setMode(event.mode)}
993
1112
  />
@@ -1015,6 +1134,10 @@ import '@bpmn-nova/vue/styles.css'
1015
1134
  const props = defineProps({ initialXml: String })
1016
1135
  const studioRef = ref(null)
1017
1136
  const mode = ref('design')
1137
+ const config = {
1138
+ modeling: { propertiesProfile: 'business' },
1139
+ ui: { controlSize: 'medium' },
1140
+ }
1018
1141
  const onChange = (model, reason, nextXml) => saveDraft(nextXml)
1019
1142
  </script>
1020
1143
 
@@ -1024,10 +1147,10 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
1024
1147
  ref="studioRef"
1025
1148
  :xml="props.initialXml"
1026
1149
  engine="flowable"
1150
+ :config="config"
1027
1151
  v-model:mode="mode"
1028
1152
  :allowed-modes="['design', 'viewer']"
1029
1153
  theme="auto"
1030
- properties-profile="business"
1031
1154
  @change="onChange"
1032
1155
  @mode-change="event => console.log(event.source)"
1033
1156
  />
@@ -1039,28 +1162,48 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
1039
1162
 
1040
1163
  ### 4.1 宿主工作台组合规则
1041
1164
 
1042
- 默认 `BpmnStudio` 是完整工作台。只有宿主明确拥有自己的区域时才配置 `regions`;例如 DX 复用自己的业务属性面板时使用 `regions: { right: 'hidden' }`,并把该面板放在 Nova 外部。隐藏区域不保留 Grid 轨道。
1165
+ 默认 `BpmnStudio` 是完整工作台。静态建模、布局、Viewer 和导出选项统一放入 `config`;数据、状态、事件和 Slots 保持顶层。宿主内容可通过右侧 Slot 复用 Nova 布局;只有需要把整个面板放到 Nova 外部时,才使用 `config: { ui: { regions: { right: 'hidden' } } }`。隐藏区域不保留 Grid 轨道或展开按钮,不能用右侧 Slot 覆盖。
1043
1166
 
1044
- Vue 使用原生 `#header-start` / `#header-actions` / `#header`,React 使用 `headerStart` / `headerActions` / `header` Render Prop。Header Start 只替换 Brand,Header Actions 只替换默认“校验 / 导入 / 导出”动作组,完整 Header 通过公开 `StudioShellActions` 重建所需编辑操作。不要创建第二个 Vue App 或 React Root 挂载 Header
1167
+ Vue 使用原生 `#header-start` / `#header-actions` / `#header` / `#right`,React 使用 `headerStart` / `headerActions` / `header` / `right` Render Prop。Header Start 只替换 Brand,Header Actions 只替换默认“校验 / 导入 / 导出”动作组,完整 Header 通过公开 `StudioShellActions` 重建所需编辑操作;Right 仅替换侧栏内容。不要创建第二个 Vue App 或 React Root 挂载这些内容。同名原生 Slot 优先于 Core DOM Slot
1045
1168
 
1046
1169
  宿主通常在 Header Actions 中组合“校验 / 保存 / 发布”。校验按钮调用 `actions.validate()`:Nova 先更新默认状态栏,再通过 Vue `validation`、React `onValidation` 或 Core `subscribeValidation()` 发送只读结果。程序化 `validate()` 的来源为 `api`,Header Action 的来源为 `toolbar`;`valid` 仅在存在 error 时为 `false`。保存、发布、权限和服务端事务始终留在宿主。顶部不再重复渲染最佳视图,底部缩放区及 `fitView()` Interface 继续可用。
1047
1170
 
1048
- Header Slot/Render Context 的 `mode` 是当前实际模式。宿主用它显隐保存、发布按钮或外部属性面板;不要从 Nova Header DOM 读取选中按钮。运行时改变 `allowedModes` 会原地更新默认模式按钮,移除当前模式时只回退一次。
1171
+ Header Slot/Render Context 的 `mode` 是当前实际模式,`ui` 是只读的实际控件尺寸。宿主用它显隐保存、发布按钮、映射自己的组件库尺寸或外部属性面板;不要从 Nova Header DOM 读取选中按钮。运行时改变 `allowedModes` 会原地更新默认模式按钮,移除当前模式时只回退一次。
1172
+
1173
+ `config.ui.controlSize` 默认为 Nova `medium`(32px),对应 Element Plus `default` 和 Ant Design `middle`;`small`、`large` 分别对应两者的同名尺寸。宿主也可传 `24px`–`48px` 的严格 CSS 像素字符串,例如 `36px`。自定义 Header 控件可以继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 和 `--nova-control-radius`。该尺寸边界只覆盖 Header,不影响底部缩放区、画布浮动工具、Palette 或 Properties。
1174
+
1175
+ `getConfig()` 返回实际归一化配置,`setConfig()` 在完整校验成功后原地更新 UI、Viewer 与 Export,并保持 Model、XML、History、Selection、Mode 和 Viewport。`config.modeling.allowedNodeTypes` 与 `allowedEdgeTypes` 只在 Controller 创建时建立不变量;运行时 Profile 仍通过 `setPropertiesProfile()` 修改。旧顶层配置仅作为 Deprecated 兼容别名;同一项新旧并存时,旧顶层显式值优先。
1049
1176
 
1050
1177
  外部业务属性面板必须以 `selection-change` / `onSelectionChange` 为状态来源,以稳定 BPMN Element ID 关联业务配置。该事件覆盖节点、连线、多选、清空和键盘选择;`element-click` / `onElementClick` 只是点击观察事件。保存草稿、发布、权限和服务端事务属于宿主,不要写入 Nova Actions。
1051
1178
 
1052
1179
  显式 `theme="auto"` 才表示跟随系统主题;默认 Theme 为兼容性的 `light`。Vue 与 React 都公开独立 `BpmnPalettePanel`,完整自定义布局时必须让 Canvas、Palette 和 Properties 复用同一个外部 Controller。
1053
1180
 
1054
- ### 4.1 模型所有权
1181
+ ### 4.2 侧栏布局与折叠门禁
1182
+
1183
+ 新增侧栏接口自 `0.3.5-preview` 提供,先核对目标安装包的版本与声明。完整 Core/Vue/React 示例见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠),不要以内部 CSS 覆盖实现本能力。
1184
+
1185
+ - `config.ui.leftPanel/rightPanel` 收拢 `collapsible`(默认 `true`)、`defaultCollapsed`(宽屏初始化默认 `false`);右侧另有 `layout: 'flex' | 'scroll'`,默认 `flex`。不要添加零散顶层布局 Props。
1186
+ - Flex 由宿主安排固定头尾和弹性滚动区,Nova 仅提供有界纵向 Flex 容器;Scroll 由 Nova 滚动整个内容。宿主高度链必须可计算,不能只给无高度基准的根节点设置 `height: 100%`。
1187
+ - Design 默认显示左右,Viewer 仅右侧,Instance 默认两侧隐藏。右侧显式 `'default'` 可在 Instance 开启,`'hidden'` 移除内容和按钮;Slot 不改变显隐优先级。`getConfig()` 保留未配置意图,不应把读取结果中的默认值再转成显式开启。
1188
+ - Shell 容器 ≤720px 首次收起可折叠面板,保留按钮;宽窄屏分别记忆状态。两侧独立展开且挤压画布,非抽屉。200ms 动画尊重减少动态效果,不卸载 Slot、不自动 Fit;Viewer 自动投影仅在动画结束后按最终宽度处理。
1189
+ - 使用 `getSidebarState()` / `setSidebarCollapsed()` / `subscribeSidebarChange()`;Vue `sidebar-change`、React `onSidebarChange` 与 Core 事件一致。折叠是 Shell UI 状态,不写入 BPMN 模型或历史。
1190
+ - Header/Right Context 共用响应式 `mode`、`state`、`ui`、`panelSelection`。Core 使用 `getPanelSelection()` / `subscribePanelSelection()`;Viewer/Instance 的实际选择不回写 Controller 设计选择,也不能用设计 `state.selection` 代替。
1191
+ - 验证折叠按钮、键盘、减少动态效果、表单/滚动状态保留、跨模式选择更新和无重复订阅;完整自定义 `layout()` 与独立 Designer/Viewer 不接管默认侧栏。
1192
+
1193
+ ### 4.3 模型所有权
1055
1194
 
1056
1195
  框架组件的 `xml`/`model` 是外部替换输入,`change`/`onChange` 的 XML 是草稿输出。宿主可以保存该输出;如果响应式状态回写的是完全相同的导出 XML,Adapter 会忽略这次回声并保留 Undo/Redo 历史。只有加载另一流程或服务器 revision 时才传入不同 XML,此时模型和历史会替换一次。
1057
1196
 
1058
1197
  React/Vue 包重新导出创建外部 Controller、Palette、Properties、Context Menu、Icon 和 Template Registry 所需的公开函数,因此高级接入仍只需要直接安装对应 Adapter 包。
1059
1198
 
1060
- 生产工作流通常只支持 BPMN 子集。通过 Controller 或 `BpmnStudio` 的 `allowedNodeTypes` 与 `allowedEdgeTypes` 声明节点、连线白名单;该限制会校验初始/外部模型,并约束创建、快捷新增、模板和节点类型转换。Palette 会服从节点白名单,但同一节点类型可以有多个 Preset。通过 `allowedModes` 限制工作台可切换模式。实例模式没有 Runtime 时保持空运行事实,不会自动加载演示数据。
1199
+ 生产工作流通常只支持 BPMN 子集。通过 Controller 或 `BpmnStudio` 的 `config.modeling.allowedNodeTypes` 与 `allowedEdgeTypes` 声明节点、连线白名单;该限制会校验初始/外部模型,并约束创建、快捷新增、模板和节点类型转换。Palette 会服从节点白名单,但同一节点类型可以有多个 Preset。通过顶层 `allowedModes` 限制工作台可切换模式。实例模式没有 Runtime 时保持空运行事实,不会自动加载演示数据。
1061
1200
 
1062
1201
  ## 5. Runtime Snapshot 与审批动作
1063
1202
 
1203
+ 接入真实审批数据、会签、回退或附件前,先完整阅读[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。使用 npm 包内上下文时读取 `llms-full.txt` 中 `SOURCE: docs/RUNTIME-INTEGRATION.md` 对应全文。按指南完成身份映射、快照转换、过期请求处理与场景验收;引擎字段缺失时向宿主报告缺口,不生成虚假的审批事实。
1204
+
1205
+ Vue/React 的 14 个具名 Runtime 类型出口自 `0.3.5-preview` 提供;旧安装版本使用指南中的 Props 推导,不增加 Studio 直依赖。`runtime` 保持顶层,Studio 展示配置进入 `config.viewer`,独立 Viewer 继续使用顶层展示 Options。
1206
+
1064
1207
  Runtime Snapshot 描述已经发生的运行事实,不负责发起审批或执行流程引擎。新动作必须关联 `elementId` 与 `visitId`;`activityId` 用于精确关联工作项。
1065
1208
 
1066
1209
  ```js
@@ -1190,6 +1333,8 @@ const artifact = await viewer.exportSvg({
1190
1333
  console.log(artifact.svg, artifact.width, artifact.height, artifact.warnings)
1191
1334
  ```
1192
1335
 
1336
+ 完整 Studio 的默认导出选项放在 `config.export`;单次调用传入的选项仍用于本次导出。
1337
+
1193
1338
  导出内容不受当前缩放、平移或滚动影响。移动时间线会展开全部历史动作。审批图片通过 `purpose: 'export'` 解析并嵌入 Data URL;普通附件只输出名称、MIME 和大小。第一版不导出 PNG/PDF。
1194
1339
 
1195
1340
  ## 9. 清理与生命周期
@@ -1240,11 +1385,843 @@ BPMN Nova 负责 BPMN 建模、XML 导入导出、只读展示、运行态审批
1240
1385
  - [项目架构](ARCHITECTURE.md)
1241
1386
 
1242
1387
 
1388
+ <!-- SOURCE: docs/RUNTIME-INTEGRATION.md -->
1389
+
1390
+ # 审批轨迹接入指南
1391
+
1392
+ 本文面向把真实审批记录接入 Nova 的前后端开发者。按“准备定义与实例 → 转换数据 → 挂载组件 → 更新与交互 → 验收”阅读;会签、驳回和附件规则集中在对应章节。
1393
+
1394
+ > 版本边界:Vue/React 根入口的 Runtime 类型重导出自 `0.3.5-preview` 提供。下列具名类型示例面向该版本及后续兼容版本;已安装旧版没有这些出口时,使用第 2.4 节的 Props 类型推导,不增加 Studio 直依赖。Studio 原有 Runtime 出口保持不变。
1395
+
1396
+ ## 1. 准备定义与最小实例
1397
+
1398
+ ### 1.1 职责与读取顺序
1399
+
1400
+ | 输入或职责 | 所有者 | 约束 |
1401
+ | --- | --- | --- |
1402
+ | BPMN XML | 宿主定义服务 | 读取实例绑定的定义版本,保留原始节点、连线和 DI |
1403
+ | 引擎当前状态、活动和任务历史 | 宿主引擎适配层 | 按实例查询、关联、去重,区分活动 ID 与工作项 ID |
1404
+ | 审批操作、回退目标、附件引用 | 宿主业务日志 | 保存真实业务事实,不从任务结束时间猜测审批决定 |
1405
+ | `runtime` | 宿主转换层 | 组装引擎中立的 `ProcessInstanceSnapshot` |
1406
+ | 图形、时间线、默认详情 | Nova | 读取 XML 与 Snapshot,派生展示,不执行审批业务 |
1407
+
1408
+ 推荐由后端聚合一致的数据快照,前端只接收定义和 Snapshot。现有宿主也可以在前端执行纯转换,但引擎鉴权、查询、分页、历史保留和审批事务仍由宿主负责。`engine="flowable"` / `engine="activiti"` 选择 XML 与属性 Profile,不会自动查询引擎。
1409
+
1410
+ 先按 `processInstanceId` 确认其 `processDefinitionId`,再获取该定义的 XML。实例迁移后的定义绑定也由后端明确提供。不要用流程 Key 对应的最新 XML,也不要在前端按名称重新生成节点 ID。
1411
+
1412
+ ### 1.2 可复制的最小数据
1413
+
1414
+ 下面保存为宿主的 `approval-data.ts`。整个指南沿用“开始 → 提交申请 → 审核 → 结束”的节点 ID。XML 是 `isExecutable="false"` 的可视化教学定义,不是包含会签表达式、回退命令和权限规则的引擎部署模板;真实接入始终使用实例自己的 XML。
1415
+
1416
+ 共享 TypeScript 文件以下以 Vue 为例。React 项目把共享文件的类型导入改为 `@bpmn-nova/react`,Core 项目改为 `@bpmn-nova/studio`;每个宿主只直接依赖所选包。示例文件名是宿主代码组织建议,不是 npm 子路径。
1417
+
1418
+ ```ts title="approval-data.ts"
1419
+ import type { ProcessInstanceSnapshot } from '@bpmn-nova/vue'
1420
+
1421
+ export const xml = `<?xml version="1.0" encoding="UTF-8"?>
1422
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
1423
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
1424
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
1425
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
1426
+ id="Definitions_Approval" targetNamespace="https://example.org/approval">
1427
+ <bpmn:process id="Process_Approval" name="申请审批" isExecutable="false">
1428
+ <bpmn:startEvent id="StartEvent_1" name="开始" />
1429
+ <bpmn:userTask id="Task_Submit" name="提交申请" />
1430
+ <bpmn:userTask id="Task_Review" name="审核" />
1431
+ <bpmn:endEvent id="EndEvent_1" name="结束" />
1432
+ <bpmn:sequenceFlow id="Flow_Start_Submit" sourceRef="StartEvent_1" targetRef="Task_Submit" />
1433
+ <bpmn:sequenceFlow id="Flow_Submit_Review" sourceRef="Task_Submit" targetRef="Task_Review" />
1434
+ <bpmn:sequenceFlow id="Flow_Review_End" sourceRef="Task_Review" targetRef="EndEvent_1" />
1435
+ </bpmn:process>
1436
+ <bpmndi:BPMNDiagram id="Diagram_Approval">
1437
+ <bpmndi:BPMNPlane id="Plane_Approval" bpmnElement="Process_Approval">
1438
+ <bpmndi:BPMNShape id="Shape_Start" bpmnElement="StartEvent_1">
1439
+ <dc:Bounds x="80" y="122" width="36" height="36" />
1440
+ </bpmndi:BPMNShape>
1441
+ <bpmndi:BPMNShape id="Shape_Submit" bpmnElement="Task_Submit">
1442
+ <dc:Bounds x="180" y="100" width="160" height="80" />
1443
+ </bpmndi:BPMNShape>
1444
+ <bpmndi:BPMNShape id="Shape_Review" bpmnElement="Task_Review">
1445
+ <dc:Bounds x="420" y="100" width="160" height="80" />
1446
+ </bpmndi:BPMNShape>
1447
+ <bpmndi:BPMNShape id="Shape_End" bpmnElement="EndEvent_1">
1448
+ <dc:Bounds x="650" y="122" width="36" height="36" />
1449
+ </bpmndi:BPMNShape>
1450
+ <bpmndi:BPMNEdge id="Edge_Start_Submit" bpmnElement="Flow_Start_Submit">
1451
+ <di:waypoint x="116" y="140" /><di:waypoint x="180" y="140" />
1452
+ </bpmndi:BPMNEdge>
1453
+ <bpmndi:BPMNEdge id="Edge_Submit_Review" bpmnElement="Flow_Submit_Review">
1454
+ <di:waypoint x="340" y="140" /><di:waypoint x="420" y="140" />
1455
+ </bpmndi:BPMNEdge>
1456
+ <bpmndi:BPMNEdge id="Edge_Review_End" bpmnElement="Flow_Review_End">
1457
+ <di:waypoint x="580" y="140" /><di:waypoint x="650" y="140" />
1458
+ </bpmndi:BPMNEdge>
1459
+ </bpmndi:BPMNPlane>
1460
+ </bpmndi:BPMNDiagram>
1461
+ </bpmn:definitions>`
1462
+
1463
+ export const runtime: ProcessInstanceSnapshot = {
1464
+ processInstanceId: 'PI-1001',
1465
+ status: 'running',
1466
+ activities: [
1467
+ { id: 'activity:h-start', elementId: 'StartEvent_1', visitId: 'visit-start-1', status: 'completed', startTime: '2026-09-05T09:00:00+08:00', endTime: '2026-09-05T09:00:00+08:00' },
1468
+ { id: 'task:t-submit-1', elementId: 'Task_Submit', visitId: 'visit-submit-1', status: 'completed', startTime: '2026-09-05T09:00:01+08:00', endTime: '2026-09-05T09:05:00+08:00', participant: { id: 'u-applicant', name: '申请人' } },
1469
+ { id: 'task:t-review-1', elementId: 'Task_Review', visitId: 'visit-review-1', status: 'active', startTime: '2026-09-05T09:05:01+08:00', participant: { id: 'u-li', name: '李经理' } },
1470
+ ],
1471
+ visitedEdges: ['Flow_Start_Submit', 'Flow_Submit_Review'],
1472
+ }
1473
+ ```
1474
+
1475
+ 最小 Snapshot 不要求 `actions`,所以这里只能展示工作项状态和处理人,不能据此声称“已经同意”。第 4 节再补充真实操作。
1476
+
1477
+ 最小 Vue 组件保存为 `ApprovalPreview.vue`,仅在浏览器挂载;Nuxt 等 SSR 宿主使用自己的客户端边界:
1478
+
1479
+ ```vue title="ApprovalPreview.vue"
1480
+ <script setup lang="ts">
1481
+ import { BpmnViewer } from '@bpmn-nova/vue'
1482
+ import '@bpmn-nova/vue/styles.css'
1483
+ import { xml, runtime } from './approval-data'
1484
+ </script>
1485
+
1486
+ <template>
1487
+ <div style="height: 640px; min-height: 320px">
1488
+ <BpmnViewer :xml="xml" :runtime="runtime" engine="flowable" projection="approval" />
1489
+ </div>
1490
+ </template>
1491
+ ```
1492
+
1493
+ 完成条件:审核节点显示处理中和李经理;切换 `standard` 能看到全部四个节点;没有请求业务后端的隐式行为。父级隐藏或高度为零时应先恢复布局,再调用公开 `fitView()`。
1494
+
1495
+ ## 2. 数据契约与身份关联
1496
+
1497
+ ### 2.1 Snapshot 与记录
1498
+
1499
+ | 字段 | 类型与必填性 | 语义 |
1500
+ | --- | --- | --- |
1501
+ | `processInstanceId` | 必填 `string` | 一个真实实例的稳定 ID |
1502
+ | `status` | 必填 `running / completed / suspended / terminated` | 实例状态;未发起不是上述状态之一 |
1503
+ | `activities` | 必填 `ActivityInstance[]` | 已发生的活动执行或工作项,包含当前记录与历史 |
1504
+ | `visitedEdges` | 必填 `string[]`,可为空 | 已确认曾经过的 BPMN 连线 ID 汇总,不表示当前全部有效 |
1505
+ | `actions` | 可选 `RuntimeApprovalAction[]` | 不可变的业务操作事实 |
1506
+ | `transitions` | 可选 `RuntimeTransition[]` | 运行时跳转,独立于 BPMN Sequence Flow |
1507
+ | `edgeVisits` | 可选 `RuntimeEdgeVisit[]` | 每次经过连线的独立记录,支持重复经过及失效历史 |
1508
+
1509
+ | 记录 | 必填字段 | 常用可选字段 |
1510
+ | --- | --- | --- |
1511
+ | `ActivityInstance` | `id`、`elementId`、`status` | `visitId`、起止时间、`participant`、会签元数据 |
1512
+ | `RuntimeApprovalAction` | `id`、`type`、`elementId`、`visitId`、`occurredAt` | `activityId`、`actor`、`targets`、`targetElementId`、`content` |
1513
+ | `RuntimeTransition` | `id`、`type`、`sourceElementId` | 来源工作项、目标节点、时间、`actionId`、状态和失效范围 |
1514
+ | `RuntimeEdgeVisit` | `id`、`edgeId` | `visitId`、`occurredAt`、`status: effective / superseded` |
1515
+
1516
+ 新接入应完整提供 `visitId` 和带时区的 ISO 时间。虽有兼容回退,缺失时间或关联 ID 不能保证多轮审批顺序正确。时间使用 `2026-09-05T09:05:00+08:00` 或 UTC `Z`,不要依赖浏览器解析无时区文本。
1517
+
1518
+ 活动状态只接受 `active / completed / failed / cancelled / skipped`;`idle` 是没有工作项时的派生状态,`rejected` 是展示状态,两者都不是 `ActivityInstance.status` 的合法值。实例挂起不等于所有工作项取消,或签剩余任务被取消也不等于流程终止。
1519
+
1520
+ ### 2.2 四类 ID
1521
+
1522
+ | ID | 示例 | 关联规则 |
1523
+ | --- | --- | --- |
1524
+ | BPMN 元素 | `Task_Review` | 来自实例 XML;`elementId` 与 Transition 端点引用它 |
1525
+ | 工作项 | `task:t-review-1` | 一次执行;Action `activityId`、Transition `sourceActivityId` 引用它 |
1526
+ | 节点到达 | `visit-review-1` | 同一节点本次到达;会签工作项共享,重入创建新 ID |
1527
+ | 操作 / 跳转 / 连线经过 | `op-approve-1` / `move-1` / `ev-1` | 每条事实稳定且唯一,不能在轮询时重新随机生成 |
1528
+
1529
+ `visitId` 建议在实例内唯一,并始终与同一个 BPMN 节点关联。`executionId`、task ID、时间戳或数组序号都不能普遍替代会签访问 ID;宿主必须保存或可靠关联每次到达及多实例作用域。没有可信关联时,应报告数据缺口,而不是合并同节点的全部历史。
1530
+
1531
+ 会签使用 `approvalMode: 'all'`,或签使用 `'any'`,单人使用 `'single'`。多人参与不会自动变成会签。`multiInstanceMode` 区分 `parallel / sequential`,`totalInstances` 是本轮总人数,`requiredInstances` 是业务完成门槛。Nova 展示这些事实,不在达到门槛时替引擎结束或取消任务;或签完成后应传入其余工作项的真实终态。
1532
+
1533
+ ### 2.3 操作、回退与历史
1534
+
1535
+ `actions` 支持 `submit / approve / reject / return / add-sign / transfer / delegate / withdraw / comment / skip` 及宿主自定义类型。转办可以只有操作记录,不需要制造一条 BPMN 边。`actor` 是实际操作者,不一定是任务最终的 `assignee`。
1536
+
1537
+ 驳回路径由 `transitions` 表达,`actionId` 关联同一次业务操作,`sourceActivityId` 精确关联工作项。仅增加 `type: 'reject'` 的 Action 不会自动画出回退路径。结束的驳回工作项仍可为 `completed`;配套 reject Transition 会派生“已驳回”展示。
1538
+
1539
+ 当前 `invalidatedActivityIds` 名称保留兼容,但其值是 **BPMN 节点 ID**,例如 `['Task_Review']`,不是 `['task:t-review-1']`。`invalidatedEdgeIds` 同样是 BPMN 连线 ID。两者应共同明确给出;任一数组存在就属于显式范围,不再自动补推另一项。缺少范围时仅对唯一、同作用域的已访问路径做兼容推断,不能覆盖所有并行、跨作用域和回环情况。
1540
+
1541
+ 失效只影响当前路径展示,不删除历史,也不修改 XML。再次经过的边应新增带更晚时间的 `edgeVisits`,旧记录继续保留;宿主确认目标本轮处理完成后把回退 Transition 更新为 `resolved`,可同时提供 `resolvedAt` / `resolvedByActivityId`。操作日志本身不随状态更新而被改写。
1542
+
1543
+ 旧 `ActivityInstance.comment/outcome` 和 `RuntimeTransition.comment` 会生成兼容动作。新接入优先显式 `actions`:填入 `activityId` 可抑制同工作项的兼容动作,填入 Transition `actionId` 可避免同次跳转再次生成动作。`normalizeRuntime()` 只是兼容归一化,不是业务事实校验器。
1544
+
1545
+ ### 2.4 公共类型与已发布版本兼容
1546
+
1547
+ 自 `0.3.5-preview` 起,Vue/React 根入口显式重导出以下类型,均为 `export type`,没有同名 JavaScript 值:
1548
+
1549
+ - `ProcessInstanceSnapshot`、`ActivityInstance`、`ActivityStatus`、`RuntimeParticipant`。
1550
+ - `RuntimeApprovalAction`、`RuntimeApprovalActionType`、`RuntimeApprovalContent`、`RuntimeApprovalContentBlock`、`RuntimeAssetRef`。
1551
+ - `RuntimeTransition`、`RuntimeEdgeVisit`。
1552
+ - `RuntimeTraceClickEvent`、`RuntimeAssetResolver`、`RuntimeAssetPurpose`。
1553
+
1554
+ 已安装包尚未提供这些具名出口时,可从现有 Props 推导,不增加依赖:
1555
+
1556
+ ```ts title="runtime-compat.ts"
1557
+ import type { BpmnStudioProps } from '@bpmn-nova/vue'
1558
+
1559
+ export type ProcessInstanceSnapshot = NonNullable<BpmnStudioProps['runtime']>
1560
+ export type ActivityInstance = ProcessInstanceSnapshot['activities'][number]
1561
+ export type RuntimeApprovalAction = NonNullable<ProcessInstanceSnapshot['actions']>[number]
1562
+ export type RuntimeTransition = NonNullable<ProcessInstanceSnapshot['transitions']>[number]
1563
+ export type RuntimeEdgeVisit = NonNullable<ProcessInstanceSnapshot['edgeVisits']>[number]
1564
+ ```
1565
+
1566
+ React 使用相同写法并改为 React 包。其他结构可继续按属性推导;不要从源码相对路径或传递依赖导入。下文 Studio `config` 示例要求包含 Config 功能的构建;旧构建沿用同义的顶层 Viewer 配置,不影响 Snapshot 格式。
1567
+
1568
+ ## 3. Flowable / Activiti 映射参考
1569
+
1570
+ ### 3.1 官方资料与边界
1571
+
1572
+ 资料核对日期:2026-09-05。字段表依据 [Flowable HistoricActivityInstance(页面标识 8.0.0)](https://www.flowable.com/open-source/docs/all-javadocs/org/flowable/engine/history/HistoricActivityInstance.html) 与 [Activiti 6.0.0 HistoricActivityInstance](https://www.activiti.org/javadocs/6.latest/org/activiti/engine/history/HistoricActivityInstance.html)。Flowable 链接是滚动文档;宿主使用其他版本时应对照实际依赖的 API,本文不声明 Activiti 7/Cloud 或所有引擎版本已经联调通过。
1573
+
1574
+ | 引擎活动字段 | 宿主映射 |
1575
+ | --- | --- |
1576
+ | `getProcessInstanceId()` / `getProcessDefinitionId()` | 校验实例归属及绑定的定义版本 |
1577
+ | `getActivityId()` | Nova `elementId`,来自 BPMN 定义 |
1578
+ | `getId()` | 历史活动实例 ID;非人工工作项可据此建立稳定记录 ID |
1579
+ | `getTaskId()` | 关联人工任务;用同一 task ID 合并任务与活动两种视角 |
1580
+ | `getExecutionId()` | 执行关联线索,不默认当作 `visitId` |
1581
+ | `getStartTime()` / `getEndTime()` | 转换为带时区的开始和结束时间 |
1582
+ | `getAssignee()` | 处理人标识;显示名称另向宿主人员服务解析 |
1583
+ | `getDeleteReason()` | 状态判断的输入之一,不通用等价于“驳回” |
1584
+
1585
+ Flowable 历史记录包含进行中与已结束实例,因此实时任务和历史查询结果应关联去重,而不是直接拼接。历史级别影响可获得的记录:`none` 没有可用历史;`activity` 保留活动级信息;`audit` / `full` 提供更丰富记录。历史清理也可能移除旧记录。接入前应记录宿主实际配置和保留策略,而不是只检查接口返回成功。[Flowable History](https://www.flowable.com/open-source/docs/bpmn/ch10-History)、[Activiti 6 History](https://www.activiti.org/userguide/6.latest/#history)
1586
+
1587
+ 上述字段并不构成完整业务审计契约。`approve/reject`、回退目标、会签访问身份、完成门槛及可靠的连线经过记录必须结合宿主已有日志或引擎扩展。数据采集不足时,在宿主页面说明“轨迹不完整”;即使历史级别为 `full`,也不要把自定义审批业务含义视为自动可得。
1588
+
1589
+ ### 3.2 宿主 DTO 与纯转换
1590
+
1591
+ 以下 `runtime-adapter.ts` 是可复制的**宿主参考代码**,不是 Nova 导出,也不是某个引擎的 REST Schema。接口边界先完成鉴权、分页、原始 JSON 校验、人员名称解析及一致性读取,再交给此函数。`revision` 是宿主为同一工作项提供的单调版本,不能用数组位置生成。
1592
+
1593
+ 人工任务统一使用 `{ kind: 'task', id: taskId }`,活动历史中的人工任务也使用相同键;非任务活动使用 `{ kind: 'activity', id: historicActivityId }`。不要同时把同一人工任务映射成一个 task 记录和一个 activity 记录。若历史暂时没有对应 task ID,应先完成关联或报告缺失,不能换一个新 ID 填入。
1594
+
1595
+ ```ts title="runtime-adapter.ts"
1596
+ import type {
1597
+ ActivityInstance, ProcessInstanceSnapshot, RuntimeApprovalAction,
1598
+ RuntimeEdgeVisit, RuntimeTransition,
1599
+ } from '@bpmn-nova/vue'
1600
+
1601
+ export interface WorkItemKey { kind: 'task' | 'activity'; id: string }
1602
+ export interface HostActivity {
1603
+ key: WorkItemKey
1604
+ revision: number
1605
+ nodeId: string
1606
+ visitId: string
1607
+ state: ActivityInstance['status']
1608
+ startedAt: string
1609
+ endedAt?: string
1610
+ participant?: ActivityInstance['participant']
1611
+ approval?: Pick<ActivityInstance,
1612
+ 'approvalMode' | 'multiInstanceId' | 'multiInstanceMode' |
1613
+ 'totalInstances' | 'requiredInstances'>
1614
+ }
1615
+ export interface HostOperation extends Omit<RuntimeApprovalAction, 'activityId'> {
1616
+ workItem: WorkItemKey
1617
+ }
1618
+ export interface HostMovement extends Omit<RuntimeTransition,
1619
+ 'sourceActivityId' | 'invalidatedActivityIds' | 'actionId'> {
1620
+ sourceWorkItem: WorkItemKey
1621
+ actionId: string
1622
+ invalidatedElementIds: string[]
1623
+ }
1624
+ export interface HostTraceSource {
1625
+ processInstanceId: string
1626
+ status: ProcessInstanceSnapshot['status']
1627
+ historicalActivities: HostActivity[]
1628
+ currentActivities: HostActivity[]
1629
+ operations: HostOperation[]
1630
+ movements: HostMovement[]
1631
+ visitedEdgeIds: string[]
1632
+ edgeVisits: RuntimeEdgeVisit[]
1633
+ coverage: { activities: 'complete' | 'partial'; actions: 'complete' | 'partial'; edges: 'complete' | 'partial' }
1634
+ }
1635
+ export interface ModelIds {
1636
+ nodes: ReadonlySet<string>
1637
+ edges: ReadonlySet<string>
1638
+ }
1639
+
1640
+ export function workItemId(key: WorkItemKey): string {
1641
+ if (!key.id.trim()) throw new Error('缺少工作项来源 ID')
1642
+ return `${key.kind}:${key.id}`
1643
+ }
1644
+
1645
+ export function toNovaRuntime(source: HostTraceSource, ids: ModelIds): ProcessInstanceSnapshot {
1646
+ const requireId = (set: ReadonlySet<string>, id: string, field: string) => {
1647
+ if (!set.has(id)) throw new Error(`${field} 无法关联: ${id}`)
1648
+ }
1649
+ const requireTime = (time: string) => {
1650
+ if (!/T.*(?:Z|[+-]\d{2}:\d{2})$/.test(time) || !Number.isFinite(Date.parse(time))) {
1651
+ throw new Error(`需要带时区的 ISO 时间: ${time}`)
1652
+ }
1653
+ }
1654
+ if (!source.processInstanceId.trim()) throw new Error('缺少 processInstanceId')
1655
+ const records = new Map<string, { revision: number; activity: ActivityInstance }>()
1656
+ for (const row of [...source.historicalActivities, ...source.currentActivities]) {
1657
+ requireId(ids.nodes, row.nodeId, 'nodeId')
1658
+ if (!row.visitId.trim()) throw new Error(`缺少 visitId: ${row.nodeId}`)
1659
+ if (!Number.isSafeInteger(row.revision) || row.revision < 0) throw new Error('无效工作项 revision')
1660
+ requireTime(row.startedAt)
1661
+ if (row.endedAt) {
1662
+ requireTime(row.endedAt)
1663
+ if (Date.parse(row.endedAt) < Date.parse(row.startedAt)) throw new Error('工作项结束早于开始')
1664
+ }
1665
+ const activity: ActivityInstance = {
1666
+ id: workItemId(row.key), elementId: row.nodeId, visitId: row.visitId,
1667
+ status: row.state, startTime: row.startedAt, endTime: row.endedAt,
1668
+ participant: row.participant ? {
1669
+ id: row.participant.id, name: row.participant.name, avatarUrl: row.participant.avatarUrl,
1670
+ } : undefined,
1671
+ approvalMode: row.approval?.approvalMode, multiInstanceId: row.approval?.multiInstanceId,
1672
+ multiInstanceMode: row.approval?.multiInstanceMode,
1673
+ totalInstances: row.approval?.totalInstances, requiredInstances: row.approval?.requiredInstances,
1674
+ }
1675
+ const previous = records.get(activity.id)
1676
+ if (previous && (previous.activity.elementId !== activity.elementId || previous.activity.visitId !== activity.visitId)) {
1677
+ throw new Error(`工作项身份冲突: ${activity.id}`)
1678
+ }
1679
+ if (previous && previous.revision === row.revision && JSON.stringify(previous.activity) !== JSON.stringify(activity)) {
1680
+ throw new Error(`相同 revision 的记录冲突: ${activity.id}`)
1681
+ }
1682
+ if (!previous || row.revision > previous.revision) records.set(activity.id, { revision: row.revision, activity })
1683
+ }
1684
+ const activities = [...records.values()].map(({ activity }) => activity)
1685
+ .sort((a, b) => Date.parse(a.startTime!) - Date.parse(b.startTime!) || a.id.localeCompare(b.id))
1686
+ const visits = new Map<string, string>()
1687
+ for (const activity of activities) {
1688
+ const nodeId = visits.get(activity.visitId!)
1689
+ if (nodeId && nodeId !== activity.elementId) throw new Error(`visitId 跨节点复用: ${activity.visitId}`)
1690
+ visits.set(activity.visitId!, activity.elementId)
1691
+ }
1692
+ const actionIds = new Set<string>()
1693
+ const actions: RuntimeApprovalAction[] = source.operations.map(({ workItem, ...operation }) => {
1694
+ if (!operation.id.trim() || actionIds.has(operation.id)) throw new Error(`操作 ID 缺失或重复: ${operation.id}`)
1695
+ actionIds.add(operation.id)
1696
+ requireTime(operation.occurredAt)
1697
+ const record = records.get(workItemId(workItem))?.activity
1698
+ if (!record || record.elementId !== operation.elementId || record.visitId !== operation.visitId) {
1699
+ throw new Error(`操作与工作项不匹配: ${operation.id}`)
1700
+ }
1701
+ if (operation.targetElementId) requireId(ids.nodes, operation.targetElementId, 'targetElementId')
1702
+ return { ...structuredClone(operation), activityId: record.id }
1703
+ }).sort((a, b) => Date.parse(a.occurredAt) - Date.parse(b.occurredAt) || a.id.localeCompare(b.id))
1704
+ const movementIds = new Set<string>()
1705
+ const transitions: RuntimeTransition[] = source.movements.map(({ sourceWorkItem, invalidatedElementIds, ...movement }) => {
1706
+ if (!movement.id.trim() || movementIds.has(movement.id)) throw new Error(`跳转 ID 缺失或重复: ${movement.id}`)
1707
+ movementIds.add(movement.id)
1708
+ const record = records.get(workItemId(sourceWorkItem))?.activity
1709
+ const action = actions.find((item) => item.id === movement.actionId)
1710
+ if (!record || record.elementId !== movement.sourceElementId || !action || action.activityId !== record.id
1711
+ || action.type !== movement.type || action.targetElementId !== movement.targetElementId) {
1712
+ throw new Error(`跳转与操作不匹配: ${movement.id}`)
1713
+ }
1714
+ if (movement.occurredAt) requireTime(movement.occurredAt)
1715
+ if (movement.resolvedAt) requireTime(movement.resolvedAt)
1716
+ if (movement.resolvedByActivityId && !records.has(movement.resolvedByActivityId)) throw new Error('跳转完成工作项不存在')
1717
+ invalidatedElementIds.forEach((id) => requireId(ids.nodes, id, 'invalidatedElementIds'))
1718
+ ;(movement.invalidatedEdgeIds ?? []).forEach((id) => requireId(ids.edges, id, 'invalidatedEdgeIds'))
1719
+ return { ...structuredClone(movement), sourceActivityId: record.id, invalidatedActivityIds: [...invalidatedElementIds] }
1720
+ })
1721
+ const edgeVisitIds = new Set<string>()
1722
+ for (const visit of source.edgeVisits) {
1723
+ if (!visit.id.trim() || edgeVisitIds.has(visit.id)) throw new Error(`连线经过 ID 缺失或重复: ${visit.id}`)
1724
+ edgeVisitIds.add(visit.id)
1725
+ requireId(ids.edges, visit.edgeId, 'edgeId')
1726
+ if (visit.occurredAt) requireTime(visit.occurredAt)
1727
+ }
1728
+ const visitedEdges = [...new Set([...source.visitedEdgeIds, ...source.edgeVisits.map((visit) => visit.edgeId)])]
1729
+ visitedEdges.forEach((id) => requireId(ids.edges, id, 'visitedEdges'))
1730
+ return {
1731
+ processInstanceId: source.processInstanceId, status: source.status, activities,
1732
+ actions, transitions, visitedEdges, edgeVisits: structuredClone(source.edgeVisits),
1733
+ }
1734
+ }
1735
+ ```
1736
+
1737
+ 此函数只去重同一工作项的不同读取视角,不合并不同工作项或不同访问轮次。相同版本冲突直接报错;不会用旧活动记录覆盖较新任务记录。它是已校验 DTO 的教学转换层,不是通用 JSON Schema 校验器,也不替宿主推断审批状态、轮次或跳转范围。
1738
+
1739
+ 参考 DTO 针对本文的工作项审批场景,要求每个操作都关联已采集工作项。公共 Action 接口的 `activityId` 实际可选;宿主存在只有节点与访问身份的操作时,应保留真实 `elementId / visitId` 并按该接口扩展转换层,而不是伪造工作项来满足本例。
1740
+
1741
+ `coverage` 是宿主页面的完整性提示,不属于 Nova Snapshot。示例中的 `ModelIds` 从同版本解析模型生成:节点集合取 `model.nodes[].id`,连线集合取 `model.edges[].id`。身份不匹配时应保留上次成功显示的数据并提示错误,而不是改 XML ID 来迁就日志。
1742
+
1743
+ ## 4. 同一流程的完整业务场景
1744
+
1745
+ 以下 `runtime-scenarios.ts` 与第 3 节转换器一起使用,没有省略的输入数组。公共事实只定义一次,各场景对象独立组装,`scenarioSnapshots` 保存每份输入对应的完整 Snapshot;`JSON.stringify(scenarioSnapshots.reject.runtime, null, 2)` 可查看驳回场景的完整输出。
1746
+
1747
+ 这些是教学数据,不是 Playground 的真实接口响应。“单人完成”“或签完成”是不同实例分支;“驳回 → 重新提交”则是同一实例的后续快照。会签/或签示例只说明相同可视节点如何接收不同工作项,实际引擎模型及宿主审批策略必须与事实一致。
1748
+
1749
+ ```ts title="runtime-scenarios.ts"
1750
+ import type { RuntimeEdgeVisit } from '@bpmn-nova/vue'
1751
+ import { toNovaRuntime } from './runtime-adapter'
1752
+ import type { HostActivity, HostOperation, HostTraceSource, ModelIds } from './runtime-adapter'
1753
+
1754
+ // 教学 XML 的身份集合。真实接入从实例对应的解析模型生成。
1755
+ const ids: ModelIds = {
1756
+ nodes: new Set(['StartEvent_1', 'Task_Submit', 'Task_Review', 'EndEvent_1']),
1757
+ edges: new Set(['Flow_Start_Submit', 'Flow_Submit_Review', 'Flow_Review_End']),
1758
+ }
1759
+ const applicant = { id: 'u-applicant', name: '申请人' }
1760
+ const manager = { id: 'u-li', name: '李经理' }
1761
+ const start: HostActivity = {
1762
+ key: { kind: 'activity', id: 'h-start' }, revision: 1,
1763
+ nodeId: 'StartEvent_1', visitId: 'visit-start-1', state: 'completed',
1764
+ startedAt: '2026-09-05T09:00:00+08:00', endedAt: '2026-09-05T09:00:00+08:00',
1765
+ }
1766
+ const submit: HostActivity = {
1767
+ key: { kind: 'task', id: 't-submit-1' }, revision: 2,
1768
+ nodeId: 'Task_Submit', visitId: 'visit-submit-1', state: 'completed', participant: applicant,
1769
+ startedAt: '2026-09-05T09:00:01+08:00', endedAt: '2026-09-05T09:05:00+08:00',
1770
+ }
1771
+ const review: HostActivity = {
1772
+ key: { kind: 'task', id: 't-review-1' }, revision: 2,
1773
+ nodeId: 'Task_Review', visitId: 'visit-review-1', state: 'active', participant: manager,
1774
+ startedAt: '2026-09-05T09:05:01+08:00', approval: { approvalMode: 'single' },
1775
+ }
1776
+ const submitAction: HostOperation = {
1777
+ id: 'op-submit-1', type: 'submit', workItem: submit.key,
1778
+ elementId: submit.nodeId, visitId: submit.visitId, actor: applicant,
1779
+ occurredAt: '2026-09-05T09:05:00+08:00', content: { plainText: '提交申请。' },
1780
+ }
1781
+ const beforeReview: RuntimeEdgeVisit[] = [
1782
+ { id: 'ev-1', edgeId: 'Flow_Start_Submit', occurredAt: '2026-09-05T09:00:01+08:00', status: 'effective' },
1783
+ { id: 'ev-2', edgeId: 'Flow_Submit_Review', occurredAt: '2026-09-05T09:05:01+08:00', status: 'effective' },
1784
+ ]
1785
+
1786
+ export const singleInput: HostTraceSource = {
1787
+ processInstanceId: 'PI-1001', status: 'running',
1788
+ historicalActivities: [start, submit, { ...review, revision: 1, participant: undefined }],
1789
+ currentActivities: [review], operations: [submitAction], movements: [],
1790
+ visitedEdgeIds: ['Flow_Start_Submit', 'Flow_Submit_Review'], edgeVisits: beforeReview,
1791
+ coverage: { activities: 'complete', actions: 'complete', edges: 'complete' },
1792
+ }
1793
+ export const waitingInput: HostTraceSource = {
1794
+ ...singleInput, processInstanceId: 'PI-WAITING',
1795
+ currentActivities: [{ ...review, revision: 3, participant: undefined }],
1796
+ }
1797
+
1798
+ const reviewed: HostActivity = {
1799
+ ...review, revision: 3, state: 'completed', endedAt: '2026-09-05T09:10:00+08:00',
1800
+ }
1801
+ const approveAction: HostOperation = {
1802
+ id: 'op-approve-1', type: 'approve', workItem: review.key,
1803
+ elementId: review.nodeId, visitId: review.visitId, actor: manager,
1804
+ occurredAt: '2026-09-05T09:10:00+08:00', content: { plainText: '同意。' },
1805
+ }
1806
+ const end: HostActivity = {
1807
+ key: { kind: 'activity', id: 'h-end' }, revision: 1,
1808
+ nodeId: 'EndEvent_1', visitId: 'visit-end-1', state: 'completed',
1809
+ startedAt: '2026-09-05T09:10:01+08:00', endedAt: '2026-09-05T09:10:01+08:00',
1810
+ }
1811
+ const toEnd: RuntimeEdgeVisit = {
1812
+ id: 'ev-end-1', edgeId: 'Flow_Review_End', occurredAt: '2026-09-05T09:10:01+08:00', status: 'effective',
1813
+ }
1814
+ export const completedInput: HostTraceSource = {
1815
+ ...singleInput, processInstanceId: 'PI-COMPLETED', status: 'completed',
1816
+ historicalActivities: [start, submit, reviewed, end], currentActivities: [],
1817
+ operations: [submitAction, approveAction], edgeVisits: [...beforeReview, toEnd],
1818
+ }
1819
+
1820
+ const allApproval: NonNullable<HostActivity['approval']> = {
1821
+ approvalMode: 'all', multiInstanceMode: 'parallel', multiInstanceId: 'mi-review-1',
1822
+ totalInstances: 3, requiredInstances: 3,
1823
+ }
1824
+ const reviewerTwo: HostActivity = {
1825
+ ...review, key: { kind: 'task', id: 't-review-2' }, participant: { id: 'u-wang', name: '王经理' }, approval: allApproval,
1826
+ }
1827
+ const reviewerThree: HostActivity = {
1828
+ ...review, key: { kind: 'task', id: 't-review-3' }, participant: { id: 'u-zhao', name: '赵经理' }, approval: allApproval,
1829
+ }
1830
+ export const countersignInput: HostTraceSource = {
1831
+ ...singleInput, processInstanceId: 'PI-ALL',
1832
+ historicalActivities: [start, submit, { ...reviewed, approval: allApproval }],
1833
+ currentActivities: [reviewerTwo, reviewerThree], operations: [submitAction, approveAction],
1834
+ }
1835
+ const anyApproval: NonNullable<HostActivity['approval']> = { ...allApproval, approvalMode: 'any', requiredInstances: 1 }
1836
+ export const anySignInput: HostTraceSource = {
1837
+ ...singleInput, processInstanceId: 'PI-ANY', status: 'completed',
1838
+ historicalActivities: [
1839
+ start, submit, { ...reviewed, approval: anyApproval },
1840
+ { ...reviewerTwo, revision: 3, state: 'cancelled', endedAt: '2026-09-05T09:10:00+08:00', approval: anyApproval },
1841
+ { ...reviewerThree, revision: 3, state: 'cancelled', endedAt: '2026-09-05T09:10:00+08:00', approval: anyApproval },
1842
+ end,
1843
+ ],
1844
+ currentActivities: [], operations: [submitAction, approveAction], edgeVisits: [...beforeReview, toEnd],
1845
+ }
1846
+
1847
+ const submitAgain: HostActivity = {
1848
+ ...submit, key: { kind: 'task', id: 't-submit-2' }, revision: 1,
1849
+ visitId: 'visit-submit-2', state: 'active', startedAt: '2026-09-05T09:10:01+08:00', endedAt: undefined,
1850
+ }
1851
+ const rejectAction: HostOperation = {
1852
+ id: 'op-reject-1', type: 'reject', workItem: review.key,
1853
+ elementId: review.nodeId, visitId: review.visitId, actor: manager,
1854
+ targetElementId: 'Task_Submit', occurredAt: '2026-09-05T09:10:00+08:00',
1855
+ content: { plainText: '请补充材料后重新提交。' },
1856
+ }
1857
+ export const rejectInput: HostTraceSource = {
1858
+ ...singleInput,
1859
+ historicalActivities: [start, submit, reviewed], currentActivities: [submitAgain],
1860
+ operations: [submitAction, rejectAction],
1861
+ movements: [{
1862
+ id: 'move-reject-1', type: 'reject', actionId: rejectAction.id, sourceWorkItem: review.key,
1863
+ sourceElementId: 'Task_Review', targetElementId: 'Task_Submit', operator: manager.name,
1864
+ occurredAt: '2026-09-05T09:10:00+08:00', state: 'active',
1865
+ invalidatedElementIds: ['Task_Review'], invalidatedEdgeIds: ['Flow_Submit_Review'],
1866
+ }],
1867
+ edgeVisits: beforeReview.map((visit) => visit.id === 'ev-2' ? { ...visit, status: 'superseded' } : visit),
1868
+ }
1869
+ const resubmitted: HostActivity = {
1870
+ ...submitAgain, revision: 2, state: 'completed', endedAt: '2026-09-05T09:15:00+08:00',
1871
+ }
1872
+ const reviewAgain: HostActivity = {
1873
+ ...review, key: { kind: 'task', id: 't-review-round-2' }, revision: 1,
1874
+ visitId: 'visit-review-2', startedAt: '2026-09-05T09:15:01+08:00',
1875
+ }
1876
+ export const reentryInput: HostTraceSource = {
1877
+ ...rejectInput,
1878
+ historicalActivities: [start, submit, reviewed, resubmitted], currentActivities: [reviewAgain],
1879
+ operations: [submitAction, rejectAction, {
1880
+ ...submitAction, id: 'op-submit-2', workItem: submitAgain.key, visitId: submitAgain.visitId,
1881
+ occurredAt: '2026-09-05T09:15:00+08:00', content: { plainText: '已补充材料,重新提交。' },
1882
+ }],
1883
+ movements: rejectInput.movements.map((movement) => ({
1884
+ ...movement, state: 'resolved', resolvedAt: '2026-09-05T09:15:00+08:00', resolvedByActivityId: 'task:t-submit-2',
1885
+ })),
1886
+ edgeVisits: [...rejectInput.edgeVisits, {
1887
+ id: 'ev-3', edgeId: 'Flow_Submit_Review', occurredAt: '2026-09-05T09:15:01+08:00', status: 'effective',
1888
+ }],
1889
+ }
1890
+
1891
+ export const scenarioSnapshots = {
1892
+ single: { input: singleInput, runtime: toNovaRuntime(singleInput, ids) },
1893
+ waiting: { input: waitingInput, runtime: toNovaRuntime(waitingInput, ids) },
1894
+ completed: { input: completedInput, runtime: toNovaRuntime(completedInput, ids) },
1895
+ countersign: { input: countersignInput, runtime: toNovaRuntime(countersignInput, ids) },
1896
+ anySign: { input: anySignInput, runtime: toNovaRuntime(anySignInput, ids) },
1897
+ reject: { input: rejectInput, runtime: toNovaRuntime(rejectInput, ids) },
1898
+ reentry: { input: reentryInput, runtime: toNovaRuntime(reentryInput, ids) },
1899
+ }
1900
+ ```
1901
+
1902
+ | 输入 → 完整输出 | 预期标准 Presentation 与默认展示 |
1903
+ | --- | --- |
1904
+ | `singleInput` → `scenarioSnapshots.single.runtime` | 3 条活动,而非 4 条;审核只有一个工作项,显示“处理中”、李经理 |
1905
+ | `waitingInput` → `scenarioSnapshots.waiting.runtime` | 审核 active,无处理人;本示例无候选配置,显示“待分配审批人” |
1906
+ | `completedInput` → `scenarioSnapshots.completed.runtime` | 流程 completed,审核已完成且有“通过”动作;结束边已经过 |
1907
+ | `countersignInput` → `scenarioSnapshots.countersign.runtime` | 审核同一 visit 中 3 个工作项,1 completed、2 active,显示“会签 1/3” |
1908
+ | `anySignInput` → `scenarioSnapshots.anySign.runtime` | 1 completed、2 cancelled,显示“或签 1/3”,门槛为 1,流程已完成;不为被取消工作项制造通过动作 |
1909
+ | `rejectInput` → `scenarioSnapshots.reject.runtime` | 提交节点第 2 次到达并 active,显示“重新审批”;审核保留驳回事实,旧审核路径失效,回退关系可见 |
1910
+ | `reentryInput` → `scenarioSnapshots.reentry.runtime` | 审核第 2 次到达显示处理中;旧驳回仍在历史内但已 resolved,回退线隐藏,新的 `ev-3` 恢复有效线路 |
1911
+
1912
+ `approval` 和 `compact` 会重排、聚合或过滤图元,展示不保证与 `standard` 的位置相同。普通开始/结束事件默认在完整 BPMN 可见,在实际路径/时间线默认隐藏。需要显示时设置 `runtimeTraceOptions.showStartMilestone / showEndMilestone`。
1913
+
1914
+ 默认投影可能追加唯一可预测的后续人工节点;预测不是已发生事实,也不应写回 `activities` 或审批日志。需要严格只读历史时配置 `runtimeTraceOptions.includePredicted: false`;Studio 中放在 `config.viewer.runtimeTraceOptions`。
1915
+
1916
+ ## 5. 加载、更新和交互
1917
+
1918
+ ### 5.1 宿主加载契约
1919
+
1920
+ **自 `0.3.5-preview` 起的侧栏行为**:Studio `instance` 默认隐藏左右区域,右侧没有展开按钮,也不占宽度。下方示例仍显式写 `regions: { left: 'hidden', right: 'hidden' }`,可兼容旧版;如果要显示宿主业务详情,改为 `config.ui.regions.right: 'default'` 并使用 Vue `#right`、React `right` 或 Core `slots.right`。右侧 Slot 不会自动覆盖 `hidden`。
1921
+
1922
+ 框架 Header/Right Context 的 `panelSelection` 和 Core `getPanelSelection()` / `subscribePanelSelection()` 提供当前选择、元素及轨迹上下文;Instance 点击不改变设计选择。面板可用默认 Flex 固定头尾/中间滚动,也可设为 Scroll 整体滚动。隐藏或收起右侧不关闭 `trace-click`、内置详情或附件交互,详见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠)。独立 `BpmnViewer` 不拥有 Shell 侧栏,不传入 `config.ui`。
1923
+
1924
+ 下面的 `trace-contract.ts` 只定义宿主函数的返回约定,不创建 Nova HTTP 端点。宿主实现 `loadTrace` 时应完成读取、原始 JSON 校验、`toNovaRuntime()` 转换及完整性说明;返回的 XML 和 Runtime 必须属于同一实例的定义版本。历史不可用时由宿主报告错误或明确的 partial 状态,不能把查询失败转成“没有经过任何节点”。
1925
+
1926
+ ```ts title="trace-contract.ts"
1927
+ import type { ProcessInstanceSnapshot } from '@bpmn-nova/vue'
1928
+ import type { HostTraceSource } from './runtime-adapter'
1929
+
1930
+ export interface TraceDocument {
1931
+ processDefinitionId: string
1932
+ xml: string
1933
+ runtime: ProcessInstanceSnapshot
1934
+ coverage: HostTraceSource['coverage']
1935
+ }
1936
+ export type LoadTrace = (instanceId: string, signal: AbortSignal) => Promise<TraceDocument>
1937
+
1938
+ export function checkTraceDocument(next: TraceDocument, instanceId: string, previous: TraceDocument | null): void {
1939
+ if (next.runtime.processInstanceId !== instanceId) throw new Error('审批轨迹响应的实例 ID 不匹配')
1940
+ if (!next.processDefinitionId || !next.xml.trim()) throw new Error('审批轨迹缺少定义版本或 XML')
1941
+ if (previous?.processDefinitionId === next.processDefinitionId && previous.xml !== next.xml) {
1942
+ throw new Error('同一定义版本返回了不同 XML,请检查定义服务')
1943
+ }
1944
+ }
1945
+ ```
1946
+
1947
+ 下面的刷新都是替换 Snapshot 对象,不是往原数组里 `push()`。Vue watcher 和 React effect 依赖 Prop 引用;闭包数据的定义态 `refreshPresentation()` 不能替代 Runtime 数据更新。
1948
+
1949
+ ### 5.2 Vue:Studio 实例模式,防止旧请求覆盖
1950
+
1951
+ `ApprovalTrace.vue` 接收宿主的 `instanceId` 和 `loadTrace`,通过 `refresh()` 暴露主动刷新入口。首次加载显示占位,同实例刷新保留当前组件;切换实例先移除旧实例内容,避免旧数据冒充新实例。宿主函数变化时保持同样的取消语义。
1952
+
1953
+ ```vue title="ApprovalTrace.vue"
1954
+ <script setup lang="ts">
1955
+ import { ref, shallowRef, watch } from 'vue'
1956
+ import { BpmnStudio } from '@bpmn-nova/vue'
1957
+ import type { BpmnStudioConfig, RuntimeTraceClickEvent } from '@bpmn-nova/vue'
1958
+ import '@bpmn-nova/vue/styles.css'
1959
+ import { checkTraceDocument } from './trace-contract'
1960
+ import type { LoadTrace, TraceDocument } from './trace-contract'
1961
+
1962
+ const props = defineProps<{ instanceId: string | null; loadTrace: LoadTrace }>()
1963
+ const emit = defineEmits<{ 'trace-click': [event: RuntimeTraceClickEvent] }>()
1964
+ const document = shallowRef<TraceDocument | null>(null)
1965
+ const loading = ref(false)
1966
+ const error = ref('')
1967
+ const refreshKey = ref(0)
1968
+ const config: BpmnStudioConfig = {
1969
+ ui: { regions: { left: 'hidden', right: 'hidden' } },
1970
+ viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
1971
+ }
1972
+
1973
+ watch(() => [props.instanceId, props.loadTrace, refreshKey.value] as const, async ([id, load], _previous, onCleanup) => {
1974
+ const controller = new AbortController()
1975
+ onCleanup(() => controller.abort())
1976
+ if (document.value?.runtime.processInstanceId !== id) document.value = null
1977
+ error.value = ''
1978
+ loading.value = Boolean(id)
1979
+ if (!id) return
1980
+ try {
1981
+ const next = await load(id, controller.signal)
1982
+ if (controller.signal.aborted) return
1983
+ checkTraceDocument(next, id, document.value)
1984
+ document.value = next
1985
+ } catch (cause) {
1986
+ if (!controller.signal.aborted) error.value = cause instanceof Error ? cause.message : '轨迹加载失败'
1987
+ } finally {
1988
+ if (!controller.signal.aborted) loading.value = false
1989
+ }
1990
+ }, { immediate: true })
1991
+
1992
+ defineExpose({ refresh: () => { refreshKey.value += 1 } })
1993
+ </script>
1994
+
1995
+ <template>
1996
+ <section>
1997
+ <p v-if="!instanceId">流程尚未发起</p>
1998
+ <p v-else-if="loading" role="status">{{ document ? '正在刷新轨迹…' : '正在加载轨迹…' }}</p>
1999
+ <p v-if="error" role="alert">{{ error }}</p>
2000
+ <p v-if="document && Object.values(document.coverage).includes('partial')">轨迹记录不完整,请结合业务审计查看。</p>
2001
+ <div v-if="document" style="height: 640px; min-height: 320px">
2002
+ <BpmnStudio :xml="document.xml" :runtime="document.runtime" :config="config"
2003
+ engine="flowable" mode="instance" :allowed-modes="['instance']" projection="approval"
2004
+ @trace-click="emit('trace-click', $event)" />
2005
+ </div>
2006
+ </section>
2007
+ </template>
2008
+ ```
2009
+
2010
+ 这里禁用了内置详情自动打开,宿主监听 `trace-click` 自行展示。需要 Nova 默认详情时设 `config.viewer.runtimeDetails.autoOpen: true`,宿主事件只做观察。卸载时 Vue 自动清理 watcher 并取消当前请求,适配组件负责销毁自己创建的实例。
2011
+
2012
+ ### 5.3 React:独立 Viewer 与 Studio 选择
2013
+
2014
+ `ApprovalTrace.tsx` 使用相同的宿主加载契约。`loadTrace` 应由宿主以稳定函数引用传入;`refreshKey` 变化代表一次新刷新,不应作为组件 React `key` 使用。
2015
+
2016
+ ```tsx title="ApprovalTrace.tsx"
2017
+ import { useEffect, useRef, useState } from 'react'
2018
+ import { BpmnStudio, BpmnViewer } from '@bpmn-nova/react'
2019
+ import type { BpmnStudioConfig, RuntimeTraceClickEvent } from '@bpmn-nova/react'
2020
+ import '@bpmn-nova/react/styles.css'
2021
+ import { checkTraceDocument } from './trace-contract'
2022
+ import type { LoadTrace, TraceDocument } from './trace-contract'
2023
+
2024
+ const studioConfig: BpmnStudioConfig = {
2025
+ ui: { regions: { left: 'hidden', right: 'hidden' } },
2026
+ viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
2027
+ }
2028
+ const details = { autoOpen: false }
2029
+ const traceOptions = { includePredicted: false }
2030
+
2031
+ export function ApprovalTrace({ instanceId, loadTrace, refreshKey = 0, useStudio = false, onTraceClick }: {
2032
+ instanceId: string | null
2033
+ loadTrace: LoadTrace
2034
+ refreshKey?: number
2035
+ useStudio?: boolean
2036
+ onTraceClick?: (event: RuntimeTraceClickEvent) => void
2037
+ }) {
2038
+ const [document, setDocument] = useState<TraceDocument | null>(null)
2039
+ const lastAccepted = useRef<TraceDocument | null>(null)
2040
+ const [loading, setLoading] = useState(false)
2041
+ const [error, setError] = useState('')
2042
+ useEffect(() => {
2043
+ const controller = new AbortController()
2044
+ if (lastAccepted.current?.runtime.processInstanceId !== instanceId) lastAccepted.current = null
2045
+ setError('')
2046
+ setLoading(Boolean(instanceId))
2047
+ if (!instanceId) { setDocument(null); return () => controller.abort() }
2048
+ void (async () => {
2049
+ try {
2050
+ const next = await loadTrace(instanceId, controller.signal)
2051
+ if (controller.signal.aborted) return
2052
+ checkTraceDocument(next, instanceId, lastAccepted.current)
2053
+ lastAccepted.current = next
2054
+ setDocument(next)
2055
+ } catch (cause) {
2056
+ if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : '轨迹加载失败')
2057
+ } finally {
2058
+ if (!controller.signal.aborted) setLoading(false)
2059
+ }
2060
+ })()
2061
+ return () => controller.abort()
2062
+ }, [instanceId, loadTrace, refreshKey])
2063
+ // render 时即按实例身份过滤,避免新请求的 effect 执行前短暂显示旧实例。
2064
+ const visible = document?.runtime.processInstanceId === instanceId ? document : null
2065
+ return <section>
2066
+ {!instanceId && <p>流程尚未发起</p>}
2067
+ {loading && <p role="status">{visible ? '正在刷新轨迹…' : '正在加载轨迹…'}</p>}
2068
+ {error && <p role="alert">{error}</p>}
2069
+ {visible && Object.values(visible.coverage).includes('partial') && <p>轨迹记录不完整,请结合业务审计查看。</p>}
2070
+ {visible && <div style={{ height: 640, minHeight: 320 }}>
2071
+ {useStudio
2072
+ ? <BpmnStudio xml={visible.xml} runtime={visible.runtime} config={studioConfig}
2073
+ engine="flowable" mode="instance" allowedModes={['instance']} projection="approval" onTraceClick={onTraceClick} />
2074
+ : <BpmnViewer xml={visible.xml} runtime={visible.runtime} engine="flowable" projection="approval"
2075
+ runtimeDetails={details} runtimeTraceOptions={traceOptions} onTraceClick={onTraceClick} />}
2076
+ </div>}
2077
+ </section>
2078
+ }
2079
+ ```
2080
+
2081
+ `useStudio` 是本例宿主布局选择,不是 Nova Prop;运行中切换它会切换组件种类。普通刷新保持它不变,使用新 Runtime 对象更新既有组件。Vue 独立 Viewer 使用相同顶层 `runtime-details` / `runtime-trace-options` 配置;只有完整 Studio 使用 `config.viewer`。
2082
+
2083
+ ### 5.4 Core:显式生命周期与更新
2084
+
2085
+ `core-trace.ts` 的两个工厂分别提供独立 Viewer 和完整 Studio。只在浏览器中调用,传入有高度的容器;调用方从宿主加载函数得到成功且未过期的 `TraceDocument` 后,再调用 `update()`。同实例、同定义的刷新只设置 Runtime;换实例或定义时才替换模型。
2086
+
2087
+ ```ts title="core-trace.ts"
2088
+ import { BpmnViewer, createStudioController, createStudioShell, importBpmn } from '@bpmn-nova/studio'
2089
+ import type { RuntimeTraceClickEvent } from '@bpmn-nova/studio'
2090
+ import '@bpmn-nova/studio/styles.css'
2091
+ import { checkTraceDocument } from './trace-contract'
2092
+ import type { TraceDocument } from './trace-contract'
2093
+
2094
+ export function mountViewer(container: HTMLElement, initial: TraceDocument, onTraceClick: (event: RuntimeTraceClickEvent) => void) {
2095
+ let current = initial
2096
+ const viewer = new BpmnViewer({
2097
+ container, model: importBpmn(initial.xml, 'flowable'), runtime: initial.runtime,
2098
+ projection: 'approval', runtimeDetails: { autoOpen: false },
2099
+ runtimeTraceOptions: { includePredicted: false }, onTraceClick,
2100
+ })
2101
+ return {
2102
+ update(next: TraceDocument) {
2103
+ checkTraceDocument(next, next.runtime.processInstanceId, current)
2104
+ if (next.runtime.processInstanceId !== current.runtime.processInstanceId || next.processDefinitionId !== current.processDefinitionId) {
2105
+ viewer.setModel(importBpmn(next.xml, 'flowable'))
2106
+ }
2107
+ viewer.setRuntime(next.runtime)
2108
+ current = next
2109
+ },
2110
+ destroy: () => viewer.destroy(),
2111
+ }
2112
+ }
2113
+
2114
+ export function mountStudio(container: HTMLElement, initial: TraceDocument, onTraceClick: (event: RuntimeTraceClickEvent) => void) {
2115
+ let current = initial
2116
+ const studio = createStudioController({ model: importBpmn(initial.xml, 'flowable') })
2117
+ const shell = createStudioShell({
2118
+ container, studio, runtime: initial.runtime, mode: 'instance', allowedModes: ['instance'], projection: 'approval',
2119
+ config: {
2120
+ ui: { regions: { left: 'hidden', right: 'hidden' } },
2121
+ viewer: { runtimeDetails: { autoOpen: false }, runtimeTraceOptions: { includePredicted: false } },
2122
+ },
2123
+ rendererOptions: { onTraceClick },
2124
+ })
2125
+ return {
2126
+ update(next: TraceDocument) {
2127
+ checkTraceDocument(next, next.runtime.processInstanceId, current)
2128
+ if (next.runtime.processInstanceId !== current.runtime.processInstanceId || next.processDefinitionId !== current.processDefinitionId) {
2129
+ studio.setModel(importBpmn(next.xml, 'flowable'))
2130
+ }
2131
+ shell.setRuntime(next.runtime)
2132
+ current = next
2133
+ },
2134
+ destroy() { shell.destroy(); studio.destroy() },
2135
+ }
2136
+ }
2137
+ ```
2138
+
2139
+ Core 的调用方仍须保存当前请求的实例 ID,并使用 `AbortController` 和过期响应检查,不能直接把任意请求结果传给 `update()`。切换实例和定义是有意的模型替换,不属于“同实例刷新不重导 XML”的保证。不要在每次刷新后自动 Fit,否则会覆盖用户正在阅读的位置。
2140
+
2141
+ ### 5.5 点击数据与内置详情
2142
+
2143
+ `RuntimeTraceClickEvent.targetType` 区分 `node / edge / visit / transition`,并提供可选的原始 `elementId`、`visitId`、`traceItem`、`transition`、`presentation`,以及 `activityInstances`、`actions`、`runtime` 和实际 `projection`。
2144
+
2145
+ 所有分支应先判断 `targetType` 和可选字段,不能把每次点击都当成人工任务。在审批投影中使用事件的原始元素/访问关联,不从重排后的 DOM 属性反推业务任务 ID。事件是展示交互,不代表已经执行通过、驳回或保存;执行这些操作仍需要宿主校验权限并调用自己的业务 API。
2146
+
2147
+ ## 6. 意见、图片与附件
2148
+
2149
+ 以下 `approval-assets.ts` 为一次真实已记录的操作补充内容,并提供可注入 Viewer/Studio 的 Resolver 工厂。资源名称、MIME、大小来自宿主元数据;审批快照不存短期地址,用户头像也不要填入凭证或短期签名。
2150
+
2151
+ ```ts title="approval-assets.ts"
2152
+ import type { RuntimeApprovalContent, RuntimeAssetPurpose, RuntimeAssetResolver } from '@bpmn-nova/vue'
2153
+ import { completedInput } from './runtime-scenarios'
2154
+ import type { HostTraceSource } from './runtime-adapter'
2155
+
2156
+ export const content: RuntimeApprovalContent = {
2157
+ plainText: '同意,核验材料见附件。',
2158
+ blocks: [
2159
+ { type: 'paragraph', text: '同意,核验材料见附件。' },
2160
+ { type: 'image', assetId: 'asset-photo', alt: '材料照片' },
2161
+ { type: 'file', assetId: 'asset-checklist' },
2162
+ ],
2163
+ assets: [
2164
+ { id: 'asset-photo', name: '材料照片.png', mediaType: 'image/png', width: 1200, height: 800 },
2165
+ { id: 'asset-checklist', name: '核验清单.pdf', mediaType: 'application/pdf', size: 4096 },
2166
+ ],
2167
+ }
2168
+ export const withAssetsInput: HostTraceSource = {
2169
+ ...completedInput,
2170
+ operations: completedInput.operations.map((operation) => operation.id === 'op-approve-1' ? { ...operation, content } : operation),
2171
+ }
2172
+
2173
+ type ResolveAssetUrl = (input: {
2174
+ assetId: string; actionId: string; purpose: RuntimeAssetPurpose; signal: AbortSignal
2175
+ }) => Promise<string | null>
2176
+
2177
+ export function createAssetResolver(resolveAssetUrl: ResolveAssetUrl): RuntimeAssetResolver {
2178
+ return (asset, { action, purpose, signal }) => resolveAssetUrl({
2179
+ assetId: asset.id, actionId: action.id, purpose, signal,
2180
+ })
2181
+ }
2182
+ ```
2183
+
2184
+ 将 `withAssetsInput` 交给同一个 `toNovaRuntime()`,输出保留一个通过动作,内容包含一张图片和一个文件;不能为“补全附件展示”再新增一个重复审批操作。`resolveAssetUrl` 是宿主函数,按当前用户、实例/操作、资源及用途进行授权后返回资源地址,不返回业务 JSON 接口本身的地址。
2185
+
2186
+ `purpose` 为 `thumbnail / preview / download / export`。宿主应把 `signal` 传递到实际网络请求并及时取消;返回相对或绝对 HTTP(S) 地址、宿主管理生命周期的 Blob URL,或者无权限/不存在时返回 `null`。资源不可用时 Nova 局部降级,不应让整条审批轨迹失败。需特别检查跨域图片、实际响应 MIME 与下载权限。
2187
+
2188
+ 原生内容块解释 `paragraph / image / file`,不执行 HTML 或 Markdown。默认图片预览和附件下载由 Viewer 处理;SVG 导出会按 `export` 用途获取图片并嵌入 Artifact,普通文件只保留元数据,不把临时地址写回 Snapshot。上传、存储、删除、审计和签名生命周期属于宿主业务。
2189
+
2190
+ ## 7. 接入验收与排错
2191
+
2192
+ | 现象 | 优先检查与处理 |
2193
+ | --- | --- |
2194
+ | 画布空白 | 样式入口、浏览器生命周期、容器与父级实际高度;先区分布局问题与数据加载失败 |
2195
+ | 无实例 ID | 宿主展示“尚未发起”,等待真实实例;不创建虚假的 running Snapshot |
2196
+ | 请求中或请求失败 | 首次加载显示占位;同实例刷新失败保留上次成功数据并提示错误 |
2197
+ | 历史为空 | 检查历史级别、保留策略、分页及权限;与真正尚未到达的节点区分 |
2198
+ | 处理人或状态消失 | 检查定义版本、原始节点 ID、人员名称,以及实时/历史记录是否重复或相互覆盖 |
2199
+ | 会签人数不对 | 检查同轮 visitId、独立工作项 ID、显式 approvalMode、人数与门槛;同一工作项不能在两个来源各计一次 |
2200
+ | 驳回没有线路 | 检查 Transition、actionId、来源/目标、时间和失效范围;Action 本身不是线路 |
2201
+ | 重新审批仍显示旧状态 | 新到达是否获得新 visitId 与工作项 ID;新 edgeVisit 是否有更晚时间;回退是否正确 resolved |
2202
+ | 历史操作重复 | 检查显式 Action 的 activityId 和 Transition actionId,避免旧 comment/outcome 再次生成兼容动作 |
2203
+ | 出现未处理的后续节点 | 检查是否预测节点;严格历史视图设置 includePredicted: false |
2204
+ | 修改数组后没有刷新 | 替换 runtime 对象引用;Core 调用 setRuntime,不依赖定义态 refreshPresentation |
2205
+ | 意见附件不可用 | 核对 assetId、Resolver、用途授权、取消信号、跨域与真实资源响应 |
2206
+
2207
+ 完成接入应逐项确认:
2208
+
2209
+ 1. 安装入口与框架一致,公共类型及生产构建通过;`0.3.5-preview` 之前缺少具名 Runtime 类型出口的版本使用兼容推导。
2210
+ 2. 真实 XML 的节点/连线 ID 与记录关联一致,刷新前后定义 XML 不变。
2211
+ 3. 单人、等待、会签、或签、驳回及重入均符合宿主业务记录;结束任务没有被自动解释为通过。
2212
+ 4. 同实例刷新不重建组件或重复导入 XML;切换实例取消旧请求,响应身份不匹配时拒绝覆盖。
2213
+ 5. `standard / approval / compact` 使用相同事实;预测与真实记录可区分,历史失效记录仍可追溯。
2214
+ 6. 内置详情与宿主详情按 autoOpen 策略工作,无重复事件订阅;Light/Dark 和窄屏下内容可读。
2215
+ 7. 附件授权失败仅局部降级,卸载后请求与实例得到清理。
2216
+
2217
+ 这些是宿主集成验收项,不代表仅复制本文就已完成真实引擎联调。示例不替宿主创建业务接口、部署流程或运行审批事务。完整参数见[组件参数](COMPONENTS.md)、[公开 API](API.md)与[自定义指南](CUSTOMIZATION.md);安装入口见 [AI 接入指南](AI-INTEGRATION.md)。
2218
+
2219
+
1243
2220
  <!-- SOURCE: docs/GETTING-STARTED.md -->
1244
2221
 
1245
2222
  # BPMN Nova 快速开始
1246
2223
 
1247
- 本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.4-preview` 要求现代浏览器;Node.js 18+ 用于构建、SSR 和开发工具。
2224
+ 本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.5-preview` 要求现代浏览器;Node.js 18+ 用于构建、SSR 和开发工具。
1248
2225
 
1249
2226
  ## 1. 按语言与框架选择入口
1250
2227
 
@@ -1316,16 +2293,24 @@ const model = createEmptyProcess('flowable')
1316
2293
  model.id = 'Process_LeaveApproval'
1317
2294
  model.name = '请假审批'
1318
2295
 
2296
+ const config = {
2297
+ modeling: {
2298
+ propertiesProfile: 'business',
2299
+ allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
2300
+ allowedEdgeTypes: ['sequenceFlow'],
2301
+ },
2302
+ ui: { controlSize: 'medium' },
2303
+ }
2304
+
1319
2305
  const studio = createStudioController({
1320
2306
  model,
1321
- propertiesProfile: 'business',
1322
- allowedNodeTypes: ['startEvent', 'userTask', 'exclusiveGateway', 'endEvent'],
1323
- allowedEdgeTypes: ['sequenceFlow'],
2307
+ config,
1324
2308
  })
1325
2309
 
1326
2310
  const shell = createStudioShell({
1327
2311
  container: document.querySelector('#studio'),
1328
2312
  studio,
2313
+ config,
1329
2314
  mode: 'design',
1330
2315
  allowedModes: ['design', 'viewer'],
1331
2316
  theme: 'auto',
@@ -1348,6 +2333,8 @@ function dispose() {
1348
2333
 
1349
2334
  Controller 持有模型、命令、选择、历史与作用域;Shell 只负责组合 Canvas、Palette、Properties 和 Viewer。销毁 Shell 不会自动销毁外部传入的 Controller。
1350
2335
 
2336
+ `config` 用一个结构化入口承载静态选项:`modeling` 传给 Controller,`ui`、`viewer` 和 `export` 传给 Shell。旧顶层字段仍兼容但已 Deprecated;同一项同时配置时,显式旧值优先。
2337
+
1351
2338
  宿主可观察实际 Mode,并在运行时调整白名单:
1352
2339
 
1353
2340
  ```js
@@ -1431,6 +2418,8 @@ Designer 子路径适合已有自定义工具栏和属性面板的宿主。需
1431
2418
 
1432
2419
  ## 6. Studio Viewer 子路径与审批轨迹
1433
2420
 
2421
+ 接入真实审批后端时先阅读[审批轨迹接入指南](RUNTIME-INTEGRATION.md),按实例定义版本准备 XML,再转换工作项、操作及路径事实。指南提供完整可编译示例,以及新增 Runtime 类型出口在 `0.3.5-preview` 之前版本的兼容写法;设置 `engine` 不会自动查询引擎。
2422
+
1434
2423
  ```js
1435
2424
  import { BpmnViewer } from '@bpmn-nova/studio/viewer'
1436
2425
  import '@bpmn-nova/studio/styles.css'
@@ -1496,8 +2485,11 @@ export function WorkflowEditor({ initialXml }) {
1496
2485
  engine="flowable"
1497
2486
  mode={mode}
1498
2487
  allowedModes={['design', 'viewer']}
2488
+ config={{
2489
+ modeling: { propertiesProfile: 'business' },
2490
+ ui: { controlSize: 'medium' },
2491
+ }}
1499
2492
  nodeSubtitleResolver={subtitleResolver}
1500
- propertiesProfile="business"
1501
2493
  theme="auto"
1502
2494
  onChange={(model, reason, nextXml) => saveDraft(nextXml)}
1503
2495
  onSelectionChange={(selection, element) => console.log(element)}
@@ -1545,7 +2537,10 @@ const onChange = (model, reason, nextXml) => saveDraft(nextXml)
1545
2537
  engine="flowable"
1546
2538
  v-model:mode="mode"
1547
2539
  :allowed-modes="['design', 'viewer']"
1548
- properties-profile="business"
2540
+ :config="{
2541
+ modeling: { propertiesProfile: 'business' },
2542
+ ui: { controlSize: 'medium' },
2543
+ }"
1549
2544
  theme="auto"
1550
2545
  @change="onChange"
1551
2546
  @mode-change="event => console.log(event.source)"
@@ -1568,11 +2563,12 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
1568
2563
  ref="studioRef"
1569
2564
  :xml="xml"
1570
2565
  engine="activiti"
1571
- mode="design"
1572
- :allowed-modes="['design']"
1573
- :allowed-node-types="supportedNodeTypes"
1574
- :allowed-edge-types="['sequenceFlow']"
1575
- :regions="{ right: 'hidden' }"
2566
+ mode="design"
2567
+ :allowed-modes="['design']"
2568
+ :config="{
2569
+ modeling: { allowedNodeTypes: supportedNodeTypes, allowedEdgeTypes: ['sequenceFlow'] },
2570
+ ui: { controlSize: 'medium', regions: { right: 'hidden' } },
2571
+ }"
1576
2572
  theme="auto"
1577
2573
  @change="handleChange"
1578
2574
  @selection-change="handleSelectionChange"
@@ -1581,7 +2577,7 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
1581
2577
  <template #header-start="{ state, actions }">
1582
2578
  <!-- 返回、业务图标、流程名称、类型 -->
1583
2579
  </template>
1584
- <template #header-actions="{ actions, mode }">
2580
+ <template #header-actions="{ actions, mode, ui }">
1585
2581
  <button type="button" :disabled="mode !== 'design'" @click="actions.validate()">校验</button>
1586
2582
  <button type="button" :disabled="mode !== 'design'" @click="saveDraft(actions.exportXml())">保存</button>
1587
2583
  <button type="button" :disabled="mode !== 'design'" @click="publishProcess(actions)">发布</button>
@@ -1591,6 +2587,28 @@ Vue 事件使用 kebab-case:`change`、`selection-change`、`scope-change`、`
1591
2587
 
1592
2588
  Vue 的 `#header-start` 只替换 Brand,`#header-actions` 只替换默认“校验 / 导入 / 导出”动作组,`#header` 替换完整 Header;React 使用等价的 `headerStart` / `headerActions` / `header` Render Prop。顶部最佳视图已去重,底部入口与 `fitView()` 保留。`actions.validate()` 先展示 Nova 内部结果,再发送 `validation` / `onValidation`,最后返回 issues。`regions` 属性变化会原地更新区域,不重建 Canvas。
1593
2589
 
2590
+ ### 8.2 对齐宿主 UI 组件库
2591
+
2592
+ Header 控件通过 `config.ui.controlSize` 统一设置。默认值是 `medium`;`small / medium / large` 的普通按钮高度分别为 28 / 32 / 40px,也可传入 `24px`–`48px` 的 CSS 像素字符串:
2593
+
2594
+ ```vue
2595
+ <BpmnStudio
2596
+ ref="studioRef"
2597
+ :config="{ ui: { controlSize: '36px' } }"
2598
+ >
2599
+ <template #header-actions="{ actions, ui }">
2600
+ <HostWorkflowActions
2601
+ :actions="actions"
2602
+ :button-height="ui.controlHeight"
2603
+ />
2604
+ </template>
2605
+ </BpmnStudio>
2606
+ ```
2607
+
2608
+ Element Plus 的 `small / default / large` 可映射到 Nova `small / medium / large`;Ant Design 的 `small / middle / large` 使用相同映射。自定义动作位于 Nova Header DOM 内,可继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 和 `--nova-control-radius`。
2609
+
2610
+ 该尺寸不会影响 Footer 缩放区、Canvas 浮动工具、Palette 或 Properties。运行时可通过 `shell.setConfig()` 或框架响应式 `config` 原地更新;`shell.getConfig()` 返回当前归一化配置。`config.modeling` 只在 Controller 创建时生效,运行中只允许现有 `setPropertiesProfile()` 调整 Profile,节点/连线白名单不会改变。
2611
+
1594
2612
  ```js
1595
2613
  async function publishProcess(actions) {
1596
2614
  const issues = actions.validate()
@@ -1599,10 +2617,25 @@ async function publishProcess(actions) {
1599
2617
  }
1600
2618
  ```
1601
2619
 
1602
- 把宿主属性面板放在 Nova 外部,以 `selection-change` / `onSelectionChange` 作为状态来源,并用稳定 BPMN Element ID 关联业务配置。该事件覆盖节点、连线、多选、清空和键盘选择;`element-click` 不能替代选择状态。Nova 不负责保存、发布、权限或服务端事务。
2620
+ 宿主拥有外部属性面板时,以 `selection-change` / `onSelectionChange` 作为设计选择状态来源,用稳定 BPMN Element ID 关联业务配置;`element-click` 不能替代选择状态。希望复用 Nova 侧栏时使用下述右侧 Slot。Nova 不负责保存、发布、权限或服务端事务。
1603
2621
 
1604
2622
  `theme="auto"` 需要显式配置才会跟随系统主题;未传 Theme 时仍默认为 `light`。完全自定义布局时,React 与 Vue 均可组合 `BpmnCanvas`、`BpmnPalettePanel` 和 `BpmnPropertiesPanel`,并传入同一个外部 Controller。
1605
2623
 
2624
+ ### 8.3 右侧内容与侧栏展开收起
2625
+
2626
+ Vue `#right`、React `right`、Core `slots.right` 替换右侧内容但保留 Nova 管理的宽度、高度和折叠按钮。不要配置 `regions.right: 'hidden'` 后再期待 Slot 显示;隐藏移除整个区域,收起才保留展开按钮。
2627
+
2628
+ ```js
2629
+ const config = { ui: {
2630
+ leftPanel: { collapsible: true, defaultCollapsed: false },
2631
+ rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
2632
+ } }
2633
+ ```
2634
+
2635
+ 默认 Flex 让宿主安排固定头尾与 `flex: 1; min-height: 0; overflow: auto` 中间内容;改为 `scroll` 时由 Nova 滚动整块内容。父容器仍需完整高度链。默认折叠按钮位于左右分隔线中点,200ms 平滑过渡并尊重减少动态效果;不卸载内容、不自动 Fit。Shell 容器 ≤720px 时首次收起并保留按钮,展开继续挤压画布,宽窄屏分别记忆状态。
2636
+
2637
+ Instance 默认隐藏右侧;显式 `regions.right: 'default'` 可开启,隐藏侧栏不影响轨迹点击/详情。通过 `panelSelection` 读取跨模式的当前元素,而非从只读页面猜测设计 `state.selection`。完整 Core/Vue/React 示例、状态方法和事件见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠);这些新增能力自 `0.3.5-preview` 提供。
2638
+
1606
2639
  ## 9. Group 与 Pool/Lane
1607
2640
 
1608
2641
  Group 是纯视觉 Artifact,不保存成员关系:
@@ -1660,7 +2693,7 @@ BPMN Nova 主题应挂在组件根容器。自定义 Renderer 不要修改 `docu
1660
2693
 
1661
2694
  # BPMN Nova 组件参数
1662
2695
 
1663
- 本手册记录 `0.3.4-preview` 的公开组件参数、回调和实例方法。所有视觉组件都需要具有实际尺寸的容器,并显式导入所属包的 `styles.css`。
2696
+ 本手册记录 `0.3.5-preview` 的公开组件参数、回调和实例方法。所有视觉组件都需要具有实际尺寸的容器,并显式导入所属包的 `styles.css`。
1664
2697
 
1665
2698
  ## 按项目环境阅读
1666
2699
 
@@ -1681,6 +2714,7 @@ type EngineId = 'flowable' | 'activiti'
1681
2714
  type ViewerProjection = 'auto' | 'standard' | 'approval' | 'compact'
1682
2715
  type NovaThemeInput = 'light' | 'dark' | 'auto' | NovaThemeOptions
1683
2716
  type PropertiesProfile = 'business' | 'developer'
2717
+ type StudioControlSize = 'small' | 'medium' | 'large' | `${number}px`
1684
2718
  ```
1685
2719
 
1686
2720
  完整 `ProcessModel`、`BpmnNode`、`BpmnEdge` 与 Runtime Snapshot 类型见对应包的 `.d.ts` 和 [API](API.md)。
@@ -1693,10 +2727,11 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
1693
2727
  | --- | --- | --- | --- |
1694
2728
  | `model` | `ProcessModel` | 必填 | 初始流程模型 |
1695
2729
  | `historyLimit` | `number` | `80` | Undo/Redo 快照上限 |
1696
- | `propertiesProfile` | `'business' \| 'developer'` | `'business'` | 属性面板可见级别 |
2730
+ | `config` | `BpmnStudioConfig` | `{}` | 结构化静态配置;Controller 使用 `config.modeling` |
2731
+ | `propertiesProfile` | `'business' \| 'developer'` | `'business'` | Deprecated 兼容别名;优先于 `config.modeling.propertiesProfile` |
1697
2732
  | `extensions` | `StudioExtension[]` | `[]` | Controller 扩展,`setup()` 可返回清理函数 |
1698
- | `allowedNodeTypes` | `Iterable<NodeType> \| null` | `null` | 完整模型节点白名单;同时约束导入与所有创建/转换命令 |
1699
- | `allowedEdgeTypes` | `Iterable<EdgeType> \| null` | `null` | 完整模型连线白名单;同时约束导入、连接与模板命令 |
2733
+ | `allowedNodeTypes` | `Iterable<NodeType> \| null` | `null` | Deprecated 兼容别名;完整模型节点白名单 |
2734
+ | `allowedEdgeTypes` | `Iterable<EdgeType> \| null` | `null` | Deprecated 兼容别名;完整模型连线白名单 |
1700
2735
 
1701
2736
  常用方法:
1702
2737
 
@@ -1719,17 +2754,18 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
1719
2754
  | --- | --- | --- | --- |
1720
2755
  | `container` | `HTMLElement` | 必填 | Studio 根容器 |
1721
2756
  | `studio` | `BpmnStudioController` | 必填 | 外部 Controller |
2757
+ | `config` | `BpmnStudioConfig` | `{}` | `ui/viewer/export` 静态配置;`modeling` 由 Controller 创建时消费 |
1722
2758
  | `mode` | `'design' \| 'viewer' \| 'instance'` | `'design'` | 当前展示模式 |
1723
2759
  | `allowedModes` | `('design' \| 'viewer' \| 'instance')[]` | 全部模式 | 默认工作台显示并允许切换的模式 |
1724
2760
  | `runtime` | `ProcessInstanceSnapshot \| null` | `null` | 实例模式数据;没有数据时保持空运行事实 |
1725
2761
  | `projection` | `ViewerProjection` | 实例模式默认 `approval` | 审批轨迹投影 |
1726
- | `responsive` | `boolean` | `false` | 是否允许审批投影响应容器宽度 |
1727
- | `projectionOptions` | `{ value, label }[]` | 默认两项 | Shell 轨迹切换选项 |
1728
- | `leftWidth` | `number` | `244` | 左侧栏宽度,像素 |
1729
- | `rightWidth` | `number` | `360` | 右侧栏宽度,像素 |
2762
+ | `responsive` | `boolean` | `false` | Deprecated;使用 `config.viewer.responsive` |
2763
+ | `projectionOptions` | `{ value, label }[]` | 默认两项 | Deprecated;使用 `config.viewer.projectionOptions` |
2764
+ | `leftWidth` | `number` | `244` | Deprecated;使用 `config.ui.sidebarWidth.left` |
2765
+ | `rightWidth` | `number` | `360` | Deprecated;使用 `config.ui.sidebarWidth.right` |
1730
2766
  | `theme` | `NovaThemeInput` | `light` | 组件实例主题 |
1731
2767
  | `runtimeAppearance` | `RuntimeAppearanceOptions` | 默认语义映射 | 状态、动作、转移的 Tone 映射 |
1732
- | `svgExport` | `SvgExportOptions & Renderer Adapters` | `{}` | SVG 文件名、节点/时间线 SVG Renderer 等导出配置 |
2768
+ | `svgExport` | `SvgExportOptions & Renderer Adapters` | `{}` | Deprecated;使用 `config.export` |
1733
2769
  | `iconRegistry` | `IconRegistry` | 默认 Registry | 图标覆盖入口 |
1734
2770
  | `paletteRegistry` | `PaletteRegistry` | 默认 Registry | Palette Provider 入口 |
1735
2771
  | `propertiesRegistry` | `PropertiesRegistry` | 默认 Registry | Properties Provider 入口 |
@@ -1738,15 +2774,27 @@ Controller 不创建 DOM,负责模型、命令、历史、选择与子流程
1738
2774
  | `rendererOptions` | `DiagramRendererOptions` | `{}` | 节点与 Runtime Renderer 配置 |
1739
2775
  | `nodeSubtitleResolver` | `NodeSubtitleResolver` | 无 | 覆盖 Design/Viewer 标准节点副标题;不影响 Instance |
1740
2776
  | `slots` | `StudioShellSlots` | `{}` | 局部 UI 替换 |
1741
- | `regions` | `StudioShellRegions` | 全部 `default` | 原地隐藏 Header、Palette、Properties Statusbar;隐藏后不保留轨道 |
2777
+ | `regions` | `StudioShellRegions` | 按模式决定 | Deprecated;使用 `config.ui.regions`;Instance 默认隐藏右侧 |
1742
2778
  | `layout` | `Function` | 默认三栏布局 | 完整布局替换 |
1743
2779
  | `onThemeChange` | `(state) => void` | 无 | 主题模式解析变化回调 |
1744
2780
 
1745
- 实例方法包括 `getMode()`、`getAllowedModes()`、`setMode()`、`setAllowedModes()`、`subscribeMode()`、`validate()`、`subscribeValidation()`、`getRegions()`、`setRegions()`、`setRuntime()`、`setProjection()`、`refreshPresentation()`、`setTheme()`、`setRuntimeAppearance()`、`fitView()`、`zoomBy()`、`exportSvg()`、`openSvgExportPreview()` 和 `destroy()`。`setMode()` 仅在真实成功切换时返回 `true`;Mode Event 的 `source` 为 `toolbar | api | allowed-modes`。
2781
+ 实例方法包括 `getConfig()`、`setConfig()`、`getMode()`、`getAllowedModes()`、`setMode()`、`setAllowedModes()`、`subscribeMode()`、`validate()`、`subscribeValidation()`、`getRegions()`、`setRegions()`、`setRuntime()`、`setProjection()`、`refreshPresentation()`、`setTheme()`、`setRuntimeAppearance()`、`fitView()`、`zoomBy()`、`exportSvg()`、`openSvgExportPreview()` 和 `destroy()`。`setMode()` 仅在真实成功切换时返回 `true`;Mode Event 的 `source` 为 `toolbar | api | allowed-modes`。
2782
+
2783
+ `0.3.5-preview` 新增 `getSidebarState()`、`setSidebarCollapsed(side, collapsed)`、`subscribeSidebarChange()`、`getPanelSelection()`、`subscribePanelSelection()`。侧栏状态与选择快照的类型、返回值和事件见 [API](API.md#侧栏配置状态与选择),完整框架示例见[侧栏指南](CUSTOMIZATION.md#侧栏布局与平滑折叠)。
2784
+
2785
+ | `config.ui` 配置 | 默认值 | 含义 |
2786
+ | --- | --- | --- |
2787
+ | `leftPanel.collapsible` / `rightPanel.collapsible` | `true` | 显示分隔线中点折叠按钮;`false` 时可见面板保持展开 |
2788
+ | `leftPanel.defaultCollapsed` / `rightPanel.defaultCollapsed` | `false` | 宽屏初始化状态;不随 Config 更新重置 |
2789
+ | `rightPanel.layout` | `'flex'` | `'flex'` 由宿主安排内部滚动;`'scroll'` 由 Nova 滚动整块内容 |
2790
+
2791
+ 首次进入 ≤720px 的 Shell 容器时,可折叠面板默认收起且保留按钮,宽窄屏分别记忆状态。折叠动画为 200ms,支持减少动态效果,不卸载 Slot、不执行 Fit。Design 默认左右可见,Viewer 仅右侧可见,Instance 默认两侧隐藏;显式 `regions.right: 'default'` 可在 Instance 开启,`'hidden'` 隐藏整个区域和按钮,不能用右侧 Slot 覆盖。
2792
+
2793
+ `BpmnStudioConfig` 分为四组:`modeling`(Profile 和节点/连线白名单)、`ui`(`controlSize`、Regions、侧栏宽度、折叠与内容布局)、`viewer`(响应式投影、投影选项、Timeline/Details/Trace)和 `export`。`setConfig()` 先完整校验,再原地更新 UI、Viewer 与 Export,不重建 Canvas。`controlSize` 默认 `medium`,接受 `small | medium | large | '24px'…'48px'`;`null`、裸数字、其他单位及范围外值会抛出包含 `config.ui.controlSize` 的错误。
1746
2794
 
1747
2795
  `shell.actions` 是默认 Header 与宿主自定义 Header 共用的稳定 Interface,提供 `undo()`、`redo()`、`beautify()`、`rerouteEdges()`、`fitView()`、`validate()`、`importXml()`、`exportXml()`、`exportSvg()` 和 `openSvgExportPreview()`。它不包含保存、发布、权限或文件上传等宿主业务动作。
1748
2796
 
1749
- `slots.headerStart` 只替换 Nova Brand;`slots.headerActions` 只替换默认“校验 / 导入 / 导出”动作组;`slots.header` 完整替换 Header,且优先级更高。Slot Context 提供 `studio`、`shell`、`canvas`、`actions`、`getState()`、`subscribe()`、`getMode()`、`getAllowedModes()`、`subscribeMode()` 和 `subscribeValidation()`。默认 Header 的最佳视图只保留在底部缩放区。`regions.header = 'hidden'` 的优先级最高。`layout()` 与 `regions` 不能同时使用。
2797
+ `slots.headerStart` 只替换 Nova Brand;`slots.headerActions` 只替换默认“校验 / 导入 / 导出”动作组;`slots.header` 完整替换 Header,且优先级更高。Slot Context 提供 `studio`、`shell`、`canvas`、`actions`、只读 `ui`、`getState()`、`subscribe()`、`getMode()`、`getAllowedModes()`、`subscribeMode()` 和 `subscribeValidation()`。`ui.controlSize` 是语义值,`ui.controlHeight` 是归一化 px 高度。默认 Header 的最佳视图只保留在底部缩放区。`regions.header = 'hidden'` 的优先级最高。`layout()` 与 `regions` 不能同时使用。
1750
2798
 
1751
2799
  `actions.validate()` 先更新 Nova 默认状态栏,再发送一次来源为 `toolbar` 的 Validation Event 并返回 issues;实例 `validate()` 的来源为 `api`。事件 `valid` 只由 error 决定,warning 不默认阻止发布。
1752
2800
 
@@ -1773,6 +2821,8 @@ new BpmnDesigner(options)
1773
2821
 
1774
2822
  ## `BpmnViewer`
1775
2823
 
2824
+ 审批轨迹的完整输入、Vue/React/Core 加载刷新和点击事件示例见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。`runtime` 是顶层实例数据;完整 Studio 的展示配置使用 `config.viewer`,独立 Viewer 保持下表的顶层 Options。指南同时说明自 `0.3.5-preview` 提供的 Adapter Runtime 类型出口与旧版兼容推导。
2825
+
1776
2826
  | 参数 | 类型 | 必填/默认 | 说明 |
1777
2827
  | --- | --- | --- | --- |
1778
2828
  | `container` | `HTMLElement` | 必填 | Viewer 容器 |
@@ -1854,6 +2904,7 @@ new BpmnDesigner(options)
1854
2904
  | --- | --- |
1855
2905
  | `studio` | 外部 Controller;不传时根据 `model/xml/engine` 创建并托管 |
1856
2906
  | `model` / `xml` / `engine` | 初始及后续受控模型输入 |
2907
+ | `config` | `modeling/ui/viewer/export` 结构化静态配置;属性变化原地应用 Shell 配置 |
1857
2908
  | `propertiesProfile` | 创建内部 Controller 时的属性 Profile |
1858
2909
  | `allowedNodeTypes` | 创建内部 Controller 时使用的完整模型节点白名单 |
1859
2910
  | `allowedEdgeTypes` | 创建内部 Controller 时使用的完整模型连线白名单 |
@@ -1861,14 +2912,17 @@ new BpmnDesigner(options)
1861
2912
  | `mode` / `onModeChange` | 受控输入与实际 Mode 事件;Header Render Context 同步提供 `mode` |
1862
2913
  | `nodeSubtitleResolver` | Design/Viewer 标准节点副标题 Resolver |
1863
2914
  | `contextMenuRegistry` | 自定义右键动作 Registry |
1864
- | `regions` | 默认 Shell 区域显隐;属性变化原地调用 `setRegions()` |
1865
- | `headerStart` / `headerActions` / `header` | React Node 或 Render Prop;使用 Portal 保留宿主 Context |
2915
+ | `regions` | Deprecated 兼容别名;使用 `config.ui.regions` |
2916
+ | `headerStart` / `headerActions` / `header` / `right` | React Node 或 Render Prop;使用 Portal 保留宿主 Context;`right` 仅替换侧栏内容 |
1866
2917
  | `onValidation` | Nova 内部校验展示完成后的只读结果事件 |
2918
+ | `onSidebarChange` | 只读侧栏折叠事件,来源 `button \| api \| responsive \| config` |
1867
2919
  | `onChange(model, reason, xml)` | 模型变化 |
1868
2920
  | `onSelectionChange(selection, element)` | 选择变化 |
1869
2921
  | `onScopeChange(activeScopeId, scopePath, state)` | 子流程作用域变化 |
1870
2922
 
1871
- Ref:`studio`、`shell`、`actions`、`mode`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()`、`exportXml()`、`fitView()`、`setTheme()`。
2923
+ Ref:`studio`、`shell`、`actions`、`mode`、`getConfig()`、`setConfig()`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()`、`exportXml()`、`fitView()`、`setTheme()`。Header Render Context 额外提供只读 `ui`。
2924
+
2925
+ Studio Ref 同时提供五个侧栏/面板选择方法:`getSidebarState()`、`setSidebarCollapsed()`、`subscribeSidebarChange()`、`getPanelSelection()`、`subscribePanelSelection()`。Header/Right Render Context 共享响应式 `state`、`mode`、`ui`、`panelSelection`,以及 `studio`、`shell`、`actions`;只读选择使用 `panelSelection`,不将 `state.selection` 误当作轨迹当前选择。
1872
2926
 
1873
2927
  `xml`/`model` 是外部替换输入。回写与最近导出完全相同的 XML 不会重复导入或清空 Undo/Redo;不同 XML 会替换模型和历史。React/Vue 包均重新导出创建外部 Controller 与 Palette、Properties、Context Menu、Icon、Template Registry 所需的公开函数。
1874
2928
 
@@ -1888,11 +2942,13 @@ Props:`model`、`xml`、`engine`、`theme`、`onChange`、`onSelectionChange`
1888
2942
 
1889
2943
  Vue 组件与 React 使用同一底层接口,主要差异为:
1890
2944
 
1891
- - Studio 提供原生 `#header-start`、`#header-actions` 和 `#header` Slot,通过 Teleport 保留宿主 provide/inject、响应式状态和生命周期。
2945
+ - Studio 提供原生 `#header-start`、`#header-actions`、`#header` 和 `#right` Slot,通过 Teleport 保留宿主 provide/inject、响应式状态和生命周期;同名原生 Slot 高于 Core DOM Slot。
1892
2946
  - `slotsConfig` 继续保留,作为高级 Core DOM Slot 入口;完整 Header 高于局部 Header Slots,原生局部 Slot 高于对应 Core Slot。
1893
- - 事件为 `change`、`selection-change`、`scope-change`、`element-click`、`trace-click`、`update:mode`、`mode-change`、`validation`;支持 `v-model:mode`。
2947
+ - 事件为 `change`、`selection-change`、`scope-change`、`element-click`、`trace-click`、`update:mode`、`mode-change`、`validation`、`sidebar-change`;支持 `v-model:mode`。
1894
2948
  - `BpmnDesigner` 发出 `change`、`selection-change`。
1895
2949
  - Header Slot Context 的 `mode` 是响应式实际状态。Ref Expose 使用 `getStudio()`、`getShell()`、`getActions()`、`getMode()`、`setMode()`、`setAllowedModes()`、`validate()`、`refreshPresentation()` 或 `getInstance()`,不直接暴露可替换字段。
2950
+ - Studio Prop 与 `useBpmnStudio()` 支持 `config`;Expose 提供 `getConfig()` / `setConfig()`,Header Slot Context 提供响应式只读 `ui`。
2951
+ - Studio Expose 提供与 React 相同的五个侧栏/面板选择方法;Header/Right Slot Context 共享响应式 `panelSelection`。
1896
2952
 
1897
2953
  Vue 提供 `BpmnStudio`、`BpmnDesigner`、`BpmnViewer`、`BpmnCanvas`、`BpmnPalettePanel`、`BpmnPropertiesPanel` 和 `useBpmnStudio()`。独立 `BpmnPalettePanel` 复用外部 Studio;传入 Registry 时不会隐式注册 Providers,未传 Registry 时才用 Providers 创建默认 Registry。
1898
2954
 
@@ -1916,7 +2972,7 @@ Runtime Details、Timeline 和 Transition Details 组件通过对应 `createReac
1916
2972
 
1917
2973
  <!-- SOURCE: docs/API.md -->
1918
2974
 
1919
- # BPMN Nova v0.3.4 Preview API
2975
+ # BPMN Nova v0.3.5 Preview API
1920
2976
 
1921
2977
  ## Framework Adapter 类型门面
1922
2978
 
@@ -1934,6 +2990,135 @@ import type {
1934
2990
 
1935
2991
  React 使用相同名称,并将入口替换为 `@bpmn-nova/react`。这些是 TypeScript-only export,不会增加 Adapter 的浏览器运行时代码。
1936
2992
 
2993
+ ### Runtime 类型门面
2994
+
2995
+ 自 `0.3.5-preview` 起,Vue/React 根入口新增显式 `export type`:`ProcessInstanceSnapshot`、`ActivityInstance`、`ActivityStatus`、`RuntimeParticipant`、`RuntimeApprovalAction`、`RuntimeApprovalActionType`、`RuntimeApprovalContent`、`RuntimeApprovalContentBlock`、`RuntimeAssetRef`、`RuntimeTransition`、`RuntimeEdgeVisit`、`RuntimeTraceClickEvent`、`RuntimeAssetResolver`、`RuntimeAssetPurpose`。它们复用现有声明,不是新增 JavaScript API;Studio 原有出口保持不变。
2996
+
2997
+ 真实数据转换、ID 关联、完整场景与已发布版本的 Props 类型推导见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。
2998
+
2999
+ ## Studio Config Interface
3000
+
3001
+ ```ts
3002
+ type StudioControlSize = 'small' | 'medium' | 'large' | `${number}px`
3003
+
3004
+ interface StudioSidebarConfig {
3005
+ collapsible?: boolean
3006
+ defaultCollapsed?: boolean
3007
+ }
3008
+
3009
+ interface StudioRightPanelConfig extends StudioSidebarConfig {
3010
+ layout?: 'flex' | 'scroll'
3011
+ }
3012
+
3013
+ interface StudioUiConfig {
3014
+ controlSize?: StudioControlSize
3015
+ regions?: StudioShellRegions
3016
+ sidebarWidth?: { left?: number; right?: number }
3017
+ leftPanel?: StudioSidebarConfig
3018
+ rightPanel?: StudioRightPanelConfig
3019
+ }
3020
+
3021
+ interface BpmnStudioConfig {
3022
+ modeling?: {
3023
+ propertiesProfile?: 'business' | 'developer'
3024
+ allowedNodeTypes?: readonly NodeType[]
3025
+ allowedEdgeTypes?: readonly EdgeType[]
3026
+ }
3027
+ ui?: StudioUiConfig
3028
+ viewer?: {
3029
+ responsive?: boolean
3030
+ projectionOptions?: readonly { value: ViewerProjection; label: string }[]
3031
+ timeline?: ViewerTimelineOptions
3032
+ runtimeDetails?: RuntimeDetailsOptions
3033
+ runtimeTraceOptions?: RuntimeTraceProjectionOptions
3034
+ }
3035
+ export?: StudioSvgExportOptions
3036
+ }
3037
+
3038
+ interface StudioUiContext {
3039
+ readonly controlSize: StudioControlSize
3040
+ readonly controlHeight: `${number}px`
3041
+ }
3042
+ ```
3043
+
3044
+ 同一个 `config` 可同时传给 `createStudioController()` 和 `createStudioShell()`;React/Vue `BpmnStudio` 与 `useBpmnStudio()` 也接受它。`config.modeling` 在 Controller 创建时建立 Profile 与建模不变量;Shell 的 `setConfig()` 只原地更新 UI、Viewer 和 Export,不改节点/连线白名单、不重建 Canvas。
3045
+
3046
+ ```js
3047
+ const next = shell.setConfig({
3048
+ ui: { controlSize: '36px', regions: { right: 'hidden' } },
3049
+ viewer: { responsive: true },
3050
+ export: { padding: 36 },
3051
+ })
3052
+
3053
+ console.log(shell.getConfig(), shell.ui.controlHeight)
3054
+ ```
3055
+
3056
+ `controlSize` 默认 `medium`。预设普通按钮高度是 `small=28px`、`medium=32px`、`large=40px`;自定义值严格接受 24px–48px。非法配置在任何 DOM 写入前抛错,错误包含 `config.ui.controlSize`。尺寸只影响 Header。Core Slot、Vue Slot 和 React Render Context 都提供只读 `ui`;自定义 Header 还可以继承 `--nova-control-height`、`--nova-header-height`、`--nova-control-font-size`、`--nova-control-padding-inline`、`--nova-control-icon-size` 与 `--nova-control-radius`。
3057
+
3058
+ 旧顶层字段继续兼容并标记 Deprecated:
3059
+
3060
+ | 旧字段 | Config 路径 |
3061
+ | --- | --- |
3062
+ | `propertiesProfile` / `allowedNodeTypes` / `allowedEdgeTypes` | `config.modeling.*` |
3063
+ | `regions` / `leftWidth` / `rightWidth` | `config.ui.regions` / `config.ui.sidebarWidth.*` |
3064
+ | `responsive` / `projectionOptions` / `timeline` / `runtimeDetails` / `runtimeTraceOptions` | `config.viewer.*` |
3065
+ | `svgExport` | `config.export` |
3066
+
3067
+ 同一项同时出现时,旧顶层显式值优先,以保证现有接入无行为变化。
3068
+
3069
+ ### 侧栏配置、状态与选择
3070
+
3071
+ 自 `0.3.5-preview` 起,新增侧栏类型和接口从 Studio/Vue/React 根声明入口提供,不要求增加其他直接依赖。`config.ui.leftPanel/rightPanel.collapsible` 默认 `true`;`defaultCollapsed` 只用于宽屏初始化,默认 `false`。`rightPanel.layout` 默认 `flex`,宿主控制内部滚动;`scroll` 由 Nova 负责整块内容滚动。完整示例见[侧栏布局与平滑折叠](CUSTOMIZATION.md#侧栏布局与平滑折叠)。
3072
+
3073
+ ```ts
3074
+ type StudioSidebarSide = 'left' | 'right'
3075
+ type StudioSidebarChangeSource = 'button' | 'api' | 'responsive' | 'config'
3076
+
3077
+ interface StudioSidebarChangeEvent {
3078
+ readonly side: StudioSidebarSide
3079
+ readonly collapsed: boolean
3080
+ readonly previousCollapsed: boolean
3081
+ readonly mode: StudioMode
3082
+ readonly source: StudioSidebarChangeSource
3083
+ }
3084
+ ```
3085
+
3086
+ | Shell 方法 | 契约 |
3087
+ | --- | --- |
3088
+ | `getSidebarState()` | 返回只读左右状态;每侧包含 `collapsed`、`hidden`、`collapsible` |
3089
+ | `setSidebarCollapsed(side, collapsed): boolean` | 仅实际切换返回 `true`;隐藏、禁用折叠、销毁或重复设置返回 `false` |
3090
+ | `subscribeSidebarChange(listener)` | 订阅只读状态变化;返回取消订阅函数,不按动画帧发送 |
3091
+ | `getPanelSelection()` | 返回 `{ selection, selectedElement, trace }`,只读空选择的三个字段均为 `null` |
3092
+ | `subscribePanelSelection(listener)` | 接收面板选择快照;返回取消订阅函数 |
3093
+
3094
+ Design 的面板选择沿用 Controller 选择语义(包括默认流程选择),Viewer/Instance 来自实际节点/轨迹点击,不改变设计选择。Model/Runtime 更新后重新解析;模式切换成功后才更新。Core Slot 只挂载一次,通过订阅响应变化;Vue/React Header 与 Right Context 直接提供响应式 `panelSelection`。
3095
+
3096
+ 框架 Studio Expose/Handle 透传上述方法;侧栏事件分别为 Vue `sidebar-change`、React `onSidebarChange`。新 Vue `#right` 与 React `right` 优先于对应 Core DOM Slot,只替换右侧内容,不重建 Canvas。
3097
+
3098
+ 默认 Design 显示左右,Viewer 仅显示右侧,Instance 两侧隐藏。未配置 `regions.right` 时采用模式默认;显式 `'default'` 可在 Instance 开启右侧,`'hidden'` 隐藏内容及按钮。`getConfig()` 保留未配置与显式配置的区别,`setConfig(getConfig())` 不改变这一意图。隐藏与收起独立,Slot 不会自动开启隐藏的侧栏。
3099
+
3100
+ Shell 容器宽度 ≤720px 为窄屏,首次进入时可折叠侧栏收起;宽/窄屏分别记忆状态。两侧可同时展开并挤压画布;收起和展开采用 200ms 过渡,尊重减少动态效果,不自动 Fit。`sidebarWidth` 为展开首选宽度,窄空间按比例约束以保留画布。折叠状态不写入 Model、XML、History 或持久化存储。自定义完整 `layout()` 和独立 Designer/Viewer 不使用这套默认侧栏。
3101
+
3102
+ ## 节点 DI 与视觉尺寸
3103
+
3104
+ `createNode()` / Palette 使用类型定义的默认宽高;加载已有 XML(包括组件接收服务端返回的 `xml`)使用 BPMN DI Bounds。XML 导出已写入节点的 `x/y/width/height`,并沿用既有取整与 waypoint 序列化规则;小尺寸节点的外壳溢出不是“保存遗漏宽高”。
3105
+
3106
+ 以下标准外壳的适配自 `0.3.5-preview` 提供,不新增公共 Interface 或尺寸配置:
3107
+
3108
+ | 标准图元 | 默认节点容器 | 内部图形基准 | 显示缩放比例 |
3109
+ | --- | --- | --- | --- |
3110
+ | 排他、并行、包容、事件、复杂网关 | 72×72 | 49×49,旋转 45° | `min(1, width / (49√2), height / (49√2))` |
3111
+ | 数据对象 | 72×88 | 50×62 | `min(1, width / 50, height / 62)` |
3112
+ | 数据存储 | 78×78 | 56×50 | `min(1, width / 56, height / 50)` |
3113
+
3114
+ 图形及其描边、圆角、折角和内部标记等比缩小、不放大,并相对节点容器居中。标签不随图形缩放,水平中心对齐容器,顶部位于容器底部下方 6px。并行网关的角色徽标保持独立。事件沿用已有尺寸约束修复;任务、子流程和泳道不在此次调整范围。
3115
+
3116
+ DOM Canvas/Viewer/Instance 与纯 SVG Export 共享内部 `node-geometry` Module,以实际渲染 Projection Model 为输入。仅复制显示路径并将首末非零线段与标准外轮廓求交;中间折点、方向、连线标签基准及持久化 waypoint 不变。命中区域使用同一显示路径,折点编辑仍操作原始数据;已贴边目标的箭头尖端与参考点重合。已有连接手柄按可见轮廓定位,不改变连接规则。
3117
+
3118
+ 非有限或非正尺寸不产生标准外壳几何;无合法交点、退化路径或 SVG 自定义 Renderer 成功接管的节点不做标准端点校正。SVG Renderer 每个节点只调用一次;返回 `false` 或抛错时继续原有标准降级/警告规则。该 Module 不作为公共 npm 包或 API 导出。宿主无需扩大 DI、删除重建节点、覆盖内部 CSS 或为此迁移服务端 XML。
3119
+
3120
+ 这里的不变性针对重绘、展示刷新和 SVG 导出,不是对所有编辑操作的无损承诺:现有 Controller `updateNode()` 会清空该节点相邻连线的 waypoint(仅改名也会触发),随后沿用 Core 自动路由。本轮没有改变该编辑语义;也不承诺 XML 的字节级往返一致性。
3121
+
1937
3122
  ## Designer
1938
3123
 
1939
3124
  ```js
@@ -2126,12 +3311,12 @@ interface StudioShellRegions {
2126
3311
  const shell = createStudioShell({
2127
3312
  container,
2128
3313
  studio,
2129
- regions: { right: 'hidden' },
3314
+ config: { ui: { regions: { right: 'hidden' } } },
2130
3315
  slots: {
2131
- headerStart({ container, studio, shell, canvas, actions, getState, subscribe, getMode, getAllowedModes, subscribeMode, subscribeValidation }) {
3316
+ headerStart({ container, studio, shell, canvas, actions, getState, subscribe, getMode, getAllowedModes, subscribeMode, subscribeValidation, ui }) {
2132
3317
  // 只替换 Brand;默认模式区和编辑工具继续保留。
2133
3318
  },
2134
- headerActions({ container, actions, getMode, subscribeMode }) {
3319
+ headerActions({ container, actions, getMode, subscribeMode, ui }) {
2135
3320
  // 替换默认“校验 / 导入 / 导出”,挂载宿主“校验 / 保存 / 发布”。
2136
3321
  // 校验调用 actions.validate();保存与发布调用宿主服务。
2137
3322
  },
@@ -2142,7 +3327,7 @@ shell.getRegions()
2142
3327
  shell.setRegions({ right: 'default' })
2143
3328
  ```
2144
3329
 
2145
- `slots.headerActions` 只替换 Header 最右侧默认动作组,可与 `slots.headerStart` 同时使用;`slots.header` 完整替换 Header,并高于两个局部 Slot;`regions.header = 'hidden'` 的优先级最高。未提供 `headerActions` 时仍显示“校验 / 导入 / 导出”。顶部不再重复渲染最佳视图,底部缩放区与 `fitView()` Interface 保持不变。`layout()` 与 `regions` 互斥。
3330
+ `slots.headerActions` 只替换 Header 最右侧默认动作组,可与 `slots.headerStart` 同时使用;`slots.header` 完整替换 Header,并高于两个局部 Slot;`regions.header = 'hidden'` 的优先级最高。Slot Context 的 `ui` 与 Shell 实际 Config 同步。未提供 `headerActions` 时仍显示“校验 / 导入 / 导出”。顶部不再重复渲染最佳视图,底部缩放区与 `fitView()` Interface 保持不变。`layout()` 与 `regions` 互斥。
2146
3331
 
2147
3332
  ```ts
2148
3333
  interface StudioShellActions {
@@ -2163,6 +3348,8 @@ interface StudioShellActions {
2163
3348
 
2164
3349
  Vue `BpmnStudio` 提供 `#header-start` / `#header-actions` / `#header` 与 `getActions()`;React 提供 `headerStart` / `headerActions` / `header` Render Prop 与 Ref `actions`。Core Slot 可通过 `getMode()` / `subscribeMode()` / `subscribeValidation()` 观察 Shell,框架 Slot/Render Context 直接提供响应式实际 `mode`。框架插槽的 `state` 会随 Controller 事件更新,Undo/Redo 禁用状态可以直接绑定 `state.canUndo/canRedo`。
2165
3350
 
3351
+ 右侧内容使用 Core `slots.right`、Vue `#right` 或 React `right`。所有 Header/Right Context 共用 `mode`、`state`、`ui`、`panelSelection` 及 Actions/Controller/Shell;右侧的布局和展开/收起由默认 Shell 管理,见上面的侧栏 Interface。
3352
+
2166
3353
  ## Group 与 Pool / Lane
2167
3354
 
2168
3355
  `Group` 是不持有成员引用的 BPMN 视觉 Artifact;`Lane` 通过 `containerId` 表示画布上的 Participant containment,并继续使用标准 `flowNodeRefs` 表示 BPMN 成员。`containerId` 不会写入 XML,导入时会根据 Participant 的 `processRef` 与 BPMN DI Bounds 恢复。
@@ -2400,6 +3587,8 @@ Viewer 与审批轨迹模式可在空白画布上直接按住左键拖动视口
2400
3587
 
2401
3588
  ### Runtime presentation
2402
3589
 
3590
+ 引擎映射和组件接入步骤集中在[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。特别注意当前 `invalidatedActivityIds` 表示 BPMN 节点 ID,而 `sourceActivityId` 表示工作项实例 ID;两者不能互换。
3591
+
2403
3592
  Runtime snapshots are engine-neutral. `visitId` separates repeated arrivals at the same BPMN element; records sharing a visit describe one approval round, while `multiInstanceId` and an explicit `approvalMode` describe multi-person approval. Multiple people are never assumed to be countersign unless the snapshot declares `approvalMode: 'all'`.
2404
3593
 
2405
3594
  ```js
@@ -2621,12 +3810,21 @@ Stage 0 将编辑器拆成无 UI 的 `BpmnStudioController`,以及可组合的
2621
3810
  ## 默认组合
2622
3811
 
2623
3812
  ```js
3813
+ const config = {
3814
+ modeling: { propertiesProfile: 'business' },
3815
+ ui: { controlSize: 'medium' },
3816
+ };
3817
+
2624
3818
  const studio = createStudioController({
2625
3819
  model: createSampleProcess('flowable'),
2626
- propertiesProfile: 'business',
3820
+ config,
2627
3821
  });
2628
3822
 
2629
- const shell = createStudioShell({ container, studio });
3823
+ const shell = createStudioShell({
3824
+ container,
3825
+ studio,
3826
+ config,
3827
+ });
2630
3828
  ```
2631
3829
 
2632
3830
  NPM 接入只需要加载 Studio 的自包含样式入口。它已经按 Theme、Icons、Renderer、Palette、Properties Renderer、Studio 的顺序打包,避免宿主重复引入内部 CSS。
@@ -2709,20 +3907,20 @@ properties.registerProvider(850, {
2709
3907
 
2710
3908
  ## 组合 Shell 区域与宿主界面
2711
3909
 
2712
- 默认 Shell 包含 Header、Palette、Canvas、Properties 和 Statusbar。仅需要移除某个默认区域时使用 `regions`,不需要重写布局:
3910
+ 默认 Shell 包含 Header、Palette、Canvas、Properties 和 Statusbar。仅需要移除某个默认区域时使用 `config.ui.regions`,不需要重写布局:
2713
3911
 
2714
3912
  ```js
2715
3913
  const shell = createStudioShell({
2716
3914
  container,
2717
3915
  studio,
2718
- regions: { right: 'hidden' },
3916
+ config: { ui: { regions: { right: 'hidden' } } },
2719
3917
  })
2720
3918
 
2721
3919
  // 原地切换,不重建 Controller、Canvas、历史或视口。
2722
3920
  shell.setRegions({ right: 'default' })
2723
3921
  ```
2724
3922
 
2725
- `header`、`left`、`right`、`footer` 均接受 `default | hidden`。隐藏区域不占 Grid 轨道、不显示边框,也不保留可聚焦控件。Viewer/Instance 的只读布局仍会隐藏 Palette;区域配置只能进一步隐藏。
3923
+ `header`、`left`、`right`、`footer` 均接受 `default | hidden`。隐藏区域不占 Grid 轨道、不显示边框或折叠按钮,也不保留可聚焦控件。左侧只属于 Design;右侧在 Design/Viewer 默认显示,在 Instance 默认隐藏。显式设置 `regions.right: 'default'` 可在 Instance 开启右侧;仅提供右侧 Slot 不会覆盖隐藏规则。
2726
3924
 
2727
3925
  局部内容替换使用 `slots.left`、`slots.right`、`slots.headerStart`、`slots.headerActions`、`slots.header` 和 `slots.footer`。`headerStart` 只替换默认 Brand;`headerActions` 只替换最右侧默认“校验 / 导入 / 导出”动作组;`header` 替换完整 Header,优先级高于两个局部 Slot。回调收到 `studio`、`shell`、`actions`、`getState()`、`subscribe()`、注册表、交互控制器和 Canvas。
2728
3926
 
@@ -2734,8 +3932,9 @@ createStudioShell({
2734
3932
  headerStart({ container, actions, getState, subscribe }) {
2735
3933
  // 挂载返回入口、业务图标、流程名称和类型;返回卸载函数。
2736
3934
  },
2737
- headerActions({ container, actions, getMode, subscribeMode }) {
3935
+ headerActions({ container, actions, getMode, subscribeMode, ui }) {
2738
3936
  // 挂载校验 / 保存 / 发布;校验调用 actions.validate(),其余调用宿主服务。
3937
+ // ui.controlSize / ui.controlHeight 始终反映 Shell 的实际配置。
2739
3938
  },
2740
3939
  left({ container, studio, interactions, canvas }) {
2741
3940
  // 挂载任意 Vanilla / React / Vue UI,返回卸载函数。
@@ -2759,11 +3958,226 @@ async function publishProcess(actions) {
2759
3958
  }
2760
3959
  ```
2761
3960
 
2762
- React 使用 `headerStart` / `headerActions` / `header` Render Prop 和 Portal;Vue 使用 `#header-start` / `#header-actions` / `#header` Named Slot 和 Teleport。两者都在宿主应用树内渲染,不创建独立 React Root 或 Vue App,因此 Context、provide/inject、响应式状态和生命周期继续有效。Vue 原有 `slotsConfig`、React 原有 `slots` 仍作为 Core DOM 接口保留。
3961
+ React 使用 `headerStart` / `headerActions` / `header` / `right` Render Prop 和 Portal;Vue 使用 `#header-start` / `#header-actions` / `#header` / `#right` Named Slot 和 Teleport。两者都在宿主应用树内渲染,不创建独立 React Root 或 Vue App,因此 Context、provide/inject、响应式状态和生命周期继续有效。Vue 原有 `slotsConfig`、React 原有 `slots` 仍作为 Core DOM 接口保留;同名框架原生 Slot 优先。右侧原生 Slot 与下述侧栏能力自 `0.3.5-preview` 提供。
3962
+
3963
+ ### 侧栏布局与平滑折叠
3964
+
3965
+ Nova 管理侧栏宽度、折叠按钮和可用高度,宿主只填入内容。不要先配置 `right: 'hidden'` 再传右侧 Slot:隐藏的是整个区域;Slot 替换的只是内容。
3966
+
3967
+ ```js
3968
+ const config = {
3969
+ ui: {
3970
+ sidebarWidth: { left: 244, right: 360 },
3971
+ leftPanel: { collapsible: true, defaultCollapsed: false },
3972
+ rightPanel: { collapsible: true, defaultCollapsed: false, layout: 'flex' },
3973
+ // regions: { right: 'default' }, // 仅在 Instance 也需要右侧时显式开启
3974
+ },
3975
+ }
3976
+ ```
3977
+
3978
+ | 状态 | 侧栏占用宽度 | 分隔线中点按钮 |
3979
+ | --- | --- | --- |
3980
+ | 展开 | 是 | 收起按钮;`collapsible: false` 时不显示 |
3981
+ | 收起 | 否 | 展开按钮仍保留在对应边缘 |
3982
+ | 隐藏 | 否 | 不显示,Slot 也不可见 |
3983
+
3984
+ - `collapsible` 默认 `true`;`defaultCollapsed` 只设置宽屏初始化状态,默认 `false`。后续更新 Config 不重置用户操作;运行时设为 `collapsible: false` 会展开面板并移除按钮。
3985
+ - 左右独立;宽屏与窄屏分别记忆当前 Shell 生命周期内的折叠状态,不存入模型或本地存储。窄屏以 **Shell 容器宽度 ≤720px** 判断,首次进入时可折叠侧栏默认收起,保留展开按钮。
3986
+ - 窄屏展开仍挤压画布,不使用抽屉、遮罩或焦点陷阱,允许两侧同时展开。空间不足按首选宽度比例缩小侧栏,为画布保留至少 `min(96px, 容器宽度)`;空间恢复后还原首选宽度。
3987
+ - 折叠宽度采用 `200ms cubic-bezier(0.2, 0, 0, 1)`,快速反向操作从当前位置继续;初始化无动画,系统启用减少动态效果时关闭动画。按钮支持键盘和 `aria-expanded`,收起内容不可聚焦。
3988
+ - 收起或隐藏不卸载 Slot、不丢失表单状态与滚动位置、不重建 Canvas,也不调用 Fit 或改变 Zoom/Pan。Viewer 自动投影的尺寸响应在动画结束后按最终宽度合并处理;跨投影阈值时沿用原投影切换行为。
3989
+ - 完整自定义 `layout()` 不接管上述默认侧栏布局;独立 Designer/Viewer 没有 Shell 侧栏配置。
3990
+
3991
+ ### Flex 或 Scroll:谁负责滚动
3992
+
3993
+ 右侧 `layout: 'flex'` 为默认值:Nova 提供满宽高纵向 Flex 内容容器,限制溢出并设置 `min-width/min-height: 0`。宿主自行安排固定标题、弹性滚动内容和固定底部;不会给每个子元素强加 `flex: 1`。内置面板使用固定标题、中间内容滚动。
3994
+
3995
+ 使用自己的类名即可,不覆盖 Nova 内部选择器:
3996
+
3997
+ ```css
3998
+ .host-panel {
3999
+ display: flex;
4000
+ flex-direction: column;
4001
+ flex: 1;
4002
+ min-width: 0;
4003
+ min-height: 0;
4004
+ overflow: hidden;
4005
+ }
4006
+ .host-panel > header,
4007
+ .host-panel > footer { flex: 0 0 auto; padding: 12px; }
4008
+ .host-panel-content { flex: 1; min-height: 0; overflow: auto; padding: 12px; }
4009
+ ```
4010
+
4011
+ 如果宿主希望只填入普通内容,设置 `layout: 'scroll'`。Nova 会让整个右侧内容纵向滚动,标题也随内容滚动;此时不要再给内容包一层固定高度或滚动容器。Studio 的宿主仍须提供可计算高度,如 `height: 720px`,或一条完整、可计算的父级 Flex 高度链;仅设置一个没有高度基准的 `height: 100%` 不够。
4012
+
4013
+ ### Vue:原生右侧 Slot
4014
+
4015
+ ```vue
4016
+ <script setup>
4017
+ import { ref } from 'vue'
4018
+ import { BpmnStudio } from '@bpmn-nova/vue'
4019
+ import '@bpmn-nova/vue/styles.css'
4020
+
4021
+ const props = defineProps({ xml: String })
4022
+ const studioRef = ref(null)
4023
+ const note = ref('')
4024
+ const config = {
4025
+ ui: {
4026
+ leftPanel: { collapsible: true },
4027
+ rightPanel: { layout: 'flex', collapsible: true },
4028
+ },
4029
+ }
4030
+ function observeSidebar(event) { console.log(event.side, event.collapsed, event.source) }
4031
+ </script>
4032
+
4033
+ <template>
4034
+ <div style="height: 720px">
4035
+ <BpmnStudio ref="studioRef" :xml="props.xml" :config="config" @sidebar-change="observeSidebar">
4036
+ <template #right="{ panelSelection, mode }">
4037
+ <section class="host-panel">
4038
+ <header>业务详情 · {{ mode }}</header>
4039
+ <div class="host-panel-content">
4040
+ <p>{{ panelSelection.selectedElement?.id ?? '未选择元素' }}</p>
4041
+ <label>宿主备注 <textarea v-model="note" /></label>
4042
+ </div>
4043
+ <footer><button type="button" @click="studioRef?.setSidebarCollapsed('right', true)">收起</button></footer>
4044
+ </section>
4045
+ </template>
4046
+ </BpmnStudio>
4047
+ </div>
4048
+ </template>
4049
+ ```
4050
+
4051
+ 示例使用前面的 `.host-panel` 样式。`note` 是宿主业务状态,不会因收起而销毁,不会自动写入 BPMN。
4052
+
4053
+ ### React:原生右侧 Render Prop
4054
+
4055
+ ```jsx
4056
+ import { useRef, useState } from 'react'
4057
+ import { BpmnStudio } from '@bpmn-nova/react'
4058
+ import '@bpmn-nova/react/styles.css'
4059
+
4060
+ const config = { ui: { rightPanel: { layout: 'flex', collapsible: true } } }
4061
+
4062
+ export function WorkflowEditor({ xml }) {
4063
+ const studioRef = useRef(null)
4064
+ const [note, setNote] = useState('')
4065
+ return (
4066
+ <div style={{ height: 720 }}>
4067
+ <BpmnStudio
4068
+ ref={studioRef}
4069
+ xml={xml}
4070
+ config={config}
4071
+ onSidebarChange={(event) => console.log(event.side, event.collapsed, event.source)}
4072
+ right={({ panelSelection, mode }) => (
4073
+ <section className="host-panel">
4074
+ <header>业务详情 · {mode}</header>
4075
+ <div className="host-panel-content">
4076
+ <p>{panelSelection.selectedElement?.id ?? '未选择元素'}</p>
4077
+ <label>宿主备注 <textarea value={note} onChange={(event) => setNote(event.target.value)} /></label>
4078
+ </div>
4079
+ <footer><button type="button" onClick={() => studioRef.current?.setSidebarCollapsed('right', true)}>收起</button></footer>
4080
+ </section>
4081
+ )}
4082
+ />
4083
+ </div>
4084
+ )
4085
+ }
4086
+ ```
4087
+
4088
+ ### Core:内容与选择订阅
4089
+
4090
+ ```js
4091
+ import { createStudioController, createStudioShell, importBpmn } from '@bpmn-nova/studio'
4092
+ import '@bpmn-nova/studio/styles.css'
4093
+
4094
+ // container 已有可计算高度;xml 为宿主加载的流程定义。
4095
+ const studio = createStudioController({ model: importBpmn(xml, 'flowable') })
4096
+ const shell = createStudioShell({
4097
+ container,
4098
+ studio,
4099
+ config: { ui: { rightPanel: { layout: 'scroll' } } },
4100
+ slots: {
4101
+ right({ container: content, shell }) {
4102
+ const heading = document.createElement('h2')
4103
+ const summary = document.createElement('p')
4104
+ heading.textContent = '业务详情'
4105
+ content.append(heading, summary)
4106
+ const render = ({ selectedElement }) => {
4107
+ summary.textContent = selectedElement?.id ?? '未选择元素'
4108
+ }
4109
+ render(shell.getPanelSelection())
4110
+ const unsubscribe = shell.subscribePanelSelection(render)
4111
+ return () => { unsubscribe(); heading.remove(); summary.remove() }
4112
+ },
4113
+ },
4114
+ })
4115
+
4116
+ const offSidebar = shell.subscribeSidebarChange((event) => {
4117
+ console.log(event.side, event.previousCollapsed, event.collapsed, event.source)
4118
+ })
4119
+ shell.setSidebarCollapsed('left', true)
4120
+ console.log(shell.getSidebarState().left)
4121
+
4122
+ // 宿主卸载时执行:offSidebar(); shell.destroy(); studio.destroy()
4123
+ ```
4124
+
4125
+ ### 折叠状态与面板选择契约
4126
+
4127
+ `getSidebarState()` 返回左右两侧各自的 `collapsed`、`hidden`、`collapsible`。`setSidebarCollapsed(side, collapsed)` 仅在实际成功改变时返回 `true`;隐藏、禁用折叠、销毁或重复设置返回 `false`。`subscribeSidebarChange()` 返回取消订阅函数,事件为只读 `{ side, collapsed, previousCollapsed, mode, source }`;`source` 为 `button | api | responsive | config`,不会按动画帧重复发送。Vue Expose/React Handle 透传三个方法,组件事件分别为 `sidebar-change`/`onSidebarChange`。
4128
+
4129
+ 所有框架 Header/Right Context 共用响应式 `mode`、`state`、`ui`、`panelSelection`,以及 `studio`、`shell`、`actions`。`panelSelection` 为 `{ selection, selectedElement, trace }`:Design 从 Controller 选择派生;Viewer/Instance 从实际点击派生,包含适用的轨迹上下文,不回写设计选择。空白点击清空;Model/Runtime 更新后重新解析,避免展示旧实例事实。Core 通过 `getPanelSelection()` / `subscribePanelSelection()` 使用相同状态。
4130
+
4131
+ `state.selection` 是设计选择,不应作为只读轨迹当前选择的替代。业务字段仍以稳定 BPMN ID 关联;需要审批动作、附件或跳转事实时读取 `trace` 或既有 `trace-click`,而不是根据属性面板可见性判断。隐藏右侧不关闭轨迹详情、点击事件或附件能力。
4132
+
4133
+ ### Header 控件尺寸与宿主 UI 库
4134
+
4135
+ 通过 `config.ui.controlSize` 统一调整顶部 Header 的 Mode、Projection、撤销/重做、美化、默认动作组和自定义 Header 容器。默认值是 `medium`;预设与常见宿主组件库的语义映射如下:
4136
+
4137
+ | Nova | 普通控件高度 | Element Plus | Ant Design |
4138
+ | --- | ---: | --- | --- |
4139
+ | `small` | 28px | `small` | `small` |
4140
+ | `medium` | 32px | `default` | `middle` |
4141
+ | `large` | 40px | `large` | `large` |
4142
+
4143
+ 宿主设计系统不落在预设上时,可传严格的 `24px`–`48px` CSS 像素字符串:
4144
+
4145
+ ```js
4146
+ const config = {
4147
+ ui: {
4148
+ controlSize: '36px',
4149
+ regions: { right: 'hidden' },
4150
+ sidebarWidth: { left: 256, right: 384 },
4151
+ },
4152
+ }
4153
+
4154
+ const shell = createStudioShell({ container, studio, config })
4155
+ shell.setConfig({ ...shell.getConfig(), ui: { ...shell.getConfig().ui, controlSize: 'large' } })
4156
+ ```
4157
+
4158
+ `setConfig()` 先完整校验再更新现有 Shell,不重建 Canvas,也不改变 Model、XML、History、Selection、Mode 或 Viewport。传入 `null`、裸数字、`rem`、任意 CSS 表达式或范围外值会抛出包含 `config.ui.controlSize` 的错误。`config.modeling.allowedNodeTypes` 和 `allowedEdgeTypes` 是 Controller 初始化不变量;运行中只有 Profile 继续通过 `setPropertiesProfile()` 修改。
4159
+
4160
+ 自定义 Header Actions 可通过 Context 的只读 `ui.controlSize` / `ui.controlHeight` 映射宿主组件尺寸,并继承以下 CSS Variables:
4161
+
4162
+ ```css
4163
+ .host-studio-actions .host-button {
4164
+ min-height: var(--nova-control-height);
4165
+ padding-inline: var(--nova-control-padding-inline);
4166
+ border-radius: var(--nova-control-radius);
4167
+ font-size: var(--nova-control-font-size);
4168
+ }
4169
+
4170
+ .host-studio-actions .host-button svg {
4171
+ width: var(--nova-control-icon-size);
4172
+ height: var(--nova-control-icon-size);
4173
+ }
4174
+ ```
4175
+
4176
+ 还可读取 `--nova-header-height`。这些变量只控制 Header;底部缩放区、画布浮动工具、Palette 和 Properties 保持自己的尺寸。旧顶层 `propertiesProfile`、`allowedNodeTypes`、`allowedEdgeTypes`、`regions`、`leftWidth`、`rightWidth`、`responsive`、`projectionOptions`、`timeline`、`runtimeDetails`、`runtimeTraceOptions` 与 `svgExport` 仍作为 Deprecated 兼容别名;新旧同时出现时,旧顶层显式值优先。
2763
4177
 
2764
4178
  若整个三栏结构都要自定义,使用 `layout({ container, mount, ...services })`。`mount.canvas()`、`mount.palette()`、`mount.properties()` 可分别挂载官方实现,也可只使用 Controller 自行实现。`layout()` 与 `regions` 是互斥 Interface,同时传入会抛出配置错误。
2765
4179
 
2766
- 宿主复用自己的业务属性面板时,把它放在 Nova 根节点外部的兄弟区域,并订阅 Controller 的 `selectionChanged`(框架中为 `selection-change` / `onSelectionChange`)。该事件覆盖节点、连线、多选、清空和键盘选择;`element-click` 只观察点击,不能替代选择状态。业务配置应使用稳定 BPMN Element ID 关联,不写入 Nova UI 状态。
4180
+ 宿主业务属性面板既可通过右侧 Slot 复用 Nova 布局,也可显式隐藏右侧并放在 Nova 根节点外部。外部设计面板订阅 Controller 的 `selectionChanged`(框架中为 `selection-change` / `onSelectionChange`);该事件覆盖节点、连线、多选、清空和键盘选择,`element-click` 不能替代它。跨模式的面板使用上述 `panelSelection`。业务配置以稳定 BPMN Element ID 关联,不写入 Nova UI 状态。
2767
4181
 
2768
4182
  ## 自定义画布右键菜单
2769
4183
 
@@ -2808,6 +4222,12 @@ createStudioShell({
2808
4222
 
2809
4223
  `rendererOptions.nodeRenderers` 按节点类型或节点 kind 注册内部渲染器。外框、端口、选择、连接、拖动和删除仍由 Canvas 管理。
2810
4224
 
4225
+ ### 标准外壳与导入尺寸
4226
+
4227
+ Palette 默认节点尺寸与 XML 中的 DI Bounds 可以不同;服务端 XML 通过组件加载时同样会进入解析路径。导出已保存宽高,不应以扩大模型、重建节点或宿主 CSS 补丁修复显示。自 `0.3.5-preview` 起,五类网关、数据对象和数据存储按实际节点容器等比缩小、居中,并只在显示层将连线端点及已有连接手柄贴合可见外轮廓;模型、标签的逻辑路径基准与原始 waypoint 均不变。完整尺寸规则见 [API:节点 DI 与视觉尺寸](API.md#节点-di-与视觉尺寸)。
4228
+
4229
+ DOM 节点内容 Renderer 仍处于 Nova 管理的标准外壳内,网关自定义内容随外壳一起缩放,角色徽标保持独立。SVG Renderer 则可接管完整节点:每个节点只调用一次,成功接管时不应用标准几何或端点校正;返回 `false` 或执行失败时沿用标准降级及既有警告规则。不要依赖内部 `node-geometry` 路径;不属于标准外轮廓的自定义图形需要由宿主自行保证其视觉与连接契约。
4230
+
2811
4231
  ```js
2812
4232
  createStudioShell({
2813
4233
  container,
@@ -2954,6 +4374,8 @@ createStudioShell({
2954
4374
 
2955
4375
  ## 自定义审批轨迹详情
2956
4376
 
4377
+ 先按[审批轨迹接入指南](RUNTIME-INTEGRATION.md)完成 Runtime 数据关联,再选择详情扩展点。该指南覆盖统一点击事件、内置详情 `autoOpen`、资源 Resolver、会签和回退历史;定义态副标题 Resolver 不用于改写这些运行事实。
4378
+
2957
4379
  默认 Instance Viewer 会把运行记录归一化为 `RuntimePresentation`。自定义节点通过 `runtimePresentation` 读取状态和处理人摘要,通过 `openRuntimeDetails()` 复用默认详情弹层,不需要再次实现会签与多轮审批聚合。
2958
4380
 
2959
4381
  ```js
@@ -3050,7 +4472,7 @@ createStudioShell({
3050
4472
 
3051
4473
  # BPMN Nova NPM 包与发布
3052
4474
 
3053
- 版本:`0.3.4-preview`。BPMN Nova 只发布三个 `@bpmn-nova` 公共包,均使用 ESM、附带 TypeScript 声明、采用 Apache-2.0 License,并通过 `preview` dist-tag 发布。
4475
+ 版本:`0.3.5-preview`。BPMN Nova 只发布三个 `@bpmn-nova` 公共包,均使用 ESM、附带 TypeScript 声明、采用 Apache-2.0 License,并通过 `preview` dist-tag 发布。
3054
4476
 
3055
4477
  ## 公共包
3056
4478
 
@@ -3066,6 +4488,8 @@ Core、Model、Renderer、Runtime、Theme、Properties、Node Presentation 和
3066
4488
 
3067
4489
  React/Vue 的声明入口显式重导出宿主常用的 Core 类型:`BpmnEdge`、`BpmnNode`、`EdgeType`、`ElementSelection`、`EngineId`、`LayoutOptions`、`NodeType` 和 `ProcessModel`。业务项目必须从自己选择的 Adapter 导入这些类型,不应仅为类型在 manifest 中增加 Studio 直依赖。
3068
4490
 
4491
+ `0.3.5-preview` 另补齐 14 个 Runtime 数据、点击事件及资源 Resolver 类型出口;只修改声明,不增加运行时代码或依赖。完整清单、旧版 Props 推导及宿主接入示例见[审批轨迹接入指南](RUNTIME-INTEGRATION.md)。这些新增出口不属于旧 `0.3.4-preview` 包。
4492
+
3069
4493
  ## Studio 子路径
3070
4494
 
3071
4495
  Studio 根入口重新导出稳定的完整能力;以下子路径用于让导入意图更清晰:
@@ -3109,6 +4533,28 @@ node scripts/build-packages.mjs --package vue
3109
4533
 
3110
4534
  `--package` 不接受内部 Module 名称。三个公开包之外的目录没有发布 manifest、NPM README 或独立版本生命周期。
3111
4535
 
4536
+ ## 全局统一版本与全量发布
4537
+
4538
+ 每次发布都是三个公共包的完整发布,不按源码是否改动选择包;未改动的 Adapter 也必须同步升版、构建、验证和发布。此规则不改变宿主只直接安装一个适配包的接入方式。
4539
+
4540
+ 同一轮目标版本 `V` 必须满足:
4541
+
4542
+ ```text
4543
+ 根 package.json.version
4544
+ = @bpmn-nova/studio.version
4545
+ = @bpmn-nova/react.version
4546
+ = @bpmn-nova/vue.version
4547
+ = React 对 Studio 的精确依赖
4548
+ = Vue 对 Studio 的精确依赖
4549
+ = V
4550
+ ```
4551
+
4552
+ 根包为私有包,只同步版本,不发布 npm;源码内部 Module 没有独立发布版本。`--package` 仅用于单包构建或打包过程,不能用来跳过本轮某个公共包的发布。
4553
+
4554
+ 发布前先查询三个包的 Registry 版本,选择统一且未被占用的目标版本,再一起更新上述版本和相关文档,执行 `npm run docs:ai` 生成 AI 文档。本地 `release:check-versions` 检查上述一致性,但不能替代发布后的远端检查。已发布版本不可覆盖;部分成功后的处理见本页“部分发布失败”。
4555
+
4556
+ 只有三个包的精确目标版本均可安装、Adapter 的 Studio 依赖精确一致、三个包的目标 dist-tag 均解析到 `V`,才算本轮发布完成。当前通道为 `preview`,保持 `latest` 不变;不将“最新版”解释为更新 `latest` 标签。
4557
+
3112
4558
  ## 发布前验证
3113
4559
 
3114
4560
  ```bash
@@ -3140,7 +4586,7 @@ git diff --check
3140
4586
 
3141
4587
  ## 发布顺序
3142
4588
 
3143
- 先发布 Studio,确认 Registry 已可解析后,再发布 React 和 Vue:
4589
+ 每轮必须发布三个包。先发布 Studio,确认 Registry 已可解析后,再发布 React 和 Vue:
3144
4590
 
3145
4591
  ```bash
3146
4592
  (cd packages/studio && npm publish --access public --tag preview)
@@ -3150,14 +4596,20 @@ git diff --check
3150
4596
 
3151
4597
  三个 manifest 都设置了 Preview 发布守卫。仍应显式使用 `--tag preview`,避免预览版本意外覆盖稳定版 `latest`。
3152
4598
 
3153
- 发布后核对:
4599
+ 发布后从仓库根目录核对;下面所有版本输出都应等于根版本,两个依赖也应精确等于该值:
3154
4600
 
3155
4601
  ```bash
3156
- npm view @bpmn-nova/studio@0.3.4-preview version
3157
- npm view @bpmn-nova/react@0.3.4-preview version
3158
- npm view @bpmn-nova/vue@0.3.4-preview version
4602
+ release_version=$(node -p "require('./package.json').version")
4603
+ npm view "@bpmn-nova/studio@$release_version" version
4604
+ npm view "@bpmn-nova/react@$release_version" version dependencies --json
4605
+ npm view "@bpmn-nova/vue@$release_version" version dependencies --json
4606
+ npm view @bpmn-nova/studio@preview version
4607
+ npm view @bpmn-nova/react@preview version
4608
+ npm view @bpmn-nova/vue@preview version
3159
4609
  ```
3160
4610
 
4611
+ 同时比较发布前后三个包的 `dist-tags.latest`,并在临时消费工程中安装本轮产物验证入口和类型。尚未通过远端核对时,报告为“部分发布”或“待验证”,不能报告全部发布成功。
4612
+
3161
4613
  ## Registry 清理说明
3162
4614
 
3163
4615
  Registry 清理是不可逆的外部操作,不由构建脚本自动执行。只有在三个 `0.3.4-preview` 新产物发布、安装和子路径验证均成功后,才能在获得单独明确授权的情况下执行:
@@ -3178,14 +4630,15 @@ Registry 清理是不可逆的外部操作,不由构建脚本自动执行。
3178
4630
 
3179
4631
  1. 使用 `npm view` 确认成功发布的包和版本。
3180
4632
  2. 修复身份、网络或顺序问题。
3181
- 3. 如果 Studio 正确而 Adapter 尚未发布,可以继续发布 React/Vue。
3182
- 4. 如果已发布 tarball 内容错误,停止本轮发布,修复后提升 Preview 版本并重新构建三个包。
4633
+ 3. 已成功发布且内容正确的包无需重复发布;继续发布本轮尚未成功的包,直到三个包及其 `preview` 标签均对齐。这是失败恢复,不是按源码改动跳过包。
4634
+ 4. 如果已发布 tarball 内容错误,停止本轮发布,修复后选择新的统一 Preview 版本,重新构建、验证并发布全部三个包,不能只提升一个包的版本。
3183
4635
 
3184
4636
  ## 文档
3185
4637
 
3186
4638
  - [快速开始](GETTING-STARTED.md)
3187
4639
  - [组件参数](COMPONENTS.md)
3188
4640
  - [公开 API](API.md)
4641
+ - [审批轨迹接入指南](RUNTIME-INTEGRATION.md)
3189
4642
  - [AI 接入指南](AI-INTEGRATION.md)
3190
4643
  - [AI 安装执行入口](../llms.txt)
3191
4644