flowsvg 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,525 @@
1
+ # flowsvg
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ flowsvg(流形)是 TypeScript ESM 声明式 SVG 图解播放器 SDK:用 JSON 或 TypeScript 对象描述逻辑图、架构图的节点、连线、场景与节拍,在网页中播放,或在无 DOM 环境导出指定时刻的 SVG。
6
+
7
+ 核心负责校验、时间求值、几何和 SVG;浏览器与框架接入负责播放、交互和生命周期。同一文档、时间和视觉选项得到一致的核心画面。节点悬停额外提供临时的浏览器反馈。
8
+
9
+ [安装](#安装) · [快速开始](#快速开始) · [Vue](#vue-集成) · [React](#react-集成) · [原生浏览器](#原生浏览器) · [Web Component](#web-component) · [API](#api) · [开发](#开发与验证) · [License](#license)
10
+
11
+ ## 安装
12
+
13
+ 从 npm 安装:
14
+
15
+ ```sh
16
+ pnpm add flowsvg
17
+ ```
18
+
19
+ 按所选入口安装框架依赖:
20
+
21
+ ```sh
22
+ # Vue 应用
23
+ pnpm add vue
24
+ # React 应用
25
+ pnpm add react react-dom
26
+ # React TypeScript 应用
27
+ pnpm add -D @types/react @types/react-dom
28
+ ```
29
+
30
+ 核心与原生浏览器入口无需安装框架。发布前可在源码仓库根目录构建本地 tarball:
31
+
32
+ ```sh
33
+ pnpm install --frozen-lockfile
34
+ pnpm --filter flowsvg exec npm pack --pack-destination /tmp
35
+ ```
36
+
37
+ 打包钩子会构建 SDK。生成的 `0.1.0` tarball 可通过 `pnpm add /tmp/flowsvg-0.1.0.tgz` 安装。
38
+
39
+ 运行核心需要 Node.js 18+ 或支持 ES2022 ESM 的环境;浏览器播放器需要 DOM、SVG 与 `requestAnimationFrame`。框架最低版本为 Vue 3.5、React 18。TypeScript 类型随包提供,React TypeScript 应用还需安装对应的 `@types/react` 和 `@types/react-dom`。
40
+
41
+ | 导入路径 | 用途 |
42
+ | --- | --- |
43
+ | `flowsvg` | 无 DOM 的文档校验、编译、求值、命中与 SVG 导出 |
44
+ | `flowsvg/browser` | 原生浏览器播放器 |
45
+ | `flowsvg/vue` | Vue 组件与实例类型 |
46
+ | `flowsvg/react` | React 组件与 ref 类型 |
47
+ | `flowsvg/web-component` | 显式注册自定义元素;也可使用 `flowsvg/element` |
48
+ | `flowsvg/styles.css` | 控件布局与可见焦点样式 |
49
+
50
+ ## 快速开始
51
+
52
+ ### 1. 创建图解文档
53
+
54
+ 将下面内容保存为 `diagram.ts`,后续集成示例复用此文件:
55
+
56
+ ```ts
57
+ import type { DiagramDocument } from "flowsvg";
58
+
59
+ export const diagramDocument: DiagramDocument = {
60
+ version: 1,
61
+ title: "请求链路",
62
+ viewport: { width: 640, height: 280 },
63
+ nodes: [
64
+ { id: "client", kind: "roundedRect", x: 40, y: 100, width: 140, height: 80, label: "Client" },
65
+ { id: "service", kind: "rect", x: 420, y: 100, width: 160, height: 80, label: "Service", visible: false }
66
+ ],
67
+ edges: [{ id: "request", from: "client", to: "service", label: "HTTP", route: "straight" }],
68
+ scenes: [{
69
+ id: "request-flow",
70
+ label: "请求进入服务",
71
+ beats: [
72
+ { durationMs: 1000, transitionMs: 250, caption: "服务出现", actions: [{ type: "show", target: "service" }] },
73
+ { durationMs: 1000, caption: "服务处理请求", actions: [{ type: "highlight", target: "service" }] }
74
+ ]
75
+ }]
76
+ };
77
+ ```
78
+
79
+ ### 2. 求值并导出 SVG
80
+
81
+ ```ts
82
+ import { createDiagram, renderSvg } from "flowsvg";
83
+ import { diagramDocument } from "./diagram.js";
84
+
85
+ const diagram = createDiagram(diagramDocument);
86
+ const frame = diagram.evaluateAt(125);
87
+ console.log(diagram.totalDuration, frame.sceneId, frame.warnings);
88
+
89
+ const svg = diagram.renderSvg(125, { theme: "dark" });
90
+ const finalSvg = renderSvg(diagramDocument, 2000);
91
+ ```
92
+
93
+ `svg` 和 `finalSvg` 是完整的静态 SVG 字符串,可以写入文件或用于预览。重复查询时复用 `createDiagram` 的结果;一次性导出可直接调用 `renderSvg`。网页播放可选择下方任一集成方式。
94
+
95
+ ## Vue 集成
96
+
97
+ 在 Vue 应用中创建组件,`diagram.ts` 与组件放在同一目录:
98
+
99
+ ```vue
100
+ <script setup lang="ts">
101
+ import { ref } from "vue";
102
+ import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/vue";
103
+ import type { PlayerErrorDetail } from "flowsvg/browser";
104
+ import "flowsvg/styles.css";
105
+ import { diagramDocument } from "./diagram.js";
106
+
107
+ const player = ref<FlowPlayerHandle | null>(null);
108
+ const timeMs = ref(0);
109
+
110
+ function handleError({ error }: PlayerErrorDetail): void {
111
+ console.error(error);
112
+ }
113
+ </script>
114
+
115
+ <template>
116
+ <FlowPlayer
117
+ ref="player"
118
+ :document="diagramDocument"
119
+ :speed="1.25"
120
+ theme="dark"
121
+ @timeupdate="timeMs = $event.timeMs"
122
+ @error="handleError"
123
+ />
124
+ <button type="button" @click="player?.play()">播放</button>
125
+ <button type="button" @click="player?.seek(0)">回到开头</button>
126
+ <output>{{ timeMs }} ms</output>
127
+ </template>
128
+ ```
129
+
130
+ 组件卸载时自动销毁播放器。脚本中通过 `player.value` 调用实例方法;模板会自动解包 ref。支持 `stepchange`、`timeupdate`、`playstatechange`、`error` 事件,参数直接为事件 detail;也可使用对应的回调 props。
131
+
132
+ ## React 集成
133
+
134
+ ```tsx
135
+ import { useRef } from "react";
136
+ import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/react";
137
+ import "flowsvg/styles.css";
138
+ import { diagramDocument } from "./diagram.js";
139
+
140
+ export default function DiagramExample() {
141
+ const player = useRef<FlowPlayerHandle | null>(null);
142
+
143
+ return (
144
+ <section>
145
+ <FlowPlayer
146
+ ref={player}
147
+ document={diagramDocument}
148
+ speed={1.25}
149
+ theme="dark"
150
+ onError={({ error }) => console.error(error)}
151
+ />
152
+ <button type="button" onClick={() => player.current?.play()}>播放</button>
153
+ <button type="button" onClick={() => player.current?.seek(0)}>回到开头</button>
154
+ </section>
155
+ );
156
+ }
157
+ ```
158
+
159
+ 组件卸载时自动清理资源。回调为 `onStepChange`、`onTimeUpdate`、`onPlayStateChange`、`onError`。
160
+
161
+ ### Vue / React 的参数与更新行为
162
+
163
+ | Prop | 默认或含义 |
164
+ | --- | --- |
165
+ | `document` | 必填图解文档;对象引用变化时重新加载,成功后暂停在零时刻 |
166
+ | `speed` | 正数倍率,默认 `1`;变化时更新速度 |
167
+ | `theme` | `light` / `dark`;未指定时使用文档主题,最终默认为浅色 |
168
+ | `controls` | 默认 `true`,设为 `false` 隐藏内置控件 |
169
+ | `reducedMotion` | 未指定时跟随浏览器减少动画偏好 |
170
+ | `hoverFill` | 默认 `false`:悬停只高亮边框;设为 `true` 时同时高亮背景和内容 |
171
+ | `initialTimeMs` | 仅初次挂载时应用;后续跳转用 `seek` |
172
+ | `className` / `style` | 宿主容器样式 |
173
+
174
+ 修改 `theme`、`controls`、`reducedMotion` 或 `hoverFill` 会重建播放器,暂停并回到零时刻;`initialTimeMs` 不会再次应用。修改文档时提供新的对象引用。ref / expose 提供下方播放器 API 中的七个控制和状态方法,清理由组件负责;挂载前或卸载后调用保留的 handle 会抛出未挂载错误。
175
+
176
+ 两种框架入口可安全用于服务端导入;SSR 输出宿主容器,播放器在客户端挂载后创建。需要服务端图解预览时,使用核心的 `renderSvg`。
177
+
178
+ ## 原生浏览器
179
+
180
+ 在页面中放置容器:
181
+
182
+ ```html
183
+ <div id="diagram" style="width: 100%; height: 360px;"></div>
184
+ ```
185
+
186
+ 在应用的客户端模块中创建播放器:
187
+
188
+ ```ts
189
+ import { createPlayer } from "flowsvg/browser";
190
+ import "flowsvg/styles.css";
191
+ import { diagramDocument } from "./diagram.js";
192
+
193
+ const host = document.querySelector<HTMLElement>("#diagram");
194
+ if (!host) throw new Error("Diagram host is missing");
195
+
196
+ const player = createPlayer(host, diagramDocument, { theme: "light" });
197
+ player.play();
198
+ ```
199
+
200
+ `createPlayer` 接管并替换容器子元素。在页面或组件卸载时调用 `player.destroy()`;在同一容器上创建新播放器前先销毁旧实例。原生 HTML 直接使用裸导入路径时需配置 import map,或通过应用构建工具处理;仓库的原生示例提供了 import map 配置。
201
+
202
+ 仓库中的 `apps/native-example/` 在固定镜头和布局下演示订单请求与响应。0–2400 ms,三条曲线分别显示圆点、`Token` 胶囊和反向预检胶囊;2400–4800 ms,另外两条曲线传递创建订单请求和库存确认;4800–7200 ms,响应消息返回网关和客户端。最终节拍在 8200 ms 结束,此时临时高亮已清除。相反方向的关系连线保持分离;悬停参与节点只增加浏览器高亮。
203
+
204
+ 可将 `apps/native-example/request-chain.js` 作为原生、Web Component、React 或 Vue 的 `document` 输入。各宿主共用 `seek(timeMs)`、`setSpeed(speed)`、`play()` 和 `pause()` 行为。`src/business-integration.test.ts` 通过四种公开入口验证同一示例,覆盖两种主题、减少动画、节拍边界及核心 SVG 一致性。
205
+
206
+ ## Web Component
207
+
208
+ 先在 HTML 中放置元素,再从客户端模块显式注册:
209
+
210
+ ```html
211
+ <flowsvg-player controls="false"></flowsvg-player>
212
+ ```
213
+
214
+ ```ts
215
+ import { defineFlowPlayer, type FlowPlayerElement } from "flowsvg/web-component";
216
+ import "flowsvg/styles.css";
217
+ import { diagramDocument } from "./diagram.js";
218
+
219
+ defineFlowPlayer();
220
+ const element = document.querySelector<FlowPlayerElement>("flowsvg-player");
221
+ if (element) element.document = diagramDocument;
222
+ ```
223
+
224
+ 默认元素名为 `flowsvg-player`,重复注册安全。对象通过 `document` property 传入;HTML `document` attribute 接受 JSON 字符串,`src` 接受相对于页面地址解析的 JSON URL。URL 请求遵循浏览器的同源与 CORS 规则。
225
+
226
+ 支持 `autoplay`、`controls`、`speed`、`theme`、`reduced-motion` 和 `hover-fill`。悬停背景默认关闭;设置 `hover-fill` 后同时提供背景和内容反馈。关闭默认显示的控件需使用 `controls="false"`;`autoplay` 默认关闭。选项变化时保留当前时间与播放/暂停状态。元素从 DOM 移除时清理播放器和请求,重新连接时可根据保留的文档重新挂载。
227
+
228
+ 元素提供 `play`、`pause`、`seek`、`step`、`setSpeed`、`getState`、`destroy`;更换文档使用 `document` property。挂载前 `getState()` 返回 `undefined`。它将四类播放/错误事件以冒泡、可穿越 Shadow DOM 边界的 CustomEvent 转发。
229
+
230
+ 实际注册需在 DOM 环境加载该入口。无 DOM 导入是安全的,但模块求值时会固定 HTMLElement 基类;同一个无 DOM 模块实例不能在稍后补上 DOM 后直接用于注册。
231
+
232
+ ## 文档与时间线
233
+
234
+ | 文档字段 | 约定 |
235
+ | --- | --- |
236
+ | `version` | 当前为 `1` |
237
+ | `viewport` | 有限且为正的 `width`、`height`,定义初始视口 |
238
+ | `nodes` | 节点列表,`id`、`kind`、`x`、`y`、`width`、`height` 必填 |
239
+ | `edges` | 连线列表,`id`、`from`、`to` 必填;端点引用节点或分组边界 |
240
+ | `scenes` | `[]` 表示静态图;场景具有唯一 `id`、`label` 和非空 `beats` |
241
+ | `title` / `theme` | 可选标题与 `light` / `dark` 文档主题 |
242
+
243
+ 节点与连线的 ID 全局唯一。节点形状为 `rect`、`roundedRect`、`ellipse`、`cylinder`、`group`;`parentId` 引用父分组,子节点坐标相对分组原点。`label` 为纯文本,`visible` 默认 `true`。节点 `style` 支持 `fill`、`stroke`、`strokeWidth`、`textColor` 和 `dashed`(用于分组边框);连线 `style` 支持 `stroke`、`strokeWidth`、`dashed`。颜色使用十六进制 RGB/RGBA,例如 `#2563eb` 或 `#2563eb80`。
244
+
245
+ 连线的 `route` 为 `straight`(默认)、`orthogonal` 或 `curved`。曲线根据端点方向端口生成平滑三次曲线,不会自动避让其他节点。正交路径避开可见普通节点,无法避让时使用固定回退;不保证最短路径或零交叉。需要固定连接侧时可使用 `fromPort` 和 `toPort`(`top`、`right`、`bottom` 或 `left`)。显式正交端口保留向外的端点线段,并绕过两端节点主体;曲线端口决定端点切线。省略端口时根据两节点中心的主方向自动选择,椭圆端口位于椭圆轮廓上。普通标签按确定性宽度估算换行,节点高度不足时保留完整标签并产生 `text-overflow` 警告。
246
+
247
+ 连线可以连接普通节点、分组,或分别连接两者。分组端点使用当前求值后的分组矩形,跟随嵌套移动并支持显式端口。隐藏端点分组或其祖先时,连线同时隐藏。传递只高亮端点分组本身,子节点保留自身高亮状态。分组边框仍不作为布线障碍。
248
+
249
+ ### 固定节点内的节点内容
250
+
251
+ `rect`、`roundedRect` 和 `cylinder` 节点可用 `content` 替代 `label`。椭圆和分组仅接受普通标签。同时声明两个字段会校验失败,空标签也算声明。`content.blocks` 按顺序排列 `title`、`paragraph`、`list` 和 `graph` 内容块。段落 `role` 为 `body`(默认)或 `auxiliary`(弱化说明)。空内容块数组只显示形状。下例需从 `flowsvg` 导入 `DiagramNode` 类型:
252
+
253
+ ```ts
254
+ const service = {
255
+ id: "service", kind: "rect", x: 24, y: 24, width: 260, height: 312,
256
+ content: {
257
+ style: { padding: 16, blockGap: 14 },
258
+ blocks: [
259
+ { type: "title", text: "Order service" },
260
+ { type: "paragraph", role: "auxiliary", text: "Core responsibilities" },
261
+ { type: "paragraph", text: "Validate and persist incoming orders." },
262
+ { type: "paragraph", text: "Return the order number.", style: { color: "#2563eb", align: "right" } }
263
+ ]
264
+ }
265
+ } satisfies DiagramNode;
266
+ ```
267
+
268
+ 标题/段落的 `text` 与列表条目的 `body`/`auxiliary`/`status`/文字标签 `text` 接受字符串或 `ContentTextSpan[]`。每个片段为 `{ text, style?: { fontSize, fontWeight, color } }`;对齐仍由内容块设置。例如 `text: [{ text: "已保存", style: { fontWeight: 700 } }, { text: " 2026-10-17", style: { fontSize: 12 } }]`。片段样式继承内容块默认值。每行片段共用基线,行高按该行最大字号计算,样式边界不强制换行。英文优先空格断行,中文、超长单词和 URL 可以在字素之间断行。`Intl.Segmenter` 将组合重音、肤色 emoji、ZWJ 序列、国旗和键帽保留为完整字素;跨源片段的字素使用首字符的样式。排版不读取 DOM 或字体加载结果。
269
+
270
+ 每个内容块的 `style` 支持正有限数 `fontSize`、100–900 的整数字重 `fontWeight`、十六进制 `color` 和 `align`(`left`、`center`、`right`)。内容整体样式支持非负有限数 `padding` 和 `blockGap`。字号与间距单位均为图解单位。默认标题字号/字重为 20/700,正文为 14/400,辅助文字为 12/400;内边距为 16,内容块间距为 14,行高至少为 20 或字号的 1.35 倍。辅助文字使用主题弱化色,作者显式指定的内容颜色保持原值。
271
+
272
+ 列表格式为 `{ type: "list", marker?: "none" | "bullet" | "number", items: ContentListItem[], style?: ContentListStyle }`,默认无标记(`none`)。每个条目可省略 `tags`、`status`、`body`、`auxiliary`、正文 `style`、`auxiliaryStyle` 和 `statusStyle`。列表 `style` 提供正文样式及非负有限 `itemGap`(默认 12)。条目正文样式覆盖列表正文样式;辅助文字保留弱化的 12 单位默认样式,可由 `auxiliaryStyle` 覆盖。圆点和编号使用统一标记列,容纳多位编号后留出 8 单位间距。正文续行和辅助文字均从正文列开始。标记与第一个存在的文字部分共用基线。缺省部分及完全空的条目不占高度或条目间距;显式空字符串和空行仍保留一行。例如:
273
+
274
+ ```ts
275
+ { type: "list", marker: "number", style: { itemGap: 16 }, items: [
276
+ { body: "校验请求。", auxiliary: "输入检查" },
277
+ { body: [{ text: "保存", style: { fontWeight: 700 } }, { text: "订单。" }] }
278
+ ] }
279
+ ```
280
+
281
+ 文字标签格式为 `{ text: ContentText, style?: { fontSize, fontWeight, color, fill } }`,默认字号 11。底色在文字左右各留 6 单位、上下各留 2 单位,圆角半径 3。列表 `style.tagGap` 为非负有限数,默认 6,用于标签之间、标签行之间及同排状态之前的间距。默认底色和文字颜色随主题变化,显式颜色保持原值。状态默认使用 12 单位的主题强调色;`statusStyle` 仅支持 `fontSize`、`fontWeight` 和 `color`,状态始终右对齐。单排标签与单行状态的自然宽度能容纳时共用基线;否则标签先换行,状态从全部标签之后开始。长标签和长状态沿用混合样式的字素换行规则。正文位于元信息之后,辅助文字位于正文之后。缺省部分不占空间。例如:
282
+
283
+ ```ts
284
+ { type: "list", items: [{
285
+ tags: [{ text: "world" }, { text: "experience", style: { fill: "#eadfff", color: "#57319b" } }],
286
+ status: "new", body: "Alice joined Google", auxiliary: "Mar 2026"
287
+ }] }
288
+ ```
289
+
290
+ 节点尺寸保持固定。矩形应用作者内边距。圆角矩形先在四边额外内缩 4 个图解单位,再应用内边距。圆柱上下各预留当前弧面半径的两倍,再应用内边距;弧面半径为 `min(14, height / 4)`。安全区宽高不小于零,形状留白与内边距占满节点时仍可求值。内容从安全区顶部开始,按确定性字宽估算换行,保留显式换行和空行。SVG 在内容安全区裁剪文字。宽度或高度不足时分别产生 `text-overflow` 警告,包含节点 ID、`nodes[index].content` 路径和不足方向。空安全区仍可求值和导出。被裁剪的内容不扩大节点命中范围。普通标签保留现有溢出行为。
291
+
292
+ `EvaluatedNode.contentLayout` 提供 `safeArea`、有序 `lines`、标签底色 `tags`、`widthOverflow` 和 `heightOverflow`。每行提供文本、基线坐标、估算宽度、字号、字重、颜色、对齐方式、语义角色(`title`、`body`、`auxiliary`、`marker`、`tag` 或 `status`)、源内容块索引 `blockIndex`,以及列表行的可选条目索引 `itemIndex`。标签文字行另有 `tagIndex`。每个标签底色提供绝对 `x`、`y`、`width`、`height`、最终 `fill`、作者颜色来源 `explicitFill` 及源索引 `blockIndex`/`itemIndex`/`tagIndex`。每行 `spans` 提供各片段的文本、左侧绝对坐标、共用基线、估算宽度及最终字号/字重/颜色;`explicitColor` 表示作者是否在内容块或源片段指定了颜色。浏览器 SVG 与静态导出使用相同结果。内容跟随当前绝对几何(包括尺寸过渡和父分组移动)、祖先显隐和有效透明度。内容不改变端口、连线路由、传递几何或节点父级规则。可运行[标题与段落示例](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/node-content.html)和[列表示例](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/list-content.html)。
293
+
294
+ 三种适用形状均支持初始节点内容、混合样式文字片段、列表和时间线替换。标签、状态及显式、传递和悬停高亮下的默认颜色可读性均已支持。
295
+
296
+
297
+ ### 节点内的实体关系小图
298
+
299
+ `graph` 内容块在支持内容的形状内部绘制无向实体关系小图。作者指定局部坐标,SDK 不自动排列实体。可从 `flowsvg` 导入 `ContentGraphBlock`:
300
+
301
+ ```ts
302
+ const entities: ContentGraphBlock = {
303
+ type: "graph", height: 130,
304
+ nodes: [
305
+ { id: "alice", label: "Alice", x: 120, y: 30, highlighted: true },
306
+ { id: "google", label: "Google", x: 50, y: 110, highlighted: true },
307
+ { id: "ml", label: "ML project", x: 190, y: 110 }
308
+ ],
309
+ edges: [{ from: "alice", to: "google" }, { from: "alice", to: "ml" }]
310
+ };
311
+ ```
312
+
313
+ `height` 为正数,预留固定高度,之后应用普通内容块间距。实体中心 `x`/`y` 是相对该区域左上角的非负有限坐标,单位为图解单位。ID 在单个图形块内唯一,关系的两个端点引用该块中的实体 ID。标签使用转义的 12 单位纯文本,居中放在半径为 4 的圆点上方。选中实体使用强调色圆点和粗体标签;关系的两个端点均选中时,关系使用强调色。文字与图元共用父节点内容裁剪,并跟随主题与高亮配色。坐标或标签超出图形块区域时,SDK 报告对应方向的溢出警告,后续内容块的位置保持不变。作者需在圆点上方为标签预留空间。实体没有独立交互身份,点击或悬停均命中父节点。
314
+
315
+ 存在图形块时,`contentLayout.graphs` 提供按 `blockIndex` 标识的绝对圆点/线段几何、局部 `highlighted` 状态和最终颜色。图形标签进入 `contentLayout.lines`。在节拍起点整体替换 `content`,即可切换局部选中状态。[Hindsight 完整示例](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/hindsight.html) 展示三幕、25 步故事,包括分组端点与实体选中状态变化。
316
+
317
+ ### 编写完整 Facts 时间线
318
+
319
+ 先指定固定形状尺寸,再按顺序添加内容块。记录使用 `marker: "none"`,能力清单使用 `"bullet"`,步骤使用 `"number"`。条目只需提供实际存在的部分。日期是普通文字;小字号片段可让日期与正文共用基线。下面的完整文档可以直接传给任一宿主:
320
+
321
+ ```ts
322
+ import { createDiagram, type DiagramDocument, type NodeContent } from "flowsvg";
323
+
324
+ const facts: NodeContent = { blocks: [
325
+ { type: "title", text: "Facts" },
326
+ { type: "paragraph", role: "auxiliary", text: "已确认的信息记录" },
327
+ { type: "list", marker: "none", items: [{
328
+ tags: [{ text: "world" }, { text: "订单", style: { fill: "#eadfff", color: "#57319b" } }],
329
+ status: "new",
330
+ body: [{ text: "订单已保存", style: { fontWeight: 700 } }, { text: " 2026-10-17", style: { fontSize: 11 } }],
331
+ auxiliary: "等待支付。"
332
+ }] }
333
+ ] };
334
+ const updated: NodeContent = { blocks: [
335
+ { type: "title", text: "Facts" },
336
+ { type: "list", items: [{ tags: [{ text: "verified" }], status: "done", body: "支付已确认", auxiliary: "2026-10-18" }] }
337
+ ] };
338
+ const document: DiagramDocument = {
339
+ version: 1, viewport: { width: 300, height: 480 },
340
+ nodes: [{ id: "facts", kind: "cylinder", x: 24, y: 20, width: 240, height: 440, content: facts }], edges: [],
341
+ scenes: [{ id: "records", label: "记录", beats: [
342
+ { durationMs: 1000, actions: [] },
343
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: updated } }] },
344
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { label: "已归档" } }] },
345
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: facts } }] },
346
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: null } }] }
347
+ ] }]
348
+ };
349
+ const diagram = createDiagram(document);
350
+ console.log(diagram.evaluateAt(1000).warnings);
351
+ const svg = diagram.renderSvg(1000, { theme: "dark", reducedMotion: true });
352
+ ```
353
+
354
+ 分享文档前检查 `contentLayout.safeArea` 与 `frame.warnings`。内容超出安全区时,作者需要增大尺寸或精简文字。SDK 裁剪溢出部分,不自动调整节点尺寸或缩小字号。切换主题或高亮时优先使用默认角色颜色;显式配色由作者负责。内容整体只有节点级交互目标,不提供内部链接、按钮、图片、HTML 或独立时间线目标。
355
+
356
+ [正式内容时间线](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/content-story.html) 将 Facts、服务能力和编号步骤与嵌套分组移动、节点变宽、显式端口、三种连线路由、同步传递及聚焦组合。[共享文档](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/content-story.js) 同时用于 React 和 Vue 示例;原生与 Web Component 在同一页面并列展示。正常、窄节点及长文字链接展示固定安全区裁剪,主题与减少动画控件传递相同视觉选项。四种宿主共用内置播放/暂停、速度、前后节拍、场景及进度控件。
357
+
358
+ 每个节拍包含正数 `durationMs` 和 `actions`,可选 `transitionMs` 与 `caption`。过渡时长默认 `min(250, durationMs)`,取值范围为 `[0, durationMs]`;`caption` 省略时继承前文,空字符串可清空。
359
+
360
+ | 动作 | 示例或可更新字段 |
361
+ | --- | --- |
362
+ | `show` / `hide` | `{ type: "show", target: "service" }`,作用于节点、分组或连线 |
363
+ | `highlight` / `unhighlight` | 改变高亮层,取消高亮后显示最新普通样式 |
364
+ | `flow` | `{ type: "flow", target: "request", direction: "forward" }`;在一个节拍内让标记沿连线移动。`direction` 为 `forward` 或 `reverse`,不会改变静态箭头方向;添加 `text: "请求"` 显示水平文字胶囊,省略 `text` 显示实心圆点。 |
365
+ | `updateNode` | `value` 支持 `x`、`y`、`width`、`height`、`label`、`content`、`fill`、`stroke`、`opacity` |
366
+ | `updateEdge` | `value` 支持 `label`、`stroke`、`strokeWidth`、`opacity` |
367
+ | `focus` | `target` 为节点/分组 ID;`null` 恢复完整视口 |
368
+
369
+ `updateNode.value.content` 在节拍起点整体替换内容,即使 `transitionMs` 非零也立即生效。结果继承到后续节拍和场景。更新 `label` 切换到普通标签模式;更新 `content` 切换到节点内容模式。`content: null` 清空任一文字模式,只保留形状,不恢复历史标签,也不改变节点尺寸。同一节拍不能同时赋值 `label` 与 `content`,也不能重复赋值任一模式;校验错误包含字段位置。跳转、暂停、播放速度及减少动画共用这一内容更新时间边界。
370
+
371
+ ```ts
372
+ // 每个 value 分别用于不同节拍的 updateNode 动作。
373
+ import type { NodeVisualState } from "flowsvg";
374
+
375
+ const replacement: NodeVisualState = { content: { blocks: [{ type: "title", text: "Updated" }] } };
376
+ const simpleLabel: NodeVisualState = { label: "Summary" };
377
+ const clearText: NodeVisualState = { content: null };
378
+ ```
379
+
380
+ 所有场景共享以毫秒计的全局时间线,后一场景继承前一场景最终状态。标签与说明文字在节拍起点切换,可插值属性在过渡阶段变化。节拍区间左闭右开,最终时间返回完成后的最后画面。隐藏节点或分组会同步遮蔽关联连线;重新显示时按对象各自保存的状态恢复。
381
+
382
+ 一个节拍可以同时在多条不同连线上传递。所有传递在节拍起点开始,以 `durationMs` 上相同的线性完成比例推进,在节拍结束时消失。因此,不同长度路径上的实际速度可以不同。只要当前传递仍使用某个端点,该节点就保持高亮。下一节拍会替换整组传递,跨场景边界也相同。传递高亮不会继承;显式高亮和普通样式继续按原有规则继承。
383
+
384
+ ## API
385
+
386
+ 传递文字属于单次动作,不替换连线的静态 `label`。胶囊宽度按 Unicode 码点和固定宽度估算,并保留内边距,不依赖浏览器测量。文字在所有路由上保持水平,并作为纯文本转义。胶囊不提供独立指针操作或键盘焦点;显式提供空字符串仍显示胶囊。
387
+
388
+ ### 核心 API
389
+
390
+ 从 `flowsvg` 导入:
391
+
392
+ | API | 返回与行为 |
393
+ | --- | --- |
394
+ | `validateDocument(input: unknown)` | 返回 `{ path, message }[]`;空数组表示通过 |
395
+ | `createDiagram(document)` | 校验、复制并编译,返回 `Diagram`;非法文档抛出 `DiagramValidationError` |
396
+ | `renderSvg(document, timeMs = 0, options?)` | 一次性编译并导出静态 SVG 字符串 |
397
+ | `diagram.totalDuration` | 全局总时长,静态文档为 `0` |
398
+ | `diagram.evaluateAt(timeMs, options?)` | 返回 `DiagramFrameState` |
399
+ | `diagram.renderSvg(timeMs = 0, options?)` | 导出相同求值结果的 SVG |
400
+ | `diagram.hitTest(timeMs, point, options?)` | 在文档坐标中返回 `{ id, type: "node" \| "edge" }` 或 `null` |
401
+ | `wrapText(label, maxWidth, typography?)` | 按固定字宽估算换行,返回文本行数组 |
402
+
403
+ `EvaluateOptions` 为 `{ theme?: "light" | "dark"; reducedMotion?: boolean }`。有限时间限制在 `[0, totalDuration]`,非有限时间按零处理。命中忽略隐藏或零透明度对象,优先级依次为普通节点、连线、分组背景。悬停所有节点默认只高亮边框,保留当前求值后的背景色,包括播放中的时间线高亮;设置 `hoverFill: true` 后才绘制背景反馈。
404
+
405
+ `DiagramFrameState` 包含 `timeMs`、`sceneId`、`beatId`、`beatProgress`、`nodes`、`edges`、`camera`、可选 `caption` 和 `warnings`。活动的 `EvaluatedEdge` 可能包含 `flow`,其中提供方向、节拍线性进度、文档坐标中的标记位置和可选消息 `text`;节拍结束、连线隐藏或启用减少动画时不提供标记。求值后的节点坐标为绝对文档坐标。静态文档的场景和节拍 ID 为 `null`;节拍 ID 格式为 `${scene.id}:${beatIndex}`,索引从零开始。
406
+
407
+ 主要公开类型包括 `DiagramDocument`、`DiagramNode`、`DiagramEdge`、`Scene`、`Beat`、`Action`、`Diagram`、`DiagramFrameState`、`EvaluateOptions`、`ValidationIssue` 和 `LayoutWarning`。类型声明及关键成员 JSDoc 随包提供。
408
+
409
+ ### 播放器 API
410
+
411
+ 从 `flowsvg/browser` 导入 `createPlayer(host, document, options?)`。`PlayerOptions` 包含核心视觉选项以及 `controls?: boolean` 和 `hoverFill?: boolean`。
412
+
413
+ | 方法或属性 | 行为 |
414
+ | --- | --- |
415
+ | `element` | 创建时传入的宿主元素,也是事件目标 |
416
+ | `play()` / `pause()` | 播放或保留当前画面并暂停;静态图或终点不会开始播放 |
417
+ | `seek(timeMs)` | 限制到时间线范围;保留播放状态,抵达终点时暂停 |
418
+ | `step(1)` / `step(-1)` | 暂停并跳到相邻节拍起点;节拍中途后退到本拍起点,在起点后退到上一拍 |
419
+ | `setSpeed(speed)` | 设置有限正数倍率,忽略非法值 |
420
+ | `load(document)` | 成功后暂停并回到零;校验失败时发出 error,保留原文档、时间和播放状态 |
421
+ | `getState()` | 返回 `PlayerState`:`timeMs`、`totalDuration`、`playing`、`speed`、`frame` |
422
+ | `destroy()` | 幂等清理 DOM、监听器与调度;之后停止事件,状态仍可读取 |
423
+
424
+ 创建时默认暂停在零时刻,内置控件默认开启。页面隐藏时暂停,重新可见后保持暂停;播放结束停在最终画面。静态图禁用时间控件。框架 handle 提供上述控制和状态方法,省去 `element` 与 `destroy`,由框架管理宿主与清理。
425
+
426
+ 播放或暂停时,悬停可见节点的形状、普通标签或结构化内容默认只高亮当前节点边框,不联动相邻节点或连线。设置 `hoverFill: true` 后才同时高亮背景与结构化内容。鼠标移出只解除悬停原因;传递与显式高亮仍有效时,节点继续高亮。全部原因结束后,节点显示当前普通样式。画面更新后,播放器重新检查未移动的指针;指针取消、文档成功替换和销毁会清除悬停。
427
+
428
+ 默认标题、正文、列表标记、辅助文字、文字标签与状态在浅深主题及显式、传递、悬停高亮下保持可读。高亮过渡的中间帧按当前求值底色选择具有足够对比度的默认文字颜色。作者指定的片段或内容块颜色及标签底色保持原值。高亮不改变文字几何、安全区或命中范围。内容内部没有独立 ID、键盘焦点、链接或按钮;点击内容返回节点身份,被裁剪的文字不增加命中区域。内容替换、文字模式切换与节点尺寸更新后,悬停只作用于当前画面;成功替换文档会清除旧文档的悬停。
429
+
430
+ `PlayerState.frame` 保留核心画面,不包含悬停状态。静态 SVG 导出也不包含悬停;没有悬停节点时,浏览器输出与相同时间的导出一致。
431
+
432
+ ### 事件
433
+
434
+ 原生播放器在宿主元素上派发 `CustomEvent`:
435
+
436
+ | 事件 | `detail` |
437
+ | --- | --- |
438
+ | `timeupdate` | 当前 `PlayerState`,画面更新后发送 |
439
+ | `stepchange` | 当前 `PlayerState`,场景或节拍改变时发送,早于该次 timeupdate |
440
+ | `playstatechange` | 当前 `PlayerState`,播放/暂停状态变化时发送 |
441
+ | `error` | `PlayerErrorDetail`:保留的播放器状态与 `error` |
442
+ | `hover` / `click` | `PlayerInteractionDetail` 或 `null`;也提供 `objecthover` / `objectclick` 同义事件 |
443
+
444
+ 交互 detail 包含 `id`、`type`、文档坐标 `point`、`timeMs`、`sceneId`、`beatId`;未命中为 `null`。Vue、React 和 Web Component 的包装层转发前四类事件。Web Component 挂载前的错误可能没有播放器状态。
445
+
446
+ 指针移动时发送 `hover`;画面更新导致未移动的指针命中其他对象时,也发送该事件。指针移出或取消时发送 `null`。
447
+
448
+ ```ts
449
+ import type { PlayerEventMap } from "flowsvg/browser";
450
+
451
+ function handleTimeUpdate(event: Event): void {
452
+ const { detail } = event as PlayerEventMap["timeupdate"];
453
+ console.log(detail.timeMs, detail.frame.caption);
454
+ }
455
+
456
+ player.element.addEventListener("timeupdate", handleTimeUpdate);
457
+ ```
458
+
459
+ 上例沿用原生集成创建的 `player`。宿主自行添加的监听器由宿主移除。
460
+
461
+ ## 主题与无障碍
462
+
463
+ 主题优先级为显式选项 → 文档主题 → 浅色;核心不读取系统主题。核心的 `reducedMotion` 默认关闭,浏览器未指定时跟随 `prefers-reduced-motion` 并响应变化。减少动画让过渡立即完成,保留总时长与时间位置。
464
+
465
+ 导入 `flowsvg/styles.css` 可获得控件布局与焦点样式。CSS 变量只定制控件,图解本身颜色由核心主题和文档样式决定:
466
+
467
+ ```css
468
+ .flowsvg {
469
+ --flowsvg-control-background: #101828;
470
+ --flowsvg-control-color: #f4f7fb;
471
+ --flowsvg-control-border: #64748b;
472
+ --flowsvg-control-accent: #60a5fa;
473
+ --flowsvg-control-focus: #fbbf24;
474
+ }
475
+ ```
476
+
477
+ 原生宿主可添加 `flowsvg` class 使用上例,框架组件会自动添加。聚焦画布后,Space 播放/暂停,左右方向键逐步移动;表单控件保留原生按键行为。说明文字使用礼貌的 live region,SVG 具有独立的可访问名称。
478
+
479
+ ## 校验与错误
480
+
481
+ 未知字段、无效引用、循环分组、重复节拍赋值、非法颜色以及无效尺寸/时长会在加载时拒绝。文本按纯文本转义。需要预先检查外部 JSON 时,先调用 `validateDocument`,根据每条 issue 的 `path` 和 `message` 展示错误。
482
+
483
+ `createDiagram` 和无效初始文档的 `createPlayer` 会抛出 `DiagramValidationError`,其 `issues` 包含详细路径。已有播放器的 `load` 通过 `error` 事件报告校验失败并保留旧状态。文字高度溢出属于非阻塞布局警告,读取 `frame.warnings` 后决定是否调整节点尺寸。
484
+
485
+ 一个节拍内每条连线最多包含一个 `flow`,方向或文字不同也不能重复。重复连线会在后一动作的 `target` 报告 `duplicate beat assignment`,其他字段无效也不影响重复检查。传递可以与同一连线的显隐、显式高亮和普通样式动作共存;各类动作继续遵循各自的校验规则。
486
+
487
+ ## 开发与验证
488
+
489
+ 以下命令在源码仓库根目录执行。完整开发环境使用 Node.js 22.12+ 与 pnpm 10.12.1;Vue/React 示例基于 Vite 7。
490
+
491
+ | 命令 | 用途 |
492
+ | --- | --- |
493
+ | `pnpm install` | 安装工作区依赖 |
494
+ | `pnpm build` | 构建 SDK 的 JS、类型与样式 |
495
+ | `pnpm typecheck` | 检查核心、浏览器和框架入口类型 |
496
+ | `pnpm test` | 运行 Vitest 行为测试 |
497
+ | `pnpm --filter flowsvg-vue-example dev` | 启动 Vue 示例 |
498
+ | `pnpm --filter flowsvg-react-example dev` | 启动 React 示例 |
499
+ | `pnpm release:verify` | 从缺失的 `dist` 打包,检查文件白名单与全部导出,再验证无框架导入、严格消费者类型及四种宿主生命周期 |
500
+ | `pnpm release` | 运行全部发布检查与 npm 预演,将已验证的 tarball 保存在 `dist/releases/`,不上传 |
501
+ | `pnpm release:publish` | 检查 npm 登录状态,重新运行全部检查与预演,然后上传已验证的 tarball |
502
+
503
+ 维护者请参照 [npm 发布指南](https://github.com/yokingma/FlowSVG/blob/HEAD/docs/RELEASING.md)。SDK 打包会删除并重建 `dist`,必须在示例构建之前完成。
504
+
505
+ 构建后,从仓库根目录启动 Node.js 静态服务器:
506
+
507
+ ```sh
508
+ pnpm dlx http-server@14.1.1 . -p 4175 -c-1
509
+ ```
510
+
511
+ 首次运行会下载 CLI,不会新增项目依赖。打开 <http://localhost:4175/apps/native-example/> 运行原生示例。浏览器验收访问 `/packages/flowsvg/tests/browser-*.html` 下的页面:`controls`、`lifecycle`、`visual-updates`、`focus`、`accessibility`、`web-component`、`flow-text`、`parallel-flows`。检查相应的 `window.browserTestResults`、`window.browserFocusResults`、`window.browserAccessibilityResults`、`window.browserWebComponentResults` 或 `window.browserFlowTextResults`,每条结果的 `passed` 应为 `true`。
512
+
513
+ 真实指针悬停验收访问 `/packages/flowsvg/tests/browser-hover.html`。分别悬停起点形状与溢出文字,使用传递中点和传递结束控件,检查鼠标移出时保留活动传递高亮,传递结束后恢复最新普通样式。其余控件用于检查固定指针下的画面更新、重载清理和销毁清理。
514
+
515
+ 自动减少动画的验收:打开 accessibility 页,切换浏览器的减少动画媒体模拟,再调用 `window.verifyMotionPreference(true)` 或 `false`,验证系统偏好变化与同选项 SVG 导出一致。
516
+
517
+ 高频播放时复用编译结果,避免每次渲染都重建播放器;大型图解应在目标设备上测量帧耗时。变更记录见 [CHANGELOG](CHANGELOG.md),实现约定见 [PATTERNS](PATTERNS.md)。
518
+
519
+ `tests/browser-parallel-flows.html` 提供先三条、后两条传递的可播放示例,并比较跨场景边界、主题和减少动画选项下的公开画面与 SVG。
520
+
521
+ ## License
522
+
523
+ [MIT](LICENSE)。可使用、修改及分发;保留许可证与版权声明。
524
+
525
+ 访问[节点内容指针验收页](https://github.com/yokingma/FlowSVG/blob/HEAD/packages/flowsvg/tests/browser-node-content.html),深色模式添加 `?theme=dark`。检查三种形状、文字标签、长文字裁剪、显式/传递/悬停分层、未移动指针下的内容/文字模式/节点尺寸更新、主题文档替换与销毁。
@@ -0,0 +1,69 @@
1
+ import { type DiagramDocument, type DiagramFrameState, type DiagramPoint, type EvaluateOptions, type HitObject, type ThemeName } from "./core.js";
2
+ /** Creation options: controls default on, motion follows browser preferences, and theme falls back to the document/light. */
3
+ export interface PlayerOptions extends EvaluateOptions {
4
+ controls?: boolean;
5
+ /** Hover highlights borders by default; true also paints the background and adjusts content ink. Applies to groups too. */
6
+ hoverFill?: boolean;
7
+ }
8
+ /** Player snapshot: times are milliseconds, speed is a positive multiplier, and frame is the core result without hover. */
9
+ export interface PlayerState {
10
+ timeMs: number;
11
+ totalDuration: number;
12
+ playing: boolean;
13
+ speed: number;
14
+ frame: DiagramFrameState;
15
+ }
16
+ /** Object interaction detail; point uses document coordinates and time/beat IDs reflect the event-time player state. */
17
+ export interface PlayerInteractionDetail extends HitObject {
18
+ point: DiagramPoint;
19
+ timeMs: number;
20
+ sceneId: string | null;
21
+ beatId: string | null;
22
+ }
23
+ /** Imperative browser player; creation/successful load pause at zero, and controls become inert after destruction. */
24
+ export interface FlowPlayer {
25
+ /** The supplied host, also used as the CustomEvent dispatch target. */
26
+ readonly element: HTMLElement;
27
+ /** Play from the current position; do not start for static diagrams, at the end, or while the page is hidden. */
28
+ play(): void;
29
+ /** Stop time advancement, retaining the current frame. */
30
+ pause(): void;
31
+ /** Seek to a clamped millisecond time; preserve playback except at the end. Non-finite times become zero. */
32
+ seek(timeMs: number): void;
33
+ /** Pause and jump to the adjacent start; backward goes to this beat's start from inside it, or the preceding start. */
34
+ step(direction: 1 | -1): void;
35
+ /** Set a finite positive speed multiplier; ignore invalid values. */
36
+ setSpeed(speed: number): void;
37
+ /** Compile before replacing; reset/pause at zero on success, or emit error and retain the old document/time/playback. */
38
+ load(document: DiagramDocument): void;
39
+ /** Read current player state; the last state remains readable after destruction. */
40
+ getState(): PlayerState;
41
+ /** Silently and idempotently release frames/listeners/host content; no further player events are dispatched. */
42
+ destroy(): void;
43
+ }
44
+ /** Retained state and original load error; DiagramValidationError.issues provides field paths for validation failures. */
45
+ export interface PlayerErrorDetail extends PlayerState {
46
+ error: Error;
47
+ }
48
+ /**
49
+ * Host events and their detail types. Frame updates emit stepchange when needed, then timeupdate.
50
+ * hover/click use null for no hit; objecthover/objectclick report the same interactions under alternate names.
51
+ * Hover also changes when a frame update changes the object under a stationary pointer.
52
+ */
53
+ export type PlayerEventMap = {
54
+ stepchange: CustomEvent<PlayerState>;
55
+ timeupdate: CustomEvent<PlayerState>;
56
+ playstatechange: CustomEvent<PlayerState>;
57
+ error: CustomEvent<PlayerErrorDetail>;
58
+ hover: CustomEvent<PlayerInteractionDetail | null>;
59
+ click: CustomEvent<PlayerInteractionDetail | null>;
60
+ objecthover: CustomEvent<PlayerInteractionDetail | null>;
61
+ objectclick: CustomEvent<PlayerInteractionDetail | null>;
62
+ };
63
+ /**
64
+ * Mount a player using the host document and observe browser motion preferences unless explicitly overridden.
65
+ * Replaces host children; destroy an existing player before replacing it on the same host.
66
+ * @throws {DiagramValidationError} For an invalid initial document, before an instance exists to emit error.
67
+ */
68
+ export declare function createPlayer(host: HTMLElement, document: DiagramDocument, options?: PlayerOptions): FlowPlayer;
69
+ export type { ThemeName };