flowsvg 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/PATTERNS.md +12 -1
  3. package/README.md +139 -117
  4. package/README.zh-CN.md +139 -117
  5. package/dist/adapters/react.d.ts +48 -0
  6. package/dist/adapters/react.js +126 -0
  7. package/dist/adapters/vue.d.ts +132 -0
  8. package/dist/adapters/vue.js +161 -0
  9. package/dist/adapters/web-component.d.ts +99 -0
  10. package/dist/adapters/web-component.js +396 -0
  11. package/dist/browser/player.d.ts +69 -0
  12. package/dist/browser/player.js +353 -0
  13. package/dist/browser.d.ts +4 -69
  14. package/dist/browser.js +2 -353
  15. package/dist/core/color.d.ts +3 -0
  16. package/dist/core/color.js +18 -0
  17. package/dist/core/diagram.d.ts +38 -0
  18. package/dist/core/diagram.js +391 -0
  19. package/dist/core/edge-geometry.d.ts +32 -0
  20. package/dist/core/edge-geometry.js +382 -0
  21. package/dist/{core.d.ts → core/model.d.ts} +2 -52
  22. package/dist/core/model.js +1 -0
  23. package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
  24. package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
  25. package/dist/{node-content.d.ts → core/node-content.d.ts} +1 -1
  26. package/dist/core/svg-number.d.ts +5 -0
  27. package/dist/core/svg-number.js +7 -0
  28. package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
  29. package/dist/core/theme.js +17 -0
  30. package/dist/{timeline.d.ts → core/timeline.d.ts} +1 -3
  31. package/dist/{timeline.js → core/timeline.js} +1 -17
  32. package/dist/core/validation.d.ts +11 -0
  33. package/dist/core/validation.js +389 -0
  34. package/dist/index.d.ts +5 -1
  35. package/dist/index.js +2 -1
  36. package/dist/react.d.ts +3 -48
  37. package/dist/react.js +2 -126
  38. package/dist/vue.d.ts +3 -132
  39. package/dist/vue.js +2 -161
  40. package/dist/web-component.d.ts +3 -103
  41. package/dist/web-component.js +2 -396
  42. package/docs/images/entity-graph.png +0 -0
  43. package/docs/images/facts-timeline.png +0 -0
  44. package/docs/images/facts-timeline.zh-CN.png +0 -0
  45. package/docs/images/node-content.png +0 -0
  46. package/docs/images/numbered-list.png +0 -0
  47. package/docs/images/numbered-list.zh-CN.png +0 -0
  48. package/docs/images/quick-start.png +0 -0
  49. package/docs/images/record-tags.png +0 -0
  50. package/package.json +2 -1
  51. package/dist/core.js +0 -1149
  52. package/dist/theme.js +0 -6
  53. /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
  54. /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
  55. /package/dist/{player-controls.d.ts → browser/controls.d.ts} +0 -0
  56. /package/dist/{player-controls.js → browser/controls.js} +0 -0
  57. /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
  58. /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
  59. /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
  60. /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
  61. /package/dist/{node-content.js → core/node-content.js} +0 -0
@@ -0,0 +1,396 @@
1
+ /**
2
+ * Web Component boundary: convert attributes/objects/URLs and manage lifecycle while reusing browser playback/rendering.
3
+ * Imports do not register the element; DOM-free imports use a placeholder base, so real registration needs a browser load.
4
+ */
5
+ import { createDiagram } from "../core/diagram.js";
6
+ import { createPlayer } from "../browser/player.js";
7
+ /** Default custom-element name registered by {@link defineFlowPlayer}. */
8
+ export const FLOW_PLAYER_TAG = "flowsvg-player";
9
+ // typeof permits DOM-free imports, but the base is fixed at evaluation time; load this entry with a DOM to create elements.
10
+ const HTMLElementBase = (typeof HTMLElement === "undefined"
11
+ ? class {
12
+ }
13
+ : HTMLElement);
14
+ /**
15
+ * Custom-element adapter for attribute conversion, URL requests and the four playback/error events.
16
+ * The document property may be assigned before connection; disconnect releases the player/requests but retains the document.
17
+ * Rendering/timeline behavior delegates to createPlayer; registration requires an explicit defineFlowPlayer call.
18
+ */
19
+ export class FlowPlayerElement extends HTMLElementBase {
20
+ static observedAttributes = [
21
+ "document",
22
+ "src",
23
+ "autoplay",
24
+ "controls",
25
+ "speed",
26
+ "theme",
27
+ "reduced-motion",
28
+ "hover-fill"
29
+ ];
30
+ player;
31
+ playerHost;
32
+ sourceAbort;
33
+ requestToken = 0;
34
+ connected = false;
35
+ manuallyDestroyed = false;
36
+ currentDocument;
37
+ controlsOverride;
38
+ forwardedEvents = [];
39
+ /** Current object document; assigning undefined clears it, cancels requests and unmounts the player. */
40
+ get document() {
41
+ return this.currentDocument;
42
+ }
43
+ set document(value) {
44
+ if (value === undefined) {
45
+ this.currentDocument = undefined;
46
+ this.cancelSourceLoad();
47
+ this.unmountPlayer();
48
+ return;
49
+ }
50
+ this.setDocument(value);
51
+ }
52
+ /** Document URL resolved against ownerDocument.baseURI; changes cancel obsolete loads. */
53
+ get src() {
54
+ return this.getAttribute?.("src") ?? undefined;
55
+ }
56
+ set src(value) {
57
+ if (value === undefined || value === "")
58
+ this.removeAttribute("src");
59
+ else
60
+ this.setAttribute("src", value);
61
+ }
62
+ /** Whether to autoplay; false pauses a mounted player. */
63
+ get autoplay() {
64
+ return readBooleanAttribute(this, "autoplay", false);
65
+ }
66
+ set autoplay(value) {
67
+ setBooleanAttribute(this, "autoplay", value);
68
+ this.applyAutoplay();
69
+ }
70
+ /** Controls default on; an explicit false is retained in HTML as controls="false". */
71
+ get controls() {
72
+ return this.controlsOverride ?? readBooleanAttribute(this, "controls", true);
73
+ }
74
+ set controls(value) {
75
+ this.controlsOverride = value;
76
+ // An omitted attribute enables controls, so disabling must preserve the false text rather than remove the attribute.
77
+ if (value)
78
+ this.setAttribute("controls", "");
79
+ else
80
+ this.setAttribute("controls", "false");
81
+ this.rebuildForOptions();
82
+ }
83
+ /** Positive speed multiplier; invalid attribute text reads as 1, while invalid property assignments are ignored. */
84
+ get speed() {
85
+ return readPositiveNumber(this.getAttribute?.("speed"), 1);
86
+ }
87
+ set speed(value) {
88
+ if (!Number.isFinite(value) || value <= 0)
89
+ return;
90
+ this.setAttribute("speed", String(value));
91
+ this.player?.setSpeed(value);
92
+ }
93
+ /** Explicit light/dark theme; the player falls back to the document theme or light when unset. */
94
+ get theme() {
95
+ const value = this.getAttribute?.("theme");
96
+ return value === "light" || value === "dark" ? value : undefined;
97
+ }
98
+ set theme(value) {
99
+ if (value === undefined)
100
+ this.removeAttribute("theme");
101
+ else
102
+ this.setAttribute("theme", value);
103
+ }
104
+ /** undefined follows the system preference; true/false explicitly override it via reduced-motion. */
105
+ get reducedMotion() {
106
+ const value = this.getAttribute?.("reduced-motion");
107
+ if (value === null || value === undefined)
108
+ return undefined;
109
+ return parseBooleanValue(value, true);
110
+ }
111
+ set reducedMotion(value) {
112
+ if (value === undefined)
113
+ this.removeAttribute("reduced-motion");
114
+ else
115
+ this.setAttribute("reduced-motion", String(value));
116
+ }
117
+ /** Hover paints only borders by default. The hover-fill attribute enables background and content feedback. */
118
+ get hoverFill() {
119
+ return readBooleanAttribute(this, "hover-fill", false);
120
+ }
121
+ set hoverFill(value) {
122
+ setBooleanAttribute(this, "hover-fill", value);
123
+ }
124
+ connectedCallback() {
125
+ this.connected = true;
126
+ this.manuallyDestroyed = false;
127
+ if (this.player)
128
+ return;
129
+ if (this.currentDocument) {
130
+ this.mountPlayer(this.currentDocument);
131
+ return;
132
+ }
133
+ const documentAttribute = this.getAttribute("document");
134
+ if (documentAttribute !== null) {
135
+ this.loadDocumentAttribute(documentAttribute);
136
+ return;
137
+ }
138
+ const source = this.getAttribute("src");
139
+ if (source)
140
+ void this.loadSource(source);
141
+ }
142
+ disconnectedCallback() {
143
+ this.connected = false;
144
+ this.cancelSourceLoad();
145
+ this.unmountPlayer();
146
+ }
147
+ attributeChangedCallback(name, _oldValue, value) {
148
+ if (_oldValue === value || !this.connected)
149
+ return;
150
+ switch (name) {
151
+ case "document":
152
+ if (value === null) {
153
+ this.currentDocument = undefined;
154
+ this.unmountPlayer();
155
+ }
156
+ else
157
+ this.loadDocumentAttribute(value);
158
+ break;
159
+ case "src":
160
+ this.cancelSourceLoad();
161
+ if (value)
162
+ void this.loadSource(value);
163
+ break;
164
+ case "controls":
165
+ this.controlsOverride = value === null ? undefined : parseBooleanValue(value, true);
166
+ case "theme":
167
+ case "reduced-motion":
168
+ case "hover-fill":
169
+ this.rebuildForOptions();
170
+ break;
171
+ case "speed":
172
+ this.player?.setSpeed(this.speed);
173
+ break;
174
+ case "autoplay":
175
+ this.applyAutoplay();
176
+ break;
177
+ default:
178
+ break;
179
+ }
180
+ }
181
+ /** Forward controls to the mounted player; calls are inert when no player is mounted. */
182
+ play() {
183
+ this.player?.play();
184
+ }
185
+ /** Pause the mounted player, retaining its current frame; do nothing before mounting. */
186
+ pause() {
187
+ this.player?.pause();
188
+ }
189
+ /** Seek in milliseconds using browser-player clamping; do nothing before mounting. */
190
+ seek(timeMs) {
191
+ this.player?.seek(timeMs);
192
+ }
193
+ /** Pause and jump to a beat boundary; backward from inside a beat goes to that beat's start. */
194
+ step(direction) {
195
+ this.player?.step(direction);
196
+ }
197
+ /** Set the speed property, retaining it for future mounts; ignore non-finite or non-positive values. */
198
+ setSpeed(value) {
199
+ this.speed = value;
200
+ }
201
+ /** Return undefined before mounting or after destruction. */
202
+ getState() {
203
+ return this.player?.getState();
204
+ }
205
+ /** Explicitly release the player and requests; the manual-destruction flag resets on the next connection. */
206
+ destroy() {
207
+ this.manuallyDestroyed = true;
208
+ this.cancelSourceLoad();
209
+ this.unmountPlayer();
210
+ }
211
+ setDocument(value) {
212
+ this.cancelSourceLoad();
213
+ try {
214
+ // Validate before replacement so an invalid document does not discard the last valid frame.
215
+ createDiagram(value);
216
+ this.currentDocument = value;
217
+ if (this.connected && !this.manuallyDestroyed) {
218
+ if (this.player)
219
+ this.player.load(value);
220
+ else
221
+ this.mountPlayer(value);
222
+ this.applyAutoplay();
223
+ }
224
+ }
225
+ catch (error) {
226
+ this.dispatchError(toError(error));
227
+ }
228
+ }
229
+ loadDocumentAttribute(value) {
230
+ try {
231
+ const parsed = JSON.parse(value);
232
+ this.setDocument(parsed);
233
+ }
234
+ catch (error) {
235
+ this.dispatchError(toError(error));
236
+ }
237
+ }
238
+ /** AbortController cancels network work; the token also rejects obsolete results already received but not yet applied. */
239
+ async loadSource(source) {
240
+ const token = ++this.requestToken;
241
+ const controller = new AbortController();
242
+ this.sourceAbort?.abort();
243
+ this.sourceAbort = controller;
244
+ try {
245
+ const owner = this.ownerDocument;
246
+ const url = new URL(source, owner.baseURI).href;
247
+ const response = await fetch(url, { signal: controller.signal });
248
+ if (!response.ok)
249
+ throw new Error(`Failed to load diagram source (${response.status})`);
250
+ const parsed = await response.json();
251
+ // Recheck connection and request ownership after parsing so stale responses cannot replace a newer document/src.
252
+ if (!this.connected || token !== this.requestToken || controller.signal.aborted)
253
+ return;
254
+ this.setDocument(parsed);
255
+ }
256
+ catch (error) {
257
+ if (controller.signal.aborted || token !== this.requestToken)
258
+ return;
259
+ this.dispatchError(toError(error));
260
+ }
261
+ finally {
262
+ if (this.sourceAbort === controller)
263
+ this.sourceAbort = undefined;
264
+ }
265
+ }
266
+ /** Restore an existing playback state during option rebuilds; autoplay applies only to a fresh mount. */
267
+ mountPlayer(document, previous) {
268
+ if (!this.connected || this.manuallyDestroyed || this.player)
269
+ return;
270
+ const owner = this.ownerDocument;
271
+ const host = owner.createElement("div");
272
+ host.className = "flowsvg-element-host";
273
+ this.playerHost = host;
274
+ this.append(host);
275
+ try {
276
+ this.player = createPlayer(host, document, this.playerOptions());
277
+ this.forwardPlayerEvents(host);
278
+ this.player.setSpeed(this.speed);
279
+ if (previous) {
280
+ this.player.seek(previous.timeMs);
281
+ if (previous.playing)
282
+ this.player.play();
283
+ }
284
+ else
285
+ this.applyAutoplay();
286
+ }
287
+ catch (error) {
288
+ this.unmountPlayer();
289
+ this.dispatchError(toError(error));
290
+ }
291
+ }
292
+ /** Rebuild for option changes and restore time/playback; document replacement instead follows load's reset-to-zero rule. */
293
+ rebuildForOptions() {
294
+ if (!this.connected || !this.currentDocument || this.manuallyDestroyed)
295
+ return;
296
+ const previous = this.player?.getState();
297
+ this.unmountPlayer();
298
+ this.mountPlayer(this.currentDocument, previous);
299
+ }
300
+ /** Detach forwarding before destruction so teardown cannot forward events from the old instance. */
301
+ unmountPlayer() {
302
+ this.removeForwardedEvents();
303
+ this.player?.destroy();
304
+ this.player = undefined;
305
+ this.playerHost?.remove();
306
+ this.playerHost = undefined;
307
+ }
308
+ forwardPlayerEvents(host) {
309
+ this.removeForwardedEvents();
310
+ for (const name of ["stepchange", "timeupdate", "playstatechange", "error"]) {
311
+ const listener = (event) => {
312
+ const source = event;
313
+ this.dispatchEvent(new CustomEvent(name, {
314
+ detail: source.detail,
315
+ bubbles: true,
316
+ composed: true
317
+ }));
318
+ };
319
+ host.addEventListener(name, listener);
320
+ this.forwardedEvents.push({ name, listener });
321
+ }
322
+ }
323
+ removeForwardedEvents() {
324
+ if (!this.playerHost) {
325
+ this.forwardedEvents.length = 0;
326
+ return;
327
+ }
328
+ for (const { name, listener } of this.forwardedEvents)
329
+ this.playerHost.removeEventListener(name, listener);
330
+ this.forwardedEvents.length = 0;
331
+ }
332
+ playerOptions() {
333
+ return {
334
+ controls: this.controls,
335
+ hoverFill: this.hoverFill,
336
+ ...(this.theme ? { theme: this.theme } : {}),
337
+ ...(this.reducedMotion === undefined ? {} : { reducedMotion: this.reducedMotion })
338
+ };
339
+ }
340
+ applyAutoplay() {
341
+ if (!this.player)
342
+ return;
343
+ if (this.autoplay)
344
+ this.player.play();
345
+ else
346
+ this.player.pause();
347
+ }
348
+ cancelSourceLoad() {
349
+ this.requestToken += 1;
350
+ this.sourceAbort?.abort();
351
+ this.sourceAbort = undefined;
352
+ }
353
+ dispatchError(error) {
354
+ const detail = { error, ...(this.player ? this.player.getState() : {}) };
355
+ this.dispatchEvent(new CustomEvent("error", { detail, bubbles: true, composed: true }));
356
+ }
357
+ }
358
+ /**
359
+ * Register explicitly; repeated calls for the default name return the existing constructor, or undefined without customElements.
360
+ * Load the module with a DOM for real registration, since the HTMLElement base is fixed at module evaluation.
361
+ * @throws {DOMException} From the registry for invalid names or reusing the constructor under another name.
362
+ */
363
+ export function defineFlowPlayer(tagName = FLOW_PLAYER_TAG) {
364
+ if (typeof customElements === "undefined")
365
+ return undefined;
366
+ const existing = customElements.get(tagName);
367
+ if (existing)
368
+ return existing;
369
+ customElements.define(tagName, FlowPlayerElement);
370
+ return FlowPlayerElement;
371
+ }
372
+ function readBooleanAttribute(element, name, fallback) {
373
+ const value = element.getAttribute(name);
374
+ return value === null ? fallback : parseBooleanValue(value, true);
375
+ }
376
+ function parseBooleanValue(value, fallback) {
377
+ const normalized = value.trim().toLowerCase();
378
+ if (normalized === "false" || normalized === "0" || normalized === "no")
379
+ return false;
380
+ if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "")
381
+ return true;
382
+ return fallback;
383
+ }
384
+ function setBooleanAttribute(element, name, value) {
385
+ if (value)
386
+ element.setAttribute(name, "");
387
+ else
388
+ element.removeAttribute(name);
389
+ }
390
+ function readPositiveNumber(value, fallback) {
391
+ const parsed = Number(value);
392
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
393
+ }
394
+ function toError(error) {
395
+ return error instanceof Error ? error : new Error(String(error));
396
+ }
@@ -0,0 +1,69 @@
1
+ import type { DiagramDocument, DiagramFrameState, DiagramPoint, EvaluateOptions, HitObject, ThemeName } from "../core/model.js";
2
+ /** Creation options: controls default on, motion follows browser preferences, and theme falls back to the document/light. */
3
+ export interface PlayerOptions extends EvaluateOptions {
4
+ controls?: boolean;
5
+ /** Hover highlights borders by default; true also paints the background and adjusts content ink. Applies to groups too. */
6
+ hoverFill?: boolean;
7
+ }
8
+ /** Player snapshot: times are milliseconds, speed is a positive multiplier, and frame is the core result without hover. */
9
+ export interface PlayerState {
10
+ timeMs: number;
11
+ totalDuration: number;
12
+ playing: boolean;
13
+ speed: number;
14
+ frame: DiagramFrameState;
15
+ }
16
+ /** Object interaction detail; point uses document coordinates and time/beat IDs reflect the event-time player state. */
17
+ export interface PlayerInteractionDetail extends HitObject {
18
+ point: DiagramPoint;
19
+ timeMs: number;
20
+ sceneId: string | null;
21
+ beatId: string | null;
22
+ }
23
+ /** Imperative browser player; creation/successful load pause at zero, and controls become inert after destruction. */
24
+ export interface FlowPlayer {
25
+ /** The supplied host, also used as the CustomEvent dispatch target. */
26
+ readonly element: HTMLElement;
27
+ /** Play from the current position; do not start for static diagrams, at the end, or while the page is hidden. */
28
+ play(): void;
29
+ /** Stop time advancement, retaining the current frame. */
30
+ pause(): void;
31
+ /** Seek to a clamped millisecond time; preserve playback except at the end. Non-finite times become zero. */
32
+ seek(timeMs: number): void;
33
+ /** Pause and jump to the adjacent start; backward goes to this beat's start from inside it, or the preceding start. */
34
+ step(direction: 1 | -1): void;
35
+ /** Set a finite positive speed multiplier; ignore invalid values. */
36
+ setSpeed(speed: number): void;
37
+ /** Compile before replacing; reset/pause at zero on success, or emit error and retain the old document/time/playback. */
38
+ load(document: DiagramDocument): void;
39
+ /** Read current player state; the last state remains readable after destruction. */
40
+ getState(): PlayerState;
41
+ /** Silently and idempotently release frames/listeners/host content; no further player events are dispatched. */
42
+ destroy(): void;
43
+ }
44
+ /** Retained state and original load error; DiagramValidationError.issues provides field paths for validation failures. */
45
+ export interface PlayerErrorDetail extends PlayerState {
46
+ error: Error;
47
+ }
48
+ /**
49
+ * Host events and their detail types. Frame updates emit stepchange when needed, then timeupdate.
50
+ * hover/click use null for no hit; objecthover/objectclick report the same interactions under alternate names.
51
+ * Hover also changes when a frame update changes the object under a stationary pointer.
52
+ */
53
+ export type PlayerEventMap = {
54
+ stepchange: CustomEvent<PlayerState>;
55
+ timeupdate: CustomEvent<PlayerState>;
56
+ playstatechange: CustomEvent<PlayerState>;
57
+ error: CustomEvent<PlayerErrorDetail>;
58
+ hover: CustomEvent<PlayerInteractionDetail | null>;
59
+ click: CustomEvent<PlayerInteractionDetail | null>;
60
+ objecthover: CustomEvent<PlayerInteractionDetail | null>;
61
+ objectclick: CustomEvent<PlayerInteractionDetail | null>;
62
+ };
63
+ /**
64
+ * Mount a player using the host document and observe browser motion preferences unless explicitly overridden.
65
+ * Replaces host children; destroy an existing player before replacing it on the same host.
66
+ * @throws {DiagramValidationError} For an invalid initial document, before an instance exists to emit error.
67
+ */
68
+ export declare function createPlayer(host: HTMLElement, document: DiagramDocument, options?: PlayerOptions): FlowPlayer;
69
+ export type { ThemeName };