flowsvg 0.1.1 → 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 (67) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/PATTERNS.md +61 -5
  3. package/README.md +149 -461
  4. package/README.zh-CN.md +149 -461
  5. package/dist/adapters/react.d.ts +7 -0
  6. package/dist/adapters/react.js +14 -2
  7. package/dist/adapters/vue.d.ts +29 -3
  8. package/dist/adapters/vue.js +20 -4
  9. package/dist/adapters/web-component.d.ts +14 -2
  10. package/dist/adapters/web-component.js +32 -3
  11. package/dist/browser/controls.d.ts +7 -3
  12. package/dist/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 +9 -1
  16. package/dist/browser/player.js +83 -24
  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 +2 -1
  20. package/dist/browser.js +2 -1
  21. package/dist/core/diagram.d.ts +4 -2
  22. package/dist/core/diagram.js +111 -21
  23. package/dist/core/edge-geometry.d.ts +16 -3
  24. package/dist/core/edge-geometry.js +264 -37
  25. package/dist/core/model.d.ts +35 -9
  26. package/dist/core/node-content-measurement.d.ts +34 -0
  27. package/dist/core/node-content-measurement.js +66 -0
  28. package/dist/core/node-content.d.ts +17 -0
  29. package/dist/core/node-content.js +30 -6
  30. package/dist/core/timeline.d.ts +11 -3
  31. package/dist/core/timeline.js +29 -5
  32. package/dist/core/validation.d.ts +1 -1
  33. package/dist/core/validation.js +13 -3
  34. package/dist/export/browser.d.ts +9 -0
  35. package/dist/export/browser.js +172 -0
  36. package/dist/export/captions.d.ts +18 -0
  37. package/dist/export/captions.js +44 -0
  38. package/dist/export/plan.d.ts +25 -0
  39. package/dist/export/plan.js +94 -0
  40. package/dist/export/types.d.ts +64 -0
  41. package/dist/export/types.js +9 -0
  42. package/dist/export/worker-source.d.ts +1 -0
  43. package/dist/export/worker-source.js +1 -0
  44. package/dist/export.d.ts +3 -0
  45. package/dist/export.js +2 -0
  46. package/dist/index.d.ts +3 -1
  47. package/dist/index.js +1 -0
  48. package/dist/node/cli.d.ts +2 -0
  49. package/dist/node/cli.js +72 -0
  50. package/dist/node/gif.d.ts +16 -0
  51. package/dist/node/gif.js +184 -0
  52. package/dist/node/html.d.ts +19 -0
  53. package/dist/node/html.js +61 -0
  54. package/dist/node/output.d.ts +2 -0
  55. package/dist/node/output.js +22 -0
  56. package/dist/node.d.ts +6 -0
  57. package/dist/node.js +3 -0
  58. package/dist/portable/LICENSE.esbuild.txt +21 -0
  59. package/dist/portable/LICENSE.flowsvg.txt +21 -0
  60. package/dist/portable/LICENSE.gifenc.txt +21 -0
  61. package/dist/portable/export.js +5 -0
  62. package/dist/portable/manifest.json +36 -0
  63. package/dist/portable/player.js +2 -0
  64. package/dist/portable/styles.css +227 -0
  65. package/dist/styles.css +194 -14
  66. package/docs/images/flowsvg-wordmark.gif +0 -0
  67. package/package.json +37 -6
@@ -10,8 +10,12 @@ export interface FlowPlayerProps {
10
10
  document: DiagramDocument;
11
11
  /** Positive playback multiplier; omit to use the player default. */
12
12
  speed?: number;
13
+ /** Defaults to false. Changes update live loop mode without rebuilding the player. */
14
+ loop?: boolean;
13
15
  theme?: ThemeName;
14
16
  controls?: boolean;
17
+ /** Defaults to false. Enable GIF downloads when controls are visible; export is independent of playback. */
18
+ gifExport?: boolean;
15
19
  /** Defaults to false: hover marks borders only. True also highlights backgrounds, including groups. */
16
20
  hoverFill?: boolean;
17
21
  reducedMotion?: boolean;
@@ -22,6 +26,7 @@ export interface FlowPlayerProps {
22
26
  onStepChange?: (state: PlayerState) => void;
23
27
  onTimeUpdate?: (state: PlayerState) => void;
24
28
  onPlayStateChange?: (state: PlayerState) => void;
29
+ onLoopChange?: (state: PlayerState) => void;
25
30
  onError?: (detail: PlayerErrorDetail) => void;
26
31
  }
27
32
  /** Imperative controls for a mounted component; calls before mounting or after unmounting throw a not-mounted error. */
@@ -36,6 +41,8 @@ export interface FlowPlayerHandle {
36
41
  step(direction: 1 | -1): void;
37
42
  /** Set a finite positive multiplier; ignore invalid values. */
38
43
  setSpeed(speed: number): void;
44
+ /** Toggle whole-timeline playback without changing the current position or play state. */
45
+ setLoop(loop: boolean): void;
39
46
  /** Reset/pause at zero on success; emit an error and retain playback state if validation fails. */
40
47
  load(document: DiagramDocument): void;
41
48
  /** Read the current browser-player snapshot. */
@@ -22,6 +22,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
22
22
  onStepChange: props.onStepChange,
23
23
  onTimeUpdate: props.onTimeUpdate,
24
24
  onPlayStateChange: props.onPlayStateChange,
25
+ onLoopChange: props.onLoopChange,
25
26
  onError: props.onError
26
27
  };
27
28
  useImperativeHandle(ref, () => ({
@@ -30,6 +31,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
30
31
  seek: (timeMs) => requirePlayer(playerRef).seek(timeMs),
31
32
  step: (direction) => requirePlayer(playerRef).step(direction),
32
33
  setSpeed: (speed) => requirePlayer(playerRef).setSpeed(speed),
34
+ setLoop: (loop) => requirePlayer(playerRef).setLoop(loop),
33
35
  load: (document) => requirePlayer(playerRef).load(document),
34
36
  getState: () => requirePlayer(playerRef).getState()
35
37
  }), []);
@@ -40,6 +42,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
40
42
  const nextOptions = {
41
43
  theme: props.theme,
42
44
  controls: props.controls,
45
+ gifExport: props.gifExport,
43
46
  hoverFill: props.hoverFill,
44
47
  reducedMotion: props.reducedMotion
45
48
  };
@@ -53,7 +56,7 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
53
56
  // Detach old callbacks before replacement so old lifecycle events cannot reach the new component state.
54
57
  eventCleanupRef.current?.();
55
58
  existing?.destroy();
56
- const player = createPlayer(host, props.document, nextOptions);
59
+ const player = createPlayer(host, props.document, { ...nextOptions, loop: props.loop ?? existing?.getState().loop });
57
60
  // A new instance needs current props even when the separate speed effect has no changed dependency.
58
61
  if (props.speed !== undefined)
59
62
  player.setSpeed(props.speed);
@@ -68,12 +71,15 @@ export const FlowPlayer = forwardRef(function FlowPlayer(props, ref) {
68
71
  return;
69
72
  }
70
73
  optionsRef.current = nextOptions;
71
- }, [props.document, props.theme, props.controls, props.reducedMotion, props.hoverFill]);
74
+ }, [props.document, props.theme, props.controls, props.gifExport, props.reducedMotion, props.hoverFill]);
72
75
  useEffect(() => {
73
76
  const player = playerRef.current;
74
77
  if (player && props.speed !== undefined)
75
78
  player.setSpeed(props.speed);
76
79
  }, [props.speed]);
80
+ useEffect(() => {
81
+ playerRef.current?.setLoop(props.loop ?? false);
82
+ }, [props.loop]);
77
83
  useEffect(() => () => {
78
84
  eventCleanupRef.current?.();
79
85
  eventCleanupRef.current = null;
@@ -96,6 +102,7 @@ function sameOptions(previous, next) {
96
102
  return false;
97
103
  return previous.theme === next.theme
98
104
  && previous.controls === next.controls
105
+ && previous.gifExport === next.gifExport
99
106
  && previous.hoverFill === next.hoverFill
100
107
  && previous.reducedMotion === next.reducedMotion;
101
108
  }
@@ -110,17 +117,22 @@ function attachPlayerEvents(player, callbacksRef) {
110
117
  const onPlayStateChange = (event) => {
111
118
  callbacksRef.current.onPlayStateChange?.(event.detail);
112
119
  };
120
+ const onLoopChange = (event) => {
121
+ callbacksRef.current.onLoopChange?.(event.detail);
122
+ };
113
123
  const onError = (event) => {
114
124
  callbacksRef.current.onError?.(event.detail);
115
125
  };
116
126
  player.element.addEventListener("stepchange", onStepChange);
117
127
  player.element.addEventListener("timeupdate", onTimeUpdate);
118
128
  player.element.addEventListener("playstatechange", onPlayStateChange);
129
+ player.element.addEventListener("loopchange", onLoopChange);
119
130
  player.element.addEventListener("error", onError);
120
131
  return () => {
121
132
  player.element.removeEventListener("stepchange", onStepChange);
122
133
  player.element.removeEventListener("timeupdate", onTimeUpdate);
123
134
  player.element.removeEventListener("playstatechange", onPlayStateChange);
135
+ player.element.removeEventListener("loopchange", onLoopChange);
124
136
  player.element.removeEventListener("error", onError);
125
137
  };
126
138
  }
@@ -10,8 +10,12 @@ export interface FlowPlayerProps {
10
10
  document: DiagramDocument;
11
11
  /** Positive playback multiplier; omit to use the player default. */
12
12
  speed?: number;
13
+ /** Defaults to false. Changes update live loop mode without rebuilding the player. */
14
+ loop?: boolean;
13
15
  theme?: ThemeName;
14
16
  controls?: boolean;
17
+ /** Defaults to false. Enable GIF downloads when controls are visible; export is independent of playback. */
18
+ gifExport?: boolean;
15
19
  /** Defaults to false: hover marks borders only. True also highlights backgrounds, including groups. */
16
20
  hoverFill?: boolean;
17
21
  reducedMotion?: boolean;
@@ -22,6 +26,7 @@ export interface FlowPlayerProps {
22
26
  onStepChange?: (state: PlayerState) => void;
23
27
  onTimeUpdate?: (state: PlayerState) => void;
24
28
  onPlayStateChange?: (state: PlayerState) => void;
29
+ onLoopChange?: (state: PlayerState) => void;
25
30
  onError?: (detail: PlayerErrorDetail) => void;
26
31
  }
27
32
  /** Imperative controls for a mounted component; calls before mounting or after unmounting throw a not-mounted error. */
@@ -36,6 +41,8 @@ export interface FlowPlayerHandle {
36
41
  step(direction: 1 | -1): void;
37
42
  /** Set a finite positive multiplier; ignore invalid values. */
38
43
  setSpeed(speed: number): void;
44
+ /** Toggle whole-timeline playback without changing the current position or play state. */
45
+ setLoop(loop: boolean): void;
39
46
  /** Reset/pause at zero on success; emit an error and retain playback state if validation fails. */
40
47
  load(document: DiagramDocument): void;
41
48
  /** Read the current browser-player snapshot. */
@@ -54,10 +61,18 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
54
61
  readonly type: NumberConstructor;
55
62
  readonly required: false;
56
63
  };
64
+ readonly loop: {
65
+ readonly type: PropType<boolean | undefined>;
66
+ readonly default: undefined;
67
+ };
57
68
  readonly theme: {
58
69
  readonly type: PropType<ThemeName>;
59
70
  readonly required: false;
60
71
  };
72
+ readonly gifExport: {
73
+ readonly type: PropType<boolean | undefined>;
74
+ readonly default: undefined;
75
+ };
61
76
  readonly controls: {
62
77
  readonly type: PropType<boolean | undefined>;
63
78
  readonly default: undefined;
@@ -94,6 +109,10 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
94
109
  readonly type: PropType<(state: PlayerState) => void>;
95
110
  readonly required: false;
96
111
  };
112
+ readonly onLoopChange: {
113
+ readonly type: PropType<(state: PlayerState) => void>;
114
+ readonly required: false;
115
+ };
97
116
  readonly onError: {
98
117
  readonly type: PropType<(detail: PlayerErrorDetail) => void>;
99
118
  readonly required: false;
@@ -104,29 +123,36 @@ export declare const FlowPlayer: import("vue").DefineComponent<import("vue").Ext
104
123
  stepchange: (_state: PlayerState) => true;
105
124
  timeupdate: (_state: PlayerState) => true;
106
125
  playstatechange: (_state: PlayerState) => true;
126
+ loopchange: (_state: PlayerState) => true;
107
127
  error: (_detail: PlayerErrorDetail) => true;
108
128
  }, string, import("vue").PublicProps, Readonly<{
109
129
  readonly document: DiagramDocument;
110
130
  } & {
111
131
  readonly style?: CSSProperties | undefined;
112
132
  readonly theme?: ThemeName | undefined;
133
+ readonly reducedMotion?: boolean | undefined;
134
+ readonly speed?: number | undefined;
113
135
  readonly controls?: boolean | undefined;
136
+ readonly loop?: boolean | undefined;
137
+ readonly gifExport?: boolean | undefined;
114
138
  readonly hoverFill?: boolean | undefined;
115
- readonly speed?: number | undefined;
116
- readonly reducedMotion?: boolean | undefined;
117
139
  readonly initialTimeMs?: number | undefined;
118
140
  readonly className?: string | undefined;
119
141
  readonly onStepChange?: ((state: PlayerState) => void) | undefined;
120
142
  readonly onTimeUpdate?: ((state: PlayerState) => void) | undefined;
121
143
  readonly onPlayStateChange?: ((state: PlayerState) => void) | undefined;
144
+ readonly onLoopChange?: ((state: PlayerState) => void) | undefined;
122
145
  readonly onError?: ((detail: PlayerErrorDetail) => void) | undefined;
123
146
  } & {
124
147
  onError?: ((_detail: PlayerErrorDetail) => any) | undefined;
125
148
  onTimeupdate?: ((_state: PlayerState) => any) | undefined;
126
149
  onPlaystatechange?: ((_state: PlayerState) => any) | undefined;
127
150
  onStepchange?: ((_state: PlayerState) => any) | undefined;
151
+ onLoopchange?: ((_state: PlayerState) => any) | undefined;
128
152
  }>, {
153
+ readonly reducedMotion: boolean | undefined;
129
154
  readonly controls: boolean | undefined;
155
+ readonly loop: boolean | undefined;
156
+ readonly gifExport: boolean | undefined;
130
157
  readonly hoverFill: boolean | undefined;
131
- readonly reducedMotion: boolean | undefined;
132
158
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}>;
@@ -7,7 +7,9 @@ import { createPlayer } from "../browser/player.js";
7
7
  const propsOptions = {
8
8
  document: { type: Object, required: true },
9
9
  speed: { type: Number, required: false },
10
+ loop: { type: Boolean, default: undefined },
10
11
  theme: { type: String, required: false },
12
+ gifExport: { type: Boolean, default: undefined },
11
13
  controls: { type: Boolean, default: undefined },
12
14
  hoverFill: { type: Boolean, default: undefined },
13
15
  reducedMotion: { type: Boolean, default: undefined },
@@ -17,6 +19,7 @@ const propsOptions = {
17
19
  onStepChange: { type: Function, required: false },
18
20
  onTimeUpdate: { type: Function, required: false },
19
21
  onPlayStateChange: { type: Function, required: false },
22
+ onLoopChange: { type: Function, required: false },
20
23
  onError: { type: Function, required: false }
21
24
  };
22
25
  /**
@@ -31,6 +34,7 @@ export const FlowPlayer = defineComponent({
31
34
  stepchange: (_state) => true,
32
35
  timeupdate: (_state) => true,
33
36
  playstatechange: (_state) => true,
37
+ loopchange: (_state) => true,
34
38
  error: (_detail) => true
35
39
  },
36
40
  setup(props, { attrs, emit, expose }) {
@@ -51,12 +55,14 @@ export const FlowPlayer = defineComponent({
51
55
  seek: (timeMs) => requirePlayer().seek(timeMs),
52
56
  step: (direction) => requirePlayer().step(direction),
53
57
  setSpeed: (speed) => requirePlayer().setSpeed(speed),
58
+ setLoop: (loop) => requirePlayer().setLoop(loop),
54
59
  load: (document) => requirePlayer().load(document),
55
60
  getState: () => requirePlayer().getState()
56
61
  });
57
62
  const currentOptions = () => ({
58
63
  theme: props.theme,
59
64
  controls: props.controls,
65
+ gifExport: props.gifExport,
60
66
  hoverFill: props.hoverFill,
61
67
  reducedMotion: props.reducedMotion
62
68
  });
@@ -76,6 +82,11 @@ export const FlowPlayer = defineComponent({
76
82
  emit("playstatechange", state);
77
83
  props.onPlayStateChange?.(state);
78
84
  };
85
+ const onLoopChange = (event) => {
86
+ const state = event.detail;
87
+ emit("loopchange", state);
88
+ props.onLoopChange?.(state);
89
+ };
79
90
  const onError = (event) => {
80
91
  const detail = event.detail;
81
92
  // Vue's error event already invokes the onError prop; an explicit callback would duplicate delivery.
@@ -84,20 +95,22 @@ export const FlowPlayer = defineComponent({
84
95
  next.element.addEventListener("stepchange", onStepChange);
85
96
  next.element.addEventListener("timeupdate", onTimeUpdate);
86
97
  next.element.addEventListener("playstatechange", onPlayStateChange);
98
+ next.element.addEventListener("loopchange", onLoopChange);
87
99
  next.element.addEventListener("error", onError);
88
100
  return () => {
89
101
  next.element.removeEventListener("stepchange", onStepChange);
90
102
  next.element.removeEventListener("timeupdate", onTimeUpdate);
91
103
  next.element.removeEventListener("playstatechange", onPlayStateChange);
104
+ next.element.removeEventListener("loopchange", onLoopChange);
92
105
  next.element.removeEventListener("error", onError);
93
106
  };
94
107
  };
95
108
  // Keep initialApplied across option rebuilds so initialTimeMs is not reapplied.
96
- const mount = () => {
109
+ const mount = (previousLoop) => {
97
110
  const host = hostRef.value;
98
111
  if (!host || player)
99
112
  return;
100
- const next = createPlayer(host, props.document, currentOptions());
113
+ const next = createPlayer(host, props.document, { ...currentOptions(), loop: props.loop ?? previousLoop });
101
114
  player = next;
102
115
  optionsSnapshot = currentOptions();
103
116
  cleanupEvents = attachEvents(next);
@@ -118,8 +131,9 @@ export const FlowPlayer = defineComponent({
118
131
  const rebuild = () => {
119
132
  if (!mounted || !player)
120
133
  return;
134
+ const previousLoop = player.getState().loop;
121
135
  unmount();
122
- mount();
136
+ mount(previousLoop);
123
137
  };
124
138
  onMounted(() => {
125
139
  mounted = true;
@@ -130,7 +144,7 @@ export const FlowPlayer = defineComponent({
130
144
  return;
131
145
  player.load(next);
132
146
  });
133
- watch(() => [props.theme, props.controls, props.reducedMotion, props.hoverFill], () => {
147
+ watch(() => [props.theme, props.controls, props.gifExport, props.reducedMotion, props.hoverFill], () => {
134
148
  const next = currentOptions();
135
149
  if (!mounted || sameOptions(optionsSnapshot, next))
136
150
  return;
@@ -140,6 +154,7 @@ export const FlowPlayer = defineComponent({
140
154
  if (player && speed !== undefined)
141
155
  player.setSpeed(speed);
142
156
  });
157
+ watch(() => props.loop, (loop) => player?.setLoop(loop ?? false));
143
158
  onBeforeUnmount(() => {
144
159
  mounted = false;
145
160
  unmount();
@@ -156,6 +171,7 @@ function sameOptions(previous, next) {
156
171
  return false;
157
172
  return previous.theme === next.theme
158
173
  && previous.controls === next.controls
174
+ && previous.gifExport === next.gifExport
159
175
  && previous.hoverFill === next.hoverFill
160
176
  && previous.reducedMotion === next.reducedMotion;
161
177
  }
@@ -7,7 +7,11 @@ declare const HTMLElementBase: ElementBase;
7
7
  /** Declarative options; autoplay defaults to false, speed to 1 and controls to true. */
8
8
  export interface FlowPlayerElementOptions extends EvaluateOptions {
9
9
  autoplay?: boolean;
10
+ /** Defaults to false; loop is a live playback preference, independent of GIF loopCount. */
11
+ loop?: boolean;
10
12
  controls?: boolean;
13
+ /** Defaults to false. Enable GIF download controls; ignored when controls is false. */
14
+ gifExport?: boolean;
11
15
  /** Defaults to false; true enables background feedback as well as borders on hover. */
12
16
  hoverFill?: boolean;
13
17
  speed?: number;
@@ -17,12 +21,12 @@ export type FlowPlayerElementErrorDetail = Partial<PlayerErrorDetail> & {
17
21
  error: Error;
18
22
  };
19
23
  /**
20
- * Custom-element adapter for attribute conversion, URL requests and the four playback/error events.
24
+ * Custom-element adapter for attribute conversion, URL requests and playback/error events.
21
25
  * The document property may be assigned before connection; disconnect releases the player/requests but retains the document.
22
26
  * Rendering/timeline behavior delegates to createPlayer; registration requires an explicit defineFlowPlayer call.
23
27
  */
24
28
  export declare class FlowPlayerElement extends HTMLElementBase {
25
- static readonly observedAttributes: readonly ["document", "src", "autoplay", "controls", "speed", "theme", "reduced-motion", "hover-fill"];
29
+ static readonly observedAttributes: readonly ["document", "src", "autoplay", "controls", "speed", "theme", "reduced-motion", "hover-fill", "gif-export", "loop"];
26
30
  private player;
27
31
  private playerHost;
28
32
  private sourceAbort;
@@ -41,6 +45,9 @@ export declare class FlowPlayerElement extends HTMLElementBase {
41
45
  /** Whether to autoplay; false pauses a mounted player. */
42
46
  get autoplay(): boolean;
43
47
  set autoplay(value: boolean);
48
+ /** Live loop mode, reflected by the loop attribute; changes retain time and play state. */
49
+ get loop(): boolean;
50
+ set loop(value: boolean);
44
51
  /** Controls default on; an explicit false is retained in HTML as controls="false". */
45
52
  get controls(): boolean;
46
53
  set controls(value: boolean);
@@ -56,6 +63,9 @@ export declare class FlowPlayerElement extends HTMLElementBase {
56
63
  /** Hover paints only borders by default. The hover-fill attribute enables background and content feedback. */
57
64
  get hoverFill(): boolean;
58
65
  set hoverFill(value: boolean);
66
+ /** Opt-in GIF controls, reflected by the gif-export attribute; defaults to false. */
67
+ get gifExport(): boolean;
68
+ set gifExport(value: boolean);
59
69
  connectedCallback(): void;
60
70
  disconnectedCallback(): void;
61
71
  attributeChangedCallback(name: string, _oldValue: string | null, value: string | null): void;
@@ -69,6 +79,8 @@ export declare class FlowPlayerElement extends HTMLElementBase {
69
79
  step(direction: 1 | -1): void;
70
80
  /** Set the speed property, retaining it for future mounts; ignore non-finite or non-positive values. */
71
81
  setSpeed(value: number): void;
82
+ /** Set loop mode and retain it for future mounts. */
83
+ setLoop(value: boolean): void;
72
84
  /** Return undefined before mounting or after destruction. */
73
85
  getState(): PlayerState | undefined;
74
86
  /** Explicitly release the player and requests; the manual-destruction flag resets on the next connection. */
@@ -12,7 +12,7 @@ const HTMLElementBase = (typeof HTMLElement === "undefined"
12
12
  }
13
13
  : HTMLElement);
14
14
  /**
15
- * Custom-element adapter for attribute conversion, URL requests and the four playback/error events.
15
+ * Custom-element adapter for attribute conversion, URL requests and playback/error events.
16
16
  * The document property may be assigned before connection; disconnect releases the player/requests but retains the document.
17
17
  * Rendering/timeline behavior delegates to createPlayer; registration requires an explicit defineFlowPlayer call.
18
18
  */
@@ -25,7 +25,9 @@ export class FlowPlayerElement extends HTMLElementBase {
25
25
  "speed",
26
26
  "theme",
27
27
  "reduced-motion",
28
- "hover-fill"
28
+ "hover-fill",
29
+ "gif-export",
30
+ "loop"
29
31
  ];
30
32
  player;
31
33
  playerHost;
@@ -67,6 +69,13 @@ export class FlowPlayerElement extends HTMLElementBase {
67
69
  setBooleanAttribute(this, "autoplay", value);
68
70
  this.applyAutoplay();
69
71
  }
72
+ /** Live loop mode, reflected by the loop attribute; changes retain time and play state. */
73
+ get loop() {
74
+ return readBooleanAttribute(this, "loop", false);
75
+ }
76
+ set loop(value) {
77
+ setBooleanAttribute(this, "loop", value);
78
+ }
70
79
  /** Controls default on; an explicit false is retained in HTML as controls="false". */
71
80
  get controls() {
72
81
  return this.controlsOverride ?? readBooleanAttribute(this, "controls", true);
@@ -121,6 +130,13 @@ export class FlowPlayerElement extends HTMLElementBase {
121
130
  set hoverFill(value) {
122
131
  setBooleanAttribute(this, "hover-fill", value);
123
132
  }
133
+ /** Opt-in GIF controls, reflected by the gif-export attribute; defaults to false. */
134
+ get gifExport() {
135
+ return readBooleanAttribute(this, "gif-export", false);
136
+ }
137
+ set gifExport(value) {
138
+ setBooleanAttribute(this, "gif-export", value);
139
+ }
124
140
  connectedCallback() {
125
141
  this.connected = true;
126
142
  this.manuallyDestroyed = false;
@@ -166,8 +182,12 @@ export class FlowPlayerElement extends HTMLElementBase {
166
182
  case "theme":
167
183
  case "reduced-motion":
168
184
  case "hover-fill":
185
+ case "gif-export":
169
186
  this.rebuildForOptions();
170
187
  break;
188
+ case "loop":
189
+ this.player?.setLoop(this.loop);
190
+ break;
171
191
  case "speed":
172
192
  this.player?.setSpeed(this.speed);
173
193
  break;
@@ -198,6 +218,10 @@ export class FlowPlayerElement extends HTMLElementBase {
198
218
  setSpeed(value) {
199
219
  this.speed = value;
200
220
  }
221
+ /** Set loop mode and retain it for future mounts. */
222
+ setLoop(value) {
223
+ this.loop = value;
224
+ }
201
225
  /** Return undefined before mounting or after destruction. */
202
226
  getState() {
203
227
  return this.player?.getState();
@@ -307,9 +331,12 @@ export class FlowPlayerElement extends HTMLElementBase {
307
331
  }
308
332
  forwardPlayerEvents(host) {
309
333
  this.removeForwardedEvents();
310
- for (const name of ["stepchange", "timeupdate", "playstatechange", "error"]) {
334
+ for (const name of ["stepchange", "timeupdate", "playstatechange", "loopchange", "error"]) {
311
335
  const listener = (event) => {
312
336
  const source = event;
337
+ if (name === "loopchange") {
338
+ setBooleanAttribute(this, "loop", source.detail.loop);
339
+ }
313
340
  this.dispatchEvent(new CustomEvent(name, {
314
341
  detail: source.detail,
315
342
  bubbles: true,
@@ -332,6 +359,8 @@ export class FlowPlayerElement extends HTMLElementBase {
332
359
  playerOptions() {
333
360
  return {
334
361
  controls: this.controls,
362
+ loop: this.loop,
363
+ gifExport: this.gifExport,
335
364
  hoverFill: this.hoverFill,
336
365
  ...(this.theme ? { theme: this.theme } : {}),
337
366
  ...(this.reducedMotion === undefined ? {} : { reducedMotion: this.reducedMotion })
@@ -1,4 +1,3 @@
1
- /** Built-in controls consume state and forward actions; the browser player owns evaluation and scheduling. */
2
1
  /** Scene selection metadata with its global start time in milliseconds. */
3
2
  interface SceneChoice {
4
3
  id: string;
@@ -11,23 +10,26 @@ interface ControlActions {
11
10
  step(direction: 1 | -1): void;
12
11
  seek(timeMs: number): void;
13
12
  setSpeed(speed: number): void;
13
+ toggleLoop(): void;
14
14
  }
15
15
  /** Player-state projection for controls; the scene list is replaced on document load. */
16
16
  interface ControlState {
17
17
  timeMs: number;
18
18
  totalDuration: number;
19
19
  playing: boolean;
20
+ loop: boolean;
20
21
  speed: number;
21
22
  sceneId: string | null;
22
23
  scenes: readonly SceneChoice[];
23
24
  }
24
25
  /**
25
26
  * Persistent control DOM translates input to commands; render synchronizes labels, progress and disabled states.
26
- * Own no clock or independent position; time-related controls are disabled for static documents.
27
+ * Own no clock or independent position; Time-related controls are disabled for static documents.
27
28
  */
28
29
  export declare class PlayerControls {
29
30
  readonly element: HTMLDivElement;
30
31
  private readonly play;
32
+ private readonly loop;
31
33
  private readonly previous;
32
34
  private readonly next;
33
35
  private readonly progress;
@@ -35,7 +37,9 @@ export declare class PlayerControls {
35
37
  private readonly speed;
36
38
  private sceneChoices;
37
39
  constructor(owner: Document, actions: ControlActions);
38
- /** Update existing controls; scene choices change by reference and custom speeds are added as options. */
40
+ /** Update existing controls; custom speeds join the default choices without changing committed state. */
39
41
  render(state: ControlState): void;
42
+ /** Release menu listeners before the player removes its DOM. */
43
+ destroy(): void;
40
44
  }
41
45
  export type { SceneChoice };
@@ -1,11 +1,14 @@
1
+ /** Built-in controls consume state and forward actions; the browser player owns evaluation and scheduling. */
2
+ import { SelectControl } from "./select-control.js";
1
3
  const DEFAULT_SPEEDS = [0.5, 1, 1.5, 2];
2
4
  /**
3
5
  * Persistent control DOM translates input to commands; render synchronizes labels, progress and disabled states.
4
- * Own no clock or independent position; time-related controls are disabled for static documents.
6
+ * Own no clock or independent position; Time-related controls are disabled for static documents.
5
7
  */
6
8
  export class PlayerControls {
7
9
  element;
8
10
  play;
11
+ loop;
9
12
  previous;
10
13
  next;
11
14
  progress;
@@ -16,6 +19,9 @@ export class PlayerControls {
16
19
  this.element = owner.createElement("div");
17
20
  this.element.className = "flowsvg-controls";
18
21
  this.play = createButton(owner, "Play", actions.toggle);
22
+ this.play.className = "flowsvg-play-toggle";
23
+ this.loop = createButton(owner, "Loop", actions.toggleLoop);
24
+ this.loop.className = "flowsvg-loop-toggle";
19
25
  this.previous = createButton(owner, "Previous step", () => actions.step(-1));
20
26
  this.next = createButton(owner, "Next step", () => actions.step(1));
21
27
  this.progress = owner.createElement("input");
@@ -24,39 +30,36 @@ export class PlayerControls {
24
30
  this.progress.step = "any";
25
31
  this.progress.setAttribute("aria-label", "Progress");
26
32
  this.progress.addEventListener("input", () => actions.seek(Number(this.progress.value)));
27
- this.scene = createSelect(owner, "Scene");
28
- this.scene.addEventListener("change", () => {
29
- const choice = this.sceneChoices?.find((scene) => scene.id === this.scene.value);
33
+ this.scene = new SelectControl(owner, "Scene", (value) => {
34
+ const choice = this.sceneChoices?.find((scene) => scene.id === value);
30
35
  if (choice)
31
36
  actions.seek(choice.startMs);
32
37
  });
33
- this.speed = createSelect(owner, "Speed");
34
- this.speed.addEventListener("change", () => actions.setSpeed(Number(this.speed.value)));
35
- this.element.append(this.previous, this.play, this.next, this.progress, this.scene, this.speed);
38
+ this.speed = new SelectControl(owner, "Speed", (value) => actions.setSpeed(Number(value)));
39
+ this.element.append(this.previous, this.play, this.next, this.progress, this.scene.element, this.speed.element, this.loop);
36
40
  }
37
- /** Update existing controls; scene choices change by reference and custom speeds are added as options. */
41
+ /** Update existing controls; custom speeds join the default choices without changing committed state. */
38
42
  render(state) {
39
43
  const isStatic = state.totalDuration === 0;
40
44
  this.play.textContent = state.playing ? "Pause" : "Play";
41
45
  this.play.setAttribute("aria-label", this.play.textContent);
42
- this.play.disabled = isStatic || (!state.playing && state.timeMs >= state.totalDuration);
46
+ this.play.disabled = isStatic || (!state.playing && !state.loop && state.timeMs >= state.totalDuration);
47
+ this.loop.setAttribute("aria-pressed", String(state.loop));
48
+ this.loop.disabled = isStatic;
43
49
  this.previous.disabled = isStatic || state.timeMs <= 0;
44
50
  this.next.disabled = isStatic || state.timeMs >= state.totalDuration;
45
51
  this.progress.disabled = isStatic;
46
52
  this.progress.max = String(state.totalDuration);
47
53
  this.progress.value = String(state.timeMs);
48
- this.scene.disabled = isStatic;
49
- this.speed.disabled = isStatic;
50
- if (this.sceneChoices !== state.scenes) {
51
- this.sceneChoices = state.scenes;
52
- this.scene.replaceChildren(...state.scenes.map((scene) => createOption(this.scene.ownerDocument, scene.id, scene.label)));
53
- }
54
- this.scene.value = state.sceneId ?? "";
54
+ this.sceneChoices = state.scenes;
55
+ this.scene.render(state.scenes.map((scene) => ({ value: scene.id, label: scene.label })), state.sceneId ?? "", isStatic);
55
56
  const speeds = [...new Set([...DEFAULT_SPEEDS, state.speed])].sort((left, right) => left - right);
56
- if (!Array.from(this.speed.options).some((option) => Number(option.value) === state.speed)) {
57
- this.speed.replaceChildren(...speeds.map((speed) => createOption(this.speed.ownerDocument, String(speed), `${speed}×`)));
58
- }
59
- this.speed.value = String(state.speed);
57
+ this.speed.render(speeds.map((speed) => ({ value: String(speed), label: `${speed}×` })), String(state.speed), isStatic);
58
+ }
59
+ /** Release menu listeners before the player removes its DOM. */
60
+ destroy() {
61
+ this.scene.destroy();
62
+ this.speed.destroy();
60
63
  }
61
64
  }
62
65
  function createButton(owner, label, click) {
@@ -67,14 +70,3 @@ function createButton(owner, label, click) {
67
70
  button.addEventListener("click", click);
68
71
  return button;
69
72
  }
70
- function createSelect(owner, label) {
71
- const select = owner.createElement("select");
72
- select.setAttribute("aria-label", label);
73
- return select;
74
- }
75
- function createOption(owner, value, label) {
76
- const option = owner.createElement("option");
77
- option.value = value;
78
- option.textContent = label;
79
- return option;
80
- }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Optional player GIF UI. One task owns its input, abort signal and download resources independently of playback.
3
+ * The browser exporter stays behind a dynamic import; portable builds replace only that import with an inline bridge.
4
+ */
5
+ import type { DiagramDocument, ThemeName } from "../core/model.js";
6
+ interface ExportInput {
7
+ document: DiagramDocument;
8
+ theme: ThemeName;
9
+ }
10
+ /** Own the visible export state and one cancellable task; destroy silences asynchronous results and releases downloads. */
11
+ export declare class GifExportControls {
12
+ private readonly owner;
13
+ private readonly captureInput;
14
+ readonly element: HTMLSpanElement;
15
+ private readonly startButton;
16
+ private readonly cancelButton;
17
+ private readonly status;
18
+ private readonly progress;
19
+ private task;
20
+ private destroyed;
21
+ private readonly downloadUrls;
22
+ constructor(owner: Document, captureInput: () => ExportInput);
23
+ destroy(): void;
24
+ /** Reserve the slot and capture input before loading code so repeated clicks and document loads cannot race the import. */
25
+ private start;
26
+ private cancel;
27
+ private run;
28
+ private isActive;
29
+ private renderProgress;
30
+ private download;
31
+ }
32
+ export {};