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