@caddis/cli 0.4.44 → 0.4.46

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 (210) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +15 -3
  3. package/bundles/antigravity-plugin/scripts/claude-oss.sh +12 -1
  4. package/bundles/antigravity-plugin/skills/_registry.md +1 -0
  5. package/bundles/antigravity-plugin/skills/flow-studio/README.md +92 -0
  6. package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +102 -0
  7. package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +19 -0
  8. package/bundles/antigravity-plugin/skills/flow-studio/examples/agent-journey.json +103 -0
  9. package/bundles/antigravity-plugin/skills/flow-studio/examples/approval-flow.json +134 -0
  10. package/bundles/antigravity-plugin/skills/flow-studio/examples/ci-cd-pipeline.json +132 -0
  11. package/bundles/antigravity-plugin/skills/flow-studio/examples/hostile.json +1783 -0
  12. package/bundles/antigravity-plugin/skills/flow-studio/examples/incident-timeline.json +109 -0
  13. package/bundles/antigravity-plugin/skills/flow-studio/examples/lineage.json +1778 -0
  14. package/bundles/antigravity-plugin/skills/flow-studio/examples/order-desk.json +99 -0
  15. package/bundles/antigravity-plugin/skills/flow-studio/examples/service-map.json +133 -0
  16. package/bundles/antigravity-plugin/skills/flow-studio/examples/state-machine.json +106 -0
  17. package/bundles/antigravity-plugin/skills/flow-studio/examples/ticket-lifecycle.json +107 -0
  18. package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +142 -0
  19. package/bundles/antigravity-plugin/skills/flow-studio/package.json +11 -0
  20. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.test.tsx +82 -0
  21. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.tsx +67 -0
  22. package/bundles/antigravity-plugin/skills/flow-studio/react/README.md +37 -0
  23. package/bundles/antigravity-plugin/skills/flow-studio/react/index.ts +3 -0
  24. package/bundles/antigravity-plugin/skills/flow-studio/react/native/FlowCanvas.tsx +58 -0
  25. package/bundles/antigravity-plugin/skills/flow-studio/react/native/Inspector.tsx +27 -0
  26. package/bundles/antigravity-plugin/skills/flow-studio/react/native/index.ts +6 -0
  27. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.css +18 -0
  28. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.test.tsx +161 -0
  29. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useFlowGeometry.ts +22 -0
  30. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useTrace.ts +29 -0
  31. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useViewport.ts +75 -0
  32. package/bundles/antigravity-plugin/skills/flow-studio/react/states.tsx +13 -0
  33. package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +169 -0
  34. package/bundles/antigravity-plugin/skills/flow-studio/references/implement-in-app.md +213 -0
  35. package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +254 -0
  36. package/bundles/antigravity-plugin/skills/flow-studio/references/notes-and-spec.md +74 -0
  37. package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +415 -0
  38. package/bundles/antigravity-plugin/skills/flow-studio/references/ux-rules.md +91 -0
  39. package/bundles/antigravity-plugin/skills/flow-studio/references/verify.md +124 -0
  40. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +95 -0
  41. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +54 -0
  42. package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +680 -0
  43. package/bundles/antigravity-plugin/skills/flow-studio/scripts/default-notes.json +67 -0
  44. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  45. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  46. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  47. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +54 -0
  48. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.mjs +18 -0
  49. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  50. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +135 -0
  51. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +179 -0
  52. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  53. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  54. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/docs.test.mjs +128 -0
  55. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.mjs +24 -0
  56. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.test.mjs +29 -0
  57. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/library.test.mjs +26 -0
  58. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  59. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  60. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  61. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  62. package/bundles/antigravity-plugin/skills/flow-studio/scripts/react-proof.mjs +92 -0
  63. package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +42 -0
  64. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.test.ts +78 -0
  65. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.ts +132 -0
  66. package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +175 -0
  67. package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +239 -0
  68. package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +134 -0
  69. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.test.ts +31 -0
  70. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.ts +34 -0
  71. package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +193 -0
  72. package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +126 -0
  73. package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +253 -0
  74. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +144 -0
  75. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +95 -0
  76. package/bundles/antigravity-plugin/skills/flow-studio/src/lib.ts +20 -0
  77. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.test.ts +57 -0
  78. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.ts +29 -0
  79. package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +167 -0
  80. package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +250 -0
  81. package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +100 -0
  82. package/bundles/antigravity-plugin/skills/flow-studio/src/page.ts +21 -0
  83. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.test.ts +104 -0
  84. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.ts +106 -0
  85. package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +193 -0
  86. package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +203 -0
  87. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +33 -0
  88. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +43 -0
  89. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +135 -0
  90. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +86 -0
  91. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.test.ts +12 -0
  92. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.ts +4 -0
  93. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +73 -0
  94. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +45 -0
  95. package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +190 -0
  96. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +40 -0
  97. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +40 -0
  98. package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.test.ts +55 -0
  99. package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.ts +45 -0
  100. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +2656 -0
  101. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +23 -0
  102. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +190 -0
  103. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  104. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +2491 -0
  105. package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +50 -0
  106. package/bundles/antigravity-plugin/skills/flow-studio/tsconfig.json +17 -0
  107. package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +92 -0
  108. package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +102 -0
  109. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +19 -0
  110. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/agent-journey.json +103 -0
  111. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/approval-flow.json +134 -0
  112. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ci-cd-pipeline.json +132 -0
  113. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/hostile.json +1783 -0
  114. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/incident-timeline.json +109 -0
  115. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/lineage.json +1778 -0
  116. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/order-desk.json +99 -0
  117. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/service-map.json +133 -0
  118. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/state-machine.json +106 -0
  119. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ticket-lifecycle.json +107 -0
  120. package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +142 -0
  121. package/bundles/codex/.codex/skills/frontend/flow-studio/package.json +11 -0
  122. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.test.tsx +82 -0
  123. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.tsx +67 -0
  124. package/bundles/codex/.codex/skills/frontend/flow-studio/react/README.md +37 -0
  125. package/bundles/codex/.codex/skills/frontend/flow-studio/react/index.ts +3 -0
  126. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/FlowCanvas.tsx +58 -0
  127. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/Inspector.tsx +27 -0
  128. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/index.ts +6 -0
  129. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.css +18 -0
  130. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.test.tsx +161 -0
  131. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useFlowGeometry.ts +22 -0
  132. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useTrace.ts +29 -0
  133. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useViewport.ts +75 -0
  134. package/bundles/codex/.codex/skills/frontend/flow-studio/react/states.tsx +13 -0
  135. package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +169 -0
  136. package/bundles/codex/.codex/skills/frontend/flow-studio/references/implement-in-app.md +213 -0
  137. package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +254 -0
  138. package/bundles/codex/.codex/skills/frontend/flow-studio/references/notes-and-spec.md +74 -0
  139. package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +415 -0
  140. package/bundles/codex/.codex/skills/frontend/flow-studio/references/ux-rules.md +91 -0
  141. package/bundles/codex/.codex/skills/frontend/flow-studio/references/verify.md +124 -0
  142. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +95 -0
  143. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +54 -0
  144. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +680 -0
  145. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/default-notes.json +67 -0
  146. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  147. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  148. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  149. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +54 -0
  150. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.mjs +18 -0
  151. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  152. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +135 -0
  153. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +179 -0
  154. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  155. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  156. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/docs.test.mjs +128 -0
  157. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.mjs +24 -0
  158. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.test.mjs +29 -0
  159. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/library.test.mjs +26 -0
  160. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  161. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  162. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  163. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  164. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/react-proof.mjs +92 -0
  165. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +42 -0
  166. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.test.ts +78 -0
  167. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.ts +132 -0
  168. package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +175 -0
  169. package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +239 -0
  170. package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +134 -0
  171. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.test.ts +31 -0
  172. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.ts +34 -0
  173. package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +193 -0
  174. package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +126 -0
  175. package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +253 -0
  176. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +144 -0
  177. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +95 -0
  178. package/bundles/codex/.codex/skills/frontend/flow-studio/src/lib.ts +20 -0
  179. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.test.ts +57 -0
  180. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.ts +29 -0
  181. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +167 -0
  182. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +250 -0
  183. package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +100 -0
  184. package/bundles/codex/.codex/skills/frontend/flow-studio/src/page.ts +21 -0
  185. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.test.ts +104 -0
  186. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.ts +106 -0
  187. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +193 -0
  188. package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +203 -0
  189. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +33 -0
  190. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +43 -0
  191. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +135 -0
  192. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +86 -0
  193. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.test.ts +12 -0
  194. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.ts +4 -0
  195. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +73 -0
  196. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +45 -0
  197. package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +190 -0
  198. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +40 -0
  199. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +40 -0
  200. package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.test.ts +55 -0
  201. package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.ts +45 -0
  202. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +2656 -0
  203. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +23 -0
  204. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +190 -0
  205. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  206. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +2491 -0
  207. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +50 -0
  208. package/bundles/codex/.codex/skills/frontend/flow-studio/tsconfig.json +17 -0
  209. package/bundles/manifest.json +2 -2
  210. package/package.json +1 -1
@@ -0,0 +1,2491 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/model.ts
8
+ var NODE_HEIGHT = { card: 54, group: 54, gateway: 64, placeholder: 54, chip: 40, start: 40, end: 40, event: 40, note: 40 };
9
+ var TONES = ["ok", "warn", "crit", "info", "muted", "accent"];
10
+ var BORDERS = ["solid", "dashed", "dotted"];
11
+ var EDGES = ["flow", "dash", "dash-short", "dot"];
12
+ var MOTIONS = ["flow", "flow-slow", "breathe", "none"];
13
+ var BUCKETS = ["good", "neutral", "gap"];
14
+ function esc(s) {
15
+ const map = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
16
+ return String(s ?? "").replace(/[&<>"']/g, (c) => map[c]);
17
+ }
18
+ function validateModel(input) {
19
+ const out = [];
20
+ const add = (level, code, where, message) => out.push({ level, code, where, message });
21
+ if (!input || typeof input !== "object") {
22
+ add("error", "model-shape", "model", "The model must be a JSON object.");
23
+ return out;
24
+ }
25
+ const m = input;
26
+ if (m.version !== 1) add("error", "model-version", "version", "version must be 1.");
27
+ if (!["columns-lanes", "free", "auto"].includes(m.layout)) add("error", "layout-unsupported", "layout", `layout ${String(m.layout)} is not supported: use "columns-lanes", "free", or "auto".`);
28
+ if (!Array.isArray(m.columns) || m.layout === "columns-lanes" && m.columns.length === 0) add("error", "columns-missing", "columns", "Columns are required for columns-lanes layout.");
29
+ if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
30
+ if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
31
+ const columns = Array.isArray(m.columns) ? m.columns : [];
32
+ const lanes = Array.isArray(m.lanes) ? m.lanes : [];
33
+ const states = m.states && typeof m.states === "object" ? m.states : {};
34
+ if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
35
+ const nodes = [];
36
+ (Array.isArray(m.nodes) ? m.nodes : []).forEach((n, i) => {
37
+ if (!n || typeof n !== "object" || typeof n.id !== "string") add("error", "node-shape", `nodes[${i}]`, "A node must be an object with a string id.");
38
+ else nodes.push(n);
39
+ });
40
+ const links = [];
41
+ (Array.isArray(m.links) ? m.links : []).forEach((l, i) => {
42
+ if (!l || typeof l !== "object" || typeof l.id !== "string") add("error", "link-shape", `links[${i}]`, "A link must be an object with a string id.");
43
+ else links.push(l);
44
+ });
45
+ if (nodes.length === 0) add("warn", "model-empty", "nodes", "The model has no nodes.");
46
+ for (const [id, s] of Object.entries(states)) {
47
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) add("error", "state-id", `states[${id}]`, `State id ${id} may only use letters, digits, "_" and "-" (it becomes a CSS class).`);
48
+ const ok = s && typeof s.word === "string" && TONES.includes(s.tone) && BORDERS.includes(s.border) && EDGES.includes(s.edge) && MOTIONS.includes(s.motion) && BUCKETS.includes(s.bucket);
49
+ if (!ok) add("error", "state-shape", `states[${id}]`, `State ${id} needs word, tone, border, edge, motion and bucket with allowed values.`);
50
+ }
51
+ const laneIds = new Set(lanes.map((l) => l.id));
52
+ const stateIds = new Set(Object.keys(states));
53
+ const nodeIds = /* @__PURE__ */ new Set();
54
+ const ncols = columns.length;
55
+ for (const n of nodes) {
56
+ const w = `nodes[${n.id}]`;
57
+ if (nodeIds.has(n.id)) add("error", "node-duplicate", w, `Duplicate node id ${n.id}.`);
58
+ nodeIds.add(n.id);
59
+ if ((m.layout !== "auto" || n.lane !== void 0) && !laneIds.has(n.lane ?? "")) add("error", "lane-undefined", w, `Lane ${n.lane} is not defined.`);
60
+ for (const s of [n.state, ...Object.values(n.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
61
+ if (m.layout === "columns-lanes" && (typeof n.row !== "number" || !Number.isFinite(n.row))) add("error", "row-invalid", w, `Node ${n.id} needs a finite number for row (it sets the vertical position).`);
62
+ const span = n.colSpan ?? 1;
63
+ if (m.layout === "columns-lanes" && (!Number.isInteger(n.col) || (n.col ?? -1) < 0 || (n.col ?? 0) + span > ncols)) add("error", "column-range", w, `Column ${n.col} with span ${span} is outside 0..${ncols - 1}.`);
64
+ if (m.layout === "free") {
65
+ if (!Number.isFinite(n.x) || !Number.isFinite(n.y)) add("error", "position-invalid", w, `Node ${n.id} needs finite x and y.`);
66
+ if (n.w !== void 0 && (!Number.isFinite(n.w) || n.w <= 0) || n.h !== void 0 && (!Number.isFinite(n.h) || n.h <= 0)) add("error", "size-invalid", w, `Node ${n.id} needs positive finite width and height.`);
67
+ }
68
+ const metric = n.metrics;
69
+ const invalidSeries = metric?.series !== void 0 && (!Array.isArray(metric.series) || metric.series.length > 60 || metric.series.some((v) => !Number.isFinite(v)));
70
+ if (metric !== void 0 && (!metric || !Number.isFinite(metric.value) || metric.max != null && !Number.isFinite(metric.max) || invalidSeries)) add("error", "metric-invalid", w, "Metrics need finite values and at most 60 finite series points.");
71
+ if (n.sla !== void 0 && (!Number.isFinite(n.sla) || n.sla <= 0)) add("error", "sla-invalid", w, "SLA must be a positive finite number of seconds.");
72
+ }
73
+ for (const n of nodes) {
74
+ if (n.kind === "chip" && (!n.ref || !nodeIds.has(n.ref))) add("error", "chip-ref", `nodes[${n.id}]`, `A chip must point at a real node (ref ${n.ref}).`);
75
+ }
76
+ const owner = /* @__PURE__ */ new Map();
77
+ for (const group of nodes.filter((n) => n.contains !== void 0)) {
78
+ if (group.kind !== "group" || !Array.isArray(group.contains)) {
79
+ add("error", "contains-shape", `nodes[${group.id}]`, "Only a group may contain a list of node ids.");
80
+ continue;
81
+ }
82
+ for (const id of group.contains) {
83
+ if (id === group.id) add("error", "contains-self", `nodes[${group.id}]`, `Group ${group.id} cannot contain itself.`);
84
+ else if (!nodeIds.has(id)) add("error", "contains-undefined", `nodes[${group.id}]`, `Contained node ${id} does not exist.`);
85
+ else if (owner.has(id)) add("error", "contains-duplicate", `nodes[${group.id}]`, `Node ${id} is already contained by ${owner.get(id)}.`);
86
+ else owner.set(id, group.id);
87
+ }
88
+ }
89
+ const linkIds = /* @__PURE__ */ new Set();
90
+ const linked = /* @__PURE__ */ new Set();
91
+ for (const l of links) {
92
+ const w = `links[${l.id}]`;
93
+ if (linkIds.has(l.id)) add("error", "link-duplicate", w, `Duplicate link id ${l.id}.`);
94
+ linkIds.add(l.id);
95
+ if (!nodeIds.has(l.from) || !nodeIds.has(l.to)) add("error", "link-endpoint", w, `Link ${l.id} has an endpoint that is not a node (${l.from} to ${l.to}).`);
96
+ else {
97
+ linked.add(l.from);
98
+ linked.add(l.to);
99
+ const a = nodes.find((n) => n.id === l.from), b = nodes.find((n) => n.id === l.to);
100
+ if (m.layout !== "free" && a.col !== void 0 && b.col !== void 0) {
101
+ const forward = b.col > a.col + (a.colSpan ?? 1) - 1;
102
+ if (!forward && l.kind !== "loop-back") add("warn", "link-direction", w, `Link ${l.id} does not run left to right (column ${a.col} to ${b.col}); mark it loop-back if it returns to an earlier stage.`);
103
+ if (forward && l.kind === "loop-back") add("warn", "loop-back-forward", w, `Link ${l.id} is marked loop-back but runs forward.`);
104
+ }
105
+ }
106
+ for (const s of [l.state, ...Object.values(l.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
107
+ }
108
+ const scenarioIds = /* @__PURE__ */ new Set();
109
+ for (const [i, scenario] of (Array.isArray(m.scenarios) ? m.scenarios : []).entries()) {
110
+ const w = `scenarios[${i}]`;
111
+ if (!scenario || typeof scenario.id !== "string" || typeof scenario.label !== "string" || !Array.isArray(scenario.steps)) {
112
+ add("error", "scenario-shape", w, "A scenario needs an id, label and steps.");
113
+ continue;
114
+ }
115
+ if (scenarioIds.has(scenario.id)) add("error", "scenario-duplicate", w, `Duplicate scenario id ${scenario.id}.`);
116
+ scenarioIds.add(scenario.id);
117
+ if (!scenario.steps.length) add("error", "scenario-empty", w, "A scenario needs at least one step.");
118
+ const timed = scenario.steps.filter((s) => s && s.at !== void 0).length;
119
+ if (timed && timed !== scenario.steps.length) add("error", "scenario-at-partial", w, "Give every step an at time or none.");
120
+ let prev, previousAt = -Infinity;
121
+ for (const [j, step] of scenario.steps.entries()) {
122
+ const sw = `${w}.steps[${j}]`;
123
+ const link = links.find((l) => l.id === step?.link);
124
+ if (!link) add("error", "scenario-link", sw, `Unknown link ${step?.link}.`);
125
+ if (step?.at !== void 0) {
126
+ if (!Number.isFinite(step.at) || step.at < 0 || step.at < previousAt) add("error", "scenario-at-order", sw, "Step times must be finite, nonnegative and nondecreasing.");
127
+ if (Number.isFinite(step.at) && step.at >= 0 && step.at >= previousAt) previousAt = step.at;
128
+ }
129
+ if (step?.dur !== void 0 && (!Number.isFinite(step.dur) || step.dur <= 0)) add("error", "scenario-duration", sw, "Duration must be positive and finite.");
130
+ if (prev && link) {
131
+ const previousTo = prev.to;
132
+ if ((nodes.find((n) => n.id === previousTo)?.ref ?? previousTo) !== (nodes.find((n) => n.id === link.from)?.ref ?? link.from)) add("warn", "scenario-gap", sw, "This step does not start at the previous step's target.");
133
+ }
134
+ if (link) prev = link;
135
+ }
136
+ }
137
+ for (const n of nodes) {
138
+ if (n.kind !== "gateway") continue;
139
+ const exits = links.filter((l) => l.from === n.id);
140
+ if (exits.length === 0) add("warn", "gateway-no-exit", `nodes[${n.id}]`, `Gateway ${n.id} has no exit.`);
141
+ else if (exits.length > 1 && exits.some((l) => !l.label?.trim())) add("warn", "gateway-exit-label", `nodes[${n.id}]`, `Every exit from gateway ${n.id} needs a label.`);
142
+ }
143
+ for (const n of nodes) {
144
+ if (!linked.has(n.id) && !n.allowIsolated && n.kind !== "note" && !(n.kind === "group" && n.contains?.length)) {
145
+ add("error", "orphan", `nodes[${n.id}]`, `Node ${n.id} has no links. Add a link, a placeholder neighbour, or set allowIsolated.`);
146
+ }
147
+ }
148
+ for (let i = 0; i < nodes.length; i++) {
149
+ for (let j = i + 1; j < nodes.length; j++) {
150
+ const a = nodes[i], b = nodes[j];
151
+ if (a.contains?.includes(b.id) || b.contains?.includes(a.id)) continue;
152
+ if (m.layout === "free") {
153
+ const aw = a.w ?? 190, ah = a.h ?? NODE_HEIGHT[a.kind] ?? 54, bw = b.w ?? 190, bh = b.h ?? NODE_HEIGHT[b.kind] ?? 54;
154
+ if ([a.x, a.y, b.x, b.y, aw, ah, bw, bh].every((v) => typeof v === "number" && Number.isFinite(v)) && a.x < b.x + bw && b.x < a.x + aw && a.y < b.y + bh && b.y < a.y + ah) {
155
+ add("error", "rect-overlap", `nodes[${a.id}]`, `Nodes ${a.id} and ${b.id} overlap.`);
156
+ }
157
+ } else if (a.col !== void 0 && b.col !== void 0 && a.row !== void 0 && b.row !== void 0) {
158
+ const ax2 = a.col + (a.colSpan ?? 1), bx2 = b.col + (b.colSpan ?? 1);
159
+ if (a.col < bx2 && b.col < ax2 && Math.abs(a.row - b.row) < 1) {
160
+ add("error", "cell-overlap", `nodes[${a.id}]`, `Nodes ${a.id} and ${b.id} overlap (same columns, rows less than 1 apart).`);
161
+ }
162
+ }
163
+ }
164
+ }
165
+ const used = new Set(nodes.map((n) => n.lane));
166
+ for (const l of lanes) if (!used.has(l.id)) add("warn", "lane-empty", `lanes[${l.id}]`, `Lane ${l.id} has no nodes.`);
167
+ return out;
168
+ }
169
+
170
+ // src/autolayout.ts
171
+ function autoLayout(input) {
172
+ const nodes = input.nodes.map((n) => ({ ...n }));
173
+ const links = input.links.map((l) => ({ ...l }));
174
+ const columns = input.columns.map((c) => ({ ...c }));
175
+ const lanes = input.lanes.map((l) => ({ ...l }));
176
+ const byId = new Map(nodes.map((n) => [n.id, n]));
177
+ const index = new Map(nodes.map((n, i) => [n.id, i]));
178
+ const outgoing = new Map(nodes.map((n) => [n.id, []]));
179
+ const incoming = new Map(nodes.map((n) => [n.id, []]));
180
+ const neighbours = new Map(nodes.map((n) => [n.id, []]));
181
+ for (const link of links) {
182
+ if (!byId.has(link.from) || !byId.has(link.to)) continue;
183
+ outgoing.get(link.from).push(link);
184
+ incoming.get(link.to).push(link);
185
+ neighbours.get(link.from).push(link.to);
186
+ neighbours.get(link.to).push(link.from);
187
+ }
188
+ const color = /* @__PURE__ */ new Map();
189
+ const post = [];
190
+ for (const root of nodes) {
191
+ if (color.has(root.id)) continue;
192
+ color.set(root.id, 1);
193
+ const stack = [{ id: root.id, next: 0 }];
194
+ while (stack.length) {
195
+ const frame = stack[stack.length - 1];
196
+ const edges = outgoing.get(frame.id);
197
+ if (frame.next >= edges.length) {
198
+ color.set(frame.id, 2);
199
+ post.push(frame.id);
200
+ stack.pop();
201
+ continue;
202
+ }
203
+ const link = edges[frame.next++];
204
+ if (link.kind === "loop-back") continue;
205
+ const state = color.get(link.to) ?? 0;
206
+ if (state === 1) link.kind = "loop-back";
207
+ else if (state === 0) {
208
+ color.set(link.to, 1);
209
+ stack.push({ id: link.to, next: 0 });
210
+ }
211
+ }
212
+ }
213
+ for (const id of post.reverse()) {
214
+ const n = byId.get(id);
215
+ if (n.col !== void 0) continue;
216
+ let col = 0;
217
+ for (const edge of incoming.get(id)) {
218
+ if (edge.kind === "loop-back") continue;
219
+ const parent = byId.get(edge.from);
220
+ col = Math.max(col, (parent.col ?? 0) + (parent.colSpan ?? 1));
221
+ }
222
+ n.col = col;
223
+ }
224
+ const seen = /* @__PURE__ */ new Set();
225
+ const usedLaneIds = new Set(lanes.map((l) => l.id));
226
+ let group = 0;
227
+ for (const first of nodes) {
228
+ if (seen.has(first.id)) continue;
229
+ const queue = [first.id];
230
+ seen.add(first.id);
231
+ for (let q = 0; q < queue.length; q++) {
232
+ for (const id of neighbours.get(queue[q])) if (!seen.has(id)) {
233
+ seen.add(id);
234
+ queue.push(id);
235
+ }
236
+ }
237
+ const missing = queue.map((id) => byId.get(id)).filter((n) => !n.lane);
238
+ if (!missing.length) continue;
239
+ group++;
240
+ let laneId = `group-${group}`;
241
+ while (usedLaneIds.has(laneId)) laneId += "-auto";
242
+ usedLaneIds.add(laneId);
243
+ lanes.push({ id: laneId, title: `Group ${group}` });
244
+ for (const n of missing) n.lane = laneId;
245
+ }
246
+ const rank = new Map(nodes.map((n, i) => [n.id, i]));
247
+ const laneOrder = lanes.map((l) => l.id);
248
+ const cells = /* @__PURE__ */ new Map();
249
+ for (const n of nodes) {
250
+ const key = `${n.lane}\0${n.col}`;
251
+ if (!cells.has(key)) cells.set(key, []);
252
+ cells.get(key).push(n);
253
+ }
254
+ const maxCol = Math.max(0, ...nodes.map((n) => (n.col ?? 0) + (n.colSpan ?? 1) - 1));
255
+ for (let sweep = 0; sweep < 4; sweep++) {
256
+ const forward = sweep % 2 === 0;
257
+ const layers = Array.from({ length: maxCol + 1 }, (_, i) => forward ? i : maxCol - i);
258
+ for (const lane of laneOrder) for (const col of layers) {
259
+ const groupNodes = cells.get(`${lane}\0${col}`);
260
+ if (!groupNodes || groupNodes.length < 2) continue;
261
+ const score = (n) => {
262
+ const edges = forward ? incoming.get(n.id) : outgoing.get(n.id);
263
+ const ns = edges.filter((e) => e.kind !== "loop-back").map((e) => forward ? e.from : e.to);
264
+ return ns.length ? ns.reduce((sum, id) => sum + (rank.get(id) ?? 0), 0) / ns.length : rank.get(n.id) ?? 0;
265
+ };
266
+ groupNodes.sort((a, b) => score(a) - score(b) || index.get(a.id) - index.get(b.id));
267
+ groupNodes.forEach((n, i) => rank.set(n.id, i));
268
+ }
269
+ }
270
+ let nextBase = 0;
271
+ const placed = nodes.filter((n) => n.row !== void 0);
272
+ for (const lane of laneOrder) {
273
+ const members = nodes.filter((n) => n.lane === lane);
274
+ if (!members.length) continue;
275
+ const ordered = [...members].sort((a, b) => (a.col ?? 0) - (b.col ?? 0) || (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0) || index.get(a.id) - index.get(b.id));
276
+ const rowAtCol = /* @__PURE__ */ new Map();
277
+ for (const n of ordered) {
278
+ if (n.row !== void 0) continue;
279
+ let row = Math.max(nextBase, rowAtCol.get(n.col ?? 0) ?? nextBase);
280
+ while (placed.some((p) => (n.col ?? 0) < (p.col ?? 0) + (p.colSpan ?? 1) && (p.col ?? 0) < (n.col ?? 0) + (n.colSpan ?? 1) && Math.abs(row - (p.row ?? 0)) < 1)) row += 1;
281
+ n.row = row;
282
+ placed.push(n);
283
+ rowAtCol.set(n.col ?? 0, row + 1);
284
+ }
285
+ const last = Math.max(nextBase - 1, ...members.map((n) => n.row ?? 0));
286
+ const loops = links.filter((l) => l.kind === "loop-back" && byId.get(l.from)?.lane === lane && byId.get(l.to)?.lane === lane).length;
287
+ nextBase = last + 1.75 + loops * 0.35;
288
+ }
289
+ const needed = Math.max(0, ...nodes.map((n) => (n.col ?? 0) + (n.colSpan ?? 1)));
290
+ while (columns.length < needed) {
291
+ const i = columns.length + 1;
292
+ columns.push({ id: `stage-${i}`, title: `Stage ${i}`, width: 190 });
293
+ }
294
+ return { ...input, nodes, links, columns, lanes };
295
+ }
296
+
297
+ // src/stateCss.ts
298
+ var cssId = (id) => id.replace(/[^A-Za-z0-9_-]/g, "_");
299
+ var TONES2 = { ok: "var(--ok)", warn: "var(--warn)", crit: "var(--crit)", info: "var(--up)", muted: "var(--muted)", accent: "var(--accent)" };
300
+ var toneVar = (t) => Object.hasOwn(TONES2, t) ? TONES2[t] : TONES2.muted;
301
+ var DASH = { flow: "5 9", dash: "7 6", "dash-short": "3 6", dot: "2 6" };
302
+ var KEYFRAMES = { flow: "flow", dash: "flowd", "dash-short": "flowc", dot: "" };
303
+ function stateCss(states) {
304
+ const out = [];
305
+ const reduced = [];
306
+ for (const [id, s] of Object.entries(states)) {
307
+ const c = cssId(id);
308
+ const T = toneVar(s.tone);
309
+ if (s.border === "dotted") {
310
+ out.push(`.node.st-${c} { border:1px dotted ${T}; background:color-mix(in srgb, ${T} 10%, var(--panel)); --breath:${T}; }`);
311
+ out.push(`.node.st-${c} .t { color:${T}; }`);
312
+ } else {
313
+ const glow = s.bucket === "good" ? ` box-shadow:0 0 14px -4px color-mix(in srgb, ${T} 55%, transparent), var(--shadow);` : "";
314
+ out.push(`.node.st-${c} { border-left:4px ${s.border} ${T};${glow} --breath:${T}; }`);
315
+ }
316
+ out.push(`.node.st-${c}.gateway polygon { stroke:${T}; fill:color-mix(in srgb, ${T} 10%, var(--panel)); }`);
317
+ out.push(`.ev.st-${c} { color:${T}; }`);
318
+ if (s.motion === "breathe") out.push(`.node.st-${c}:not(.dim) { animation:breathe 3.2s ease-in-out infinite; }`);
319
+ const edge = Object.hasOwn(DASH, s.edge) ? s.edge : "dash";
320
+ const flows = edge !== "dot" && (s.motion === "flow" || s.motion === "flow-slow");
321
+ let rule = `stroke:${T}; stroke-dasharray:${DASH[edge]}; opacity:${s.motion === "flow" ? ".85" : ".9"};`;
322
+ if (edge === "dot") rule += " stroke-linecap:round;";
323
+ if (flows) rule += ` animation:${KEYFRAMES[edge]} ${s.motion === "flow" ? "3s" : "7s"} linear infinite;`;
324
+ if (flows && s.motion === "flow") rule += ` filter:drop-shadow(0 0 3px ${T});`;
325
+ out.push(`path.edge.st-${c} { ${rule} }`);
326
+ if (s.bucket === "gap") out.push(`path.edge.hot.st-${c} { stroke:${T}; filter:drop-shadow(0 0 5px ${T}); }`);
327
+ if (flows) reduced.push(`path.edge.st-${c} { stroke-dasharray:none; }`);
328
+ }
329
+ if (reduced.length) out.push(`@media (prefers-reduced-motion: reduce) { ${reduced.join(" ")} }`);
330
+ return out.join("\n");
331
+ }
332
+
333
+ // src/rules.ts
334
+ var rules_exports = {};
335
+ __export(rules_exports, {
336
+ baselineView: () => baselineView,
337
+ bucketOf: () => bucketOf,
338
+ canonOf: () => canonOf,
339
+ columnTitle: () => columnTitle,
340
+ defaultView: () => defaultView,
341
+ effectiveLinks: () => effectiveLinks,
342
+ groupFor: () => groupFor,
343
+ initialCollapsed: () => initialCollapsed,
344
+ isImproved: () => isImproved,
345
+ isNew: () => isNew,
346
+ linkStateId: () => linkStateId,
347
+ linkVisible: () => linkVisible,
348
+ nodeStateId: () => nodeStateId,
349
+ nodeVisible: () => nodeVisible,
350
+ toggleGroupState: () => toggleGroupState
351
+ });
352
+ var baselineView = (m) => m.views?.[0]?.id ?? "all";
353
+ var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
354
+ var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
355
+ var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
356
+ function groupFor(m, nodeId) {
357
+ return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
358
+ }
359
+ var nodeVisible = (m, view, n, collapsed) => {
360
+ if (n.visibleIn && !n.visibleIn.includes(view)) return false;
361
+ if (!collapsed) return true;
362
+ if (n.kind === "group" && n.contains) return collapsed.has(n.id);
363
+ const owner = groupFor(m, n.id);
364
+ return !owner || !collapsed.has(owner.id);
365
+ };
366
+ function toggleGroupState(m, view, collapsed, id) {
367
+ const group = m.nodes.find((n) => n.id === id && n.kind === "group" && n.contains?.length);
368
+ if (!group) return null;
369
+ const next = new Set(collapsed);
370
+ const expanding = next.has(id);
371
+ if (expanding) next.delete(id);
372
+ else next.add(id);
373
+ const selected = expanding ? group.contains?.find((child) => {
374
+ const n = m.nodes.find((candidate) => candidate.id === child);
375
+ return !!n && nodeVisible(m, view, n, next);
376
+ }) ?? null : id;
377
+ return { collapsed: next, selected, expanding };
378
+ }
379
+ var nodeStateId = (_m, view, n) => n.stateIn?.[view] ?? n.state;
380
+ function linkVisible(m, view, l, nodes) {
381
+ if (l.visibleIn && !l.visibleIn.includes(view)) return false;
382
+ const a = nodes.get(l.from);
383
+ const b = nodes.get(l.to);
384
+ return !!a && !!b && nodeVisible(m, view, a) && nodeVisible(m, view, b);
385
+ }
386
+ var linkStateId = (_m, view, l) => l.stateIn?.[view] ?? l.state;
387
+ var bucketOf = (m, stateId) => m.states[stateId]?.bucket ?? "gap";
388
+ var isNew = (m, view, n) => view !== baselineView(m) && nodeVisible(m, view, n) && !nodeVisible(m, baselineView(m), n);
389
+ var isImproved = (m, view, n) => {
390
+ const base = baselineView(m);
391
+ return view !== base && nodeVisible(m, base, n) && bucketOf(m, nodeStateId(m, base, n)) === "gap" && bucketOf(m, nodeStateId(m, view, n)) !== "gap";
392
+ };
393
+ var canonOf = (nodes, id) => nodes.get(id)?.ref ?? id;
394
+ function effectiveLinks(m, view, collapsed, nodes) {
395
+ const byPair = /* @__PURE__ */ new Map();
396
+ const rank = { good: 0, neutral: 1, gap: 2 };
397
+ for (const link of m.links) {
398
+ if (link.visibleIn && !link.visibleIn.includes(view)) continue;
399
+ const a = nodes.get(link.from), b = nodes.get(link.to);
400
+ if (!a || !b || a.visibleIn && !a.visibleIn.includes(view) || b.visibleIn && !b.visibleIn.includes(view)) continue;
401
+ const groupA = groupFor(m, a.id), groupB = groupFor(m, b.id);
402
+ const from = groupA && collapsed.has(groupA.id) ? groupA.id : a.id;
403
+ const to = groupB && collapsed.has(groupB.id) ? groupB.id : b.id;
404
+ if (from === to) continue;
405
+ const fromNode = nodes.get(from), toNode = nodes.get(to);
406
+ if (!fromNode || !toNode || !nodeVisible(m, view, fromNode, collapsed) || !nodeVisible(m, view, toNode, collapsed)) continue;
407
+ const state = linkStateId(m, view, link);
408
+ const key = JSON.stringify([from, to]);
409
+ const prev = byPair.get(key);
410
+ if (!prev) byPair.set(key, { ...link, from, to, state, stateIn: void 0 });
411
+ else if (rank[bucketOf(m, state)] > rank[bucketOf(m, prev.state)]) prev.state = state;
412
+ }
413
+ return [...byPair.values()];
414
+ }
415
+
416
+ // src/trace.ts
417
+ var trace_exports = {};
418
+ __export(trace_exports, {
419
+ connected: () => connected,
420
+ relatives: () => relatives,
421
+ withinHops: () => withinHops
422
+ });
423
+ function relatives(edges, canonOf2, start, dir) {
424
+ const set = /* @__PURE__ */ new Set([canonOf2(start)]);
425
+ let grew = true;
426
+ while (grew) {
427
+ grew = false;
428
+ for (const e of edges) {
429
+ const a = canonOf2(e.from);
430
+ const b = canonOf2(e.to);
431
+ if (dir === "up" && set.has(b) && !set.has(a)) {
432
+ set.add(a);
433
+ grew = true;
434
+ }
435
+ if (dir === "down" && set.has(a) && !set.has(b)) {
436
+ set.add(b);
437
+ grew = true;
438
+ }
439
+ }
440
+ }
441
+ return set;
442
+ }
443
+ function connected(edges, canonOf2, start) {
444
+ return /* @__PURE__ */ new Set([...relatives(edges, canonOf2, start, "up"), ...relatives(edges, canonOf2, start, "down")]);
445
+ }
446
+ function withinHops(edges, canonOf2, starts, n) {
447
+ const found = new Set(starts.map(canonOf2));
448
+ const radius = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
449
+ let frontier = [...found];
450
+ for (let hop = 0; hop < radius && frontier.length; hop++) {
451
+ const next = [];
452
+ const current = new Set(frontier);
453
+ for (const edge of edges) {
454
+ const a = canonOf2(edge.from), b = canonOf2(edge.to);
455
+ if (current.has(a) && !found.has(b)) {
456
+ found.add(b);
457
+ next.push(b);
458
+ }
459
+ if (current.has(b) && !found.has(a)) {
460
+ found.add(a);
461
+ next.push(a);
462
+ }
463
+ }
464
+ frontier = next;
465
+ }
466
+ return found;
467
+ }
468
+
469
+ // src/selection.ts
470
+ function notifySelection(previous, next, onSelect) {
471
+ if (previous !== next) onSelect?.(next);
472
+ }
473
+
474
+ // src/interact.ts
475
+ function relationsOf(ctx) {
476
+ const given = ctx.model.inspector?.relations;
477
+ if (given && given.length) return given;
478
+ const last = Math.max(0, ctx.model.columns.length - 1);
479
+ return [
480
+ { id: "up", label: "Upstream", dir: "up" },
481
+ { id: "down", label: "Downstream", dir: "down" },
482
+ { id: "end", label: "At the end", dir: "down", filter: { col: last } }
483
+ ];
484
+ }
485
+ function visibleEdges(ctx) {
486
+ return effectiveLinks(ctx.model, ctx.mode, ctx.collapsed, ctx.nodes).map((l) => ({ from: l.from, to: l.to }));
487
+ }
488
+ function relationSet(ctx, rel, start) {
489
+ const canon = (id) => canonOf(ctx.nodes, id);
490
+ const set = relatives(visibleEdges(ctx), canon, start, rel.dir);
491
+ if (!rel.filter) return set;
492
+ const out = /* @__PURE__ */ new Set([canon(start)]);
493
+ for (const id of set) {
494
+ const n = ctx.nodes.get(id);
495
+ if (!n) continue;
496
+ if ((rel.filter.col === void 0 || n.col === rel.filter.col) && (rel.filter.kind === void 0 || n.kind === rel.filter.kind)) out.add(id);
497
+ }
498
+ return out;
499
+ }
500
+ function matches(ctx, n) {
501
+ const q = ctx.query;
502
+ return !!q && (n.title.toLowerCase().includes(q) || (n.members ?? []).some((m) => m.toLowerCase().includes(q)));
503
+ }
504
+ function install(ctx) {
505
+ const { els, model } = ctx;
506
+ const canon = (id) => canonOf(ctx.nodes, id);
507
+ const visibleNodes = () => model.nodes.filter((n) => nodeVisible(model, ctx.mode, n, ctx.collapsed));
508
+ const inspInset = () => els.inspector.classList.contains("open") ? 340 : 0;
509
+ function activeSet(id) {
510
+ const edges = visibleEdges(ctx);
511
+ if (id) {
512
+ if (id === ctx.selected && ctx.hl) {
513
+ const rel = relationsOf(ctx).find((r) => r.id === ctx.hl);
514
+ if (rel) return relationSet(ctx, rel, id);
515
+ }
516
+ return connected(edges, canon, id);
517
+ }
518
+ if (ctx.filter) {
519
+ return new Set(visibleNodes().filter((n) => {
520
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
521
+ return ctx.filter === "gap" ? b === "gap" : !!n.upgrade && b !== "good";
522
+ }).map((n) => canon(n.id)));
523
+ }
524
+ if (ctx.query) return withinHops(edges, canon, visibleNodes().filter((n) => matches(ctx, n)).map((n) => n.id), ctx.hops);
525
+ return null;
526
+ }
527
+ function trace(id) {
528
+ const set = activeSet(id);
529
+ const lit = !!id && !!set;
530
+ for (const n of model.nodes) {
531
+ const d = ctx.nodeEls.get(n.id);
532
+ if (!d) continue;
533
+ d.classList.toggle("dim", !!set && !set.has(canon(n.id)));
534
+ d.classList.toggle("lit", lit && !!set && set.has(canon(n.id)));
535
+ }
536
+ ctx.edgeEls.forEach((p) => {
537
+ if (!p) return;
538
+ const inSet = !!set && set.has(canon(p.dataset.a ?? "")) && set.has(canon(p.dataset.b ?? ""));
539
+ p.classList.toggle("dim", !!set && !inSet);
540
+ p.classList.toggle("hot", lit && inSet);
541
+ });
542
+ ctx.labelEls.forEach((l) => {
543
+ if (l) l.classList.toggle("show", lit && !!set && set.has(canon(l.dataset.a ?? "")) && set.has(canon(l.dataset.b ?? "")));
544
+ });
545
+ }
546
+ function select(id) {
547
+ clearSim();
548
+ const previous = ctx.selected;
549
+ ctx.selected = id ? canon(id) : null;
550
+ ctx.hl = null;
551
+ ctx.dismissed = false;
552
+ for (const n of model.nodes) ctx.nodeEls.get(n.id)?.classList.toggle("sel", !!ctx.selected && canon(n.id) === ctx.selected);
553
+ trace(ctx.selected);
554
+ ctx.fn.renderInspector();
555
+ drawTrail();
556
+ if (ctx.selected) revealNode(ctx.selected);
557
+ notifySelection(previous, ctx.selected, ctx.onSelect);
558
+ }
559
+ function overlay(edge, cls, delay, dur) {
560
+ const o = edge.cloneNode(false);
561
+ o.setAttribute("class", cls);
562
+ o.setAttribute("pathLength", "1");
563
+ o.removeAttribute("data-i");
564
+ els.layer.appendChild(o);
565
+ ctx.overlays.push(o);
566
+ if (ctx.reduced) return o;
567
+ o.style.strokeDasharray = "1 1";
568
+ o.style.strokeDashoffset = "1";
569
+ o.getBoundingClientRect();
570
+ o.style.transition = `stroke-dashoffset ${dur}s ${delay}s ease-out`;
571
+ o.style.strokeDashoffset = "0";
572
+ return o;
573
+ }
574
+ function dropOverlays(cls) {
575
+ ctx.overlays.filter((o) => o.classList.contains(cls)).forEach((o) => o.remove());
576
+ ctx.overlays = ctx.overlays.filter((o) => o.isConnected);
577
+ }
578
+ function drawTrail() {
579
+ dropOverlays("trail");
580
+ if (!ctx.selected || ctx.reduced) return;
581
+ const set = connected(visibleEdges(ctx), canon, ctx.selected);
582
+ const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
583
+ for (const p of ctx.edgeEls) {
584
+ if (!p || p.classList.contains("off")) continue;
585
+ const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
586
+ if (!(set.has(a) && set.has(b))) continue;
587
+ const delay = Math.min(Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0), Math.abs((ctx.nodes.get(b)?.col ?? 0) - c0)) * 0.09;
588
+ const o = overlay(p, "trail", delay, 0.55);
589
+ window.setTimeout(() => {
590
+ o.style.transition = "opacity .6s";
591
+ o.style.opacity = "0";
592
+ window.setTimeout(() => o.remove(), 650);
593
+ }, (delay + 0.55) * 1e3 + 150);
594
+ }
595
+ }
596
+ function simulate(actionId) {
597
+ clearSim();
598
+ if (!ctx.selected) return;
599
+ const action = model.inspector?.actions?.find((a) => a.id === actionId);
600
+ const stale = action?.staleText ?? "potentially stale";
601
+ ctx.simActive = true;
602
+ const down = relatives(visibleEdges(ctx), canon, ctx.selected, "down");
603
+ const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
604
+ for (const p of ctx.edgeEls) {
605
+ if (!p || p.classList.contains("off")) continue;
606
+ const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
607
+ if (down.has(a) && down.has(b)) overlay(p, "ripple", Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0) * 0.11, 0.5);
608
+ }
609
+ down.forEach((id) => {
610
+ if (id === ctx.selected) return;
611
+ const d = ctx.nodeEls.get(id);
612
+ if (!d) return;
613
+ const delay = ctx.reduced ? 0 : Math.abs((ctx.nodes.get(id)?.col ?? 0) - c0) * 110 + 450;
614
+ ctx.simTimers.push(window.setTimeout(() => {
615
+ d.classList.add("fail");
616
+ const sub = d.querySelector(".s");
617
+ if (sub) {
618
+ d.dataset.s = sub.innerHTML;
619
+ sub.textContent = stale;
620
+ }
621
+ }, delay));
622
+ });
623
+ ctx.fn.announce(`Simulation: ${down.size - 1} groups downstream of ${ctx.nodes.get(ctx.selected)?.title ?? ""} would be ${stale}.`);
624
+ ctx.fn.renderInspector();
625
+ }
626
+ function clearSim() {
627
+ ctx.simTimers.forEach((t) => clearTimeout(t));
628
+ ctx.simTimers = [];
629
+ dropOverlays("ripple");
630
+ document.querySelectorAll(".node.fail").forEach((d) => {
631
+ d.classList.remove("fail");
632
+ const sub = d.querySelector(".s");
633
+ if (sub && d.dataset.s !== void 0) {
634
+ sub.innerHTML = d.dataset.s;
635
+ delete d.dataset.s;
636
+ }
637
+ });
638
+ ctx.simActive = false;
639
+ }
640
+ function revealNode(id) {
641
+ const d = ctx.nodeEls.get(id);
642
+ if (!d) return;
643
+ const r = d.getBoundingClientRect(), v = els.vp.getBoundingClientRect(), edge = v.right - 340 - 24;
644
+ if (r.right > edge) {
645
+ ctx.userMoved = true;
646
+ ctx.fn.setView({ k: ctx.view.k, x: ctx.view.x + Math.max(edge - r.right, v.left + 60 - r.left), y: ctx.view.y }, true);
647
+ }
648
+ }
649
+ function rectOf(ids) {
650
+ const ps = ids.map((i) => ctx.geom.nodes[i]).filter((p) => !!p && !!ctx.nodes.get(p.id) && nodeVisible(model, ctx.mode, ctx.nodes.get(p.id), ctx.collapsed));
651
+ if (!ps.length) return null;
652
+ const x1 = Math.min(...ps.map((p) => p.x)), y1 = Math.min(...ps.map((p) => p.y));
653
+ const x2 = Math.max(...ps.map((p) => p.x + p.w)), y2 = Math.max(...ps.map((p) => p.y + p.h));
654
+ return { x: x1 - 20, y: y1 - 20, w: x2 - x1 + 40, h: y2 - y1 + 40 };
655
+ }
656
+ function zoomToPath() {
657
+ if (!ctx.selected) return;
658
+ const ids = [...connected(visibleEdges(ctx), canon, ctx.selected)];
659
+ ids.push(...model.nodes.filter((n) => n.ref && ids.includes(n.ref)).map((n) => n.id));
660
+ const r = rectOf(ids);
661
+ if (r) {
662
+ ctx.userMoved = true;
663
+ ctx.fn.fit(true, r, inspInset());
664
+ ctx.fn.announce(`Zoomed to the path of ${ctx.nodes.get(ctx.selected)?.title ?? ""}`);
665
+ }
666
+ }
667
+ function zoomToBand(id) {
668
+ const r = ctx.geom.lanes[id];
669
+ if (!r) return;
670
+ ctx.userMoved = true;
671
+ ctx.fn.fit(true, { x: r.x - 10, y: r.y - 10, w: r.w + 20, h: r.h + 20 }, inspInset());
672
+ ctx.fn.announce(`Zoomed to the lane ${model.lanes.find((l) => l.id === id)?.title ?? id}`);
673
+ }
674
+ function escape() {
675
+ clearSim();
676
+ select(null);
677
+ ctx.filter = null;
678
+ ctx.query = "";
679
+ els.q.value = "";
680
+ els.matchinfo.textContent = "";
681
+ ctx.fn.buildChrome();
682
+ trace(null);
683
+ ctx.fn.toggleNotes(false);
684
+ }
685
+ function setMode(id) {
686
+ if (ctx.mode === id) return;
687
+ document.body.classList.remove("intro");
688
+ clearSim();
689
+ ctx.mode = id;
690
+ const sel = ctx.selected && ctx.nodes.get(ctx.selected);
691
+ if (sel && !nodeVisible(model, id, sel, ctx.collapsed)) select(null);
692
+ ctx.fn.paint(true);
693
+ ctx.userMoved = false;
694
+ ctx.fn.fitWidth(true);
695
+ const added = model.nodes.filter((n) => nodeVisible(model, id, n, ctx.collapsed) && !nodeVisible(model, model.views?.[0]?.id ?? "all", n, ctx.collapsed)).length;
696
+ ctx.fn.announce(added ? `${model.views?.find((v) => v.id === id)?.label ?? id} view: ${added} groups added.` : `${model.views?.find((v) => v.id === id)?.label ?? id} view.`);
697
+ }
698
+ ctx.fn.trace = trace;
699
+ ctx.fn.select = select;
700
+ ctx.fn.drawTrail = drawTrail;
701
+ ctx.fn.simulate = simulate;
702
+ ctx.fn.clearSim = clearSim;
703
+ ctx.fn.rectOf = rectOf;
704
+ ctx.fn.zoomToPath = zoomToPath;
705
+ ctx.fn.zoomToBand = zoomToBand;
706
+ ctx.fn.escape = escape;
707
+ ctx.fn.setMode = setMode;
708
+ ctx.fn.toggleGroup = (id) => {
709
+ const group = model.nodes.find((n) => n.id === id && n.kind === "group" && n.contains?.length);
710
+ if (!group) return;
711
+ const change = toggleGroupState(model, ctx.mode, ctx.collapsed, id);
712
+ if (!change) return;
713
+ ctx.collapsed = change.collapsed;
714
+ ctx.fn.paint();
715
+ ctx.fn.select(change.selected);
716
+ ctx.fn.announce(change.expanding ? `Expanded ${group.title}.` : `Collapsed into ${group.title}.`);
717
+ };
718
+ ctx.fn.announce = (t) => {
719
+ els.live.textContent = t;
720
+ };
721
+ }
722
+
723
+ // src/ctx.ts
724
+ var SVG_NS = "http://www.w3.org/2000/svg";
725
+ function el(tag, cls, html) {
726
+ const e = document.createElement(tag);
727
+ if (cls) e.className = cls;
728
+ if (html !== void 0) e.innerHTML = html;
729
+ return e;
730
+ }
731
+ function sv(tag, attrs) {
732
+ const e = document.createElementNS(SVG_NS, tag);
733
+ for (const k of Object.keys(attrs)) e.setAttribute(k, attrs[k]);
734
+ return e;
735
+ }
736
+ function on(ctx, target, type, fn, opts) {
737
+ target.addEventListener(type, fn, opts);
738
+ ctx.disposers.push(() => target.removeEventListener(type, fn, opts));
739
+ }
740
+
741
+ // src/layout.ts
742
+ var DEFAULTS = { pitch: 62, top: 64, gap: 34, padX: 24, visible: () => true };
743
+ var LANE_PAD_TOP = 38;
744
+ var LANE_PAD_BOTTOM = 14;
745
+ function computeGeometry(m, opt = {}) {
746
+ const o = { ...DEFAULTS, ...opt };
747
+ const xs = [];
748
+ let x = o.padX;
749
+ for (const c of m.columns) {
750
+ xs.push(x);
751
+ x += c.width + o.gap;
752
+ }
753
+ const world = { w: Math.max(1, x - o.gap + o.padX), h: 1 };
754
+ const nodes = {};
755
+ for (const n of m.nodes) {
756
+ const h = NODE_HEIGHT[n.kind] ?? 54;
757
+ if (m.layout === "free") {
758
+ nodes[n.id] = { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w: n.w ?? 190, h: n.h ?? h };
759
+ continue;
760
+ }
761
+ const first = Math.min(Math.max(0, n.col ?? 0), m.columns.length - 1);
762
+ const last = Math.min(m.columns.length - 1, first + (n.colSpan ?? 1) - 1);
763
+ const x0 = xs[first] ?? o.padX;
764
+ const x1 = (xs[last] ?? x0) + (m.columns[last]?.width ?? 0);
765
+ nodes[n.id] = { id: n.id, x: x0, y: o.top + 40 + (n.row ?? 0) * o.pitch - h / 2, w: Math.max(1, x1 - x0), h };
766
+ }
767
+ if (m.layout === "free") world.w = Math.max(world.w, ...Object.values(nodes).map((p) => p.x + p.w + o.padX), 1);
768
+ const loops = {};
769
+ const lanes = {};
770
+ const visibleMembers = (id) => m.nodes.filter((n) => n.lane === id && o.visible(n.id)).map((n) => nodes[n.id]);
771
+ const ordered = m.lanes.map((l) => ({ l, mem: visibleMembers(l.id) })).filter((x2) => x2.mem.length).sort((a, b) => Math.min(...a.mem.map((p) => p.y)) - Math.min(...b.mem.map((p) => p.y)));
772
+ let previousBottom = -Infinity;
773
+ for (const { l, mem } of ordered) {
774
+ const top = Math.min(...mem.map((p) => p.y)) - LANE_PAD_TOP;
775
+ const shift = m.layout === "free" ? 0 : Math.max(0, previousBottom + 8 - top);
776
+ if (shift) for (const p of mem) p.y += shift;
777
+ const y1 = top + shift;
778
+ const laneNodeIds = new Set(mem.map((p) => p.id));
779
+ const possibleLoops = m.links.filter((link) => link.kind === "loop-back" && o.visible(link.from) && o.visible(link.to) && (laneNodeIds.has(link.from) || laneNodeIds.has(link.to))).length;
780
+ const bottom = Math.max(...mem.map((p) => p.y + p.h));
781
+ const y2 = bottom + (possibleLoops ? 10 + 8 * (possibleLoops - 1) : 0) + LANE_PAD_BOTTOM;
782
+ lanes[l.id] = { x: 8, y: y1, w: world.w - 16, h: y2 - y1 };
783
+ previousBottom = y2;
784
+ }
785
+ const channelCounts = /* @__PURE__ */ new Map();
786
+ for (const link of m.links) {
787
+ if (link.kind !== "loop-back" || !o.visible(link.from) || !o.visible(link.to)) continue;
788
+ const a = nodes[link.from], b = nodes[link.to];
789
+ if (!a || !b) continue;
790
+ const lower = a.y + a.h >= b.y + b.h ? a : b;
791
+ const laneId = m.nodes.find((n) => n.id === lower.id)?.lane;
792
+ if (!laneId || !lanes[laneId]) continue;
793
+ const left = Math.min(a.x + a.w / 2, b.x + b.w / 2);
794
+ const right = Math.max(a.x + a.w / 2, b.x + b.w / 2);
795
+ const laneMembers = visibleMembers(laneId);
796
+ const crossed = laneMembers.filter((p) => p.x < right && p.x + p.w > left);
797
+ const lowestBottom = Math.max(a.y + a.h, b.y + b.h, ...crossed.map((p) => p.y + p.h));
798
+ const index = channelCounts.get(laneId) ?? 0;
799
+ loops[link.id] = { channelY: lowestBottom + 10 + 8 * index };
800
+ channelCounts.set(laneId, index + 1);
801
+ const lane = lanes[laneId];
802
+ lane.h = Math.max(lane.h, loops[link.id].channelY + LANE_PAD_BOTTOM - lane.y);
803
+ }
804
+ const all = Object.values(lanes);
805
+ let bounds = { x: 0, y: 0, w: world.w, h: 1 };
806
+ if (all.length) {
807
+ const top = Math.min(...all.map((r) => r.y));
808
+ bounds = { x: 8, y: top, w: world.w - 16, h: Math.max(...all.map((r) => r.y + r.h)) - top };
809
+ }
810
+ world.h = Math.max(1, bounds.y + bounds.h + 70);
811
+ return { xs, nodes, lanes, loops, bounds, world };
812
+ }
813
+
814
+ // src/routing.ts
815
+ var routing_exports = {};
816
+ __export(routing_exports, {
817
+ elbow: () => elbow,
818
+ loopBack: () => loopBack
819
+ });
820
+ function elbow(x1, y1, x2, y2, tx) {
821
+ const r = 8;
822
+ if (Math.abs(y2 - y1) < 1) return `M${x1},${y1} H${x2}`;
823
+ if (Math.abs(y2 - y1) < 2 * r + 2) return `M${x1},${y1} C${tx},${y1} ${tx},${y2} ${x2},${y2}`;
824
+ const dir = y2 > y1 ? 1 : -1;
825
+ return `M${x1},${y1} H${tx - r} Q${tx},${y1} ${tx},${y1 + dir * r} V${y2 - dir * r} Q${tx},${y2} ${tx + r},${y2} H${x2}`;
826
+ }
827
+ function loopBack(x1, y1, x2, y2, channelY, detour = {}) {
828
+ x1 = Number.isFinite(x1) ? x1 : 0;
829
+ x2 = Number.isFinite(x2) ? x2 : 0;
830
+ y1 = Number.isFinite(y1) ? y1 : 0;
831
+ y2 = Number.isFinite(y2) ? y2 : 0;
832
+ channelY = Number.isFinite(channelY) ? channelY : Math.max(y1, y2) + 10;
833
+ channelY = Math.max(channelY, y1, y2);
834
+ const from = detour.from, to = detour.to;
835
+ const startX = from?.x ?? x1, endX = to?.x ?? x2;
836
+ const start = from ? `M${x1},${y1} V${from.y} H${startX}` : `M${x1},${y1}`;
837
+ const finish = to ? ` V${to.y} H${x2} V${y2}` : ` V${y2}`;
838
+ if (from || to) {
839
+ const dir2 = endX >= startX ? 1 : -1;
840
+ const r2 = Math.max(0, Math.min(8, Math.abs(endX - startX) / 2, (channelY - (from?.y ?? y1)) / 2, (channelY - (to?.y ?? y2)) / 2));
841
+ return `${start} V${channelY - r2} Q${startX},${channelY} ${startX + dir2 * r2},${channelY} H${endX - dir2 * r2} Q${endX},${channelY} ${endX},${channelY - r2}${finish}`;
842
+ }
843
+ const dx = x2 - x1;
844
+ const dir = dx >= 0 ? 1 : -1;
845
+ const run = Math.abs(dx) || 32;
846
+ const radius = Math.min(8, run / 2, (channelY - y1) / 2, (channelY - y2) / 2);
847
+ const r = Math.max(0, radius);
848
+ if (dx === 0) {
849
+ return `M${x1},${y1} V${channelY - r} Q${x1},${channelY} ${x1 + r},${channelY} H${x1 + 16} H${x1 - r} Q${x1},${channelY} ${x1},${channelY - r} V${y2}`;
850
+ }
851
+ return `M${x1},${y1} V${channelY - r} Q${x1},${channelY} ${x1 + dir * r},${channelY} H${x2 - dir * r} Q${x2},${channelY} ${x2},${channelY - r} V${y2}`;
852
+ }
853
+
854
+ // src/metrics.ts
855
+ function sparklinePoints(series, width, height) {
856
+ if (!Array.isArray(series) || series.length < 2 || series.some((v) => !Number.isFinite(v))) return "";
857
+ const min = Math.min(...series), max = Math.max(...series), span = max - min;
858
+ return series.map((v, i) => `${Math.round(i / (series.length - 1) * width * 100) / 100},${Math.round((span ? 1 - (v - min) / span : 0.5) * height * 100) / 100}`).join(" ");
859
+ }
860
+ function metricBadge(metric) {
861
+ if (!metric) return "";
862
+ const label = metric.label ?? "Metric";
863
+ const valStr = esc(String(metric.value));
864
+ const unitStr = metric.unit ? " " + esc(metric.unit) : "";
865
+ const title = `${esc(label)}: ${valStr}${unitStr}`;
866
+ const hasMax = metric.max != null && Number.isFinite(metric.max) && metric.max > 0;
867
+ const pct = hasMax && Number.isFinite(metric.value) ? Math.max(0, Math.min(100, metric.value / metric.max * 100)) : 0;
868
+ const bar = hasMax ? `<i style="width:${pct}%"></i>` : "";
869
+ return `<span class="metric-badge" title="${title}">${valStr}${unitStr}${bar}</span>`;
870
+ }
871
+ function sparkline(metric, width = 56, height = 12) {
872
+ if (!metric?.series || metric.series.length < 2) return "";
873
+ const points = sparklinePoints(metric.series, width, height);
874
+ if (!points) return "";
875
+ return `<svg class="spark" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" aria-hidden="true"><polyline points="${points}"/></svg>`;
876
+ }
877
+
878
+ // src/render.ts
879
+ function loopRoute(ctx, fromId, toId, from, to, channel) {
880
+ const detourFor = (end, other) => {
881
+ const center = end.x + end.w / 2, bottom = end.y + end.h;
882
+ const blocker = ctx.model.nodes.filter((n) => n.id !== fromId && n.id !== toId && nodeVisible(ctx.model, ctx.mode, n, ctx.collapsed)).map((n) => ctx.geom.nodes[n.id]).filter((p) => !!p && p.x < center && center < p.x + p.w && p.y > bottom && p.y < channel).sort((a, b) => a.y - b.y)[0];
883
+ if (!blocker) return void 0;
884
+ return { x: other.x + other.w / 2 < center ? blocker.x - 10 : blocker.x + blocker.w + 10, y: (bottom + blocker.y) / 2 };
885
+ };
886
+ return loopBack(
887
+ from.x + from.w / 2,
888
+ from.y + from.h,
889
+ to.x + to.w / 2,
890
+ to.y + to.h,
891
+ channel,
892
+ { from: detourFor(from, to), to: detourFor(to, from) }
893
+ );
894
+ }
895
+ function nodeHtml(ctx, n) {
896
+ const badge = metricBadge(n.metrics);
897
+ const spark = sparkline(n.metrics, 56, 12);
898
+ if (n.kind === "gateway") {
899
+ return `<svg class="gw" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"><polygon points="50,0 100,50 50,100 0,50"></polygon></svg><span class="gw-title">${esc(n.title)}</span>${badge}${spark}`;
900
+ }
901
+ const m = ctx.model;
902
+ const sid = nodeStateId(m, ctx.mode, n);
903
+ const word = m.states[sid]?.word ?? sid;
904
+ const bucket = bucketOf(m, sid);
905
+ const sub = n.subtitle ?? m.columns[n.col ?? 0]?.title ?? "";
906
+ const weight = Math.max(1, n.weight ?? 1);
907
+ const showWord = n.kind !== "placeholder" && n.kind !== "note";
908
+ const bar = weight > 1 && n.kind !== "chip" ? `<div class="bar"><i class="${["good", "neutral", "gap"].includes(bucket) ? bucket : "gap"}" style="width:100%"></i></div>` : "";
909
+ const improved = isImproved(m, ctx.mode, n) ? '<span class="cor up" title="Gap filled in this view">\u25B2</span>' : "";
910
+ const upgrade = n.upgrade && bucket !== "good" ? `<span class="cor meas" title="${esc("Can improve: " + n.upgrade)}">\u2197</span>` : "";
911
+ const arrow = n.kind === "chip" ? '<span aria-hidden="true">\u2191 </span>' : "";
912
+ const lead = showWord ? `<b class="ev st-${cssId(sid)}">${esc(word)}</b> \xB7 ` : "";
913
+ const fold = n.kind === "group" && n.contains?.length ? `<span class="fold">\u25B8 ${n.contains.length}</span>` : "";
914
+ return `<div class="t"><span class="newdot" title="Added in this view"></span><span class="name">${arrow}${esc(n.title)}</span>${fold}${badge}</div><div class="s${spark ? " has-spark" : ""}" title="${esc(sub)}">${lead}${esc(sub)}</div>${spark}${bar}${improved}${upgrade}`;
915
+ }
916
+ function build(ctx) {
917
+ const { model, els } = ctx;
918
+ ctx.geom = computeGeometry(model);
919
+ els.world.style.width = ctx.geom.world.w + "px";
920
+ els.world.style.height = ctx.geom.world.h + "px";
921
+ els.svg.setAttribute("width", String(ctx.geom.world.w));
922
+ els.svg.setAttribute("height", String(ctx.geom.world.h));
923
+ els.world.querySelectorAll(".node, .band, .elabel").forEach((n) => n.remove());
924
+ els.layer.innerHTML = "";
925
+ ctx.nodeEls.clear();
926
+ ctx.laneEls.clear();
927
+ ctx.edgeEls = [];
928
+ ctx.labelEls = [];
929
+ for (const l of model.lanes) {
930
+ const d = el("div", "band" + (l.gap ? " gap" : ""));
931
+ d.id = "lane_" + l.id;
932
+ const badge = l.badge ? `<span class="bd t-${["ok", "warn", "muted"].includes(l.badgeTone ?? "") ? l.badgeTone : "muted"}">${esc(l.badge)}</span>` : "";
933
+ const note = l.note ? `<span class="bn">${esc(l.note)}</span>` : "";
934
+ d.innerHTML = `<div class="bandlabel" role="button" tabindex="0" aria-label="${esc("Zoom to the lane " + l.title)}"><b>${esc(l.title)}</b>${badge}${note}</div>`;
935
+ const label = d.firstElementChild;
936
+ on(ctx, label, "click", (e) => {
937
+ e.stopPropagation();
938
+ ctx.fn.zoomToBand(l.id);
939
+ });
940
+ on(ctx, label, "keydown", (e) => {
941
+ if (e.key === "Enter") ctx.fn.zoomToBand(l.id);
942
+ });
943
+ els.world.insertBefore(d, els.svg.parentElement === els.world ? els.svg : null);
944
+ ctx.laneEls.set(l.id, d);
945
+ }
946
+ for (const n of model.nodes) {
947
+ const p = ctx.geom.nodes[n.id];
948
+ if (!p) continue;
949
+ const d = el("div", "node" + (n.kind === "chip" ? " ref" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : ""));
950
+ d.id = "n_" + n.id;
951
+ d.tabIndex = 0;
952
+ d.setAttribute("role", "button");
953
+ d.style.setProperty("--c", String(n.col ?? 0));
954
+ d.style.left = p.x + "px";
955
+ d.style.top = p.y + "px";
956
+ d.style.width = p.w + "px";
957
+ d.style.height = p.h + "px";
958
+ on(ctx, d, "click", () => {
959
+ if (ctx.moved > 4) return;
960
+ ctx.fn.select(canonOf(ctx.nodes, n.id));
961
+ });
962
+ on(ctx, d, "dblclick", (e) => {
963
+ e.stopPropagation();
964
+ ctx.fn.select(canonOf(ctx.nodes, n.id));
965
+ ctx.fn.zoomToPath();
966
+ });
967
+ on(ctx, d, "keydown", (e) => {
968
+ if (e.key === "Enter" || e.key === " ") {
969
+ e.preventDefault();
970
+ ctx.fn.select(canonOf(ctx.nodes, n.id));
971
+ }
972
+ });
973
+ on(ctx, d, "mouseenter", () => ctx.fn.trace(n.id));
974
+ on(ctx, d, "mouseleave", () => ctx.fn.trace(ctx.selected));
975
+ on(ctx, d, "focus", () => ctx.fn.trace(n.id));
976
+ on(ctx, d, "blur", () => ctx.fn.trace(ctx.selected));
977
+ els.world.appendChild(d);
978
+ ctx.nodeEls.set(n.id, d);
979
+ }
980
+ const gap = DEFAULTS.gap;
981
+ ctx.links.forEach((l, i) => {
982
+ const A = ctx.geom.nodes[l.from], B = ctx.geom.nodes[l.to];
983
+ if (!A || !B) {
984
+ console.warn(`flow-studio: link ${l.id} skipped (an end is missing)`);
985
+ return;
986
+ }
987
+ const x1 = A.x + A.w, y1 = A.y + A.h / 2, x2 = B.x, y2 = B.y + B.h / 2, tx = x1 + gap / 2;
988
+ const loop = l.kind === "loop-back";
989
+ const channel = ctx.geom.loops[l.id]?.channelY ?? Math.max(A.y + A.h, B.y + B.h) + 10;
990
+ const d = loop ? loopRoute(ctx, l.from, l.to, A, B, channel) : elbow(x1, y1, x2 - 1, y2, tx);
991
+ const path = sv("path", { d, class: "edge" + (loop ? " loop" : ""), "data-i": String(i), "data-a": l.from, "data-b": l.to, "marker-end": "url(#arr)" });
992
+ els.layer.appendChild(path);
993
+ ctx.edgeEls[i] = path;
994
+ if (l.label) {
995
+ const lab = el("div", "elabel", esc(l.label));
996
+ lab.dataset.i = String(i);
997
+ lab.dataset.a = l.from;
998
+ lab.dataset.b = l.to;
999
+ lab.style.left = (loop ? (A.x + A.w / 2 + B.x + B.w / 2) / 2 : Math.abs(y2 - y1) < 1 ? (x1 + x2) / 2 : tx) + "px";
1000
+ lab.style.top = (loop ? ctx.geom.loops[l.id]?.channelY ?? Math.max(A.y + A.h, B.y + B.h) + 10 : (y1 + y2) / 2) + "px";
1001
+ els.world.appendChild(lab);
1002
+ ctx.labelEls[i] = lab;
1003
+ }
1004
+ });
1005
+ }
1006
+ function paint(ctx, fromSwitch = false) {
1007
+ const { model } = ctx;
1008
+ const mode = ctx.mode;
1009
+ const effective = effectiveLinks(model, mode, ctx.collapsed, ctx.nodes);
1010
+ const effectiveById = new Map(effective.map((l) => [l.id, l]));
1011
+ ctx.geom = computeGeometry({ ...model, links: effective }, { visible: (id) => {
1012
+ const n = ctx.nodes.get(id);
1013
+ return !!n && nodeVisible(model, mode, n, ctx.collapsed);
1014
+ } });
1015
+ ctx.els.world.style.height = ctx.geom.world.h + "px";
1016
+ ctx.els.svg.setAttribute("height", String(ctx.geom.world.h));
1017
+ document.body.classList.toggle("alt", !!model.views && mode !== baselineView(model));
1018
+ for (const n of model.nodes) {
1019
+ const d = ctx.nodeEls.get(n.id);
1020
+ if (!d) continue;
1021
+ const rect = ctx.geom.nodes[n.id];
1022
+ if (rect) {
1023
+ d.style.left = rect.x + "px";
1024
+ d.style.top = rect.y + "px";
1025
+ }
1026
+ const sid = nodeStateId(model, mode, n);
1027
+ const show = nodeVisible(model, mode, n, ctx.collapsed);
1028
+ const bucket = bucketOf(model, sid);
1029
+ const cors = (isImproved(model, mode, n) ? 1 : 0) + (n.upgrade && bucket !== "good" ? 1 : 0);
1030
+ const fresh = isNew(model, mode, n);
1031
+ d.className = "node st-" + cssId(sid) + (n.kind === "chip" ? " ref" : "") + (n.kind === "gateway" ? " gateway" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : "") + (n.kind === "placeholder" ? " placeholder" : "") + (show ? "" : " off") + (fresh ? " isnew" : "") + (cors ? cors > 1 ? " hascor2" : " hascor" : "") + (ctx.selected && canonOf(ctx.nodes, n.id) === ctx.selected ? " sel" : "") + (metricBadge(n.metrics) ? " has-metrics" : "");
1032
+ d.innerHTML = nodeHtml(ctx, n);
1033
+ d.setAttribute("aria-hidden", show ? "false" : "true");
1034
+ d.tabIndex = show ? 0 : -1;
1035
+ const word = model.states[sid]?.word ?? sid;
1036
+ const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
1037
+ d.setAttribute("aria-label", n.kind === "chip" ? `${n.title}: a reference to the node in the ${lane} lane. Press Enter to select it.` : `${n.title}. ${n.subtitle ?? model.columns[n.col ?? 0]?.title ?? ""}, ${word}. Press Enter to inspect.`);
1038
+ if (fromSwitch && fresh && !ctx.reduced) {
1039
+ d.style.transitionDelay = (n.col ?? 0) * 60 + "ms";
1040
+ d.classList.add("pulse");
1041
+ window.setTimeout(() => {
1042
+ d.style.transitionDelay = "";
1043
+ d.classList.remove("pulse");
1044
+ }, 2200);
1045
+ } else {
1046
+ d.style.transitionDelay = "";
1047
+ }
1048
+ }
1049
+ ctx.links.forEach((l, i) => {
1050
+ const p = ctx.edgeEls[i];
1051
+ if (!p) return;
1052
+ const edge = effectiveById.get(l.id);
1053
+ const vis = !!edge;
1054
+ const a = edge && ctx.geom.nodes[edge.from], b = edge && ctx.geom.nodes[edge.to];
1055
+ const loop = edge?.kind === "loop-back";
1056
+ if (a && b) {
1057
+ p.dataset.a = edge.from;
1058
+ p.dataset.b = edge.to;
1059
+ const channel = ctx.geom.loops[edge.id]?.channelY ?? Math.max(a.y + a.h, b.y + b.h) + 10;
1060
+ p.setAttribute("d", loop ? loopRoute(ctx, edge.from, edge.to, a, b, channel) : elbow(a.x + a.w, a.y + a.h / 2, b.x - 1, b.y + b.h / 2, a.x + a.w + DEFAULTS.gap / 2));
1061
+ const lab = ctx.labelEls[i];
1062
+ if (lab) {
1063
+ lab.dataset.a = edge.from;
1064
+ lab.dataset.b = edge.to;
1065
+ lab.style.left = (loop ? (a.x + a.w / 2 + b.x + b.w / 2) / 2 : Math.abs(b.y + b.h / 2 - a.y - a.h / 2) < 1 ? (a.x + a.w + b.x) / 2 : a.x + a.w + DEFAULTS.gap / 2) + "px";
1066
+ lab.style.top = (loop ? channel : (a.y + a.h / 2 + b.y + b.h / 2) / 2) + "px";
1067
+ }
1068
+ }
1069
+ p.setAttribute("class", "edge st-" + cssId(edge?.state ?? linkStateId(model, mode, l)) + (loop ? " loop" : "") + (vis ? "" : " off"));
1070
+ ctx.labelEls[i]?.classList.toggle("off", !vis);
1071
+ });
1072
+ for (const l of model.lanes) {
1073
+ const d = ctx.laneEls.get(l.id);
1074
+ const r = ctx.geom.lanes[l.id];
1075
+ if (!d) continue;
1076
+ d.classList.toggle("off", !r);
1077
+ if (r) {
1078
+ d.style.left = r.x + "px";
1079
+ d.style.top = r.y + "px";
1080
+ d.style.width = r.w + "px";
1081
+ d.style.height = r.h + "px";
1082
+ }
1083
+ }
1084
+ ctx.fn.buildChrome();
1085
+ ctx.fn.trace(ctx.selected);
1086
+ ctx.fn.renderInspector();
1087
+ ctx.fn.stickLabels();
1088
+ ctx.fn.placeHeaders();
1089
+ }
1090
+ function install2(ctx) {
1091
+ ctx.fn.buildWorld = () => build(ctx);
1092
+ ctx.fn.paint = (fromSwitch) => paint(ctx, fromSwitch);
1093
+ }
1094
+
1095
+ // src/viewport.ts
1096
+ var viewport_exports = {};
1097
+ __export(viewport_exports, {
1098
+ K_MAX: () => K_MAX,
1099
+ K_MIN: () => K_MIN,
1100
+ clampK: () => clampK,
1101
+ clampPan: () => clampPan,
1102
+ fitRect: () => fitRect,
1103
+ fitWidth: () => fitWidth,
1104
+ zoomAt: () => zoomAt
1105
+ });
1106
+ var K_MIN = 0.25;
1107
+ var K_MAX = 2.5;
1108
+ var clampK = (k) => Number.isFinite(k) ? Math.min(K_MAX, Math.max(K_MIN, k)) : 1;
1109
+ function zoomAt(v, factor, cx, cy) {
1110
+ const base = Number.isFinite(v.k) && v.k > 0 ? v.k : 1;
1111
+ const k = clampK(base * factor);
1112
+ const r = k / base;
1113
+ return { k, x: cx - (cx - v.x) * r, y: cy - (cy - v.y) * r };
1114
+ }
1115
+ function fitRect(rect, size, pad, top) {
1116
+ if (!(rect.w > 0 && rect.h > 0)) return { x: 0, y: 0, k: 1 };
1117
+ const aw = Math.max(1, size.w - 2 * pad);
1118
+ const ah = Math.max(1, size.h - 2 * pad - top);
1119
+ const k = Math.min(1, clampK(Math.min(aw / rect.w, ah / rect.h)));
1120
+ return { k, x: pad + (aw - rect.w * k) / 2 - rect.x * k, y: pad + top + (ah - rect.h * k) / 2 - rect.y * k };
1121
+ }
1122
+ function fitWidth(rect, size, pad, top) {
1123
+ if (!(rect.w > 0)) return { x: 0, y: 0, k: 1 };
1124
+ const k = Math.min(1, clampK((Math.max(1, size.w) - 2 * pad) / rect.w));
1125
+ return { k, x: pad - rect.x * k, y: top - rect.y * k };
1126
+ }
1127
+ function clampPan(v, bounds, size) {
1128
+ const keep = 80;
1129
+ const minX = keep - (bounds.x + bounds.w) * v.k;
1130
+ const maxX = size.w - keep - bounds.x * v.k;
1131
+ const minY = keep - (bounds.y + bounds.h) * v.k;
1132
+ const maxY = size.h - keep - bounds.y * v.k;
1133
+ return { k: v.k, x: Math.min(maxX, Math.max(minX, v.x)), y: Math.min(maxY, Math.max(minY, v.y)) };
1134
+ }
1135
+
1136
+ // src/controls.ts
1137
+ var HEAD = 44;
1138
+ var ICON = {
1139
+ select: '<path d="M5 3l12 6.5-5.2 1.6L9.5 17z"/>',
1140
+ hand: '<path d="M8 11V5.5a1.5 1.5 0 013 0V10m0-5a1.5 1.5 0 013 0v5m0-3.5a1.5 1.5 0 013 0V13c0 4-2.5 7-6.5 7S5 17 5 14l-1.6-3.2a1.4 1.4 0 012.4-1.4L8 12"/>',
1141
+ plus: '<path d="M12 5v14M5 12h14"/>',
1142
+ minus: '<path d="M5 12h14"/>',
1143
+ fit: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>',
1144
+ one: '<path d="M9 8l3-2v12"/><path d="M5 20h14"/>',
1145
+ sel: '<circle cx="12" cy="12" r="3.2"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3"/>',
1146
+ full: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>'
1147
+ };
1148
+ function install3(ctx) {
1149
+ const { els, model } = ctx;
1150
+ const vp = els.vp;
1151
+ let cached = { w: vp.clientWidth, h: vp.clientHeight };
1152
+ const size = () => cached;
1153
+ let vpRect = vp.getBoundingClientRect();
1154
+ const refreshRect = () => {
1155
+ vpRect = vp.getBoundingClientRect();
1156
+ };
1157
+ let animTimer = 0;
1158
+ let raf = 0;
1159
+ function applyView() {
1160
+ const v = ctx.view;
1161
+ els.world.style.transform = `translate(${v.x}px,${v.y}px) scale(${v.k})`;
1162
+ vp.style.setProperty("--inv", Math.max(1, 0.9 / v.k).toFixed(3));
1163
+ vp.style.setProperty("--k", v.k.toFixed(4));
1164
+ vp.classList.toggle("lod1", v.k < 0.62);
1165
+ vp.classList.toggle("lod2", v.k < 0.4);
1166
+ els.zoom.textContent = Math.round(v.k * 100) + "%";
1167
+ ctx.fn.stickLabels();
1168
+ ctx.fn.placeHeaders();
1169
+ if (!raf) raf = requestAnimationFrame(() => {
1170
+ raf = 0;
1171
+ if (document.body.classList.contains("notes")) ctx.fn.placeMarkers();
1172
+ });
1173
+ }
1174
+ function setView(v, animate = false) {
1175
+ const c = clampPan(v, ctx.geom.bounds, size());
1176
+ ctx.view = [c.x, c.y, c.k].every(Number.isFinite) ? c : { x: 0, y: 0, k: 1 };
1177
+ const glide = animate && !ctx.reduced;
1178
+ vp.classList.toggle("anim", glide);
1179
+ if (glide) {
1180
+ clearTimeout(animTimer);
1181
+ animTimer = window.setTimeout(() => vp.classList.remove("anim"), 330);
1182
+ }
1183
+ applyView();
1184
+ }
1185
+ function fit(animate = false, rect, inset = 0) {
1186
+ const r = rect ?? ctx.geom.bounds;
1187
+ if (!(r.w > 0)) return;
1188
+ const s = size();
1189
+ setView(fitRect(r, { w: s.w - inset, h: s.h }, 28, HEAD + 8), animate);
1190
+ }
1191
+ function fitW(animate = false) {
1192
+ setView(fitWidth(ctx.geom.bounds, size(), 28, HEAD + 8), animate);
1193
+ }
1194
+ function zoomBy(f) {
1195
+ const s = size();
1196
+ ctx.userMoved = true;
1197
+ setView(zoomAt(ctx.view, f, s.w / 2, s.h / 2), true);
1198
+ ctx.fn.announce("Zoom " + Math.round(ctx.view.k * 100) + " percent");
1199
+ }
1200
+ function placeHeaders() {
1201
+ const cols = els.cols;
1202
+ cols.style.display = model.columns.length ? "" : "none";
1203
+ if (!cols.firstChild) {
1204
+ model.columns.forEach((c, i2) => {
1205
+ const h = el("div", "hd");
1206
+ h.textContent = (c.short ?? c.title).toUpperCase();
1207
+ h.title = c.title;
1208
+ h.dataset.i = String(i2);
1209
+ cols.appendChild(h);
1210
+ });
1211
+ let i = 0;
1212
+ while (i < model.columns.length) {
1213
+ const name = model.columns[i]?.phase;
1214
+ let j = i;
1215
+ while (j + 1 < model.columns.length && model.columns[j + 1]?.phase === name) j++;
1216
+ if (name) {
1217
+ const p = el("div", "ph");
1218
+ p.innerHTML = "<span></span><i></i>";
1219
+ p.firstElementChild.textContent = name;
1220
+ p.dataset.a = String(i);
1221
+ p.dataset.b = String(j);
1222
+ cols.appendChild(p);
1223
+ }
1224
+ i = j + 1;
1225
+ }
1226
+ }
1227
+ const v = ctx.view, xs = ctx.geom.xs;
1228
+ cols.querySelectorAll(".hd").forEach((h) => {
1229
+ const i = +(h.dataset.i ?? 0);
1230
+ h.style.left = v.x + (xs[i] ?? 0) * v.k + "px";
1231
+ h.style.width = Math.max(0, (model.columns[i]?.width ?? 0) * v.k - 6) + "px";
1232
+ });
1233
+ cols.querySelectorAll(".ph").forEach((p) => {
1234
+ const a = +(p.dataset.a ?? 0), b = +(p.dataset.b ?? 0);
1235
+ p.style.left = v.x + (xs[a] ?? 0) * v.k + "px";
1236
+ p.style.width = Math.max(40, ((xs[b] ?? 0) + (model.columns[b]?.width ?? 0) - (xs[a] ?? 0)) * v.k) + "px";
1237
+ });
1238
+ }
1239
+ function stickLabels() {
1240
+ const wl = (52 - ctx.view.x) / ctx.view.k;
1241
+ for (const l of model.lanes) {
1242
+ const r = ctx.geom.lanes[l.id], d = ctx.laneEls.get(l.id);
1243
+ if (!r || !d?.firstElementChild) continue;
1244
+ d.firstElementChild.style.left = Math.min(Math.max(12, wl - r.x), Math.max(12, r.w - 300)) + "px";
1245
+ }
1246
+ }
1247
+ const pointers = /* @__PURE__ */ new Map();
1248
+ let drag = null;
1249
+ let pinch = null;
1250
+ on(ctx, vp, "pointerdown", (e) => {
1251
+ if (e.target.closest("#tools, .bandlabel")) return;
1252
+ pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1253
+ ctx.moved = 0;
1254
+ if (pointers.size === 2) {
1255
+ const [a, b] = [...pointers.values()];
1256
+ pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), view: { ...ctx.view } };
1257
+ drag = null;
1258
+ return;
1259
+ }
1260
+ const onNode = !!e.target.closest(".node");
1261
+ if (e.button === 1 || ctx.tool === "hand" || ctx.spaceDown || !onNode) {
1262
+ drag = { x: e.clientX, y: e.clientY, vx: ctx.view.x, vy: ctx.view.y };
1263
+ try {
1264
+ vp.setPointerCapture(e.pointerId);
1265
+ } catch {
1266
+ }
1267
+ vp.classList.add("panning");
1268
+ if (e.button === 1) e.preventDefault();
1269
+ }
1270
+ });
1271
+ on(ctx, vp, "pointermove", (e) => {
1272
+ if (pointers.has(e.pointerId)) pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
1273
+ if (pinch && pointers.size === 2) {
1274
+ const [a, b] = [...pointers.values()];
1275
+ const d = Math.hypot(a.x - b.x, a.y - b.y), r = vpRect;
1276
+ ctx.userMoved = true;
1277
+ setView(zoomAt(pinch.view, d / pinch.d, (a.x + b.x) / 2 - r.left, (a.y + b.y) / 2 - r.top), false);
1278
+ return;
1279
+ }
1280
+ if (!drag) return;
1281
+ const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
1282
+ ctx.moved = Math.max(ctx.moved, Math.hypot(dx, dy));
1283
+ if (ctx.moved > 4) {
1284
+ ctx.userMoved = true;
1285
+ setView({ k: ctx.view.k, x: drag.vx + dx, y: drag.vy + dy }, false);
1286
+ }
1287
+ });
1288
+ const endDrag = (e) => {
1289
+ pointers.delete(e.pointerId);
1290
+ if (pointers.size < 2) pinch = null;
1291
+ if (drag) {
1292
+ drag = null;
1293
+ vp.classList.remove("panning");
1294
+ try {
1295
+ vp.releasePointerCapture(e.pointerId);
1296
+ } catch {
1297
+ }
1298
+ window.setTimeout(() => {
1299
+ ctx.moved = 0;
1300
+ }, 0);
1301
+ }
1302
+ };
1303
+ on(ctx, vp, "pointerup", endDrag);
1304
+ on(ctx, vp, "pointercancel", endDrag);
1305
+ on(ctx, vp, "wheel", (e) => {
1306
+ e.preventDefault();
1307
+ const r = vpRect, unit = e.deltaMode === 1 ? 16 : 1;
1308
+ ctx.userMoved = true;
1309
+ setView(zoomAt(ctx.view, Math.exp(-e.deltaY * unit * (e.ctrlKey ? 0.01 : 12e-4)), e.clientX - r.left, e.clientY - r.top), false);
1310
+ }, { passive: false });
1311
+ on(ctx, vp, "dblclick", (e) => {
1312
+ if (!e.target.closest(".node, .bandlabel, #tools")) {
1313
+ ctx.userMoved = false;
1314
+ fit(true);
1315
+ }
1316
+ });
1317
+ on(ctx, vp, "click", (e) => {
1318
+ if (!e.target.closest(".node, .bandlabel, #tools") && ctx.moved <= 4) ctx.fn.select(null);
1319
+ });
1320
+ const ro = new ResizeObserver(() => {
1321
+ cached = { w: vp.clientWidth, h: vp.clientHeight };
1322
+ refreshRect();
1323
+ if (!ctx.userMoved) fitW(false);
1324
+ else setView(ctx.view, false);
1325
+ });
1326
+ ro.observe(vp);
1327
+ on(ctx, vp, "pointerenter", refreshRect);
1328
+ on(ctx, window, "scroll", refreshRect, { passive: true });
1329
+ ctx.disposers.push(() => ro.disconnect());
1330
+ function setTool(id) {
1331
+ ctx.tool = id;
1332
+ vp.classList.toggle("hand", id === "hand");
1333
+ els.tools.querySelectorAll("[data-id='select'], [data-id='hand']").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.id === id)));
1334
+ ctx.fn.announce(id === "hand" ? "Hand tool: drag to pan" : "Select tool");
1335
+ }
1336
+ function toggleFull() {
1337
+ const ws = els.workspace;
1338
+ if (document.fullscreenElement) void document.exitFullscreen();
1339
+ else if (ws.requestFullscreen) ws.requestFullscreen().catch(() => ws.classList.toggle("fs"));
1340
+ else ws.classList.toggle("fs");
1341
+ }
1342
+ const TOOLS = [
1343
+ ["select", "Select (V)", "tool"],
1344
+ ["hand", "Hand: drag to pan (H)", "tool"],
1345
+ ["-"],
1346
+ ["plus", "Zoom in (+)", () => zoomBy(1.25)],
1347
+ ["minus", "Zoom out (-)", () => zoomBy(1 / 1.25)],
1348
+ ["fit", "Fit to screen (0)", () => {
1349
+ ctx.userMoved = false;
1350
+ fit(true);
1351
+ ctx.fn.announce("Fit to screen");
1352
+ }],
1353
+ ["one", "Actual size (1)", () => {
1354
+ const s = size();
1355
+ ctx.userMoved = true;
1356
+ setView(zoomAt(ctx.view, 1 / ctx.view.k, s.w / 2, s.h / 2), true);
1357
+ }],
1358
+ ["sel", "Zoom to selection (F)", () => ctx.fn.zoomToPath()],
1359
+ ["-"],
1360
+ ["full", "Fullscreen", "full"]
1361
+ ];
1362
+ els.tools.innerHTML = "";
1363
+ for (const t of TOOLS) {
1364
+ if (t[0] === "-") {
1365
+ els.tools.appendChild(el("div", "sep"));
1366
+ continue;
1367
+ }
1368
+ const [id, label, act] = t;
1369
+ const b = el("button", "tb", `<svg viewBox="0 0 24 24" aria-hidden="true">${ICON[id] ?? ""}</svg>`);
1370
+ b.type = "button";
1371
+ b.title = label;
1372
+ b.setAttribute("aria-label", label);
1373
+ b.dataset.id = id;
1374
+ if (act === "tool") {
1375
+ b.setAttribute("aria-pressed", String(ctx.tool === id));
1376
+ on(ctx, b, "click", () => setTool(id));
1377
+ } else if (act === "full") on(ctx, b, "click", toggleFull);
1378
+ else on(ctx, b, "click", act);
1379
+ els.tools.appendChild(b);
1380
+ }
1381
+ requestAnimationFrame(() => {
1382
+ const r = els.tools.getBoundingClientRect(), v = vp.getBoundingClientRect();
1383
+ els.zoom.style.top = r.bottom - v.top + 6 + "px";
1384
+ });
1385
+ on(ctx, document, "fullscreenchange", () => window.setTimeout(() => {
1386
+ if (!ctx.userMoved) fitW(false);
1387
+ }, 60));
1388
+ on(ctx, document, "keydown", (e) => {
1389
+ const typing = !!e.target && /INPUT|TEXTAREA/.test(e.target.tagName);
1390
+ if (typing) {
1391
+ if (e.key === "Escape") e.target.blur();
1392
+ return;
1393
+ }
1394
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
1395
+ if (e.key === " " && document.activeElement === vp) {
1396
+ ctx.spaceDown = true;
1397
+ e.preventDefault();
1398
+ vp.classList.add("hand");
1399
+ return;
1400
+ }
1401
+ const k = e.key, step = e.shiftKey ? 240 : 60;
1402
+ if (k === "/") {
1403
+ e.preventDefault();
1404
+ els.q.focus();
1405
+ } else if (k === "n" || k === "N") ctx.fn.toggleNotes();
1406
+ else if (k === "v" || k === "V") setTool("select");
1407
+ else if (k === "h" || k === "H") setTool("hand");
1408
+ else if (k === "e" || k === "E") {
1409
+ const selected = ctx.selected && ctx.nodes.get(ctx.selected);
1410
+ if (selected) {
1411
+ const group = selected.kind === "group" ? selected : ctx.model.nodes.find((n) => n.kind === "group" && n.contains?.includes(selected.id));
1412
+ if (group) ctx.fn.toggleGroup(group.id);
1413
+ }
1414
+ } else if (k === "+" || k === "=") zoomBy(1.25);
1415
+ else if (k === "-" || k === "_") zoomBy(1 / 1.25);
1416
+ else if (k === "0") {
1417
+ ctx.userMoved = false;
1418
+ fit(true);
1419
+ } else if (k === "1") {
1420
+ const s = size();
1421
+ ctx.userMoved = true;
1422
+ setView(zoomAt(ctx.view, 1 / ctx.view.k, s.w / 2, s.h / 2), true);
1423
+ } else if (k === "f" || k === "F") ctx.fn.zoomToPath();
1424
+ else if (k.startsWith("Arrow") && (document.activeElement === vp || document.activeElement === document.body)) {
1425
+ e.preventDefault();
1426
+ ctx.userMoved = true;
1427
+ setView({ k: ctx.view.k, x: ctx.view.x + (k === "ArrowLeft" ? step : k === "ArrowRight" ? -step : 0), y: ctx.view.y + (k === "ArrowUp" ? step : k === "ArrowDown" ? -step : 0) }, false);
1428
+ } else if (k === "Escape") ctx.fn.escape();
1429
+ });
1430
+ on(ctx, document, "keyup", (e) => {
1431
+ if (e.key === " ") {
1432
+ ctx.spaceDown = false;
1433
+ if (ctx.tool !== "hand") vp.classList.remove("hand");
1434
+ }
1435
+ });
1436
+ ctx.fn.setView = setView;
1437
+ ctx.fn.fit = fit;
1438
+ ctx.fn.fitWidth = fitW;
1439
+ ctx.fn.zoomBy = zoomBy;
1440
+ ctx.fn.stickLabels = stickLabels;
1441
+ ctx.fn.placeHeaders = placeHeaders;
1442
+ }
1443
+
1444
+ // src/inspector.ts
1445
+ var BUCKET_COLOR = { good: "ok", neutral: "muted", gap: "warn" };
1446
+ var PILL = { ok: "ok", warn: "warn", crit: "warn", info: "info", accent: "info", muted: "muted" };
1447
+ function install4(ctx) {
1448
+ const { els, model } = ctx;
1449
+ const matchesFilter = (n, f) => !f || (f.col === void 0 || n.col === f.col) && (f.kind === void 0 || n.kind === f.kind);
1450
+ function wire(box) {
1451
+ box.querySelector("#hideInsp")?.addEventListener("click", () => {
1452
+ if (ctx.selected) ctx.fn.select(null);
1453
+ else {
1454
+ ctx.dismissed = true;
1455
+ ctx.fn.renderInspector();
1456
+ }
1457
+ });
1458
+ box.querySelectorAll("[data-hl]").forEach((b) => b.addEventListener("click", () => {
1459
+ ctx.hl = ctx.hl === b.dataset.hl ? null : b.dataset.hl ?? null;
1460
+ ctx.fn.trace(ctx.selected);
1461
+ ctx.fn.renderInspector();
1462
+ }));
1463
+ box.querySelectorAll("[data-g]").forEach((b) => b.addEventListener("click", () => {
1464
+ ctx.fn.select(b.dataset.g ?? null);
1465
+ ctx.fn.zoomToPath();
1466
+ }));
1467
+ box.querySelectorAll("[data-act]").forEach((b) => b.addEventListener("click", () => ctx.fn.simulate(b.dataset.act ?? "")));
1468
+ box.querySelectorAll("[data-toggle-group]").forEach((b) => b.addEventListener("click", () => ctx.fn.toggleGroup(b.dataset.toggleGroup ?? "")));
1469
+ box.querySelector("#simReset")?.addEventListener("click", () => {
1470
+ ctx.fn.clearSim();
1471
+ ctx.fn.renderInspector();
1472
+ ctx.fn.announce("Simulation reset.");
1473
+ });
1474
+ box.querySelector("#zPath")?.addEventListener("click", () => ctx.fn.zoomToPath());
1475
+ box.querySelector("#zBand")?.addEventListener("click", () => {
1476
+ const n = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
1477
+ if (n?.lane) ctx.fn.zoomToBand(n.lane);
1478
+ });
1479
+ }
1480
+ function renderInspector() {
1481
+ const box = els.inspector, ws = els.workspace;
1482
+ const sel = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
1483
+ const head = `<div class="insp-h"><span>Inspector</span><button type="button" id="hideInsp" aria-label="Close the inspector">Close \u2715</button></div>`;
1484
+ if (!sel) {
1485
+ const found = ctx.query && !ctx.dismissed ? model.nodes.filter((n) => nodeVisible(model, ctx.mode, n, ctx.collapsed) && matches(ctx, n)) : [];
1486
+ const open = found.length > 0;
1487
+ box.classList.toggle("open", open);
1488
+ ws.classList.toggle("inspopen", open);
1489
+ box.setAttribute("aria-hidden", String(!open));
1490
+ if (!open) return;
1491
+ const rows = found.map((f) => {
1492
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, f));
1493
+ const hits = (f.members ?? []).filter((m) => m.toLowerCase().includes(ctx.query)).length;
1494
+ return `<button type="button" class="dr" data-g="${esc(f.id)}"><span class="dbar" style="background:var(--${BUCKET_COLOR[b]})"></span><span class="dtx"><span class="dn">${esc(f.title)}</span><span class="dk">${hits} match \xB7 ${esc(model.columns[f.col ?? 0]?.title ?? "")}</span></span></button>`;
1495
+ }).join("");
1496
+ box.innerHTML = head + `<div class="insp-b"><div class="insp-sec"><b>${found.length}</b> group${found.length === 1 ? "" : "s"} match \u201C${esc(ctx.query)}\u201D</div><div class="drill">${rows}</div></div>`;
1497
+ wire(box);
1498
+ return;
1499
+ }
1500
+ box.classList.add("open");
1501
+ ws.classList.add("inspopen");
1502
+ box.setAttribute("aria-hidden", "false");
1503
+ const sid = nodeStateId(model, ctx.mode, sel);
1504
+ const st = model.states[sid];
1505
+ const bucket = bucketOf(model, sid);
1506
+ const lane = model.lanes.find((l) => l.id === sel.lane);
1507
+ const counts = relationsOf(ctx).slice(0, 3).map((r) => {
1508
+ const set = relationSet(ctx, r, sel.id);
1509
+ let n = 0;
1510
+ for (const id of set) {
1511
+ const x = ctx.nodes.get(id);
1512
+ if (!x) continue;
1513
+ if (r.filter) {
1514
+ if (matchesFilter(x, r.filter)) n += Math.max(1, x.weight ?? 1);
1515
+ } else if (id !== sel.id) n += 1;
1516
+ }
1517
+ const cls = r.id === "up" ? "cn--up" : r.id === "down" ? "cn--down" : "";
1518
+ return `<button class="cbtn" data-hl="${esc(r.id)}" aria-pressed="${ctx.hl === r.id}"><span class="cn ${cls}">${n}</span><span class="cl">${esc(r.label)}</span></button>`;
1519
+ }).join("");
1520
+ const pills = `<span class="pill ${PILL[st?.tone ?? "muted"]}">${esc(st?.word ?? sid)}</span>` + (isImproved(model, ctx.mode, sel) ? '<span class="pill info">gap filled</span>' : "") + (isNew(model, ctx.mode, sel) ? '<span class="pill muted">new in this view</span>' : "");
1521
+ const actions = (model.inspector?.actions ?? []).map((a) => `<button type="button" class="danger" data-act="${esc(a.id)}">\u25B6 ${esc(a.label)}</button>`).join("");
1522
+ const members = sel.members ?? [];
1523
+ const owner = groupFor(model, sel.id);
1524
+ const foldAction = sel.kind === "group" && sel.contains?.length && ctx.collapsed.has(sel.id) ? `<button type="button" data-toggle-group="${esc(sel.id)}">Expand group</button>` : owner && !ctx.collapsed.has(owner.id) ? `<button type="button" data-toggle-group="${esc(owner.id)}">Collapse into ${esc(owner.title)}</button>` : "";
1525
+ const exits = sel.kind === "gateway" ? effectiveLinks(model, ctx.mode, ctx.collapsed, ctx.nodes).filter((l) => l.from === sel.id).map((l) => {
1526
+ const target = ctx.nodes.get(l.to);
1527
+ return `<div class="dr"><span class="dtx"><span class="dn">${esc(l.label ?? "Unlabelled")}</span><span class="dk">${esc(target?.title ?? l.to)}</span></span></div>`;
1528
+ }).join("") : "";
1529
+ const metric = sel.metrics;
1530
+ const series = metric?.series ?? [];
1531
+ const points = sparklinePoints(series, 300, 40);
1532
+ const metricHtml = metric ? `<div class="insp-sec metrics"><b>Metrics</b><table class="meta"><tr><th>label</th><td>${esc(metric.label ?? "Metric")}</td></tr><tr><th>value</th><td>${esc(String(metric.value))} ${esc(metric.unit ?? "")}</td></tr>${metric.max != null ? `<tr><th>max</th><td>${esc(String(metric.max))} ${esc(metric.unit ?? "")}</td></tr>` : ""}</table>${points ? `<svg width="300" height="40" viewBox="0 0 300 40" role="img" aria-label="Metric trend"><polyline points="${points}"/></svg><div class="metric-range"><span>${esc(String(Math.min(...series)))}</span><span>${esc(String(Math.max(...series)))}</span></div>` : ""}</div>` : "";
1533
+ box.innerHTML = head + `<div class="counts">${counts}</div>
1534
+ <div class="insp-b"><div class="insp-sec"><p class="nname">${esc(sel.title)}</p><div class="nkind">${esc(model.columns[sel.col ?? 0]?.title ?? "")} \xB7 ${esc(lane?.title ?? sel.lane)}</div><div class="pillrow">${pills}</div>${sel.subtitle ? `<p class="note">${esc(sel.subtitle)}</p>` : ""}</div>
1535
+ <div class="insp-sec"><b>Details</b><table class="meta"><tr><th>state</th><td>${esc(st?.word ?? sid)}</td></tr><tr><th>members</th><td>${Math.max(1, sel.weight ?? 1)}</td></tr><tr><th>lane</th><td>${esc(lane?.title ?? sel.lane)}${lane?.badge ? ` (${esc(lane.badge)})` : ""}</td></tr>${sel.upgrade && bucket !== "good" ? `<tr><th>can improve</th><td>${esc(sel.upgrade)}</td></tr>` : ""}</table></div>
1536
+ ${metricHtml}
1537
+ ${sel.kind === "gateway" ? `<div class="insp-sec"><b>Exits</b><div class="drill">${exits}</div></div>` : ""}
1538
+ ${members.length ? `<div class="insp-sec"><b>Members</b> (${Math.max(members.length, sel.weight ?? 0)})<input id="mq" type="search" placeholder="Filter ${members.length} members\u2026" aria-label="Filter members"><div class="drill" id="mlist"></div><p class="drill-hint" id="mcount"></p></div>` : `<div class="insp-sec"><p class="note">No member names are recorded for this group.</p></div>`}
1539
+ ${foldAction ? `<div class="acts">${foldAction}</div>` : ""}
1540
+ <div class="acts"><button type="button" id="zPath">Zoom to path</button><button type="button" id="zBand">Go to lane</button></div>
1541
+ ${actions ? `<div class="acts">${actions}<button type="button" id="simReset"${ctx.simActive ? "" : " hidden"}>Reset</button></div>` : ""}</div>`;
1542
+ wire(box);
1543
+ const mq = box.querySelector("#mq");
1544
+ const draw = () => {
1545
+ const f = (mq?.value ?? "").toLowerCase();
1546
+ const rows = members.filter((m) => m.toLowerCase().includes(f));
1547
+ const list = box.querySelector("#mlist");
1548
+ if (list) list.innerHTML = rows.slice(0, 14).map((m) => `<div class="dr"><span class="dbar" style="background:var(--${BUCKET_COLOR[bucket]})"></span><span class="dtx"><span class="dn mono" style="font-size:12px">${esc(m)}</span></span></div>`).join("");
1549
+ const cnt = box.querySelector("#mcount");
1550
+ const weight = sel.weight ?? 0;
1551
+ if (cnt) cnt.textContent = `${Math.min(14, rows.length)} of ${rows.length}${weight > members.length ? ` named (${weight} members in all)` : ""}`;
1552
+ };
1553
+ if (mq) {
1554
+ mq.addEventListener("input", draw);
1555
+ draw();
1556
+ }
1557
+ }
1558
+ ctx.fn.renderInspector = renderInspector;
1559
+ }
1560
+
1561
+ // src/chrome.ts
1562
+ var DEFAULT_LABELS = {
1563
+ tileGood: "links in a good state",
1564
+ tileNeutral: "links in a neutral state",
1565
+ tileGap: "groups with a gap: click to show",
1566
+ tileUpgrade: "groups you can improve: click to show",
1567
+ ledgerTitle: "What this view could not establish",
1568
+ tableTitle: "Table view"
1569
+ };
1570
+ function install5(ctx) {
1571
+ const { els, model } = ctx;
1572
+ const label = (k) => model.meta.labels?.[k] ?? DEFAULT_LABELS[k] ?? k;
1573
+ const visible = (id) => {
1574
+ const n = ctx.nodes.get(id);
1575
+ return !!n && nodeVisible(model, ctx.mode, n, ctx.collapsed);
1576
+ };
1577
+ function countUp(node, to, ms = 700) {
1578
+ if (ctx.reduced) {
1579
+ node.textContent = String(to);
1580
+ return;
1581
+ }
1582
+ const t0 = performance.now();
1583
+ const step = (t) => {
1584
+ const k = Math.min(1, (t - t0) / ms), e = 1 - Math.pow(1 - k, 3);
1585
+ node.textContent = String(Math.round(to * e));
1586
+ if (k < 1) requestAnimationFrame(step);
1587
+ };
1588
+ requestAnimationFrame(step);
1589
+ }
1590
+ function buildStrip() {
1591
+ const box = els.strip;
1592
+ box.style.display = model.columns.length ? "" : "none";
1593
+ box.style.gridTemplateColumns = `repeat(${Math.max(1, model.columns.length)}, minmax(0, 1fr))`;
1594
+ if (!box.firstChild) {
1595
+ box.innerHTML = model.columns.map((c) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
1596
+ }
1597
+ box.querySelectorAll(".lcell").forEach((cell, i) => {
1598
+ cell.style.setProperty("--i", String(i));
1599
+ let tot = 0, good = 0, neutral = 0, gap = 0;
1600
+ for (const n of model.nodes) {
1601
+ if (n.col !== i || n.kind === "chip") continue;
1602
+ if (!visible(n.id)) continue;
1603
+ const w = Math.max(1, n.weight ?? 1);
1604
+ tot += w;
1605
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
1606
+ if (b === "good") good += w;
1607
+ else if (b === "neutral") neutral += w;
1608
+ else gap += w;
1609
+ }
1610
+ const t = tot || 1, bars = cell.querySelectorAll("i");
1611
+ requestAnimationFrame(() => {
1612
+ if (bars[0]) bars[0].style.width = good / t * 100 + "%";
1613
+ if (bars[1]) bars[1].style.width = neutral / t * 100 + "%";
1614
+ if (bars[2]) bars[2].style.width = gap / t * 100 + "%";
1615
+ });
1616
+ const lv = cell.querySelector(".lv");
1617
+ if (lv) lv.textContent = `${good + neutral} / ${tot}`;
1618
+ cell.title = `${model.columns[i]?.title ?? ""}: ${good} good, ${neutral} neutral, ${gap} gap`;
1619
+ });
1620
+ }
1621
+ function buildTiles() {
1622
+ const k = els.tiles;
1623
+ let good = 0, neutral = 0;
1624
+ for (const l of effectiveLinks(model, ctx.mode, ctx.collapsed, ctx.nodes)) {
1625
+ const b = bucketOf(model, l.state);
1626
+ if (b === "good") good++;
1627
+ else if (b === "neutral") neutral++;
1628
+ }
1629
+ let gaps = 0, upgrades = 0;
1630
+ for (const n of model.nodes) {
1631
+ if (!visible(n.id) || n.kind === "chip") continue;
1632
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
1633
+ if (b === "gap") gaps += 1;
1634
+ if (n.upgrade && b !== "good") upgrades++;
1635
+ }
1636
+ k.innerHTML = "";
1637
+ const tile = (cls, n, text, filter) => {
1638
+ const b = el(filter ? "button" : "div", "tile " + cls, `<span class="tn">0</span><span class="tl">${esc(text)}</span>`);
1639
+ b.style.setProperty("--i", String(k.children.length + 9));
1640
+ if (filter) {
1641
+ b.type = "button";
1642
+ b.setAttribute("aria-pressed", String(ctx.filter === filter));
1643
+ b.addEventListener("click", () => {
1644
+ ctx.filter = ctx.filter === filter ? null : filter;
1645
+ ctx.fn.select(null);
1646
+ buildTiles();
1647
+ ctx.fn.trace(null);
1648
+ ctx.fn.announce(ctx.filter ? "Filter on" : "Filter off");
1649
+ });
1650
+ }
1651
+ k.appendChild(b);
1652
+ countUp(b.querySelector(".tn"), n);
1653
+ };
1654
+ tile("ok", good, label("tileGood"));
1655
+ tile("", neutral, label("tileNeutral"));
1656
+ tile("warn", gaps, label("tileGap"), "gap");
1657
+ tile("", upgrades, label("tileUpgrade"), "upgrade");
1658
+ }
1659
+ function buildLegend() {
1660
+ const parts = Object.entries(model.states).map(([id, s]) => {
1661
+ const cap = s.edge === "dot" ? ' stroke-linecap="round"' : "";
1662
+ return `<span><svg width="40" height="10" aria-hidden="true"><path d="M0 5 H40" stroke="${toneVar(s.tone)}" stroke-width="2" stroke-dasharray="${DASH[s.edge]}"${cap}/></svg><b class="ev st-${cssId(id)}">${esc(s.word)}</b></span>`;
1663
+ });
1664
+ parts.push('<span id="hintline">scroll = zoom \xB7 drag = pan \xB7 double-click = fit</span>');
1665
+ els.legend.innerHTML = parts.join("");
1666
+ }
1667
+ function buildLedger() {
1668
+ let gaps = 0, total = 0;
1669
+ for (const n of model.nodes) {
1670
+ if (n.kind === "chip" || !visible(n.id)) continue;
1671
+ total++;
1672
+ if (bucketOf(model, nodeStateId(model, ctx.mode, n)) === "gap") gaps++;
1673
+ }
1674
+ const noLane = model.lanes.filter((l) => l.gap).length;
1675
+ els.ledger.innerHTML = `<h4>${esc(label("ledgerTitle"))}</h4><p><b>${gaps}</b> of <b>${total}</b> groups in this view have a gap: they are drawn dotted and are never guessed. ${noLane ? `<b>${noLane}</b> lane${noLane === 1 ? " has" : "s have"} no source document yet.` : ""}</p><p>${esc(model.meta.source)}</p>`;
1676
+ }
1677
+ function buildTable() {
1678
+ const rows = model.nodes.filter((n) => visible(n.id) && n.kind !== "chip").map((n) => {
1679
+ const sid = nodeStateId(model, ctx.mode, n);
1680
+ const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
1681
+ return `<tr><th scope="row">${esc(columnTitle(model, n))}</th><td>${esc(lane)}</td><td>${esc(n.title)}</td><td>${esc(model.states[sid]?.word ?? sid)}</td><td>${Math.max(1, n.weight ?? 1)}</td><td>${esc(n.subtitle ?? "")}</td><td>${esc(n.upgrade ?? "")}</td></tr>`;
1682
+ }).join("");
1683
+ els.table.innerHTML = `<thead><tr><th scope="col">Column</th><th scope="col">Lane</th><th scope="col">Group</th><th scope="col">State</th><th scope="col">Members</th><th scope="col">Detail</th><th scope="col">Can improve</th></tr></thead><tbody>${rows}</tbody>`;
1684
+ }
1685
+ ctx.fn.buildChrome = () => {
1686
+ buildStrip();
1687
+ buildTiles();
1688
+ buildTable();
1689
+ buildLedger();
1690
+ };
1691
+ document.title = model.meta.title;
1692
+ els.title.textContent = model.meta.title;
1693
+ els.help.textContent = model.meta.help;
1694
+ els.readat.textContent = model.meta.asOf ? `as of ${model.meta.asOf}` : "";
1695
+ els.footer.textContent = model.meta.source;
1696
+ buildLegend();
1697
+ const views = model.views ?? [];
1698
+ els.modeSwitch.hidden = views.length < 2;
1699
+ els.modeSwitch.innerHTML = "";
1700
+ for (const v of views) {
1701
+ const b = el("button", "seg");
1702
+ b.type = "button";
1703
+ b.setAttribute("role", "tab");
1704
+ b.dataset.mode = v.id;
1705
+ b.setAttribute("aria-selected", String(v.id === ctx.mode));
1706
+ b.textContent = v.label;
1707
+ on(ctx, b, "click", () => {
1708
+ els.modeSwitch.querySelectorAll("button").forEach((x) => x.setAttribute("aria-selected", String(x === b)));
1709
+ ctx.fn.setMode(v.id);
1710
+ });
1711
+ els.modeSwitch.appendChild(b);
1712
+ }
1713
+ on(ctx, els.q, "input", (e) => {
1714
+ ctx.query = e.target.value.trim().toLowerCase();
1715
+ ctx.dismissed = false;
1716
+ const n = model.nodes.filter((x) => visible(x.id) && matches(ctx, x)).length;
1717
+ els.matchinfo.textContent = ctx.query ? `${n} group${n === 1 ? "" : "s"} match` : "";
1718
+ if (!ctx.selected) {
1719
+ ctx.fn.trace(null);
1720
+ ctx.fn.renderInspector();
1721
+ }
1722
+ });
1723
+ on(ctx, els.hops, "change", () => {
1724
+ ctx.hops = Number(els.hops.value);
1725
+ ctx.fn.trace(ctx.selected);
1726
+ });
1727
+ on(ctx, els.q, "keydown", (e) => {
1728
+ if (e.key !== "Enter" || !ctx.query) return;
1729
+ const ids = model.nodes.filter((x) => visible(x.id) && matches(ctx, x)).map((x) => x.id);
1730
+ const r = ctx.fn.rectOf(ids);
1731
+ if (r) {
1732
+ ctx.userMoved = true;
1733
+ ctx.fn.fit(true, r, els.inspector.classList.contains("open") ? 340 : 0);
1734
+ ctx.fn.announce(`Zoomed to ${ids.length} matching groups`);
1735
+ }
1736
+ });
1737
+ on(ctx, els.theme, "click", () => {
1738
+ const r = document.documentElement;
1739
+ const dark = r.dataset.theme ? r.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
1740
+ r.dataset.theme = dark ? "light" : "dark";
1741
+ });
1742
+ }
1743
+
1744
+ // src/notes.ts
1745
+ function install6(ctx) {
1746
+ const { els, model } = ctx;
1747
+ const notes = ctx.notes.map((n, i) => ({ id: i + 1, ...n }));
1748
+ const card = (id) => els.notelist.querySelector(`[data-n="${id}"]`);
1749
+ const anchorOf = (sel) => {
1750
+ try {
1751
+ return document.querySelector(sel);
1752
+ } catch {
1753
+ return null;
1754
+ }
1755
+ };
1756
+ function clearMarkers() {
1757
+ document.querySelectorAll(".marker").forEach((m) => m.remove());
1758
+ document.querySelectorAll(".anchored").forEach((a) => a.classList.remove("anchored"));
1759
+ }
1760
+ function placeMarkers() {
1761
+ clearMarkers();
1762
+ for (const n of notes) {
1763
+ const a = anchorOf(n.anchor);
1764
+ if (!a) continue;
1765
+ let r = a.getBoundingClientRect();
1766
+ if (!r.width && !r.height) {
1767
+ const d = a.closest("details");
1768
+ if (d) r = d.getBoundingClientRect();
1769
+ }
1770
+ if (!r.width && !r.height) continue;
1771
+ if (a.id === "inspector" && !a.classList.contains("open")) {
1772
+ const vr = els.vp.getBoundingClientRect();
1773
+ r = { left: vr.right - 130, top: vr.top + 56, right: vr.right - 128, bottom: vr.top + 58, width: 2, height: 2, x: 0, y: 0, toJSON: () => ({}) };
1774
+ }
1775
+ const v = els.vp.getBoundingClientRect();
1776
+ if (a.closest("#world") && (r.right < v.left || r.left > v.right || r.bottom < v.top || r.top > v.bottom)) continue;
1777
+ const m = el("button", "marker", String(n.id));
1778
+ m.type = "button";
1779
+ m.dataset.n = String(n.id);
1780
+ m.title = n.title;
1781
+ m.setAttribute("aria-label", `Note ${n.id}: ${n.title}`);
1782
+ m.style.left = r.left + scrollX - 10 + "px";
1783
+ m.style.top = r.top + scrollY - 10 + "px";
1784
+ m.addEventListener("mouseenter", () => {
1785
+ a.classList.add("anchored");
1786
+ card(n.id)?.classList.add("on");
1787
+ });
1788
+ m.addEventListener("mouseleave", () => {
1789
+ a.classList.remove("anchored");
1790
+ card(n.id)?.classList.remove("on");
1791
+ });
1792
+ m.addEventListener("click", () => card(n.id)?.scrollIntoView({ block: "center", behavior: ctx.reduced ? "auto" : "smooth" }));
1793
+ document.body.appendChild(m);
1794
+ }
1795
+ }
1796
+ function toggleNotes(next) {
1797
+ const on_ = next === void 0 ? !document.body.classList.contains("notes") : next;
1798
+ document.body.classList.toggle("notes", on_);
1799
+ els.notesBtn.setAttribute("aria-pressed", String(on_));
1800
+ if (on_) window.setTimeout(placeMarkers, ctx.reduced ? 30 : 340);
1801
+ else clearMarkers();
1802
+ }
1803
+ els.notelist.innerHTML = notes.map((n) => `<div class="ncard" data-n="${n.id}"><b><span>${n.id}</span>${esc(n.title)}</b>${esc(n.text)}</div>`).join("") || '<p class="note">This page has no implementation notes.</p>';
1804
+ els.notelist.querySelectorAll(".ncard").forEach((c) => {
1805
+ c.addEventListener("mouseenter", () => {
1806
+ const n = notes.find((x) => String(x.id) === c.dataset.n);
1807
+ const a = n && anchorOf(n.anchor);
1808
+ if (a) a.classList.add("anchored");
1809
+ });
1810
+ c.addEventListener("mouseleave", () => document.querySelectorAll(".anchored").forEach((a) => a.classList.remove("anchored")));
1811
+ });
1812
+ on(ctx, els.notesBtn, "click", () => toggleNotes());
1813
+ on(ctx, window, "scroll", () => {
1814
+ if (document.body.classList.contains("notes")) placeMarkers();
1815
+ }, { passive: true });
1816
+ const markdown = () => {
1817
+ const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
1818
+ for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
1819
+ out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
1820
+ out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
1821
+ out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
1822
+ if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
1823
+ return out.join("\n");
1824
+ };
1825
+ const rows = (items) => `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
1826
+ els.specbody.innerHTML = `<div class="specbtns"><button type="button" id="cpMd">Copy as Markdown</button><button type="button" id="cpJson">Copy model JSON</button><span class="note" id="cpMsg" style="margin:0"></span></div>
1827
+ <h3>States</h3>${rows(Object.entries(model.states).map(([id, s]) => `<b>${esc(s.word)}</b> (<code>${esc(id)}</code>): tone ${esc(s.tone)}, border ${esc(s.border)}, link ${esc(s.edge)}, motion ${esc(s.motion)}, bucket ${esc(s.bucket)}`))}
1828
+ <h3>Columns</h3>${rows(model.columns.map((c) => `${esc(c.title)}${c.phase ? ` (${esc(c.phase)})` : ""}`))}
1829
+ <h3>Lanes</h3>${rows(model.lanes.map((l) => `${esc(l.title)}${l.badge ? ` [${esc(l.badge)}]` : ""}${l.note ? `: ${esc(l.note)}` : ""}`))}
1830
+ <h3>Size</h3><p>${model.nodes.length} nodes, ${model.links.length} links.</p>`;
1831
+ const msg = (t) => {
1832
+ const m = document.getElementById("cpMsg");
1833
+ if (m) m.textContent = t;
1834
+ };
1835
+ const copy = async (text, what) => {
1836
+ try {
1837
+ await navigator.clipboard.writeText(text);
1838
+ msg(`${what} copied.`);
1839
+ } catch {
1840
+ const ta = document.createElement("textarea");
1841
+ ta.value = text;
1842
+ document.body.appendChild(ta);
1843
+ ta.select();
1844
+ try {
1845
+ document.execCommand("copy");
1846
+ msg(`${what} copied.`);
1847
+ } catch {
1848
+ msg("Copy is blocked here: select the text instead.");
1849
+ }
1850
+ ta.remove();
1851
+ }
1852
+ };
1853
+ document.getElementById("cpMd")?.addEventListener("click", () => void copy(markdown(), "Markdown"));
1854
+ document.getElementById("cpJson")?.addEventListener("click", () => void copy(JSON.stringify(model, null, 2), "JSON"));
1855
+ ctx.fn.toggleNotes = toggleNotes;
1856
+ ctx.fn.placeMarkers = placeMarkers;
1857
+ }
1858
+
1859
+ // src/playback.ts
1860
+ var playback_exports = {};
1861
+ __export(playback_exports, {
1862
+ buildManualRoute: () => buildManualRoute,
1863
+ exitsOf: () => exitsOf,
1864
+ extendScenarioWithRoute: () => extendScenarioWithRoute,
1865
+ formatElapsed: () => formatElapsed,
1866
+ planScenario: () => planScenario,
1867
+ positionAt: () => positionAt,
1868
+ slaState: () => slaState,
1869
+ suggestedExit: () => suggestedExit
1870
+ });
1871
+ function planScenario(model, scenario) {
1872
+ const source = scenario.steps ?? [];
1873
+ const mode = source.some((s) => s?.at !== void 0) ? "elapsed" : "none";
1874
+ const start = source[0]?.at ?? 0;
1875
+ const scale = mode === "elapsed" ? Math.max(1, ...source.slice(1).map((s, i) => {
1876
+ const gap = ((s?.at ?? 0) - (source[i]?.at ?? 0)) * 1e3;
1877
+ return gap > 0 ? (source[i]?.dur ?? 900) / gap : 1;
1878
+ })) : 1;
1879
+ const steps = [];
1880
+ for (let i = 0; i < source.length; i++) {
1881
+ const s = source[i];
1882
+ if (!s || typeof s !== "object") continue;
1883
+ const link = model.links.find((l) => l.id === s.link);
1884
+ if (!link) continue;
1885
+ const from = model.nodes.find((n) => n.id === link.from);
1886
+ const to = model.nodes.find((n) => n.id === link.to);
1887
+ const dur = Number.isFinite(s.dur) && (s.dur ?? 0) > 0 ? s.dur : 900;
1888
+ const proposed = mode === "elapsed" ? Math.max(0, ((s.at ?? 0) - start) * 1e3 * scale) : 0;
1889
+ const at = steps.length === 0 ? 0 : Math.max(proposed, steps.at(-1).at + steps.at(-1).dur);
1890
+ steps.push({ link: s.link, from: from?.ref ?? link.from, to: to?.ref ?? link.to, at, dur, caption: s.caption ?? `${from?.title ?? link.from} to ${to?.title ?? link.to}`, index: i });
1891
+ }
1892
+ return { steps, mode, totalMs: steps.length ? steps.at(-1).at + steps.at(-1).dur : 0 };
1893
+ }
1894
+ function positionAt(plan, tMs) {
1895
+ if (!plan.steps.length) return { stepIndex: -1, progress: 0, done: true };
1896
+ const t = Number.isNaN(tMs) ? 0 : Math.max(0, tMs);
1897
+ if (t >= plan.totalMs) return { stepIndex: plan.steps.length - 1, progress: 1, done: true };
1898
+ let i = plan.steps.length - 1;
1899
+ while (i > 0 && t < plan.steps[i].at) i--;
1900
+ const s = plan.steps[i];
1901
+ return { stepIndex: i, progress: Math.max(0, Math.min(1, (t - s.at) / s.dur)), done: false };
1902
+ }
1903
+ function slaState(elapsedSec, sla) {
1904
+ if (elapsedSec >= sla) return "breach";
1905
+ return elapsedSec >= sla * 0.8 ? "warn" : "ok";
1906
+ }
1907
+ function formatElapsed(sec) {
1908
+ const n = Math.max(0, Math.round(Number.isFinite(sec) ? sec : 0));
1909
+ const m = Math.floor(n / 60), s = String(n % 60).padStart(2, "0");
1910
+ return n < 3600 ? `${m}:${s}` : `${Math.floor(n / 3600)}:${String(m % 60).padStart(2, "0")}:${s}`;
1911
+ }
1912
+ function exitsOf(model, nodeId) {
1913
+ const canon = (id) => model.nodes.find((n) => n.id === id)?.ref ?? id;
1914
+ return model.links.filter((l) => canon(l.from) === canon(nodeId));
1915
+ }
1916
+ function suggestedExit(exits, nextLink) {
1917
+ return exits.find((l) => l.id === nextLink);
1918
+ }
1919
+ function buildManualRoute(model, choiceLink) {
1920
+ const route = [choiceLink];
1921
+ const seen = /* @__PURE__ */ new Set([choiceLink.id]);
1922
+ let at = choiceLink.to;
1923
+ while (exitsOf(model, at).length === 1) {
1924
+ const next = exitsOf(model, at)[0];
1925
+ if (seen.has(next.id)) break;
1926
+ route.push(next);
1927
+ seen.add(next.id);
1928
+ at = next.to;
1929
+ }
1930
+ return route;
1931
+ }
1932
+ function extendScenarioWithRoute(scenario, plan, stepIndex, route) {
1933
+ const isElapsed = plan.mode === "elapsed" || scenario.steps.some((s) => s?.at !== void 0);
1934
+ const prefix = scenario.steps.slice(0, stepIndex + 1).map((s) => ({ ...s }));
1935
+ for (let i = prefix.length; i <= stepIndex && i < plan.steps.length; i++) {
1936
+ const ps = plan.steps[i];
1937
+ prefix.push({ link: ps.link, dur: ps.dur, caption: ps.caption });
1938
+ }
1939
+ let lastAt = 0;
1940
+ if (isElapsed) {
1941
+ const lastWithAt = [...prefix].reverse().find((s) => s?.at !== void 0);
1942
+ lastAt = lastWithAt?.at ?? 0;
1943
+ }
1944
+ const newSteps = route.map((l) => {
1945
+ const step = { link: l.id };
1946
+ if (isElapsed) {
1947
+ lastAt += 1;
1948
+ step.at = lastAt;
1949
+ }
1950
+ return step;
1951
+ });
1952
+ return {
1953
+ ...scenario,
1954
+ steps: [...prefix, ...newSteps]
1955
+ };
1956
+ }
1957
+
1958
+ // src/playbackui.ts
1959
+ function install7(ctx) {
1960
+ const e = ctx.els;
1961
+ const scenarios = ctx.model.scenarios ?? [];
1962
+ e.playbar.hidden = !scenarios.length;
1963
+ e.pbScenario.replaceChildren(...scenarios.map((s) => {
1964
+ const o = document.createElement("option");
1965
+ o.value = s.id;
1966
+ o.textContent = s.label;
1967
+ return o;
1968
+ }));
1969
+ let scenario = scenarios[0];
1970
+ let plan = scenario ? planScenario(ctx.model, scenario) : { steps: [], mode: "none", totalMs: 0 };
1971
+ let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
1972
+ let markedEdges = [];
1973
+ let markedNodes = [];
1974
+ const token = sv("circle", { id: "token", class: "token", r: "6" });
1975
+ e.layer.appendChild(token);
1976
+ const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
1977
+ const nodeOf = (id) => ctx.nodeEls.get(id);
1978
+ const clearMarks = () => {
1979
+ for (const p of markedEdges) p.classList.remove("playing", "visited");
1980
+ for (const n of markedNodes) n.classList.remove("playing", "visited");
1981
+ markedEdges = [];
1982
+ markedNodes = [];
1983
+ };
1984
+ function stop() {
1985
+ playing = false;
1986
+ cancelAnimationFrame(frame);
1987
+ clearTimeout(timer);
1988
+ document.body.classList.remove("playing");
1989
+ e.pbPlay.setAttribute("aria-label", "Play");
1990
+ e.pbPlay.textContent = "\u25B6";
1991
+ }
1992
+ function pause() {
1993
+ if (!playing) return;
1994
+ stop();
1995
+ const p = positionAt(plan, t);
1996
+ draw(p.stepIndex, ctx.reduced ? 1 : p.progress);
1997
+ }
1998
+ function choices(step2) {
1999
+ if (playing || !step2) {
2000
+ if (choicesKey !== "") {
2001
+ e.pbChoices.replaceChildren();
2002
+ choicesKey = "";
2003
+ }
2004
+ return;
2005
+ }
2006
+ const key = `${scenario?.id ?? ""}|${idx}|${plan.steps.length}|${plan.steps[idx + 1]?.link ?? ""}`;
2007
+ if (key === choicesKey) return;
2008
+ choicesKey = key;
2009
+ e.pbChoices.replaceChildren();
2010
+ const exits = exitsOf(ctx.model, step2.to);
2011
+ if (exits.length < 2) return;
2012
+ const suggested = suggestedExit(exits, plan.steps[idx + 1]?.link);
2013
+ for (const link of exits) {
2014
+ const b = document.createElement("button");
2015
+ b.type = "button";
2016
+ b.textContent = link.label ?? ctx.nodes.get(link.to)?.title ?? link.to;
2017
+ b.dataset.link = link.id;
2018
+ if (link.id === suggested?.id) b.classList.add("suggested");
2019
+ b.addEventListener("click", () => {
2020
+ if (!scenario) return;
2021
+ const route = buildManualRoute(ctx.model, link);
2022
+ scenario = extendScenarioWithRoute(scenario, plan, idx, route);
2023
+ plan = planScenario(ctx.model, scenario);
2024
+ t = plan.steps[idx + 1]?.at ?? 0;
2025
+ play();
2026
+ });
2027
+ e.pbChoices.appendChild(b);
2028
+ }
2029
+ }
2030
+ function draw(stepIndex, progress) {
2031
+ const step2 = plan.steps[stepIndex];
2032
+ clearMarks();
2033
+ if (!step2) {
2034
+ token.style.display = "none";
2035
+ return;
2036
+ }
2037
+ token.style.display = "";
2038
+ for (let i = 0; i < stepIndex; i++) {
2039
+ const s = plan.steps[i];
2040
+ const p = pathOf(s.link);
2041
+ if (p) {
2042
+ p.classList.add("visited");
2043
+ markedEdges.push(p);
2044
+ }
2045
+ const n = nodeOf(s.to);
2046
+ if (n) {
2047
+ n.classList.add("visited");
2048
+ markedNodes.push(n);
2049
+ }
2050
+ }
2051
+ const path = pathOf(step2.link);
2052
+ if (path) {
2053
+ path.classList.add("playing");
2054
+ markedEdges.push(path);
2055
+ }
2056
+ const node = nodeOf(step2.to);
2057
+ if (node) {
2058
+ node.classList.add("playing");
2059
+ markedNodes.push(node);
2060
+ }
2061
+ if (path && !path.classList.contains("off")) {
2062
+ const p = path.getPointAtLength(path.getTotalLength() * progress);
2063
+ token.setAttribute("cx", String(p.x));
2064
+ token.setAttribute("cy", String(p.y));
2065
+ } else token.style.display = "none";
2066
+ e.pbCaption.textContent = step2.caption;
2067
+ e.pbScrub.value = String(plan.totalMs ? Math.round(t / plan.totalMs * 1e3) : 0);
2068
+ e.pbScrub.setAttribute("aria-valuetext", `Step ${stepIndex + 1} of ${plan.steps.length}: ${step2.caption}`);
2069
+ e.pbCounter.className = "";
2070
+ e.pbCounter.hidden = plan.mode === "none";
2071
+ if (plan.mode === "elapsed") {
2072
+ const sec = scenario?.steps[step2.index]?.at ?? 0;
2073
+ const sla = ctx.nodes.get(step2.to)?.sla;
2074
+ e.pbCounter.textContent = sla === void 0 ? formatElapsed(sec) : `${formatElapsed(sec)} / ${formatElapsed(sla)}`;
2075
+ if (sla !== void 0) e.pbCounter.classList.add(slaState(sec, sla));
2076
+ }
2077
+ if (idx !== stepIndex) ctx.fn.announce(step2.caption);
2078
+ idx = stepIndex;
2079
+ choices(step2);
2080
+ }
2081
+ function rewind() {
2082
+ stop();
2083
+ t = 0;
2084
+ idx = -1;
2085
+ clearMarks();
2086
+ choicesKey = "";
2087
+ e.pbCaption.textContent = "";
2088
+ e.pbCounter.hidden = true;
2089
+ e.pbCounter.className = "";
2090
+ e.pbChoices.replaceChildren();
2091
+ token.style.display = "none";
2092
+ e.pbScrub.value = "0";
2093
+ e.pbScrub.setAttribute("aria-valuetext", "");
2094
+ }
2095
+ function select(id) {
2096
+ scenario = scenarios.find((s) => s.id === id) ?? scenarios[0];
2097
+ plan = scenario ? planScenario(ctx.model, scenario) : { steps: [], mode: "none", totalMs: 0 };
2098
+ e.pbScenario.value = scenario?.id ?? "";
2099
+ rewind();
2100
+ }
2101
+ function seek(value) {
2102
+ stop();
2103
+ t = Math.max(0, Math.min(plan.totalMs, Number.isFinite(value) ? value : 0));
2104
+ const p = positionAt(plan, t);
2105
+ draw(p.stepIndex, ctx.reduced ? 1 : p.progress);
2106
+ }
2107
+ function tick(now) {
2108
+ if (!playing) return;
2109
+ if (!stamp) stamp = now;
2110
+ const delta = (now - stamp) * Number(e.pbSpeed.value || 1);
2111
+ stamp = now;
2112
+ t = Math.min(plan.totalMs, t + delta);
2113
+ const p = positionAt(plan, t);
2114
+ if (p.done) {
2115
+ stop();
2116
+ draw(p.stepIndex, 1);
2117
+ return;
2118
+ }
2119
+ draw(p.stepIndex, p.progress);
2120
+ frame = requestAnimationFrame(tick);
2121
+ }
2122
+ function reducedStep() {
2123
+ if (!playing) return;
2124
+ const next = Math.min(plan.steps.length - 1, idx + 1);
2125
+ if (next < 0) {
2126
+ stop();
2127
+ return;
2128
+ }
2129
+ t = plan.steps[next].at + plan.steps[next].dur;
2130
+ if (next === plan.steps.length - 1) stop();
2131
+ draw(next, 1);
2132
+ if (playing) timer = window.setTimeout(reducedStep, 1e3 / Number(e.pbSpeed.value || 1));
2133
+ }
2134
+ function play(id) {
2135
+ if (id && id !== scenario?.id) select(id);
2136
+ stop();
2137
+ if (!plan.steps.length) return;
2138
+ if (t >= plan.totalMs) {
2139
+ t = 0;
2140
+ idx = -1;
2141
+ }
2142
+ playing = true;
2143
+ document.body.classList.add("playing");
2144
+ e.pbPlay.setAttribute("aria-label", "Pause");
2145
+ e.pbPlay.textContent = "\u275A\u275A";
2146
+ e.pbChoices.replaceChildren();
2147
+ choicesKey = "";
2148
+ if (ctx.reduced) reducedStep();
2149
+ else {
2150
+ stamp = 0;
2151
+ frame = requestAnimationFrame(tick);
2152
+ }
2153
+ }
2154
+ function step() {
2155
+ stop();
2156
+ const next = Math.min(idx + 1, plan.steps.length - 1);
2157
+ if (next < 0) return;
2158
+ t = plan.steps[next].at + plan.steps[next].dur;
2159
+ draw(next, 1);
2160
+ }
2161
+ on(ctx, e.pbScenario, "change", () => select(e.pbScenario.value));
2162
+ on(ctx, e.pbPlay, "click", () => playing ? pause() : play());
2163
+ on(ctx, e.pbStep, "click", step);
2164
+ on(ctx, e.pbReset, "click", () => rewind());
2165
+ on(ctx, e.pbScrub, "input", () => seek(Number(e.pbScrub.value) / 1e3 * plan.totalMs));
2166
+ on(ctx, document, "keydown", (event) => {
2167
+ if (event.ctrlKey || event.metaKey || event.altKey || event.target instanceof HTMLElement && (event.target.isContentEditable || /INPUT|TEXTAREA|SELECT/.test(event.target.tagName))) return;
2168
+ if (event.key.toLowerCase() === "p") {
2169
+ event.preventDefault();
2170
+ playing ? pause() : play();
2171
+ }
2172
+ if (event.key === ".") {
2173
+ event.preventDefault();
2174
+ step();
2175
+ }
2176
+ });
2177
+ ctx.disposers.push(() => {
2178
+ stop();
2179
+ token.remove();
2180
+ });
2181
+ select(scenario?.id ?? "");
2182
+ return {
2183
+ play,
2184
+ pause,
2185
+ step,
2186
+ seek,
2187
+ playback: () => ({ scenario: scenario?.id ?? null, step: idx, playing, t }),
2188
+ restore: (state) => {
2189
+ if (!state.scenario || !scenarios.some((s) => s.id === state.scenario)) return;
2190
+ select(state.scenario);
2191
+ const current = plan.steps[state.step];
2192
+ if (!current) return;
2193
+ t = Math.max(current.at, Math.min(current.at + current.dur, state.t));
2194
+ draw(state.step, ctx.reduced ? 1 : current.dur > 0 ? (t - current.at) / current.dur : 1);
2195
+ if (state.playing) play();
2196
+ },
2197
+ reset: () => rewind()
2198
+ };
2199
+ }
2200
+
2201
+ // src/diff.ts
2202
+ var diff_exports = {};
2203
+ __export(diff_exports, {
2204
+ diffModels: () => diffModels
2205
+ });
2206
+ var fields = ["state", "stateIn", "title", "subtitle", "weight", "metrics", "visibleIn", "lane", "sla"];
2207
+ function stableStringify(val) {
2208
+ if (val === void 0) return "undefined";
2209
+ if (val === null) return "null";
2210
+ if (typeof val === "number") return Number.isFinite(val) ? JSON.stringify(val) : String(val);
2211
+ if (typeof val === "bigint") return String(val);
2212
+ if (typeof val === "object") {
2213
+ if (Array.isArray(val)) return `[${val.map(stableStringify).join(",")}]`;
2214
+ const rec = val;
2215
+ const keys = Object.keys(rec).filter((k) => rec[k] !== void 0).sort();
2216
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
2217
+ }
2218
+ return JSON.stringify(val) ?? "undefined";
2219
+ }
2220
+ function diffModels(a, b) {
2221
+ const old = new Map(a.nodes.map((n) => [n.id, n]));
2222
+ const now = new Map(b.nodes.map((n) => [n.id, n]));
2223
+ return {
2224
+ added: b.nodes.filter((n) => !old.has(n.id)).map((n) => n.id),
2225
+ removed: a.nodes.filter((n) => !now.has(n.id)).map((n) => n.id),
2226
+ changed: b.nodes.filter((n) => {
2227
+ const prev = old.get(n.id);
2228
+ return prev && fields.some((k) => stableStringify(prev[k]) !== stableStringify(n[k]));
2229
+ }).map((n) => n.id)
2230
+ };
2231
+ }
2232
+
2233
+ // src/engine.ts
2234
+ var IDS = {
2235
+ vp: "viewport",
2236
+ world: "world",
2237
+ svg: "edges",
2238
+ layer: "edgeLayer",
2239
+ cols: "cols",
2240
+ tools: "tools",
2241
+ zoom: "zoomLabel",
2242
+ inspector: "inspector",
2243
+ workspace: "workspace",
2244
+ live: "live",
2245
+ strip: "layerstrip",
2246
+ tiles: "kpis",
2247
+ legend: "legend",
2248
+ ledger: "ledger",
2249
+ table: "tbl",
2250
+ q: "q",
2251
+ hops: "hops",
2252
+ matchinfo: "matchinfo",
2253
+ modeSwitch: "modeSwitch",
2254
+ drawer: "drawer",
2255
+ notelist: "notelist",
2256
+ notesBtn: "notesBtn",
2257
+ theme: "theme",
2258
+ specbody: "specbody",
2259
+ spec: "spec",
2260
+ title: "title",
2261
+ readat: "readat",
2262
+ help: "help",
2263
+ footer: "footer",
2264
+ playbar: "playbar",
2265
+ pbScenario: "pbScenario",
2266
+ pbPlay: "pbPlay",
2267
+ pbStep: "pbStep",
2268
+ pbReset: "pbReset",
2269
+ pbSpeed: "pbSpeed",
2270
+ pbScrub: "pbScrub",
2271
+ pbCaption: "pbCaption",
2272
+ pbCounter: "pbCounter",
2273
+ pbChoices: "pbChoices",
2274
+ banner: "banner"
2275
+ };
2276
+ function findEls(root) {
2277
+ const out = {};
2278
+ for (const [key, id] of Object.entries(IDS)) {
2279
+ const e = root.querySelector("#" + id);
2280
+ if (!e) throw new Error(`flow-studio: the page has no element #${id}`);
2281
+ out[key] = e;
2282
+ }
2283
+ return out;
2284
+ }
2285
+ function mount(root, model, opts = {}) {
2286
+ if (!model || !Array.isArray(model.columns) || !Array.isArray(model.lanes) || !Array.isArray(model.nodes) || !Array.isArray(model.links) || !model.states) {
2287
+ throw new Error("flow-studio: the model needs columns, lanes, states, nodes and links");
2288
+ }
2289
+ if (model.layout === "auto") model = autoLayout(model);
2290
+ const els = findEls(root);
2291
+ const issues = validateModel(model);
2292
+ const errors = issues.filter((i) => i.level === "error");
2293
+ for (const i of issues) (i.level === "error" ? console.error : console.warn)(`flow-studio ${i.code} ${i.where}: ${i.message}`);
2294
+ if (errors.length) els.help.textContent = `${errors.length} model error(s). See the console. The page shows what it can.`;
2295
+ const rm = matchMedia("(prefers-reduced-motion: reduce)");
2296
+ const ctx = {
2297
+ model,
2298
+ root,
2299
+ els,
2300
+ onSelect: opts.onSelect,
2301
+ notes: opts.notes && opts.notes.length ? opts.notes : model.notes ?? [],
2302
+ nodes: new Map(model.nodes.map((n) => [n.id, n])),
2303
+ links: model.links,
2304
+ geom: { xs: [], nodes: {}, lanes: {}, loops: {}, bounds: { x: 0, y: 0, w: 1, h: 1 }, world: { w: 1, h: 1 } },
2305
+ view: { x: 0, y: 0, k: 1 },
2306
+ reduced: rm.matches,
2307
+ mode: opts.restore && (model.views?.some((v) => v.id === opts.restore.mode) || !model.views?.length && opts.restore.mode === "all") ? opts.restore.mode : defaultView(model),
2308
+ collapsed: opts.restore ? new Set(model.nodes.filter((n) => n.kind === "group" && n.contains && (opts.restore.collapsed.has(n.id) || !opts.restore.knownGroups.has(n.id) && n.collapsed !== false)).map((n) => n.id)) : initialCollapsed(model),
2309
+ selected: opts.restore?.selected && model.nodes.some((n) => n.id === opts.restore.selected) ? opts.restore.selected : null,
2310
+ hl: null,
2311
+ query: opts.restore?.query ?? "",
2312
+ hops: opts.restore?.hops ?? 0,
2313
+ filter: opts.restore?.filter ?? null,
2314
+ tool: "select",
2315
+ spaceDown: false,
2316
+ userMoved: false,
2317
+ dismissed: false,
2318
+ simActive: false,
2319
+ moved: 0,
2320
+ nodeEls: /* @__PURE__ */ new Map(),
2321
+ laneEls: /* @__PURE__ */ new Map(),
2322
+ edgeEls: [],
2323
+ labelEls: [],
2324
+ overlays: [],
2325
+ simTimers: [],
2326
+ disposers: [],
2327
+ fn: {}
2328
+ };
2329
+ const onRm = (e) => {
2330
+ ctx.reduced = e.matches;
2331
+ };
2332
+ rm.addEventListener("change", onRm);
2333
+ ctx.disposers.push(() => rm.removeEventListener("change", onRm));
2334
+ let style = document.getElementById("state-css");
2335
+ if (!style) {
2336
+ style = document.createElement("style");
2337
+ style.id = "state-css";
2338
+ document.head.prepend(style);
2339
+ }
2340
+ style.textContent = stateCss(model.states);
2341
+ install(ctx);
2342
+ install2(ctx);
2343
+ install3(ctx);
2344
+ install4(ctx);
2345
+ install5(ctx);
2346
+ install6(ctx);
2347
+ els.q.value = ctx.query;
2348
+ els.hops.value = String(ctx.hops);
2349
+ ctx.fn.buildWorld();
2350
+ ctx.fn.paint(false);
2351
+ const playback = install7(ctx);
2352
+ if (opts.restore) {
2353
+ ctx.userMoved = true;
2354
+ ctx.fn.setView(opts.restore.view, false);
2355
+ playback.restore(opts.restore.playback);
2356
+ if (opts.restore.inspectorOpen) {
2357
+ els.inspector.classList.add("open");
2358
+ els.workspace.classList.add("inspopen");
2359
+ els.inspector.setAttribute("aria-hidden", "false");
2360
+ ctx.fn.renderInspector();
2361
+ } else {
2362
+ els.inspector.classList.remove("open");
2363
+ els.workspace.classList.remove("inspopen");
2364
+ els.inspector.setAttribute("aria-hidden", "true");
2365
+ }
2366
+ } else {
2367
+ document.body.classList.add("intro");
2368
+ const introTimer = window.setTimeout(() => document.body.classList.remove("intro"), 2100);
2369
+ ctx.disposers.push(() => {
2370
+ window.clearTimeout(introTimer);
2371
+ document.body.classList.remove("intro");
2372
+ });
2373
+ ctx.fn.fitWidth(false);
2374
+ const fitTimer = window.setTimeout(() => {
2375
+ if (!ctx.userMoved) ctx.fn.fitWidth(false);
2376
+ }, 120);
2377
+ ctx.disposers.push(() => window.clearTimeout(fitTimer));
2378
+ }
2379
+ const api = {
2380
+ model,
2381
+ geometry: () => ctx.geom,
2382
+ view: () => ({ ...ctx.view }),
2383
+ setView: (v, animate) => ctx.fn.setView(v, animate),
2384
+ fit: (animate) => {
2385
+ ctx.userMoved = false;
2386
+ ctx.fn.fit(animate);
2387
+ },
2388
+ select: (id) => ctx.fn.select(id),
2389
+ selected: () => ctx.selected,
2390
+ mode: () => ctx.mode,
2391
+ setMode: (id) => ctx.fn.setMode(id),
2392
+ simulate: (id) => ctx.fn.simulate(id),
2393
+ resetSimulation: () => {
2394
+ ctx.fn.clearSim();
2395
+ ctx.fn.renderInspector();
2396
+ },
2397
+ play: (id) => playback.play(id),
2398
+ pause: () => playback.pause(),
2399
+ step: () => playback.step(),
2400
+ seek: (t) => playback.seek(t),
2401
+ playback: () => playback.playback(),
2402
+ update: (nextModel, updateOpts) => {
2403
+ const prepared = nextModel.layout === "auto" ? autoLayout(nextModel) : nextModel;
2404
+ const nextIssues = validateModel(prepared);
2405
+ const count = nextIssues.filter((i) => i.level === "error").length;
2406
+ if (count) {
2407
+ els.banner.textContent = `Update failed: ${count} errors. Showing the last good graph.`;
2408
+ els.banner.hidden = false;
2409
+ return { ok: false, issues: nextIssues };
2410
+ }
2411
+ const changes = diffModels(ctx.model, prepared);
2412
+ const saved = { view: { ...ctx.view }, mode: ctx.mode, selected: ctx.selected, inspectorOpen: els.inspector.classList.contains("open"), query: ctx.query, hops: ctx.hops, filter: ctx.filter, collapsed: new Set(ctx.collapsed), knownGroups: new Set(ctx.model.nodes.filter((n) => n.kind === "group").map((n) => n.id)), playback: playback.playback() };
2413
+ api.destroy();
2414
+ const replacement = mount(root, prepared, { notes: opts.notes, onSelect: opts.onSelect, restore: saved });
2415
+ notifySelection(saved.selected, replacement.selected(), opts.onSelect);
2416
+ Object.assign(api, replacement);
2417
+ window.FlowStudio = api;
2418
+ els.banner.hidden = true;
2419
+ els.banner.textContent = "";
2420
+ if (updateOpts?.animate !== false && !ctx.reduced) {
2421
+ for (const id of changes.changed) root.querySelector(`#n_${CSS.escape(id)}`)?.classList.add("updated");
2422
+ for (const id of changes.added) root.querySelector(`#n_${CSS.escape(id)}`)?.classList.add("added");
2423
+ }
2424
+ return { ok: true, issues: nextIssues };
2425
+ },
2426
+ destroy: () => {
2427
+ ctx.fn.clearSim();
2428
+ ctx.disposers.forEach((d) => d());
2429
+ ctx.disposers = [];
2430
+ style?.remove();
2431
+ if (window.FlowStudio === api || window.FlowStudio?.model === api.model) delete window.FlowStudio;
2432
+ }
2433
+ };
2434
+ window.FlowStudio = api;
2435
+ return api;
2436
+ }
2437
+
2438
+ // src/lib.ts
2439
+ var SHELL_BODY_HTML = "<header class=\"top\">\r\n <span class=\"brand\"><i></i>flow-studio</span>\r\n <span class=\"spacer\"></span>\r\n <button id=\"notesBtn\" type=\"button\" aria-pressed=\"false\">Implementation notes (N)</button>\r\n <button id=\"theme\" type=\"button\" aria-label=\"Toggle light and dark\">☀ / ☾</button>\r\n</header>\r\n<main>\r\n <div class=\"titlerow\"><h1 id=\"title\"></h1><span class=\"readline\" id=\"readat\"></span></div>\r\n <p class=\"help\" id=\"help\"></p>\r\n <div id=\"banner\" role=\"alert\" hidden></div>\r\n\r\n <section class=\"summary\"><div class=\"layerstrip\" id=\"layerstrip\" aria-label=\"Completeness by column\"></div><div class=\"tiles\" id=\"kpis\" aria-label=\"Summary\"></div></section>\r\n\r\n <div class=\"toolbar\">\r\n <div class=\"seggrp\" role=\"tablist\" aria-label=\"View\" id=\"modeSwitch\"></div>\r\n <input id=\"q\" type=\"search\" placeholder=\"Find a group or member… ( / )\" aria-label=\"Find a group or member\">\r\n <select id=\"hops\" aria-label=\"Search radius\"><option value=\"0\">This group</option><option value=\"1\">+1 hop</option><option value=\"2\">+2 hops</option><option value=\"3\">+3 hops</option></select>\r\n <span id=\"matchinfo\" class=\"note\" style=\"margin:0\"></span>\r\n </div>\r\n\r\n <div class=\"workspace\" id=\"workspace\">\r\n <section class=\"canvaswrap\">\r\n <div id=\"viewport\" tabindex=\"0\" role=\"application\" aria-label=\"Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.\">\r\n <div id=\"world\"><svg id=\"edges\" aria-hidden=\"true\"><defs><marker id=\"arr\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\" markerWidth=\"9\" markerHeight=\"9\" markerUnits=\"userSpaceOnUse\" orient=\"auto\"><path d=\"M0 0 L8 4 L0 8 z\" fill=\"context-stroke\"/></marker></defs><g id=\"edgeLayer\"></g></svg></div>\r\n <div id=\"cols\" aria-hidden=\"true\"></div>\r\n <div id=\"tools\" role=\"toolbar\" aria-label=\"Canvas controls\" aria-orientation=\"vertical\"></div>\r\n <div id=\"zoomLabel\" aria-hidden=\"true\">100%</div>\r\n </div>\r\n <div id=\"playbar\" hidden aria-label=\"Scenario playback\">\r\n <select id=\"pbScenario\" aria-label=\"Scenario\"></select>\r\n <button id=\"pbPlay\" type=\"button\" aria-label=\"Play\">▶</button>\r\n <button id=\"pbStep\" type=\"button\" aria-label=\"Step\">Step</button>\r\n <button id=\"pbReset\" type=\"button\" aria-label=\"Reset\">Reset</button>\r\n <select id=\"pbSpeed\" aria-label=\"Speed\"><option value=\"0.5\">0.5x</option><option value=\"1\" selected>1x</option><option value=\"2\">2x</option></select>\r\n <input id=\"pbScrub\" type=\"range\" min=\"0\" max=\"1000\" value=\"0\" aria-label=\"Playback position\" aria-valuetext=\"Step 1 of 1\">\r\n <span id=\"pbCaption\"></span><span id=\"pbCounter\"></span><span id=\"pbChoices\"></span>\r\n </div>\r\n <div class=\"keyline\" id=\"legend\" aria-label=\"Legend\"></div>\r\n <aside class=\"insp\" id=\"inspector\" aria-label=\"Inspector\" aria-hidden=\"true\"></aside>\r\n </section>\r\n </div>\r\n <div id=\"live\" class=\"sr\" role=\"status\" aria-live=\"polite\"></div>\r\n\r\n <div class=\"ledger\" id=\"ledger\"></div>\r\n <details class=\"tableview\"><summary>Table view</summary><table class=\"sources\" id=\"tbl\"></table></details>\r\n <details class=\"spec\" id=\"spec\"><summary>Model reference (for the person or agent who builds this)</summary><div id=\"specbody\"></div></details>\r\n</main>\r\n<footer id=\"footer\"></footer>\r\n<aside id=\"drawer\" aria-label=\"Implementation notes\"><h2>Implementation notes</h2><p class=\"note\">Numbered markers on the page match these cards. Hover a card to find its place; click a marker to jump to its card.</p><div id=\"notelist\"></div></aside>\r\n\r\n";
2440
+ export {
2441
+ K_MAX,
2442
+ K_MIN,
2443
+ NODE_HEIGHT,
2444
+ SHELL_BODY_HTML,
2445
+ autoLayout,
2446
+ baselineView,
2447
+ bucketOf,
2448
+ buildManualRoute,
2449
+ canonOf,
2450
+ clampK,
2451
+ clampPan,
2452
+ columnTitle,
2453
+ computeGeometry,
2454
+ connected,
2455
+ defaultView,
2456
+ diff_exports as diff,
2457
+ diffModels,
2458
+ effectiveLinks,
2459
+ elbow,
2460
+ esc,
2461
+ exitsOf,
2462
+ extendScenarioWithRoute,
2463
+ fitRect,
2464
+ fitWidth,
2465
+ formatElapsed,
2466
+ groupFor,
2467
+ initialCollapsed,
2468
+ isImproved,
2469
+ isNew,
2470
+ linkStateId,
2471
+ linkVisible,
2472
+ loopBack,
2473
+ mount,
2474
+ nodeStateId,
2475
+ nodeVisible,
2476
+ planScenario,
2477
+ playback_exports as playback,
2478
+ positionAt,
2479
+ relatives,
2480
+ routing_exports as routing,
2481
+ rules_exports as rules,
2482
+ slaState,
2483
+ stateCss,
2484
+ suggestedExit,
2485
+ toggleGroupState,
2486
+ trace_exports as trace,
2487
+ validateModel,
2488
+ viewport_exports as viewport,
2489
+ withinHops,
2490
+ zoomAt
2491
+ };