@caddis/cli 0.4.45 → 0.4.47
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.
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/README.md +46 -7
- package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +81 -81
- package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +8 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/agent-journey.json +103 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/approval-flow.json +134 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/ci-cd-pipeline.json +132 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/incident-timeline.json +109 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/order-desk.json +99 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/service-map.json +133 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/state-machine.json +106 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/ticket-lifecycle.json +107 -0
- package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +27 -8
- package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.test.tsx +82 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/FlowStudioView.tsx +67 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/README.md +37 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/index.ts +3 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/FlowCanvas.tsx +58 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/Inspector.tsx +27 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/index.ts +6 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.css +18 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/native.test.tsx +161 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useFlowGeometry.ts +22 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useTrace.ts +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/native/useViewport.ts +75 -0
- package/bundles/antigravity-plugin/skills/flow-studio/react/states.tsx +13 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +177 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/implement-in-app.md +213 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +254 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/notes-and-spec.md +74 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +385 -49
- package/bundles/antigravity-plugin/skills/flow-studio/references/ux-rules.md +91 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/verify.md +124 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +54 -14
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +21 -4
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +320 -28
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/default-notes.json +67 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.mjs +18 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-args.test.mjs +15 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +115 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.mjs +23 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/docs.test.mjs +128 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/library.test.mjs +26 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/styles.test.mjs +37 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/react-proof.mjs +92 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +5 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.test.ts +78 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/autolayout.ts +132 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +11 -7
- package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +8 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +16 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/diff.test.ts +31 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/diff.ts +34 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +83 -32
- package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +23 -5
- package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +21 -8
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +75 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +49 -11
- package/bundles/antigravity-plugin/skills/flow-studio/src/lib.ts +20 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.test.ts +57 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/metrics.ts +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +62 -2
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +101 -16
- package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +8 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/page.ts +21 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playback.test.ts +104 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playback.ts +106 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +193 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +66 -18
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +21 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +32 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +48 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +52 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/selection.test.ts +12 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/selection.ts +4 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +7 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +1 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +44 -10
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +21 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +18 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +995 -82
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +1 -39
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +191 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.d.mts +1 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +2492 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +50 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +46 -7
- package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +81 -81
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +8 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/agent-journey.json +103 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/approval-flow.json +134 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ci-cd-pipeline.json +132 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/incident-timeline.json +109 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/order-desk.json +99 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/service-map.json +133 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/state-machine.json +106 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/ticket-lifecycle.json +107 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +27 -8
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.test.tsx +82 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/FlowStudioView.tsx +67 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/README.md +37 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/index.ts +3 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/FlowCanvas.tsx +58 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/Inspector.tsx +27 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/index.ts +6 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.css +18 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/native.test.tsx +161 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useFlowGeometry.ts +22 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useTrace.ts +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/native/useViewport.ts +75 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/react/states.tsx +13 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +177 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/implement-in-app.md +213 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +254 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/notes-and-spec.md +74 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +385 -49
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/ux-rules.md +91 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/verify.md +124 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +54 -14
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +21 -4
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +320 -28
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/default-notes.json +67 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.mjs +5 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry-guard.test.mjs +14 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/auto-entry.test.mjs +31 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +30 -2
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.mjs +18 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-args.test.mjs +15 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +64 -4
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +115 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.mjs +23 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/demo-pack.test.mjs +34 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/docs.test.mjs +128 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/library.test.mjs +26 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.mjs +12 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-cleanup.test.mjs +45 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.mjs +3 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/react-proof-page.test.mjs +9 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/styles.test.mjs +37 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/react-proof.mjs +92 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +5 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.test.ts +78 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/autolayout.ts +132 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +11 -7
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +8 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +16 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.test.ts +31 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/diff.ts +34 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +83 -32
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +23 -5
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +21 -8
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +75 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +49 -11
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/lib.ts +20 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.test.ts +57 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/metrics.ts +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +62 -2
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +101 -16
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +8 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/page.ts +21 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.test.ts +104 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playback.ts +106 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +193 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +66 -18
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +21 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +32 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +48 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +52 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.test.ts +12 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/selection.ts +4 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +7 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +1 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +44 -10
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +21 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +18 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +995 -82
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +1 -39
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +191 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.d.mts +1 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +2492 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +50 -0
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<style>:root {
|
|
16
16
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
17
17
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
18
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
|
|
18
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:1.1px; --edge-w-hot:1.5px; --edge-w-play:1.6px; --trail-w:1.8px; --ripple-w:2px;
|
|
19
19
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
20
20
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
21
21
|
}
|
|
@@ -23,14 +23,14 @@
|
|
|
23
23
|
:root:not([data-theme="light"]) {
|
|
24
24
|
--bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
|
|
25
25
|
--accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
|
|
26
|
-
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
|
|
26
|
+
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
|
|
27
27
|
--warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
:root[data-theme="dark"] {
|
|
31
31
|
--bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
|
|
32
32
|
--accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
|
|
33
|
-
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
|
|
33
|
+
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04); --dot:rgba(255,255,255,.16);
|
|
34
34
|
--warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
|
|
35
35
|
}
|
|
36
36
|
* { box-sizing:border-box; }
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
.workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
|
|
73
73
|
.canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
|
|
74
74
|
#viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
|
|
75
|
-
background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%),
|
|
75
|
+
background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), radial-gradient(circle, var(--dot) var(--dot-size), transparent calc(var(--dot-size) + .5px)) 0 0 / var(--dot-gap) var(--dot-gap), var(--bg); }
|
|
76
76
|
#viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
|
|
77
77
|
#world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
|
|
78
78
|
#viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
|
|
@@ -84,15 +84,21 @@
|
|
|
84
84
|
.bandlabel b { font-size:13px; } .bandlabel .bd { font-size:10.5px; font-weight:700; padding:0 6px; border:1px solid currentColor; color:var(--muted); letter-spacing:.03em; text-transform:uppercase; }
|
|
85
85
|
.bandlabel .bd.t-ok { color:var(--ok); } .bandlabel .bd.t-warn { color:var(--warn); } .bandlabel .bn { font-size:11.5px; color:var(--muted); }
|
|
86
86
|
.node { position:absolute; background:var(--panel); border:1px solid var(--edge); border-radius:8px; padding:5px 10px 7px; box-shadow:var(--shadow); transition:opacity .35s, transform .35s, box-shadow .2s, border-color .2s; cursor:pointer; z-index:2; }
|
|
87
|
+
.node.gateway { border:0; background:transparent; box-shadow:none; padding:0; border-radius:0; display:flex; align-items:center; justify-content:center; }
|
|
88
|
+
.node.gateway svg.gw { position:absolute; inset:0; width:100%; height:100%; overflow:visible; }
|
|
89
|
+
.node.gateway polygon { stroke-width:2; vector-effect:non-scaling-stroke; }
|
|
90
|
+
.node.gateway .gw-title { position:relative; width:58%; text-align:center; font-size:11px; font-weight:600; line-height:1.2; color:var(--fg); overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow-wrap:anywhere; }
|
|
91
|
+
.node.gateway:hover, .node.gateway.lit, .node.gateway.sel { filter:drop-shadow(0 0 7px var(--accent)); }
|
|
87
92
|
.node.wide { overflow:hidden; }
|
|
88
93
|
.node:hover { border-color:var(--accent); box-shadow:0 0 0 3px var(--glow), var(--shadow); }
|
|
89
94
|
.node .t { display:flex; gap:4px; align-items:flex-start; font-weight:600; line-height:1.25; font-size:13px; }
|
|
90
95
|
.node .t .name { overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
|
96
|
+
.node .fold { flex:none; color:var(--accent); font-size:11px; }
|
|
91
97
|
.node .s { color:var(--muted); font-size:12px; line-height:1.3; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
92
98
|
.ev { font-size:10.5px; font-weight:700; letter-spacing:.03em; }
|
|
93
99
|
.node.wide .t { font-weight:500; } .node.wide .s { white-space:normal; }
|
|
94
|
-
.node.ref { padding:4px 9px; border-style:dashed; box-shadow:none; background:color-mix(in srgb, var(--panel) 70%, transparent); } .node.ref .t { font-size:12px; } .node.ref .s { font-size:11px; }
|
|
95
|
-
.node.sel { outline:2px solid var(--accent); outline-offset:2px; } .node.dim { opacity:.28; } .node.off { opacity:0; transform:translateY(8px); pointer-events:none; }
|
|
100
|
+
.node.ref { padding:4px 9px; border-style:dashed; box-shadow:none; background:color-mix(in srgb, var(--panel) 70%, transparent); } .node.ref .t { font-size:12px; } .node.ref .t .name { display:block; min-width:0; white-space:nowrap; text-overflow:ellipsis; } .node.ref .s { font-size:11px; }
|
|
101
|
+
.node.sel { outline:2px solid var(--accent); outline-offset:2px; } .node.dim { opacity:.28; } .node.off { opacity:0; visibility:hidden; transform:translateY(8px); pointer-events:none; }
|
|
96
102
|
.node .bar { position:absolute; left:0; right:0; bottom:0; display:flex; height:4px; overflow:hidden; border-radius:0 0 7px 7px; background:var(--panel2); } .node .bar i { display:block; height:100%; } .bar .good { background:var(--ok); } .bar .neutral { background:var(--muted); } .bar .gap { background:var(--warn); }
|
|
97
103
|
.newdot { display:none; width:7px; height:7px; border-radius:50%; background:var(--accent); margin:5px 4px 0 0; flex:none; } body.alt .node.isnew .newdot { display:inline-block; }
|
|
98
104
|
body.alt .node.isnew.pulse { animation:newring 1.8s ease-out 1; }
|
|
@@ -100,8 +106,8 @@
|
|
|
100
106
|
.cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
|
|
101
107
|
.node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
|
|
102
108
|
#viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
|
|
103
|
-
path.edge { fill:none; stroke-width:
|
|
104
|
-
path.edge.hot { stroke:var(--accent); stroke-width:
|
|
109
|
+
path.edge { fill:none; stroke-width:var(--edge-w); transition:opacity .35s; vector-effect:non-scaling-stroke; }
|
|
110
|
+
path.edge.hot { stroke:var(--accent); stroke-width:var(--edge-w-hot); opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 3px var(--accent)); }
|
|
105
111
|
path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
|
|
106
112
|
.elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
|
|
107
113
|
#cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
|
|
@@ -154,8 +160,8 @@
|
|
|
154
160
|
.node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
|
|
155
161
|
.node.sel { animation:selpulse 2.4s ease-out infinite; }
|
|
156
162
|
.node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
|
|
157
|
-
path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(
|
|
158
|
-
path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(
|
|
163
|
+
path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(var(--trail-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--accent)); opacity:.95; }
|
|
164
|
+
path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(var(--ripple-w) / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 4px var(--crit)); }
|
|
159
165
|
.intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
|
|
160
166
|
.intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
|
|
161
167
|
#viewport:focus-visible { outline-offset:-2px; }
|
|
@@ -169,6 +175,34 @@
|
|
|
169
175
|
@keyframes beat { 0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 60%, transparent);} 70%,100%{box-shadow:0 0 0 9px transparent;} }
|
|
170
176
|
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } path.edge.hot { stroke-dasharray:none; } }
|
|
171
177
|
@media (max-width: 1100px) { .summary { grid-template-columns:1fr; } .workspace { height:auto; } .workspace .canvaswrap { height:75vh; } .insp { width:min(340px, 92%); } #drawer { width:100%; } .layerstrip { grid-template-columns:repeat(3,1fr) !important; } }
|
|
178
|
+
#banner:not([hidden]) { margin:0 14px 10px; padding:8px 12px; color:var(--crit); background:var(--crit-bg); border:1px solid var(--crit); font-size:12px; }
|
|
179
|
+
#playbar:not([hidden]) { display:flex; flex-wrap:wrap; align-items:center; gap:7px; padding:7px 12px; border-top:1px solid var(--rule); background:var(--panel); font-size:12px; }
|
|
180
|
+
#playbar select, #playbar button { font-size:11px; }
|
|
181
|
+
#pbScrub { flex:1 1 140px; min-width:90px; }
|
|
182
|
+
#pbCaption { flex:1 1 100%; color:var(--fg); }
|
|
183
|
+
#pbCounter { font:11px var(--mono); color:var(--fg); }
|
|
184
|
+
#pbCounter.warn { color:var(--warn); } #pbCounter.breach { color:var(--crit); font-weight:700; }
|
|
185
|
+
#pbChoices { display:flex; gap:5px; flex-wrap:wrap; } #pbChoices:empty { display:none; }
|
|
186
|
+
#pbChoices .suggested { border-color:var(--accent); box-shadow:inset 0 -2px 0 var(--accent); }
|
|
187
|
+
circle.token { fill:var(--accent); stroke:var(--panel); stroke-width:2; filter:drop-shadow(0 0 5px var(--accent)); pointer-events:none; }
|
|
188
|
+
path.edge.playing { stroke:var(--accent); stroke-width:var(--edge-w-play); filter:drop-shadow(0 0 4px var(--accent)); opacity:1; }
|
|
189
|
+
path.edge.visited { stroke:var(--accent); opacity:.7; }
|
|
190
|
+
.node.playing { outline:2px solid var(--accent); outline-offset:2px; }
|
|
191
|
+
/* The pulse runs only while the player really plays; a paused step keeps the plain outline. */
|
|
192
|
+
body.playing .node.playing { animation:selpulse 1.8s ease-out infinite; }
|
|
193
|
+
.node.visited::after { content:"✓"; position:absolute; right:3px; bottom:1px; color:var(--accent); font-size:11px; }
|
|
194
|
+
.node.updated { animation:newring 1.2s ease-out 1; }
|
|
195
|
+
.node.added { animation:rise 1.2s ease-out 1; }
|
|
196
|
+
.node .t .name { min-width:0; flex:1 1 auto; }
|
|
197
|
+
.node .metric-badge { flex:none; max-width:72px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; position:relative; padding-bottom:3px; text-align:right; font:11px var(--mono); color:var(--accent); }
|
|
198
|
+
.node .metric-badge i { display:block; height:2px; position:absolute; bottom:0; left:0; background:var(--accent); }
|
|
199
|
+
.node .s.has-spark { padding-right:60px; }
|
|
200
|
+
.node .spark { position:absolute; right:6px; bottom:4px; overflow:visible; }
|
|
201
|
+
.node.gateway .metric-badge { position:absolute; right:9px; top:18px; }
|
|
202
|
+
.node.gateway:has(.metric-badge) .gw-title, .node.gateway.has-metrics .gw-title { width:45%; margin-right:42px; }
|
|
203
|
+
.node .spark polyline, .metrics polyline { fill:none; stroke:var(--accent); stroke-width:1.5; }
|
|
204
|
+
.metrics svg { max-width:100%; margin-top:8px; } .metric-range { display:flex; justify-content:space-between; width:300px; max-width:100%; font:10px var(--mono); color:var(--muted); }
|
|
205
|
+
@media (forced-colors: active) { #viewport { background: Canvas; } }
|
|
172
206
|
</style>
|
|
173
207
|
</head>
|
|
174
208
|
<body>
|
|
@@ -181,23 +215,34 @@
|
|
|
181
215
|
<main>
|
|
182
216
|
<div class="titlerow"><h1 id="title"></h1><span class="readline" id="readat"></span></div>
|
|
183
217
|
<p class="help" id="help"></p>
|
|
218
|
+
<div id="banner" role="alert" hidden></div>
|
|
184
219
|
|
|
185
220
|
<section class="summary"><div class="layerstrip" id="layerstrip" aria-label="Completeness by column"></div><div class="tiles" id="kpis" aria-label="Summary"></div></section>
|
|
186
221
|
|
|
187
222
|
<div class="toolbar">
|
|
188
223
|
<div class="seggrp" role="tablist" aria-label="View" id="modeSwitch"></div>
|
|
189
224
|
<input id="q" type="search" placeholder="Find a group or member… ( / )" aria-label="Find a group or member">
|
|
225
|
+
<select id="hops" aria-label="Search radius"><option value="0">This group</option><option value="1">+1 hop</option><option value="2">+2 hops</option><option value="3">+3 hops</option></select>
|
|
190
226
|
<span id="matchinfo" class="note" style="margin:0"></span>
|
|
191
227
|
</div>
|
|
192
228
|
|
|
193
229
|
<div class="workspace" id="workspace">
|
|
194
230
|
<section class="canvaswrap">
|
|
195
231
|
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
196
|
-
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="
|
|
232
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="auto"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
197
233
|
<div id="cols" aria-hidden="true"></div>
|
|
198
234
|
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
199
235
|
<div id="zoomLabel" aria-hidden="true">100%</div>
|
|
200
236
|
</div>
|
|
237
|
+
<div id="playbar" hidden aria-label="Scenario playback">
|
|
238
|
+
<select id="pbScenario" aria-label="Scenario"></select>
|
|
239
|
+
<button id="pbPlay" type="button" aria-label="Play">▶</button>
|
|
240
|
+
<button id="pbStep" type="button" aria-label="Step">Step</button>
|
|
241
|
+
<button id="pbReset" type="button" aria-label="Reset">Reset</button>
|
|
242
|
+
<select id="pbSpeed" aria-label="Speed"><option value="0.5">0.5x</option><option value="1" selected>1x</option><option value="2">2x</option></select>
|
|
243
|
+
<input id="pbScrub" type="range" min="0" max="1000" value="0" aria-label="Playback position" aria-valuetext="Step 1 of 1">
|
|
244
|
+
<span id="pbCaption"></span><span id="pbCounter"></span><span id="pbChoices"></span>
|
|
245
|
+
</div>
|
|
201
246
|
<div class="keyline" id="legend" aria-label="Legend"></div>
|
|
202
247
|
<aside class="insp" id="inspector" aria-label="Inspector" aria-hidden="true"></aside>
|
|
203
248
|
</section>
|
|
@@ -211,11 +256,13 @@
|
|
|
211
256
|
<footer id="footer"></footer>
|
|
212
257
|
<aside id="drawer" aria-label="Implementation notes"><h2>Implementation notes</h2><p class="note">Numbered markers on the page match these cards. Hover a card to find its place; click a marker to jump to its card.</p><div id="notelist"></div></aside>
|
|
213
258
|
|
|
259
|
+
|
|
214
260
|
<script type="application/json" id="flow-model">/*__MODEL__*/{}</script>
|
|
215
261
|
<script type="application/json" id="flow-notes">/*__NOTES__*/[]</script>
|
|
216
262
|
<script>"use strict";
|
|
217
263
|
(() => {
|
|
218
264
|
// src/model.ts
|
|
265
|
+
var NODE_HEIGHT = { card: 54, group: 54, gateway: 64, placeholder: 54, chip: 40, start: 40, end: 40, event: 40, note: 40 };
|
|
219
266
|
var TONES = ["ok", "warn", "crit", "info", "muted", "accent"];
|
|
220
267
|
var BORDERS = ["solid", "dashed", "dotted"];
|
|
221
268
|
var EDGES = ["flow", "dash", "dash-short", "dot"];
|
|
@@ -234,9 +281,9 @@
|
|
|
234
281
|
}
|
|
235
282
|
const m = input;
|
|
236
283
|
if (m.version !== 1) add("error", "model-version", "version", "version must be 1.");
|
|
237
|
-
if (
|
|
238
|
-
if (!Array.isArray(m.columns) || m.columns.length === 0) add("error", "columns-missing", "columns", "
|
|
239
|
-
if (!Array.isArray(m.lanes) || m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
284
|
+
if (!["columns-lanes", "free", "auto"].includes(m.layout)) add("error", "layout-unsupported", "layout", `layout ${String(m.layout)} is not supported: use "columns-lanes", "free", or "auto".`);
|
|
285
|
+
if (!Array.isArray(m.columns) || m.layout === "columns-lanes" && m.columns.length === 0) add("error", "columns-missing", "columns", "Columns are required for columns-lanes layout.");
|
|
286
|
+
if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
240
287
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
241
288
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
242
289
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
@@ -266,15 +313,36 @@
|
|
|
266
313
|
const w = `nodes[${n.id}]`;
|
|
267
314
|
if (nodeIds.has(n.id)) add("error", "node-duplicate", w, `Duplicate node id ${n.id}.`);
|
|
268
315
|
nodeIds.add(n.id);
|
|
269
|
-
if (!laneIds.has(n.lane)) add("error", "lane-undefined", w, `Lane ${n.lane} is not defined.`);
|
|
316
|
+
if ((m.layout !== "auto" || n.lane !== void 0) && !laneIds.has(n.lane ?? "")) add("error", "lane-undefined", w, `Lane ${n.lane} is not defined.`);
|
|
270
317
|
for (const s of [n.state, ...Object.values(n.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
|
|
271
|
-
if (typeof n.row !== "number" || !Number.isFinite(n.row)) add("error", "row-invalid", w, `Node ${n.id} needs a finite number for row (it sets the vertical position).`);
|
|
318
|
+
if (m.layout === "columns-lanes" && (typeof n.row !== "number" || !Number.isFinite(n.row))) add("error", "row-invalid", w, `Node ${n.id} needs a finite number for row (it sets the vertical position).`);
|
|
272
319
|
const span = n.colSpan ?? 1;
|
|
273
|
-
if (!Number.isInteger(n.col) || n.col < 0 || n.col + span > ncols) add("error", "column-range", w, `Column ${n.col} with span ${span} is outside 0..${ncols - 1}.`);
|
|
320
|
+
if (m.layout === "columns-lanes" && (!Number.isInteger(n.col) || (n.col ?? -1) < 0 || (n.col ?? 0) + span > ncols)) add("error", "column-range", w, `Column ${n.col} with span ${span} is outside 0..${ncols - 1}.`);
|
|
321
|
+
if (m.layout === "free") {
|
|
322
|
+
if (!Number.isFinite(n.x) || !Number.isFinite(n.y)) add("error", "position-invalid", w, `Node ${n.id} needs finite x and y.`);
|
|
323
|
+
if (n.w !== void 0 && (!Number.isFinite(n.w) || n.w <= 0) || n.h !== void 0 && (!Number.isFinite(n.h) || n.h <= 0)) add("error", "size-invalid", w, `Node ${n.id} needs positive finite width and height.`);
|
|
324
|
+
}
|
|
325
|
+
const metric = n.metrics;
|
|
326
|
+
const invalidSeries = metric?.series !== void 0 && (!Array.isArray(metric.series) || metric.series.length > 60 || metric.series.some((v) => !Number.isFinite(v)));
|
|
327
|
+
if (metric !== void 0 && (!metric || !Number.isFinite(metric.value) || metric.max != null && !Number.isFinite(metric.max) || invalidSeries)) add("error", "metric-invalid", w, "Metrics need finite values and at most 60 finite series points.");
|
|
328
|
+
if (n.sla !== void 0 && (!Number.isFinite(n.sla) || n.sla <= 0)) add("error", "sla-invalid", w, "SLA must be a positive finite number of seconds.");
|
|
274
329
|
}
|
|
275
330
|
for (const n of nodes) {
|
|
276
331
|
if (n.kind === "chip" && (!n.ref || !nodeIds.has(n.ref))) add("error", "chip-ref", `nodes[${n.id}]`, `A chip must point at a real node (ref ${n.ref}).`);
|
|
277
332
|
}
|
|
333
|
+
const owner = /* @__PURE__ */ new Map();
|
|
334
|
+
for (const group of nodes.filter((n) => n.contains !== void 0)) {
|
|
335
|
+
if (group.kind !== "group" || !Array.isArray(group.contains)) {
|
|
336
|
+
add("error", "contains-shape", `nodes[${group.id}]`, "Only a group may contain a list of node ids.");
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
for (const id of group.contains) {
|
|
340
|
+
if (id === group.id) add("error", "contains-self", `nodes[${group.id}]`, `Group ${group.id} cannot contain itself.`);
|
|
341
|
+
else if (!nodeIds.has(id)) add("error", "contains-undefined", `nodes[${group.id}]`, `Contained node ${id} does not exist.`);
|
|
342
|
+
else if (owner.has(id)) add("error", "contains-duplicate", `nodes[${group.id}]`, `Node ${id} is already contained by ${owner.get(id)}.`);
|
|
343
|
+
else owner.set(id, group.id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
278
346
|
const linkIds = /* @__PURE__ */ new Set();
|
|
279
347
|
const linked = /* @__PURE__ */ new Set();
|
|
280
348
|
for (const l of links) {
|
|
@@ -286,23 +354,68 @@
|
|
|
286
354
|
linked.add(l.from);
|
|
287
355
|
linked.add(l.to);
|
|
288
356
|
const a = nodes.find((n) => n.id === l.from), b = nodes.find((n) => n.id === l.to);
|
|
289
|
-
if (
|
|
290
|
-
|
|
357
|
+
if (m.layout !== "free" && a.col !== void 0 && b.col !== void 0) {
|
|
358
|
+
const forward = b.col > a.col + (a.colSpan ?? 1) - 1;
|
|
359
|
+
if (!forward && l.kind !== "loop-back") add("warn", "link-direction", w, `Link ${l.id} does not run left to right (column ${a.col} to ${b.col}); mark it loop-back if it returns to an earlier stage.`);
|
|
360
|
+
if (forward && l.kind === "loop-back") add("warn", "loop-back-forward", w, `Link ${l.id} is marked loop-back but runs forward.`);
|
|
291
361
|
}
|
|
292
362
|
}
|
|
293
363
|
for (const s of [l.state, ...Object.values(l.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
|
|
294
364
|
}
|
|
365
|
+
const scenarioIds = /* @__PURE__ */ new Set();
|
|
366
|
+
for (const [i, scenario] of (Array.isArray(m.scenarios) ? m.scenarios : []).entries()) {
|
|
367
|
+
const w = `scenarios[${i}]`;
|
|
368
|
+
if (!scenario || typeof scenario.id !== "string" || typeof scenario.label !== "string" || !Array.isArray(scenario.steps)) {
|
|
369
|
+
add("error", "scenario-shape", w, "A scenario needs an id, label and steps.");
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (scenarioIds.has(scenario.id)) add("error", "scenario-duplicate", w, `Duplicate scenario id ${scenario.id}.`);
|
|
373
|
+
scenarioIds.add(scenario.id);
|
|
374
|
+
if (!scenario.steps.length) add("error", "scenario-empty", w, "A scenario needs at least one step.");
|
|
375
|
+
const timed = scenario.steps.filter((s) => s && s.at !== void 0).length;
|
|
376
|
+
if (timed && timed !== scenario.steps.length) add("error", "scenario-at-partial", w, "Give every step an at time or none.");
|
|
377
|
+
let prev, previousAt = -Infinity;
|
|
378
|
+
for (const [j, step] of scenario.steps.entries()) {
|
|
379
|
+
const sw = `${w}.steps[${j}]`;
|
|
380
|
+
const link = links.find((l) => l.id === step?.link);
|
|
381
|
+
if (!link) add("error", "scenario-link", sw, `Unknown link ${step?.link}.`);
|
|
382
|
+
if (step?.at !== void 0) {
|
|
383
|
+
if (!Number.isFinite(step.at) || step.at < 0 || step.at < previousAt) add("error", "scenario-at-order", sw, "Step times must be finite, nonnegative and nondecreasing.");
|
|
384
|
+
if (Number.isFinite(step.at) && step.at >= 0 && step.at >= previousAt) previousAt = step.at;
|
|
385
|
+
}
|
|
386
|
+
if (step?.dur !== void 0 && (!Number.isFinite(step.dur) || step.dur <= 0)) add("error", "scenario-duration", sw, "Duration must be positive and finite.");
|
|
387
|
+
if (prev && link) {
|
|
388
|
+
const previousTo = prev.to;
|
|
389
|
+
if ((nodes.find((n) => n.id === previousTo)?.ref ?? previousTo) !== (nodes.find((n) => n.id === link.from)?.ref ?? link.from)) add("warn", "scenario-gap", sw, "This step does not start at the previous step's target.");
|
|
390
|
+
}
|
|
391
|
+
if (link) prev = link;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
295
394
|
for (const n of nodes) {
|
|
296
|
-
if (
|
|
395
|
+
if (n.kind !== "gateway") continue;
|
|
396
|
+
const exits = links.filter((l) => l.from === n.id);
|
|
397
|
+
if (exits.length === 0) add("warn", "gateway-no-exit", `nodes[${n.id}]`, `Gateway ${n.id} has no exit.`);
|
|
398
|
+
else if (exits.length > 1 && exits.some((l) => !l.label?.trim())) add("warn", "gateway-exit-label", `nodes[${n.id}]`, `Every exit from gateway ${n.id} needs a label.`);
|
|
399
|
+
}
|
|
400
|
+
for (const n of nodes) {
|
|
401
|
+
if (!linked.has(n.id) && !n.allowIsolated && n.kind !== "note" && !(n.kind === "group" && n.contains?.length)) {
|
|
297
402
|
add("error", "orphan", `nodes[${n.id}]`, `Node ${n.id} has no links. Add a link, a placeholder neighbour, or set allowIsolated.`);
|
|
298
403
|
}
|
|
299
404
|
}
|
|
300
405
|
for (let i = 0; i < nodes.length; i++) {
|
|
301
406
|
for (let j = i + 1; j < nodes.length; j++) {
|
|
302
407
|
const a = nodes[i], b = nodes[j];
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
408
|
+
if (a.contains?.includes(b.id) || b.contains?.includes(a.id)) continue;
|
|
409
|
+
if (m.layout === "free") {
|
|
410
|
+
const aw = a.w ?? 190, ah = a.h ?? NODE_HEIGHT[a.kind] ?? 54, bw = b.w ?? 190, bh = b.h ?? NODE_HEIGHT[b.kind] ?? 54;
|
|
411
|
+
if ([a.x, a.y, b.x, b.y, aw, ah, bw, bh].every((v) => typeof v === "number" && Number.isFinite(v)) && a.x < b.x + bw && b.x < a.x + aw && a.y < b.y + bh && b.y < a.y + ah) {
|
|
412
|
+
add("error", "rect-overlap", `nodes[${a.id}]`, `Nodes ${a.id} and ${b.id} overlap.`);
|
|
413
|
+
}
|
|
414
|
+
} else if (a.col !== void 0 && b.col !== void 0 && a.row !== void 0 && b.row !== void 0) {
|
|
415
|
+
const ax2 = a.col + (a.colSpan ?? 1), bx2 = b.col + (b.colSpan ?? 1);
|
|
416
|
+
if (a.col < bx2 && b.col < ax2 && Math.abs(a.row - b.row) < 1) {
|
|
417
|
+
add("error", "cell-overlap", `nodes[${a.id}]`, `Nodes ${a.id} and ${b.id} overlap (same columns, rows less than 1 apart).`);
|
|
418
|
+
}
|
|
306
419
|
}
|
|
307
420
|
}
|
|
308
421
|
}
|
|
@@ -311,6 +424,133 @@
|
|
|
311
424
|
return out;
|
|
312
425
|
}
|
|
313
426
|
|
|
427
|
+
// src/autolayout.ts
|
|
428
|
+
function autoLayout(input) {
|
|
429
|
+
const nodes = input.nodes.map((n) => ({ ...n }));
|
|
430
|
+
const links = input.links.map((l) => ({ ...l }));
|
|
431
|
+
const columns = input.columns.map((c) => ({ ...c }));
|
|
432
|
+
const lanes = input.lanes.map((l) => ({ ...l }));
|
|
433
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
434
|
+
const index = new Map(nodes.map((n, i) => [n.id, i]));
|
|
435
|
+
const outgoing = new Map(nodes.map((n) => [n.id, []]));
|
|
436
|
+
const incoming = new Map(nodes.map((n) => [n.id, []]));
|
|
437
|
+
const neighbours = new Map(nodes.map((n) => [n.id, []]));
|
|
438
|
+
for (const link of links) {
|
|
439
|
+
if (!byId.has(link.from) || !byId.has(link.to)) continue;
|
|
440
|
+
outgoing.get(link.from).push(link);
|
|
441
|
+
incoming.get(link.to).push(link);
|
|
442
|
+
neighbours.get(link.from).push(link.to);
|
|
443
|
+
neighbours.get(link.to).push(link.from);
|
|
444
|
+
}
|
|
445
|
+
const color = /* @__PURE__ */ new Map();
|
|
446
|
+
const post = [];
|
|
447
|
+
for (const root of nodes) {
|
|
448
|
+
if (color.has(root.id)) continue;
|
|
449
|
+
color.set(root.id, 1);
|
|
450
|
+
const stack = [{ id: root.id, next: 0 }];
|
|
451
|
+
while (stack.length) {
|
|
452
|
+
const frame = stack[stack.length - 1];
|
|
453
|
+
const edges = outgoing.get(frame.id);
|
|
454
|
+
if (frame.next >= edges.length) {
|
|
455
|
+
color.set(frame.id, 2);
|
|
456
|
+
post.push(frame.id);
|
|
457
|
+
stack.pop();
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const link = edges[frame.next++];
|
|
461
|
+
if (link.kind === "loop-back") continue;
|
|
462
|
+
const state = color.get(link.to) ?? 0;
|
|
463
|
+
if (state === 1) link.kind = "loop-back";
|
|
464
|
+
else if (state === 0) {
|
|
465
|
+
color.set(link.to, 1);
|
|
466
|
+
stack.push({ id: link.to, next: 0 });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const id of post.reverse()) {
|
|
471
|
+
const n = byId.get(id);
|
|
472
|
+
if (n.col !== void 0) continue;
|
|
473
|
+
let col = 0;
|
|
474
|
+
for (const edge of incoming.get(id)) {
|
|
475
|
+
if (edge.kind === "loop-back") continue;
|
|
476
|
+
const parent = byId.get(edge.from);
|
|
477
|
+
col = Math.max(col, (parent.col ?? 0) + (parent.colSpan ?? 1));
|
|
478
|
+
}
|
|
479
|
+
n.col = col;
|
|
480
|
+
}
|
|
481
|
+
const seen = /* @__PURE__ */ new Set();
|
|
482
|
+
const usedLaneIds = new Set(lanes.map((l) => l.id));
|
|
483
|
+
let group = 0;
|
|
484
|
+
for (const first of nodes) {
|
|
485
|
+
if (seen.has(first.id)) continue;
|
|
486
|
+
const queue = [first.id];
|
|
487
|
+
seen.add(first.id);
|
|
488
|
+
for (let q = 0; q < queue.length; q++) {
|
|
489
|
+
for (const id of neighbours.get(queue[q])) if (!seen.has(id)) {
|
|
490
|
+
seen.add(id);
|
|
491
|
+
queue.push(id);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const missing = queue.map((id) => byId.get(id)).filter((n) => !n.lane);
|
|
495
|
+
if (!missing.length) continue;
|
|
496
|
+
group++;
|
|
497
|
+
let laneId = `group-${group}`;
|
|
498
|
+
while (usedLaneIds.has(laneId)) laneId += "-auto";
|
|
499
|
+
usedLaneIds.add(laneId);
|
|
500
|
+
lanes.push({ id: laneId, title: `Group ${group}` });
|
|
501
|
+
for (const n of missing) n.lane = laneId;
|
|
502
|
+
}
|
|
503
|
+
const rank = new Map(nodes.map((n, i) => [n.id, i]));
|
|
504
|
+
const laneOrder = lanes.map((l) => l.id);
|
|
505
|
+
const cells = /* @__PURE__ */ new Map();
|
|
506
|
+
for (const n of nodes) {
|
|
507
|
+
const key = `${n.lane}\0${n.col}`;
|
|
508
|
+
if (!cells.has(key)) cells.set(key, []);
|
|
509
|
+
cells.get(key).push(n);
|
|
510
|
+
}
|
|
511
|
+
const maxCol = Math.max(0, ...nodes.map((n) => (n.col ?? 0) + (n.colSpan ?? 1) - 1));
|
|
512
|
+
for (let sweep = 0; sweep < 4; sweep++) {
|
|
513
|
+
const forward = sweep % 2 === 0;
|
|
514
|
+
const layers = Array.from({ length: maxCol + 1 }, (_, i) => forward ? i : maxCol - i);
|
|
515
|
+
for (const lane of laneOrder) for (const col of layers) {
|
|
516
|
+
const groupNodes = cells.get(`${lane}\0${col}`);
|
|
517
|
+
if (!groupNodes || groupNodes.length < 2) continue;
|
|
518
|
+
const score = (n) => {
|
|
519
|
+
const edges = forward ? incoming.get(n.id) : outgoing.get(n.id);
|
|
520
|
+
const ns = edges.filter((e) => e.kind !== "loop-back").map((e) => forward ? e.from : e.to);
|
|
521
|
+
return ns.length ? ns.reduce((sum, id) => sum + (rank.get(id) ?? 0), 0) / ns.length : rank.get(n.id) ?? 0;
|
|
522
|
+
};
|
|
523
|
+
groupNodes.sort((a, b) => score(a) - score(b) || index.get(a.id) - index.get(b.id));
|
|
524
|
+
groupNodes.forEach((n, i) => rank.set(n.id, i));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
let nextBase = 0;
|
|
528
|
+
const placed = nodes.filter((n) => n.row !== void 0);
|
|
529
|
+
for (const lane of laneOrder) {
|
|
530
|
+
const members = nodes.filter((n) => n.lane === lane);
|
|
531
|
+
if (!members.length) continue;
|
|
532
|
+
const ordered = [...members].sort((a, b) => (a.col ?? 0) - (b.col ?? 0) || (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0) || index.get(a.id) - index.get(b.id));
|
|
533
|
+
const rowAtCol = /* @__PURE__ */ new Map();
|
|
534
|
+
for (const n of ordered) {
|
|
535
|
+
if (n.row !== void 0) continue;
|
|
536
|
+
let row = Math.max(nextBase, rowAtCol.get(n.col ?? 0) ?? nextBase);
|
|
537
|
+
while (placed.some((p) => (n.col ?? 0) < (p.col ?? 0) + (p.colSpan ?? 1) && (p.col ?? 0) < (n.col ?? 0) + (n.colSpan ?? 1) && Math.abs(row - (p.row ?? 0)) < 1)) row += 1;
|
|
538
|
+
n.row = row;
|
|
539
|
+
placed.push(n);
|
|
540
|
+
rowAtCol.set(n.col ?? 0, row + 1);
|
|
541
|
+
}
|
|
542
|
+
const last = Math.max(nextBase - 1, ...members.map((n) => n.row ?? 0));
|
|
543
|
+
const loops = links.filter((l) => l.kind === "loop-back" && byId.get(l.from)?.lane === lane && byId.get(l.to)?.lane === lane).length;
|
|
544
|
+
nextBase = last + 1.75 + loops * 0.35;
|
|
545
|
+
}
|
|
546
|
+
const needed = Math.max(0, ...nodes.map((n) => (n.col ?? 0) + (n.colSpan ?? 1)));
|
|
547
|
+
while (columns.length < needed) {
|
|
548
|
+
const i = columns.length + 1;
|
|
549
|
+
columns.push({ id: `stage-${i}`, title: `Stage ${i}`, width: 190 });
|
|
550
|
+
}
|
|
551
|
+
return { ...input, nodes, links, columns, lanes };
|
|
552
|
+
}
|
|
553
|
+
|
|
314
554
|
// src/stateCss.ts
|
|
315
555
|
var cssId = (id) => id.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
316
556
|
var TONES2 = { ok: "var(--ok)", warn: "var(--warn)", crit: "var(--crit)", info: "var(--up)", muted: "var(--muted)", accent: "var(--accent)" };
|
|
@@ -330,6 +570,7 @@
|
|
|
330
570
|
const glow = s.bucket === "good" ? ` box-shadow:0 0 14px -4px color-mix(in srgb, ${T} 55%, transparent), var(--shadow);` : "";
|
|
331
571
|
out.push(`.node.st-${c} { border-left:4px ${s.border} ${T};${glow} --breath:${T}; }`);
|
|
332
572
|
}
|
|
573
|
+
out.push(`.node.st-${c}.gateway polygon { stroke:${T}; fill:color-mix(in srgb, ${T} 10%, var(--panel)); }`);
|
|
333
574
|
out.push(`.ev.st-${c} { color:${T}; }`);
|
|
334
575
|
if (s.motion === "breathe") out.push(`.node.st-${c}:not(.dim) { animation:breathe 3.2s ease-in-out infinite; }`);
|
|
335
576
|
const edge = Object.hasOwn(DASH, s.edge) ? s.edge : "dash";
|
|
@@ -349,14 +590,32 @@
|
|
|
349
590
|
// src/rules.ts
|
|
350
591
|
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
351
592
|
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
352
|
-
var
|
|
353
|
-
var
|
|
354
|
-
function
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
593
|
+
var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
|
|
594
|
+
var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
|
|
595
|
+
function groupFor(m, nodeId) {
|
|
596
|
+
return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
|
|
597
|
+
}
|
|
598
|
+
var nodeVisible = (m, view, n, collapsed) => {
|
|
599
|
+
if (n.visibleIn && !n.visibleIn.includes(view)) return false;
|
|
600
|
+
if (!collapsed) return true;
|
|
601
|
+
if (n.kind === "group" && n.contains) return collapsed.has(n.id);
|
|
602
|
+
const owner = groupFor(m, n.id);
|
|
603
|
+
return !owner || !collapsed.has(owner.id);
|
|
604
|
+
};
|
|
605
|
+
function toggleGroupState(m, view, collapsed, id) {
|
|
606
|
+
const group = m.nodes.find((n) => n.id === id && n.kind === "group" && n.contains?.length);
|
|
607
|
+
if (!group) return null;
|
|
608
|
+
const next = new Set(collapsed);
|
|
609
|
+
const expanding = next.has(id);
|
|
610
|
+
if (expanding) next.delete(id);
|
|
611
|
+
else next.add(id);
|
|
612
|
+
const selected = expanding ? group.contains?.find((child) => {
|
|
613
|
+
const n = m.nodes.find((candidate) => candidate.id === child);
|
|
614
|
+
return !!n && nodeVisible(m, view, n, next);
|
|
615
|
+
}) ?? null : id;
|
|
616
|
+
return { collapsed: next, selected, expanding };
|
|
359
617
|
}
|
|
618
|
+
var nodeStateId = (_m, view, n) => n.stateIn?.[view] ?? n.state;
|
|
360
619
|
var linkStateId = (_m, view, l) => l.stateIn?.[view] ?? l.state;
|
|
361
620
|
var bucketOf = (m, stateId) => m.states[stateId]?.bucket ?? "gap";
|
|
362
621
|
var isNew = (m, view, n) => view !== baselineView(m) && nodeVisible(m, view, n) && !nodeVisible(m, baselineView(m), n);
|
|
@@ -365,6 +624,27 @@
|
|
|
365
624
|
return view !== base && nodeVisible(m, base, n) && bucketOf(m, nodeStateId(m, base, n)) === "gap" && bucketOf(m, nodeStateId(m, view, n)) !== "gap";
|
|
366
625
|
};
|
|
367
626
|
var canonOf = (nodes, id) => nodes.get(id)?.ref ?? id;
|
|
627
|
+
function effectiveLinks(m, view, collapsed, nodes) {
|
|
628
|
+
const byPair = /* @__PURE__ */ new Map();
|
|
629
|
+
const rank = { good: 0, neutral: 1, gap: 2 };
|
|
630
|
+
for (const link of m.links) {
|
|
631
|
+
if (link.visibleIn && !link.visibleIn.includes(view)) continue;
|
|
632
|
+
const a = nodes.get(link.from), b = nodes.get(link.to);
|
|
633
|
+
if (!a || !b || a.visibleIn && !a.visibleIn.includes(view) || b.visibleIn && !b.visibleIn.includes(view)) continue;
|
|
634
|
+
const groupA = groupFor(m, a.id), groupB = groupFor(m, b.id);
|
|
635
|
+
const from = groupA && collapsed.has(groupA.id) ? groupA.id : a.id;
|
|
636
|
+
const to = groupB && collapsed.has(groupB.id) ? groupB.id : b.id;
|
|
637
|
+
if (from === to) continue;
|
|
638
|
+
const fromNode = nodes.get(from), toNode = nodes.get(to);
|
|
639
|
+
if (!fromNode || !toNode || !nodeVisible(m, view, fromNode, collapsed) || !nodeVisible(m, view, toNode, collapsed)) continue;
|
|
640
|
+
const state = linkStateId(m, view, link);
|
|
641
|
+
const key = JSON.stringify([from, to]);
|
|
642
|
+
const prev = byPair.get(key);
|
|
643
|
+
if (!prev) byPair.set(key, { ...link, from, to, state, stateIn: void 0 });
|
|
644
|
+
else if (rank[bucketOf(m, state)] > rank[bucketOf(m, prev.state)]) prev.state = state;
|
|
645
|
+
}
|
|
646
|
+
return [...byPair.values()];
|
|
647
|
+
}
|
|
368
648
|
|
|
369
649
|
// src/trace.ts
|
|
370
650
|
function relatives(edges, canonOf2, start, dir) {
|
|
@@ -390,6 +670,33 @@
|
|
|
390
670
|
function connected(edges, canonOf2, start) {
|
|
391
671
|
return /* @__PURE__ */ new Set([...relatives(edges, canonOf2, start, "up"), ...relatives(edges, canonOf2, start, "down")]);
|
|
392
672
|
}
|
|
673
|
+
function withinHops(edges, canonOf2, starts, n) {
|
|
674
|
+
const found = new Set(starts.map(canonOf2));
|
|
675
|
+
const radius = Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
676
|
+
let frontier = [...found];
|
|
677
|
+
for (let hop = 0; hop < radius && frontier.length; hop++) {
|
|
678
|
+
const next = [];
|
|
679
|
+
const current = new Set(frontier);
|
|
680
|
+
for (const edge of edges) {
|
|
681
|
+
const a = canonOf2(edge.from), b = canonOf2(edge.to);
|
|
682
|
+
if (current.has(a) && !found.has(b)) {
|
|
683
|
+
found.add(b);
|
|
684
|
+
next.push(b);
|
|
685
|
+
}
|
|
686
|
+
if (current.has(b) && !found.has(a)) {
|
|
687
|
+
found.add(a);
|
|
688
|
+
next.push(a);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
frontier = next;
|
|
692
|
+
}
|
|
693
|
+
return found;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// src/selection.ts
|
|
697
|
+
function notifySelection(previous, next, onSelect) {
|
|
698
|
+
if (previous !== next) onSelect?.(next);
|
|
699
|
+
}
|
|
393
700
|
|
|
394
701
|
// src/interact.ts
|
|
395
702
|
function relationsOf(ctx) {
|
|
@@ -403,7 +710,7 @@
|
|
|
403
710
|
];
|
|
404
711
|
}
|
|
405
712
|
function visibleEdges(ctx) {
|
|
406
|
-
return
|
|
713
|
+
return effectiveLinks(ctx.model, ctx.mode, ctx.collapsed, ctx.nodes).map((l) => ({ from: l.from, to: l.to }));
|
|
407
714
|
}
|
|
408
715
|
function relationSet(ctx, rel, start) {
|
|
409
716
|
const canon = (id) => canonOf(ctx.nodes, id);
|
|
@@ -424,7 +731,7 @@
|
|
|
424
731
|
function install(ctx) {
|
|
425
732
|
const { els, model } = ctx;
|
|
426
733
|
const canon = (id) => canonOf(ctx.nodes, id);
|
|
427
|
-
const visibleNodes = () => model.nodes.filter((n) => nodeVisible(model, ctx.mode, n));
|
|
734
|
+
const visibleNodes = () => model.nodes.filter((n) => nodeVisible(model, ctx.mode, n, ctx.collapsed));
|
|
428
735
|
const inspInset = () => els.inspector.classList.contains("open") ? 340 : 0;
|
|
429
736
|
function activeSet(id) {
|
|
430
737
|
const edges = visibleEdges(ctx);
|
|
@@ -441,7 +748,7 @@
|
|
|
441
748
|
return ctx.filter === "gap" ? b === "gap" : !!n.upgrade && b !== "good";
|
|
442
749
|
}).map((n) => canon(n.id)));
|
|
443
750
|
}
|
|
444
|
-
if (ctx.query) return
|
|
751
|
+
if (ctx.query) return withinHops(edges, canon, visibleNodes().filter((n) => matches(ctx, n)).map((n) => n.id), ctx.hops);
|
|
445
752
|
return null;
|
|
446
753
|
}
|
|
447
754
|
function trace(id) {
|
|
@@ -465,6 +772,7 @@
|
|
|
465
772
|
}
|
|
466
773
|
function select(id) {
|
|
467
774
|
clearSim();
|
|
775
|
+
const previous = ctx.selected;
|
|
468
776
|
ctx.selected = id ? canon(id) : null;
|
|
469
777
|
ctx.hl = null;
|
|
470
778
|
ctx.dismissed = false;
|
|
@@ -473,6 +781,7 @@
|
|
|
473
781
|
ctx.fn.renderInspector();
|
|
474
782
|
drawTrail();
|
|
475
783
|
if (ctx.selected) revealNode(ctx.selected);
|
|
784
|
+
notifySelection(previous, ctx.selected, ctx.onSelect);
|
|
476
785
|
}
|
|
477
786
|
function overlay(edge, cls, delay, dur) {
|
|
478
787
|
const o = edge.cloneNode(false);
|
|
@@ -565,7 +874,7 @@
|
|
|
565
874
|
}
|
|
566
875
|
}
|
|
567
876
|
function rectOf(ids) {
|
|
568
|
-
const ps = ids.map((i) => ctx.geom.nodes[i]).filter((p) => !!p && !!ctx.nodes.get(p.id) && nodeVisible(model, ctx.mode, ctx.nodes.get(p.id)));
|
|
877
|
+
const ps = ids.map((i) => ctx.geom.nodes[i]).filter((p) => !!p && !!ctx.nodes.get(p.id) && nodeVisible(model, ctx.mode, ctx.nodes.get(p.id), ctx.collapsed));
|
|
569
878
|
if (!ps.length) return null;
|
|
570
879
|
const x1 = Math.min(...ps.map((p) => p.x)), y1 = Math.min(...ps.map((p) => p.y));
|
|
571
880
|
const x2 = Math.max(...ps.map((p) => p.x + p.w)), y2 = Math.max(...ps.map((p) => p.y + p.h));
|
|
@@ -606,11 +915,11 @@
|
|
|
606
915
|
clearSim();
|
|
607
916
|
ctx.mode = id;
|
|
608
917
|
const sel = ctx.selected && ctx.nodes.get(ctx.selected);
|
|
609
|
-
if (sel && !nodeVisible(model, id, sel)) select(null);
|
|
918
|
+
if (sel && !nodeVisible(model, id, sel, ctx.collapsed)) select(null);
|
|
610
919
|
ctx.fn.paint(true);
|
|
611
920
|
ctx.userMoved = false;
|
|
612
921
|
ctx.fn.fitWidth(true);
|
|
613
|
-
const added = model.nodes.filter((n) => nodeVisible(model, id, n) && !nodeVisible(model, model.views?.[0]?.id ?? "all", n)).length;
|
|
922
|
+
const added = model.nodes.filter((n) => nodeVisible(model, id, n, ctx.collapsed) && !nodeVisible(model, model.views?.[0]?.id ?? "all", n, ctx.collapsed)).length;
|
|
614
923
|
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.`);
|
|
615
924
|
}
|
|
616
925
|
ctx.fn.trace = trace;
|
|
@@ -623,6 +932,16 @@
|
|
|
623
932
|
ctx.fn.zoomToBand = zoomToBand;
|
|
624
933
|
ctx.fn.escape = escape;
|
|
625
934
|
ctx.fn.setMode = setMode;
|
|
935
|
+
ctx.fn.toggleGroup = (id) => {
|
|
936
|
+
const group = model.nodes.find((n) => n.id === id && n.kind === "group" && n.contains?.length);
|
|
937
|
+
if (!group) return;
|
|
938
|
+
const change = toggleGroupState(model, ctx.mode, ctx.collapsed, id);
|
|
939
|
+
if (!change) return;
|
|
940
|
+
ctx.collapsed = change.collapsed;
|
|
941
|
+
ctx.fn.paint();
|
|
942
|
+
ctx.fn.select(change.selected);
|
|
943
|
+
ctx.fn.announce(change.expanding ? `Expanded ${group.title}.` : `Collapsed into ${group.title}.`);
|
|
944
|
+
};
|
|
626
945
|
ctx.fn.announce = (t) => {
|
|
627
946
|
els.live.textContent = t;
|
|
628
947
|
};
|
|
@@ -648,7 +967,6 @@
|
|
|
648
967
|
|
|
649
968
|
// src/layout.ts
|
|
650
969
|
var DEFAULTS = { pitch: 62, top: 64, gap: 34, padX: 24, visible: () => true };
|
|
651
|
-
var HEIGHT = { card: 54, group: 54, gateway: 54, placeholder: 54, chip: 34, start: 40, end: 40, event: 40, note: 40 };
|
|
652
970
|
var LANE_PAD_TOP = 38;
|
|
653
971
|
var LANE_PAD_BOTTOM = 14;
|
|
654
972
|
function computeGeometry(m, opt = {}) {
|
|
@@ -662,20 +980,53 @@
|
|
|
662
980
|
const world = { w: Math.max(1, x - o.gap + o.padX), h: 1 };
|
|
663
981
|
const nodes = {};
|
|
664
982
|
for (const n of m.nodes) {
|
|
665
|
-
const h =
|
|
666
|
-
|
|
983
|
+
const h = NODE_HEIGHT[n.kind] ?? 54;
|
|
984
|
+
if (m.layout === "free") {
|
|
985
|
+
nodes[n.id] = { id: n.id, x: n.x ?? 0, y: n.y ?? 0, w: n.w ?? 190, h: n.h ?? h };
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const first = Math.min(Math.max(0, n.col ?? 0), m.columns.length - 1);
|
|
667
989
|
const last = Math.min(m.columns.length - 1, first + (n.colSpan ?? 1) - 1);
|
|
668
990
|
const x0 = xs[first] ?? o.padX;
|
|
669
991
|
const x1 = (xs[last] ?? x0) + (m.columns[last]?.width ?? 0);
|
|
670
|
-
nodes[n.id] = { id: n.id, x: x0, y: o.top + 40 + n.row * o.pitch - h / 2, w: Math.max(1, x1 - x0), h };
|
|
992
|
+
nodes[n.id] = { id: n.id, x: x0, y: o.top + 40 + (n.row ?? 0) * o.pitch - h / 2, w: Math.max(1, x1 - x0), h };
|
|
671
993
|
}
|
|
994
|
+
if (m.layout === "free") world.w = Math.max(world.w, ...Object.values(nodes).map((p) => p.x + p.w + o.padX), 1);
|
|
995
|
+
const loops = {};
|
|
672
996
|
const lanes = {};
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
const
|
|
997
|
+
const visibleMembers = (id) => m.nodes.filter((n) => n.lane === id && o.visible(n.id)).map((n) => nodes[n.id]);
|
|
998
|
+
const ordered = m.lanes.map((l) => ({ l, mem: visibleMembers(l.id) })).filter((x2) => x2.mem.length).sort((a, b) => Math.min(...a.mem.map((p) => p.y)) - Math.min(...b.mem.map((p) => p.y)));
|
|
999
|
+
let previousBottom = -Infinity;
|
|
1000
|
+
for (const { l, mem } of ordered) {
|
|
1001
|
+
const top = Math.min(...mem.map((p) => p.y)) - LANE_PAD_TOP;
|
|
1002
|
+
const shift = m.layout === "free" ? 0 : Math.max(0, previousBottom + 8 - top);
|
|
1003
|
+
if (shift) for (const p of mem) p.y += shift;
|
|
1004
|
+
const y1 = top + shift;
|
|
1005
|
+
const laneNodeIds = new Set(mem.map((p) => p.id));
|
|
1006
|
+
const possibleLoops = m.links.filter((link) => link.kind === "loop-back" && o.visible(link.from) && o.visible(link.to) && (laneNodeIds.has(link.from) || laneNodeIds.has(link.to))).length;
|
|
1007
|
+
const bottom = Math.max(...mem.map((p) => p.y + p.h));
|
|
1008
|
+
const y2 = bottom + (possibleLoops ? 10 + 8 * (possibleLoops - 1) : 0) + LANE_PAD_BOTTOM;
|
|
678
1009
|
lanes[l.id] = { x: 8, y: y1, w: world.w - 16, h: y2 - y1 };
|
|
1010
|
+
previousBottom = y2;
|
|
1011
|
+
}
|
|
1012
|
+
const channelCounts = /* @__PURE__ */ new Map();
|
|
1013
|
+
for (const link of m.links) {
|
|
1014
|
+
if (link.kind !== "loop-back" || !o.visible(link.from) || !o.visible(link.to)) continue;
|
|
1015
|
+
const a = nodes[link.from], b = nodes[link.to];
|
|
1016
|
+
if (!a || !b) continue;
|
|
1017
|
+
const lower = a.y + a.h >= b.y + b.h ? a : b;
|
|
1018
|
+
const laneId = m.nodes.find((n) => n.id === lower.id)?.lane;
|
|
1019
|
+
if (!laneId || !lanes[laneId]) continue;
|
|
1020
|
+
const left = Math.min(a.x + a.w / 2, b.x + b.w / 2);
|
|
1021
|
+
const right = Math.max(a.x + a.w / 2, b.x + b.w / 2);
|
|
1022
|
+
const laneMembers = visibleMembers(laneId);
|
|
1023
|
+
const crossed = laneMembers.filter((p) => p.x < right && p.x + p.w > left);
|
|
1024
|
+
const lowestBottom = Math.max(a.y + a.h, b.y + b.h, ...crossed.map((p) => p.y + p.h));
|
|
1025
|
+
const index = channelCounts.get(laneId) ?? 0;
|
|
1026
|
+
loops[link.id] = { channelY: lowestBottom + 10 + 8 * index };
|
|
1027
|
+
channelCounts.set(laneId, index + 1);
|
|
1028
|
+
const lane = lanes[laneId];
|
|
1029
|
+
lane.h = Math.max(lane.h, loops[link.id].channelY + LANE_PAD_BOTTOM - lane.y);
|
|
679
1030
|
}
|
|
680
1031
|
const all = Object.values(lanes);
|
|
681
1032
|
let bounds = { x: 0, y: 0, w: world.w, h: 1 };
|
|
@@ -684,7 +1035,7 @@
|
|
|
684
1035
|
bounds = { x: 8, y: top, w: world.w - 16, h: Math.max(...all.map((r) => r.y + r.h)) - top };
|
|
685
1036
|
}
|
|
686
1037
|
world.h = Math.max(1, bounds.y + bounds.h + 70);
|
|
687
|
-
return { xs, nodes, lanes, bounds, world };
|
|
1038
|
+
return { xs, nodes, lanes, loops, bounds, world };
|
|
688
1039
|
}
|
|
689
1040
|
|
|
690
1041
|
// src/routing.ts
|
|
@@ -695,14 +1046,85 @@
|
|
|
695
1046
|
const dir = y2 > y1 ? 1 : -1;
|
|
696
1047
|
return `M${x1},${y1} H${tx - r} Q${tx},${y1} ${tx},${y1 + dir * r} V${y2 - dir * r} Q${tx},${y2} ${tx + r},${y2} H${x2}`;
|
|
697
1048
|
}
|
|
1049
|
+
function loopBack(x1, y1, x2, y2, channelY, detour = {}) {
|
|
1050
|
+
x1 = Number.isFinite(x1) ? x1 : 0;
|
|
1051
|
+
x2 = Number.isFinite(x2) ? x2 : 0;
|
|
1052
|
+
y1 = Number.isFinite(y1) ? y1 : 0;
|
|
1053
|
+
y2 = Number.isFinite(y2) ? y2 : 0;
|
|
1054
|
+
channelY = Number.isFinite(channelY) ? channelY : Math.max(y1, y2) + 10;
|
|
1055
|
+
channelY = Math.max(channelY, y1, y2);
|
|
1056
|
+
const from = detour.from, to = detour.to;
|
|
1057
|
+
const startX = from?.x ?? x1, endX = to?.x ?? x2;
|
|
1058
|
+
const start = from ? `M${x1},${y1} V${from.y} H${startX}` : `M${x1},${y1}`;
|
|
1059
|
+
const finish = to ? ` V${to.y} H${x2} V${y2}` : ` V${y2}`;
|
|
1060
|
+
if (from || to) {
|
|
1061
|
+
const dir2 = endX >= startX ? 1 : -1;
|
|
1062
|
+
const r2 = Math.max(0, Math.min(8, Math.abs(endX - startX) / 2, (channelY - (from?.y ?? y1)) / 2, (channelY - (to?.y ?? y2)) / 2));
|
|
1063
|
+
return `${start} V${channelY - r2} Q${startX},${channelY} ${startX + dir2 * r2},${channelY} H${endX - dir2 * r2} Q${endX},${channelY} ${endX},${channelY - r2}${finish}`;
|
|
1064
|
+
}
|
|
1065
|
+
const dx = x2 - x1;
|
|
1066
|
+
const dir = dx >= 0 ? 1 : -1;
|
|
1067
|
+
const run = Math.abs(dx) || 32;
|
|
1068
|
+
const radius = Math.min(8, run / 2, (channelY - y1) / 2, (channelY - y2) / 2);
|
|
1069
|
+
const r = Math.max(0, radius);
|
|
1070
|
+
if (dx === 0) {
|
|
1071
|
+
return `M${x1},${y1} V${channelY - r} Q${x1},${channelY} ${x1 + r},${channelY} H${x1 + 16} H${x1 - r} Q${x1},${channelY} ${x1},${channelY - r} V${y2}`;
|
|
1072
|
+
}
|
|
1073
|
+
return `M${x1},${y1} V${channelY - r} Q${x1},${channelY} ${x1 + dir * r},${channelY} H${x2 - dir * r} Q${x2},${channelY} ${x2},${channelY - r} V${y2}`;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/metrics.ts
|
|
1077
|
+
function sparklinePoints(series, width, height) {
|
|
1078
|
+
if (!Array.isArray(series) || series.length < 2 || series.some((v) => !Number.isFinite(v))) return "";
|
|
1079
|
+
const min = Math.min(...series), max = Math.max(...series), span = max - min;
|
|
1080
|
+
return series.map((v, i) => `${Math.round(i / (series.length - 1) * width * 100) / 100},${Math.round((span ? 1 - (v - min) / span : 0.5) * height * 100) / 100}`).join(" ");
|
|
1081
|
+
}
|
|
1082
|
+
function metricBadge(metric) {
|
|
1083
|
+
if (!metric) return "";
|
|
1084
|
+
const label = metric.label ?? "Metric";
|
|
1085
|
+
const valStr = esc(String(metric.value));
|
|
1086
|
+
const unitStr = metric.unit ? " " + esc(metric.unit) : "";
|
|
1087
|
+
const title = `${esc(label)}: ${valStr}${unitStr}`;
|
|
1088
|
+
const hasMax = metric.max != null && Number.isFinite(metric.max) && metric.max > 0;
|
|
1089
|
+
const pct = hasMax && Number.isFinite(metric.value) ? Math.max(0, Math.min(100, metric.value / metric.max * 100)) : 0;
|
|
1090
|
+
const bar = hasMax ? `<i style="width:${pct}%"></i>` : "";
|
|
1091
|
+
return `<span class="metric-badge" title="${title}">${valStr}${unitStr}${bar}</span>`;
|
|
1092
|
+
}
|
|
1093
|
+
function sparkline(metric, width = 56, height = 12) {
|
|
1094
|
+
if (!metric?.series || metric.series.length < 2) return "";
|
|
1095
|
+
const points = sparklinePoints(metric.series, width, height);
|
|
1096
|
+
if (!points) return "";
|
|
1097
|
+
return `<svg class="spark" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" aria-hidden="true"><polyline points="${points}"/></svg>`;
|
|
1098
|
+
}
|
|
698
1099
|
|
|
699
1100
|
// src/render.ts
|
|
1101
|
+
function loopRoute(ctx, fromId, toId, from, to, channel) {
|
|
1102
|
+
const detourFor = (end, other) => {
|
|
1103
|
+
const center = end.x + end.w / 2, bottom = end.y + end.h;
|
|
1104
|
+
const blocker = ctx.model.nodes.filter((n) => n.id !== fromId && n.id !== toId && nodeVisible(ctx.model, ctx.mode, n, ctx.collapsed)).map((n) => ctx.geom.nodes[n.id]).filter((p) => !!p && p.x < center && center < p.x + p.w && p.y > bottom && p.y < channel).sort((a, b) => a.y - b.y)[0];
|
|
1105
|
+
if (!blocker) return void 0;
|
|
1106
|
+
return { x: other.x + other.w / 2 < center ? blocker.x - 10 : blocker.x + blocker.w + 10, y: (bottom + blocker.y) / 2 };
|
|
1107
|
+
};
|
|
1108
|
+
return loopBack(
|
|
1109
|
+
from.x + from.w / 2,
|
|
1110
|
+
from.y + from.h,
|
|
1111
|
+
to.x + to.w / 2,
|
|
1112
|
+
to.y + to.h,
|
|
1113
|
+
channel,
|
|
1114
|
+
{ from: detourFor(from, to), to: detourFor(to, from) }
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
700
1117
|
function nodeHtml(ctx, n) {
|
|
1118
|
+
const badge = metricBadge(n.metrics);
|
|
1119
|
+
const spark = sparkline(n.metrics, 56, 12);
|
|
1120
|
+
if (n.kind === "gateway") {
|
|
1121
|
+
return `<svg class="gw" viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true"><polygon points="50,0 100,50 50,100 0,50"></polygon></svg><span class="gw-title">${esc(n.title)}</span>${badge}${spark}`;
|
|
1122
|
+
}
|
|
701
1123
|
const m = ctx.model;
|
|
702
1124
|
const sid = nodeStateId(m, ctx.mode, n);
|
|
703
1125
|
const word = m.states[sid]?.word ?? sid;
|
|
704
1126
|
const bucket = bucketOf(m, sid);
|
|
705
|
-
const sub = n.subtitle ?? m.columns[n.col]?.title ?? "";
|
|
1127
|
+
const sub = n.subtitle ?? m.columns[n.col ?? 0]?.title ?? "";
|
|
706
1128
|
const weight = Math.max(1, n.weight ?? 1);
|
|
707
1129
|
const showWord = n.kind !== "placeholder" && n.kind !== "note";
|
|
708
1130
|
const bar = weight > 1 && n.kind !== "chip" ? `<div class="bar"><i class="${["good", "neutral", "gap"].includes(bucket) ? bucket : "gap"}" style="width:100%"></i></div>` : "";
|
|
@@ -710,7 +1132,8 @@
|
|
|
710
1132
|
const upgrade = n.upgrade && bucket !== "good" ? `<span class="cor meas" title="${esc("Can improve: " + n.upgrade)}">\u2197</span>` : "";
|
|
711
1133
|
const arrow = n.kind === "chip" ? '<span aria-hidden="true">\u2191 </span>' : "";
|
|
712
1134
|
const lead = showWord ? `<b class="ev st-${cssId(sid)}">${esc(word)}</b> \xB7 ` : "";
|
|
713
|
-
|
|
1135
|
+
const fold = n.kind === "group" && n.contains?.length ? `<span class="fold">\u25B8 ${n.contains.length}</span>` : "";
|
|
1136
|
+
return `<div class="t"><span class="newdot" title="Added in this view"></span><span class="name">${arrow}${esc(n.title)}</span>${fold}${badge}</div><div class="s${spark ? " has-spark" : ""}" title="${esc(sub)}">${lead}${esc(sub)}</div>${spark}${bar}${improved}${upgrade}`;
|
|
714
1137
|
}
|
|
715
1138
|
function build(ctx) {
|
|
716
1139
|
const { model, els } = ctx;
|
|
@@ -749,7 +1172,7 @@
|
|
|
749
1172
|
d.id = "n_" + n.id;
|
|
750
1173
|
d.tabIndex = 0;
|
|
751
1174
|
d.setAttribute("role", "button");
|
|
752
|
-
d.style.setProperty("--c", String(n.col));
|
|
1175
|
+
d.style.setProperty("--c", String(n.col ?? 0));
|
|
753
1176
|
d.style.left = p.x + "px";
|
|
754
1177
|
d.style.top = p.y + "px";
|
|
755
1178
|
d.style.width = p.w + "px";
|
|
@@ -784,7 +1207,10 @@
|
|
|
784
1207
|
return;
|
|
785
1208
|
}
|
|
786
1209
|
const x1 = A.x + A.w, y1 = A.y + A.h / 2, x2 = B.x, y2 = B.y + B.h / 2, tx = x1 + gap / 2;
|
|
787
|
-
const
|
|
1210
|
+
const loop = l.kind === "loop-back";
|
|
1211
|
+
const channel = ctx.geom.loops[l.id]?.channelY ?? Math.max(A.y + A.h, B.y + B.h) + 10;
|
|
1212
|
+
const d = loop ? loopRoute(ctx, l.from, l.to, A, B, channel) : elbow(x1, y1, x2 - 1, y2, tx);
|
|
1213
|
+
const path = sv("path", { d, class: "edge" + (loop ? " loop" : ""), "data-i": String(i), "data-a": l.from, "data-b": l.to, "marker-end": "url(#arr)" });
|
|
788
1214
|
els.layer.appendChild(path);
|
|
789
1215
|
ctx.edgeEls[i] = path;
|
|
790
1216
|
if (l.label) {
|
|
@@ -792,8 +1218,8 @@
|
|
|
792
1218
|
lab.dataset.i = String(i);
|
|
793
1219
|
lab.dataset.a = l.from;
|
|
794
1220
|
lab.dataset.b = l.to;
|
|
795
|
-
lab.style.left = (Math.abs(y2 - y1) < 1 ? (x1 + x2) / 2 : tx) + "px";
|
|
796
|
-
lab.style.top = (y1 + y2) / 2 + "px";
|
|
1221
|
+
lab.style.left = (loop ? (A.x + A.w / 2 + B.x + B.w / 2) / 2 : Math.abs(y2 - y1) < 1 ? (x1 + x2) / 2 : tx) + "px";
|
|
1222
|
+
lab.style.top = (loop ? ctx.geom.loops[l.id]?.channelY ?? Math.max(A.y + A.h, B.y + B.h) + 10 : (y1 + y2) / 2) + "px";
|
|
797
1223
|
els.world.appendChild(lab);
|
|
798
1224
|
ctx.labelEls[i] = lab;
|
|
799
1225
|
}
|
|
@@ -802,28 +1228,37 @@
|
|
|
802
1228
|
function paint(ctx, fromSwitch = false) {
|
|
803
1229
|
const { model } = ctx;
|
|
804
1230
|
const mode = ctx.mode;
|
|
805
|
-
|
|
1231
|
+
const effective = effectiveLinks(model, mode, ctx.collapsed, ctx.nodes);
|
|
1232
|
+
const effectiveById = new Map(effective.map((l) => [l.id, l]));
|
|
1233
|
+
ctx.geom = computeGeometry({ ...model, links: effective }, { visible: (id) => {
|
|
806
1234
|
const n = ctx.nodes.get(id);
|
|
807
|
-
return !!n && nodeVisible(model, mode, n);
|
|
1235
|
+
return !!n && nodeVisible(model, mode, n, ctx.collapsed);
|
|
808
1236
|
} });
|
|
1237
|
+
ctx.els.world.style.height = ctx.geom.world.h + "px";
|
|
1238
|
+
ctx.els.svg.setAttribute("height", String(ctx.geom.world.h));
|
|
809
1239
|
document.body.classList.toggle("alt", !!model.views && mode !== baselineView(model));
|
|
810
1240
|
for (const n of model.nodes) {
|
|
811
1241
|
const d = ctx.nodeEls.get(n.id);
|
|
812
1242
|
if (!d) continue;
|
|
1243
|
+
const rect = ctx.geom.nodes[n.id];
|
|
1244
|
+
if (rect) {
|
|
1245
|
+
d.style.left = rect.x + "px";
|
|
1246
|
+
d.style.top = rect.y + "px";
|
|
1247
|
+
}
|
|
813
1248
|
const sid = nodeStateId(model, mode, n);
|
|
814
|
-
const show = nodeVisible(model, mode, n);
|
|
1249
|
+
const show = nodeVisible(model, mode, n, ctx.collapsed);
|
|
815
1250
|
const bucket = bucketOf(model, sid);
|
|
816
1251
|
const cors = (isImproved(model, mode, n) ? 1 : 0) + (n.upgrade && bucket !== "good" ? 1 : 0);
|
|
817
1252
|
const fresh = isNew(model, mode, n);
|
|
818
|
-
d.className = "node st-" + cssId(sid) + (n.kind === "chip" ? " ref" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : "") + (n.kind === "placeholder" ? " placeholder" : "") + (show ? "" : " off") + (fresh ? " isnew" : "") + (cors ? cors > 1 ? " hascor2" : " hascor" : "") + (ctx.selected && canonOf(ctx.nodes, n.id) === ctx.selected ? " sel" : "");
|
|
1253
|
+
d.className = "node st-" + cssId(sid) + (n.kind === "chip" ? " ref" : "") + (n.kind === "gateway" ? " gateway" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : "") + (n.kind === "placeholder" ? " placeholder" : "") + (show ? "" : " off") + (fresh ? " isnew" : "") + (cors ? cors > 1 ? " hascor2" : " hascor" : "") + (ctx.selected && canonOf(ctx.nodes, n.id) === ctx.selected ? " sel" : "") + (metricBadge(n.metrics) ? " has-metrics" : "");
|
|
819
1254
|
d.innerHTML = nodeHtml(ctx, n);
|
|
820
1255
|
d.setAttribute("aria-hidden", show ? "false" : "true");
|
|
821
1256
|
d.tabIndex = show ? 0 : -1;
|
|
822
1257
|
const word = model.states[sid]?.word ?? sid;
|
|
823
1258
|
const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
|
|
824
|
-
d.setAttribute("aria-label", n.kind === "chip" ? `${n.title}: a reference to the node in the ${lane} lane. Press Enter to select it.` : `${n.title}. ${n.subtitle ?? model.columns[n.col]?.title ?? ""}, ${word}. Press Enter to inspect.`);
|
|
1259
|
+
d.setAttribute("aria-label", n.kind === "chip" ? `${n.title}: a reference to the node in the ${lane} lane. Press Enter to select it.` : `${n.title}. ${n.subtitle ?? model.columns[n.col ?? 0]?.title ?? ""}, ${word}. Press Enter to inspect.`);
|
|
825
1260
|
if (fromSwitch && fresh && !ctx.reduced) {
|
|
826
|
-
d.style.transitionDelay = n.col * 60 + "ms";
|
|
1261
|
+
d.style.transitionDelay = (n.col ?? 0) * 60 + "ms";
|
|
827
1262
|
d.classList.add("pulse");
|
|
828
1263
|
window.setTimeout(() => {
|
|
829
1264
|
d.style.transitionDelay = "";
|
|
@@ -836,8 +1271,24 @@
|
|
|
836
1271
|
ctx.links.forEach((l, i) => {
|
|
837
1272
|
const p = ctx.edgeEls[i];
|
|
838
1273
|
if (!p) return;
|
|
839
|
-
const
|
|
840
|
-
|
|
1274
|
+
const edge = effectiveById.get(l.id);
|
|
1275
|
+
const vis = !!edge;
|
|
1276
|
+
const a = edge && ctx.geom.nodes[edge.from], b = edge && ctx.geom.nodes[edge.to];
|
|
1277
|
+
const loop = edge?.kind === "loop-back";
|
|
1278
|
+
if (a && b) {
|
|
1279
|
+
p.dataset.a = edge.from;
|
|
1280
|
+
p.dataset.b = edge.to;
|
|
1281
|
+
const channel = ctx.geom.loops[edge.id]?.channelY ?? Math.max(a.y + a.h, b.y + b.h) + 10;
|
|
1282
|
+
p.setAttribute("d", loop ? loopRoute(ctx, edge.from, edge.to, a, b, channel) : elbow(a.x + a.w, a.y + a.h / 2, b.x - 1, b.y + b.h / 2, a.x + a.w + DEFAULTS.gap / 2));
|
|
1283
|
+
const lab = ctx.labelEls[i];
|
|
1284
|
+
if (lab) {
|
|
1285
|
+
lab.dataset.a = edge.from;
|
|
1286
|
+
lab.dataset.b = edge.to;
|
|
1287
|
+
lab.style.left = (loop ? (a.x + a.w / 2 + b.x + b.w / 2) / 2 : Math.abs(b.y + b.h / 2 - a.y - a.h / 2) < 1 ? (a.x + a.w + b.x) / 2 : a.x + a.w + DEFAULTS.gap / 2) + "px";
|
|
1288
|
+
lab.style.top = (loop ? channel : (a.y + a.h / 2 + b.y + b.h / 2) / 2) + "px";
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
p.setAttribute("class", "edge st-" + cssId(edge?.state ?? linkStateId(model, mode, l)) + (loop ? " loop" : "") + (vis ? "" : " off"));
|
|
841
1292
|
ctx.labelEls[i]?.classList.toggle("off", !vis);
|
|
842
1293
|
});
|
|
843
1294
|
for (const l of model.lanes) {
|
|
@@ -960,6 +1411,7 @@
|
|
|
960
1411
|
}
|
|
961
1412
|
function placeHeaders() {
|
|
962
1413
|
const cols = els.cols;
|
|
1414
|
+
cols.style.display = model.columns.length ? "" : "none";
|
|
963
1415
|
if (!cols.firstChild) {
|
|
964
1416
|
model.columns.forEach((c, i2) => {
|
|
965
1417
|
const h = el("div", "hd");
|
|
@@ -1165,7 +1617,13 @@
|
|
|
1165
1617
|
} else if (k === "n" || k === "N") ctx.fn.toggleNotes();
|
|
1166
1618
|
else if (k === "v" || k === "V") setTool("select");
|
|
1167
1619
|
else if (k === "h" || k === "H") setTool("hand");
|
|
1168
|
-
else if (k === "
|
|
1620
|
+
else if (k === "e" || k === "E") {
|
|
1621
|
+
const selected = ctx.selected && ctx.nodes.get(ctx.selected);
|
|
1622
|
+
if (selected) {
|
|
1623
|
+
const group = selected.kind === "group" ? selected : ctx.model.nodes.find((n) => n.kind === "group" && n.contains?.includes(selected.id));
|
|
1624
|
+
if (group) ctx.fn.toggleGroup(group.id);
|
|
1625
|
+
}
|
|
1626
|
+
} else if (k === "+" || k === "=") zoomBy(1.25);
|
|
1169
1627
|
else if (k === "-" || k === "_") zoomBy(1 / 1.25);
|
|
1170
1628
|
else if (k === "0") {
|
|
1171
1629
|
ctx.userMoved = false;
|
|
@@ -1219,6 +1677,7 @@
|
|
|
1219
1677
|
ctx.fn.zoomToPath();
|
|
1220
1678
|
}));
|
|
1221
1679
|
box.querySelectorAll("[data-act]").forEach((b) => b.addEventListener("click", () => ctx.fn.simulate(b.dataset.act ?? "")));
|
|
1680
|
+
box.querySelectorAll("[data-toggle-group]").forEach((b) => b.addEventListener("click", () => ctx.fn.toggleGroup(b.dataset.toggleGroup ?? "")));
|
|
1222
1681
|
box.querySelector("#simReset")?.addEventListener("click", () => {
|
|
1223
1682
|
ctx.fn.clearSim();
|
|
1224
1683
|
ctx.fn.renderInspector();
|
|
@@ -1227,7 +1686,7 @@
|
|
|
1227
1686
|
box.querySelector("#zPath")?.addEventListener("click", () => ctx.fn.zoomToPath());
|
|
1228
1687
|
box.querySelector("#zBand")?.addEventListener("click", () => {
|
|
1229
1688
|
const n = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
|
|
1230
|
-
if (n) ctx.fn.zoomToBand(n.lane);
|
|
1689
|
+
if (n?.lane) ctx.fn.zoomToBand(n.lane);
|
|
1231
1690
|
});
|
|
1232
1691
|
}
|
|
1233
1692
|
function renderInspector() {
|
|
@@ -1235,7 +1694,7 @@
|
|
|
1235
1694
|
const sel = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
|
|
1236
1695
|
const head = `<div class="insp-h"><span>Inspector</span><button type="button" id="hideInsp" aria-label="Close the inspector">Close \u2715</button></div>`;
|
|
1237
1696
|
if (!sel) {
|
|
1238
|
-
const found = ctx.query && !ctx.dismissed ? model.nodes.filter((n) => nodeVisible(model, ctx.mode, n) && matches(ctx, n)) : [];
|
|
1697
|
+
const found = ctx.query && !ctx.dismissed ? model.nodes.filter((n) => nodeVisible(model, ctx.mode, n, ctx.collapsed) && matches(ctx, n)) : [];
|
|
1239
1698
|
const open = found.length > 0;
|
|
1240
1699
|
box.classList.toggle("open", open);
|
|
1241
1700
|
ws.classList.toggle("inspopen", open);
|
|
@@ -1244,7 +1703,7 @@
|
|
|
1244
1703
|
const rows = found.map((f) => {
|
|
1245
1704
|
const b = bucketOf(model, nodeStateId(model, ctx.mode, f));
|
|
1246
1705
|
const hits = (f.members ?? []).filter((m) => m.toLowerCase().includes(ctx.query)).length;
|
|
1247
|
-
return `<button type="button" class="dr" data-g="${esc(f.id)}"><span class="dbar" style="background:var(--${BUCKET_COLOR[b]})"></span><span class="dtx"><span class="dn">${esc(f.title)}</span><span class="dk">${hits} match \xB7 ${esc(model.columns[f.col]?.title ?? "")}</span></span></button>`;
|
|
1706
|
+
return `<button type="button" class="dr" data-g="${esc(f.id)}"><span class="dbar" style="background:var(--${BUCKET_COLOR[b]})"></span><span class="dtx"><span class="dn">${esc(f.title)}</span><span class="dk">${hits} match \xB7 ${esc(model.columns[f.col ?? 0]?.title ?? "")}</span></span></button>`;
|
|
1248
1707
|
}).join("");
|
|
1249
1708
|
box.innerHTML = head + `<div class="insp-b"><div class="insp-sec"><b>${found.length}</b> group${found.length === 1 ? "" : "s"} match \u201C${esc(ctx.query)}\u201D</div><div class="drill">${rows}</div></div>`;
|
|
1250
1709
|
wire(box);
|
|
@@ -1273,10 +1732,23 @@
|
|
|
1273
1732
|
const pills = `<span class="pill ${PILL[st?.tone ?? "muted"]}">${esc(st?.word ?? sid)}</span>` + (isImproved(model, ctx.mode, sel) ? '<span class="pill info">gap filled</span>' : "") + (isNew(model, ctx.mode, sel) ? '<span class="pill muted">new in this view</span>' : "");
|
|
1274
1733
|
const actions = (model.inspector?.actions ?? []).map((a) => `<button type="button" class="danger" data-act="${esc(a.id)}">\u25B6 ${esc(a.label)}</button>`).join("");
|
|
1275
1734
|
const members = sel.members ?? [];
|
|
1735
|
+
const owner = groupFor(model, sel.id);
|
|
1736
|
+
const foldAction = sel.kind === "group" && sel.contains?.length && ctx.collapsed.has(sel.id) ? `<button type="button" data-toggle-group="${esc(sel.id)}">Expand group</button>` : owner && !ctx.collapsed.has(owner.id) ? `<button type="button" data-toggle-group="${esc(owner.id)}">Collapse into ${esc(owner.title)}</button>` : "";
|
|
1737
|
+
const exits = sel.kind === "gateway" ? effectiveLinks(model, ctx.mode, ctx.collapsed, ctx.nodes).filter((l) => l.from === sel.id).map((l) => {
|
|
1738
|
+
const target = ctx.nodes.get(l.to);
|
|
1739
|
+
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>`;
|
|
1740
|
+
}).join("") : "";
|
|
1741
|
+
const metric = sel.metrics;
|
|
1742
|
+
const series = metric?.series ?? [];
|
|
1743
|
+
const points = sparklinePoints(series, 300, 40);
|
|
1744
|
+
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>` : "";
|
|
1276
1745
|
box.innerHTML = head + `<div class="counts">${counts}</div>
|
|
1277
|
-
<div class="insp-b"><div class="insp-sec"><p class="nname">${esc(sel.title)}</p><div class="nkind">${esc(model.columns[sel.col]?.title ?? "")} \xB7 ${esc(lane?.title ?? sel.lane)}</div><div class="pillrow">${pills}</div>${sel.subtitle ? `<p class="note">${esc(sel.subtitle)}</p>` : ""}</div>
|
|
1746
|
+
<div class="insp-b"><div class="insp-sec"><p class="nname">${esc(sel.title)}</p><div class="nkind">${esc(model.columns[sel.col ?? 0]?.title ?? "")} \xB7 ${esc(lane?.title ?? sel.lane)}</div><div class="pillrow">${pills}</div>${sel.subtitle ? `<p class="note">${esc(sel.subtitle)}</p>` : ""}</div>
|
|
1278
1747
|
<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>
|
|
1748
|
+
${metricHtml}
|
|
1749
|
+
${sel.kind === "gateway" ? `<div class="insp-sec"><b>Exits</b><div class="drill">${exits}</div></div>` : ""}
|
|
1279
1750
|
${members.length ? `<div class="insp-sec"><b>Members</b> (${Math.max(members.length, sel.weight ?? 0)})<input id="mq" type="search" placeholder="Filter ${members.length} members\u2026" aria-label="Filter members"><div class="drill" id="mlist"></div><p class="drill-hint" id="mcount"></p></div>` : `<div class="insp-sec"><p class="note">No member names are recorded for this group.</p></div>`}
|
|
1751
|
+
${foldAction ? `<div class="acts">${foldAction}</div>` : ""}
|
|
1280
1752
|
<div class="acts"><button type="button" id="zPath">Zoom to path</button><button type="button" id="zBand">Go to lane</button></div>
|
|
1281
1753
|
${actions ? `<div class="acts">${actions}<button type="button" id="simReset"${ctx.simActive ? "" : " hidden"}>Reset</button></div>` : ""}</div>`;
|
|
1282
1754
|
wire(box);
|
|
@@ -1312,7 +1784,7 @@
|
|
|
1312
1784
|
const label = (k) => model.meta.labels?.[k] ?? DEFAULT_LABELS[k] ?? k;
|
|
1313
1785
|
const visible = (id) => {
|
|
1314
1786
|
const n = ctx.nodes.get(id);
|
|
1315
|
-
return !!n && nodeVisible(model, ctx.mode, n);
|
|
1787
|
+
return !!n && nodeVisible(model, ctx.mode, n, ctx.collapsed);
|
|
1316
1788
|
};
|
|
1317
1789
|
function countUp(node, to, ms = 700) {
|
|
1318
1790
|
if (ctx.reduced) {
|
|
@@ -1329,6 +1801,7 @@
|
|
|
1329
1801
|
}
|
|
1330
1802
|
function buildStrip() {
|
|
1331
1803
|
const box = els.strip;
|
|
1804
|
+
box.style.display = model.columns.length ? "" : "none";
|
|
1332
1805
|
box.style.gridTemplateColumns = `repeat(${Math.max(1, model.columns.length)}, minmax(0, 1fr))`;
|
|
1333
1806
|
if (!box.firstChild) {
|
|
1334
1807
|
box.innerHTML = model.columns.map((c) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
@@ -1338,9 +1811,9 @@
|
|
|
1338
1811
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1339
1812
|
for (const n of model.nodes) {
|
|
1340
1813
|
if (n.col !== i || n.kind === "chip") continue;
|
|
1814
|
+
if (!visible(n.id)) continue;
|
|
1341
1815
|
const w = Math.max(1, n.weight ?? 1);
|
|
1342
1816
|
tot += w;
|
|
1343
|
-
if (!visible(n.id)) continue;
|
|
1344
1817
|
const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
|
|
1345
1818
|
if (b === "good") good += w;
|
|
1346
1819
|
else if (b === "neutral") neutral += w;
|
|
@@ -1360,9 +1833,8 @@
|
|
|
1360
1833
|
function buildTiles() {
|
|
1361
1834
|
const k = els.tiles;
|
|
1362
1835
|
let good = 0, neutral = 0;
|
|
1363
|
-
for (const l of ctx.
|
|
1364
|
-
|
|
1365
|
-
const b = bucketOf(model, linkStateId(model, ctx.mode, l));
|
|
1836
|
+
for (const l of effectiveLinks(model, ctx.mode, ctx.collapsed, ctx.nodes)) {
|
|
1837
|
+
const b = bucketOf(model, l.state);
|
|
1366
1838
|
if (b === "good") good++;
|
|
1367
1839
|
else if (b === "neutral") neutral++;
|
|
1368
1840
|
}
|
|
@@ -1418,7 +1890,7 @@
|
|
|
1418
1890
|
const rows = model.nodes.filter((n) => visible(n.id) && n.kind !== "chip").map((n) => {
|
|
1419
1891
|
const sid = nodeStateId(model, ctx.mode, n);
|
|
1420
1892
|
const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
|
|
1421
|
-
return `<tr><th scope="row">${esc(model
|
|
1893
|
+
return `<tr><th scope="row">${esc(columnTitle(model, n))}</th><td>${esc(lane)}</td><td>${esc(n.title)}</td><td>${esc(model.states[sid]?.word ?? sid)}</td><td>${Math.max(1, n.weight ?? 1)}</td><td>${esc(n.subtitle ?? "")}</td><td>${esc(n.upgrade ?? "")}</td></tr>`;
|
|
1422
1894
|
}).join("");
|
|
1423
1895
|
els.table.innerHTML = `<thead><tr><th scope="col">Column</th><th scope="col">Lane</th><th scope="col">Group</th><th scope="col">State</th><th scope="col">Members</th><th scope="col">Detail</th><th scope="col">Can improve</th></tr></thead><tbody>${rows}</tbody>`;
|
|
1424
1896
|
}
|
|
@@ -1460,6 +1932,10 @@
|
|
|
1460
1932
|
ctx.fn.renderInspector();
|
|
1461
1933
|
}
|
|
1462
1934
|
});
|
|
1935
|
+
on(ctx, els.hops, "change", () => {
|
|
1936
|
+
ctx.hops = Number(els.hops.value);
|
|
1937
|
+
ctx.fn.trace(ctx.selected);
|
|
1938
|
+
});
|
|
1463
1939
|
on(ctx, els.q, "keydown", (e) => {
|
|
1464
1940
|
if (e.key !== "Enter" || !ctx.query) return;
|
|
1465
1941
|
const ids = model.nodes.filter((x) => visible(x.id) && matches(ctx, x)).map((x) => x.id);
|
|
@@ -1499,6 +1975,11 @@
|
|
|
1499
1975
|
const a = anchorOf(n.anchor);
|
|
1500
1976
|
if (!a) continue;
|
|
1501
1977
|
let r = a.getBoundingClientRect();
|
|
1978
|
+
if (!r.width && !r.height) {
|
|
1979
|
+
let d = a.closest("details");
|
|
1980
|
+
while (d && d.open) d = d.parentElement?.closest("details") ?? null;
|
|
1981
|
+
if (d) r = d.getBoundingClientRect();
|
|
1982
|
+
}
|
|
1502
1983
|
if (!r.width && !r.height) continue;
|
|
1503
1984
|
if (a.id === "inspector" && !a.classList.contains("open")) {
|
|
1504
1985
|
const vr = els.vp.getBoundingClientRect();
|
|
@@ -1588,6 +2069,365 @@
|
|
|
1588
2069
|
ctx.fn.placeMarkers = placeMarkers;
|
|
1589
2070
|
}
|
|
1590
2071
|
|
|
2072
|
+
// src/playback.ts
|
|
2073
|
+
function planScenario(model, scenario) {
|
|
2074
|
+
const source = scenario.steps ?? [];
|
|
2075
|
+
const mode = source.some((s) => s?.at !== void 0) ? "elapsed" : "none";
|
|
2076
|
+
const start = source[0]?.at ?? 0;
|
|
2077
|
+
const scale = mode === "elapsed" ? Math.max(1, ...source.slice(1).map((s, i) => {
|
|
2078
|
+
const gap = ((s?.at ?? 0) - (source[i]?.at ?? 0)) * 1e3;
|
|
2079
|
+
return gap > 0 ? (source[i]?.dur ?? 900) / gap : 1;
|
|
2080
|
+
})) : 1;
|
|
2081
|
+
const steps = [];
|
|
2082
|
+
for (let i = 0; i < source.length; i++) {
|
|
2083
|
+
const s = source[i];
|
|
2084
|
+
if (!s || typeof s !== "object") continue;
|
|
2085
|
+
const link = model.links.find((l) => l.id === s.link);
|
|
2086
|
+
if (!link) continue;
|
|
2087
|
+
const from = model.nodes.find((n) => n.id === link.from);
|
|
2088
|
+
const to = model.nodes.find((n) => n.id === link.to);
|
|
2089
|
+
const dur = Number.isFinite(s.dur) && (s.dur ?? 0) > 0 ? s.dur : 900;
|
|
2090
|
+
const proposed = mode === "elapsed" ? Math.max(0, ((s.at ?? 0) - start) * 1e3 * scale) : 0;
|
|
2091
|
+
const at = steps.length === 0 ? 0 : Math.max(proposed, steps.at(-1).at + steps.at(-1).dur);
|
|
2092
|
+
steps.push({ link: s.link, from: from?.ref ?? link.from, to: to?.ref ?? link.to, at, dur, caption: s.caption ?? `${from?.title ?? link.from} to ${to?.title ?? link.to}`, index: i });
|
|
2093
|
+
}
|
|
2094
|
+
return { steps, mode, totalMs: steps.length ? steps.at(-1).at + steps.at(-1).dur : 0 };
|
|
2095
|
+
}
|
|
2096
|
+
function positionAt(plan, tMs) {
|
|
2097
|
+
if (!plan.steps.length) return { stepIndex: -1, progress: 0, done: true };
|
|
2098
|
+
const t = Number.isNaN(tMs) ? 0 : Math.max(0, tMs);
|
|
2099
|
+
if (t >= plan.totalMs) return { stepIndex: plan.steps.length - 1, progress: 1, done: true };
|
|
2100
|
+
let i = plan.steps.length - 1;
|
|
2101
|
+
while (i > 0 && t < plan.steps[i].at) i--;
|
|
2102
|
+
const s = plan.steps[i];
|
|
2103
|
+
return { stepIndex: i, progress: Math.max(0, Math.min(1, (t - s.at) / s.dur)), done: false };
|
|
2104
|
+
}
|
|
2105
|
+
function slaState(elapsedSec, sla) {
|
|
2106
|
+
if (elapsedSec >= sla) return "breach";
|
|
2107
|
+
return elapsedSec >= sla * 0.8 ? "warn" : "ok";
|
|
2108
|
+
}
|
|
2109
|
+
function formatElapsed(sec) {
|
|
2110
|
+
const n = Math.max(0, Math.round(Number.isFinite(sec) ? sec : 0));
|
|
2111
|
+
const m = Math.floor(n / 60), s = String(n % 60).padStart(2, "0");
|
|
2112
|
+
return n < 3600 ? `${m}:${s}` : `${Math.floor(n / 3600)}:${String(m % 60).padStart(2, "0")}:${s}`;
|
|
2113
|
+
}
|
|
2114
|
+
function exitsOf(model, nodeId) {
|
|
2115
|
+
const canon = (id) => model.nodes.find((n) => n.id === id)?.ref ?? id;
|
|
2116
|
+
return model.links.filter((l) => canon(l.from) === canon(nodeId));
|
|
2117
|
+
}
|
|
2118
|
+
function suggestedExit(exits, nextLink) {
|
|
2119
|
+
return exits.find((l) => l.id === nextLink);
|
|
2120
|
+
}
|
|
2121
|
+
function buildManualRoute(model, choiceLink) {
|
|
2122
|
+
const route = [choiceLink];
|
|
2123
|
+
const seen = /* @__PURE__ */ new Set([choiceLink.id]);
|
|
2124
|
+
let at = choiceLink.to;
|
|
2125
|
+
while (exitsOf(model, at).length === 1) {
|
|
2126
|
+
const next = exitsOf(model, at)[0];
|
|
2127
|
+
if (seen.has(next.id)) break;
|
|
2128
|
+
route.push(next);
|
|
2129
|
+
seen.add(next.id);
|
|
2130
|
+
at = next.to;
|
|
2131
|
+
}
|
|
2132
|
+
return route;
|
|
2133
|
+
}
|
|
2134
|
+
function extendScenarioWithRoute(scenario, plan, stepIndex, route) {
|
|
2135
|
+
const isElapsed = plan.mode === "elapsed" || scenario.steps.some((s) => s?.at !== void 0);
|
|
2136
|
+
const prefix = scenario.steps.slice(0, stepIndex + 1).map((s) => ({ ...s }));
|
|
2137
|
+
for (let i = prefix.length; i <= stepIndex && i < plan.steps.length; i++) {
|
|
2138
|
+
const ps = plan.steps[i];
|
|
2139
|
+
prefix.push({ link: ps.link, dur: ps.dur, caption: ps.caption });
|
|
2140
|
+
}
|
|
2141
|
+
let lastAt = 0;
|
|
2142
|
+
if (isElapsed) {
|
|
2143
|
+
const lastWithAt = [...prefix].reverse().find((s) => s?.at !== void 0);
|
|
2144
|
+
lastAt = lastWithAt?.at ?? 0;
|
|
2145
|
+
}
|
|
2146
|
+
const newSteps = route.map((l) => {
|
|
2147
|
+
const step = { link: l.id };
|
|
2148
|
+
if (isElapsed) {
|
|
2149
|
+
lastAt += 1;
|
|
2150
|
+
step.at = lastAt;
|
|
2151
|
+
}
|
|
2152
|
+
return step;
|
|
2153
|
+
});
|
|
2154
|
+
return {
|
|
2155
|
+
...scenario,
|
|
2156
|
+
steps: [...prefix, ...newSteps]
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/playbackui.ts
|
|
2161
|
+
function install7(ctx) {
|
|
2162
|
+
const e = ctx.els;
|
|
2163
|
+
const scenarios = ctx.model.scenarios ?? [];
|
|
2164
|
+
e.playbar.hidden = !scenarios.length;
|
|
2165
|
+
e.pbScenario.replaceChildren(...scenarios.map((s) => {
|
|
2166
|
+
const o = document.createElement("option");
|
|
2167
|
+
o.value = s.id;
|
|
2168
|
+
o.textContent = s.label;
|
|
2169
|
+
return o;
|
|
2170
|
+
}));
|
|
2171
|
+
let scenario = scenarios[0];
|
|
2172
|
+
let plan = scenario ? planScenario(ctx.model, scenario) : { steps: [], mode: "none", totalMs: 0 };
|
|
2173
|
+
let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
|
|
2174
|
+
let markedEdges = [];
|
|
2175
|
+
let markedNodes = [];
|
|
2176
|
+
const token = sv("circle", { id: "token", class: "token", r: "6" });
|
|
2177
|
+
e.layer.appendChild(token);
|
|
2178
|
+
const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
|
|
2179
|
+
const nodeOf = (id) => ctx.nodeEls.get(id);
|
|
2180
|
+
const clearMarks = () => {
|
|
2181
|
+
for (const p of markedEdges) p.classList.remove("playing", "visited");
|
|
2182
|
+
for (const n of markedNodes) n.classList.remove("playing", "visited");
|
|
2183
|
+
markedEdges = [];
|
|
2184
|
+
markedNodes = [];
|
|
2185
|
+
};
|
|
2186
|
+
function stop() {
|
|
2187
|
+
playing = false;
|
|
2188
|
+
cancelAnimationFrame(frame);
|
|
2189
|
+
clearTimeout(timer);
|
|
2190
|
+
document.body.classList.remove("playing");
|
|
2191
|
+
e.pbPlay.setAttribute("aria-label", "Play");
|
|
2192
|
+
e.pbPlay.textContent = "\u25B6";
|
|
2193
|
+
}
|
|
2194
|
+
function pause() {
|
|
2195
|
+
if (!playing) return;
|
|
2196
|
+
stop();
|
|
2197
|
+
const p = positionAt(plan, t);
|
|
2198
|
+
draw(p.stepIndex, ctx.reduced ? 1 : p.progress);
|
|
2199
|
+
}
|
|
2200
|
+
function choices(step2) {
|
|
2201
|
+
if (playing || !step2) {
|
|
2202
|
+
if (choicesKey !== "") {
|
|
2203
|
+
e.pbChoices.replaceChildren();
|
|
2204
|
+
choicesKey = "";
|
|
2205
|
+
}
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const key = `${scenario?.id ?? ""}|${idx}|${plan.steps.length}|${plan.steps[idx + 1]?.link ?? ""}`;
|
|
2209
|
+
if (key === choicesKey) return;
|
|
2210
|
+
choicesKey = key;
|
|
2211
|
+
e.pbChoices.replaceChildren();
|
|
2212
|
+
const exits = exitsOf(ctx.model, step2.to);
|
|
2213
|
+
if (exits.length < 2) return;
|
|
2214
|
+
const suggested = suggestedExit(exits, plan.steps[idx + 1]?.link);
|
|
2215
|
+
for (const link of exits) {
|
|
2216
|
+
const b = document.createElement("button");
|
|
2217
|
+
b.type = "button";
|
|
2218
|
+
b.textContent = link.label ?? ctx.nodes.get(link.to)?.title ?? link.to;
|
|
2219
|
+
b.dataset.link = link.id;
|
|
2220
|
+
if (link.id === suggested?.id) b.classList.add("suggested");
|
|
2221
|
+
b.addEventListener("click", () => {
|
|
2222
|
+
if (!scenario) return;
|
|
2223
|
+
const route = buildManualRoute(ctx.model, link);
|
|
2224
|
+
scenario = extendScenarioWithRoute(scenario, plan, idx, route);
|
|
2225
|
+
plan = planScenario(ctx.model, scenario);
|
|
2226
|
+
t = plan.steps[idx + 1]?.at ?? 0;
|
|
2227
|
+
play();
|
|
2228
|
+
});
|
|
2229
|
+
e.pbChoices.appendChild(b);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function draw(stepIndex, progress) {
|
|
2233
|
+
const step2 = plan.steps[stepIndex];
|
|
2234
|
+
clearMarks();
|
|
2235
|
+
if (!step2) {
|
|
2236
|
+
token.style.display = "none";
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
token.style.display = "";
|
|
2240
|
+
for (let i = 0; i < stepIndex; i++) {
|
|
2241
|
+
const s = plan.steps[i];
|
|
2242
|
+
const p = pathOf(s.link);
|
|
2243
|
+
if (p) {
|
|
2244
|
+
p.classList.add("visited");
|
|
2245
|
+
markedEdges.push(p);
|
|
2246
|
+
}
|
|
2247
|
+
const n = nodeOf(s.to);
|
|
2248
|
+
if (n) {
|
|
2249
|
+
n.classList.add("visited");
|
|
2250
|
+
markedNodes.push(n);
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
const path = pathOf(step2.link);
|
|
2254
|
+
if (path) {
|
|
2255
|
+
path.classList.add("playing");
|
|
2256
|
+
markedEdges.push(path);
|
|
2257
|
+
}
|
|
2258
|
+
const node = nodeOf(step2.to);
|
|
2259
|
+
if (node) {
|
|
2260
|
+
node.classList.add("playing");
|
|
2261
|
+
markedNodes.push(node);
|
|
2262
|
+
}
|
|
2263
|
+
if (path && !path.classList.contains("off")) {
|
|
2264
|
+
const p = path.getPointAtLength(path.getTotalLength() * progress);
|
|
2265
|
+
token.setAttribute("cx", String(p.x));
|
|
2266
|
+
token.setAttribute("cy", String(p.y));
|
|
2267
|
+
} else token.style.display = "none";
|
|
2268
|
+
e.pbCaption.textContent = step2.caption;
|
|
2269
|
+
e.pbScrub.value = String(plan.totalMs ? Math.round(t / plan.totalMs * 1e3) : 0);
|
|
2270
|
+
e.pbScrub.setAttribute("aria-valuetext", `Step ${stepIndex + 1} of ${plan.steps.length}: ${step2.caption}`);
|
|
2271
|
+
e.pbCounter.className = "";
|
|
2272
|
+
e.pbCounter.hidden = plan.mode === "none";
|
|
2273
|
+
if (plan.mode === "elapsed") {
|
|
2274
|
+
const sec = scenario?.steps[step2.index]?.at ?? 0;
|
|
2275
|
+
const sla = ctx.nodes.get(step2.to)?.sla;
|
|
2276
|
+
e.pbCounter.textContent = sla === void 0 ? formatElapsed(sec) : `${formatElapsed(sec)} / ${formatElapsed(sla)}`;
|
|
2277
|
+
if (sla !== void 0) e.pbCounter.classList.add(slaState(sec, sla));
|
|
2278
|
+
}
|
|
2279
|
+
if (idx !== stepIndex) ctx.fn.announce(step2.caption);
|
|
2280
|
+
idx = stepIndex;
|
|
2281
|
+
choices(step2);
|
|
2282
|
+
}
|
|
2283
|
+
function rewind() {
|
|
2284
|
+
stop();
|
|
2285
|
+
t = 0;
|
|
2286
|
+
idx = -1;
|
|
2287
|
+
clearMarks();
|
|
2288
|
+
choicesKey = "";
|
|
2289
|
+
e.pbCaption.textContent = "";
|
|
2290
|
+
e.pbCounter.hidden = true;
|
|
2291
|
+
e.pbCounter.className = "";
|
|
2292
|
+
e.pbChoices.replaceChildren();
|
|
2293
|
+
token.style.display = "none";
|
|
2294
|
+
e.pbScrub.value = "0";
|
|
2295
|
+
e.pbScrub.setAttribute("aria-valuetext", "");
|
|
2296
|
+
}
|
|
2297
|
+
function select(id) {
|
|
2298
|
+
scenario = scenarios.find((s) => s.id === id) ?? scenarios[0];
|
|
2299
|
+
plan = scenario ? planScenario(ctx.model, scenario) : { steps: [], mode: "none", totalMs: 0 };
|
|
2300
|
+
e.pbScenario.value = scenario?.id ?? "";
|
|
2301
|
+
rewind();
|
|
2302
|
+
}
|
|
2303
|
+
function seek(value) {
|
|
2304
|
+
stop();
|
|
2305
|
+
t = Math.max(0, Math.min(plan.totalMs, Number.isFinite(value) ? value : 0));
|
|
2306
|
+
const p = positionAt(plan, t);
|
|
2307
|
+
draw(p.stepIndex, ctx.reduced ? 1 : p.progress);
|
|
2308
|
+
}
|
|
2309
|
+
function tick(now) {
|
|
2310
|
+
if (!playing) return;
|
|
2311
|
+
if (!stamp) stamp = now;
|
|
2312
|
+
const delta = (now - stamp) * Number(e.pbSpeed.value || 1);
|
|
2313
|
+
stamp = now;
|
|
2314
|
+
t = Math.min(plan.totalMs, t + delta);
|
|
2315
|
+
const p = positionAt(plan, t);
|
|
2316
|
+
if (p.done) {
|
|
2317
|
+
stop();
|
|
2318
|
+
draw(p.stepIndex, 1);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
draw(p.stepIndex, p.progress);
|
|
2322
|
+
frame = requestAnimationFrame(tick);
|
|
2323
|
+
}
|
|
2324
|
+
function reducedStep() {
|
|
2325
|
+
if (!playing) return;
|
|
2326
|
+
const next = Math.min(plan.steps.length - 1, idx + 1);
|
|
2327
|
+
if (next < 0) {
|
|
2328
|
+
stop();
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
t = plan.steps[next].at + plan.steps[next].dur;
|
|
2332
|
+
if (next === plan.steps.length - 1) stop();
|
|
2333
|
+
draw(next, 1);
|
|
2334
|
+
if (playing) timer = window.setTimeout(reducedStep, 1e3 / Number(e.pbSpeed.value || 1));
|
|
2335
|
+
}
|
|
2336
|
+
function play(id) {
|
|
2337
|
+
if (id && id !== scenario?.id) select(id);
|
|
2338
|
+
stop();
|
|
2339
|
+
if (!plan.steps.length) return;
|
|
2340
|
+
if (t >= plan.totalMs) {
|
|
2341
|
+
t = 0;
|
|
2342
|
+
idx = -1;
|
|
2343
|
+
}
|
|
2344
|
+
playing = true;
|
|
2345
|
+
document.body.classList.add("playing");
|
|
2346
|
+
e.pbPlay.setAttribute("aria-label", "Pause");
|
|
2347
|
+
e.pbPlay.textContent = "\u275A\u275A";
|
|
2348
|
+
e.pbChoices.replaceChildren();
|
|
2349
|
+
choicesKey = "";
|
|
2350
|
+
if (ctx.reduced) reducedStep();
|
|
2351
|
+
else {
|
|
2352
|
+
stamp = 0;
|
|
2353
|
+
frame = requestAnimationFrame(tick);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
function step() {
|
|
2357
|
+
stop();
|
|
2358
|
+
const next = Math.min(idx + 1, plan.steps.length - 1);
|
|
2359
|
+
if (next < 0) return;
|
|
2360
|
+
t = plan.steps[next].at + plan.steps[next].dur;
|
|
2361
|
+
draw(next, 1);
|
|
2362
|
+
}
|
|
2363
|
+
on(ctx, e.pbScenario, "change", () => select(e.pbScenario.value));
|
|
2364
|
+
on(ctx, e.pbPlay, "click", () => playing ? pause() : play());
|
|
2365
|
+
on(ctx, e.pbStep, "click", step);
|
|
2366
|
+
on(ctx, e.pbReset, "click", () => rewind());
|
|
2367
|
+
on(ctx, e.pbScrub, "input", () => seek(Number(e.pbScrub.value) / 1e3 * plan.totalMs));
|
|
2368
|
+
on(ctx, document, "keydown", (event) => {
|
|
2369
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.target instanceof HTMLElement && (event.target.isContentEditable || /INPUT|TEXTAREA|SELECT/.test(event.target.tagName))) return;
|
|
2370
|
+
if (event.key.toLowerCase() === "p") {
|
|
2371
|
+
event.preventDefault();
|
|
2372
|
+
playing ? pause() : play();
|
|
2373
|
+
}
|
|
2374
|
+
if (event.key === ".") {
|
|
2375
|
+
event.preventDefault();
|
|
2376
|
+
step();
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
ctx.disposers.push(() => {
|
|
2380
|
+
stop();
|
|
2381
|
+
token.remove();
|
|
2382
|
+
});
|
|
2383
|
+
select(scenario?.id ?? "");
|
|
2384
|
+
return {
|
|
2385
|
+
play,
|
|
2386
|
+
pause,
|
|
2387
|
+
step,
|
|
2388
|
+
seek,
|
|
2389
|
+
playback: () => ({ scenario: scenario?.id ?? null, step: idx, playing, t }),
|
|
2390
|
+
restore: (state) => {
|
|
2391
|
+
if (!state.scenario || !scenarios.some((s) => s.id === state.scenario)) return;
|
|
2392
|
+
select(state.scenario);
|
|
2393
|
+
const current = plan.steps[state.step];
|
|
2394
|
+
if (!current) return;
|
|
2395
|
+
t = Math.max(current.at, Math.min(current.at + current.dur, state.t));
|
|
2396
|
+
draw(state.step, ctx.reduced ? 1 : current.dur > 0 ? (t - current.at) / current.dur : 1);
|
|
2397
|
+
if (state.playing) play();
|
|
2398
|
+
},
|
|
2399
|
+
reset: () => rewind()
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
// src/diff.ts
|
|
2404
|
+
var fields = ["state", "stateIn", "title", "subtitle", "weight", "metrics", "visibleIn", "lane", "sla"];
|
|
2405
|
+
function stableStringify(val) {
|
|
2406
|
+
if (val === void 0) return "undefined";
|
|
2407
|
+
if (val === null) return "null";
|
|
2408
|
+
if (typeof val === "number") return Number.isFinite(val) ? JSON.stringify(val) : String(val);
|
|
2409
|
+
if (typeof val === "bigint") return String(val);
|
|
2410
|
+
if (typeof val === "object") {
|
|
2411
|
+
if (Array.isArray(val)) return `[${val.map(stableStringify).join(",")}]`;
|
|
2412
|
+
const rec = val;
|
|
2413
|
+
const keys = Object.keys(rec).filter((k) => rec[k] !== void 0).sort();
|
|
2414
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(rec[k])}`).join(",")}}`;
|
|
2415
|
+
}
|
|
2416
|
+
return JSON.stringify(val) ?? "undefined";
|
|
2417
|
+
}
|
|
2418
|
+
function diffModels(a, b) {
|
|
2419
|
+
const old = new Map(a.nodes.map((n) => [n.id, n]));
|
|
2420
|
+
const now = new Map(b.nodes.map((n) => [n.id, n]));
|
|
2421
|
+
return {
|
|
2422
|
+
added: b.nodes.filter((n) => !old.has(n.id)).map((n) => n.id),
|
|
2423
|
+
removed: a.nodes.filter((n) => !now.has(n.id)).map((n) => n.id),
|
|
2424
|
+
changed: b.nodes.filter((n) => {
|
|
2425
|
+
const prev = old.get(n.id);
|
|
2426
|
+
return prev && fields.some((k) => stableStringify(prev[k]) !== stableStringify(n[k]));
|
|
2427
|
+
}).map((n) => n.id)
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
|
|
1591
2431
|
// src/engine.ts
|
|
1592
2432
|
var IDS = {
|
|
1593
2433
|
vp: "viewport",
|
|
@@ -1606,6 +2446,7 @@
|
|
|
1606
2446
|
ledger: "ledger",
|
|
1607
2447
|
table: "tbl",
|
|
1608
2448
|
q: "q",
|
|
2449
|
+
hops: "hops",
|
|
1609
2450
|
matchinfo: "matchinfo",
|
|
1610
2451
|
modeSwitch: "modeSwitch",
|
|
1611
2452
|
drawer: "drawer",
|
|
@@ -1617,7 +2458,18 @@
|
|
|
1617
2458
|
title: "title",
|
|
1618
2459
|
readat: "readat",
|
|
1619
2460
|
help: "help",
|
|
1620
|
-
footer: "footer"
|
|
2461
|
+
footer: "footer",
|
|
2462
|
+
playbar: "playbar",
|
|
2463
|
+
pbScenario: "pbScenario",
|
|
2464
|
+
pbPlay: "pbPlay",
|
|
2465
|
+
pbStep: "pbStep",
|
|
2466
|
+
pbReset: "pbReset",
|
|
2467
|
+
pbSpeed: "pbSpeed",
|
|
2468
|
+
pbScrub: "pbScrub",
|
|
2469
|
+
pbCaption: "pbCaption",
|
|
2470
|
+
pbCounter: "pbCounter",
|
|
2471
|
+
pbChoices: "pbChoices",
|
|
2472
|
+
banner: "banner"
|
|
1621
2473
|
};
|
|
1622
2474
|
function findEls(root) {
|
|
1623
2475
|
const out = {};
|
|
@@ -1632,6 +2484,7 @@
|
|
|
1632
2484
|
if (!model || !Array.isArray(model.columns) || !Array.isArray(model.lanes) || !Array.isArray(model.nodes) || !Array.isArray(model.links) || !model.states) {
|
|
1633
2485
|
throw new Error("flow-studio: the model needs columns, lanes, states, nodes and links");
|
|
1634
2486
|
}
|
|
2487
|
+
if (model.layout === "auto") model = autoLayout(model);
|
|
1635
2488
|
const els = findEls(root);
|
|
1636
2489
|
const issues = validateModel(model);
|
|
1637
2490
|
const errors = issues.filter((i) => i.level === "error");
|
|
@@ -1642,17 +2495,20 @@
|
|
|
1642
2495
|
model,
|
|
1643
2496
|
root,
|
|
1644
2497
|
els,
|
|
2498
|
+
onSelect: opts.onSelect,
|
|
1645
2499
|
notes: opts.notes && opts.notes.length ? opts.notes : model.notes ?? [],
|
|
1646
2500
|
nodes: new Map(model.nodes.map((n) => [n.id, n])),
|
|
1647
2501
|
links: model.links,
|
|
1648
|
-
geom: { xs: [], nodes: {}, lanes: {}, bounds: { x: 0, y: 0, w: 1, h: 1 }, world: { w: 1, h: 1 } },
|
|
2502
|
+
geom: { xs: [], nodes: {}, lanes: {}, loops: {}, bounds: { x: 0, y: 0, w: 1, h: 1 }, world: { w: 1, h: 1 } },
|
|
1649
2503
|
view: { x: 0, y: 0, k: 1 },
|
|
1650
2504
|
reduced: rm.matches,
|
|
1651
|
-
mode: defaultView(model),
|
|
1652
|
-
|
|
2505
|
+
mode: opts.restore && (model.views?.some((v) => v.id === opts.restore.mode) || !model.views?.length && opts.restore.mode === "all") ? opts.restore.mode : defaultView(model),
|
|
2506
|
+
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),
|
|
2507
|
+
selected: opts.restore?.selected && model.nodes.some((n) => n.id === opts.restore.selected) ? opts.restore.selected : null,
|
|
1653
2508
|
hl: null,
|
|
1654
|
-
query: "",
|
|
1655
|
-
|
|
2509
|
+
query: opts.restore?.query ?? "",
|
|
2510
|
+
hops: opts.restore?.hops ?? 0,
|
|
2511
|
+
filter: opts.restore?.filter ?? null,
|
|
1656
2512
|
tool: "select",
|
|
1657
2513
|
spaceDown: false,
|
|
1658
2514
|
userMoved: false,
|
|
@@ -1686,12 +2542,38 @@
|
|
|
1686
2542
|
install4(ctx);
|
|
1687
2543
|
install5(ctx);
|
|
1688
2544
|
install6(ctx);
|
|
1689
|
-
|
|
1690
|
-
|
|
2545
|
+
els.q.value = ctx.query;
|
|
2546
|
+
els.hops.value = String(ctx.hops);
|
|
1691
2547
|
ctx.fn.buildWorld();
|
|
1692
2548
|
ctx.fn.paint(false);
|
|
1693
|
-
ctx
|
|
1694
|
-
|
|
2549
|
+
const playback = install7(ctx);
|
|
2550
|
+
if (opts.restore) {
|
|
2551
|
+
ctx.userMoved = true;
|
|
2552
|
+
ctx.fn.setView(opts.restore.view, false);
|
|
2553
|
+
playback.restore(opts.restore.playback);
|
|
2554
|
+
if (opts.restore.inspectorOpen) {
|
|
2555
|
+
els.inspector.classList.add("open");
|
|
2556
|
+
els.workspace.classList.add("inspopen");
|
|
2557
|
+
els.inspector.setAttribute("aria-hidden", "false");
|
|
2558
|
+
ctx.fn.renderInspector();
|
|
2559
|
+
} else {
|
|
2560
|
+
els.inspector.classList.remove("open");
|
|
2561
|
+
els.workspace.classList.remove("inspopen");
|
|
2562
|
+
els.inspector.setAttribute("aria-hidden", "true");
|
|
2563
|
+
}
|
|
2564
|
+
} else {
|
|
2565
|
+
document.body.classList.add("intro");
|
|
2566
|
+
const introTimer = window.setTimeout(() => document.body.classList.remove("intro"), 2100);
|
|
2567
|
+
ctx.disposers.push(() => {
|
|
2568
|
+
window.clearTimeout(introTimer);
|
|
2569
|
+
document.body.classList.remove("intro");
|
|
2570
|
+
});
|
|
2571
|
+
ctx.fn.fitWidth(false);
|
|
2572
|
+
const fitTimer = window.setTimeout(() => {
|
|
2573
|
+
if (!ctx.userMoved) ctx.fn.fitWidth(false);
|
|
2574
|
+
}, 120);
|
|
2575
|
+
ctx.disposers.push(() => window.clearTimeout(fitTimer));
|
|
2576
|
+
}
|
|
1695
2577
|
const api = {
|
|
1696
2578
|
model,
|
|
1697
2579
|
geometry: () => ctx.geom,
|
|
@@ -1710,17 +2592,48 @@
|
|
|
1710
2592
|
ctx.fn.clearSim();
|
|
1711
2593
|
ctx.fn.renderInspector();
|
|
1712
2594
|
},
|
|
2595
|
+
play: (id) => playback.play(id),
|
|
2596
|
+
pause: () => playback.pause(),
|
|
2597
|
+
step: () => playback.step(),
|
|
2598
|
+
seek: (t) => playback.seek(t),
|
|
2599
|
+
playback: () => playback.playback(),
|
|
2600
|
+
update: (nextModel, updateOpts) => {
|
|
2601
|
+
const prepared = nextModel.layout === "auto" ? autoLayout(nextModel) : nextModel;
|
|
2602
|
+
const nextIssues = validateModel(prepared);
|
|
2603
|
+
const count = nextIssues.filter((i) => i.level === "error").length;
|
|
2604
|
+
if (count) {
|
|
2605
|
+
els.banner.textContent = `Update failed: ${count} errors. Showing the last good graph.`;
|
|
2606
|
+
els.banner.hidden = false;
|
|
2607
|
+
return { ok: false, issues: nextIssues };
|
|
2608
|
+
}
|
|
2609
|
+
const changes = diffModels(ctx.model, prepared);
|
|
2610
|
+
const saved = { view: { ...ctx.view }, mode: ctx.mode, selected: ctx.selected, inspectorOpen: els.inspector.classList.contains("open"), query: ctx.query, hops: ctx.hops, filter: ctx.filter, collapsed: new Set(ctx.collapsed), knownGroups: new Set(ctx.model.nodes.filter((n) => n.kind === "group").map((n) => n.id)), playback: playback.playback() };
|
|
2611
|
+
api.destroy();
|
|
2612
|
+
const replacement = mount(root, prepared, { notes: opts.notes, onSelect: opts.onSelect, restore: saved });
|
|
2613
|
+
notifySelection(saved.selected, replacement.selected(), opts.onSelect);
|
|
2614
|
+
Object.assign(api, replacement);
|
|
2615
|
+
window.FlowStudio = api;
|
|
2616
|
+
els.banner.hidden = true;
|
|
2617
|
+
els.banner.textContent = "";
|
|
2618
|
+
if (updateOpts?.animate !== false && !ctx.reduced) {
|
|
2619
|
+
for (const id of changes.changed) root.querySelector(`#n_${CSS.escape(id)}`)?.classList.add("updated");
|
|
2620
|
+
for (const id of changes.added) root.querySelector(`#n_${CSS.escape(id)}`)?.classList.add("added");
|
|
2621
|
+
}
|
|
2622
|
+
return { ok: true, issues: nextIssues };
|
|
2623
|
+
},
|
|
1713
2624
|
destroy: () => {
|
|
1714
2625
|
ctx.fn.clearSim();
|
|
1715
2626
|
ctx.disposers.forEach((d) => d());
|
|
1716
2627
|
ctx.disposers = [];
|
|
1717
2628
|
style?.remove();
|
|
1718
|
-
if (window.FlowStudio === api) delete window.FlowStudio;
|
|
2629
|
+
if (window.FlowStudio === api || window.FlowStudio?.model === api.model) delete window.FlowStudio;
|
|
1719
2630
|
}
|
|
1720
2631
|
};
|
|
1721
2632
|
window.FlowStudio = api;
|
|
1722
2633
|
return api;
|
|
1723
2634
|
}
|
|
2635
|
+
|
|
2636
|
+
// src/page.ts
|
|
1724
2637
|
function auto() {
|
|
1725
2638
|
const m = document.getElementById("flow-model");
|
|
1726
2639
|
if (!m) return;
|