@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,193 @@
1
+ // flow-studio engine entry. mount() draws a model into a page that has the shell's element ids and returns
2
+ // a small API (also placed on window.FlowStudio for the check harness). The generated demo page calls mount() itself.
3
+
4
+ import { validateModel, type Model, type NoteDef } from "./model.ts";
5
+ import { autoLayout } from "./autolayout.ts";
6
+ import type { Ctx, Els } from "./ctx.ts";
7
+ import type { View } from "./viewport.ts";
8
+ import type { Geometry } from "./layout.ts";
9
+ import { stateCss } from "./stateCss.ts";
10
+ import { defaultView, initialCollapsed } from "./rules.ts";
11
+ import { install as installInteract } from "./interact.ts";
12
+ import { install as installRender } from "./render.ts";
13
+ import { install as installControls } from "./controls.ts";
14
+ import { install as installInspector } from "./inspector.ts";
15
+ import { install as installChrome } from "./chrome.ts";
16
+ import { install as installNotes } from "./notes.ts";
17
+ import { install as installPlayback, type PlaybackController } from "./playbackui.ts";
18
+ import { diffModels } from "./diff.ts";
19
+ import { notifySelection } from "./selection.ts";
20
+ import type { Issue } from "./model.ts";
21
+
22
+ export interface FlowStudio {
23
+ model: Model;
24
+ geometry(): Geometry;
25
+ view(): View;
26
+ setView(v: View, animate?: boolean): void;
27
+ fit(animate?: boolean): void;
28
+ select(id: string | null): void;
29
+ selected(): string | null;
30
+ mode(): string;
31
+ setMode(id: string): void;
32
+ simulate(actionId: string): void;
33
+ resetSimulation(): void;
34
+ play(scenarioId?: string): void;
35
+ pause(): void;
36
+ step(): void;
37
+ seek(tMs: number): void;
38
+ playback(): { scenario: string | null; step: number; playing: boolean; t: number };
39
+ update(model: Model, opts?: { animate?: boolean }): { ok: boolean; issues: Issue[] };
40
+ destroy(): void;
41
+ }
42
+
43
+ declare global {
44
+ interface Window { FlowStudio?: FlowStudio; flowStudioMount?: typeof mount }
45
+ }
46
+
47
+ const IDS: Record<keyof Els, string> = {
48
+ vp: "viewport", world: "world", svg: "edges", layer: "edgeLayer", cols: "cols", tools: "tools", zoom: "zoomLabel",
49
+ inspector: "inspector", workspace: "workspace", live: "live", strip: "layerstrip", tiles: "kpis", legend: "legend",
50
+ ledger: "ledger", table: "tbl", q: "q", hops: "hops", matchinfo: "matchinfo", modeSwitch: "modeSwitch", drawer: "drawer",
51
+ notelist: "notelist", notesBtn: "notesBtn", theme: "theme", specbody: "specbody", spec: "spec", title: "title",
52
+ readat: "readat", help: "help", footer: "footer", playbar: "playbar", pbScenario: "pbScenario", pbPlay: "pbPlay",
53
+ pbStep: "pbStep", pbReset: "pbReset", pbSpeed: "pbSpeed", pbScrub: "pbScrub", pbCaption: "pbCaption",
54
+ pbCounter: "pbCounter", pbChoices: "pbChoices", banner: "banner",
55
+ };
56
+
57
+ function findEls(root: ParentNode): Els {
58
+ const out = {} as Record<string, Element>;
59
+ for (const [key, id] of Object.entries(IDS)) {
60
+ const e = root.querySelector("#" + id);
61
+ if (!e) throw new Error(`flow-studio: the page has no element #${id}`);
62
+ out[key] = e;
63
+ }
64
+ return out as unknown as Els;
65
+ }
66
+
67
+ interface SavedState { view: View; mode: string; selected: string | null; inspectorOpen: boolean; query: string; hops: number; filter: Ctx["filter"]; collapsed: Set<string>; knownGroups: Set<string>; playback: ReturnType<PlaybackController["playback"]> }
68
+ export function mount(root: ParentNode, model: Model, opts: { notes?: NoteDef[]; onSelect?: (id: string | null) => void; restore?: SavedState } = {}): FlowStudio {
69
+ if (!model || !Array.isArray(model.columns) || !Array.isArray(model.lanes) || !Array.isArray(model.nodes) || !Array.isArray(model.links) || !model.states) {
70
+ throw new Error("flow-studio: the model needs columns, lanes, states, nodes and links");
71
+ }
72
+ if (model.layout === "auto") model = autoLayout(model);
73
+ const els = findEls(root);
74
+ const issues = validateModel(model);
75
+ const errors = issues.filter(i => i.level === "error");
76
+ for (const i of issues) (i.level === "error" ? console.error : console.warn)(`flow-studio ${i.code} ${i.where}: ${i.message}`);
77
+ if (errors.length) els.help.textContent = `${errors.length} model error(s). See the console. The page shows what it can.`;
78
+
79
+ const rm = matchMedia("(prefers-reduced-motion: reduce)");
80
+ const ctx: Ctx = {
81
+ model, root, els, onSelect: opts.onSelect,
82
+ notes: opts.notes && opts.notes.length ? opts.notes : model.notes ?? [],
83
+ nodes: new Map(model.nodes.map(n => [n.id, n])),
84
+ links: model.links,
85
+ geom: { xs: [], nodes: {}, lanes: {}, loops: {}, bounds: { x: 0, y: 0, w: 1, h: 1 }, world: { w: 1, h: 1 } },
86
+ view: { x: 0, y: 0, k: 1 },
87
+ reduced: rm.matches,
88
+ mode: opts.restore && (model.views?.some(v => v.id === opts.restore!.mode) || (!model.views?.length && opts.restore.mode === "all")) ? opts.restore.mode : defaultView(model),
89
+ 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),
90
+ selected: opts.restore?.selected && model.nodes.some(n => n.id === opts.restore!.selected) ? opts.restore.selected : null,
91
+ hl: null, query: opts.restore?.query ?? "", hops: opts.restore?.hops ?? 0, filter: opts.restore?.filter ?? null, tool: "select",
92
+ spaceDown: false, userMoved: false, dismissed: false, simActive: false, moved: 0,
93
+ nodeEls: new Map(), laneEls: new Map(), edgeEls: [], labelEls: [], overlays: [], simTimers: [], disposers: [],
94
+ fn: {} as Ctx["fn"],
95
+ };
96
+ const onRm = (e: MediaQueryListEvent) => { ctx.reduced = e.matches; };
97
+ rm.addEventListener("change", onRm);
98
+ ctx.disposers.push(() => rm.removeEventListener("change", onRm));
99
+
100
+ let style = document.getElementById("state-css") as HTMLStyleElement | null;
101
+ if (!style) { style = document.createElement("style"); style.id = "state-css"; document.head.prepend(style); }
102
+ style.textContent = stateCss(model.states);
103
+
104
+ installInteract(ctx);
105
+ installRender(ctx);
106
+ installControls(ctx);
107
+ installInspector(ctx);
108
+ installChrome(ctx);
109
+ installNotes(ctx);
110
+ els.q.value = ctx.query;
111
+ els.hops.value = String(ctx.hops);
112
+
113
+ ctx.fn.buildWorld();
114
+ ctx.fn.paint(false);
115
+ const playback = installPlayback(ctx);
116
+ if (opts.restore) {
117
+ ctx.userMoved = true;
118
+ ctx.fn.setView(opts.restore.view, false);
119
+ playback.restore(opts.restore.playback);
120
+ if (opts.restore.inspectorOpen) {
121
+ els.inspector.classList.add("open");
122
+ els.workspace.classList.add("inspopen");
123
+ els.inspector.setAttribute("aria-hidden", "false");
124
+ ctx.fn.renderInspector();
125
+ } else {
126
+ els.inspector.classList.remove("open");
127
+ els.workspace.classList.remove("inspopen");
128
+ els.inspector.setAttribute("aria-hidden", "true");
129
+ }
130
+ } else {
131
+ // The entrance animation is for the first mount only; a live update restores and must not replay it.
132
+ document.body.classList.add("intro");
133
+ const introTimer = window.setTimeout(() => document.body.classList.remove("intro"), 2100);
134
+ // destroy() clears these timers, so an update inside the window cannot fire the old instance's callbacks.
135
+ ctx.disposers.push(() => { window.clearTimeout(introTimer); document.body.classList.remove("intro"); });
136
+ ctx.fn.fitWidth(false);
137
+ const fitTimer = window.setTimeout(() => { if (!ctx.userMoved) ctx.fn.fitWidth(false); }, 120);
138
+ ctx.disposers.push(() => window.clearTimeout(fitTimer));
139
+ }
140
+
141
+ const api: FlowStudio = {
142
+ model,
143
+ geometry: () => ctx.geom,
144
+ view: () => ({ ...ctx.view }),
145
+ setView: (v, animate) => ctx.fn.setView(v, animate),
146
+ fit: animate => { ctx.userMoved = false; ctx.fn.fit(animate); },
147
+ select: id => ctx.fn.select(id),
148
+ selected: () => ctx.selected,
149
+ mode: () => ctx.mode,
150
+ setMode: id => ctx.fn.setMode(id),
151
+ simulate: id => ctx.fn.simulate(id),
152
+ resetSimulation: () => { ctx.fn.clearSim(); ctx.fn.renderInspector(); },
153
+ play: id => playback.play(id),
154
+ pause: () => playback.pause(),
155
+ step: () => playback.step(),
156
+ seek: t => playback.seek(t),
157
+ playback: () => playback.playback(),
158
+ update: (nextModel, updateOpts) => {
159
+ const prepared = nextModel.layout === "auto" ? autoLayout(nextModel) : nextModel;
160
+ const nextIssues = validateModel(prepared);
161
+ const count = nextIssues.filter(i => i.level === "error").length;
162
+ if (count) {
163
+ els.banner.textContent = `Update failed: ${count} errors. Showing the last good graph.`;
164
+ els.banner.hidden = false;
165
+ return { ok: false, issues: nextIssues };
166
+ }
167
+ const changes = diffModels(ctx.model, prepared);
168
+ const saved: SavedState = { 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() };
169
+ api.destroy();
170
+ const replacement = mount(root, prepared, { notes: opts.notes, onSelect: opts.onSelect, restore: saved });
171
+ notifySelection(saved.selected, replacement.selected(), opts.onSelect);
172
+ // Refresh this api in place: callers keep their reference and window.FlowStudio stays this
173
+ // instance, so an update can never overwrite a different mounted engine.
174
+ Object.assign(api, replacement);
175
+ window.FlowStudio = api;
176
+ els.banner.hidden = true; els.banner.textContent = "";
177
+ if (updateOpts?.animate !== false && !ctx.reduced) {
178
+ for (const id of changes.changed) root.querySelector<HTMLElement>(`#n_${CSS.escape(id)}`)?.classList.add("updated");
179
+ for (const id of changes.added) root.querySelector<HTMLElement>(`#n_${CSS.escape(id)}`)?.classList.add("added");
180
+ }
181
+ return { ok: true, issues: nextIssues };
182
+ },
183
+ destroy: () => {
184
+ ctx.fn.clearSim();
185
+ ctx.disposers.forEach(d => d());
186
+ ctx.disposers = [];
187
+ style?.remove();
188
+ if (window.FlowStudio === api || window.FlowStudio?.model === api.model) delete window.FlowStudio;
189
+ },
190
+ };
191
+ window.FlowStudio = api;
192
+ return api;
193
+ }
@@ -0,0 +1,126 @@
1
+ // The slide-in inspector. It uses the markup and class names of the host app's own inspector
2
+ // (.insp, .insp-h, .counts .cbtn, .insp-b, .drill, .pill, .meta), so it looks native when the page lives in an app.
3
+ // It is closed until a click, and it also opens to list search matches when nothing is selected.
4
+
5
+ import type { Ctx } from "./ctx.ts";
6
+ import { esc } from "./model.ts";
7
+ import type { NodeDef, RelationDef } from "./model.ts";
8
+ import { bucketOf, effectiveLinks, groupFor, isImproved, isNew, nodeStateId, nodeVisible } from "./rules.ts";
9
+ import { matches, relationSet, relationsOf } from "./interact.ts";
10
+ import { sparklinePoints } from "./metrics.ts";
11
+
12
+ const BUCKET_COLOR = { good: "ok", neutral: "muted", gap: "warn" } as const;
13
+ const PILL = { ok: "ok", warn: "warn", crit: "warn", info: "info", accent: "info", muted: "muted" } as const;
14
+
15
+ export function install(ctx: Ctx): void {
16
+ const { els, model } = ctx;
17
+
18
+ const matchesFilter = (n: NodeDef, f: RelationDef["filter"]): boolean =>
19
+ !f || ((f.col === undefined || n.col === f.col) && (f.kind === undefined || n.kind === f.kind));
20
+
21
+ function wire(box: HTMLElement): void {
22
+ box.querySelector("#hideInsp")?.addEventListener("click", () => {
23
+ if (ctx.selected) ctx.fn.select(null);
24
+ else { ctx.dismissed = true; ctx.fn.renderInspector(); }
25
+ });
26
+ box.querySelectorAll<HTMLElement>("[data-hl]").forEach(b => b.addEventListener("click", () => {
27
+ ctx.hl = ctx.hl === b.dataset.hl ? null : (b.dataset.hl ?? null);
28
+ ctx.fn.trace(ctx.selected);
29
+ ctx.fn.renderInspector();
30
+ }));
31
+ box.querySelectorAll<HTMLElement>("[data-g]").forEach(b => b.addEventListener("click", () => { ctx.fn.select(b.dataset.g ?? null); ctx.fn.zoomToPath(); }));
32
+ box.querySelectorAll<HTMLElement>("[data-act]").forEach(b => b.addEventListener("click", () => ctx.fn.simulate(b.dataset.act ?? "")));
33
+ box.querySelectorAll<HTMLElement>("[data-toggle-group]").forEach(b => b.addEventListener("click", () => ctx.fn.toggleGroup(b.dataset.toggleGroup ?? "")));
34
+ box.querySelector("#simReset")?.addEventListener("click", () => { ctx.fn.clearSim(); ctx.fn.renderInspector(); ctx.fn.announce("Simulation reset."); });
35
+ box.querySelector("#zPath")?.addEventListener("click", () => ctx.fn.zoomToPath());
36
+ box.querySelector("#zBand")?.addEventListener("click", () => { const n = ctx.selected ? ctx.nodes.get(ctx.selected) : undefined; if (n?.lane) ctx.fn.zoomToBand(n.lane); });
37
+ }
38
+
39
+ function renderInspector(): void {
40
+ const box = els.inspector, ws = els.workspace;
41
+ const sel = ctx.selected ? ctx.nodes.get(ctx.selected) : undefined;
42
+ const head = `<div class="insp-h"><span>Inspector</span><button type="button" id="hideInsp" aria-label="Close the inspector">Close ✕</button></div>`;
43
+
44
+ if (!sel) {
45
+ const found = ctx.query && !ctx.dismissed ? model.nodes.filter(n => nodeVisible(model, ctx.mode, n, ctx.collapsed) && matches(ctx, n)) : [];
46
+ const open = found.length > 0;
47
+ box.classList.toggle("open", open);
48
+ ws.classList.toggle("inspopen", open);
49
+ box.setAttribute("aria-hidden", String(!open));
50
+ if (!open) return;
51
+ const rows = found.map(f => {
52
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, f));
53
+ const hits = (f.members ?? []).filter(m => m.toLowerCase().includes(ctx.query)).length;
54
+ 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 · ${esc(model.columns[f.col ?? 0]?.title ?? "")}</span></span></button>`;
55
+ }).join("");
56
+ box.innerHTML = head + `<div class="insp-b"><div class="insp-sec"><b>${found.length}</b> group${found.length === 1 ? "" : "s"} match “${esc(ctx.query)}”</div><div class="drill">${rows}</div></div>`;
57
+ wire(box);
58
+ return;
59
+ }
60
+
61
+ box.classList.add("open");
62
+ ws.classList.add("inspopen");
63
+ box.setAttribute("aria-hidden", "false");
64
+
65
+ const sid = nodeStateId(model, ctx.mode, sel);
66
+ const st = model.states[sid];
67
+ const bucket = bucketOf(model, sid);
68
+ const lane = model.lanes.find(l => l.id === sel.lane);
69
+ const counts = relationsOf(ctx).slice(0, 3).map(r => {
70
+ const set = relationSet(ctx, r, sel.id);
71
+ let n = 0;
72
+ for (const id of set) {
73
+ const x = ctx.nodes.get(id);
74
+ if (!x) continue;
75
+ if (r.filter) { if (matchesFilter(x, r.filter)) n += Math.max(1, x.weight ?? 1); }
76
+ else if (id !== sel.id) n += 1;
77
+ }
78
+ const cls = r.id === "up" ? "cn--up" : r.id === "down" ? "cn--down" : "";
79
+ 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>`;
80
+ }).join("");
81
+
82
+ const pills = `<span class="pill ${PILL[st?.tone ?? "muted"]}">${esc(st?.word ?? sid)}</span>`
83
+ + (isImproved(model, ctx.mode, sel) ? '<span class="pill info">gap filled</span>' : "")
84
+ + (isNew(model, ctx.mode, sel) ? '<span class="pill muted">new in this view</span>' : "");
85
+ const actions = (model.inspector?.actions ?? []).map(a => `<button type="button" class="danger" data-act="${esc(a.id)}">▶ ${esc(a.label)}</button>`).join("");
86
+ const members = sel.members ?? [];
87
+ const owner = groupFor(model, sel.id);
88
+ const foldAction = sel.kind === "group" && sel.contains?.length && ctx.collapsed.has(sel.id)
89
+ ? `<button type="button" data-toggle-group="${esc(sel.id)}">Expand group</button>`
90
+ : owner && !ctx.collapsed.has(owner.id)
91
+ ? `<button type="button" data-toggle-group="${esc(owner.id)}">Collapse into ${esc(owner.title)}</button>` : "";
92
+ const exits = sel.kind === "gateway" ? effectiveLinks(model, ctx.mode, ctx.collapsed, ctx.nodes).filter(l => l.from === sel.id).map(l => {
93
+ const target = ctx.nodes.get(l.to);
94
+ 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>`;
95
+ }).join("") : "";
96
+ const metric = sel.metrics;
97
+ const series = metric?.series ?? [];
98
+ const points = sparklinePoints(series, 300, 40);
99
+ 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>` : "";
100
+
101
+ box.innerHTML = head + `<div class="counts">${counts}</div>
102
+ <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 ?? "")} · ${esc(lane?.title ?? sel.lane)}</div><div class="pillrow">${pills}</div>${sel.subtitle ? `<p class="note">${esc(sel.subtitle)}</p>` : ""}</div>
103
+ <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>
104
+ ${metricHtml}
105
+ ${sel.kind === "gateway" ? `<div class="insp-sec"><b>Exits</b><div class="drill">${exits}</div></div>` : ""}
106
+ ${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…" 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>`}
107
+ ${foldAction ? `<div class="acts">${foldAction}</div>` : ""}
108
+ <div class="acts"><button type="button" id="zPath">Zoom to path</button><button type="button" id="zBand">Go to lane</button></div>
109
+ ${actions ? `<div class="acts">${actions}<button type="button" id="simReset"${ctx.simActive ? "" : " hidden"}>Reset</button></div>` : ""}</div>`;
110
+ wire(box);
111
+
112
+ const mq = box.querySelector<HTMLInputElement>("#mq");
113
+ const draw = () => {
114
+ const f = (mq?.value ?? "").toLowerCase();
115
+ const rows = members.filter(m => m.toLowerCase().includes(f));
116
+ const list = box.querySelector<HTMLElement>("#mlist");
117
+ 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("");
118
+ const cnt = box.querySelector<HTMLElement>("#mcount");
119
+ const weight = sel.weight ?? 0;
120
+ if (cnt) cnt.textContent = `${Math.min(14, rows.length)} of ${rows.length}${weight > members.length ? ` named (${weight} members in all)` : ""}`;
121
+ };
122
+ if (mq) { mq.addEventListener("input", draw); draw(); }
123
+ }
124
+
125
+ ctx.fn.renderInspector = renderInspector;
126
+ }
@@ -0,0 +1,253 @@
1
+ // Interaction: tracing a path, selecting, the draw-in trail, the what-if action, zooming to a path or lane.
2
+
3
+ import type { Ctx } from "./ctx.ts";
4
+ import type { NodeDef, RelationDef } from "./model.ts";
5
+ import type { Rect } from "./viewport.ts";
6
+ import { connected, relatives, withinHops, type Edge } from "./trace.ts";
7
+ import { bucketOf, canonOf, effectiveLinks, nodeStateId, nodeVisible, toggleGroupState } from "./rules.ts";
8
+ import { notifySelection } from "./selection.ts";
9
+
10
+ export function relationsOf(ctx: Ctx): RelationDef[] {
11
+ const given = ctx.model.inspector?.relations;
12
+ if (given && given.length) return given;
13
+ const last = Math.max(0, ctx.model.columns.length - 1);
14
+ return [
15
+ { id: "up", label: "Upstream", dir: "up" },
16
+ { id: "down", label: "Downstream", dir: "down" },
17
+ { id: "end", label: "At the end", dir: "down", filter: { col: last } },
18
+ ];
19
+ }
20
+
21
+ export function visibleEdges(ctx: Ctx): Edge[] {
22
+ return effectiveLinks(ctx.model, ctx.mode, ctx.collapsed, ctx.nodes).map(l => ({ from: l.from, to: l.to }));
23
+ }
24
+
25
+ export function relationSet(ctx: Ctx, rel: RelationDef, start: string): Set<string> {
26
+ const canon = (id: string) => canonOf(ctx.nodes, id);
27
+ const set = relatives(visibleEdges(ctx), canon, start, rel.dir);
28
+ if (!rel.filter) return set;
29
+ const out = new Set<string>([canon(start)]);
30
+ for (const id of set) {
31
+ const n = ctx.nodes.get(id);
32
+ if (!n) continue;
33
+ if ((rel.filter.col === undefined || n.col === rel.filter.col) && (rel.filter.kind === undefined || n.kind === rel.filter.kind)) out.add(id);
34
+ }
35
+ return out;
36
+ }
37
+
38
+ export function matches(ctx: Ctx, n: NodeDef): boolean {
39
+ const q = ctx.query;
40
+ return !!q && (n.title.toLowerCase().includes(q) || (n.members ?? []).some(m => m.toLowerCase().includes(q)));
41
+ }
42
+
43
+ export function install(ctx: Ctx): void {
44
+ const { els, model } = ctx;
45
+ const canon = (id: string) => canonOf(ctx.nodes, id);
46
+ const visibleNodes = () => model.nodes.filter(n => nodeVisible(model, ctx.mode, n, ctx.collapsed));
47
+ const inspInset = () => (els.inspector.classList.contains("open") ? 340 : 0);
48
+
49
+ function activeSet(id: string | null): Set<string> | null {
50
+ const edges = visibleEdges(ctx);
51
+ if (id) {
52
+ if (id === ctx.selected && ctx.hl) {
53
+ const rel = relationsOf(ctx).find(r => r.id === ctx.hl);
54
+ if (rel) return relationSet(ctx, rel, id);
55
+ }
56
+ return connected(edges, canon, id);
57
+ }
58
+ if (ctx.filter) {
59
+ return new Set(visibleNodes().filter(n => {
60
+ const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
61
+ return ctx.filter === "gap" ? b === "gap" : !!n.upgrade && b !== "good";
62
+ }).map(n => canon(n.id)));
63
+ }
64
+ if (ctx.query) return withinHops(edges, canon, visibleNodes().filter(n => matches(ctx, n)).map(n => n.id), ctx.hops);
65
+ return null;
66
+ }
67
+
68
+ function trace(id: string | null): void {
69
+ const set = activeSet(id);
70
+ const lit = !!id && !!set;
71
+ for (const n of model.nodes) {
72
+ const d = ctx.nodeEls.get(n.id);
73
+ if (!d) continue;
74
+ d.classList.toggle("dim", !!set && !set.has(canon(n.id)));
75
+ d.classList.toggle("lit", lit && !!set && set.has(canon(n.id)));
76
+ }
77
+ ctx.edgeEls.forEach(p => {
78
+ if (!p) return;
79
+ const inSet = !!set && set.has(canon(p.dataset.a ?? "")) && set.has(canon(p.dataset.b ?? ""));
80
+ p.classList.toggle("dim", !!set && !inSet);
81
+ p.classList.toggle("hot", lit && inSet);
82
+ });
83
+ ctx.labelEls.forEach(l => { if (l) l.classList.toggle("show", lit && !!set && set.has(canon(l.dataset.a ?? "")) && set.has(canon(l.dataset.b ?? ""))); });
84
+ }
85
+
86
+ function select(id: string | null): void {
87
+ clearSim();
88
+ const previous = ctx.selected;
89
+ ctx.selected = id ? canon(id) : null;
90
+ ctx.hl = null;
91
+ ctx.dismissed = false;
92
+ for (const n of model.nodes) ctx.nodeEls.get(n.id)?.classList.toggle("sel", !!ctx.selected && canon(n.id) === ctx.selected);
93
+ trace(ctx.selected);
94
+ ctx.fn.renderInspector();
95
+ drawTrail();
96
+ if (ctx.selected) revealNode(ctx.selected);
97
+ notifySelection(previous, ctx.selected, ctx.onSelect);
98
+ }
99
+
100
+ /** An overlay is a copy of one link drawn on top. pathLength=1 makes the draw-in independent of zoom. */
101
+ function overlay(edge: SVGPathElement, cls: string, delay: number, dur: number): SVGPathElement {
102
+ const o = edge.cloneNode(false) as SVGPathElement;
103
+ o.setAttribute("class", cls);
104
+ o.setAttribute("pathLength", "1");
105
+ o.removeAttribute("data-i");
106
+ els.layer.appendChild(o);
107
+ ctx.overlays.push(o);
108
+ if (ctx.reduced) return o;
109
+ o.style.strokeDasharray = "1 1";
110
+ o.style.strokeDashoffset = "1";
111
+ o.getBoundingClientRect();
112
+ o.style.transition = `stroke-dashoffset ${dur}s ${delay}s ease-out`;
113
+ o.style.strokeDashoffset = "0";
114
+ return o;
115
+ }
116
+ function dropOverlays(cls: string): void {
117
+ ctx.overlays.filter(o => o.classList.contains(cls)).forEach(o => o.remove());
118
+ ctx.overlays = ctx.overlays.filter(o => o.isConnected);
119
+ }
120
+ function drawTrail(): void {
121
+ dropOverlays("trail");
122
+ if (!ctx.selected || ctx.reduced) return;
123
+ const set = connected(visibleEdges(ctx), canon, ctx.selected);
124
+ const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
125
+ for (const p of ctx.edgeEls) {
126
+ if (!p || p.classList.contains("off")) continue;
127
+ const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
128
+ if (!(set.has(a) && set.has(b))) continue;
129
+ const delay = Math.min(Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0), Math.abs((ctx.nodes.get(b)?.col ?? 0) - c0)) * 0.09;
130
+ const o = overlay(p, "trail", delay, 0.55);
131
+ window.setTimeout(() => { o.style.transition = "opacity .6s"; o.style.opacity = "0"; window.setTimeout(() => o.remove(), 650); }, (delay + 0.55) * 1000 + 150);
132
+ }
133
+ }
134
+
135
+ function simulate(actionId: string): void {
136
+ clearSim();
137
+ if (!ctx.selected) return;
138
+ const action = model.inspector?.actions?.find(a => a.id === actionId);
139
+ const stale = action?.staleText ?? "potentially stale";
140
+ ctx.simActive = true;
141
+ const down = relatives(visibleEdges(ctx), canon, ctx.selected, "down");
142
+ const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
143
+ for (const p of ctx.edgeEls) {
144
+ if (!p || p.classList.contains("off")) continue;
145
+ const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
146
+ if (down.has(a) && down.has(b)) overlay(p, "ripple", Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0) * 0.11, 0.5);
147
+ }
148
+ down.forEach(id => {
149
+ if (id === ctx.selected) return;
150
+ const d = ctx.nodeEls.get(id);
151
+ if (!d) return;
152
+ const delay = ctx.reduced ? 0 : Math.abs((ctx.nodes.get(id)?.col ?? 0) - c0) * 110 + 450;
153
+ ctx.simTimers.push(window.setTimeout(() => {
154
+ d.classList.add("fail");
155
+ const sub = d.querySelector(".s");
156
+ if (sub) { d.dataset.s = sub.innerHTML; sub.textContent = stale; }
157
+ }, delay));
158
+ });
159
+ ctx.fn.announce(`Simulation: ${down.size - 1} groups downstream of ${ctx.nodes.get(ctx.selected)?.title ?? ""} would be ${stale}.`);
160
+ ctx.fn.renderInspector();
161
+ }
162
+ function clearSim(): void {
163
+ ctx.simTimers.forEach(t => clearTimeout(t));
164
+ ctx.simTimers = [];
165
+ dropOverlays("ripple");
166
+ document.querySelectorAll<HTMLElement>(".node.fail").forEach(d => {
167
+ d.classList.remove("fail");
168
+ const sub = d.querySelector(".s");
169
+ if (sub && d.dataset.s !== undefined) { sub.innerHTML = d.dataset.s; delete d.dataset.s; }
170
+ });
171
+ ctx.simActive = false;
172
+ }
173
+
174
+ /** The inspector covers the right 340 px. If the group would sit under it, glide the view left. */
175
+ function revealNode(id: string): void {
176
+ const d = ctx.nodeEls.get(id);
177
+ if (!d) return;
178
+ const r = d.getBoundingClientRect(), v = els.vp.getBoundingClientRect(), edge = v.right - 340 - 24;
179
+ if (r.right > edge) {
180
+ ctx.userMoved = true;
181
+ 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);
182
+ }
183
+ }
184
+
185
+ function rectOf(ids: string[]): Rect | null {
186
+ const ps = ids.map(i => ctx.geom.nodes[i]).filter((p): p is NonNullable<typeof p> => !!p && !!ctx.nodes.get(p.id) && nodeVisible(model, ctx.mode, ctx.nodes.get(p.id) as NodeDef, ctx.collapsed));
187
+ if (!ps.length) return null;
188
+ const x1 = Math.min(...ps.map(p => p.x)), y1 = Math.min(...ps.map(p => p.y));
189
+ const x2 = Math.max(...ps.map(p => p.x + p.w)), y2 = Math.max(...ps.map(p => p.y + p.h));
190
+ return { x: x1 - 20, y: y1 - 20, w: x2 - x1 + 40, h: y2 - y1 + 40 };
191
+ }
192
+ function zoomToPath(): void {
193
+ if (!ctx.selected) return;
194
+ const ids = [...connected(visibleEdges(ctx), canon, ctx.selected)];
195
+ ids.push(...model.nodes.filter(n => n.ref && ids.includes(n.ref)).map(n => n.id));
196
+ const r = rectOf(ids);
197
+ if (r) { ctx.userMoved = true; ctx.fn.fit(true, r, inspInset()); ctx.fn.announce(`Zoomed to the path of ${ctx.nodes.get(ctx.selected)?.title ?? ""}`); }
198
+ }
199
+ function zoomToBand(id: string): void {
200
+ const r = ctx.geom.lanes[id];
201
+ if (!r) return;
202
+ ctx.userMoved = true;
203
+ ctx.fn.fit(true, { x: r.x - 10, y: r.y - 10, w: r.w + 20, h: r.h + 20 }, inspInset());
204
+ ctx.fn.announce(`Zoomed to the lane ${model.lanes.find(l => l.id === id)?.title ?? id}`);
205
+ }
206
+
207
+ function escape(): void {
208
+ clearSim();
209
+ select(null);
210
+ ctx.filter = null;
211
+ ctx.query = "";
212
+ els.q.value = "";
213
+ els.matchinfo.textContent = "";
214
+ ctx.fn.buildChrome();
215
+ trace(null);
216
+ ctx.fn.toggleNotes(false);
217
+ }
218
+ function setMode(id: string): void {
219
+ if (ctx.mode === id) return;
220
+ document.body.classList.remove("intro");
221
+ clearSim();
222
+ ctx.mode = id;
223
+ const sel = ctx.selected && ctx.nodes.get(ctx.selected);
224
+ if (sel && !nodeVisible(model, id, sel, ctx.collapsed)) select(null);
225
+ ctx.fn.paint(true);
226
+ ctx.userMoved = false;
227
+ ctx.fn.fitWidth(true);
228
+ const added = model.nodes.filter(n => nodeVisible(model, id, n, ctx.collapsed) && !nodeVisible(model, model.views?.[0]?.id ?? "all", n, ctx.collapsed)).length;
229
+ 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.`);
230
+ }
231
+
232
+ ctx.fn.trace = trace;
233
+ ctx.fn.select = select;
234
+ ctx.fn.drawTrail = drawTrail;
235
+ ctx.fn.simulate = simulate;
236
+ ctx.fn.clearSim = clearSim;
237
+ ctx.fn.rectOf = rectOf;
238
+ ctx.fn.zoomToPath = zoomToPath;
239
+ ctx.fn.zoomToBand = zoomToBand;
240
+ ctx.fn.escape = escape;
241
+ ctx.fn.setMode = setMode;
242
+ ctx.fn.toggleGroup = (id: string) => {
243
+ const group = model.nodes.find(n => n.id === id && n.kind === "group" && n.contains?.length);
244
+ if (!group) return;
245
+ const change = toggleGroupState(model, ctx.mode, ctx.collapsed, id);
246
+ if (!change) return;
247
+ ctx.collapsed = change.collapsed;
248
+ ctx.fn.paint();
249
+ ctx.fn.select(change.selected);
250
+ ctx.fn.announce(change.expanding ? `Expanded ${group.title}.` : `Collapsed into ${group.title}.`);
251
+ };
252
+ ctx.fn.announce = (t: string) => { els.live.textContent = t; };
253
+ }