@bpmn-nova/studio 0.3.6-preview → 0.3.7-preview

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计
4
4
 
5
5
  > **English summary:** The complete framework-neutral BPMN Nova package for process design, viewing, approval traces, themes, properties, and pure SVG export.
6
6
 
7
- > 当前版本为 `0.3.6-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
7
+ > 当前版本为 `0.3.7-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
8
8
 
9
9
  ![BPMN Nova 流程设计器](https://raw.githubusercontent.com/daxiangme/bpmn-nova/dev/docs/assets/bpmn-nova-designer.jpg)
10
10
 
@@ -588,6 +588,95 @@ async function resolvedImageData(asset, action, context, warnings, signal) {
588
588
  }
589
589
  }
590
590
 
591
+ const AVATAR_SIZE = 32;
592
+ const AVATAR_OVERLAP = 8;
593
+ const AVATAR_VISIBLE_LIMIT = 2;
594
+ let avatarClipSequence = 0;
595
+
596
+ function timelineAvatarPeople(item) {
597
+ if (!item || item.kind === 'transition' || item.predicted || item.automated) return [];
598
+ return (item.participants || []).filter((participant) => String(participant?.name || '').trim());
599
+ }
600
+
601
+ function participantInitial(participant) {
602
+ return String(participant?.name || '').trim().slice(0, 1) || '?';
603
+ }
604
+
605
+ async function resolvedAvatarData(url, context, warnings, signal, item) {
606
+ const source = String(url || '').trim();
607
+ if (!source) return null;
608
+ const cache = context.avatarCache || (context.avatarCache = new Map());
609
+ if (cache.has(source)) return cache.get(source);
610
+ try {
611
+ signal?.throwIfAborted?.();
612
+ let data = source;
613
+ if (source.startsWith('data:')) {
614
+ if (!source.startsWith('data:image/')) throw new Error('Unexpected Data URL MIME type.');
615
+ } else {
616
+ const ownerWindow = context.document?.defaultView || globalThis;
617
+ const response = await ownerWindow.fetch(source, { signal });
618
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
619
+ const blob = await response.blob();
620
+ if (!blob.type.startsWith('image/')) throw new Error(`Unexpected MIME type: ${blob.type || 'unknown'}`);
621
+ data = await new Promise((resolve, reject) => {
622
+ const reader = new ownerWindow.FileReader();
623
+ reader.addEventListener('load', () => resolve(String(reader.result)), { once: true });
624
+ reader.addEventListener('error', () => reject(reader.error || new Error('Unable to read image.')), { once: true });
625
+ reader.readAsDataURL(blob);
626
+ });
627
+ }
628
+ cache.set(source, data);
629
+ return data;
630
+ } catch (error) {
631
+ cache.delete(source);
632
+ if (signal?.aborted) throw error;
633
+ warnings.push({ code: 'avatar-unavailable', message: '处理人头像无法嵌入 SVG,已显示姓名首字。', elementId: item?.elementId });
634
+ return null;
635
+ }
636
+ }
637
+
638
+ async function renderTimelineAvatars({ document, parent, people, x, y, theme, context, warnings, signal, item }) {
639
+ const visible = people.slice(0, AVATAR_VISIBLE_LIMIT);
640
+ const overflow = people.length - AVATAR_VISIBLE_LIMIT;
641
+ const count = visible.length + (overflow > 0 ? 1 : 0);
642
+ const primary = tone(theme, 'primary');
643
+ for (let index = 0; index < visible.length; index += 1) {
644
+ const left = x + index * (AVATAR_SIZE - AVATAR_OVERLAP);
645
+ const cx = left + AVATAR_SIZE / 2;
646
+ const cy = y + AVATAR_SIZE / 2;
647
+ const person = visible[index];
648
+ const data = await resolvedAvatarData(person.avatarUrl, context, warnings, signal, item);
649
+ parent.appendChild(svgElement(document, 'circle', {
650
+ cx, cy, r: AVATAR_SIZE / 2, fill: theme.colors.primarySoft, stroke: theme.colors.surface, 'stroke-width': 2,
651
+ }));
652
+ if (data) {
653
+ avatarClipSequence += 1;
654
+ const clipId = `nova-timeline-avatar-${avatarClipSequence}`;
655
+ const clip = svgElement(document, 'clipPath', { id: clipId });
656
+ clip.appendChild(svgElement(document, 'circle', { cx, cy, r: AVATAR_SIZE / 2 }));
657
+ parent.appendChild(clip);
658
+ parent.appendChild(svgElement(document, 'image', {
659
+ x: left, y, width: AVATAR_SIZE, height: AVATAR_SIZE, href: data, preserveAspectRatio: 'xMidYMid slice', 'clip-path': `url(#${clipId})`,
660
+ }));
661
+ } else {
662
+ parent.appendChild(svgElement(document, 'text', {
663
+ x: cx, y: y + 21, 'text-anchor': 'middle', fill: primary.foreground, 'font-size': 12, 'font-weight': 600, 'font-family': theme.fontFamily,
664
+ }, participantInitial(person)));
665
+ }
666
+ }
667
+ if (overflow > 0) {
668
+ const left = x + visible.length * (AVATAR_SIZE - AVATAR_OVERLAP);
669
+ const cx = left + AVATAR_SIZE / 2;
670
+ parent.appendChild(svgElement(document, 'circle', {
671
+ cx, cy: y + AVATAR_SIZE / 2, r: AVATAR_SIZE / 2, fill: theme.colors.primarySoft, stroke: theme.colors.surface, 'stroke-width': 2,
672
+ }));
673
+ parent.appendChild(svgElement(document, 'text', {
674
+ x: cx, y: y + 21, 'text-anchor': 'middle', fill: primary.foreground, 'font-size': 10, 'font-weight': 600, 'font-family': theme.fontFamily,
675
+ }, `+${overflow}`));
676
+ }
677
+ return count > 0 ? AVATAR_SIZE + (count - 1) * (AVATAR_SIZE - AVATAR_OVERLAP) : 0;
678
+ }
679
+
591
680
  function timelineSequence(projection) {
592
681
  const groupedIds = new Set((projection.groups || []).flatMap((group) => group.itemIds || []));
593
682
  return [
@@ -670,9 +759,17 @@ async function renderTimelineItem({ document, parent, item, x, y, width, theme,
670
759
  parent.appendChild(svgElement(document, 'circle', { cx: railX, cy: y + 22, r: 4.5, fill: itemTone.strong }));
671
760
  const contentX = x + 46;
672
761
  const contentWidth = width - 46;
673
- parent.appendChild(svgElement(document, 'text', { x: contentX, y: y + 20, fill: theme.colors.text, 'font-size': 15, 'font-weight': 650, 'font-family': theme.fontFamily }, `${item.name}${item.round > 1 ? ` 第 ${item.round} 次` : ''}`));
762
+ const people = timelineAvatarPeople(item);
763
+ let textX = contentX;
764
+ if (people.length) {
765
+ const stackWidth = await renderTimelineAvatars({
766
+ document, parent, people, x: contentX, y: y + 6, theme, context, warnings, signal, item,
767
+ });
768
+ textX = contentX + stackWidth + 8;
769
+ }
770
+ parent.appendChild(svgElement(document, 'text', { x: textX, y: y + 20, fill: theme.colors.text, 'font-size': 15, 'font-weight': 650, 'font-family': theme.fontFamily }, `${item.name}${item.round > 1 ? ` 第 ${item.round} 次` : ''}`));
674
771
  parent.appendChild(svgElement(document, 'text', { x: contentX + contentWidth, y: y + 20, 'text-anchor': 'end', fill: itemTone.foreground, 'font-size': 12, 'font-weight': 600, 'font-family': theme.fontFamily }, resultLabel(item)));
675
- parent.appendChild(svgElement(document, 'text', { x: contentX, y: y + 42, fill: theme.colors.textSecondary, 'font-size': 11.5, 'font-family': theme.fontFamily }, item.summary || ''));
772
+ parent.appendChild(svgElement(document, 'text', { x: textX, y: y + 42, fill: theme.colors.textSecondary, 'font-size': 11.5, 'font-family': theme.fontFamily }, item.summary || ''));
676
773
  parent.appendChild(svgElement(document, 'text', { x: contentX + contentWidth, y: y + 42, 'text-anchor': 'end', fill: theme.colors.textMuted, 'font-size': 11, 'font-family': theme.fontFamily }, formatTime(item.time)));
677
774
  let actionY = y + 56;
678
775
  for (const action of actions) {
@@ -0,0 +1,49 @@
1
+ function element(tag, className, text) {
2
+ const node = document.createElement(tag);
3
+ if (className) node.className = className;
4
+ if (text !== undefined) node.textContent = text;
5
+ return node;
6
+ }
7
+
8
+ function participantInitial(participant) {
9
+ const name = String(participant?.name || '').trim();
10
+ return name.slice(0, 1) || '?';
11
+ }
12
+
13
+ export function renderRuntimeAvatar(participant, className = 'mb-runtime-details-avatar') {
14
+ const initial = participantInitial(participant);
15
+ const avatar = element('span', className, initial);
16
+ avatar.setAttribute('aria-hidden', 'true');
17
+ const url = String(participant?.avatarUrl || '').trim();
18
+ if (!url) return avatar;
19
+ const image = document.createElement('img');
20
+ image.src = url;
21
+ image.alt = '';
22
+ image.addEventListener('error', () => {
23
+ image.remove();
24
+ if (!avatar.textContent) avatar.textContent = initial;
25
+ }, { once: true });
26
+ avatar.textContent = '';
27
+ avatar.appendChild(image);
28
+ return avatar;
29
+ }
30
+
31
+ export function renderRuntimeAvatarStack(participants = [], { visibleLimit = 2 } = {}) {
32
+ const people = (participants || []).filter((item) => String(item?.name || '').trim());
33
+ if (!people.length) return null;
34
+ const stack = element('span', 'mb-runtime-timeline-avatars');
35
+ stack.setAttribute('aria-hidden', 'true');
36
+ people.slice(0, visibleLimit).forEach((participant) => {
37
+ stack.appendChild(renderRuntimeAvatar(participant));
38
+ });
39
+ const overflow = people.length - visibleLimit;
40
+ if (overflow > 0) {
41
+ stack.appendChild(element('span', 'mb-runtime-details-avatar mb-runtime-timeline-avatar-more', `+${overflow}`));
42
+ }
43
+ return stack;
44
+ }
45
+
46
+ export function shouldRenderTimelineAvatars(item) {
47
+ if (!item || item.predicted || item.automated) return false;
48
+ return (item.participants || []).some((participant) => String(participant?.name || '').trim());
49
+ }
@@ -1,6 +1,7 @@
1
1
  import { createDefaultIconRegistry, createIconElement } from '../icons/index.js';
2
2
  import { formatRuntimeInstant } from '../runtime/index.js';
3
3
  import { renderRuntimeApprovalContent } from './runtime-content.js';
4
+ import { renderRuntimeAvatarStack, shouldRenderTimelineAvatars } from './runtime-avatar.js';
4
5
  import { applyRuntimeTone } from '../theme/index.js';
5
6
 
6
7
  function element(tag, className, text) {
@@ -100,7 +101,15 @@ function renderActivityItem({ item, registry, conditionLabel, onItemClick, onDet
100
101
  heading.append(title, element('em', '', resultText(item)));
101
102
  const meta = element('div', 'mb-runtime-timeline-meta');
102
103
  meta.append(element('span', '', item.summary || (item.automated ? item.statusLabel : '')), element('time', '', formatTime(item.time)));
103
- trigger.append(heading, meta);
104
+ const copy = element('div', 'mb-runtime-timeline-card-copy');
105
+ copy.append(heading, meta);
106
+ const avatars = shouldRenderTimelineAvatars(item) ? renderRuntimeAvatarStack(item.participants) : null;
107
+ if (avatars) {
108
+ trigger.classList.add('has-avatars');
109
+ trigger.append(avatars, copy);
110
+ } else {
111
+ trigger.appendChild(copy);
112
+ }
104
113
  trigger.addEventListener('click', (event) => {
105
114
  onItemClick?.(item, event);
106
115
  onDetailsRequest?.(item, event.currentTarget, event);
package/dist/styles.css CHANGED
@@ -938,6 +938,11 @@ button.mb-runtime-action-summary:hover, button.mb-runtime-action-summary:focus-v
938
938
  .mb-runtime-timeline-content { min-width: 0; display: grid; gap: 4px; }
939
939
  .mb-runtime-timeline-card { width: 100%; min-width: 0; overflow: visible; border: 0; border-radius: 0; display: grid; color: inherit; background: transparent; box-shadow: none; }
940
940
  .mb-runtime-timeline-card-main { width: 100%; min-width: 0; border: 0; border-radius: 8px; padding: 5px 7px; display: grid; gap: 3px; color: inherit; background: transparent; text-align: left; font: inherit; cursor: pointer; transition: background-color .14s ease, box-shadow .14s ease; }
941
+ .mb-runtime-timeline-card-main.has-avatars { grid-template-columns: auto minmax(0, 1fr); align-items: start; column-gap: 8px; }
942
+ .mb-runtime-timeline-card-copy { min-width: 0; display: grid; gap: 3px; }
943
+ .mb-runtime-timeline-avatars { display: flex; align-items: center; margin-top: 2px; }
944
+ .mb-runtime-timeline-avatars .mb-runtime-details-avatar + .mb-runtime-details-avatar { margin-left: -8px; box-shadow: 0 0 0 2px var(--nova-color-surface, #fff); }
945
+ .mb-runtime-timeline-avatar-more { font-size: var(--nova-font-size-xs, 10px); }
941
946
  .mb-runtime-timeline-card-main:hover { background: var(--nova-color-primary-soft, #f5f6ff); }
942
947
  .mb-runtime-timeline-card-main:focus-visible { outline: none; background: var(--nova-color-primary-soft, #f5f6ff); box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--nova-color-primary) 22%, transparent); }
943
948
  .mb-runtime-timeline-card-main:active { background: color-mix(in srgb, var(--nova-color-primary-soft) 84%, var(--nova-color-primary)); }
package/llms-full.txt CHANGED
@@ -323,7 +323,7 @@ Before changing Nova package versions, publishing, or recovering a partial relea
323
323
  - **流程展示**:只读 BPMN、实际审批路径、移动时间线和完整 BPMN 运行态。
324
324
  - **嵌入与扩展**:宿主布局、业务属性、节点内容、主题、审批详情及纯 SVG 导出。
325
325
 
326
- 当前版本为 **`0.3.6-preview`**,使用 `@preview` 安装。组件负责建模、展示和交互;流程执行、审批提交及权限由宿主业务系统负责。完整能力与 Preview 边界见[项目介绍](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/src/guide/overview.md)。
326
+ 当前版本为 **`0.3.7-preview`**,使用 `@preview` 安装。组件负责建模、展示和交互;流程执行、审批提交及权限由宿主业务系统负责。完整能力与 Preview 边界见[项目介绍](https://github.com/daxiangme/bpmn-nova/blob/dev/docs/src/guide/overview.md)。
327
327
 
328
328
  ## 安装
329
329
 
@@ -412,7 +412,7 @@ npm run docs:ai:check
412
412
  BPMN Nova 提供可嵌入的流程设计器、只读 Viewer、运行态审批轨迹、实例级主题、纯 SVG 导出,以及 React / Vue 适配。项目使用独立的 DOM / SVG 渲染实现,不依赖 `bpmn-js`,并为 Flowable 与 Activiti 提供 XML Profile 和属性扩展。
413
413
 
414
414
  > [!IMPORTANT]
415
- > 当前版本为 `0.3.6-preview`。它适合 SDK 评估、产品集成验证和企业审批原型;公开 Interface、BPMN XML round-trip 与引擎兼容能力仍在持续稳定中,建议使用 `@preview` 安装并在生产接入前完成目标流程验证。
415
+ > 当前版本为 `0.3.7-preview`。它适合 SDK 评估、产品集成验证和企业审批原型;公开 Interface、BPMN XML round-trip 与引擎兼容能力仍在持续稳定中,建议使用 `@preview` 安装并在生产接入前完成目标流程验证。
416
416
 
417
417
  AI 或代码生成工具接入必须从 [`llms.txt`](https://github.com/daxiangme/bpmn-nova/blob/dev/llms.txt) 的完整安装流程开始;需要全部接口与定制上下文时再读取 [`llms-full.txt`](https://github.com/daxiangme/bpmn-nova/blob/dev/llms-full.txt)。这两份文件也会随三个公开 npm 包发布。
418
418
 
@@ -479,7 +479,7 @@ BPMN Nova 是一个引擎中立、实例级可嵌入的 BPMN UI SDK。它把“
479
479
  | 流程设计 | Studio / Designer | 编辑完整流程定义、属性和 XML |
480
480
  | 只读展示 | Viewer `standard` | 展示完整 BPMN 结构,不叠加实例轨迹 |
481
481
  | 实际路径 | Viewer `approval` | 按实例已发生的有效路径展示状态、意见和异常线路 |
482
- | 移动时间线 | Viewer `compact` | 按审批访问轮次纵向展示动作、图文、附件和 Bottom Sheet 详情 |
482
+ | 移动时间线 | Viewer `compact` | 按审批访问轮次纵向展示处理人头像、动作、图文、附件和 Bottom Sheet 详情 |
483
483
  | 完整 BPMN | Viewer `standard` + Runtime | 保留完整 BPMN,并叠加节点状态、动作摘要和回退线路 |
484
484
 
485
485
  `projection: 'auto'` 会根据容器与 Runtime 场景选择合适投影;需要稳定布局语义时,应显式传入投影。
@@ -944,7 +944,7 @@ console.log(artifact.svg, artifact.width, artifact.height, artifact.warnings)
944
944
 
945
945
  完整 Studio 的默认导出选项放在 `config.export`;单次调用传入的选项仍用于本次导出。
946
946
 
947
- 导出内容不受当前缩放、平移或滚动影响。移动时间线会展开全部历史动作。审批图片通过 `purpose: 'export'` 解析并嵌入 Data URL;普通附件只输出名称、MIME 和大小。第一版不导出 PNG/PDF。
947
+ 导出内容不受当前缩放、平移或滚动影响。移动时间线会展开全部历史动作,并与页面一样绘制处理人头像。审批图片通过 `purpose: 'export'` 解析并嵌入 Data URL;普通附件只输出名称、MIME 和大小。第一版不导出 PNG/PDF。
948
948
 
949
949
  ## 9. 清理与生命周期
950
950
 
@@ -1528,6 +1528,8 @@ export const scenarioSnapshots = {
1528
1528
 
1529
1529
  `approval` 和 `compact` 会重排、聚合或过滤图元,展示不保证与 `standard` 的位置相同。普通开始/结束事件默认在完整 BPMN 可见,在实际路径/时间线默认隐藏。需要显示时设置 `runtimeTraceOptions.showStartMilestone / showEndMilestone`。
1530
1530
 
1531
+ 自 `0.3.7-preview` 起,`compact` 移动时间线在人工任务卡标题左侧展示处理人头像:读取 `participant.avatarUrl`,没有图片时用名字首字,与默认节点详情相同。只写 `assignee: '李经理'` 的旧记录出不了照片。`avatarUrl` 不要填凭证或短期签名。预测节点、自动任务和驳回/退回条不画头像。会签多人叠放最多两个头像,其余以 `+N` 表示。移动时间线的 SVG 导出与确认预览使用同一套头像:有图则嵌入 Data URL,失败时回退首字。
1532
+
1531
1533
  默认投影可能追加唯一可预测的后续人工节点;预测不是已发生事实,也不应写回 `activities` 或审批日志。需要严格只读历史时配置 `runtimeTraceOptions.includePredicted: false`;Studio 中放在 `config.viewer.runtimeTraceOptions`。
1532
1534
 
1533
1535
  ## 5. 加载、更新和交互
@@ -1840,7 +1842,7 @@ export function createAssetResolver(resolveAssetUrl: ResolveAssetUrl): RuntimeAs
1840
1842
 
1841
1843
  # BPMN Nova 快速开始
1842
1844
 
1843
- 本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.6-preview` 要求现代浏览器;Node.js 18+ 用于构建、SSR 和开发工具。
1845
+ 本文面向通过 NPM 集成 BPMN Nova 的应用开发者。版本 `0.3.7-preview` 要求现代浏览器;Node.js 18+ 用于构建、SSR 和开发工具。
1844
1846
 
1845
1847
  ## 1. 按语言与框架选择入口
1846
1848
 
@@ -2073,7 +2075,7 @@ viewer.destroy()
2073
2075
  | --- | --- |
2074
2076
  | `standard` | 完整 BPMN 流程图 |
2075
2077
  | `approval` | 桌面端实际有效路径图 |
2076
- | `compact` | 适合移动端的纵向审批时间线 |
2078
+ | `compact` | 适合移动端的纵向审批时间线,人工任务卡展示处理人头像 |
2077
2079
  | `auto` | 按 Viewer 容器宽度在 `approval` 与 `compact` 间切换 |
2078
2080
 
2079
2081
  只有显式设置 `responsive: true` 且没有固定投影,或直接设置 `projection: 'auto'` 时,才启用响应式切换。
@@ -2316,7 +2318,7 @@ BPMN Nova 的 Vanilla JavaScript / TypeScript 完整入口,提供流程设计
2316
2318
 
2317
2319
  > **English summary:** The complete framework-neutral BPMN Nova package for process design, viewing, approval traces, themes, properties, and pure SVG export.
2318
2320
 
2319
- > 当前版本为 `0.3.6-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
2321
+ > 当前版本为 `0.3.7-preview`。请使用 `@preview` 安装,并在生产接入前验证目标 BPMN XML 与引擎扩展。
2320
2322
 
2321
2323
  ![BPMN Nova 流程设计器](../../assets/bpmn-nova-designer.jpg)
2322
2324
 
@@ -2573,7 +2575,7 @@ Viewer 接收引擎中立的 Runtime Snapshot。三种投影使用相同数据
2573
2575
  | 投影 | 配置 | 适用场景 |
2574
2576
  | --- | --- | --- |
2575
2577
  | 实际路径 | `projection: 'approval'` | 只展示实际发生的有效路径 |
2576
- | 移动时间线 | `projection: 'compact'` | 按访问轮次展示动作、图片和附件 |
2578
+ | 移动时间线 | `projection: 'compact'` | 按访问轮次展示处理人头像、动作、图片和附件 |
2577
2579
  | 完整 BPMN | `projection: 'standard'` + `runtime` | 完整结构叠加运行状态 |
2578
2580
 
2579
2581
  ```js
@@ -2686,7 +2688,7 @@ const artifact = await shell.exportSvg({ theme: 'dark' })
2686
2688
  console.log(artifact.svg, artifact.warnings)
2687
2689
  ```
2688
2690
 
2689
- 导出完整业务内容,不受当前缩放、平移或滚动影响。图片会通过 Resolver 嵌入 SVG,普通附件只展示摘要。
2691
+ 导出完整业务内容,不受当前缩放、平移或滚动影响。图片会通过 Resolver 嵌入 SVG,普通附件只展示摘要。移动时间线导出与页面一致,在人工任务卡标题左侧绘制处理人头像。
2690
2692
 
2691
2693
  ![SVG 导出确认预览](../../assets/bpmn-nova-svg-export-preview.jpg)
2692
2694
 
@@ -2737,7 +2739,7 @@ BPMN Nova 的 Vue 3.3+ Adapter,提供 Studio、Designer、Viewer、审批轨
2737
2739
 
2738
2740
  > **English summary:** Vue 3.3+ components and exposed instance methods for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
2739
2741
 
2740
- > 当前版本为 `0.3.6-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
2742
+ > 当前版本为 `0.3.7-preview`。Vue 由宿主工程提供,本包不会替应用选择或升级框架版本。
2741
2743
 
2742
2744
  ![BPMN Nova Vue 流程工作台](../../assets/bpmn-nova-designer.jpg)
2743
2745
 
@@ -2989,7 +2991,7 @@ defineProps({
2989
2991
  ```
2990
2992
 
2991
2993
  - `approval`:实际发生的有效路径。
2992
- - `compact`:移动时间线、审批动作、图片和附件。
2994
+ - `compact`:移动时间线、处理人头像、审批动作、图片和附件。
2993
2995
  - `standard`:完整 BPMN 叠加运行状态。
2994
2996
 
2995
2997
  ![BPMN Nova Vue 移动审批时间线](../../assets/bpmn-nova-approval-mobile-timeline.jpg)
@@ -3111,7 +3113,7 @@ BPMN Nova 的 React 18+ Adapter,提供 Studio、Designer、Viewer、审批轨
3111
3113
 
3112
3114
  > **English summary:** React 18+ components and refs for BPMN Nova process design, viewing, approval traces, properties, themes, and SVG export.
3113
3115
 
3114
- > 当前版本为 `0.3.6-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
3116
+ > 当前版本为 `0.3.7-preview`。React 与 React DOM 由宿主工程提供,本包不会替应用选择或升级框架版本。
3115
3117
 
3116
3118
  ![BPMN Nova React 流程工作台](../../assets/bpmn-nova-designer.jpg)
3117
3119
 
@@ -3334,7 +3336,7 @@ export function ApprovalTrace({ xml, runtime, resolveAsset }) {
3334
3336
  ```
3335
3337
 
3336
3338
  - `approval`:实际发生的有效路径。
3337
- - `compact`:移动时间线、审批动作、图片和附件。
3339
+ - `compact`:移动时间线、处理人头像、审批动作、图片和附件。
3338
3340
  - `standard`:完整 BPMN 叠加运行状态。
3339
3341
 
3340
3342
  ![BPMN Nova React 移动审批时间线](../../assets/bpmn-nova-approval-mobile-timeline.jpg)
@@ -3452,7 +3454,7 @@ Apache-2.0. See [LICENSE](https://github.com/daxiangme/bpmn-nova/blob/dev/packag
3452
3454
 
3453
3455
  # BPMN Nova 组件参数
3454
3456
 
3455
- 本手册记录 `0.3.6-preview` 的公开组件参数、回调和实例方法。所有视觉组件都需要具有实际尺寸的容器,并显式导入所属包的 `styles.css`。
3457
+ 本手册记录 `0.3.7-preview` 的公开组件参数、回调和实例方法。所有视觉组件都需要具有实际尺寸的容器,并显式导入所属包的 `styles.css`。
3456
3458
 
3457
3459
  ## 在线体验
3458
3460
 
@@ -3736,7 +3738,7 @@ Runtime Details、Timeline 和 Transition Details 组件通过对应 `createReac
3736
3738
 
3737
3739
  <!-- SOURCE: docs/src/api/index.md -->
3738
3740
 
3739
- # BPMN Nova v0.3.6 Preview API
3741
+ # BPMN Nova v0.3.7 Preview API
3740
3742
 
3741
3743
  ## Framework Adapter 类型门面
3742
3744
 
@@ -4340,7 +4342,7 @@ Studio Shell 的轨迹切换项可通过 `projectionOptions` 配置。Playground
4340
4342
 
4341
4343
  运行实例默认使用稳定的 `approval` 实际路径图,不根据容器宽度自动改变展示形态。传入 `responsive: true` 且未显式设置 `projection`,或直接设置 `projection: 'auto'`,才会启用响应式切换:Viewer 容器宽度 `>= 720px` 时渲染实际有效路径图,`< 720px` 时切换为原生纵向滚动的审批时间线。响应式判断使用 `ResizeObserver`,不依赖全局窗口宽度;显式指定 `standard / approval / compact` 会固定投影。无 Runtime Snapshot 时默认仍为 `standard`。Compact 时间线始终占满 Viewer 容器宽度,不保留桌面居中最大宽度。
4342
4344
 
4343
- `approval` 与 `compact` 都由 `createRuntimeTraceProjection()` 生成。普通 StartEvent / EndEvent 默认只在 `standard` 完整 BPMN 中显示,实际路径图和移动时间线从第一个真实 Activity Visit 开始;消息、定时、信号等特殊事件仍作为运行时里程碑保留。可通过 `runtimeTraceOptions.showStartMilestone / showEndMilestone` 恢复普通里程碑。投影优先按带时间的 `edgeVisits`、Activity Visit 和运行时转移还原实际路径,只在路径唯一可预测时追加后续人工任务。条件网关不会成为轨迹卡片,实际命中的 Sequence Flow 名称会作为步骤间条件标签,原始表达式不会进入轨迹 UI。需要核对流程定义时切换到 `standard` 查看完整 BPMN
4345
+ `approval` 与 `compact` 都由 `createRuntimeTraceProjection()` 生成。普通 StartEvent / EndEvent 默认只在 `standard` 完整 BPMN 中显示,实际路径图和移动时间线从第一个真实 Activity Visit 开始;消息、定时、信号等特殊事件仍作为运行时里程碑保留。可通过 `runtimeTraceOptions.showStartMilestone / showEndMilestone` 恢复普通里程碑。投影优先按带时间的 `edgeVisits`、Activity Visit 和运行时转移还原实际路径,只在路径唯一可预测时追加后续人工任务。条件网关不会成为轨迹卡片,实际命中的 Sequence Flow 名称会作为步骤间条件标签,原始表达式不会进入轨迹 UI。需要核对流程定义时切换到 `standard` 查看完整 BPMN。自 `0.3.7-preview` 起,默认 Compact 时间线读取 `RuntimeParticipant`:人工任务卡在标题左侧展示处理人头像,有 `avatarUrl` 时出图,否则用名字首字;预测节点、自动任务和驳回/退回条不画头像。
4344
4346
 
4345
4347
  ```js
4346
4348
  const trace = createRuntimeTraceProjection({ model, runtime, presentation })
@@ -4457,7 +4459,7 @@ const artifact = await viewer.exportSvg({
4457
4459
  viewer.openSvgExportPreview()
4458
4460
  ```
4459
4461
 
4460
- `SvgExportArtifact` 包含 `svg`、`blob`、文件名、尺寸、`viewBox` 与非阻断 `warnings`。移动时间线会展开全部历史动作;附件只输出名称、MIME 和大小,审批图片通过 `runtimeAssetResolver(asset, { purpose: 'export', ... })` 解析并转为 Data URL 内嵌。自定义 HTML Renderer 需要在 `svgExport.nodeRenderers` 或 `svgExport.runtimeTimelineRenderer` 提供纯 SVG 适配,否则导出器使用标准视觉并记录警告。导出不使用 `foreignObject`、页面 CSS、Blob URL 或临时附件地址。
4462
+ `SvgExportArtifact` 包含 `svg`、`blob`、文件名、尺寸、`viewBox` 与非阻断 `warnings`。移动时间线会展开全部历史动作;附件只输出名称、MIME 和大小,审批图片通过 `runtimeAssetResolver(asset, { purpose: 'export', ... })` 解析并转为 Data URL 内嵌。自 `0.3.7-preview` 起,默认 Compact 时间线导出还会绘制处理人头像:有 `RuntimeParticipant.avatarUrl` 时直接抓取并嵌入,失败或未提供时用名字首字,不走附件 Resolver。自定义 HTML Renderer 需要在 `svgExport.nodeRenderers` 或 `svgExport.runtimeTimelineRenderer` 提供纯 SVG 适配,否则导出器使用标准视觉并记录警告。导出不使用 `foreignObject`、页面 CSS、Blob URL 或临时附件地址。
4461
4463
 
4462
4464
  ## 实例级主题与运行态外观
4463
4465
 
@@ -5517,7 +5519,7 @@ Default ●
5517
5519
 
5518
5520
  # BPMN Nova NPM 包与发布
5519
5521
 
5520
- 版本:`0.3.6-preview`。BPMN Nova 只发布三个 `@bpmn-nova` 公共包,均使用 ESM、附带 TypeScript 声明、采用 Apache-2.0 License,并通过 `preview` dist-tag 发布。
5522
+ 版本:`0.3.7-preview`。BPMN Nova 只发布三个 `@bpmn-nova` 公共包,均使用 ESM、附带 TypeScript 声明、采用 Apache-2.0 License,并通过 `preview` dist-tag 发布。
5521
5523
 
5522
5524
  ## 公共包
5523
5525
 
@@ -5537,6 +5539,8 @@ React/Vue 的声明入口显式重导出宿主常用的 Core 类型:`BpmnEdge`
5537
5539
 
5538
5540
  `0.3.6-preview` 在 Adapter 根入口补充 `inspectRuntime`、`formatRuntimeInstant`、`parseRuntimeInstant`、`activityState` 及对应类型;并修正同一实例 Runtime 刷新与 `setRegions()` 的视口行为。详见更新日志与审批轨迹接入指南。
5539
5541
 
5542
+ `0.3.7-preview` 在 Compact 移动时间线的人工任务卡展示处理人头像,SVG 导出与确认预览同步;无 `avatarUrl` 时用名字首字,与默认节点详情一致。详见更新日志与审批轨迹接入指南。
5543
+
5540
5544
  ## Studio 子路径
5541
5545
 
5542
5546
  Studio 根入口重新导出稳定的完整能力;以下子路径用于让导入意图更清晰:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bpmn-nova/studio",
3
- "version": "0.3.6-preview",
3
+ "version": "0.3.7-preview",
4
4
  "description": "Complete BPMN Nova Studio with Designer, Viewer, Palette, Properties and runtime visualization.",
5
5
  "keywords": [
6
6
  "bpmn",