flowsvg 0.0.0-stage → 0.1.1

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.
Files changed (61) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/PATTERNS.md +112 -0
  4. package/README.md +546 -2
  5. package/README.zh-CN.md +547 -0
  6. package/dist/adapters/react.d.ts +48 -0
  7. package/dist/adapters/react.js +126 -0
  8. package/dist/adapters/vue.d.ts +132 -0
  9. package/dist/adapters/vue.js +161 -0
  10. package/dist/adapters/web-component.d.ts +99 -0
  11. package/dist/adapters/web-component.js +396 -0
  12. package/dist/browser/accessibility.d.ts +16 -0
  13. package/dist/browser/accessibility.js +36 -0
  14. package/dist/browser/controls.d.ts +41 -0
  15. package/dist/browser/controls.js +80 -0
  16. package/dist/browser/lifecycle.d.ts +6 -0
  17. package/dist/browser/lifecycle.js +31 -0
  18. package/dist/browser/player.d.ts +69 -0
  19. package/dist/browser/player.js +353 -0
  20. package/dist/browser/svg-dom.d.ts +6 -0
  21. package/dist/browser/svg-dom.js +74 -0
  22. package/dist/browser.d.ts +4 -0
  23. package/dist/browser.js +2 -0
  24. package/dist/core/color.d.ts +3 -0
  25. package/dist/core/color.js +18 -0
  26. package/dist/core/diagram.d.ts +38 -0
  27. package/dist/core/diagram.js +391 -0
  28. package/dist/core/edge-geometry.d.ts +32 -0
  29. package/dist/core/edge-geometry.js +382 -0
  30. package/dist/core/model.d.ts +256 -0
  31. package/dist/core/model.js +1 -0
  32. package/dist/core/node-content-paint.d.ts +10 -0
  33. package/dist/core/node-content-paint.js +45 -0
  34. package/dist/core/node-content.d.ts +184 -0
  35. package/dist/core/node-content.js +554 -0
  36. package/dist/core/svg-number.d.ts +5 -0
  37. package/dist/core/svg-number.js +7 -0
  38. package/dist/core/theme.d.ts +21 -0
  39. package/dist/core/theme.js +17 -0
  40. package/dist/core/timeline.d.ts +56 -0
  41. package/dist/core/timeline.js +148 -0
  42. package/dist/core/validation.d.ts +11 -0
  43. package/dist/core/validation.js +389 -0
  44. package/dist/index.d.ts +5 -0
  45. package/dist/index.js +2 -0
  46. package/dist/react.d.ts +3 -0
  47. package/dist/react.js +2 -0
  48. package/dist/styles.css +47 -0
  49. package/dist/vue.d.ts +3 -0
  50. package/dist/vue.js +2 -0
  51. package/dist/web-component.d.ts +3 -0
  52. package/dist/web-component.js +2 -0
  53. package/docs/images/entity-graph.png +0 -0
  54. package/docs/images/facts-timeline.png +0 -0
  55. package/docs/images/facts-timeline.zh-CN.png +0 -0
  56. package/docs/images/node-content.png +0 -0
  57. package/docs/images/numbered-list.png +0 -0
  58. package/docs/images/numbered-list.zh-CN.png +0 -0
  59. package/docs/images/quick-start.png +0 -0
  60. package/docs/images/record-tags.png +0 -0
  61. package/package.json +88 -4
@@ -0,0 +1,547 @@
1
+ # FlowSVG
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ FlowSVG(流形)用 SVG 播放逻辑图和架构图。用 JSON 或 TypeScript 定义节点、连线、场景与节拍,即可在浏览器播放或导出静态 SVG。
6
+
7
+ SDK 使用 ESM,提供无 DOM 核心及原生浏览器、Vue、React、Web Component 接入。各接入方式共用同一文档和时间线。
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
+ 核心与原生浏览器入口无需框架,类型声明随包提供。核心要求 Node.js 18+ 或 ES2022 ESM;浏览器要求 DOM、SVG 和 `requestAnimationFrame`;框架最低版本为 Vue 3.5、React 18。
31
+
32
+ | 导入路径 | 用途 |
33
+ | --- | --- |
34
+ | `flowsvg` | 无 DOM 的文档校验、编译、求值、命中与 SVG 导出 |
35
+ | `flowsvg/browser` | 原生浏览器播放器 |
36
+ | `flowsvg/vue` | Vue 组件与实例类型 |
37
+ | `flowsvg/react` | React 组件与 ref 类型 |
38
+ | `flowsvg/web-component` | 显式注册自定义元素;也可使用 `flowsvg/element` |
39
+ | `flowsvg/styles.css` | 控件布局与可见焦点样式 |
40
+
41
+ ## 快速开始
42
+
43
+ ### 1. 创建图解文档
44
+
45
+ 将下面内容保存为 `diagram.ts`。这个 5.2 秒的示例包含六个节点、三个场景和五个节拍,依次展示身份校验、订单写库,以及异步事件与响应的并行传递。后续集成示例复用此文件:
46
+
47
+ ```ts
48
+ import type { DiagramDocument } from "flowsvg";
49
+
50
+ export const diagramDocument: DiagramDocument = {
51
+ version: 1,
52
+ title: "订单请求生命周期",
53
+ viewport: { width: 960, height: 440 },
54
+ nodes: [
55
+ { id: "client", kind: "roundedRect", x: 40, y: 205, width: 120, height: 70, label: "Client" },
56
+ { id: "gateway", kind: "roundedRect", x: 260, y: 205, width: 130, height: 70, label: "API gateway" },
57
+ { id: "auth", kind: "ellipse", x: 260, y: 40, width: 130, height: 72, label: "Auth" },
58
+ { id: "orders", kind: "roundedRect", x: 500, y: 205, width: 150, height: 70, label: "Order service" },
59
+ { id: "database", kind: "cylinder", x: 780, y: 65, width: 140, height: 90, label: "Orders" },
60
+ { id: "events", kind: "rect", x: 780, y: 330, width: 140, height: 70, label: "Event queue" }
61
+ ],
62
+ edges: [
63
+ { id: "request", from: "client", to: "gateway", route: "straight" },
64
+ { id: "authorize", from: "gateway", to: "auth", route: "straight", fromPort: "top", toPort: "bottom" },
65
+ { id: "create", from: "gateway", to: "orders", route: "straight" },
66
+ { id: "persist", from: "orders", to: "database", route: "orthogonal", fromPort: "right", toPort: "left" },
67
+ { id: "publish", from: "orders", to: "events", route: "curved", fromPort: "bottom", toPort: "left" },
68
+ { id: "result", from: "orders", to: "gateway", route: "curved", fromPort: "bottom", toPort: "bottom" }
69
+ ],
70
+ scenes: [
71
+ { id: "request", label: "接收与认证", beats: [
72
+ { durationMs: 800, caption: "接收订单请求", actions: [
73
+ { type: "flow", target: "request", direction: "forward", text: "POST" }
74
+ ] },
75
+ { durationMs: 800, caption: "校验访问令牌", actions: [
76
+ { type: "flow", target: "authorize", direction: "forward", text: "Token" }
77
+ ] }
78
+ ] },
79
+ { id: "create-order", label: "创建订单", beats: [
80
+ { durationMs: 1000, caption: "将已认证请求交给订单服务", actions: [
81
+ { type: "flow", target: "create", direction: "forward", text: "Create" }
82
+ ] },
83
+ { durationMs: 1000, caption: "将新订单写入数据库", actions: [
84
+ { type: "flow", target: "persist", direction: "forward", text: "INSERT" }
85
+ ] }
86
+ ] },
87
+ { id: "response", label: "发布事件与响应", beats: [
88
+ { durationMs: 1600, caption: "发布 OrderCreated 事件,同时返回订单号", actions: [
89
+ { type: "flow", target: "publish", direction: "forward", text: "OrderCreated" },
90
+ { type: "flow", target: "result", direction: "forward", text: "201 Created" },
91
+ { type: "flow", target: "request", direction: "reverse", text: "Order ID" }
92
+ ] }
93
+ ] }
94
+ ]
95
+ };
96
+ ```
97
+
98
+ 浅色主题下的 4400 ms 画面:事件与响应消息同时传递,仅高亮当前参与的节点。
99
+
100
+ ![订单流程包含 Client、API gateway、Auth、Order service、Orders 数据库和 Event queue,正在传递 OrderCreated 事件与响应消息](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/quick-start.png)
101
+
102
+ ### 2. 求值并导出 SVG
103
+
104
+ ```ts
105
+ import { createDiagram, renderSvg } from "flowsvg";
106
+ import { diagramDocument } from "./diagram.js";
107
+
108
+ const diagram = createDiagram(diagramDocument);
109
+ const frame = diagram.evaluateAt(125);
110
+ console.log(diagram.totalDuration, frame.sceneId, frame.warnings);
111
+
112
+ const svg = diagram.renderSvg(125, { theme: "dark" });
113
+ const finalSvg = renderSvg(diagramDocument, 5200);
114
+ ```
115
+
116
+ 两种调用均返回静态 SVG 字符串。重复查询时复用 `createDiagram`;单次导出用 `renderSvg`。
117
+
118
+ ## Vue 集成
119
+
120
+ 在 Vue 应用中创建组件,`diagram.ts` 与组件放在同一目录:
121
+
122
+ ```vue
123
+ <script setup lang="ts">
124
+ import { ref } from "vue";
125
+ import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/vue";
126
+ import type { PlayerErrorDetail } from "flowsvg/browser";
127
+ import "flowsvg/styles.css";
128
+ import { diagramDocument } from "./diagram.js";
129
+
130
+ const player = ref<FlowPlayerHandle | null>(null);
131
+ const timeMs = ref(0);
132
+
133
+ function handleError({ error }: PlayerErrorDetail): void {
134
+ console.error(error);
135
+ }
136
+ </script>
137
+
138
+ <template>
139
+ <FlowPlayer
140
+ ref="player"
141
+ :document="diagramDocument"
142
+ :speed="1.25"
143
+ theme="dark"
144
+ @timeupdate="timeMs = $event.timeMs"
145
+ @error="handleError"
146
+ />
147
+ <button type="button" @click="player?.play()">播放</button>
148
+ <button type="button" @click="player?.seek(0)">回到开头</button>
149
+ <output>{{ timeMs }} ms</output>
150
+ </template>
151
+ ```
152
+
153
+ 卸载时自动销毁。脚本中通过 `player.value` 调用方法,模板自动解包 ref。事件直接传入 detail:`stepchange`、`timeupdate`、`playstatechange`、`error`,也支持对应回调 props。
154
+
155
+ ## React 集成
156
+
157
+ ```tsx
158
+ import { useRef } from "react";
159
+ import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/react";
160
+ import "flowsvg/styles.css";
161
+ import { diagramDocument } from "./diagram.js";
162
+
163
+ export default function DiagramExample() {
164
+ const player = useRef<FlowPlayerHandle | null>(null);
165
+
166
+ return (
167
+ <section>
168
+ <FlowPlayer
169
+ ref={player}
170
+ document={diagramDocument}
171
+ speed={1.25}
172
+ theme="dark"
173
+ onError={({ error }) => console.error(error)}
174
+ />
175
+ <button type="button" onClick={() => player.current?.play()}>播放</button>
176
+ <button type="button" onClick={() => player.current?.seek(0)}>回到开头</button>
177
+ </section>
178
+ );
179
+ }
180
+ ```
181
+
182
+ 组件卸载时自动清理资源。回调为 `onStepChange`、`onTimeUpdate`、`onPlayStateChange`、`onError`。
183
+
184
+ ### Vue / React 的参数与更新行为
185
+
186
+ | Prop | 默认或含义 |
187
+ | --- | --- |
188
+ | `document` | 必填图解文档;对象引用变化时重新加载,成功后暂停在零时刻 |
189
+ | `speed` | 正数倍率,默认 `1`;变化时更新速度 |
190
+ | `theme` | `light` / `dark`;未指定时使用文档主题,最终默认为浅色 |
191
+ | `controls` | 默认 `true`,设为 `false` 隐藏内置控件 |
192
+ | `reducedMotion` | 未指定时跟随浏览器减少动画偏好 |
193
+ | `hoverFill` | 默认 `false`:悬停只高亮边框;设为 `true` 时同时高亮背景和内容 |
194
+ | `initialTimeMs` | 仅初次挂载时应用;后续跳转用 `seek` |
195
+ | `className` / `style` | 宿主容器样式 |
196
+
197
+ 修改 `theme`、`controls`、`reducedMotion` 或 `hoverFill` 会重建播放器,暂停在零时刻,不再应用 `initialTimeMs`。更新文档需提供新对象引用。实例暴露[播放器 API](#播放器-api)中的七个方法;挂载前或卸载后调用会抛出错误。
198
+
199
+ 两种入口均支持服务端导入。SSR 输出容器,客户端挂载后创建播放器。服务端图解预览使用 `renderSvg`。
200
+
201
+ ## 原生浏览器
202
+
203
+ 在页面中放置容器:
204
+
205
+ ```html
206
+ <div id="diagram" style="width: 100%; height: 360px;"></div>
207
+ ```
208
+
209
+ 在应用的客户端模块中创建播放器:
210
+
211
+ ```ts
212
+ import { createPlayer } from "flowsvg/browser";
213
+ import "flowsvg/styles.css";
214
+ import { diagramDocument } from "./diagram.js";
215
+
216
+ const host = document.querySelector<HTMLElement>("#diagram");
217
+ if (!host) throw new Error("Diagram host is missing");
218
+
219
+ const player = createPlayer(host, diagramDocument, { theme: "light" });
220
+ player.play();
221
+ ```
222
+
223
+ `createPlayer` 替换容器子元素。卸载或复用容器前调用 `destroy()`。原生 HTML 使用包导入路径时,需配置 import map 或构建工具。
224
+
225
+ [原生示例](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/README.md)演示请求与响应传递,同一文档也可用于其余三种接入方式。
226
+
227
+ ## Web Component
228
+
229
+ 先在 HTML 中放置元素,再从客户端模块显式注册:
230
+
231
+ ```html
232
+ <flowsvg-player controls="false"></flowsvg-player>
233
+ ```
234
+
235
+ ```ts
236
+ import { defineFlowPlayer, type FlowPlayerElement } from "flowsvg/web-component";
237
+ import "flowsvg/styles.css";
238
+ import { diagramDocument } from "./diagram.js";
239
+
240
+ defineFlowPlayer();
241
+ const element = document.querySelector<FlowPlayerElement>("flowsvg-player");
242
+ if (element) element.document = diagramDocument;
243
+ ```
244
+
245
+ 默认注册 `<flowsvg-player>`,重复注册安全。对象赋给 `element.document`;JSON 字符串赋给 HTML `document` 属性;JSON URL 赋给 `src`。URL 相对页面解析,遵循浏览器 CORS 规则。
246
+
247
+ 支持 `autoplay`、`controls`、`speed`、`theme`、`reduced-motion`、`hover-fill`。默认显示控件,关闭自动播放与悬停背景;`controls="false"` 隐藏控件。主题、控件及动画/悬停选项变化时保留时间和播放状态;`autoplay` 则直接控制播放或暂停。移除元素会取消请求并销毁播放器,重新连接时使用保留的文档。
248
+
249
+ 方法:`play`、`pause`、`seek`、`step`、`setSpeed`、`getState`、`destroy`。更换文档使用 `element.document`。挂载前或销毁后,`getState()` 返回 `undefined`。播放与错误事件可冒泡并穿越 Shadow DOM 边界。
250
+
251
+ 请在 DOM 环境注册。无 DOM 导入安全,但该模块实例不能在稍后补上 DOM 后用于注册。
252
+
253
+ ## 文档与时间线
254
+
255
+ | 文档字段 | 约定 |
256
+ | --- | --- |
257
+ | `version` | 当前为 `1` |
258
+ | `viewport` | 有限且为正的 `width`、`height`,定义初始视口 |
259
+ | `nodes` | 节点列表,`id`、`kind`、`x`、`y`、`width`、`height` 必填 |
260
+ | `edges` | 连线列表,`id`、`from`、`to` 必填;端点引用节点或分组边界 |
261
+ | `scenes` | `[]` 表示静态图;场景具有唯一 `id`、`label` 和非空 `beats` |
262
+ | `title` / `theme` | 可选标题与 `light` / `dark` 文档主题 |
263
+
264
+ 节点与连线 ID 全局唯一。形状:`rect`、`roundedRect`、`ellipse`、`cylinder`、`group`。`parentId` 指向父分组,子节点坐标相对分组原点。标签为纯文本,`visible` 默认 `true`。节点样式:`fill`、`stroke`、`strokeWidth`、`textColor`、`dashed`(分组边框);连线样式:`stroke`、`strokeWidth`、`dashed`。颜色使用十六进制 RGB/RGBA,如 `#2563eb`、`#2563eb80`。
265
+
266
+ 路由:`straight`(默认)、`orthogonal`、`curved`。曲线跟随端口切线,不避让其他节点;正交路径避开可见普通节点,受阻时固定回退,不保证最短路径或零交叉。`fromPort` / `toPort` 可设为 `top`、`right`、`bottom`、`left`。显式正交端口保留向外线段,曲线端口决定切线。省略时按中心连线主方向选择,椭圆端点位于轮廓。普通标签按估算宽度换行;高度溢出时仍显示全文,并报告 `text-overflow`。
267
+
268
+ 连线可连接节点或分组边界。分组端点跟随当前几何和显式端口;祖先隐藏时遮蔽连线。传递仅高亮端点分组,不联动子节点。分组边框不作为布线障碍。
269
+
270
+ ### 固定节点内的节点内容
271
+
272
+ `rect`、`roundedRect`、`cylinder` 支持 `content`;椭圆和分组仅支持普通标签。`content` 与 `label` 互斥,空标签也算声明。`content.blocks` 按顺序排列 `title`、`paragraph`、`list`、`graph` 内容块。段落角色为 `body`(默认)或 `auxiliary`(辅助文字);空数组只显示形状。
273
+
274
+ ```ts
275
+ import type { DiagramNode } from "flowsvg";
276
+
277
+ const service = {
278
+ id: "service", kind: "rect", x: 24, y: 24, width: 260, height: 312,
279
+ content: {
280
+ style: { padding: 16, blockGap: 14 },
281
+ blocks: [
282
+ { type: "title", text: "Order service" },
283
+ { type: "paragraph", role: "auxiliary", text: "Core responsibilities" },
284
+ { type: "paragraph", text: "Validate and persist incoming orders." },
285
+ { type: "paragraph", text: "Return the order number.", style: { color: "#2563eb", align: "right" } }
286
+ ]
287
+ }
288
+ } satisfies DiagramNode;
289
+ ```
290
+
291
+ `service` 节点在浅色主题下的渲染结果:
292
+
293
+ ![固定尺寸的 Order service 节点,包含标题、辅助文字、正文与蓝色右对齐段落](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/node-content.png)
294
+
295
+ 文字字段接受字符串或 `ContentTextSpan[]`:`{ text, style?: { fontSize, fontWeight, color } }`。片段继承内容块样式并共用基线,对齐由内容块决定。每行使用最大字号;换行保留显式空行和完整 Unicode 字素,优先在英文空格处断行,必要时拆分长单词或 URL。跨片段的字素采用首字符样式。排版使用宽度估算,不测量 DOM。
296
+
297
+ 文字样式:有限正数 `fontSize`、100–900 整数 `fontWeight`、十六进制 `color`、`align`(`left`、`center`、`right`)。内容的 `padding`、`blockGap` 为非负有限数。默认值按图解单位计:标题 20/700、正文 14/400、辅助文字 12/400、内边距 16、块间距 14;行高为 `max(20, fontSize × 1.35)`。显式颜色覆盖主题默认值。
298
+
299
+ 列表接受 `marker`(默认 `none`,可选 `bullet`、`number`)、`items` 和 `style`。条目支持 `tags`、`status`、`body`、`auxiliary`、`style`、`auxiliaryStyle`、`statusStyle`。列表样式设置正文样式和非负有限数 `itemGap`(默认 12),条目正文样式可覆盖;辅助文字默认使用 12 单位弱化色。标记共用一列,与正文间隔 8 单位;正文续行与辅助文字对齐。标记对齐首个实际文字的基线。缺省条目/部分不占空间,显式空文字保留一行。
300
+
301
+ ```ts
302
+ import type { ContentBlock } from "flowsvg";
303
+
304
+ const steps: ContentBlock = { type: "list", marker: "number", style: { itemGap: 16 }, items: [
305
+ { body: "校验请求。", auxiliary: "输入检查" },
306
+ { body: [{ text: "保存", style: { fontWeight: 700 } }, { text: "订单。" }] }
307
+ ] };
308
+ ```
309
+
310
+ 将 `steps` 放入 300 × 180 的矩形,设置 `content: { blocks: [steps] }`:
311
+
312
+ ![包含辅助文字和加粗保存文字的两个编号步骤](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/numbered-list.zh-CN.png)
313
+
314
+ 文字标签接受 `{ text, style?: { fontSize, fontWeight, color, fill } }`。默认字号 11,水平/垂直内边距 6/2,圆角 3。列表 `tagGap` 默认 6,须为非负有限数。状态靠右,默认使用 12 单位强调色;`statusStyle` 支持 `fontSize`、`fontWeight`、`color`。宽度足够时标签与状态共用基线;否则先换行标签,再将状态放到下方。正文、辅助文字随后排列。显式颜色和底色覆盖主题默认值。
315
+
316
+ ```ts
317
+ import type { ContentBlock } from "flowsvg";
318
+
319
+ const records: ContentBlock = { type: "list", items: [{
320
+ tags: [{ text: "world" }, { text: "experience", style: { fill: "#eadfff", color: "#57319b" } }],
321
+ status: "new", body: "Alice joined Google", auxiliary: "Mar 2026"
322
+ }] };
323
+ ```
324
+
325
+ 将 `records` 放入 300 × 180 的矩形,设置 `content: { blocks: [records] }`:
326
+
327
+ ![记录包含 world 和 experience 标签、new 状态、正文与日期](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/record-tags.png)
328
+
329
+ 内容保持节点原尺寸。安全区在内边距之外额外避让:圆角矩形每侧 4 单位,圆柱上下各两倍端盖半径(`min(14, height / 4)`);宽高最小为零。SVG 按安全区裁剪,宽/高不足时报告带节点 ID、`nodes[index].content` 路径和方向的 `text-overflow`。裁剪文字不扩大命中范围,普通标签沿用独立的溢出规则。
330
+
331
+ `EvaluatedNode.contentLayout` 提供安全区、文字行/片段、标签底色、可选图形几何及宽高溢出标志。几何使用绝对坐标,跟随节点尺寸、父级移动、显隐和透明度。文字行保留块/条目/标签索引,`explicitColor`、`explicitFill` 标识作者配色。浏览器与静态 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)示例。
332
+
333
+ ### 节点内的实体关系小图
334
+
335
+ `graph` 内容块按作者指定的局部坐标绘制无向实体关系图:
336
+
337
+ ```ts
338
+ import type { ContentGraphBlock } from "flowsvg";
339
+
340
+ const entities: ContentGraphBlock = {
341
+ type: "graph", height: 130,
342
+ nodes: [
343
+ { id: "alice", label: "Alice", x: 120, y: 30, highlighted: true },
344
+ { id: "google", label: "Google", x: 50, y: 110, highlighted: true },
345
+ { id: "ml", label: "ML project", x: 190, y: 110 }
346
+ ],
347
+ edges: [{ from: "alice", to: "google" }, { from: "alice", to: "ml" }]
348
+ };
349
+ ```
350
+
351
+ 将 `entities` 放入 280 × 180 的矩形,设置 `content: { blocks: [entities] }`:
352
+
353
+ ![实体关系图中 Alice 和 Google 高亮,ML project 未选中](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/entity-graph.png)
354
+
355
+ `height` 预留固定正数高度。实体中心 `x`/`y` 是相对区域左上角的非负有限坐标。ID 仅在块内唯一,关系端点引用块内 ID。12 单位标签位于半径为 4 的圆点上方。高亮实体使用强调色圆点与粗体;关系两端均高亮时使用强调色。溢出按方向报告,不移动后续内容块。图形共用父节点裁剪与主题,交互命中父节点。
356
+
357
+ `contentLayout.graphs` 按 `blockIndex` 提供绝对几何和高亮状态,标签进入 `contentLayout.lines`。在节拍起点替换 `content` 可切换选中状态。参见 [Hindsight 示例](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/hindsight.html)。
358
+
359
+ ### 编写完整 Facts 时间线
360
+
361
+ 先确定尺寸,再排列内容块。记录用无标记列表,能力清单用圆点,步骤用编号。下例可用于全部四种接入方式:
362
+
363
+ ```ts
364
+ import { createDiagram, type DiagramDocument, type NodeContent } from "flowsvg";
365
+
366
+ const facts: NodeContent = { blocks: [
367
+ { type: "title", text: "Facts" },
368
+ { type: "paragraph", role: "auxiliary", text: "已确认的信息记录" },
369
+ { type: "list", marker: "none", items: [{
370
+ tags: [{ text: "world" }, { text: "订单", style: { fill: "#eadfff", color: "#57319b" } }],
371
+ status: "new",
372
+ body: [{ text: "订单已保存", style: { fontWeight: 700 } }, { text: " 2026-10-17", style: { fontSize: 11 } }],
373
+ auxiliary: "等待支付。"
374
+ }] }
375
+ ] };
376
+ const updated: NodeContent = { blocks: [
377
+ { type: "title", text: "Facts" },
378
+ { type: "list", items: [{ tags: [{ text: "verified" }], status: "done", body: "支付已确认", auxiliary: "2026-10-18" }] }
379
+ ] };
380
+ const document: DiagramDocument = {
381
+ version: 1, viewport: { width: 300, height: 480 },
382
+ nodes: [{ id: "facts", kind: "cylinder", x: 24, y: 20, width: 240, height: 440, content: facts }], edges: [],
383
+ scenes: [{ id: "records", label: "记录", beats: [
384
+ { durationMs: 1000, actions: [] },
385
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: updated } }] },
386
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { label: "已归档" } }] },
387
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: facts } }] },
388
+ { durationMs: 1000, actions: [{ type: "updateNode", target: "facts", value: { content: null } }] }
389
+ ] }]
390
+ };
391
+ const diagram = createDiagram(document);
392
+ console.log(diagram.evaluateAt(1000).warnings);
393
+ const svg = diagram.renderSvg(1000, { theme: "dark", reducedMotion: true });
394
+ ```
395
+
396
+ 左图为 0 ms 的浅色主题初始画面,右图为 1000 ms 的深色主题替换后画面:
397
+
398
+ ![Facts 时间线在记录替换前后的浅色与深色主题画面](https://cdn.jsdelivr.net/npm/flowsvg@0.1.1/docs/images/facts-timeline.zh-CN.png)
399
+
400
+ 分享前检查 `frame.warnings`,按需增大节点或精简文字;SDK 不自动调整尺寸或缩小字号。优先使用默认颜色保持主题与高亮下的对比度。内容只有节点级交互,不支持内部链接、按钮、图片或 HTML。
401
+
402
+ [内容时间线](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)也用于 Vue、React 示例。
403
+
404
+ 每个节拍包含正数 `durationMs` 和 `actions`,可选 `transitionMs` 与 `caption`。过渡时长默认 `min(250, durationMs)`,取值范围为 `[0, durationMs]`;`caption` 省略时继承前文,空字符串可清空。
405
+
406
+ | 动作 | 示例或可更新字段 |
407
+ | --- | --- |
408
+ | `show` / `hide` | `{ type: "show", target: "service" }`,作用于节点、分组或连线 |
409
+ | `highlight` / `unhighlight` | 改变高亮层,取消高亮后显示最新普通样式 |
410
+ | `flow` | `{ type: "flow", target: "request", direction: "forward" }`;在一个节拍内让标记沿连线移动。`direction` 为 `forward` 或 `reverse`,不会改变静态箭头方向;添加 `text: "请求"` 显示水平文字胶囊,省略 `text` 显示实心圆点。 |
411
+ | `updateNode` | `value` 支持 `x`、`y`、`width`、`height`、`label`、`content`、`fill`、`stroke`、`opacity` |
412
+ | `updateEdge` | `value` 支持 `label`、`stroke`、`strokeWidth`、`opacity` |
413
+ | `focus` | `target` 为节点/分组 ID;`null` 恢复完整视口 |
414
+
415
+ `updateNode.value.content` 在节拍起点整体替换内容,不受过渡时长影响,结果继承到后续节拍与场景。`label` 切换到普通标签,`content` 切换到节点内容;`content: null` 清空任一模式,不恢复旧标签或改变尺寸。同一节拍同时赋值两种模式或重复赋值会报告字段路径错误。
416
+
417
+ ```ts
418
+ // 每个 value 分别用于不同节拍的 updateNode 动作。
419
+ import type { NodeVisualState } from "flowsvg";
420
+
421
+ const replacement: NodeVisualState = { content: { blocks: [{ type: "title", text: "Updated" }] } };
422
+ const simpleLabel: NodeVisualState = { label: "Summary" };
423
+ const clearText: NodeVisualState = { content: null };
424
+ ```
425
+
426
+ 场景共用毫秒时间线,继承前一场景的最终状态。文字在节拍起点切换,插值属性在过渡期变化。节拍左闭右开,最终时间返回完成画面。隐藏节点/分组会遮蔽关联连线,重新显示时恢复各连线保存的显隐状态。
427
+
428
+ 每拍可在多条不同连线上各传递一次。标记共用 `durationMs` 内的线性进度,因此路径越长,移动越快。标记和端点临时高亮在拍末消失,下一拍提供新的一组传递;显式高亮和普通样式继续继承。
429
+
430
+ 传递 `text` 按纯文本转义,始终水平,胶囊宽度按固定规则估算。文字不替换连线标签,也不产生独立交互目标;空字符串仍显示胶囊。
431
+
432
+ ## API
433
+
434
+ ### 核心 API
435
+
436
+ 从 `flowsvg` 导入:
437
+
438
+ | API | 返回与行为 |
439
+ | --- | --- |
440
+ | `validateDocument(input: unknown)` | 返回 `{ path, message }[]`;空数组表示通过 |
441
+ | `createDiagram(document)` | 校验、复制并编译,返回 `Diagram`;非法文档抛出 `DiagramValidationError` |
442
+ | `renderSvg(document, timeMs = 0, options?)` | 一次性编译并导出静态 SVG 字符串 |
443
+ | `diagram.totalDuration` | 全局总时长,静态文档为 `0` |
444
+ | `diagram.evaluateAt(timeMs, options?)` | 返回 `DiagramFrameState` |
445
+ | `diagram.renderSvg(timeMs = 0, options?)` | 导出相同求值结果的 SVG |
446
+ | `diagram.hitTest(timeMs, point, options?)` | 在文档坐标中返回 `{ id, type: "node" \| "edge" }` 或 `null` |
447
+ | `wrapText(label, maxWidth, typography?)` | 按固定字宽估算换行,返回文本行数组 |
448
+
449
+ `EvaluateOptions`:`{ theme?: "light" | "dark"; reducedMotion?: boolean }`。时间限制在 `[0, totalDuration]`,非有限值按零处理。命中跳过隐藏/零透明度对象,依次检查普通节点、连线、分组背景。
450
+
451
+ `DiagramFrameState` 包含时间、场景/节拍 ID、进度、求值后的节点/连线、镜头、说明文字及警告。坐标为绝对坐标。静态图的场景/节拍 ID 为 `null`;节拍 ID 为 `${scene.id}:${beatIndex}`,索引从零开始。活动连线的 `flow` 提供方向、进度、位置及可选文字;拍末、隐藏或减少动画时不显示标记。
452
+
453
+ 类型及成员 JSDoc 随包提供:`DiagramDocument`、`DiagramNode`、`DiagramEdge`、`Scene`、`Beat`、`Action`、`Diagram`、`DiagramFrameState`、`EvaluateOptions`、`ValidationIssue`、`LayoutWarning`。
454
+
455
+ ### 播放器 API
456
+
457
+ 从 `flowsvg/browser` 导入 `createPlayer(host, document, options?)`。`PlayerOptions` 包含核心视觉选项以及 `controls?: boolean` 和 `hoverFill?: boolean`。
458
+
459
+ | 方法或属性 | 行为 |
460
+ | --- | --- |
461
+ | `element` | 创建时传入的宿主元素,也是事件目标 |
462
+ | `play()` / `pause()` | 播放或保留当前画面并暂停;静态图或终点不会开始播放 |
463
+ | `seek(timeMs)` | 限制到时间线范围;保留播放状态,抵达终点时暂停 |
464
+ | `step(1)` / `step(-1)` | 暂停并跳到相邻节拍起点;节拍中途后退到本拍起点,在起点后退到上一拍 |
465
+ | `setSpeed(speed)` | 设置有限正数倍率,忽略非法值 |
466
+ | `load(document)` | 成功后暂停并回到零;校验失败时发出 error,保留原文档、时间和播放状态 |
467
+ | `getState()` | 返回 `PlayerState`:`timeMs`、`totalDuration`、`playing`、`speed`、`frame` |
468
+ | `destroy()` | 幂等清理 DOM、监听器与调度;之后停止事件,状态仍可读取 |
469
+
470
+ 创建时暂停在零时刻,默认显示控件。页面隐藏时暂停,恢复后保持暂停;播放在终点停止,静态图禁用时间控件。框架实例提供表中方法,但不暴露 `element` 和 `destroy`。
471
+
472
+ 悬停形状、标签或内容时高亮当前节点边框。`hoverFill: true` 同时高亮背景与内容,也适用于分组。移出只解除悬停高亮,活动的传递/显式高亮仍保留。画面更新会重新检查未移动的指针;指针取消、文档成功替换和销毁会清除悬停。
473
+
474
+ 默认内容颜色跟随主题与高亮底色,作者配色和标签底色保持原值。高亮不改变布局或命中范围。内容交互返回父节点,裁剪文字不增加命中区域。
475
+
476
+ `PlayerState.frame` 和静态 SVG 不含悬停;没有悬停时,浏览器输出与同时间导出一致。
477
+
478
+ ### 事件
479
+
480
+ 原生播放器在宿主元素上派发 `CustomEvent`:
481
+
482
+ | 事件 | `detail` |
483
+ | --- | --- |
484
+ | `timeupdate` | 当前 `PlayerState`,画面更新后发送 |
485
+ | `stepchange` | 当前 `PlayerState`,场景或节拍改变时发送,早于该次 timeupdate |
486
+ | `playstatechange` | 当前 `PlayerState`,播放/暂停状态变化时发送 |
487
+ | `error` | `PlayerErrorDetail`:保留的播放器状态与 `error` |
488
+ | `hover` / `click` | `PlayerInteractionDetail` 或 `null`;也提供 `objecthover` / `objectclick` 同义事件 |
489
+
490
+ 交互 detail 包含 `id`、`type`、`point`(文档坐标)、`timeMs`、`sceneId`、`beatId`,未命中为 `null`。包装层转发四类播放/错误事件;Web Component 挂载前的错误可能没有播放器状态。
491
+
492
+ 指针移动会发送 `hover`;画面更新导致未移动的指针命中其他对象时也发送。移出或取消时发送 `null`。
493
+
494
+ ```ts
495
+ import type { PlayerEventMap } from "flowsvg/browser";
496
+
497
+ function handleTimeUpdate(event: Event): void {
498
+ const { detail } = event as PlayerEventMap["timeupdate"];
499
+ console.log(detail.timeMs, detail.frame.caption);
500
+ }
501
+
502
+ player.element.addEventListener("timeupdate", handleTimeUpdate);
503
+ ```
504
+
505
+ 上例沿用原生集成创建的 `player`。宿主自行添加的监听器由宿主移除。
506
+
507
+ ## 主题与无障碍
508
+
509
+ 主题:显式选项 → 文档主题 → 浅色,核心不读取系统偏好。浏览器动画默认跟随 `prefers-reduced-motion`,核心默认正常动画。减少动画会立即完成过渡并隐藏传递标记,保留总时长。
510
+
511
+ 默认样式使用中性边框与连线,以蓝色突出活动对象:
512
+
513
+ | 颜色 | 浅色 | 深色 |
514
+ | --- | --- | --- |
515
+ | 背景 / 节点填充 | `#ffffff` / `#ffffff` | `#101828` / `#1d2939` |
516
+ | 节点边框 / 文字 | `#cfd8e3` / `#1d2939` | `#475467` / `#f2f4f7` |
517
+ | 连线 | `#8892a4` | `#98a2b3` |
518
+ | 高亮填充 / 边框 | `#eaf1ff` / `#2a63c8` | `#233b60` / `#8ab4f8` |
519
+ | 传递标记 / 标记文字 | `#2a63c8` / `#ffffff` | `#8ab4f8` / `#101828` |
520
+
521
+ 默认节点边宽为 1.2 图解单位,连线边宽为 2,圆角矩形的圆角半径上限为 8。节点标签使用无描边文字;连线标签保留背景描边,隔开穿过文字的路径。图解文字使用系统字体,并提供中文字体回退。显式样式替换普通默认值;时间线和悬停高亮叠加在普通样式上,解除高亮后恢复当前普通颜色。
522
+
523
+ 导入 `flowsvg/styles.css` 可获得控件布局与焦点样式。CSS 变量只定制控件,图解本身颜色由核心主题和文档样式决定:
524
+
525
+ ```css
526
+ .flowsvg {
527
+ --flowsvg-control-background: #101828;
528
+ --flowsvg-control-color: #f4f7fb;
529
+ --flowsvg-control-border: #64748b;
530
+ --flowsvg-control-accent: #60a5fa;
531
+ --flowsvg-control-focus: #fbbf24;
532
+ }
533
+ ```
534
+
535
+ 原生宿主添加 `flowsvg` 类即可应用变量,框架组件自动添加。画布聚焦后,Space 播放/暂停,方向键逐拍移动;表单控件保留原生按键。说明文字使用礼貌播报区,SVG 提供可访问名称。
536
+
537
+ ## 校验与错误
538
+
539
+ 校验拒绝未知字段、错误引用、循环分组、重复节拍赋值、非法颜色和尺寸/时长。文本按纯文本转义。检查外部 JSON 时,调用 `validateDocument` 并读取各条 `path`、`message`。
540
+
541
+ `createDiagram` 和首次 `createPlayer` 遇到非法文档会抛出 `DiagramValidationError`,`issues` 包含字段路径。后续 `load` 失败时发送 `error`,保留旧状态。文字溢出不阻止渲染,可读取 `frame.warnings`。
542
+
543
+ 同一连线重复声明 `flow` 时,后一 `target` 报告 `duplicate beat assignment`,与方向、文字及其他字段错误无关。传递可与同一连线的显隐、显式高亮和样式动作共存。
544
+
545
+ ## License
546
+
547
+ [MIT](LICENSE)。可使用、修改及分发;保留许可证与版权声明。
@@ -0,0 +1,48 @@
1
+ /**
2
+ * React adapter for component lifecycle, props, events and imperative controls.
3
+ * Playback/rendering reuse browser; imports do not create DOM, and the player is created only on mount.
4
+ */
5
+ import { type CSSProperties } from "react";
6
+ import { type PlayerErrorDetail, type PlayerState } from "../browser/player.js";
7
+ import type { DiagramDocument, ThemeName } from "../core/model.js";
8
+ /** React document, visual options and callbacks; document reference changes trigger a reload. */
9
+ export interface FlowPlayerProps {
10
+ document: DiagramDocument;
11
+ /** Positive playback multiplier; omit to use the player default. */
12
+ speed?: number;
13
+ theme?: ThemeName;
14
+ controls?: boolean;
15
+ /** Defaults to false: hover marks borders only. True also highlights backgrounds, including groups. */
16
+ hoverFill?: boolean;
17
+ reducedMotion?: boolean;
18
+ /** Initial position in milliseconds, applied once; use seek afterward, and document reloads reset to zero. */
19
+ initialTimeMs?: number;
20
+ className?: string;
21
+ style?: CSSProperties;
22
+ onStepChange?: (state: PlayerState) => void;
23
+ onTimeUpdate?: (state: PlayerState) => void;
24
+ onPlayStateChange?: (state: PlayerState) => void;
25
+ onError?: (detail: PlayerErrorDetail) => void;
26
+ }
27
+ /** Imperative controls for a mounted component; calls before mounting or after unmounting throw a not-mounted error. */
28
+ export interface FlowPlayerHandle {
29
+ /** Resume from the current position when playback can advance. */
30
+ play(): void;
31
+ /** Pause while retaining the current frame. */
32
+ pause(): void;
33
+ /** Seek in milliseconds, clamping to the timeline; non-finite values become zero. */
34
+ seek(timeMs: number): void;
35
+ /** Pause and jump to a beat boundary; backward from inside a beat goes to that beat's start. */
36
+ step(direction: 1 | -1): void;
37
+ /** Set a finite positive multiplier; ignore invalid values. */
38
+ setSpeed(speed: number): void;
39
+ /** Reset/pause at zero on success; emit an error and retain playback state if validation fails. */
40
+ load(document: DiagramDocument): void;
41
+ /** Read the current browser-player snapshot. */
42
+ getState(): PlayerState;
43
+ }
44
+ /**
45
+ * SSR renders only a host container; an effect mounts the browser player and unmount cleanup releases it.
46
+ * Document reference changes call load; visual/control options rebuild the instance, with initial positioning applied once.
47
+ */
48
+ export declare const FlowPlayer: import("react").ForwardRefExoticComponent<FlowPlayerProps & import("react").RefAttributes<FlowPlayerHandle>>;