flowsvg 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/PATTERNS.md +71 -4
  3. package/README.md +148 -438
  4. package/README.zh-CN.md +148 -438
  5. package/dist/adapters/react.d.ts +55 -0
  6. package/dist/adapters/react.js +138 -0
  7. package/dist/adapters/vue.d.ts +158 -0
  8. package/dist/adapters/vue.js +177 -0
  9. package/dist/adapters/web-component.d.ts +111 -0
  10. package/dist/adapters/web-component.js +425 -0
  11. package/dist/{player-controls.d.ts → browser/controls.d.ts} +7 -3
  12. package/dist/{player-controls.js → browser/controls.js} +23 -31
  13. package/dist/browser/export-controls.d.ts +32 -0
  14. package/dist/browser/export-controls.js +151 -0
  15. package/dist/browser/player.d.ts +77 -0
  16. package/dist/browser/player.js +412 -0
  17. package/dist/browser/select-control.d.ts +33 -0
  18. package/dist/browser/select-control.js +171 -0
  19. package/dist/browser.d.ts +5 -69
  20. package/dist/browser.js +3 -353
  21. package/dist/core/color.d.ts +3 -0
  22. package/dist/core/color.js +18 -0
  23. package/dist/core/diagram.d.ts +40 -0
  24. package/dist/core/diagram.js +481 -0
  25. package/dist/core/edge-geometry.d.ts +45 -0
  26. package/dist/core/edge-geometry.js +609 -0
  27. package/dist/{core.d.ts → core/model.d.ts} +37 -61
  28. package/dist/core/model.js +1 -0
  29. package/dist/core/node-content-measurement.d.ts +34 -0
  30. package/dist/core/node-content-measurement.js +66 -0
  31. package/dist/{node-content-paint.d.ts → core/node-content-paint.d.ts} +2 -2
  32. package/dist/{node-content-paint.js → core/node-content-paint.js} +11 -20
  33. package/dist/{node-content.d.ts → core/node-content.d.ts} +18 -1
  34. package/dist/{node-content.js → core/node-content.js} +30 -6
  35. package/dist/core/svg-number.d.ts +5 -0
  36. package/dist/core/svg-number.js +7 -0
  37. package/dist/{theme.d.ts → core/theme.d.ts} +6 -0
  38. package/dist/core/theme.js +17 -0
  39. package/dist/{timeline.d.ts → core/timeline.d.ts} +11 -5
  40. package/dist/{timeline.js → core/timeline.js} +30 -22
  41. package/dist/core/validation.d.ts +11 -0
  42. package/dist/core/validation.js +399 -0
  43. package/dist/export/browser.d.ts +9 -0
  44. package/dist/export/browser.js +172 -0
  45. package/dist/export/captions.d.ts +18 -0
  46. package/dist/export/captions.js +44 -0
  47. package/dist/export/plan.d.ts +25 -0
  48. package/dist/export/plan.js +94 -0
  49. package/dist/export/types.d.ts +64 -0
  50. package/dist/export/types.js +9 -0
  51. package/dist/export/worker-source.d.ts +1 -0
  52. package/dist/export/worker-source.js +1 -0
  53. package/dist/export.d.ts +3 -0
  54. package/dist/export.js +2 -0
  55. package/dist/index.d.ts +7 -1
  56. package/dist/index.js +3 -1
  57. package/dist/node/cli.d.ts +2 -0
  58. package/dist/node/cli.js +72 -0
  59. package/dist/node/gif.d.ts +16 -0
  60. package/dist/node/gif.js +184 -0
  61. package/dist/node/html.d.ts +19 -0
  62. package/dist/node/html.js +61 -0
  63. package/dist/node/output.d.ts +2 -0
  64. package/dist/node/output.js +22 -0
  65. package/dist/node.d.ts +6 -0
  66. package/dist/node.js +3 -0
  67. package/dist/portable/LICENSE.esbuild.txt +21 -0
  68. package/dist/portable/LICENSE.flowsvg.txt +21 -0
  69. package/dist/portable/LICENSE.gifenc.txt +21 -0
  70. package/dist/portable/export.js +5 -0
  71. package/dist/portable/manifest.json +36 -0
  72. package/dist/portable/player.js +2 -0
  73. package/dist/portable/styles.css +227 -0
  74. package/dist/react.d.ts +3 -48
  75. package/dist/react.js +2 -126
  76. package/dist/styles.css +194 -14
  77. package/dist/vue.d.ts +3 -132
  78. package/dist/vue.js +2 -161
  79. package/dist/web-component.d.ts +3 -103
  80. package/dist/web-component.js +2 -396
  81. package/docs/images/entity-graph.png +0 -0
  82. package/docs/images/facts-timeline.png +0 -0
  83. package/docs/images/facts-timeline.zh-CN.png +0 -0
  84. package/docs/images/flowsvg-wordmark.gif +0 -0
  85. package/docs/images/node-content.png +0 -0
  86. package/docs/images/numbered-list.png +0 -0
  87. package/docs/images/numbered-list.zh-CN.png +0 -0
  88. package/docs/images/quick-start.png +0 -0
  89. package/docs/images/record-tags.png +0 -0
  90. package/package.json +38 -6
  91. package/dist/core.js +0 -1149
  92. package/dist/theme.js +0 -6
  93. /package/dist/{player-accessibility.d.ts → browser/accessibility.d.ts} +0 -0
  94. /package/dist/{player-accessibility.js → browser/accessibility.js} +0 -0
  95. /package/dist/{player-lifecycle.d.ts → browser/lifecycle.d.ts} +0 -0
  96. /package/dist/{player-lifecycle.js → browser/lifecycle.js} +0 -0
  97. /package/dist/{svg-dom.d.ts → browser/svg-dom.d.ts} +0 -0
  98. /package/dist/{svg-dom.js → browser/svg-dom.js} +0 -0
@@ -1,4 +1,10 @@
1
+ import { mixColor } from "./color.js";
2
+ import { DiagramValidationError } from "./validation.js";
1
3
  const DEFAULT_TRANSITION_MS = 250;
4
+ const FLOW_ARRIVAL_TRANSITION_MS = 200;
5
+ const FLOW_FULL_HIGHLIGHT_HOLD_MS = 100;
6
+ const FLOW_DEPARTURE_TRANSITION_MS = 200;
7
+ const FLOW_ARRIVAL_HOLD_MS = FLOW_ARRIVAL_TRANSITION_MS + FLOW_FULL_HIGHLIGHT_HOLD_MS + FLOW_DEPARTURE_TRANSITION_MS;
2
8
  const NUMERIC_VISUAL_FIELDS = ["x", "y", "width", "height", "opacity", "strokeWidth"];
3
9
  const DEFAULT_STROKE_WIDTH = 2;
4
10
  /**
@@ -8,10 +14,11 @@ const DEFAULT_STROKE_WIDTH = 2;
8
14
  */
9
15
  export class CompiledTimeline {
10
16
  totalDuration;
17
+ schedule;
11
18
  beats;
12
19
  initial;
13
20
  title;
14
- constructor(document) {
21
+ constructor(document, movementDurations = new Map()) {
15
22
  this.initial = new Map([
16
23
  ...document.nodes.map((node) => [node.id, {
17
24
  visible: node.visible !== false, highlighted: false, kind: "node", values: {
@@ -30,7 +37,7 @@ export class CompiledTimeline {
30
37
  let focus = null;
31
38
  let startMs = 0;
32
39
  let caption = document.title;
33
- for (const scene of document.scenes) {
40
+ for (const [sceneIndex, scene] of document.scenes.entries()) {
34
41
  scene.beats.forEach((beat, index) => {
35
42
  // Copy the index, then replace changed objects/values so earlier beat snapshots retain their original state.
36
43
  const after = new Map(state);
@@ -38,7 +45,8 @@ export class CompiledTimeline {
38
45
  const flows = [];
39
46
  for (const action of beat.actions) {
40
47
  if (action.type === "flow") {
41
- flows.push({ target: action.target, direction: action.direction, ...(action.text !== undefined ? { text: action.text } : {}) });
48
+ flows.push({ target: action.target, direction: action.direction, highlightParticipants: action.highlightParticipants ?? true,
49
+ ...(action.text !== undefined ? { text: action.text } : {}) });
42
50
  continue;
43
51
  }
44
52
  if (action.type === "focus") {
@@ -63,13 +71,25 @@ export class CompiledTimeline {
63
71
  }
64
72
  }
65
73
  caption = beat.caption ?? caption;
66
- const endMs = startMs + beat.durationMs;
67
- beats.push({ startMs, endMs, transitionMs: beat.transitionMs ?? Math.min(DEFAULT_TRANSITION_MS, beat.durationMs), sceneId: scene.id, beatId: `${scene.id}:${index}`, caption, focusBefore, focusAfter: focus, before: state, after, flows });
74
+ const beatId = `${scene.id}:${index}`;
75
+ const movementMs = movementDurations.get(beatId) ?? beat.durationMs;
76
+ // Reference compilation keeps authored timing; effective single-flow beats own a fixed arrival hold.
77
+ const durationMs = movementDurations.has(beatId) ? movementMs + FLOW_ARRIVAL_HOLD_MS : beat.durationMs;
78
+ const endMs = startMs + durationMs;
79
+ // A shortened beat must finish its opening transition before the next snapshot takes over.
80
+ const transitionMs = Math.min(durationMs, beat.transitionMs ?? Math.min(DEFAULT_TRANSITION_MS, beat.durationMs));
81
+ if (!Number.isFinite(movementMs) || !Number.isFinite(endMs)) {
82
+ throw new DiagramValidationError([{ path: `scenes[${sceneIndex}].beats[${index}].durationMs`, message: "effective beat timing must be finite" }]);
83
+ }
84
+ beats.push({ startMs, endMs, movementMs, transitionMs, sceneId: scene.id, beatId, beatIndex: index, caption, focusBefore, focusAfter: focus, before: state, after, flows });
68
85
  state = after;
69
86
  startMs = endMs;
70
87
  });
71
88
  }
72
89
  this.beats = beats;
90
+ this.schedule = Object.freeze(beats.map(({ sceneId, beatId, beatIndex, startMs, endMs, caption }) => Object.freeze({
91
+ sceneId, beatId, beatIndex, startMs, endMs, ...(caption !== undefined ? { caption } : {})
92
+ })));
73
93
  this.totalDuration = startMs;
74
94
  }
75
95
  /** Clamp finite times globally; reduced motion skips interpolation but preserves beat selection and total duration. */
@@ -97,6 +117,11 @@ export class CompiledTimeline {
97
117
  sceneId: beat?.sceneId ?? null,
98
118
  beatId: beat?.beatId ?? null,
99
119
  beatProgress: beat ? (time - beat.startMs) / (beat.endMs - beat.startMs) : 0,
120
+ flowProgress: beat ? Math.min(1, (time - beat.startMs) / beat.movementMs) : 0,
121
+ flowArrivalProgress: !beat || options.reducedMotion || beat.flows.length !== 1 ? 1
122
+ : easeInOutCubic(Math.max(0, Math.min(1, (time - beat.startMs - beat.movementMs) / FLOW_ARRIVAL_TRANSITION_MS))),
123
+ flowDepartureProgress: !beat || options.reducedMotion || beat.flows.length !== 1 ? 0
124
+ : easeInOutCubic(Math.max(0, Math.min(1, (time - beat.endMs + FLOW_DEPARTURE_TRANSITION_MS) / FLOW_DEPARTURE_TRANSITION_MS))),
100
125
  transitionProgress: progress,
101
126
  focusBefore: beat?.focusBefore ?? null,
102
127
  focusAfter: beat?.focusAfter ?? null,
@@ -145,20 +170,3 @@ function interpolateValues(source, target, progress, palette) {
145
170
  stroke: mixColor(source.values.stroke ?? strokeFallback, target.values.stroke ?? strokeFallback, progress)
146
171
  };
147
172
  }
148
- /** Interpolate validated hexadecimal RGB/RGBA channels; preserve endpoint strings and normalize intermediate colors. */
149
- export function mixColor(source, target, progress) {
150
- if (progress === 0)
151
- return source;
152
- if (progress === 1)
153
- return target;
154
- const channels = (color) => {
155
- const hex = color.slice(1);
156
- const expanded = hex.length <= 4 ? [...hex].map((digit) => digit + digit).join("") : hex;
157
- return [0, 2, 4, 6].map((offset) => offset < expanded.length ? parseInt(expanded.slice(offset, offset + 2), 16) : 255);
158
- };
159
- const from = channels(source);
160
- const to = channels(target);
161
- const values = from.map((value, index) => Math.round(value + (to[index] - value) * progress));
162
- const hex = values.map((value) => value.toString(16).padStart(2, "0"));
163
- return `#${hex.slice(0, values[3] === 255 ? 3 : 4).join("")}`;
164
- }
@@ -0,0 +1,11 @@
1
+ import type { ValidationIssue } from "./model.js";
2
+ /** Validation error for invalid documents or author-time measurement inputs; issues retains all affected field paths. */
3
+ export declare class DiagramValidationError extends Error {
4
+ readonly issues: readonly ValidationIssue[];
5
+ constructor(issues: ValidationIssue[]);
6
+ }
7
+ /**
8
+ * Validate document structure, values and references; an empty list means validation passed.
9
+ * Accumulate issues across inspectable branches without mutating input or producing a partial Diagram.
10
+ */
11
+ export declare function validateDocument(input: unknown): ValidationIssue[];
@@ -0,0 +1,399 @@
1
+ /** Validate document fields, references and action conflicts without mutating input; content rules stay shared. */
2
+ import { supportsNodeContent, validateNodeContent } from "./node-content.js";
3
+ /** Validation error for invalid documents or author-time measurement inputs; issues retains all affected field paths. */
4
+ export class DiagramValidationError extends Error {
5
+ issues;
6
+ constructor(issues) {
7
+ super(`Invalid diagram document (${issues.length} issue${issues.length === 1 ? "" : "s"})`);
8
+ this.name = "DiagramValidationError";
9
+ this.issues = issues;
10
+ }
11
+ }
12
+ /**
13
+ * Validate document structure, values and references; an empty list means validation passed.
14
+ * Accumulate issues across inspectable branches without mutating input or producing a partial Diagram.
15
+ */
16
+ export function validateDocument(input) {
17
+ const issues = [];
18
+ if (!isRecord(input)) {
19
+ return [{ path: "$", message: "document must be an object" }];
20
+ }
21
+ validateUnknownFields(input, ["version", "title", "viewport", "theme", "nodes", "edges", "scenes"], "$", issues);
22
+ if (input.version !== 1)
23
+ issues.push({ path: "version", message: "must be 1" });
24
+ if (input.title !== undefined && typeof input.title !== "string")
25
+ issues.push({ path: "title", message: "must be a string" });
26
+ if (input.theme !== undefined && input.theme !== "light" && input.theme !== "dark")
27
+ issues.push({ path: "theme", message: "must be light or dark" });
28
+ const viewport = input.viewport;
29
+ if (!isRecord(viewport)) {
30
+ issues.push({ path: "viewport", message: "must be an object" });
31
+ }
32
+ else {
33
+ validateUnknownFields(viewport, ["width", "height", "fitHeight"], "viewport", issues);
34
+ validatePositiveFinite(viewport.width, "viewport.width", issues);
35
+ validatePositiveFinite(viewport.height, "viewport.height", issues);
36
+ if (viewport.fitHeight !== undefined) {
37
+ if (!isRecord(viewport.fitHeight))
38
+ issues.push({ path: "viewport.fitHeight", message: "must be an object" });
39
+ else {
40
+ validateUnknownFields(viewport.fitHeight, ["padding"], "viewport.fitHeight", issues);
41
+ validateNonNegativeFinite(viewport.fitHeight.padding, "viewport.fitHeight.padding", issues);
42
+ }
43
+ }
44
+ }
45
+ if (!Array.isArray(input.nodes))
46
+ issues.push({ path: "nodes", message: "must be an array" });
47
+ if (!Array.isArray(input.edges))
48
+ issues.push({ path: "edges", message: "must be an array" });
49
+ if (!Array.isArray(input.scenes))
50
+ issues.push({ path: "scenes", message: "must be an array" });
51
+ const nodes = Array.isArray(input.nodes) ? input.nodes : [];
52
+ const edges = Array.isArray(input.edges) ? input.edges : [];
53
+ const scenes = Array.isArray(input.scenes) ? input.scenes : [];
54
+ validateDenseArray(nodes, "nodes", issues);
55
+ validateDenseArray(edges, "edges", issues);
56
+ validateDenseArray(scenes, "scenes", issues);
57
+ const ids = new Set();
58
+ nodes.forEach((node, index) => {
59
+ const path = `nodes[${index}]`;
60
+ if (!isRecord(node)) {
61
+ issues.push({ path, message: "must be an object" });
62
+ return;
63
+ }
64
+ validateUnknownFields(node, ["id", "kind", "x", "y", "width", "height", "label", "content", "parentId", "visible", "style"], path, issues);
65
+ if (!isNonEmptyString(node.id))
66
+ issues.push({ path: `${path}.id`, message: "must be a non-empty string" });
67
+ else if (ids.has(node.id))
68
+ issues.push({ path: `${path}.id`, message: `duplicate id \"${node.id}\"` });
69
+ else
70
+ ids.add(node.id);
71
+ if (!isNodeKind(node.kind))
72
+ issues.push({ path: `${path}.kind`, message: "must be a supported node kind" });
73
+ validateFinite(node.x, `${path}.x`, issues);
74
+ validateFinite(node.y, `${path}.y`, issues);
75
+ validatePositiveFinite(node.width, `${path}.width`, issues);
76
+ validatePositiveFinite(node.height, `${path}.height`, issues);
77
+ if (node.label !== undefined && typeof node.label !== "string")
78
+ issues.push({ path: `${path}.label`, message: "must be a string" });
79
+ if (node.content !== undefined) {
80
+ validateNodeContent(node.content, `${path}.content`, issues);
81
+ if (node.label !== undefined)
82
+ issues.push({ path: `${path}.content`, message: "must not be declared together with label" });
83
+ if (!supportsNodeContent(node.kind))
84
+ issues.push({ path: `${path}.content`, message: "requires a rect, roundedRect or cylinder node" });
85
+ }
86
+ if (node.parentId !== undefined && !isNonEmptyString(node.parentId))
87
+ issues.push({ path: `${path}.parentId`, message: "must be a non-empty string" });
88
+ if (node.visible !== undefined && typeof node.visible !== "boolean")
89
+ issues.push({ path: `${path}.visible`, message: "must be a boolean" });
90
+ validateNodeStyle(node.style, `${path}.style`, issues);
91
+ });
92
+ const nodeById = new Map();
93
+ nodes.forEach((node) => {
94
+ if (isRecord(node) && isNonEmptyString(node.id))
95
+ nodeById.set(node.id, node);
96
+ });
97
+ nodes.forEach((node, index) => {
98
+ if (isRecord(node) && node.parentId !== undefined && isNonEmptyString(node.parentId)) {
99
+ const parent = nodeById.get(node.parentId);
100
+ if (!parent)
101
+ issues.push({ path: `nodes[${index}].parentId`, message: "must reference an existing node" });
102
+ else if (parent.kind !== "group")
103
+ issues.push({ path: `nodes[${index}].parentId`, message: "must reference a group node" });
104
+ }
105
+ });
106
+ validateParentCycles(nodes, nodeById, issues);
107
+ const edgeById = new Map();
108
+ edges.forEach((edge, index) => {
109
+ const path = `edges[${index}]`;
110
+ if (!isRecord(edge)) {
111
+ issues.push({ path, message: "must be an object" });
112
+ return;
113
+ }
114
+ validateUnknownFields(edge, ["id", "from", "to", "label", "route", "fromPort", "toPort", "visible", "style"], path, issues);
115
+ if (!isNonEmptyString(edge.id))
116
+ issues.push({ path: `${path}.id`, message: "must be a non-empty string" });
117
+ else if (ids.has(edge.id))
118
+ issues.push({ path: `${path}.id`, message: `duplicate id \"${edge.id}\"` });
119
+ else {
120
+ ids.add(edge.id);
121
+ edgeById.set(edge.id, edge);
122
+ }
123
+ validateEdgeEndpoint(edge.from, `${path}.from`, nodeById, issues);
124
+ validateEdgeEndpoint(edge.to, `${path}.to`, nodeById, issues);
125
+ if (edge.label !== undefined && typeof edge.label !== "string")
126
+ issues.push({ path: `${path}.label`, message: "must be a string" });
127
+ if (edge.route !== undefined && edge.route !== "straight" && edge.route !== "orthogonal" && edge.route !== "curved")
128
+ issues.push({ path: `${path}.route`, message: "must be straight, orthogonal or curved" });
129
+ if (edge.fromPort !== undefined && !isPortDirection(edge.fromPort))
130
+ issues.push({ path: `${path}.fromPort`, message: "must be top, right, bottom or left" });
131
+ if (edge.toPort !== undefined && !isPortDirection(edge.toPort))
132
+ issues.push({ path: `${path}.toPort`, message: "must be top, right, bottom or left" });
133
+ if (edge.visible !== undefined && typeof edge.visible !== "boolean")
134
+ issues.push({ path: `${path}.visible`, message: "must be a boolean" });
135
+ validateEdgeStyle(edge.style, `${path}.style`, issues);
136
+ });
137
+ const allIds = new Set([...nodeById.keys(), ...edgeById.keys()]);
138
+ const sceneIds = new Set();
139
+ scenes.forEach((scene, sceneIndex) => {
140
+ const path = `scenes[${sceneIndex}]`;
141
+ if (!isRecord(scene)) {
142
+ issues.push({ path, message: "must be an object" });
143
+ return;
144
+ }
145
+ validateUnknownFields(scene, ["id", "label", "beats"], path, issues);
146
+ if (!isNonEmptyString(scene.id))
147
+ issues.push({ path: `${path}.id`, message: "must be a non-empty string" });
148
+ else if (sceneIds.has(scene.id))
149
+ issues.push({ path: `${path}.id`, message: `duplicate id \"${scene.id}\"` });
150
+ else
151
+ sceneIds.add(scene.id);
152
+ if (!isNonEmptyString(scene.label))
153
+ issues.push({ path: `${path}.label`, message: "must be a non-empty string" });
154
+ if (!Array.isArray(scene.beats))
155
+ issues.push({ path: `${path}.beats`, message: "must be an array" });
156
+ else {
157
+ validateDenseArray(scene.beats, `${path}.beats`, issues);
158
+ if (scene.beats.length === 0)
159
+ issues.push({ path: `${path}.beats`, message: "must contain at least one beat" });
160
+ scene.beats.forEach((beat, beatIndex) => validateBeat(beat, `${path}.beats[${beatIndex}]`, allIds, nodeById, edgeById, issues));
161
+ }
162
+ });
163
+ return issues;
164
+ }
165
+ function isRecord(value) {
166
+ if (typeof value !== "object" || value === null || Array.isArray(value))
167
+ return false;
168
+ const prototype = Object.getPrototypeOf(value);
169
+ return prototype === Object.prototype || prototype === null;
170
+ }
171
+ function isNonEmptyString(value) {
172
+ return typeof value === "string" && value.length > 0;
173
+ }
174
+ function isNodeKind(value) {
175
+ return value === "rect" || value === "roundedRect" || value === "ellipse" || value === "cylinder" || value === "group";
176
+ }
177
+ function isPortDirection(value) {
178
+ return value === "top" || value === "right" || value === "bottom" || value === "left";
179
+ }
180
+ function validateFinite(value, path, issues) {
181
+ if (typeof value !== "number" || !Number.isFinite(value))
182
+ issues.push({ path, message: "must be a finite number" });
183
+ }
184
+ function validatePositiveFinite(value, path, issues) {
185
+ validateFinite(value, path, issues);
186
+ if (typeof value === "number" && Number.isFinite(value) && value <= 0)
187
+ issues.push({ path, message: "must be greater than zero" });
188
+ }
189
+ function validateNonNegativeFinite(value, path, issues) {
190
+ validateFinite(value, path, issues);
191
+ if (typeof value === "number" && Number.isFinite(value) && value < 0)
192
+ issues.push({ path, message: "must be zero or greater" });
193
+ }
194
+ function validateUnknownFields(record, allowed, path, issues) {
195
+ const allowedFields = new Set(allowed);
196
+ for (const key of Object.keys(record))
197
+ if (!allowedFields.has(key))
198
+ issues.push({ path: path === "$" ? key : `${path}.${key}`, message: "unknown field" });
199
+ }
200
+ function validateDenseArray(values, path, issues) {
201
+ for (let index = 0; index < values.length; index += 1)
202
+ if (!(index in values))
203
+ issues.push({ path: `${path}[${index}]`, message: "must be a value" });
204
+ }
205
+ /** Restrict colors to interpolable hexadecimal data, excluding CSS expressions and external resource references. */
206
+ function validateColor(value, path, issues) {
207
+ if (typeof value !== "string" || !/^#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})$/i.test(value))
208
+ issues.push({ path, message: "must be a hexadecimal color" });
209
+ }
210
+ function validateNodeStyle(style, path, issues) {
211
+ if (style === undefined)
212
+ return;
213
+ if (!isRecord(style)) {
214
+ issues.push({ path, message: "must be an object" });
215
+ return;
216
+ }
217
+ validateUnknownFields(style, ["fill", "stroke", "strokeWidth", "textColor", "dashed"], path, issues);
218
+ for (const key of ["fill", "stroke", "textColor"])
219
+ if (style[key] !== undefined)
220
+ validateColor(style[key], `${path}.${key}`, issues);
221
+ if (style.strokeWidth !== undefined)
222
+ validateNonNegativeFinite(style.strokeWidth, `${path}.strokeWidth`, issues);
223
+ if (style.dashed !== undefined && typeof style.dashed !== "boolean")
224
+ issues.push({ path: `${path}.dashed`, message: "must be a boolean" });
225
+ }
226
+ function validateEdgeStyle(style, path, issues) {
227
+ if (style === undefined)
228
+ return;
229
+ if (!isRecord(style)) {
230
+ issues.push({ path, message: "must be an object" });
231
+ return;
232
+ }
233
+ validateUnknownFields(style, ["stroke", "strokeWidth", "dashed"], path, issues);
234
+ if (style.stroke !== undefined)
235
+ validateColor(style.stroke, `${path}.stroke`, issues);
236
+ if (style.strokeWidth !== undefined)
237
+ validateNonNegativeFinite(style.strokeWidth, `${path}.strokeWidth`, issues);
238
+ if (style.dashed !== undefined && typeof style.dashed !== "boolean")
239
+ issues.push({ path: `${path}.dashed`, message: "must be a boolean" });
240
+ }
241
+ function validateEdgeEndpoint(value, path, nodeById, issues) {
242
+ if (!isNonEmptyString(value)) {
243
+ issues.push({ path, message: "must reference an existing node" });
244
+ return;
245
+ }
246
+ const node = nodeById.get(value);
247
+ if (!node)
248
+ issues.push({ path, message: "must reference an existing node" });
249
+ }
250
+ /** Reject parent cycles at load time so later position and visibility recursion terminates. */
251
+ function validateParentCycles(nodes, nodeById, issues) {
252
+ const reported = new Set();
253
+ for (const [id, node] of nodeById) {
254
+ const chain = new Set();
255
+ let current = id;
256
+ while (current) {
257
+ if (chain.has(current)) {
258
+ const index = nodes.findIndex((candidate) => isRecord(candidate) && candidate.id === id);
259
+ const path = index >= 0 ? `nodes[${index}].parentId` : "nodes.parentId";
260
+ if (!reported.has(path)) {
261
+ issues.push({ path, message: "group parent references form a cycle" });
262
+ reported.add(path);
263
+ }
264
+ break;
265
+ }
266
+ chain.add(current);
267
+ const parentValue = nodeById.get(current)?.parentId;
268
+ current = isNonEmptyString(parentValue) ? parentValue : undefined;
269
+ }
270
+ }
271
+ }
272
+ /** Track assignment keys per beat: allow different fields on one target but reject duplicate writes to a field. */
273
+ function validateBeat(beat, path, allIds, nodeById, edgeById, issues) {
274
+ if (!isRecord(beat)) {
275
+ issues.push({ path, message: "must be an object" });
276
+ return;
277
+ }
278
+ validateUnknownFields(beat, ["durationMs", "transitionMs", "caption", "actions"], path, issues);
279
+ validatePositiveFinite(beat.durationMs, `${path}.durationMs`, issues);
280
+ if (beat.transitionMs !== undefined) {
281
+ validateNonNegativeFinite(beat.transitionMs, `${path}.transitionMs`, issues);
282
+ if (typeof beat.durationMs === "number" && Number.isFinite(beat.durationMs) && typeof beat.transitionMs === "number" && Number.isFinite(beat.transitionMs) && beat.transitionMs > beat.durationMs)
283
+ issues.push({ path: `${path}.transitionMs`, message: "must not exceed durationMs" });
284
+ }
285
+ if (beat.caption !== undefined && typeof beat.caption !== "string")
286
+ issues.push({ path: `${path}.caption`, message: "must be a string" });
287
+ if (!Array.isArray(beat.actions)) {
288
+ issues.push({ path: `${path}.actions`, message: "must be an array" });
289
+ return;
290
+ }
291
+ validateDenseArray(beat.actions, `${path}.actions`, issues);
292
+ const assignments = new Set();
293
+ beat.actions.forEach((action, index) => validateAction(action, `${path}.actions[${index}]`, allIds, nodeById, edgeById, assignments, issues));
294
+ }
295
+ /** Validate fields/targets by action kind; visibility and highlight keys are separate from ordinary style updates. */
296
+ function validateAction(action, path, allIds, nodeById, edgeById, assignments, issues) {
297
+ if (!isRecord(action)) {
298
+ issues.push({ path, message: "must be an object" });
299
+ return;
300
+ }
301
+ if (typeof action.type !== "string") {
302
+ issues.push({ path: `${path}.type`, message: "must be a supported action" });
303
+ return;
304
+ }
305
+ const type = action.type;
306
+ const allowedByType = {
307
+ show: ["type", "target"], hide: ["type", "target"], highlight: ["type", "target"], unhighlight: ["type", "target"],
308
+ updateNode: ["type", "target", "value"], updateEdge: ["type", "target", "value"], focus: ["type", "target"],
309
+ flow: ["type", "target", "direction", "text", "highlightParticipants"]
310
+ };
311
+ const allowed = Object.hasOwn(allowedByType, type) ? allowedByType[type] : undefined;
312
+ if (!allowed) {
313
+ issues.push({ path: `${path}.type`, message: "unknown action" });
314
+ return;
315
+ }
316
+ validateUnknownFields(action, allowed, path, issues);
317
+ if (["show", "hide", "highlight", "unhighlight"].includes(type)) {
318
+ if (!isNonEmptyString(action.target)) {
319
+ issues.push({ path: `${path}.target`, message: "must reference an existing object" });
320
+ return;
321
+ }
322
+ if (!allIds.has(action.target))
323
+ issues.push({ path: `${path}.target`, message: "must reference an existing object" });
324
+ addAssignment(`${type === "show" || type === "hide" ? "visibility" : "highlight"}:${action.target}`, path, assignments, issues);
325
+ return;
326
+ }
327
+ if (type === "focus") {
328
+ if (action.target !== null && (!isNonEmptyString(action.target) || !nodeById.has(action.target)))
329
+ issues.push({ path: `${path}.target`, message: "must reference a node or group" });
330
+ if (action.target === null || (isNonEmptyString(action.target) && nodeById.has(action.target)))
331
+ addAssignment("focus", path, assignments, issues);
332
+ return;
333
+ }
334
+ if (type === "flow") {
335
+ if (!isNonEmptyString(action.target))
336
+ issues.push({ path: `${path}.target`, message: "must reference an existing edge" });
337
+ else if (!edgeById.has(action.target))
338
+ issues.push({ path: `${path}.target`, message: "must reference an existing edge" });
339
+ if (action.direction !== "forward" && action.direction !== "reverse")
340
+ issues.push({ path: `${path}.direction`, message: "must be forward or reverse" });
341
+ if (action.text !== undefined && typeof action.text !== "string")
342
+ issues.push({ path: `${path}.text`, message: "must be a string" });
343
+ if (action.highlightParticipants !== undefined && typeof action.highlightParticipants !== "boolean")
344
+ issues.push({ path: `${path}.highlightParticipants`, message: "must be a boolean" });
345
+ // Reserve the edge independently of other field errors so invalid directions cannot hide duplicate transfers.
346
+ if (isNonEmptyString(action.target) && edgeById.has(action.target))
347
+ addAssignment(`flow:${action.target}`, `${path}.target`, assignments, issues);
348
+ return;
349
+ }
350
+ if (!isNonEmptyString(action.target)) {
351
+ issues.push({ path: `${path}.target`, message: `must reference a ${type === "updateNode" ? "node" : "edge"}` });
352
+ }
353
+ else if (type === "updateNode" ? !nodeById.has(action.target) : !edgeById.has(action.target))
354
+ issues.push({ path: `${path}.target`, message: `must reference an existing ${type === "updateNode" ? "node" : "edge"}` });
355
+ validateUpdateValue(action.value, `${path}.value`, type === "updateNode", assignments, action.target, issues);
356
+ const targetNode = typeof action.target === "string" ? nodeById.get(action.target) : undefined;
357
+ if (type === "updateNode" && targetNode && isRecord(action.value) && action.value.content !== undefined && action.value.content !== null && !supportsNodeContent(targetNode.kind)) {
358
+ issues.push({ path: `${path}.value.content`, message: "requires a rect, roundedRect or cylinder node" });
359
+ }
360
+ }
361
+ function validateUpdateValue(value, path, isNode, assignments, target, issues) {
362
+ if (!isRecord(value)) {
363
+ issues.push({ path, message: "must be an object" });
364
+ return;
365
+ }
366
+ const allowed = isNode ? ["x", "y", "width", "height", "label", "content", "fill", "stroke", "opacity"] : ["label", "stroke", "strokeWidth", "opacity"];
367
+ validateUnknownFields(value, allowed, path, issues);
368
+ if (isNode && Object.hasOwn(value, "label") && Object.hasOwn(value, "content")) {
369
+ issues.push({ path: `${path}.content`, message: "must not be declared together with label" });
370
+ }
371
+ for (const key of Object.keys(value)) {
372
+ if (!allowed.includes(key))
373
+ continue;
374
+ // Both node text fields replace the same display mode, even across separate actions.
375
+ const assignmentField = isNode && (key === "label" || key === "content") ? "text-mode" : key;
376
+ addAssignment(`${isNode ? "node" : "edge"}:${String(target)}:${assignmentField}`, `${path}.${key}`, assignments, issues);
377
+ const field = value[key];
378
+ if (["x", "y", "width", "height", "opacity"].includes(key))
379
+ validateFinite(field, `${path}.${key}`, issues);
380
+ if (key === "strokeWidth")
381
+ validateNonNegativeFinite(field, `${path}.${key}`, issues);
382
+ if (["width", "height"].includes(key) && typeof field === "number" && field <= 0)
383
+ issues.push({ path: `${path}.${key}`, message: "must be greater than zero" });
384
+ if (key === "opacity" && typeof field === "number" && (field < 0 || field > 1))
385
+ issues.push({ path: `${path}.${key}`, message: "must be between zero and one" });
386
+ if (["fill", "stroke"].includes(key))
387
+ validateColor(field, `${path}.${key}`, issues);
388
+ if (key === "label" && typeof field !== "string")
389
+ issues.push({ path: `${path}.${key}`, message: "must be a string" });
390
+ if (key === "content" && field !== null)
391
+ validateNodeContent(field, `${path}.${key}`, issues);
392
+ }
393
+ }
394
+ function addAssignment(key, path, assignments, issues) {
395
+ if (assignments.has(key))
396
+ issues.push({ path, message: "duplicate beat assignment" });
397
+ else
398
+ assignments.add(key);
399
+ }
@@ -0,0 +1,9 @@
1
+ import type { DiagramDocument } from "../core/model.js";
2
+ import { type GifExportOptions } from "./types.js";
3
+ /**
4
+ * Return opaque GIF bytes without downloading or changing playback. Snapshot inputs synchronously
5
+ * and release task resources on success, failure or cancellation. Captions are measured before sampling.
6
+ * @throws {GifExportError} With a distinct code for invalid options/document, limits, caption overflow,
7
+ * unavailable browser services, rasterization, encoding or cancellation. See GifExportOptions for limits.
8
+ */
9
+ export declare function exportGif(input: DiagramDocument, options?: GifExportOptions): Promise<Uint8Array>;