flowsvg 0.1.0 → 0.2.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.
Files changed (98) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/PATTERNS.md +71 -4
  3. package/README.md +148 -438
  4. package/README.zh-CN.md +148 -438
  5. package/dist/adapters/react.d.ts +55 -0
  6. package/dist/adapters/react.js +138 -0
  7. package/dist/adapters/vue.d.ts +158 -0
  8. package/dist/adapters/vue.js +177 -0
  9. package/dist/adapters/web-component.d.ts +111 -0
  10. package/dist/adapters/web-component.js +425 -0
  11. package/dist/{player-controls.d.ts → browser/controls.d.ts} +7 -3
  12. package/dist/{player-controls.js → browser/controls.js} +23 -31
  13. package/dist/browser/export-controls.d.ts +32 -0
  14. package/dist/browser/export-controls.js +151 -0
  15. package/dist/browser/player.d.ts +77 -0
  16. package/dist/browser/player.js +412 -0
  17. package/dist/browser/select-control.d.ts +33 -0
  18. package/dist/browser/select-control.js +171 -0
  19. package/dist/browser.d.ts +5 -69
  20. package/dist/browser.js +3 -353
  21. package/dist/core/color.d.ts +3 -0
  22. package/dist/core/color.js +18 -0
  23. package/dist/core/diagram.d.ts +40 -0
  24. package/dist/core/diagram.js +481 -0
  25. package/dist/core/edge-geometry.d.ts +45 -0
  26. package/dist/core/edge-geometry.js +609 -0
  27. package/dist/{core.d.ts → core/model.d.ts} +37 -61
  28. package/dist/core/model.js +1 -0
  29. package/dist/core/node-content-measurement.d.ts +34 -0
  30. package/dist/core/node-content-measurement.js +66 -0
  31. package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
  32. package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
  33. package/dist/{node-content.d.ts → core/node-content.d.ts} +18 -1
  34. package/dist/{node-content.js → core/node-content.js} +30 -6
  35. package/dist/core/svg-number.d.ts +5 -0
  36. package/dist/core/svg-number.js +7 -0
  37. package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
  38. package/dist/core/theme.js +17 -0
  39. package/dist/{timeline.d.ts → core/timeline.d.ts} +11 -5
  40. package/dist/{timeline.js → core/timeline.js} +30 -22
  41. package/dist/core/validation.d.ts +11 -0
  42. package/dist/core/validation.js +399 -0
  43. package/dist/export/browser.d.ts +9 -0
  44. package/dist/export/browser.js +172 -0
  45. package/dist/export/captions.d.ts +18 -0
  46. package/dist/export/captions.js +44 -0
  47. package/dist/export/plan.d.ts +25 -0
  48. package/dist/export/plan.js +94 -0
  49. package/dist/export/types.d.ts +64 -0
  50. package/dist/export/types.js +9 -0
  51. package/dist/export/worker-source.d.ts +1 -0
  52. package/dist/export/worker-source.js +1 -0
  53. package/dist/export.d.ts +3 -0
  54. package/dist/export.js +2 -0
  55. package/dist/index.d.ts +7 -1
  56. package/dist/index.js +3 -1
  57. package/dist/node/cli.d.ts +2 -0
  58. package/dist/node/cli.js +72 -0
  59. package/dist/node/gif.d.ts +16 -0
  60. package/dist/node/gif.js +184 -0
  61. package/dist/node/html.d.ts +19 -0
  62. package/dist/node/html.js +61 -0
  63. package/dist/node/output.d.ts +2 -0
  64. package/dist/node/output.js +22 -0
  65. package/dist/node.d.ts +6 -0
  66. package/dist/node.js +3 -0
  67. package/dist/portable/LICENSE.esbuild.txt +21 -0
  68. package/dist/portable/LICENSE.flowsvg.txt +21 -0
  69. package/dist/portable/LICENSE.gifenc.txt +21 -0
  70. package/dist/portable/export.js +5 -0
  71. package/dist/portable/manifest.json +36 -0
  72. package/dist/portable/player.js +2 -0
  73. package/dist/portable/styles.css +227 -0
  74. package/dist/react.d.ts +3 -48
  75. package/dist/react.js +2 -126
  76. package/dist/styles.css +194 -14
  77. package/dist/vue.d.ts +3 -132
  78. package/dist/vue.js +2 -161
  79. package/dist/web-component.d.ts +3 -103
  80. package/dist/web-component.js +2 -396
  81. package/docs/images/entity-graph.png +0 -0
  82. package/docs/images/facts-timeline.png +0 -0
  83. package/docs/images/facts-timeline.zh-CN.png +0 -0
  84. package/docs/images/flowsvg-wordmark.gif +0 -0
  85. package/docs/images/node-content.png +0 -0
  86. package/docs/images/numbered-list.png +0 -0
  87. package/docs/images/numbered-list.zh-CN.png +0 -0
  88. package/docs/images/quick-start.png +0 -0
  89. package/docs/images/record-tags.png +0 -0
  90. package/package.json +38 -6
  91. package/dist/core.js +0 -1149
  92. package/dist/theme.js +0 -6
  93. /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
  94. /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
  95. /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
  96. /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
  97. /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
  98. /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
package/README.md CHANGED
@@ -1,525 +1,235 @@
1
- # flowsvg
1
+ <p align="center">
2
+ <img src="https://cdn.jsdelivr.net/npm/flowsvg@0.2.0/docs/images/flowsvg-wordmark.gif" alt="FlowSVG" width="280">
3
+ </p>
2
4
 
3
- English | [简体中文](README.zh-CN.md)
5
+ <p align="center">Declarative SVG diagrams with timeline playback and SVG/GIF export.</p>
4
6
 
5
- flowsvg (流形) is a TypeScript ESM SDK for declarative SVG diagram playback. Describe logic and architecture diagrams with nodes, edges, scenes, and beats in JSON or TypeScript objects, then play the diagram in a browser or export an SVG at a specific time without a DOM.
7
+ <p align="center">English | <a href="https://cdn.jsdelivr.net/npm/flowsvg@0.2.0/README.zh-CN.md">简体中文</a></p>
6
8
 
7
- The core handles validation, time evaluation, geometry, and SVG output. Browser and framework adapters handle playback, interaction, and lifecycle. The same document, time, and visual options produce the same core frame. Node hover adds temporary browser feedback.
9
+ <p align="center">
10
+ <a href="#define-a-diagram">Quick&nbsp;start</a> ·
11
+ <a href="#control-playback">Playback&nbsp;API</a> ·
12
+ <a href="#core-api--flowsvg">Core&nbsp;API</a> ·
13
+ <a href="#document-and-actions">Document&nbsp;model</a> ·
14
+ <a href="#export-api">Export&nbsp;API</a>
15
+ </p>
8
16
 
9
- [Installation](#installation) · [Quick start](#quick-start) · [Vue](#vue-integration) · [React](#react-integration) · [Native browser](#native-browser) · [Web Component](#web-component) · [API](#api) · [Development](#development-and-verification) · [License](#license)
10
-
11
- ## Installation
12
-
13
- Install from npm:
14
-
15
- ```sh
16
- pnpm add flowsvg
17
- ```
18
-
19
- Install only the framework for your chosen entry:
20
-
21
- ```sh
22
- # Vue applications
23
- pnpm add vue
24
- # React applications
25
- pnpm add react react-dom
26
- # React TypeScript applications
27
- pnpm add -D @types/react @types/react-dom
28
- ```
29
-
30
- The core and native browser entries require neither framework. To build a local tarball before publication, run from the repository root:
17
+ Define the data once and use it in native browser, Vue, React, or Web Component integrations.
31
18
 
32
19
  ```sh
33
- pnpm install --frozen-lockfile
34
- pnpm --filter flowsvg exec npm pack --pack-destination /tmp
20
+ npm install flowsvg
35
21
  ```
36
22
 
37
- The packaging hook builds the SDK. Install the resulting `0.1.0` tarball with `pnpm add /tmp/flowsvg-0.1.0.tgz`.
38
-
39
- The core requires Node.js 18+ or an environment supporting ES2022 ESM. Browser playback requires a DOM, SVG, and `requestAnimationFrame`. Minimum framework versions are Vue 3.5 and React 18. TypeScript declarations are included; React TypeScript applications also need the matching `@types/react` and `@types/react-dom` packages.
40
-
41
- | Import path | Purpose |
42
- | --- | --- |
43
- | `flowsvg` | DOM-free document validation, compilation, evaluation, hit testing, and SVG export |
44
- | `flowsvg/browser` | Native browser player |
45
- | `flowsvg/vue` | Vue component and handle types |
46
- | `flowsvg/react` | React component and ref types |
47
- | `flowsvg/web-component` | Explicit custom element registration; also available as `flowsvg/element` |
48
- | `flowsvg/styles.css` | Control layout and visible focus styles |
49
-
50
- ## Quick start
51
-
52
- ### 1. Create a diagram document
53
-
54
- Save this as `diagram.ts`. The integration examples below reuse this file:
23
+ ## Define a diagram
55
24
 
56
25
  ```ts
26
+ // diagram.ts
57
27
  import type { DiagramDocument } from "flowsvg";
58
28
 
59
- export const diagramDocument: DiagramDocument = {
29
+ export const diagram: DiagramDocument = {
60
30
  version: 1,
61
- title: "Request flow",
62
- viewport: { width: 640, height: 280 },
31
+ viewport: { width: 640, height: 240 },
63
32
  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 }
33
+ { id: "client", kind: "roundedRect", x: 40, y: 90, width: 140, height: 64, label: "Client" },
34
+ { id: "service", kind: "roundedRect", x: 400, y: 90, width: 180, height: 64, label: "Service" }
66
35
  ],
67
- edges: [{ id: "request", from: "client", to: "service", label: "HTTP", route: "straight" }],
36
+ edges: [{ id: "request", from: "client", to: "service", route: "orthogonal" }],
68
37
  scenes: [{
69
- id: "request-flow",
70
- label: "Request reaches the service",
38
+ id: "request-response",
39
+ label: "Request and response",
71
40
  beats: [
72
- { durationMs: 1000, transitionMs: 250, caption: "Show the service", actions: [{ type: "show", target: "service" }] },
73
- { durationMs: 1000, caption: "The service handles the request", actions: [{ type: "highlight", target: "service" }] }
41
+ { durationMs: 1000, caption: "Send request", actions: [
42
+ { type: "flow", target: "request", direction: "forward", text: "Request" }
43
+ ] },
44
+ { durationMs: 1000, caption: "Return response", actions: [
45
+ { type: "flow", target: "request", direction: "reverse" }
46
+ ] }
74
47
  ]
75
48
  }]
76
49
  };
77
50
  ```
78
51
 
79
- ### 2. Evaluate and export SVG
52
+ Nodes use explicit positions and dimensions. Routes are `straight`, `orthogonal` (rounded bends), or `curved`. Flow text produces a capsule; omitting text produces a moving dot.
53
+
54
+ ## Play
55
+
56
+ ```html
57
+ <div id="diagram"></div>
58
+ ```
80
59
 
81
60
  ```ts
82
- import { createDiagram, renderSvg } from "flowsvg";
83
- import { diagramDocument } from "./diagram.js";
61
+ import { createPlayer } from "flowsvg/browser";
62
+ import "flowsvg/styles.css";
63
+ import { diagram } from "./diagram.js";
84
64
 
85
- const diagram = createDiagram(diagramDocument);
86
- const frame = diagram.evaluateAt(125);
87
- console.log(diagram.totalDuration, frame.sceneId, frame.warnings);
65
+ const player = createPlayer(document.querySelector<HTMLElement>("#diagram")!, diagram, {
66
+ theme: "light",
67
+ loop: true,
68
+ gifExport: true
69
+ });
88
70
 
89
- const svg = diagram.renderSvg(125, { theme: "dark" });
90
- const finalSvg = renderSvg(diagramDocument, 2000);
71
+ player.play();
72
+ // On disposal: player.destroy();
91
73
  ```
92
74
 
93
- `svg` and `finalSvg` are complete static SVG strings, ready to save to a file or preview. Reuse the result of `createDiagram` for repeated queries; use `renderSvg` for a single export. Choose one of the integrations below for browser playback.
75
+ Players start paused, with playback controls enabled. Loop and GIF downloads default off. Import the CSS once. Call `destroy()` when disposing of a native player; framework components handle cleanup.
94
76
 
95
- ## Vue integration
96
-
97
- Create a component in your Vue application, with `diagram.ts` in the same directory:
77
+ ### Vue
98
78
 
99
79
  ```vue
100
80
  <script setup lang="ts">
101
- import { ref } from "vue";
102
- import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/vue";
103
- import type { PlayerErrorDetail } from "flowsvg/browser";
81
+ import { FlowPlayer } from "flowsvg/vue";
104
82
  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
- }
83
+ import { diagram } from "./diagram.js";
113
84
  </script>
114
85
 
115
86
  <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()">Play</button>
125
- <button type="button" @click="player?.seek(0)">Back to start</button>
126
- <output>{{ timeMs }} ms</output>
87
+ <FlowPlayer :document="diagram" loop gif-export />
127
88
  </template>
128
89
  ```
129
90
 
130
- The component destroys the player on unmount. Use `player.value` to call handle methods in script; templates unwrap the ref automatically. Events are `stepchange`, `timeupdate`, `playstatechange`, and `error`, with the event detail passed directly as the argument. Corresponding callback props are also available.
131
-
132
- ## React integration
91
+ ### React
133
92
 
134
93
  ```tsx
135
- import { useRef } from "react";
136
- import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/react";
94
+ import { FlowPlayer } from "flowsvg/react";
137
95
  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()}>Play</button>
153
- <button type="button" onClick={() => player.current?.seek(0)}>Back to start</button>
154
- </section>
155
- );
96
+ import { diagram } from "./diagram.js";
97
+
98
+ export function Diagram() {
99
+ return <FlowPlayer document={diagram} loop gifExport />;
156
100
  }
157
101
  ```
158
102
 
159
- The component releases resources on unmount. Callbacks are `onStepChange`, `onTimeUpdate`, `onPlayStateChange`, and `onError`.
103
+ ## Control playback
160
104
 
161
- ### Vue / React props and updates
105
+ `createPlayer(host: HTMLElement, document: DiagramDocument, options?: PlayerOptions): FlowPlayer` mounts into the host, replacing its children. Invalid initial documents throw `DiagramValidationError`.
162
106
 
163
- | Prop | Default or behavior |
107
+ | Method / property | Behavior |
164
108
  | --- | --- |
165
- | `document` | Required diagram document; a new object reference triggers a reload, pausing at zero on success |
166
- | `speed` | Positive playback multiplier, default `1`; changes update the speed |
167
- | `theme` | `light` / `dark`; falls back to the document theme, then light |
168
- | `controls` | Default `true`; set to `false` to hide built-in controls |
169
- | `reducedMotion` | Follows the browser's reduced-motion preference when omitted |
170
- | `hoverFill` | Default `false`: hover highlights the border only; set `true` to also highlight the background and content |
171
- | `initialTimeMs` | Applied only on initial mount; use `seek` for later changes |
172
- | `className` / `style` | Host container styles |
173
-
174
- Changing `theme`, `controls`, `reducedMotion`, or `hoverFill` recreates the player, paused at zero; `initialTimeMs` is not reapplied. Provide a new object reference when changing the document. The ref / exposed handle provides the seven control and state methods in the Player API below; the component owns cleanup. Calling a retained handle before mounting or after unmounting throws a not-mounted error.
175
-
176
- Both framework entries are safe to import on the server. SSR renders a host container, and the player is created after client mounting. Use the core `renderSvg` for a server-rendered diagram preview.
109
+ | `play(): void` / `pause(): void` | Resume / pause at the current position. At the end, `play()` restarts only when Loop is enabled; static diagrams cannot play. |
110
+ | `seek(timeMs: number): void` | Clamp to the timeline; retain playback except at the end. Non-finite values become zero. |
111
+ | `step(direction: 1 \| -1): void` | Pause and move to a beat boundary. Backward from inside a beat goes to that beat's start. |
112
+ | `setSpeed(speed: number): void` | Set a finite positive multiplier; ignore invalid values. Default `1`. |
113
+ | `setLoop(loop: boolean): void` | Change whole-timeline looping without changing position or playback. |
114
+ | `load(document: DiagramDocument): void` | Pause at zero on success; emit `error` and retain the old document/state on failure. |
115
+ | `getState(): PlayerState` | Read `{ timeMs, totalDuration, playing, loop, speed, frame }`. Times are milliseconds. |
116
+ | `destroy(): void` | Release DOM, listeners and animation resources. Safe to repeat. Not exposed by Vue/React refs. |
117
+ | `element: HTMLElement` | Readonly host and event target. Not exposed by Vue/React refs. |
177
118
 
178
- ## Native browser
179
-
180
- Add a container to your page:
181
-
182
- ```html
183
- <div id="diagram" style="width: 100%; height: 360px;"></div>
184
- ```
119
+ Vue/React refs expose the eight playback/state methods above. Replace the `document` object to reload a component. Changes to `theme`, `controls`, `gifExport`, `reducedMotion`, or `hoverFill` recreate the player paused at zero.
185
120
 
186
- Create the player in a client-side application module:
121
+ ### Options and component props
187
122
 
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` takes over the container and replaces its children. Call `player.destroy()` when the page or component is disposed, and destroy the old instance before creating another player in the same container. Bare imports in plain HTML need an import map or application build tooling; the repository's native example includes an import map.
201
-
202
- The repository example at `apps/native-example/` keeps the camera and layout fixed throughout an order request and response. At 0–2400 ms, three curved edges carry a dot, a `Token` capsule, and a reverse precheck capsule. At 2400–4800 ms, two other curves carry the order request and inventory confirmation. At 4800–7200 ms, response messages return to the gateway and client; the final beat ends at 8200 ms with no temporary highlights. Opposite relationship edges remain separate, and hovering a participant adds only a browser highlight.
203
-
204
- Reuse `apps/native-example/request-chain.js` as the `document` input for native, Web Component, React, or Vue integration. Each host uses the same `seek(timeMs)`, `setSpeed(speed)`, `play()`, and `pause()` behavior. `src/business-integration.test.ts` verifies this shared example through all four public entries, including both themes, reduced motion, beat boundaries, and core SVG equality.
205
-
206
- ## Web Component
207
-
208
- Place the element in HTML, then register it explicitly from a client-side module:
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
- The default element name is `flowsvg-player`; repeated registration is safe. Pass objects through the `document` property. The HTML `document` attribute accepts a JSON string; `src` accepts a JSON URL resolved relative to the page. URL requests follow browser same-origin and CORS rules.
225
-
226
- Supported options are `autoplay`, `controls`, `speed`, `theme`, `reduced-motion`, and `hover-fill`. Hover fill is off by default; set `hover-fill` to enable background and content feedback. Use `controls="false"` to hide the controls, which are visible by default. Autoplay is off by default. Option changes preserve the current time and playing/paused state. Removing the element cleans up the player and pending requests; reconnecting can mount it again from its retained document.
227
-
228
- The element exposes `play`, `pause`, `seek`, `step`, `setSpeed`, `getState`, and `destroy`. Replace the document through the `document` property. Before mounting, `getState()` returns `undefined`. The element forwards the four playback/error event types as bubbling, composed CustomEvents that can cross Shadow DOM boundaries.
229
-
230
- Load this entry in a DOM environment for actual registration. Importing without a DOM is safe, but module evaluation fixes the HTMLElement base class; the same module instance imported without a DOM cannot register directly after a DOM is added later.
231
-
232
- ## Documents and timelines
233
-
234
- | Document field | Contract |
123
+ | `PlayerOptions` | Default / meaning |
235
124
  | --- | --- |
236
- | `version` | Currently `1` |
237
- | `viewport` | Finite positive `width` and `height`, defining the initial viewport |
238
- | `nodes` | Node array; `id`, `kind`, `x`, `y`, `width`, and `height` are required |
239
- | `edges` | Edge array; `id`, `from`, and `to` are required; endpoints reference nodes or group boundaries |
240
- | `scenes` | `[]` creates a static diagram; each scene has a unique `id`, a `label`, and nonempty `beats` |
241
- | `title` / `theme` | Optional title and `light` / `dark` document theme |
125
+ | `theme?: "light" \| "dark"` | Document theme, then `"light"`. |
126
+ | `controls?: boolean` | `true`; show playback controls. |
127
+ | `loop?: boolean` | `false`; loop the entire timeline. |
128
+ | `gifExport?: boolean` | `false`; show GIF download controls when `controls` is enabled. |
129
+ | `hoverFill?: boolean` | `false`; hover marks borders only. `true` also highlights backgrounds. |
130
+ | `reducedMotion?: boolean` | Follow browser preference; `true` hides flow markers and completes visual transitions immediately without changing timing. |
242
131
 
243
- Node and edge IDs are globally unique. Node shapes are `rect`, `roundedRect`, `ellipse`, `cylinder`, and `group`. A node's `parentId` references its parent group; child coordinates are relative to the group origin. Labels are plain text; `visible` defaults to `true`. Node `style` supports `fill`, `stroke`, `strokeWidth`, `textColor`, and `dashed` (used for group borders); edge `style` supports `stroke`, `strokeWidth`, and `dashed`. Colors use hexadecimal RGB/RGBA, such as `#2563eb` or `#2563eb80`.
244
-
245
- Edge `route` is `straight` by default, `orthogonal`, or `curved`. Curved routes are smooth cubic paths generated from endpoint ports; they do not auto-avoid other nodes. Orthogonal paths avoid visible ordinary nodes, with a deterministic fallback when no route is found; shortest paths and zero crossings are not guaranteed. Use `fromPort` and `toPort` (`top`, `right`, `bottom`, or `left`) when a specific side is required. Explicit orthogonal ports reserve outward endpoint segments and route around endpoint bodies; curved ports set endpoint tangents. Omitted ports are selected from the dominant center-to-center axis, and ellipse ports use the ellipse contour. Simple labels wrap using deterministic width estimates. Insufficient node height preserves the full label and emits a `text-overflow` warning.
246
-
247
- Edges may connect ordinary nodes, groups, or one of each. A group endpoint uses the evaluated group rectangle, including nested movement and explicit ports. Hiding an endpoint group or its ancestor masks the edge. A transfer highlights the endpoint group itself; descendants keep their own highlight state. Group frames remain excluded from routing obstacles.
248
-
249
- ### Fixed-size node content
250
-
251
- A `rect`, `roundedRect`, or `cylinder` node may declare `content` instead of `label`. Ellipses and groups accept simple labels only. Declaring both fields, including an empty label, is invalid. `content.blocks` is an ordered array of `title`, `paragraph`, `list` and `graph` blocks. Paragraph `role` is `body` (default) or `auxiliary`. An empty block array displays only the shape. Import the `DiagramNode` type from `flowsvg` for this example:
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
- ```
132
+ Vue/React `FlowPlayerProps` also accept required `document: DiagramDocument`, `speed?: number` (default `1`), `initialTimeMs?: number` (applied once), `className`, and `style`. Both entries export `FlowPlayerHandle` and `FlowPlayerProps`. Native players start through `play()` and change speed through `setSpeed()`.
267
133
 
268
- Title/paragraph `text` and list-item `body`/`auxiliary`/`status`/tag `text` accept a string or `ContentTextSpan[]`. Each span is `{ text, style?: { fontSize, fontWeight, color } }`; alignment stays on the block. For example: `text: [{ text: "Saved", style: { fontWeight: 700 } }, { text: " 2026-10-17", style: { fontSize: 12 } }]`. Spans inherit block defaults and share each line's baseline. Line height uses that line's largest font, and style boundaries do not force line breaks. Wrapping prefers English spaces, then breaks CJK, long words and URLs between graphemes. `Intl.Segmenter` preserves combining accents, skin-tone emoji, ZWJ sequences, flags and keycaps. A grapheme crossing source spans uses its first character's style. Layout does not read DOM measurements or font-loading state.
269
-
270
- Each block accepts `style.fontSize` (finite and positive), `fontWeight` (integer 100–900), hexadecimal `color`, and `align` (`left`, `center`, `right`). Content style accepts nonnegative finite `padding` and `blockGap`. Sizes and spacing use diagram units. Defaults are title 20/700, body 14/400, auxiliary 12/400, padding 16, block gap 14, and line height at least 20 or font size × 1.35. Auxiliary text uses a muted theme color; explicit content colors retain their authored values.
271
-
272
- A list is `{ type: "list", marker?: "none" | "bullet" | "number", items: ContentListItem[], style?: ContentListStyle }`. The default marker is `none`. Each item accepts optional `tags`, `status`, `body`, `auxiliary`, body `style`, `auxiliaryStyle`, and `statusStyle`. List `style` supplies body typography and a nonnegative finite `itemGap` (default 12). Item body styles override the list's body styles; auxiliary text keeps its muted 12-unit defaults unless `auxiliaryStyle` overrides them. Marked lists reserve one common marker column, including multi-digit numbers, then an 8-unit gap. Body continuation lines and auxiliary text use the same body column. Markers align with the first present text part's baseline. Absent parts and completely absent entries consume no height or item gap; explicit empty strings and blank lines still consume authored lines. For example:
273
-
274
- ```ts
275
- { type: "list", marker: "number", style: { itemGap: 16 }, items: [
276
- { body: "Validate the request.", auxiliary: "Input checks" },
277
- { body: [{ text: "Save", style: { fontWeight: 700 } }, { text: " the order." }] }
278
- ] }
279
- ```
280
-
281
- Tags use `{ text: ContentText, style?: { fontSize, fontWeight, color, fill } }`. Tag font size defaults to 11; each background reserves 6 units on each horizontal side and 2 on each vertical side, with a 3-unit corner radius. List `style.tagGap` is finite and nonnegative (default 6), separating tags, tag rows and same-row status. Tags use theme background/text colors unless explicitly overridden. Status defaults to 12-unit theme accent text; `statusStyle` accepts only `fontSize`, `fontWeight`, and `color`, and status remains right-aligned. A single tag row shares a baseline with a single-line status when their natural widths fit. Otherwise tags wrap first and status starts below all tags. Long tags and status use the same mixed-style grapheme wrapping. Body follows the metadata row(s), then auxiliary text. Omitted parts reserve no space. For example:
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
- Node dimensions stay fixed. Rectangles apply the authored padding. Rounded rectangles reserve an additional 4 units on each side before padding. Cylinders reserve twice the current cap radius above and below content before padding; the cap radius is `min(14, height / 4)`. The safe area has nonnegative dimensions, including when shape clearance and padding consume the entire node. Content starts at the top of its safe area and wraps using deterministic width estimates. Explicit line endings and blank lines are preserved. SVG clips text to the safe area. Insufficient width or height emits a separate `text-overflow` warning with the node ID, `nodes[index].content` path, and direction. Empty safe areas remain valid. Clipped text does not extend node hit bounds. Simple labels retain their existing overflow behavior.
291
-
292
- `EvaluatedNode.contentLayout` exposes `safeArea`, ordered `lines`, tag backgrounds `tags`, `widthOverflow`, and `heightOverflow`. Each line provides text, baseline coordinates, estimated width, typography, alignment, semantic role (`title`, `body`, `auxiliary`, `marker`, `tag` or `status`), source `blockIndex`, and optional list `itemIndex`. Tag lines also expose `tagIndex`. Each tag background provides absolute `x`, `y`, `width`, `height`, resolved `fill`, author provenance `explicitFill`, and source `blockIndex`/`itemIndex`/`tagIndex`. Each line's `spans` provide text, absolute left coordinates, shared baseline, estimated width, and resolved typography/color. `explicitColor` records whether the block or source span supplied its color. The same values drive browser SVG and static export. Content follows current absolute geometry, including interpolated dimensions and parent-group movement, ancestor visibility, and effective opacity. Content does not change ports, edge routes, transfer geometry, or node-parent rules. See the runnable [title and paragraph example](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/node-content.html) and [list example](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/list-content.html).
134
+ ### Events
293
135
 
294
- All three content shapes support initial content, styled spans, lists and timeline replacement. Tags, status and readable default colors under explicit, flow and hover highlights are supported.
136
+ Native events are `CustomEvent`s on `player.element`; read `event.detail`. Vue handlers and React callbacks receive the detail directly.
295
137
 
138
+ | Native / Vue event | React callback | Detail |
139
+ | --- | --- | --- |
140
+ | `stepchange` | `onStepChange` | `PlayerState`; scene/beat selection changed. |
141
+ | `timeupdate` | `onTimeUpdate` | `PlayerState`; frame updated. |
142
+ | `playstatechange` | `onPlayStateChange` | `PlayerState`; playing state changed. |
143
+ | `loopchange` | `onLoopChange` | `PlayerState`; loop setting changed. |
144
+ | `error` | `onError` | `PlayerErrorDetail`: player state plus `error: Error`. |
145
+ | `hover`, `click` | — | Browser/Web Component: `{ id, type, point, timeMs, sceneId, beatId }`, or `null` for no hit. Aliases: `objecthover`, `objectclick`. |
296
146
 
297
- ### Entity graphs inside nodes
147
+ Interaction `point` uses diagram coordinates; `type` is `"node"` or `"edge"`. A frame update emits `stepchange`, when needed, before `timeupdate`. Web Component errors before mounting may omit player-state fields.
298
148
 
299
- A `graph` block draws a small, undirected entity graph inside any supported content shape. Authors specify local coordinates; the SDK does not arrange entities automatically. Use `ContentGraphBlock` from `flowsvg`:
149
+ ## Export
300
150
 
301
151
  ```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` is positive and reserves a fixed vertical section, followed by the ordinary block gap. Entity centers `x`/`y` are nonnegative finite offsets from the section's top-left corner in diagram units. IDs must be unique within each graph block; each relation references two local IDs. Labels use escaped plain text at 12 units, centered above 4-unit dots. Selected entities use accent dots and bold labels; a relation uses accent paint when both endpoints are selected. Text and primitives share the parent content clip and theme/highlight paint. Coordinates or labels outside the graph section produce directional overflow warnings without moving the following block. Leave enough room above each dot for its label. Entities remain decorative: clicking or hovering them targets the parent node.
152
+ import { createDiagram } from "flowsvg";
153
+ import { exportGif } from "flowsvg/export";
154
+ import { diagram } from "./diagram.js";
314
155
 
315
- `contentLayout.graphs`, when present, provides absolute circle/line geometry, local `highlighted` state and resolved colors per `blockIndex`. Graph labels join `contentLayout.lines`. Replace the complete `content` at a beat start to change local selections. The [complete Hindsight example](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/hindsight.html) demonstrates all three acts and 25 beats, including group endpoints and changing entity selections.
316
-
317
- ### Author a complete Facts timeline
318
-
319
- Start with fixed shape dimensions, then choose ordered blocks. Use `marker: "none"` for records, `"bullet"` for capabilities, and `"number"` for steps. Give each entry only the parts it needs. Dates are plain text; a small inline span keeps a date on the body baseline. This complete document can be passed unchanged to any host:
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: "Confirmed records" },
327
- { type: "list", marker: "none", items: [{
328
- tags: [{ text: "world" }, { text: "orders", style: { fill: "#eadfff", color: "#57319b" } }],
329
- status: "new",
330
- body: [{ text: "Order saved", style: { fontWeight: 700 } }, { text: " 2026-10-17", style: { fontSize: 11 } }],
331
- auxiliary: "Ready for payment."
332
- }] }
333
- ] };
334
- const updated: NodeContent = { blocks: [
335
- { type: "title", text: "Facts" },
336
- { type: "list", items: [{ tags: [{ text: "verified" }], status: "done", body: "Payment confirmed", 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: "Records", 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: "Archived" } }] },
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 });
156
+ const compiled = createDiagram(diagram);
157
+ const frame = compiled.evaluateAt(1200);
158
+ const svg = compiled.renderSvg(1200, { theme: "dark" });
159
+ const gif = await exportGif(diagram, { width: 640, fps: 15 });
352
160
  ```
353
161
 
354
- Review `contentLayout.safeArea` and `frame.warnings` before sharing a document. If content exceeds the safe area, increase the authored dimensions or shorten the text. The SDK clips overflow and never resizes nodes or shrinks fonts. Prefer default role colors when switching themes or highlighting; explicit colors remain the author's responsibility. Content is one node interaction target, with no internal links, buttons, images, HTML, or independent timeline targets.
162
+ SVG returns a string; GIF returns `Uint8Array`. Browser GIF export supports `signal` and `onProgress`. Use `gifExport: true` for built-in download controls. Node 20+ also provides `renderHtml` and `exportGif` through `flowsvg/node`; Node GIF requires prepared Chromium.
355
163
 
356
- The [official content story](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/content-story.html) combines Facts, service capabilities and numbered steps with nested group movement, node resizing, explicit ports, all three edge routes, synchronized flows and focus. Its [shared document](https://github.com/yokingma/FlowSVG/blob/HEAD/apps/native-example/content-story.js) is also used by the React and Vue example applications. Native and Web Component hosts appear together. Normal, narrow and long-text links expose fixed-area clipping; theme and reduced-motion controls apply the same visual options. All hosts use the built-in play/pause, speed, step, scene and progress controls.
164
+ ## Core API — `flowsvg`
357
165
 
358
- Each beat has a positive `durationMs` and an `actions` array, with optional `transitionMs` and `caption`. Transition duration defaults to `min(250, durationMs)` and must be within `[0, durationMs]`. An omitted caption inherits the previous caption; an empty string clears it.
166
+ All times are milliseconds; positions and dimensions use diagram units. `options?: EvaluateOptions` accepts `theme?: "light" | "dark"` and `reducedMotion?: boolean` (default `false`).
359
167
 
360
- | Action | Example or update fields |
168
+ | Function | Result / behavior |
361
169
  | --- | --- |
362
- | `show` / `hide` | `{ type: "show", target: "service" }`; targets a node, group, or edge |
363
- | `highlight` / `unhighlight` | Changes the highlight layer; removing a highlight reveals the latest ordinary style |
364
- | `flow` | `{ type: "flow", target: "request", direction: "forward" }`; moves a beat-local marker along an edge. `direction` is `forward` or `reverse`; the static edge arrow does not change. Add `text: "Request"` for a horizontal text capsule; omit `text` for a solid dot. |
365
- | `updateNode` | `value` supports `x`, `y`, `width`, `height`, `label`, `content`, `fill`, `stroke`, and `opacity` |
366
- | `updateEdge` | `value` supports `label`, `stroke`, `strokeWidth`, and `opacity` |
367
- | `focus` | `target` is a node/group ID; `null` restores the full viewport |
170
+ | `createDiagram(document: DiagramDocument): Diagram` | Validate, copy, freeze and compile the document for repeated queries. |
171
+ | `renderSvg(document: DiagramDocument, timeMs = 0, options?): string` | Compile and render one static SVG. |
172
+ | `validateDocument(input: unknown): ValidationIssue[]` | Check fields and references; return `{ path, message }` issues. An empty array means these checks passed; compilation can still reject non-finite derived flow timing. |
173
+ | `measureNodeContent({ kind, width, content }): NodeContentMeasurement` | Return `{ height, diagnostics }`. `kind`: `rect`, `roundedRect`, or `cylinder`; `content`: one `NodeContent` or a nonempty array of versions. Return the largest required height across versions. |
174
+ | `wrapText(label: string, maxWidth: number, typography?): string[]` | Estimate wrapping without the DOM. Optional typography supplies all three numeric fields: `fontSize`, `lineHeight`, `charWidth`; omitted uses node typography. |
368
175
 
369
- `updateNode.value.content` replaces the complete content object at the beat start, including when `transitionMs` is nonzero. The result persists into later beats and scenes. A `label` update switches to simple-label mode; a content update switches to structured-content mode. `content: null` clears either text mode and leaves only the shape. Clearing never restores an earlier label or changes node dimensions. Do not assign both `label` and `content`, or assign either mode more than once in the same beat; these declarations produce field-path errors. Seeking, pausing, playback speed, and reduced motion use the same content time boundary.
370
-
371
- ```ts
372
- // Each value belongs to a separate beat's updateNode action.
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
- All scenes share a global timeline in milliseconds. Each scene inherits the previous scene's final state. Labels and captions change at beat start; interpolated properties change during the transition. Beat intervals include their start and exclude their end; the final time returns the completed final frame. Hiding a node or group also masks connected edges; showing it again restores visibility according to each object's saved state.
381
-
382
- A beat can transfer along several distinct edges at once. All transfers start at the beat start, use the same linear completion ratio over `durationMs`, and disappear at the beat end. Different path lengths therefore produce different physical speeds. Shared endpoint nodes remain highlighted while any current transfer uses them. The next beat replaces the complete transfer group, including across scene boundaries. Transfer highlights do not inherit; explicit highlights and ordinary styles retain their existing inheritance rules.
383
-
384
- ## API
385
-
386
- Transfer text belongs to one action and does not replace the edge's static `label`. Capsule width uses deterministic Unicode code-point estimates with padding; it does not use browser font measurements. Text remains horizontal on every route and is escaped as plain text. Capsules are decorative, with no independent pointer or keyboard interaction. A provided empty string still uses a capsule.
387
-
388
- ### Core API
389
-
390
- Import from `flowsvg`:
391
-
392
- | API | Result and behavior |
176
+ | Compiled `Diagram` | Result / behavior |
393
177
  | --- | --- |
394
- | `validateDocument(input: unknown)` | Returns `{ path, message }[]`; an empty array means valid |
395
- | `createDiagram(document)` | Validates, copies, and compiles into a `Diagram`; invalid documents throw `DiagramValidationError` |
396
- | `renderSvg(document, timeMs = 0, options?)` | Compiles once and exports a static SVG string |
397
- | `diagram.totalDuration` | Total timeline duration; `0` for a static document |
398
- | `diagram.evaluateAt(timeMs, options?)` | Returns `DiagramFrameState` |
399
- | `diagram.renderSvg(timeMs = 0, options?)` | Exports SVG from the same evaluation result |
400
- | `diagram.hitTest(timeMs, point, options?)` | Returns `{ id, type: "node" \| "edge" }` or `null`, using document coordinates |
401
- | `wrapText(label, maxWidth, typography?)` | Wraps text with fixed width estimates and returns an array of lines |
402
-
403
- `EvaluateOptions` is `{ theme?: "light" | "dark"; reducedMotion?: boolean }`. Finite times are clamped to `[0, totalDuration]`; non-finite times become zero. Hit testing ignores hidden or zero-opacity objects and checks ordinary nodes, then edges, then group backgrounds. Group hover highlights only the border and preserves the evaluated fill, including any active timeline highlight.
178
+ | `totalDuration: number` | Readonly effective duration; `0` for static documents. |
179
+ | `beats: readonly DiagramBeat[]` | Compiled `{ sceneId, beatId, beatIndex, startMs, endMs, caption? }` intervals. |
180
+ | `evaluateAt(timeMs: number, options?): DiagramFrameState` | Return `{ timeMs, sceneId, beatId, beatProgress, nodes, edges, camera, caption?, warnings }`. |
181
+ | `renderSvg(timeMs = 0, options?): string` | Render without recompiling. |
182
+ | `hitTest(timeMs: number, point: DiagramPoint, options?): HitObject \| null` | Hit-test `{ x, y }` in diagram coordinates; return `{ id, type }` or `null`. |
404
183
 
405
- `DiagramFrameState` includes `timeMs`, `sceneId`, `beatId`, `beatProgress`, `nodes`, `edges`, `camera`, optional `caption`, and `warnings`. An active `EvaluatedEdge` may include `flow` with its direction, linear beat progress, document-coordinate point, and optional message `text`; the marker is omitted at the beat end, when the edge is hidden, or when reduced motion is enabled. Evaluated node coordinates are absolute document coordinates. Scene and beat IDs are `null` for static documents. Beat IDs use `${scene.id}:${beatIndex}`, with zero-based indexes.
184
+ Query times clamp to `[0, totalDuration]`; non-finite values become zero. `DiagramValidationError.issues` contains validation paths and messages. Measurement diagnostics contain `{ versionIndex, path, dimension, message }`; increasing height does not fix width overflow or overflow inside a fixed-height graph block.
406
185
 
407
- Main public types include `DiagramDocument`, `DiagramNode`, `DiagramEdge`, `Scene`, `Beat`, `Action`, `Diagram`, `DiagramFrameState`, `EvaluateOptions`, `ValidationIssue`, and `LayoutWarning`. Type declarations and JSDoc for key members are included in the package.
186
+ ### Document and actions
408
187
 
409
- ### Player API
410
-
411
- Import `createPlayer(host, document, options?)` from `flowsvg/browser`. `PlayerOptions` extends core visual options with `controls?: boolean` and `hoverFill?: boolean`.
412
-
413
- | Method or property | Behavior |
188
+ | Type | Core fields |
414
189
  | --- | --- |
415
- | `element` | The original host element and event target |
416
- | `play()` / `pause()` | Starts playback or pauses on the current frame; static diagrams and the timeline end cannot start playback |
417
- | `seek(timeMs)` | Clamps to the timeline and retains playback state; reaching the end pauses |
418
- | `step(1)` / `step(-1)` | Pauses and jumps to the adjacent beat start; stepping backward inside a beat goes to its start, and backward at a beat start goes to the previous beat |
419
- | `setSpeed(speed)` | Sets a finite positive multiplier; ignores invalid values |
420
- | `load(document)` | Pauses at zero on success; validation failure emits error and retains the document, time, and playback state |
421
- | `getState()` | Returns `PlayerState`: `timeMs`, `totalDuration`, `playing`, `speed`, and `frame` |
422
- | `destroy()` | Idempotently cleans up DOM, listeners, and scheduling; stops events, while state remains readable |
423
-
424
- Players start paused at zero, with built-in controls enabled. Hiding the page pauses playback; making it visible again leaves playback paused. Playback stops at the final frame. Static diagrams disable timeline controls. Framework handles provide the control and state methods above, excluding `element` and `destroy`; frameworks manage the host and cleanup.
190
+ | `Viewport` | Required finite positive `width` and `height`; optional `fitHeight: { padding }` with finite nonnegative bottom padding. |
191
+ | `DiagramDocument` | `version: 1`, `viewport`, `nodes`, `edges`, `scenes`; optional `title`, `theme`. Empty `scenes` makes a static diagram. |
192
+ | `DiagramNode` | `id`, `kind`, `x`, `y`, `width`, `height`; optional `label` or `content` (mutually exclusive), `parentId`, `visible`, `style`. Kinds: `rect`, `roundedRect`, `ellipse`, `cylinder`, `group`. |
193
+ | `DiagramEdge` | `id`, `from`, `to`; optional `route` (default `straight`), `fromPort`, `toPort`, `label`, `visible`, `style`. Ports: `top`, `right`, `bottom`, `left`. |
194
+ | `Scene` / `Beat` | Scene: `{ id, label, beats }`. Beat: `{ durationMs, actions, transitionMs?, caption? }`. `durationMs` must be positive. `transitionMs` must be in `[0, durationMs]`, defaults to `min(250, durationMs)`, and is capped at effective beat length. |
195
+ | `Action` | `{ type, target }`; `updateNode` / `updateEdge` also require `value`. `flow` requires `direction` (`forward` / `reverse`), with optional `text` and `highlightParticipants` (default `true`). `focus` targets a node/group ID or `null` for full view. |
425
196
 
426
- Hovering a visible node's shape, simple label, or structured content highlights that node's border during playback or pause. The default border-only feedback preserves the evaluated fill and content paint. Set `hoverFill: true` to also paint the background and structured content. Hover does not highlight neighboring nodes or edges. Leaving the node removes only the hover cause; active flow and explicit highlights remain. After all causes end, the current ordinary style is shown. The player rechecks a stationary pointer after frame updates and clears hover on pointer cancellation, successful document replacement, or destruction.
197
+ `updateNode.value` supports `x`, `y`, `width`, `height`, `label`, `content` (`null` clears it), `fill`, `stroke`, and `opacity`. `updateEdge.value` supports `label`, `stroke`, `strokeWidth`, and `opacity`. Node/edge IDs are globally unique; child positions are relative to their parent group. `NodeContent` and its block types are exported from `flowsvg`.
427
198
 
428
- Default title/body/marker, auxiliary, tag, and status paint stays readable in both themes and during explicit, flow, and hover highlights. Intermediate highlight frames select contrasting default ink against the current evaluated fill. Authored span/block colors and tag fills remain unchanged. Highlight paint never changes text metrics, safe areas, or hit bounds. Content children have no separate IDs, keyboard focus, links, or buttons; content clicks report the node. Clipped text adds no hit area. Content replacement, text-mode changes, and node geometry updates reapply hover to the current frame; successful load clears the old document's hover.
199
+ ## Export API
429
200
 
430
- `PlayerState.frame` remains the core frame and excludes hover. Static SVG exports also exclude hover; browser output matches the same-time export when no node is hovered.
431
-
432
- ### Events
433
-
434
- Native players dispatch `CustomEvent` on the host element:
435
-
436
- | Event | `detail` |
201
+ | Entry / function | Return / requirements |
437
202
  | --- | --- |
438
- | `timeupdate` | Current `PlayerState`, sent after the frame updates |
439
- | `stepchange` | Current `PlayerState`, sent when the scene or beat changes, before that update's timeupdate |
440
- | `playstatechange` | Current `PlayerState`, sent when playing/paused state changes |
441
- | `error` | `PlayerErrorDetail`: retained player state and `error` |
442
- | `hover` / `click` | `PlayerInteractionDetail` or `null`; aliases are `objecthover` / `objectclick` |
443
-
444
- Interaction details contain `id`, `type`, document-coordinate `point`, `timeMs`, `sceneId`, and `beatId`; a miss produces `null`. Vue, React, and Web Component wrappers forward the first four event types. Web Component errors before mounting may lack player state.
445
-
446
- Pointer movement emits `hover`; frame updates also emit it when the object under a stationary pointer changes. Leaving or cancelling the pointer emits `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
- This example reuses `player` from the native integration. Remove any listeners your application adds when disposing of the host.
460
-
461
- ## Themes and accessibility
203
+ | `flowsvg/export` → `exportGif(document, options?: GifExportOptions)` | `Promise<Uint8Array>`; browser canvas and worker support. |
204
+ | `flowsvg/node` → `renderHtml(document, options?: HtmlRenderOptions)` | `Promise<string>`; standalone offline player HTML. Node 20+, no browser required. |
205
+ | `flowsvg/node` → `exportGif(document, options?: NodeGifExportOptions)` | `Promise<Uint8Array>`; Node 20+ and prepared Chromium. Set `executablePath` or supply a Playwright `browser`, never both. A supplied browser remains caller-owned. |
462
206
 
463
- Theme precedence is explicit option → document theme → light. The core does not read the system theme. Core `reducedMotion` defaults to off; the browser follows `prefers-reduced-motion` and responds to changes unless an explicit option is supplied. Reduced motion completes transitions immediately while preserving duration and time position.
464
-
465
- Import `flowsvg/styles.css` for control layout and focus styles. CSS variables customize controls only; core themes and document styles determine diagram colors:
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
- Add the `flowsvg` class to a native host to use this rule; framework components add it automatically. With the canvas focused, Space plays/pauses and the left/right arrow keys step through beats. Form controls retain their native key behavior. Captions use a polite live region, and each SVG has its own accessible name.
478
-
479
- ## Validation and errors
480
-
481
- Loading rejects unknown fields, invalid references, cyclic groups, duplicate assignments within a beat, invalid colors, and invalid dimensions/durations. Text is escaped as plain text. To check external JSON first, call `validateDocument` and display each issue's `path` and `message`.
482
-
483
- `createDiagram` and `createPlayer` with an invalid initial document throw `DiagramValidationError`, whose `issues` contain field paths. An existing player's `load` reports validation failure through the `error` event and preserves the old state. Text height overflow is a nonblocking layout warning; inspect `frame.warnings` to decide whether to resize nodes.
484
-
485
- Each beat permits at most one `flow` per edge, even when directions or text differ. Repeating an edge reports `duplicate beat assignment` at the later action’s `target`, independently of other field errors. A flow can coexist with visibility, explicit highlight, and ordinary style actions on the same edge; those actions retain their own validation rules.
486
-
487
- ## Development and verification
488
-
489
- Run these commands from the source repository root. Development uses Node.js 22.12+ and pnpm 10.12.1; Vue/React examples use Vite 7.
490
-
491
- | Command | Purpose |
207
+ | `GifExportOptions` | Default / meaning |
492
208
  | --- | --- |
493
- | `pnpm install` | Install workspace dependencies |
494
- | `pnpm build` | Build SDK JavaScript, declarations, and styles |
495
- | `pnpm typecheck` | Check core, browser, and framework entry types |
496
- | `pnpm test` | Run Vitest behavior tests |
497
- | `pnpm --filter flowsvg-vue-example dev` | Start the Vue example |
498
- | `pnpm --filter flowsvg-react-example dev` | Start the React example |
499
- | `pnpm release:verify` | Pack from a missing `dist`, verify the file allowlist and every export, then check framework-free imports, strict consumer types and all four host lifecycles |
500
- | `pnpm release` | Run all release gates and npm preview; retain the verified tarball in `dist/releases/` without uploading |
501
- | `pnpm release:publish` | Check npm authentication, repeat all release gates and preview, then upload the verified tarball |
502
-
503
- Maintainers: follow the [npm release guide](https://github.com/yokingma/FlowSVG/blob/HEAD/docs/RELEASING.md). Run SDK packaging before example builds; packaging removes and rebuilds `dist`.
504
-
505
- After building, run the Node.js static server from the repository root:
506
-
507
- ```sh
508
- pnpm dlx http-server@14.1.1 . -p 4175 -c-1
509
- ```
209
+ | `theme`, `reducedMotion` | Document theme then `light`; reduced motion defaults to `false`. |
210
+ | `startMs`, `endMs`, `speed` | Full timeline at `1×`; range start is inclusive and end exclusive, except the global end includes the completed state. |
211
+ | `width`, `fps` | `min(viewport.width, 1280)` pixels; `15` samples/second (`1`–`30`, integer). |
212
+ | `includeCaptions`, `captionHeight` | `true`; fixed caption region of `80` pixels. |
213
+ | `loopCount`, `finalHoldMs` | `0` means infinite GIF looping; `1`–`65536` means total plays. Add `1000` ms of final hold by default. Independent of live player looping. |
214
+ | `signal`, `onProgress` | `AbortSignal`; callback receives `{ phase, completed, total }`. Phases: `preparing`, `sampling`, `encoding`, `complete`. |
510
215
 
511
- The first run downloads the CLI without adding a project dependency. Open <http://localhost:4175/apps/native-example/> for the native example. For browser acceptance checks, open the `/packages/flowsvg/tests/browser-*.html` pages: `controls`, `lifecycle`, `visual-updates`, `focus`, `accessibility`, `web-component`, `flow-text`, and `parallel-flows`. Inspect the corresponding `window.browserTestResults`, `window.browserFocusResults`, `window.browserAccessibilityResults`, `window.browserWebComponentResults`, or `window.browserFlowTextResults`; every result's `passed` must be `true`.
216
+ GIF limits: 120 effective seconds, 1800 samples, 1801 encoded frames, final long edge ≤1920 pixels including captions. `GifExportError.code` is `options`, `limit`, `document`, `caption-overflow`, `environment`, `rasterization`, `encoding`, or `cancelled`. Both export entries expose `GifExportError` and their option/progress types.
512
217
 
513
- Open `/packages/flowsvg/tests/browser-hover.html` for real-pointer hover checks. Hover the source shape and overflowing label, use the flow/after-flow controls, and verify that leaving preserves active flow highlights and restores the latest ordinary style afterward. Check stationary-pointer updates, reload cleanup, and destroy cleanup with the remaining controls.
218
+ `HtmlRenderOptions`: `theme`, `autoplay` (default `false`), `loop` (`false`), `controls` (`true`), `gifExport` (`false`). Built-in GIF downloads export the full timeline at `1×`, using the current theme.
514
219
 
515
- To verify automatic reduced motion, open the accessibility page, change the browser's reduced-motion media emulation, then call `window.verifyMotionPreference(true)` or `false`. Check that preference changes match SVG export with the same options.
220
+ ## Other browser APIs
516
221
 
517
- Reuse compiled diagrams during playback and avoid recreating players on every render. Measure frame costs for large diagrams on target devices. See [CHANGELOG](CHANGELOG.md) for changes and [PATTERNS](PATTERNS.md) for implementation conventions.
222
+ - `flowsvg/web-component` (alias `flowsvg/element`): call `defineFlowPlayer(tagName = "flowsvg-player")` in the browser; returns the registered constructor, or `undefined` without a custom-element registry. Set `element.document` to an object, use the `document` JSON attribute, or load a URL through `src`. Playback options use attributes such as `loop`, `gif-export`, and `reduced-motion`; `autoplay` defaults to `false`, `speed` to `1`. Use playback methods except `load()`; assign `.document` to reload. `getState()` can return `undefined` before mounting or after destruction. Events bubble and cross shadow boundaries.
223
+ - `flowsvg/browser`: `new SelectControl(owner: Document, label: string, onChange: (value: string) => void)`. Append `.element`, synchronize with `render(choices: readonly SelectChoice[], value: string, disabled = false)`, and call `destroy()` on disposal. Each choice is `{ value, label }`; the caller owns the committed value.
518
224
 
519
- Parallel transfer checks and a playable three-then-two transfer demo are in `tests/browser-parallel-flows.html`. The checks compare public frames and SVG output across scene boundaries, themes, and reduced motion.
225
+ ## Authoring rules
520
226
 
521
- ## License
227
+ - `show`, `hide`, `highlight`, `unhighlight`, `updateNode`, `updateEdge`, and `focus` persist across beats. `flow` lasts for its beat only.
228
+ - A single flow moves at a reference 180 diagram units/second (minimum 400 ms), then uses a 500 ms arrival interval including highlight fades. This replaces authored `durationMs`. Multiple flows and no-flow beats retain authored timing. Read effective boundaries from `compiled.beats`; use separate no-flow beats for reading time.
229
+ - Structured `content` supports titles, paragraphs, lists, tags, and entity graphs in rectangles, rounded rectangles, and cylinders. Use `measureNodeContent({ kind, width, content })` before authoring fixed dimensions; inspect its `diagnostics`. It does not resize nodes during playback.
230
+ - `viewport.fitHeight: { padding: 24 }` fits current full-view height. GIF keeps the authored viewport dimensions and scale.
231
+ - Invalid documents throw `DiagramValidationError` with field paths. Text overflow appears in `frame.warnings`. `validateDocument(input)` checks external JSON before use.
522
232
 
523
- [MIT](LICENSE). Use, modify, and distribute with the license and copyright notice retained.
233
+ ESM with TypeScript declarations. Core runs without DOM on Node 18+; Node tools require Node 20+. Vue 3.5+ / React 18+.
524
234
 
525
- Open [the node-content pointer fixture](https://github.com/yokingma/FlowSVG/blob/HEAD/packages/flowsvg/tests/browser-node-content.html) (add `?theme=dark` for dark mode) to check all three shapes, tags, clipped long text, explicit/flow/hover layering, stationary-pointer content/text-mode/size updates, theme replacement and destruction.
235
+ [Full SDK guide](https://github.com/yokingma/FlowSVG/blob/main/docs/SDK-GUIDE.md) · [Changelog](https://cdn.jsdelivr.net/npm/flowsvg@0.2.0/CHANGELOG.md) · [MIT](https://cdn.jsdelivr.net/npm/flowsvg@0.2.0/LICENSE)