flowsvg 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/LICENSE +21 -0
- package/PATTERNS.md +101 -0
- package/README.md +524 -2
- package/README.zh-CN.md +525 -0
- package/dist/browser.d.ts +69 -0
- package/dist/browser.js +353 -0
- package/dist/core.d.ts +306 -0
- package/dist/core.js +1149 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node-content-paint.d.ts +10 -0
- package/dist/node-content-paint.js +54 -0
- package/dist/node-content.d.ts +184 -0
- package/dist/node-content.js +554 -0
- package/dist/player-accessibility.d.ts +16 -0
- package/dist/player-accessibility.js +36 -0
- package/dist/player-controls.d.ts +41 -0
- package/dist/player-controls.js +80 -0
- package/dist/player-lifecycle.d.ts +6 -0
- package/dist/player-lifecycle.js +31 -0
- package/dist/react.d.ts +48 -0
- package/dist/react.js +126 -0
- package/dist/styles.css +47 -0
- package/dist/svg-dom.d.ts +6 -0
- package/dist/svg-dom.js +74 -0
- package/dist/theme.d.ts +15 -0
- package/dist/theme.js +6 -0
- package/dist/timeline.d.ts +58 -0
- package/dist/timeline.js +164 -0
- package/dist/vue.d.ts +132 -0
- package/dist/vue.js +161 -0
- package/dist/web-component.d.ts +103 -0
- package/dist/web-component.js +396 -0
- package/package.json +87 -4
package/README.md
CHANGED
|
@@ -1,3 +1,525 @@
|
|
|
1
|
-
#
|
|
1
|
+
# flowsvg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
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.
|
|
6
|
+
|
|
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.
|
|
8
|
+
|
|
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:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pnpm install --frozen-lockfile
|
|
34
|
+
pnpm --filter flowsvg exec npm pack --pack-destination /tmp
|
|
35
|
+
```
|
|
36
|
+
|
|
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:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import type { DiagramDocument } from "flowsvg";
|
|
58
|
+
|
|
59
|
+
export const diagramDocument: DiagramDocument = {
|
|
60
|
+
version: 1,
|
|
61
|
+
title: "Request flow",
|
|
62
|
+
viewport: { width: 640, height: 280 },
|
|
63
|
+
nodes: [
|
|
64
|
+
{ id: "client", kind: "roundedRect", x: 40, y: 100, width: 140, height: 80, label: "Client" },
|
|
65
|
+
{ id: "service", kind: "rect", x: 420, y: 100, width: 160, height: 80, label: "Service", visible: false }
|
|
66
|
+
],
|
|
67
|
+
edges: [{ id: "request", from: "client", to: "service", label: "HTTP", route: "straight" }],
|
|
68
|
+
scenes: [{
|
|
69
|
+
id: "request-flow",
|
|
70
|
+
label: "Request reaches the service",
|
|
71
|
+
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" }] }
|
|
74
|
+
]
|
|
75
|
+
}]
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Evaluate and export SVG
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { createDiagram, renderSvg } from "flowsvg";
|
|
83
|
+
import { diagramDocument } from "./diagram.js";
|
|
84
|
+
|
|
85
|
+
const diagram = createDiagram(diagramDocument);
|
|
86
|
+
const frame = diagram.evaluateAt(125);
|
|
87
|
+
console.log(diagram.totalDuration, frame.sceneId, frame.warnings);
|
|
88
|
+
|
|
89
|
+
const svg = diagram.renderSvg(125, { theme: "dark" });
|
|
90
|
+
const finalSvg = renderSvg(diagramDocument, 2000);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`svg` 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.
|
|
94
|
+
|
|
95
|
+
## Vue integration
|
|
96
|
+
|
|
97
|
+
Create a component in your Vue application, with `diagram.ts` in the same directory:
|
|
98
|
+
|
|
99
|
+
```vue
|
|
100
|
+
<script setup lang="ts">
|
|
101
|
+
import { ref } from "vue";
|
|
102
|
+
import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/vue";
|
|
103
|
+
import type { PlayerErrorDetail } from "flowsvg/browser";
|
|
104
|
+
import "flowsvg/styles.css";
|
|
105
|
+
import { diagramDocument } from "./diagram.js";
|
|
106
|
+
|
|
107
|
+
const player = ref<FlowPlayerHandle | null>(null);
|
|
108
|
+
const timeMs = ref(0);
|
|
109
|
+
|
|
110
|
+
function handleError({ error }: PlayerErrorDetail): void {
|
|
111
|
+
console.error(error);
|
|
112
|
+
}
|
|
113
|
+
</script>
|
|
114
|
+
|
|
115
|
+
<template>
|
|
116
|
+
<FlowPlayer
|
|
117
|
+
ref="player"
|
|
118
|
+
:document="diagramDocument"
|
|
119
|
+
:speed="1.25"
|
|
120
|
+
theme="dark"
|
|
121
|
+
@timeupdate="timeMs = $event.timeMs"
|
|
122
|
+
@error="handleError"
|
|
123
|
+
/>
|
|
124
|
+
<button type="button" @click="player?.play()">Play</button>
|
|
125
|
+
<button type="button" @click="player?.seek(0)">Back to start</button>
|
|
126
|
+
<output>{{ timeMs }} ms</output>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
129
|
+
|
|
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
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
import { useRef } from "react";
|
|
136
|
+
import { FlowPlayer, type FlowPlayerHandle } from "flowsvg/react";
|
|
137
|
+
import "flowsvg/styles.css";
|
|
138
|
+
import { diagramDocument } from "./diagram.js";
|
|
139
|
+
|
|
140
|
+
export default function DiagramExample() {
|
|
141
|
+
const player = useRef<FlowPlayerHandle | null>(null);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<section>
|
|
145
|
+
<FlowPlayer
|
|
146
|
+
ref={player}
|
|
147
|
+
document={diagramDocument}
|
|
148
|
+
speed={1.25}
|
|
149
|
+
theme="dark"
|
|
150
|
+
onError={({ error }) => console.error(error)}
|
|
151
|
+
/>
|
|
152
|
+
<button type="button" onClick={() => player.current?.play()}>Play</button>
|
|
153
|
+
<button type="button" onClick={() => player.current?.seek(0)}>Back to start</button>
|
|
154
|
+
</section>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The component releases resources on unmount. Callbacks are `onStepChange`, `onTimeUpdate`, `onPlayStateChange`, and `onError`.
|
|
160
|
+
|
|
161
|
+
### Vue / React props and updates
|
|
162
|
+
|
|
163
|
+
| Prop | Default or behavior |
|
|
164
|
+
| --- | --- |
|
|
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.
|
|
177
|
+
|
|
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
|
+
```
|
|
185
|
+
|
|
186
|
+
Create the player in a client-side application module:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { createPlayer } from "flowsvg/browser";
|
|
190
|
+
import "flowsvg/styles.css";
|
|
191
|
+
import { diagramDocument } from "./diagram.js";
|
|
192
|
+
|
|
193
|
+
const host = document.querySelector<HTMLElement>("#diagram");
|
|
194
|
+
if (!host) throw new Error("Diagram host is missing");
|
|
195
|
+
|
|
196
|
+
const player = createPlayer(host, diagramDocument, { theme: "light" });
|
|
197
|
+
player.play();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`createPlayer` 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 |
|
|
235
|
+
| --- | --- |
|
|
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 |
|
|
242
|
+
|
|
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
|
+
```
|
|
267
|
+
|
|
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).
|
|
293
|
+
|
|
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.
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
### Entity graphs inside nodes
|
|
298
|
+
|
|
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`:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const entities: ContentGraphBlock = {
|
|
303
|
+
type: "graph", height: 130,
|
|
304
|
+
nodes: [
|
|
305
|
+
{ id: "alice", label: "Alice", x: 120, y: 30, highlighted: true },
|
|
306
|
+
{ id: "google", label: "Google", x: 50, y: 110, highlighted: true },
|
|
307
|
+
{ id: "ml", label: "ML project", x: 190, y: 110 }
|
|
308
|
+
],
|
|
309
|
+
edges: [{ from: "alice", to: "google" }, { from: "alice", to: "ml" }]
|
|
310
|
+
};
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`height` 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.
|
|
314
|
+
|
|
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 });
|
|
352
|
+
```
|
|
353
|
+
|
|
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.
|
|
355
|
+
|
|
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.
|
|
357
|
+
|
|
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.
|
|
359
|
+
|
|
360
|
+
| Action | Example or update fields |
|
|
361
|
+
| --- | --- |
|
|
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 |
|
|
368
|
+
|
|
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 |
|
|
393
|
+
| --- | --- |
|
|
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.
|
|
404
|
+
|
|
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.
|
|
406
|
+
|
|
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.
|
|
408
|
+
|
|
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 |
|
|
414
|
+
| --- | --- |
|
|
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.
|
|
425
|
+
|
|
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.
|
|
427
|
+
|
|
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.
|
|
429
|
+
|
|
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` |
|
|
437
|
+
| --- | --- |
|
|
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
|
|
462
|
+
|
|
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 |
|
|
492
|
+
| --- | --- |
|
|
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
|
+
```
|
|
510
|
+
|
|
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`.
|
|
512
|
+
|
|
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.
|
|
514
|
+
|
|
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.
|
|
516
|
+
|
|
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.
|
|
518
|
+
|
|
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.
|
|
520
|
+
|
|
521
|
+
## License
|
|
522
|
+
|
|
523
|
+
[MIT](LICENSE). Use, modify, and distribute with the license and copyright notice retained.
|
|
524
|
+
|
|
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.
|