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