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