@caddis/cli 0.4.45 → 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 (191) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/skills/flow-studio/README.md +46 -7
  3. package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +80 -81
  4. package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +8 -0
  5. package/bundles/antigravity-plugin/skills/flow-studio/examples/agent-journey.json +103 -0
  6. package/bundles/antigravity-plugin/skills/flow-studio/examples/approval-flow.json +134 -0
  7. package/bundles/antigravity-plugin/skills/flow-studio/examples/ci-cd-pipeline.json +132 -0
  8. package/bundles/antigravity-plugin/skills/flow-studio/examples/incident-timeline.json +109 -0
  9. package/bundles/antigravity-plugin/skills/flow-studio/examples/order-desk.json +99 -0
  10. package/bundles/antigravity-plugin/skills/flow-studio/examples/service-map.json +133 -0
  11. package/bundles/antigravity-plugin/skills/flow-studio/examples/state-machine.json +106 -0
  12. package/bundles/antigravity-plugin/skills/flow-studio/examples/ticket-lifecycle.json +107 -0
  13. package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +27 -8
  14. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.test.tsx +82 -0
  15. package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.tsx +67 -0
  16. package/bundles/antigravity-plugin/skills/flow-studio/react/README.md +37 -0
  17. package/bundles/antigravity-plugin/skills/flow-studio/react/index.ts +3 -0
  18. package/bundles/antigravity-plugin/skills/flow-studio/react/native/FlowCanvas.tsx +58 -0
  19. package/bundles/antigravity-plugin/skills/flow-studio/react/native/Inspector.tsx +27 -0
  20. package/bundles/antigravity-plugin/skills/flow-studio/react/native/index.ts +6 -0
  21. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.css +18 -0
  22. package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.test.tsx +161 -0
  23. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useFlowGeometry.ts +22 -0
  24. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useTrace.ts +29 -0
  25. package/bundles/antigravity-plugin/skills/flow-studio/react/native/useViewport.ts +75 -0
  26. package/bundles/antigravity-plugin/skills/flow-studio/react/states.tsx +13 -0
  27. package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +169 -0
  28. package/bundles/antigravity-plugin/skills/flow-studio/references/implement-in-app.md +213 -0
  29. package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +254 -0
  30. package/bundles/antigravity-plugin/skills/flow-studio/references/notes-and-spec.md +74 -0
  31. package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +385 -49
  32. package/bundles/antigravity-plugin/skills/flow-studio/references/ux-rules.md +91 -0
  33. package/bundles/antigravity-plugin/skills/flow-studio/references/verify.md +124 -0
  34. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +54 -14
  35. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +21 -4
  36. package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +320 -28
  37. package/bundles/antigravity-plugin/skills/flow-studio/scripts/default-notes.json +67 -0
  38. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  39. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  40. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  41. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
  42. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.mjs +18 -0
  43. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  44. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
  45. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +115 -0
  46. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  47. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  48. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/docs.test.mjs +128 -0
  49. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/library.test.mjs +26 -0
  50. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  51. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  52. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  53. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  54. package/bundles/antigravity-plugin/skills/flow-studio/scripts/react-proof.mjs +92 -0
  55. package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +5 -1
  56. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.test.ts +78 -0
  57. package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.ts +132 -0
  58. package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +11 -7
  59. package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +8 -0
  60. package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +16 -0
  61. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.test.ts +31 -0
  62. package/bundles/antigravity-plugin/skills/flow-studio/src/diff.ts +34 -0
  63. package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +83 -32
  64. package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +23 -5
  65. package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +21 -8
  66. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +75 -0
  67. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +49 -11
  68. package/bundles/antigravity-plugin/skills/flow-studio/src/lib.ts +20 -0
  69. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.test.ts +57 -0
  70. package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.ts +29 -0
  71. package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +62 -2
  72. package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +101 -16
  73. package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +8 -0
  74. package/bundles/antigravity-plugin/skills/flow-studio/src/page.ts +21 -0
  75. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.test.ts +104 -0
  76. package/bundles/antigravity-plugin/skills/flow-studio/src/playback.ts +106 -0
  77. package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +193 -0
  78. package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +66 -18
  79. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +21 -1
  80. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +32 -0
  81. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +48 -1
  82. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +52 -1
  83. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.test.ts +12 -0
  84. package/bundles/antigravity-plugin/skills/flow-studio/src/selection.ts +4 -0
  85. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +7 -0
  86. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +1 -0
  87. package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +35 -2
  88. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +21 -1
  89. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +18 -0
  90. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +986 -74
  91. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +1 -39
  92. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +190 -0
  93. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  94. package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +2491 -0
  95. package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +50 -0
  96. package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +46 -7
  97. package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +80 -81
  98. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +8 -0
  99. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/agent-journey.json +103 -0
  100. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/approval-flow.json +134 -0
  101. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ci-cd-pipeline.json +132 -0
  102. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/incident-timeline.json +109 -0
  103. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/order-desk.json +99 -0
  104. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/service-map.json +133 -0
  105. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/state-machine.json +106 -0
  106. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ticket-lifecycle.json +107 -0
  107. package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +27 -8
  108. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.test.tsx +82 -0
  109. package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.tsx +67 -0
  110. package/bundles/codex/.codex/skills/frontend/flow-studio/react/README.md +37 -0
  111. package/bundles/codex/.codex/skills/frontend/flow-studio/react/index.ts +3 -0
  112. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/FlowCanvas.tsx +58 -0
  113. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/Inspector.tsx +27 -0
  114. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/index.ts +6 -0
  115. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.css +18 -0
  116. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.test.tsx +161 -0
  117. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useFlowGeometry.ts +22 -0
  118. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useTrace.ts +29 -0
  119. package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useViewport.ts +75 -0
  120. package/bundles/codex/.codex/skills/frontend/flow-studio/react/states.tsx +13 -0
  121. package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +169 -0
  122. package/bundles/codex/.codex/skills/frontend/flow-studio/references/implement-in-app.md +213 -0
  123. package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +254 -0
  124. package/bundles/codex/.codex/skills/frontend/flow-studio/references/notes-and-spec.md +74 -0
  125. package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +385 -49
  126. package/bundles/codex/.codex/skills/frontend/flow-studio/references/ux-rules.md +91 -0
  127. package/bundles/codex/.codex/skills/frontend/flow-studio/references/verify.md +124 -0
  128. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +54 -14
  129. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +21 -4
  130. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +320 -28
  131. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/default-notes.json +67 -0
  132. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
  133. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
  134. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
  135. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
  136. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.mjs +18 -0
  137. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.test.mjs +15 -0
  138. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
  139. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +115 -0
  140. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.mjs +23 -0
  141. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
  142. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/docs.test.mjs +128 -0
  143. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/library.test.mjs +26 -0
  144. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
  145. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
  146. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
  147. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
  148. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/react-proof.mjs +92 -0
  149. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +5 -1
  150. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.test.ts +78 -0
  151. package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.ts +132 -0
  152. package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +11 -7
  153. package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +8 -0
  154. package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +16 -0
  155. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.test.ts +31 -0
  156. package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.ts +34 -0
  157. package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +83 -32
  158. package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +23 -5
  159. package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +21 -8
  160. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +75 -0
  161. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +49 -11
  162. package/bundles/codex/.codex/skills/frontend/flow-studio/src/lib.ts +20 -0
  163. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.test.ts +57 -0
  164. package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.ts +29 -0
  165. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +62 -2
  166. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +101 -16
  167. package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +8 -0
  168. package/bundles/codex/.codex/skills/frontend/flow-studio/src/page.ts +21 -0
  169. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.test.ts +104 -0
  170. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.ts +106 -0
  171. package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +193 -0
  172. package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +66 -18
  173. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +21 -1
  174. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +32 -0
  175. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +48 -1
  176. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +52 -1
  177. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.test.ts +12 -0
  178. package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.ts +4 -0
  179. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +7 -0
  180. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +1 -0
  181. package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +35 -2
  182. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +21 -1
  183. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +18 -0
  184. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +986 -74
  185. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +1 -39
  186. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +190 -0
  187. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.d.mts +1 -0
  188. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +2491 -0
  189. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +50 -0
  190. package/bundles/manifest.json +2 -2
  191. package/package.json +1 -1
@@ -0,0 +1,18 @@
1
+ .flow-native { position: relative; min-height: 460px; overflow: hidden; }
2
+ .flow-native-tools { display: flex; gap: .5rem; align-items: center; padding: .5rem; }
3
+ .flow-native-cols { position: relative; height: 2.5rem; overflow: hidden; }
4
+ .flow-native-cols span { position: absolute; overflow: hidden; white-space: nowrap; }
5
+ .flow-native-viewport { position: relative; height: 440px; overflow: hidden; touch-action: none; outline-offset: -2px; }
6
+ .flow-native-world { position: absolute; transform-origin: 0 0; }
7
+ .flow-native-lane, .flow-native-node, .flow-native-edges { position: absolute; }
8
+ .flow-native-lane { border: 1px solid currentColor; opacity: .45; pointer-events: none; }
9
+ .flow-native-lane strong { display: block; padding: .3rem; }
10
+ .flow-native-node { display: flex; text-align: left; background: inherit; color: inherit; border: 1px solid currentColor; }
11
+ .flow-native-node-main { flex: 1; min-width: 0; padding: .4rem; background: transparent; color: inherit; border: 0; text-align: left; }
12
+ .flow-native-node-main[aria-pressed="true"] { outline: 2px solid currentColor; }
13
+ .flow-native-toggle { align-self: start; padding: 0 .35rem; cursor: pointer; }
14
+ .flow-native .insp { position: absolute; top: 0; right: 0; bottom: 0; width: min(320px, 90%); background: Canvas; color: CanvasText; border-left: 1px solid; overflow: auto; }
15
+ .flow-native .insp-h { display: flex; justify-content: space-between; padding: 1rem; }
16
+ .flow-native .insp-b { padding: 1rem; }
17
+ .flow-native .counts, .flow-native .drill { display: flex; gap: .5rem; flex-wrap: wrap; }
18
+ .flow-native-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
@@ -0,0 +1,161 @@
1
+ if (typeof window !== "undefined" && typeof window.PointerEvent === "undefined") {
2
+ class PointerEvent extends MouseEvent {
3
+ pointerId: number;
4
+ pointerType: string;
5
+ constructor(type: string, params: PointerEventInit = {}) {
6
+ super(type, params);
7
+ this.pointerId = params.pointerId ?? 0;
8
+ this.pointerType = params.pointerType ?? "mouse";
9
+ }
10
+ }
11
+ window.PointerEvent = PointerEvent as unknown as typeof window.PointerEvent;
12
+ }
13
+ import React from "react";
14
+ import { afterEach, describe, expect, it } from "vitest";
15
+ import { cleanup, fireEvent, render, screen } from "@testing-library/react";
16
+ import type { Model } from "../../src/model.ts";
17
+ import { useViewport } from "./useViewport.ts";
18
+ import { useFlowGeometry } from "./useFlowGeometry.ts";
19
+ import { FlowCanvas } from "./FlowCanvas.tsx";
20
+
21
+ const model: Model = {
22
+ version: 1, meta: { title: "Sample", help: "", asOf: "", source: "" }, layout: "columns-lanes",
23
+ columns: [{ id: "one", title: "One", width: 180 }, { id: "two", title: "Two", width: 180 }],
24
+ lanes: [{ id: "lane", title: "Lane" }],
25
+ states: { good: { word: "Ready", tone: "ok", border: "solid", edge: "flow", motion: "none", bucket: "good" } },
26
+ nodes: [
27
+ { id: "a", kind: "card", lane: "lane", col: 0, row: 0, state: "good", title: "Alpha" },
28
+ { id: "b", kind: "card", lane: "lane", col: 1, row: 0, state: "good", title: "Beta" },
29
+ ], links: [{ id: "ab", from: "a", to: "b", state: "good" }],
30
+ };
31
+
32
+ function ViewProbe() {
33
+ const { view, bind, fit, zoomBy, zoomAboutCenter } = useViewport({ x: 0, y: 0, w: 1000, h: 500 });
34
+ return <>
35
+ <div data-testid="pad" {...bind} />
36
+ <output data-testid="view">{JSON.stringify(view)}</output>
37
+ <button onClick={() => zoomBy(2, 100, 80)}>Zoom at point</button>
38
+ <button onClick={() => zoomAboutCenter(2)}>Zoom about center</button>
39
+ <button onClick={() => fit()}>Fit measured</button>
40
+ <button onClick={() => fit({ w: 500, h: 300 })}>Fit explicit</button>
41
+ </>;
42
+ }
43
+
44
+ const view = () => JSON.parse(screen.getByTestId("view").textContent!) as { x: number; y: number; k: number };
45
+ /** jsdom reports 0 for every box; the hook must read the sizes from the attached element. */
46
+ const measure = (element: Element, w: number, h: number) => {
47
+ Object.defineProperty(element, "clientWidth", { value: w, configurable: true });
48
+ Object.defineProperty(element, "clientHeight", { value: h, configurable: true });
49
+ };
50
+
51
+ afterEach(cleanup);
52
+
53
+ describe("native flow viewport", () => {
54
+ it("keeps the point below the cursor during zoom and fits given bounds", () => {
55
+ render(<ViewProbe />);
56
+ fireEvent.click(screen.getByText("Zoom at point"));
57
+ expect(view()).toEqual({ x: -100, y: -80, k: 2 });
58
+ fireEvent.click(screen.getByText("Fit explicit"));
59
+ expect(view().k).toBeLessThan(1);
60
+ });
61
+
62
+ it("measures the attached element when fit() gets no size", () => {
63
+ render(<ViewProbe />);
64
+ measure(screen.getByTestId("pad"), 800, 400);
65
+ fireEvent.click(screen.getByText("Fit measured"));
66
+ expect(view().k).toBeCloseTo(0.616, 5);
67
+ expect(view().x).toBeCloseTo(92, 5);
68
+ expect(view().y).toBeCloseTo(68, 5);
69
+ });
70
+
71
+ it("zooms from a native non-passive wheel listener that can block page scroll", () => {
72
+ render(<ViewProbe />);
73
+ const pad = screen.getByTestId("pad");
74
+ const wheel = new WheelEvent("wheel", { deltaY: -120, clientX: 60, clientY: 40, cancelable: true, bubbles: true });
75
+ fireEvent(pad, wheel);
76
+ expect(wheel.defaultPrevented).toBe(true);
77
+ expect(view().k).toBeGreaterThan(1);
78
+ });
79
+
80
+ it("pans with a pointer drag and stops tracking after pointer up", () => {
81
+ render(<ViewProbe />);
82
+ const pad = screen.getByTestId("pad");
83
+ // jsdom has no real pointer capture; the pan maths must not depend on it.
84
+ if (typeof pad.setPointerCapture === "function") pad.setPointerCapture = () => {};
85
+ if (typeof pad.releasePointerCapture === "function") pad.releasePointerCapture = () => {};
86
+ measure(pad, 800, 400);
87
+ fireEvent.pointerDown(pad, { button: 0, pointerId: 1, clientX: 100, clientY: 100 });
88
+ fireEvent.pointerMove(pad, { pointerId: 1, clientX: 150, clientY: 120 });
89
+ expect(view()).toEqual({ x: 50, y: 20, k: 1 });
90
+ fireEvent.pointerUp(pad, { pointerId: 1 });
91
+ fireEvent.pointerMove(pad, { pointerId: 1, clientX: 400, clientY: 300 });
92
+ expect(view()).toEqual({ x: 50, y: 20, k: 1 });
93
+ });
94
+
95
+ it("zooms with the keyboard about the measured center and fits with 0", () => {
96
+ render(<ViewProbe />);
97
+ const pad = screen.getByTestId("pad");
98
+ measure(pad, 800, 400);
99
+ fireEvent.keyDown(pad, { key: "+" });
100
+ expect(view()).toEqual({ x: -80, y: -40, k: 1.2 });
101
+ fireEvent.keyDown(pad, { key: "-" });
102
+ expect(view().k).toBeCloseTo(1, 10);
103
+ fireEvent.keyDown(pad, { key: "0" });
104
+ expect(view().k).toBeCloseTo(0.616, 5);
105
+ });
106
+
107
+ it("zoomAboutCenter keeps the middle of the viewport fixed", () => {
108
+ render(<ViewProbe />);
109
+ measure(screen.getByTestId("pad"), 800, 400);
110
+ fireEvent.click(screen.getByText("Zoom about center"));
111
+ expect(view()).toEqual({ x: -400, y: -200, k: 2 });
112
+ });
113
+
114
+ it("reuses the geometry across renders when the collapsed default is omitted", () => {
115
+ const seen: unknown[] = [];
116
+ function GeometryProbe() {
117
+ const { geometry } = useFlowGeometry(model, { x: 0, y: 0, k: 1 });
118
+ seen.push(geometry);
119
+ return null;
120
+ }
121
+ const rendered = render(<GeometryProbe />);
122
+ rendered.rerender(<GeometryProbe />);
123
+ rendered.rerender(<GeometryProbe />);
124
+ expect(seen[1]).toBe(seen[0]);
125
+ expect(seen[2]).toBe(seen[0]);
126
+ });
127
+ });
128
+
129
+ describe("native flow canvas", () => {
130
+ it("selects a node, traces its link, and closes the inspector with Escape", () => {
131
+ render(<FlowCanvas model={model} renderNode={node => <span>{node.title}</span>} />);
132
+ fireEvent.click(screen.getByRole("button", { name: "Alpha" }));
133
+ expect(screen.getByRole("dialog").textContent).toContain("Alpha");
134
+ expect(document.querySelector("#n_b button")?.getAttribute("data-connected")).toBe("true");
135
+ expect(screen.getByRole("button", { name: "0 upstream nodes" })).toBeTruthy();
136
+ expect(screen.getByRole("button", { name: "1 downstream nodes" })).toBeTruthy();
137
+ fireEvent.keyDown(document, { key: "Escape" });
138
+ expect(screen.queryByRole("dialog")).toBeNull();
139
+ });
140
+
141
+ it("hides contained nodes while their group is collapsed", () => {
142
+ const grouped: Model = { ...model, nodes: [
143
+ { id: "g", kind: "group", lane: "lane", col: 0, row: 0, state: "good", title: "Group", contains: ["a"] },
144
+ { ...model.nodes[0]!, row: 1 }, model.nodes[1]!,
145
+ ] };
146
+ render(<FlowCanvas model={grouped} renderNode={node => <span>{node.title}</span>} />);
147
+ expect(screen.queryByRole("button", { name: "Alpha" })).toBeNull();
148
+ fireEvent.click(screen.getByRole("button", { name: "Expand Group" }));
149
+ expect(screen.getByRole("button", { name: "Alpha" })).toBeTruthy();
150
+ });
151
+
152
+ it("zooms the tool buttons about the viewport center, not a fixed point", () => {
153
+ render(<FlowCanvas model={model} renderNode={node => <span>{node.title}</span>} />);
154
+ measure(document.getElementById("viewport")!, 800, 400);
155
+ fireEvent.click(screen.getByRole("button", { name: "Zoom in" }));
156
+ expect(document.getElementById("world")!.style.transform).toMatch(/translate\(-80px, -40px\) scale\(1\.2\)/);
157
+ expect(screen.getByText("120%")).toBeTruthy();
158
+ fireEvent.click(screen.getByRole("button", { name: "Zoom out" }));
159
+ expect(screen.getByText("100%")).toBeTruthy();
160
+ });
161
+ });
@@ -0,0 +1,22 @@
1
+ import { useMemo } from "react";
2
+ import { autoLayout } from "../../src/autolayout.ts";
3
+ import { computeGeometry } from "../../src/layout.ts";
4
+ import type { Model } from "../../src/model.ts";
5
+ import { defaultView, nodeVisible } from "../../src/rules.ts";
6
+ import type { View } from "../../src/viewport.ts";
7
+
8
+ // Shared default. A fresh Set() per render would change the memo identity below and recompute
9
+ // the geometry on every render. Never mutate it.
10
+ const EMPTY_COLLAPSED = new Set<string>();
11
+
12
+ /** Layout is world-space; the returned view is applied as a transform by the canvas. */
13
+ export function useFlowGeometry(model: Model, view: View, collapsed: Set<string> = EMPTY_COLLAPSED, mode = defaultView(model)) {
14
+ const laidOut = useMemo(() => model.layout === "auto" ? autoLayout(model) : model, [model]);
15
+ const geometry = useMemo(() => computeGeometry(laidOut, {
16
+ visible: id => {
17
+ const node = laidOut.nodes.find(item => item.id === id);
18
+ return !!node && nodeVisible(laidOut, mode, node, collapsed);
19
+ },
20
+ }), [laidOut, collapsed, mode]);
21
+ return { model: laidOut, geometry, view };
22
+ }
@@ -0,0 +1,29 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import type { Model } from "../../src/model.ts";
3
+ import { canonOf, defaultView, effectiveLinks, initialCollapsed, nodeVisible, toggleGroupState } from "../../src/rules.ts";
4
+ import { connected, relatives } from "../../src/trace.ts";
5
+
6
+ export function useTrace(model: Model, mode = defaultView(model)) {
7
+ const [selected, setSelected] = useState<string | null>(null);
8
+ const [hover, setHover] = useState<string | null>(null);
9
+ const [collapsed, setCollapsed] = useState<Set<string>>(() => initialCollapsed(model));
10
+ const nodes = useMemo(() => new Map(model.nodes.map(node => [node.id, node])), [model]);
11
+ const links = useMemo(() => effectiveLinks(model, mode, collapsed, nodes), [model, mode, collapsed, nodes]);
12
+ const visible = useMemo(() => new Set(model.nodes.filter(node => nodeVisible(model, mode, node, collapsed)).map(node => node.id)), [model, mode, collapsed]);
13
+ const focus = selected ?? hover;
14
+ const canonical = (id: string) => canonOf(nodes, id);
15
+ const upstream = useMemo(() => focus ? relatives(links, canonical, focus, "up") : new Set<string>(), [links, focus, nodes]);
16
+ const downstream = useMemo(() => focus ? relatives(links, canonical, focus, "down") : new Set<string>(), [links, focus, nodes]);
17
+ const connectedNodes = useMemo(() => focus ? connected(links, canonical, focus) : new Set<string>(), [links, focus, nodes]);
18
+ const toggleGroup = (id: string) => {
19
+ const next = toggleGroupState(model, mode, collapsed, id);
20
+ if (next) { setCollapsed(next.collapsed); setSelected(next.selected); }
21
+ };
22
+ useEffect(() => {
23
+ const onKey = (event: KeyboardEvent) => { if (event.key === "Escape") setSelected(null); };
24
+ document.addEventListener("keydown", onKey);
25
+ return () => document.removeEventListener("keydown", onKey);
26
+ }, []);
27
+ useEffect(() => { if (selected && !visible.has(selected)) setSelected(null); }, [visible, selected]);
28
+ return { selected, setSelected, hover, setHover, collapsed, toggleGroup, links, visible, connected: connectedNodes, upstream, downstream };
29
+ }
@@ -0,0 +1,75 @@
1
+ import { useCallback, useRef, useState, type KeyboardEventHandler, type PointerEventHandler } from "react";
2
+ import { clampPan, fitRect, zoomAt, type Rect, type Size, type View } from "../../src/viewport.ts";
3
+
4
+ export interface ViewportBind {
5
+ /** Spread onto the viewport element. The hook owns this ref: it measures the element and listens here. */
6
+ ref: (element: HTMLElement | null) => void;
7
+ onPointerDown: PointerEventHandler<HTMLElement>;
8
+ onPointerMove: PointerEventHandler<HTMLElement>;
9
+ onPointerUp: PointerEventHandler<HTMLElement>;
10
+ onPointerCancel: PointerEventHandler<HTMLElement>;
11
+ onKeyDown: KeyboardEventHandler<HTMLElement>;
12
+ tabIndex: number;
13
+ }
14
+
15
+ /** Canvas state and DOM bindings. Coordinates passed to zoomBy are local to the viewport. */
16
+ export function useViewport(bounds: Rect, initial: View = { x: 0, y: 0, k: 1 }) {
17
+ const [view, setView] = useState<View>(initial);
18
+ const viewport = useRef<HTMLElement | null>(null);
19
+ const drag = useRef<{ x: number; y: number; view: View } | null>(null);
20
+ const zoomBy = useCallback((factor: number, x: number, y: number) => {
21
+ setView(previous => zoomAt(previous, factor, x, y));
22
+ }, []);
23
+ /** Zoom by a factor about the center of the attached viewport element. */
24
+ const zoomAboutCenter = useCallback((factor: number) => {
25
+ const element = viewport.current;
26
+ zoomBy(factor, (element?.clientWidth ?? 0) / 2, (element?.clientHeight ?? 0) / 2);
27
+ }, [zoomBy]);
28
+ const fit = useCallback((size?: Size) => {
29
+ const measured = size ?? { w: viewport.current?.clientWidth ?? 0, h: viewport.current?.clientHeight ?? 0 };
30
+ setView(fitRect(bounds, measured, 24, 44));
31
+ }, [bounds]);
32
+ // React's synthetic onWheel listener is passive, so its preventDefault cannot stop the page
33
+ // scrolling. Wheel zoom must be a native listener with { passive: false } on the viewport element.
34
+ const onWheelZoom = useCallback((event: WheelEvent) => {
35
+ event.preventDefault();
36
+ const element = event.currentTarget as HTMLElement | null;
37
+ if (!element) return;
38
+ const rect = element.getBoundingClientRect();
39
+ zoomBy(Math.exp(-event.deltaY * 0.001), event.clientX - rect.left, event.clientY - rect.top);
40
+ }, [zoomBy]);
41
+ // The element ref is also what makes fit() work before the first pointer or wheel interaction.
42
+ const setViewportElement = useCallback((element: HTMLElement | null) => {
43
+ if (viewport.current === element) return;
44
+ viewport.current?.removeEventListener("wheel", onWheelZoom);
45
+ viewport.current = element;
46
+ element?.addEventListener("wheel", onWheelZoom, { passive: false });
47
+ }, [onWheelZoom]);
48
+ const stop: PointerEventHandler<HTMLElement> = event => {
49
+ if (drag.current) event.currentTarget.releasePointerCapture?.(event.pointerId);
50
+ drag.current = null;
51
+ };
52
+ const bind: ViewportBind = {
53
+ tabIndex: 0,
54
+ ref: setViewportElement,
55
+ onPointerDown: event => {
56
+ if (event.button !== 0 || (event.target as Element).closest("button,a,input")) return;
57
+ drag.current = { x: event.clientX, y: event.clientY, view };
58
+ event.currentTarget.setPointerCapture?.(event.pointerId);
59
+ },
60
+ onPointerMove: event => {
61
+ if (!drag.current) return;
62
+ const { x, y, view: start } = drag.current;
63
+ const size = { w: event.currentTarget.clientWidth, h: event.currentTarget.clientHeight };
64
+ setView(clampPan({ ...start, x: start.x + event.clientX - x, y: start.y + event.clientY - y }, bounds, size));
65
+ },
66
+ onPointerUp: stop,
67
+ onPointerCancel: stop,
68
+ onKeyDown: event => {
69
+ if (event.key === "+" || event.key === "=") { event.preventDefault(); zoomAboutCenter(1.2); }
70
+ if (event.key === "-") { event.preventDefault(); zoomAboutCenter(1 / 1.2); }
71
+ if (event.key === "0") { event.preventDefault(); fit(); }
72
+ },
73
+ };
74
+ return { view, bind, fit, zoomBy, zoomAboutCenter, setView };
75
+ }
@@ -0,0 +1,13 @@
1
+ import type { Model } from "../src/model.ts";
2
+
3
+ export function FlowLoading() {
4
+ return <p role="status">Loading flow…</p>;
5
+ }
6
+
7
+ export function FlowEmpty({ model }: { model: Model }) {
8
+ return model.nodes.length === 0 ? <p role="status">No flow items to show.</p> : null;
9
+ }
10
+
11
+ export function FlowError({ message, onRetry }: { message: string; onRetry?: () => void }) {
12
+ return <div role="alert"><p>{message}</p>{onRetry && <button type="button" onClick={onRetry}>Retry</button>}</div>;
13
+ }
@@ -0,0 +1,169 @@
1
+ # Design language
2
+
3
+ Grounded in `src/styles.css`. This document defines the visual design system, CSS custom properties, typography, spatial scales, state encodings, motion catalogue, and dark theme strategy for flow-studio.
4
+
5
+ ## 1. Design tokens
6
+
7
+ Tokens are defined on `:root` and adapted for dark mode via `@media (prefers-color-scheme: dark)` and `:root[data-theme="dark"]`.
8
+
9
+ | Token | Light value | Dark value | Description / Purpose |
10
+ |---|---|---|---|
11
+ | `--bg` | `#f6f8fa` | `#0d1117` | Canvas and page background |
12
+ | `--panel` | `#ffffff` | `#161b22` | Cards, drawer, top header, surfaces |
13
+ | `--panel2` | `#f6f8fa` | `#0d1117` | Secondary surface, subtle contrast, table header |
14
+ | `--fg` | `#1f2328` | `#e6edf3` | Primary text and foreground elements |
15
+ | `--muted` | `#59636e` | `#9198a1` | Secondary text, captions, inactive icons |
16
+ | `--edge` | `#d0d7de` | `#30363d` | Element borders, tool dividers, card outlines |
17
+ | `--rule` | `#d8dee4` | `#2b323a` | Dividers, grid separators, table borders |
18
+ | `--accent` | `#0969da` | `#4493f8` | Primary brand accent, selected state, hot trails |
19
+ | `--accent-solid` | `#0969da` | `#1f6feb` | Solid button background, active tab pill |
20
+ | `--ok` | `#1a7f37` | `#3fb950` | Healthy, passed, on-time indicators |
21
+ | `--warn` | `#9a6700` | `#d29922` | Warning, degraded, late indicators |
22
+ | `--crit` | `#cf222e` | `#f85149` | Critical failure, breach, error indicators |
23
+ | `--up` | `#0a7ea4` | `#6fcbe6` | Upstream relation count and badge color |
24
+ | `--down` | `#8250df` | `#d0a0ee` | Downstream relation count and notes marker |
25
+ | `--glow` | `rgba(9,105,218,.22)` | `rgba(68,147,248,.35)` | Accent halo for selection, hover, and canvas focus |
26
+ | `--shadow` | `0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08)` | `0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45)` | Elevated card and modal elevation shadow |
27
+ | `--grid` | `rgba(31,35,40,.035)` | `rgba(255,255,255,.04)` | Viewport grid pattern dots and lines |
28
+ | `--ok-bg` | `rgba(26,127,55,.10)` | `rgba(63,185,80,.12)` | Subtle tinted background for healthy states |
29
+ | `--warn-bg` | `rgba(154,103,0,.10)` | `rgba(210,153,34,.14)` | Subtle tinted background for warning states |
30
+ | `--crit-bg` | `rgba(207,34,46,.08)` | `rgba(248,81,73,.12)` | Subtle tinted background for critical states |
31
+ | `--info-bg` | `rgba(9,105,218,.10)` | `rgba(68,147,248,.14)` | Subtle tinted background for informational badges |
32
+ | `--mono` | `"IBM Plex Mono", ui-monospace, ...` | Same | Monospace font family for metrics, counters, and code |
33
+
34
+ ---
35
+
36
+ ## 2. Typography scale
37
+
38
+ Flow-studio uses system sans-serif (`Inter, "Segoe UI", system-ui, -apple-system, sans-serif`) for readable text and `--mono` for tabular numerals and technical metadata.
39
+
40
+ | Role | Size | Weight | Line height | Letter spacing | Font family |
41
+ |---|---|---|---|---|---|
42
+ | Page title (`h1`) | 20px | 700 | 1.2 | normal | Sans-serif |
43
+ | KPI / Tile numbers (`.tn`, `.cn`) | 20px | 600 | 1.1 | tabular-nums | Sans-serif |
44
+ | Inspector node title (`.nname`) | 15px | 600 | 1.25 | normal | Sans-serif |
45
+ | Drawer heading (`#drawer h2`) | 15px | 700 | 1.3 | normal | Sans-serif |
46
+ | Body / Default | 14px | 400 | 1.45 | normal | Sans-serif |
47
+ | Card title (`.node .t`) | 13px | 600 | 1.25 | normal | Sans-serif |
48
+ | Lane label (`.bandlabel b`) | 13px | 700 | 1.2 | normal | Sans-serif |
49
+ | Card subtitle / state (`.node .s`) | 12px | 400 | 1.3 | normal | Sans-serif |
50
+ | Help text / Footer | 12px | 400 | 1.4 | normal | Sans-serif |
51
+ | Column header (`#cols .hd`) | 10.5px | 700 | 1.2 | 0.08em (caps) | Sans-serif |
52
+ | Phase pill (`#cols .ph`) | 10px | 700 | 1.2 | 0.14em (caps) | Sans-serif |
53
+ | Evidence / Badges (`.ev`, `.pill`) | 9.5px to 10.5px | 700 | 1.2 | 0.03em (caps) | Sans-serif |
54
+ | Metric badges & code (`.mono`, `code`) | 11px to 11.5px | 500 | 1.2 | normal | Monospace |
55
+
56
+ ---
57
+
58
+ ## 3. Spacing scale
59
+
60
+ Layout elements follow a strict 4px grid rhythm:
61
+
62
+ | Scale | Value | Typical usage |
63
+ |---|---|---|
64
+ | 2xs | 2px to 4px | Inline icon gaps, badge vertical padding, bottom border highlights |
65
+ | xs | 4px to 7px | Button vertical padding (`4px 11px`), card vertical padding (`5px 10px 7px`), playbar gaps (`7px`) |
66
+ | sm | 8px to 10px | Card horizontal padding (`10px`), toolbar item gaps, summary grid gaps |
67
+ | md | 12px to 16px | Page padding (`12px 14px 20px`), header gaps, inspector body padding |
68
+ | lg | 20px to 28px | Section margins, top header padding, grid line pitch (28px canvas grid) |
69
+ | xl | 32px to 44px | Column header heights (44px), tool button dimensions (34px x 32px) |
70
+
71
+ ---
72
+
73
+ ## 4. Border radius and shadow tiers
74
+
75
+ ### Border radius
76
+ - **Sharp (0px):** Segmented buttons inside groups, summary tiles inside strips, tools.
77
+ - **Small (6px):** Buttons, search inputs, dropdowns, segmented group wrappers.
78
+ - **Medium (8px):** Node cards (`.node`).
79
+ - **Pill (9px to 10px):** Edge labels (`.elabel`), lane bands (`.band` radius 10px).
80
+ - **Large (12px):** Workspace container (`.workspace`).
81
+ - **Circular (50%):** Markers, status dots, tokens, and avatar chips.
82
+
83
+ ### Shadow tiers
84
+ - **Subtle border shadow (`var(--shadow)`):** `0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08)` (Light) / `0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45)` (Dark).
85
+ - **Interactive hover:** `box-shadow: 0 0 0 3px var(--glow), var(--shadow)`.
86
+ - **Slide-in drawer elevation:** `box-shadow: -14px 0 34px rgba(0,0,0,.16), -1px 0 0 var(--glow)`.
87
+ - **Marker pulse halo:** `box-shadow: 0 0 0 3px rgba(130,80,223,.3)`.
88
+
89
+ ---
90
+
91
+ ## 5. Glow tiers
92
+
93
+ Glows communicate focus, connection, and selection:
94
+ 1. **Normal:** `box-shadow: 0 0 14px -4px color-mix(in srgb, <tone> 55%, transparent)` on healthy or highlighted cards.
95
+ 2. **Hover:** `box-shadow: 0 0 0 3px var(--glow)` with border color change to `--accent`.
96
+ 3. **Selected / Lit:** `box-shadow: 0 0 0 2px var(--glow), 0 0 18px var(--glow)` plus animated ripple or trail.
97
+
98
+ ---
99
+
100
+ ## 6. State encoding table
101
+
102
+ The engine generates CSS from 12 canonical domain states defined in `src/stateCss.ts`:
103
+
104
+ | State ID | Tone | Border Style | Edge Style | Motion | Bucket | Semantic Meaning |
105
+ |---|---|---|---|---|---|---|
106
+ | `ok` | `ok` | `solid` | `flow` | `flow` | `good` | Fully healthy, active, operational |
107
+ | `healthy` | `ok` | `solid` | `flow` | `flow` | `good` | System running within normal operating SLAs |
108
+ | `planned` | `muted` | `dashed` | `dash` | `flow-slow` | `neutral` | Future work or planned integration |
109
+ | `in-progress` | `accent` | `solid` | `flow` | `flow` | `neutral` | Actively processing or undergoing update |
110
+ | `waiting` | `warn` | `dashed` | `dash-short` | `breathe` | `neutral` | Blocked on upstream dependency or approval |
111
+ | `late` | `warn` | `solid` | `flow` | `flow` | `neutral` | Running behind schedule but executing |
112
+ | `degraded` | `warn` | `dashed` | `dash` | `breathe` | `neutral` | Performance or capacity reduced |
113
+ | `failed` | `crit` | `solid` | `flow` | `flow` | `gap` | Execution halted due to fatal defect |
114
+ | `breached` | `crit` | `solid` | `flow` | `flow` | `gap` | SLA threshold crossed |
115
+ | `blocked` | `crit` | `dashed` | `dot` | `none` | `gap` | Cannot proceed due to hard impediment |
116
+ | `unknown` | `warn` | `dotted` | `dot` | `breathe` | `gap` | No telemetry or unverified source |
117
+ | `skipped` | `muted` | `dotted` | `dot` | `none` | `neutral` | Deliberately omitted execution step |
118
+
119
+ ---
120
+
121
+ ## 7. Motion catalogue
122
+
123
+ `src/styles.css` defines 12 `@keyframes`; the count treats the three flow dash variants (`flow`, `flowd`, `flowc`) separately. Motion is never decorative; every animation carries status information.
124
+
125
+ | Animation name | Duration | Easing | Meaning / State Carried |
126
+ |---|---|---|---|
127
+ | `beat` | 2.6s | ease (default), infinite | Live indicator pulse on the header brand dot. Indicates the engine session is active. |
128
+ | `fadein` | 0.6s / 1s (0.5s delay) | ease (default) | Lane bands and the edge layer fade in during initial canvas load. |
129
+ | `rise` | 0.5s / 0.55s / 1.2s | ease (default) / ease-out | Cards, tiles and summary cells enter with subtle upward movement (`translateY(8px)` to `none`); `1.2s` marks a node added by a live update. |
130
+ | `flow` | 3s or 7s | linear infinite | Dash-offset marching on links whose state has `edge: "flow"` (`3s` with `motion: "flow"`, `7s` with `motion: "flow-slow"`). Indicates active data transit. |
131
+ | `flowd` | 3s or 7s | linear infinite | Same marching for `edge: "dash"` links. |
132
+ | `flowc` | 3s or 7s | linear infinite | Same marching for `edge: "dash-short"` links. |
133
+ | `flowhot` | 0.9s | linear infinite | Faster marching on highlighted (hot) edges: search trails, relations, playback. |
134
+ | `selpulse` | 2.4s / 1.8s | ease-out infinite | Expanding glowing ring around a selected card (`2.4s`) or the active playback node (`1.8s`). |
135
+ | `breathe` | 3.2s | ease-in-out infinite | Soft pulsing box-shadow on warning or waiting nodes (`color-mix` with `--warn` or the state tone). |
136
+ | `failflash` | 0.9s | ease-out 1 | Red box-shadow burst indicating SLA breach or what-if blast-radius failure. |
137
+ | `newring` | 1.2s / 1.8s | ease-out 1 | Accent ring on a node changed by a live update (`1.2s`) or newly appeared in a view (`1.8s`). |
138
+ | `ring` | - | - | Defined but referenced by no rule today; reserved. |
139
+
140
+ ---
141
+
142
+ ## 8. Reduced motion fallback rules
143
+
144
+ Accessibility rules respect user OS preferences:
145
+
146
+ ```css
147
+ @media (prefers-reduced-motion: reduce) {
148
+ *, *::before, *::after {
149
+ animation: none !important;
150
+ transition: none !important;
151
+ }
152
+ path.edge.hot {
153
+ stroke-dasharray: none;
154
+ }
155
+ }
156
+ ```
157
+
158
+ - When reduced motion is active, token animations are immediate without transitions.
159
+ - Selection rings remain solid outlines without pulsating box-shadows.
160
+ - Links show solid or static dashed strokes without moving dashes.
161
+
162
+ ---
163
+
164
+ ## 9. Dark theme strategy
165
+
166
+ 1. The page listens to system preference: `@media (prefers-color-scheme: dark)` applies dark tokens to `:root:not([data-theme="light"])`.
167
+ 2. Manual override is supported via the `data-theme="dark"` attribute set on `<html>` or `:root`.
168
+ 3. The theme toggle button switches between light and dark without page reload.
169
+ 4. Colors do not invert naively; backgrounds use `#0d1117`, card panels use `#161b22`, borders use `#30363d`, and semantic colors (ok, warn, crit) use high-contrast luminance tuned for dark backgrounds.