@caddis/cli 0.4.43 → 0.4.45
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/scripts/caddis_file_lock.py +48 -7
- package/bundles/antigravity-plugin/scripts/caddis_lanes.py +27 -0
- package/bundles/antigravity-plugin/scripts/caddis_todo.py +7 -5
- package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +15 -3
- package/bundles/antigravity-plugin/scripts/claude-oss.sh +12 -1
- package/bundles/antigravity-plugin/skills/_registry.md +5 -4
- package/bundles/antigravity-plugin/skills/adopt-app/SKILL.md +6 -1
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +7 -1
- package/bundles/antigravity-plugin/skills/decide/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/README.md +53 -0
- package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +11 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/hostile.json +1783 -0
- package/bundles/antigravity-plugin/skills/flow-studio/examples/lineage.json +1778 -0
- package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +123 -0
- package/bundles/antigravity-plugin/skills/flow-studio/package.json +11 -0
- package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +79 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +55 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +37 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +388 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +26 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +75 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +64 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.mjs +24 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.test.mjs +29 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +38 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +171 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +231 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +118 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +142 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +108 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +240 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +69 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +57 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +107 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +165 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +92 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +155 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +13 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +11 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +88 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +35 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +66 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +44 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +157 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +20 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +22 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.test.ts +55 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.ts +45 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +1744 -0
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +61 -0
- package/bundles/antigravity-plugin/skills/flow-studio/tsconfig.json +17 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +8 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +6 -1
- package/bundles/antigravity-plugin/skills/port-to-github/SKILL.md +6 -1
- package/bundles/antigravity-plugin/skills/tech-explainer/SKILL.md +5 -6
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin-extras/skills/adopt-rbac/SKILL.md +26 -0
- package/bundles/antigravity-plugin-extras/skills/case-management/SKILL.md +24 -0
- package/bundles/codex/.codex/skills/devops/adopt-app/SKILL.md +6 -1
- package/bundles/codex/.codex/skills/devops/port-to-github/SKILL.md +6 -1
- package/bundles/codex/.codex/skills/docs/tech-explainer/SKILL.md +5 -6
- package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +53 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +103 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +11 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/hostile.json +1783 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/examples/lineage.json +1778 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +123 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/package.json +11 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +79 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +55 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +37 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +388 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +26 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +75 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +64 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.mjs +24 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.test.mjs +29 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +38 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +171 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +231 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +118 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +142 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +108 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +240 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +69 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +57 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +107 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +165 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +92 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +155 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +13 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +11 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +88 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +35 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +66 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +44 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +157 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +20 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +22 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.test.ts +55 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.ts +45 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +1744 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +61 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/tsconfig.json +17 -0
- package/bundles/codex/.codex/skills/media/particle-art/SKILL.md +6 -1
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +7 -1
- package/bundles/manifest.json +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,1744 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
flow-studio page (generated). Do not edit this file by hand.
|
|
4
|
+
Source: .github/skills/frontend/flow-studio (src/*.ts, src/styles.css, templates/engine.shell.html).
|
|
5
|
+
Rebuild the engine: node --experimental-strip-types scripts/build-engine.mjs
|
|
6
|
+
Build a page: node --experimental-strip-types scripts/build.mjs model.json out.html
|
|
7
|
+
The model and the notes are JSON in the two script tags at the end of the body.
|
|
8
|
+
-->
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13
|
+
<title>flow-studio</title>
|
|
14
|
+
<style id="state-css"></style>
|
|
15
|
+
<style>:root {
|
|
16
|
+
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
17
|
+
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
18
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035);
|
|
19
|
+
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
20
|
+
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
21
|
+
}
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:root:not([data-theme="light"]) {
|
|
24
|
+
--bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
|
|
25
|
+
--accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
|
|
26
|
+
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
|
|
27
|
+
--warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
:root[data-theme="dark"] {
|
|
31
|
+
--bg:#0d1117; --panel:#161b22; --panel2:#0d1117; --edge:#30363d; --rule:#2b323a; --fg:#e6edf3; --muted:#9198a1;
|
|
32
|
+
--accent:#4493f8; --accent-solid:#1f6feb; --ok:#3fb950; --warn:#d29922; --crit:#f85149; --glow:rgba(68,147,248,.35);
|
|
33
|
+
--up:#6fcbe6; --down:#d0a0ee; --shadow:0 1px 0 rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.45); --grid:rgba(255,255,255,.04);
|
|
34
|
+
--warn-bg:rgba(210,153,34,.14); --ok-bg:rgba(63,185,80,.12); --crit-bg:rgba(248,81,73,.12); --info-bg:rgba(68,147,248,.14);
|
|
35
|
+
}
|
|
36
|
+
* { box-sizing:border-box; }
|
|
37
|
+
body { margin:0; color:var(--fg); font:14px/1.45 Inter, "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
38
|
+
background:radial-gradient(900px 380px at 8% -10%, var(--glow), transparent 60%), var(--bg); }
|
|
39
|
+
.mono { font-family:var(--mono); }
|
|
40
|
+
header.top { display:flex; align-items:center; gap:16px; padding:10px 20px; border-bottom:1px solid var(--edge); background:color-mix(in srgb, var(--panel) 88%, transparent); backdrop-filter:blur(10px); position:sticky; top:0; z-index:70; }
|
|
41
|
+
.brand { font-weight:700; display:flex; align-items:center; gap:8px; }
|
|
42
|
+
.brand i { width:10px; height:10px; border-radius:50%; background:var(--ok); animation:beat 2.6s infinite; }
|
|
43
|
+
.crumb { color:var(--muted); } .spacer { flex:1; }
|
|
44
|
+
button, select, input { font:inherit; color:var(--fg); background:var(--panel); border:1px solid var(--edge); border-radius:6px; padding:4px 11px; }
|
|
45
|
+
button { cursor:pointer; transition:border-color .15s, box-shadow .15s, background .15s; }
|
|
46
|
+
button:hover { border-color:var(--accent); box-shadow:0 0 0 3px var(--glow); }
|
|
47
|
+
button:focus-visible, input:focus-visible, .node:focus-visible, summary:focus-visible, #viewport:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
|
|
48
|
+
button[aria-pressed="true"].tb, #notesBtn[aria-pressed="true"], button.seg[aria-selected="true"] { background:var(--accent-solid); border-color:var(--accent-solid); color:#fff; }
|
|
49
|
+
main { width:100%; padding:12px 14px 20px; }
|
|
50
|
+
.titlerow { display:flex; align-items:baseline; gap:16px; flex-wrap:wrap; }
|
|
51
|
+
h1 { margin:0 0 2px; font-size:20px; padding-bottom:5px; border-bottom:3px solid var(--accent); display:inline-block; }
|
|
52
|
+
.readline { color:var(--muted); font-size:12px; }
|
|
53
|
+
p.help { margin:0 0 8px; color:var(--muted); max-width:130ch; }
|
|
54
|
+
.summary { display:grid; grid-template-columns:minmax(0,1.6fr) minmax(0,1fr); gap:10px; margin:0 0 8px; }
|
|
55
|
+
.layerstrip { display:grid; grid-template-columns:repeat(9,minmax(0,1fr)); gap:1px; background:var(--rule); border:1px solid var(--rule); }
|
|
56
|
+
.lcell { background:var(--panel); padding:7px 9px; min-width:0; }
|
|
57
|
+
.lcell .ln { font-size:10.5px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
58
|
+
.lcell .lb { display:flex; height:5px; margin:5px 0 4px; background:var(--panel2); border:1px solid var(--rule); overflow:hidden; }
|
|
59
|
+
.lcell .lb i { display:block; height:100%; transition:width .9s cubic-bezier(.2,.8,.2,1); } .lb .good { background:var(--ok); } .lb .neutral { background:var(--muted); } .lb .gap { background:var(--warn); }
|
|
60
|
+
.lcell .lv { font-size:12px; font-weight:600; font-variant-numeric:tabular-nums; }
|
|
61
|
+
.tiles { display:grid; grid-template-columns:repeat(4,1fr); gap:1px; background:var(--rule); border:1px solid var(--rule); }
|
|
62
|
+
.tile { background:var(--panel); padding:5px 11px; text-align:left; border:0; border-radius:0; min-width:0; color:var(--fg); }
|
|
63
|
+
.tile .tn { font-size:20px; font-weight:600; line-height:1.1; font-variant-numeric:tabular-nums; display:block; }
|
|
64
|
+
.tile .tl { font-size:10.5px; color:var(--muted); display:block; margin-top:2px; }
|
|
65
|
+
.tile.ok .tn { color:var(--ok); } .tile.warn .tn { color:var(--warn); }
|
|
66
|
+
button.tile:hover { background:var(--panel2); } button.tile[aria-pressed="true"] { background:var(--panel2); box-shadow:inset 0 -2px 0 var(--accent); }
|
|
67
|
+
.toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:0 0 6px; }
|
|
68
|
+
.tabs { display:flex; gap:0; margin-right:6px; } .tabs button { border-radius:0; } .tabs button:first-child { border-radius:6px 0 0 6px; } .tabs button:last-child { border-radius:0 6px 6px 0; }
|
|
69
|
+
.tabs button[aria-selected="true"] { border-color:var(--accent); font-weight:600; } .tabs button:disabled { opacity:.5; cursor:default; }
|
|
70
|
+
.seggrp { display:inline-flex; border:1px solid var(--edge); border-radius:6px; overflow:hidden; } .seggrp button.seg { border:0; border-radius:0; }
|
|
71
|
+
.toolbar input[type="search"] { min-width:280px; }
|
|
72
|
+
.workspace { display:block; height:max(540px, calc(100vh - 232px)); border:1px solid var(--edge); background:var(--panel); border-radius:12px; overflow:hidden; box-shadow:var(--shadow); }
|
|
73
|
+
.canvaswrap { position:relative; display:flex; flex-direction:column; min-width:0; min-height:0; height:100%; }
|
|
74
|
+
#viewport { position:relative; flex:1; min-height:0; overflow:hidden; cursor:grab; touch-action:none; user-select:none;
|
|
75
|
+
background:radial-gradient(760px 380px at 4% 0%, var(--glow), transparent 65%), radial-gradient(640px 320px at 98% 100%, color-mix(in srgb, var(--ok) 16%, transparent), transparent 70%), linear-gradient(var(--grid) 1px, transparent 1px) 0 0/28px 28px, linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0/28px 28px, var(--bg); }
|
|
76
|
+
#viewport.panning { cursor:grabbing; } #viewport.hand .node { cursor:grab; }
|
|
77
|
+
#world { position:absolute; left:0; top:0; transform-origin:0 0; will-change:transform; }
|
|
78
|
+
#viewport.anim #world, #viewport.anim .hd, #viewport.anim .ph { transition:transform .28s cubic-bezier(.2,.8,.2,1), left .28s cubic-bezier(.2,.8,.2,1), width .28s cubic-bezier(.2,.8,.2,1); }
|
|
79
|
+
#edges { position:absolute; left:0; top:0; overflow:visible; pointer-events:none; }
|
|
80
|
+
.band { position:absolute; border:1px solid var(--edge); border-radius:10px; background:color-mix(in srgb, var(--panel) 55%, transparent); transition:top .35s, height .35s, opacity .35s; pointer-events:none; }
|
|
81
|
+
.band.gap { border:1px dashed var(--warn); background:repeating-linear-gradient(135deg, transparent 0 9px, color-mix(in srgb, var(--warn) 7%, transparent) 9px 10px); }
|
|
82
|
+
.band.off { opacity:0; }
|
|
83
|
+
.bandlabel { position:absolute; left:12px; top:7px; display:flex; gap:8px; align-items:baseline; transform-origin:0 0; transform:scale(var(--inv,1)); white-space:nowrap; pointer-events:auto; cursor:pointer; z-index:3; }
|
|
84
|
+
.bandlabel b { font-size:13px; } .bandlabel .bd { font-size:10.5px; font-weight:700; padding:0 6px; border:1px solid currentColor; color:var(--muted); letter-spacing:.03em; text-transform:uppercase; }
|
|
85
|
+
.bandlabel .bd.t-ok { color:var(--ok); } .bandlabel .bd.t-warn { color:var(--warn); } .bandlabel .bn { font-size:11.5px; color:var(--muted); }
|
|
86
|
+
.node { position:absolute; background:var(--panel); border:1px solid var(--edge); border-radius:8px; padding:5px 10px 7px; box-shadow:var(--shadow); transition:opacity .35s, transform .35s, box-shadow .2s, border-color .2s; cursor:pointer; z-index:2; }
|
|
87
|
+
.node.wide { overflow:hidden; }
|
|
88
|
+
.node:hover { border-color:var(--accent); box-shadow:0 0 0 3px var(--glow), var(--shadow); }
|
|
89
|
+
.node .t { display:flex; gap:4px; align-items:flex-start; font-weight:600; line-height:1.25; font-size:13px; }
|
|
90
|
+
.node .t .name { overflow-wrap:anywhere; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
|
91
|
+
.node .s { color:var(--muted); font-size:12px; line-height:1.3; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
92
|
+
.ev { font-size:10.5px; font-weight:700; letter-spacing:.03em; }
|
|
93
|
+
.node.wide .t { font-weight:500; } .node.wide .s { white-space:normal; }
|
|
94
|
+
.node.ref { padding:4px 9px; border-style:dashed; box-shadow:none; background:color-mix(in srgb, var(--panel) 70%, transparent); } .node.ref .t { font-size:12px; } .node.ref .s { font-size:11px; }
|
|
95
|
+
.node.sel { outline:2px solid var(--accent); outline-offset:2px; } .node.dim { opacity:.28; } .node.off { opacity:0; transform:translateY(8px); pointer-events:none; }
|
|
96
|
+
.node .bar { position:absolute; left:0; right:0; bottom:0; display:flex; height:4px; overflow:hidden; border-radius:0 0 7px 7px; background:var(--panel2); } .node .bar i { display:block; height:100%; } .bar .good { background:var(--ok); } .bar .neutral { background:var(--muted); } .bar .gap { background:var(--warn); }
|
|
97
|
+
.newdot { display:none; width:7px; height:7px; border-radius:50%; background:var(--accent); margin:5px 4px 0 0; flex:none; } body.alt .node.isnew .newdot { display:inline-block; }
|
|
98
|
+
body.alt .node.isnew.pulse { animation:newring 1.8s ease-out 1; }
|
|
99
|
+
.cor { position:absolute; top:5px; width:17px; height:17px; border-radius:50%; font-size:10px; display:flex; align-items:center; justify-content:center; font-weight:700; }
|
|
100
|
+
.cor.up { right:6px; background:var(--info-bg); color:var(--accent); } .cor.meas { right:6px; background:var(--ok-bg); color:var(--ok); } .cor.up + .cor.meas { right:27px; }
|
|
101
|
+
.node.hascor .t { padding-right:24px; } .node.hascor2 .t { padding-right:44px; }
|
|
102
|
+
#viewport.lod1 .node .s, #viewport.lod1 .bandlabel .bn { display:none; } #viewport.lod2 .node .cor, #viewport.lod2 .node.ref .s { display:none; }
|
|
103
|
+
path.edge { fill:none; stroke-width:1.8; transition:opacity .35s; vector-effect:non-scaling-stroke; }
|
|
104
|
+
path.edge.hot { stroke:var(--accent); stroke-width:2.4; opacity:1; stroke-dasharray:6 8; animation:flowhot .9s linear infinite; filter:drop-shadow(0 0 5px var(--accent)); }
|
|
105
|
+
path.edge.dim { opacity:.07; filter:none; } path.edge.off { opacity:0; }
|
|
106
|
+
.elabel { position:absolute; font-size:11px; padding:0 6px; border-radius:9px; background:var(--panel); border:1px solid var(--edge); color:var(--muted); white-space:nowrap; transform:translate(-50%,-50%); opacity:0; transition:opacity .35s; z-index:4; } .elabel.show { opacity:1; } .elabel.off { display:none; }
|
|
107
|
+
#cols { position:absolute; left:0; right:0; top:0; height:44px; pointer-events:none; z-index:20; background:linear-gradient(var(--bg) 62%, transparent); }
|
|
108
|
+
#cols .ph { position:absolute; top:3px; height:16px; font-size:10px; letter-spacing:.14em; font-weight:700; color:var(--accent); text-transform:uppercase; display:flex; align-items:center; gap:6px; overflow:hidden; }
|
|
109
|
+
#cols .ph::before { content:""; height:1px; background:var(--accent); width:8px; opacity:.7; } #cols .ph::after { content:"›"; margin-left:auto; padding-right:3px; opacity:.8; flex:none; }
|
|
110
|
+
#cols .ph span { flex:none; } #cols .ph i { flex:1; height:1px; background:var(--accent); opacity:.4; }
|
|
111
|
+
#cols .hd { position:absolute; top:22px; font-size:10.5px; letter-spacing:.08em; font-weight:700; color:var(--muted); text-transform:uppercase; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } #cols .hd b { color:var(--fg); }
|
|
112
|
+
#tools { position:absolute; left:10px; top:58px; z-index:30; display:flex; flex-direction:column; background:var(--panel); border:1px solid var(--edge); box-shadow:var(--shadow); }
|
|
113
|
+
#tools button { width:34px; height:32px; padding:0; border:0; border-radius:0; border-bottom:1px solid var(--rule); display:flex; align-items:center; justify-content:center; background:transparent; }
|
|
114
|
+
#tools button:last-child { border-bottom:0; } #tools button:hover { background:var(--panel2); color:var(--accent); box-shadow:none; } #tools button[aria-pressed="true"] { background:var(--panel2); box-shadow:inset 3px 0 0 var(--accent); color:var(--accent); }
|
|
115
|
+
#tools svg { width:17px; height:17px; stroke:currentColor; fill:none; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; } #tools .sep { height:1px; background:var(--edge); margin:0; }
|
|
116
|
+
#zoomLabel { position:absolute; left:10px; top:auto; z-index:30; font:11px var(--mono); color:var(--muted); background:var(--panel); border:1px solid var(--edge); padding:1px 6px; }
|
|
117
|
+
.keyline { display:flex; flex-wrap:wrap; align-items:center; gap:6px 15px; padding:6px 13px; border-top:1px solid var(--rule); background:var(--panel2); font-size:10.5px; color:var(--muted); } .keyline svg { vertical-align:middle; margin-right:6px; }
|
|
118
|
+
.insp { display:flex; flex-direction:column; background:var(--panel); min-height:0; min-width:0; font-family:var(--mono); position:absolute; top:0; right:0; bottom:0; width:340px; z-index:40; border-left:1px solid var(--edge); box-shadow:-14px 0 34px rgba(0,0,0,.16), -1px 0 0 var(--glow); transform:translateX(104%); visibility:hidden; transition:transform .3s cubic-bezier(.2,.8,.2,1), visibility 0s .3s; }
|
|
119
|
+
.insp.open { transform:none; visibility:visible; transition:transform .3s cubic-bezier(.2,.8,.2,1), visibility 0s; }
|
|
120
|
+
.insp-h { display:flex; align-items:center; justify-content:space-between; padding:0 12px 0 15px; height:39px; border-bottom:1px solid var(--rule); background:var(--panel2); font-size:10.5px; color:var(--muted); letter-spacing:.02em; flex:none; }
|
|
121
|
+
.insp-h button { padding:0 8px; height:24px; font-size:11px; }
|
|
122
|
+
.counts { display:flex; border-bottom:1px solid var(--rule); flex:none; }
|
|
123
|
+
.counts .cbtn { flex:1; padding:9px 6px 8px; border:0; border-right:1px solid var(--rule); border-radius:0; text-align:center; background:transparent; }
|
|
124
|
+
.counts .cbtn:last-child { border-right:0; } .counts .cbtn:hover { background:var(--panel2); } .counts .cbtn[aria-pressed="true"] { background:var(--panel2); box-shadow:inset 0 -2px 0 var(--accent); } .counts .cbtn:disabled { opacity:.45; cursor:default; }
|
|
125
|
+
.counts .cn { font-size:20px; font-weight:600; display:block; line-height:1.1; font-variant-numeric:tabular-nums; } .cn--up { color:var(--up); } .cn--down { color:var(--down); } .counts .cl { font-size:9.5px; color:var(--muted); display:block; margin-top:3px; }
|
|
126
|
+
.insp-b { padding:14px 15px 18px; min-width:0; flex:1 1 auto; overflow:auto; min-height:0; }
|
|
127
|
+
.insp-sec { font-size:10.5px; color:var(--muted); margin:15px 0 0; padding-top:10px; border-top:1px solid var(--rule); } .insp-sec:first-child { margin-top:0; padding-top:0; border-top:0; } .insp-sec b { color:var(--fg); font-weight:600; }
|
|
128
|
+
.nname { font-size:15px; font-weight:600; line-height:1.25; word-break:break-word; margin:0 0 3px; } .nkind { font-size:10.5px; color:var(--muted); margin-bottom:9px; }
|
|
129
|
+
.pillrow { display:flex; gap:6px; margin-bottom:11px; flex-wrap:wrap; } .pill { font-size:9.5px; letter-spacing:.04em; text-transform:uppercase; padding:2px 7px; border:1px solid currentColor; }
|
|
130
|
+
.pill.ok { color:var(--ok); } .pill.warn { color:var(--warn); } .pill.info { color:var(--accent); } .pill.muted { color:var(--muted); }
|
|
131
|
+
.note { margin:0 0 12px; font-size:12.5px; color:var(--muted); line-height:1.5; } .note code, .insp code { font-family:var(--mono); font-size:11.5px; background:var(--panel2); padding:1px 4px; }
|
|
132
|
+
.meta { width:100%; border-collapse:collapse; font-size:11.5px; } .meta th, .meta td { text-align:left; padding:4px 0; border-top:1px solid var(--rule); vertical-align:top; } .meta th { color:var(--muted); font-weight:400; width:42%; }
|
|
133
|
+
.drill { margin:0 -15px; } .drill .dr { display:flex; align-items:baseline; gap:8px; width:100%; text-align:left; border:0; border-top:1px solid var(--rule); border-radius:0; background:transparent; padding:6px 15px; font:inherit; }
|
|
134
|
+
button.dr:hover { background:var(--panel2); } .drill .dbar { width:3px; align-self:stretch; flex:none; } .drill .dtx { min-width:0; flex:1; } .drill .dn { font-size:12.5px; display:block; word-break:break-word; line-height:1.3; } .drill .dk { font-size:10px; color:var(--muted); display:block; margin-top:1px; }
|
|
135
|
+
.drill-hint { font-size:10.5px; color:var(--muted); margin:10px 0 0; } .insp input[type="search"] { width:100%; margin:4px 0 6px; font-size:12px; }
|
|
136
|
+
.insp .acts { display:flex; gap:6px; margin:10px 0 0; flex-wrap:wrap; } .insp .acts button { font-size:11.5px; }
|
|
137
|
+
.ledger { margin:12px 0 0; border-top:1px solid var(--rule); padding-top:10px; } .ledger h4 { margin:0 0 6px; font-size:10.5px; color:var(--muted); font-weight:400; } .ledger p { margin:0 0 4px; font-size:12px; color:var(--muted); line-height:1.55; } .ledger p b { color:var(--fg); }
|
|
138
|
+
.marker { position:absolute; z-index:90; width:24px; height:24px; border-radius:50%; background:#8250df; color:#fff; font-weight:700; font-size:12px; display:flex; align-items:center; justify-content:center; box-shadow:0 0 0 3px rgba(130,80,223,.3), var(--shadow); cursor:pointer; border:0; padding:0; } .marker:hover, .marker.on { box-shadow:0 0 0 5px rgba(130,80,223,.45); transform:scale(1.1); }
|
|
139
|
+
.anchored { outline:2px dashed #8250df; outline-offset:3px; }
|
|
140
|
+
#drawer { position:fixed; top:56px; left:0; bottom:0; width:380px; background:var(--panel); border-right:1px solid var(--edge); box-shadow:var(--shadow); z-index:80; transform:translateX(-100%); transition:transform .3s cubic-bezier(.2,.8,.2,1); overflow:auto; padding:14px 16px; } body.notes #drawer { transform:none; } main { transition:margin-left .3s cubic-bezier(.2,.8,.2,1); } body.notes main { margin-left:380px; } @media (max-width:1100px) { body.notes main { margin-left:0; } }
|
|
141
|
+
#drawer h2 { margin:0 0 8px; font-size:15px; } #drawer .ncard { border:1px solid var(--edge); padding:8px 10px; margin:8px 0; font-size:12.5px; } #drawer .ncard.on { border-color:#8250df; box-shadow:0 0 0 3px rgba(130,80,223,.25); }
|
|
142
|
+
#drawer .ncard b { display:flex; gap:8px; align-items:center; margin-bottom:2px; font-size:13px; } #drawer .ncard b span { background:#8250df; color:#fff; width:20px; height:20px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center; font-size:11px; flex:none; }
|
|
143
|
+
details.tableview, details.spec { margin:12px 0; background:var(--panel); border:1px solid var(--edge); padding:8px 14px; } details summary { cursor:pointer; color:var(--accent); font-weight:600; }
|
|
144
|
+
table.sources { width:100%; border-collapse:collapse; margin-top:8px; font-size:13px; } table.sources th, table.sources td { text-align:left; padding:5px 10px; border-bottom:1px solid var(--rule); vertical-align:top; } table.sources th { color:var(--muted); font-size:12px; text-transform:uppercase; letter-spacing:.05em; }
|
|
145
|
+
.spec h3 { margin:14px 0 4px; font-size:14px; } .spec h4 { margin:10px 0 2px; font-size:13px; } .spec ul { margin:4px 0 4px 18px; padding:0; } .spec li { margin:2px 0; } .spec .task { border:1px solid var(--edge); padding:6px 12px; margin:8px 0; }
|
|
146
|
+
.spec code { font-family:var(--mono); font-size:12px; background:var(--panel2); padding:0 4px; } .specbtns { display:flex; gap:8px; margin:8px 0; flex-wrap:wrap; }
|
|
147
|
+
footer { color:var(--muted); font-size:12.5px; padding:0 14px 28px; }
|
|
148
|
+
.sr { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; }
|
|
149
|
+
:fullscreen.workspace { height:100vh; } .workspace.fs { position:fixed; inset:0; z-index:95; height:100vh; }
|
|
150
|
+
button.danger { background:var(--crit); border-color:var(--crit); color:#fff; font-weight:600; } button.danger:hover { box-shadow:0 0 0 3px var(--crit-bg); }
|
|
151
|
+
.layerstrip, .tiles { box-shadow:var(--shadow); } .lcell:hover { background:var(--panel2); } button.tile:hover { box-shadow:inset 0 0 0 1px var(--accent), 0 0 0 3px var(--glow); position:relative; z-index:1; }
|
|
152
|
+
.bandlabel:hover b { color:var(--accent); }
|
|
153
|
+
.node:hover { transform:translateY(-2px); }
|
|
154
|
+
.node.lit { border-color:var(--accent); box-shadow:0 0 0 2px var(--glow), 0 0 18px var(--glow), var(--shadow); }
|
|
155
|
+
.node.sel { animation:selpulse 2.4s ease-out infinite; }
|
|
156
|
+
.node.fail { border-color:var(--crit); background:var(--crit-bg); box-shadow:0 0 0 2px var(--crit-bg), 0 0 20px color-mix(in srgb, var(--crit) 45%, transparent); animation:failflash .9s ease-out 1; } .node.fail .s { color:var(--crit); font-weight:600; }
|
|
157
|
+
path.trail { fill:none; stroke:var(--accent); vector-effect:none; stroke-width:calc(3.4px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 6px var(--accent)); opacity:.95; }
|
|
158
|
+
path.ripple { fill:none; stroke:var(--crit); vector-effect:none; stroke-width:calc(3.6px / var(--k, 1)); stroke-linecap:round; filter:drop-shadow(0 0 7px var(--crit)); }
|
|
159
|
+
.intro .band { animation:fadein .6s both; } .intro .node { animation:rise .55s both; animation-delay:calc(var(--c, 0) * 60ms + 150ms); } .intro #edges { animation:fadein 1s .5s both; }
|
|
160
|
+
.intro .lcell, .intro .tile { animation:rise .5s both; animation-delay:calc(var(--i, 0) * 55ms); }
|
|
161
|
+
#viewport:focus-visible { outline-offset:-2px; }
|
|
162
|
+
@keyframes fadein { from { opacity:0; } to { opacity:1; } }
|
|
163
|
+
@keyframes flowd { to { stroke-dashoffset:-26; } } @keyframes flowc { to { stroke-dashoffset:-27; } } @keyframes flowhot { to { stroke-dashoffset:-28; } }
|
|
164
|
+
@keyframes selpulse { 0% { box-shadow:0 0 0 0 color-mix(in srgb, var(--accent) 55%, transparent), 0 0 18px var(--glow); } 80%, 100% { box-shadow:0 0 0 14px transparent, 0 0 18px var(--glow); } }
|
|
165
|
+
@keyframes breathe { 0%, 100% { box-shadow:0 0 0 0 transparent, var(--shadow); } 50% { box-shadow:0 0 16px 1px color-mix(in srgb, var(--breath, var(--warn)) 40%, transparent), var(--shadow); } }
|
|
166
|
+
@keyframes failflash { 0% { box-shadow:0 0 0 0 color-mix(in srgb, var(--crit) 60%, transparent); } 100% { box-shadow:0 0 0 16px transparent; } }
|
|
167
|
+
@keyframes newring { 0% { box-shadow:0 0 0 0 var(--accent); } 100% { box-shadow:0 0 0 14px transparent; } }
|
|
168
|
+
@keyframes flow { to { stroke-dashoffset:-28; } } @keyframes rise { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } } @keyframes ring { to { box-shadow:0 0 0 14px transparent; } }
|
|
169
|
+
@keyframes beat { 0%{box-shadow:0 0 0 0 color-mix(in srgb, var(--ok) 60%, transparent);} 70%,100%{box-shadow:0 0 0 9px transparent;} }
|
|
170
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } path.edge.hot { stroke-dasharray:none; } }
|
|
171
|
+
@media (max-width: 1100px) { .summary { grid-template-columns:1fr; } .workspace { height:auto; } .workspace .canvaswrap { height:75vh; } .insp { width:min(340px, 92%); } #drawer { width:100%; } .layerstrip { grid-template-columns:repeat(3,1fr) !important; } }
|
|
172
|
+
</style>
|
|
173
|
+
</head>
|
|
174
|
+
<body>
|
|
175
|
+
<header class="top">
|
|
176
|
+
<span class="brand"><i></i>flow-studio</span>
|
|
177
|
+
<span class="spacer"></span>
|
|
178
|
+
<button id="notesBtn" type="button" aria-pressed="false">Implementation notes (N)</button>
|
|
179
|
+
<button id="theme" type="button" aria-label="Toggle light and dark">☀ / ☾</button>
|
|
180
|
+
</header>
|
|
181
|
+
<main>
|
|
182
|
+
<div class="titlerow"><h1 id="title"></h1><span class="readline" id="readat"></span></div>
|
|
183
|
+
<p class="help" id="help"></p>
|
|
184
|
+
|
|
185
|
+
<section class="summary"><div class="layerstrip" id="layerstrip" aria-label="Completeness by column"></div><div class="tiles" id="kpis" aria-label="Summary"></div></section>
|
|
186
|
+
|
|
187
|
+
<div class="toolbar">
|
|
188
|
+
<div class="seggrp" role="tablist" aria-label="View" id="modeSwitch"></div>
|
|
189
|
+
<input id="q" type="search" placeholder="Find a group or member… ( / )" aria-label="Find a group or member">
|
|
190
|
+
<span id="matchinfo" class="note" style="margin:0"></span>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="workspace" id="workspace">
|
|
194
|
+
<section class="canvaswrap">
|
|
195
|
+
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
196
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="0"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
197
|
+
<div id="cols" aria-hidden="true"></div>
|
|
198
|
+
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
199
|
+
<div id="zoomLabel" aria-hidden="true">100%</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="keyline" id="legend" aria-label="Legend"></div>
|
|
202
|
+
<aside class="insp" id="inspector" aria-label="Inspector" aria-hidden="true"></aside>
|
|
203
|
+
</section>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="live" class="sr" role="status" aria-live="polite"></div>
|
|
206
|
+
|
|
207
|
+
<div class="ledger" id="ledger"></div>
|
|
208
|
+
<details class="tableview"><summary>Table view</summary><table class="sources" id="tbl"></table></details>
|
|
209
|
+
<details class="spec" id="spec"><summary>Model reference (for the person or agent who builds this)</summary><div id="specbody"></div></details>
|
|
210
|
+
</main>
|
|
211
|
+
<footer id="footer"></footer>
|
|
212
|
+
<aside id="drawer" aria-label="Implementation notes"><h2>Implementation notes</h2><p class="note">Numbered markers on the page match these cards. Hover a card to find its place; click a marker to jump to its card.</p><div id="notelist"></div></aside>
|
|
213
|
+
|
|
214
|
+
<script type="application/json" id="flow-model">/*__MODEL__*/{}</script>
|
|
215
|
+
<script type="application/json" id="flow-notes">/*__NOTES__*/[]</script>
|
|
216
|
+
<script>"use strict";
|
|
217
|
+
(() => {
|
|
218
|
+
// src/model.ts
|
|
219
|
+
var TONES = ["ok", "warn", "crit", "info", "muted", "accent"];
|
|
220
|
+
var BORDERS = ["solid", "dashed", "dotted"];
|
|
221
|
+
var EDGES = ["flow", "dash", "dash-short", "dot"];
|
|
222
|
+
var MOTIONS = ["flow", "flow-slow", "breathe", "none"];
|
|
223
|
+
var BUCKETS = ["good", "neutral", "gap"];
|
|
224
|
+
function esc(s) {
|
|
225
|
+
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
226
|
+
return String(s ?? "").replace(/[&<>"']/g, (c) => map[c]);
|
|
227
|
+
}
|
|
228
|
+
function validateModel(input) {
|
|
229
|
+
const out = [];
|
|
230
|
+
const add = (level, code, where, message) => out.push({ level, code, where, message });
|
|
231
|
+
if (!input || typeof input !== "object") {
|
|
232
|
+
add("error", "model-shape", "model", "The model must be a JSON object.");
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
const m = input;
|
|
236
|
+
if (m.version !== 1) add("error", "model-version", "version", "version must be 1.");
|
|
237
|
+
if (m.layout !== "columns-lanes") add("error", "layout-unsupported", "layout", `layout ${String(m.layout)} is not supported yet: use "columns-lanes" ("free" and "auto" arrive in a later stage).`);
|
|
238
|
+
if (!Array.isArray(m.columns) || m.columns.length === 0) add("error", "columns-missing", "columns", "At least one column is required.");
|
|
239
|
+
if (!Array.isArray(m.lanes) || m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
240
|
+
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
241
|
+
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
242
|
+
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
243
|
+
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
244
|
+
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
245
|
+
const nodes = [];
|
|
246
|
+
(Array.isArray(m.nodes) ? m.nodes : []).forEach((n, i) => {
|
|
247
|
+
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.");
|
|
248
|
+
else nodes.push(n);
|
|
249
|
+
});
|
|
250
|
+
const links = [];
|
|
251
|
+
(Array.isArray(m.links) ? m.links : []).forEach((l, i) => {
|
|
252
|
+
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.");
|
|
253
|
+
else links.push(l);
|
|
254
|
+
});
|
|
255
|
+
if (nodes.length === 0) add("warn", "model-empty", "nodes", "The model has no nodes.");
|
|
256
|
+
for (const [id, s] of Object.entries(states)) {
|
|
257
|
+
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).`);
|
|
258
|
+
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);
|
|
259
|
+
if (!ok) add("error", "state-shape", `states[${id}]`, `State ${id} needs word, tone, border, edge, motion and bucket with allowed values.`);
|
|
260
|
+
}
|
|
261
|
+
const laneIds = new Set(lanes.map((l) => l.id));
|
|
262
|
+
const stateIds = new Set(Object.keys(states));
|
|
263
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
264
|
+
const ncols = columns.length;
|
|
265
|
+
for (const n of nodes) {
|
|
266
|
+
const w = `nodes[${n.id}]`;
|
|
267
|
+
if (nodeIds.has(n.id)) add("error", "node-duplicate", w, `Duplicate node id ${n.id}.`);
|
|
268
|
+
nodeIds.add(n.id);
|
|
269
|
+
if (!laneIds.has(n.lane)) add("error", "lane-undefined", w, `Lane ${n.lane} is not defined.`);
|
|
270
|
+
for (const s of [n.state, ...Object.values(n.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
|
|
271
|
+
if (typeof n.row !== "number" || !Number.isFinite(n.row)) add("error", "row-invalid", w, `Node ${n.id} needs a finite number for row (it sets the vertical position).`);
|
|
272
|
+
const span = n.colSpan ?? 1;
|
|
273
|
+
if (!Number.isInteger(n.col) || n.col < 0 || n.col + span > ncols) add("error", "column-range", w, `Column ${n.col} with span ${span} is outside 0..${ncols - 1}.`);
|
|
274
|
+
}
|
|
275
|
+
for (const n of nodes) {
|
|
276
|
+
if (n.kind === "chip" && (!n.ref || !nodeIds.has(n.ref))) add("error", "chip-ref", `nodes[${n.id}]`, `A chip must point at a real node (ref ${n.ref}).`);
|
|
277
|
+
}
|
|
278
|
+
const linkIds = /* @__PURE__ */ new Set();
|
|
279
|
+
const linked = /* @__PURE__ */ new Set();
|
|
280
|
+
for (const l of links) {
|
|
281
|
+
const w = `links[${l.id}]`;
|
|
282
|
+
if (linkIds.has(l.id)) add("error", "link-duplicate", w, `Duplicate link id ${l.id}.`);
|
|
283
|
+
linkIds.add(l.id);
|
|
284
|
+
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}).`);
|
|
285
|
+
else {
|
|
286
|
+
linked.add(l.from);
|
|
287
|
+
linked.add(l.to);
|
|
288
|
+
const a = nodes.find((n) => n.id === l.from), b = nodes.find((n) => n.id === l.to);
|
|
289
|
+
if (b.col <= a.col + (a.colSpan ?? 1) - 1) {
|
|
290
|
+
add("warn", "link-direction", w, `Link ${l.id} does not run left to right (column ${a.col} to ${b.col}); it will look wrong until loop-back links arrive.`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const s of [l.state, ...Object.values(l.stateIn ?? {})]) if (!stateIds.has(s)) add("error", "state-undefined", w, `State ${s} is not defined.`);
|
|
294
|
+
}
|
|
295
|
+
for (const n of nodes) {
|
|
296
|
+
if (!linked.has(n.id) && !n.allowIsolated && n.kind !== "note") {
|
|
297
|
+
add("error", "orphan", `nodes[${n.id}]`, `Node ${n.id} has no links. Add a link, a placeholder neighbour, or set allowIsolated.`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
301
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
302
|
+
const a = nodes[i], b = nodes[j];
|
|
303
|
+
const ax2 = a.col + (a.colSpan ?? 1), bx2 = b.col + (b.colSpan ?? 1);
|
|
304
|
+
if (a.col < bx2 && b.col < ax2 && Math.abs(a.row - b.row) < 1) {
|
|
305
|
+
add("error", "cell-overlap", `nodes[${a.id}]`, `Nodes ${a.id} and ${b.id} overlap (same columns, rows less than 1 apart).`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const used = new Set(nodes.map((n) => n.lane));
|
|
310
|
+
for (const l of lanes) if (!used.has(l.id)) add("warn", "lane-empty", `lanes[${l.id}]`, `Lane ${l.id} has no nodes.`);
|
|
311
|
+
return out;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/stateCss.ts
|
|
315
|
+
var cssId = (id) => id.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
316
|
+
var TONES2 = { ok: "var(--ok)", warn: "var(--warn)", crit: "var(--crit)", info: "var(--up)", muted: "var(--muted)", accent: "var(--accent)" };
|
|
317
|
+
var toneVar = (t) => Object.hasOwn(TONES2, t) ? TONES2[t] : TONES2.muted;
|
|
318
|
+
var DASH = { flow: "5 9", dash: "7 6", "dash-short": "3 6", dot: "2 6" };
|
|
319
|
+
var KEYFRAMES = { flow: "flow", dash: "flowd", "dash-short": "flowc", dot: "" };
|
|
320
|
+
function stateCss(states) {
|
|
321
|
+
const out = [];
|
|
322
|
+
const reduced = [];
|
|
323
|
+
for (const [id, s] of Object.entries(states)) {
|
|
324
|
+
const c = cssId(id);
|
|
325
|
+
const T = toneVar(s.tone);
|
|
326
|
+
if (s.border === "dotted") {
|
|
327
|
+
out.push(`.node.st-${c} { border:1px dotted ${T}; background:color-mix(in srgb, ${T} 10%, var(--panel)); --breath:${T}; }`);
|
|
328
|
+
out.push(`.node.st-${c} .t { color:${T}; }`);
|
|
329
|
+
} else {
|
|
330
|
+
const glow = s.bucket === "good" ? ` box-shadow:0 0 14px -4px color-mix(in srgb, ${T} 55%, transparent), var(--shadow);` : "";
|
|
331
|
+
out.push(`.node.st-${c} { border-left:4px ${s.border} ${T};${glow} --breath:${T}; }`);
|
|
332
|
+
}
|
|
333
|
+
out.push(`.ev.st-${c} { color:${T}; }`);
|
|
334
|
+
if (s.motion === "breathe") out.push(`.node.st-${c}:not(.dim) { animation:breathe 3.2s ease-in-out infinite; }`);
|
|
335
|
+
const edge = Object.hasOwn(DASH, s.edge) ? s.edge : "dash";
|
|
336
|
+
const flows = edge !== "dot" && (s.motion === "flow" || s.motion === "flow-slow");
|
|
337
|
+
let rule = `stroke:${T}; stroke-dasharray:${DASH[edge]}; opacity:${s.motion === "flow" ? ".85" : ".9"};`;
|
|
338
|
+
if (edge === "dot") rule += " stroke-linecap:round;";
|
|
339
|
+
if (flows) rule += ` animation:${KEYFRAMES[edge]} ${s.motion === "flow" ? "3s" : "7s"} linear infinite;`;
|
|
340
|
+
if (flows && s.motion === "flow") rule += ` filter:drop-shadow(0 0 3px ${T});`;
|
|
341
|
+
out.push(`path.edge.st-${c} { ${rule} }`);
|
|
342
|
+
if (s.bucket === "gap") out.push(`path.edge.hot.st-${c} { stroke:${T}; filter:drop-shadow(0 0 5px ${T}); }`);
|
|
343
|
+
if (flows) reduced.push(`path.edge.st-${c} { stroke-dasharray:none; }`);
|
|
344
|
+
}
|
|
345
|
+
if (reduced.length) out.push(`@media (prefers-reduced-motion: reduce) { ${reduced.join(" ")} }`);
|
|
346
|
+
return out.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/rules.ts
|
|
350
|
+
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
351
|
+
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
352
|
+
var nodeVisible = (_m, view, n) => !n.visibleIn || n.visibleIn.includes(view);
|
|
353
|
+
var nodeStateId = (_m, view, n) => n.stateIn?.[view] ?? n.state;
|
|
354
|
+
function linkVisible(m, view, l, nodes) {
|
|
355
|
+
if (l.visibleIn && !l.visibleIn.includes(view)) return false;
|
|
356
|
+
const a = nodes.get(l.from);
|
|
357
|
+
const b = nodes.get(l.to);
|
|
358
|
+
return !!a && !!b && nodeVisible(m, view, a) && nodeVisible(m, view, b);
|
|
359
|
+
}
|
|
360
|
+
var linkStateId = (_m, view, l) => l.stateIn?.[view] ?? l.state;
|
|
361
|
+
var bucketOf = (m, stateId) => m.states[stateId]?.bucket ?? "gap";
|
|
362
|
+
var isNew = (m, view, n) => view !== baselineView(m) && nodeVisible(m, view, n) && !nodeVisible(m, baselineView(m), n);
|
|
363
|
+
var isImproved = (m, view, n) => {
|
|
364
|
+
const base = baselineView(m);
|
|
365
|
+
return view !== base && nodeVisible(m, base, n) && bucketOf(m, nodeStateId(m, base, n)) === "gap" && bucketOf(m, nodeStateId(m, view, n)) !== "gap";
|
|
366
|
+
};
|
|
367
|
+
var canonOf = (nodes, id) => nodes.get(id)?.ref ?? id;
|
|
368
|
+
|
|
369
|
+
// src/trace.ts
|
|
370
|
+
function relatives(edges, canonOf2, start, dir) {
|
|
371
|
+
const set = /* @__PURE__ */ new Set([canonOf2(start)]);
|
|
372
|
+
let grew = true;
|
|
373
|
+
while (grew) {
|
|
374
|
+
grew = false;
|
|
375
|
+
for (const e of edges) {
|
|
376
|
+
const a = canonOf2(e.from);
|
|
377
|
+
const b = canonOf2(e.to);
|
|
378
|
+
if (dir === "up" && set.has(b) && !set.has(a)) {
|
|
379
|
+
set.add(a);
|
|
380
|
+
grew = true;
|
|
381
|
+
}
|
|
382
|
+
if (dir === "down" && set.has(a) && !set.has(b)) {
|
|
383
|
+
set.add(b);
|
|
384
|
+
grew = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return set;
|
|
389
|
+
}
|
|
390
|
+
function connected(edges, canonOf2, start) {
|
|
391
|
+
return /* @__PURE__ */ new Set([...relatives(edges, canonOf2, start, "up"), ...relatives(edges, canonOf2, start, "down")]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// src/interact.ts
|
|
395
|
+
function relationsOf(ctx) {
|
|
396
|
+
const given = ctx.model.inspector?.relations;
|
|
397
|
+
if (given && given.length) return given;
|
|
398
|
+
const last = Math.max(0, ctx.model.columns.length - 1);
|
|
399
|
+
return [
|
|
400
|
+
{ id: "up", label: "Upstream", dir: "up" },
|
|
401
|
+
{ id: "down", label: "Downstream", dir: "down" },
|
|
402
|
+
{ id: "end", label: "At the end", dir: "down", filter: { col: last } }
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
function visibleEdges(ctx) {
|
|
406
|
+
return ctx.links.filter((l) => linkVisible(ctx.model, ctx.mode, l, ctx.nodes)).map((l) => ({ from: l.from, to: l.to }));
|
|
407
|
+
}
|
|
408
|
+
function relationSet(ctx, rel, start) {
|
|
409
|
+
const canon = (id) => canonOf(ctx.nodes, id);
|
|
410
|
+
const set = relatives(visibleEdges(ctx), canon, start, rel.dir);
|
|
411
|
+
if (!rel.filter) return set;
|
|
412
|
+
const out = /* @__PURE__ */ new Set([canon(start)]);
|
|
413
|
+
for (const id of set) {
|
|
414
|
+
const n = ctx.nodes.get(id);
|
|
415
|
+
if (!n) continue;
|
|
416
|
+
if ((rel.filter.col === void 0 || n.col === rel.filter.col) && (rel.filter.kind === void 0 || n.kind === rel.filter.kind)) out.add(id);
|
|
417
|
+
}
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
function matches(ctx, n) {
|
|
421
|
+
const q = ctx.query;
|
|
422
|
+
return !!q && (n.title.toLowerCase().includes(q) || (n.members ?? []).some((m) => m.toLowerCase().includes(q)));
|
|
423
|
+
}
|
|
424
|
+
function install(ctx) {
|
|
425
|
+
const { els, model } = ctx;
|
|
426
|
+
const canon = (id) => canonOf(ctx.nodes, id);
|
|
427
|
+
const visibleNodes = () => model.nodes.filter((n) => nodeVisible(model, ctx.mode, n));
|
|
428
|
+
const inspInset = () => els.inspector.classList.contains("open") ? 340 : 0;
|
|
429
|
+
function activeSet(id) {
|
|
430
|
+
const edges = visibleEdges(ctx);
|
|
431
|
+
if (id) {
|
|
432
|
+
if (id === ctx.selected && ctx.hl) {
|
|
433
|
+
const rel = relationsOf(ctx).find((r) => r.id === ctx.hl);
|
|
434
|
+
if (rel) return relationSet(ctx, rel, id);
|
|
435
|
+
}
|
|
436
|
+
return connected(edges, canon, id);
|
|
437
|
+
}
|
|
438
|
+
if (ctx.filter) {
|
|
439
|
+
return new Set(visibleNodes().filter((n) => {
|
|
440
|
+
const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
|
|
441
|
+
return ctx.filter === "gap" ? b === "gap" : !!n.upgrade && b !== "good";
|
|
442
|
+
}).map((n) => canon(n.id)));
|
|
443
|
+
}
|
|
444
|
+
if (ctx.query) return new Set(visibleNodes().filter((n) => matches(ctx, n)).map((n) => canon(n.id)));
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
function trace(id) {
|
|
448
|
+
const set = activeSet(id);
|
|
449
|
+
const lit = !!id && !!set;
|
|
450
|
+
for (const n of model.nodes) {
|
|
451
|
+
const d = ctx.nodeEls.get(n.id);
|
|
452
|
+
if (!d) continue;
|
|
453
|
+
d.classList.toggle("dim", !!set && !set.has(canon(n.id)));
|
|
454
|
+
d.classList.toggle("lit", lit && !!set && set.has(canon(n.id)));
|
|
455
|
+
}
|
|
456
|
+
ctx.edgeEls.forEach((p) => {
|
|
457
|
+
if (!p) return;
|
|
458
|
+
const inSet = !!set && set.has(canon(p.dataset.a ?? "")) && set.has(canon(p.dataset.b ?? ""));
|
|
459
|
+
p.classList.toggle("dim", !!set && !inSet);
|
|
460
|
+
p.classList.toggle("hot", lit && inSet);
|
|
461
|
+
});
|
|
462
|
+
ctx.labelEls.forEach((l) => {
|
|
463
|
+
if (l) l.classList.toggle("show", lit && !!set && set.has(canon(l.dataset.a ?? "")) && set.has(canon(l.dataset.b ?? "")));
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function select(id) {
|
|
467
|
+
clearSim();
|
|
468
|
+
ctx.selected = id ? canon(id) : null;
|
|
469
|
+
ctx.hl = null;
|
|
470
|
+
ctx.dismissed = false;
|
|
471
|
+
for (const n of model.nodes) ctx.nodeEls.get(n.id)?.classList.toggle("sel", !!ctx.selected && canon(n.id) === ctx.selected);
|
|
472
|
+
trace(ctx.selected);
|
|
473
|
+
ctx.fn.renderInspector();
|
|
474
|
+
drawTrail();
|
|
475
|
+
if (ctx.selected) revealNode(ctx.selected);
|
|
476
|
+
}
|
|
477
|
+
function overlay(edge, cls, delay, dur) {
|
|
478
|
+
const o = edge.cloneNode(false);
|
|
479
|
+
o.setAttribute("class", cls);
|
|
480
|
+
o.setAttribute("pathLength", "1");
|
|
481
|
+
o.removeAttribute("data-i");
|
|
482
|
+
els.layer.appendChild(o);
|
|
483
|
+
ctx.overlays.push(o);
|
|
484
|
+
if (ctx.reduced) return o;
|
|
485
|
+
o.style.strokeDasharray = "1 1";
|
|
486
|
+
o.style.strokeDashoffset = "1";
|
|
487
|
+
o.getBoundingClientRect();
|
|
488
|
+
o.style.transition = `stroke-dashoffset ${dur}s ${delay}s ease-out`;
|
|
489
|
+
o.style.strokeDashoffset = "0";
|
|
490
|
+
return o;
|
|
491
|
+
}
|
|
492
|
+
function dropOverlays(cls) {
|
|
493
|
+
ctx.overlays.filter((o) => o.classList.contains(cls)).forEach((o) => o.remove());
|
|
494
|
+
ctx.overlays = ctx.overlays.filter((o) => o.isConnected);
|
|
495
|
+
}
|
|
496
|
+
function drawTrail() {
|
|
497
|
+
dropOverlays("trail");
|
|
498
|
+
if (!ctx.selected || ctx.reduced) return;
|
|
499
|
+
const set = connected(visibleEdges(ctx), canon, ctx.selected);
|
|
500
|
+
const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
|
|
501
|
+
for (const p of ctx.edgeEls) {
|
|
502
|
+
if (!p || p.classList.contains("off")) continue;
|
|
503
|
+
const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
|
|
504
|
+
if (!(set.has(a) && set.has(b))) continue;
|
|
505
|
+
const delay = Math.min(Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0), Math.abs((ctx.nodes.get(b)?.col ?? 0) - c0)) * 0.09;
|
|
506
|
+
const o = overlay(p, "trail", delay, 0.55);
|
|
507
|
+
window.setTimeout(() => {
|
|
508
|
+
o.style.transition = "opacity .6s";
|
|
509
|
+
o.style.opacity = "0";
|
|
510
|
+
window.setTimeout(() => o.remove(), 650);
|
|
511
|
+
}, (delay + 0.55) * 1e3 + 150);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
function simulate(actionId) {
|
|
515
|
+
clearSim();
|
|
516
|
+
if (!ctx.selected) return;
|
|
517
|
+
const action = model.inspector?.actions?.find((a) => a.id === actionId);
|
|
518
|
+
const stale = action?.staleText ?? "potentially stale";
|
|
519
|
+
ctx.simActive = true;
|
|
520
|
+
const down = relatives(visibleEdges(ctx), canon, ctx.selected, "down");
|
|
521
|
+
const c0 = ctx.nodes.get(ctx.selected)?.col ?? 0;
|
|
522
|
+
for (const p of ctx.edgeEls) {
|
|
523
|
+
if (!p || p.classList.contains("off")) continue;
|
|
524
|
+
const a = canon(p.dataset.a ?? ""), b = canon(p.dataset.b ?? "");
|
|
525
|
+
if (down.has(a) && down.has(b)) overlay(p, "ripple", Math.abs((ctx.nodes.get(a)?.col ?? 0) - c0) * 0.11, 0.5);
|
|
526
|
+
}
|
|
527
|
+
down.forEach((id) => {
|
|
528
|
+
if (id === ctx.selected) return;
|
|
529
|
+
const d = ctx.nodeEls.get(id);
|
|
530
|
+
if (!d) return;
|
|
531
|
+
const delay = ctx.reduced ? 0 : Math.abs((ctx.nodes.get(id)?.col ?? 0) - c0) * 110 + 450;
|
|
532
|
+
ctx.simTimers.push(window.setTimeout(() => {
|
|
533
|
+
d.classList.add("fail");
|
|
534
|
+
const sub = d.querySelector(".s");
|
|
535
|
+
if (sub) {
|
|
536
|
+
d.dataset.s = sub.innerHTML;
|
|
537
|
+
sub.textContent = stale;
|
|
538
|
+
}
|
|
539
|
+
}, delay));
|
|
540
|
+
});
|
|
541
|
+
ctx.fn.announce(`Simulation: ${down.size - 1} groups downstream of ${ctx.nodes.get(ctx.selected)?.title ?? ""} would be ${stale}.`);
|
|
542
|
+
ctx.fn.renderInspector();
|
|
543
|
+
}
|
|
544
|
+
function clearSim() {
|
|
545
|
+
ctx.simTimers.forEach((t) => clearTimeout(t));
|
|
546
|
+
ctx.simTimers = [];
|
|
547
|
+
dropOverlays("ripple");
|
|
548
|
+
document.querySelectorAll(".node.fail").forEach((d) => {
|
|
549
|
+
d.classList.remove("fail");
|
|
550
|
+
const sub = d.querySelector(".s");
|
|
551
|
+
if (sub && d.dataset.s !== void 0) {
|
|
552
|
+
sub.innerHTML = d.dataset.s;
|
|
553
|
+
delete d.dataset.s;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
ctx.simActive = false;
|
|
557
|
+
}
|
|
558
|
+
function revealNode(id) {
|
|
559
|
+
const d = ctx.nodeEls.get(id);
|
|
560
|
+
if (!d) return;
|
|
561
|
+
const r = d.getBoundingClientRect(), v = els.vp.getBoundingClientRect(), edge = v.right - 340 - 24;
|
|
562
|
+
if (r.right > edge) {
|
|
563
|
+
ctx.userMoved = true;
|
|
564
|
+
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);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function rectOf(ids) {
|
|
568
|
+
const ps = ids.map((i) => ctx.geom.nodes[i]).filter((p) => !!p && !!ctx.nodes.get(p.id) && nodeVisible(model, ctx.mode, ctx.nodes.get(p.id)));
|
|
569
|
+
if (!ps.length) return null;
|
|
570
|
+
const x1 = Math.min(...ps.map((p) => p.x)), y1 = Math.min(...ps.map((p) => p.y));
|
|
571
|
+
const x2 = Math.max(...ps.map((p) => p.x + p.w)), y2 = Math.max(...ps.map((p) => p.y + p.h));
|
|
572
|
+
return { x: x1 - 20, y: y1 - 20, w: x2 - x1 + 40, h: y2 - y1 + 40 };
|
|
573
|
+
}
|
|
574
|
+
function zoomToPath() {
|
|
575
|
+
if (!ctx.selected) return;
|
|
576
|
+
const ids = [...connected(visibleEdges(ctx), canon, ctx.selected)];
|
|
577
|
+
ids.push(...model.nodes.filter((n) => n.ref && ids.includes(n.ref)).map((n) => n.id));
|
|
578
|
+
const r = rectOf(ids);
|
|
579
|
+
if (r) {
|
|
580
|
+
ctx.userMoved = true;
|
|
581
|
+
ctx.fn.fit(true, r, inspInset());
|
|
582
|
+
ctx.fn.announce(`Zoomed to the path of ${ctx.nodes.get(ctx.selected)?.title ?? ""}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function zoomToBand(id) {
|
|
586
|
+
const r = ctx.geom.lanes[id];
|
|
587
|
+
if (!r) return;
|
|
588
|
+
ctx.userMoved = true;
|
|
589
|
+
ctx.fn.fit(true, { x: r.x - 10, y: r.y - 10, w: r.w + 20, h: r.h + 20 }, inspInset());
|
|
590
|
+
ctx.fn.announce(`Zoomed to the lane ${model.lanes.find((l) => l.id === id)?.title ?? id}`);
|
|
591
|
+
}
|
|
592
|
+
function escape() {
|
|
593
|
+
clearSim();
|
|
594
|
+
select(null);
|
|
595
|
+
ctx.filter = null;
|
|
596
|
+
ctx.query = "";
|
|
597
|
+
els.q.value = "";
|
|
598
|
+
els.matchinfo.textContent = "";
|
|
599
|
+
ctx.fn.buildChrome();
|
|
600
|
+
trace(null);
|
|
601
|
+
ctx.fn.toggleNotes(false);
|
|
602
|
+
}
|
|
603
|
+
function setMode(id) {
|
|
604
|
+
if (ctx.mode === id) return;
|
|
605
|
+
document.body.classList.remove("intro");
|
|
606
|
+
clearSim();
|
|
607
|
+
ctx.mode = id;
|
|
608
|
+
const sel = ctx.selected && ctx.nodes.get(ctx.selected);
|
|
609
|
+
if (sel && !nodeVisible(model, id, sel)) select(null);
|
|
610
|
+
ctx.fn.paint(true);
|
|
611
|
+
ctx.userMoved = false;
|
|
612
|
+
ctx.fn.fitWidth(true);
|
|
613
|
+
const added = model.nodes.filter((n) => nodeVisible(model, id, n) && !nodeVisible(model, model.views?.[0]?.id ?? "all", n)).length;
|
|
614
|
+
ctx.fn.announce(added ? `${model.views?.find((v) => v.id === id)?.label ?? id} view: ${added} groups added.` : `${model.views?.find((v) => v.id === id)?.label ?? id} view.`);
|
|
615
|
+
}
|
|
616
|
+
ctx.fn.trace = trace;
|
|
617
|
+
ctx.fn.select = select;
|
|
618
|
+
ctx.fn.drawTrail = drawTrail;
|
|
619
|
+
ctx.fn.simulate = simulate;
|
|
620
|
+
ctx.fn.clearSim = clearSim;
|
|
621
|
+
ctx.fn.rectOf = rectOf;
|
|
622
|
+
ctx.fn.zoomToPath = zoomToPath;
|
|
623
|
+
ctx.fn.zoomToBand = zoomToBand;
|
|
624
|
+
ctx.fn.escape = escape;
|
|
625
|
+
ctx.fn.setMode = setMode;
|
|
626
|
+
ctx.fn.announce = (t) => {
|
|
627
|
+
els.live.textContent = t;
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/ctx.ts
|
|
632
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
633
|
+
function el(tag, cls, html) {
|
|
634
|
+
const e = document.createElement(tag);
|
|
635
|
+
if (cls) e.className = cls;
|
|
636
|
+
if (html !== void 0) e.innerHTML = html;
|
|
637
|
+
return e;
|
|
638
|
+
}
|
|
639
|
+
function sv(tag, attrs) {
|
|
640
|
+
const e = document.createElementNS(SVG_NS, tag);
|
|
641
|
+
for (const k of Object.keys(attrs)) e.setAttribute(k, attrs[k]);
|
|
642
|
+
return e;
|
|
643
|
+
}
|
|
644
|
+
function on(ctx, target, type, fn, opts) {
|
|
645
|
+
target.addEventListener(type, fn, opts);
|
|
646
|
+
ctx.disposers.push(() => target.removeEventListener(type, fn, opts));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/layout.ts
|
|
650
|
+
var DEFAULTS = { pitch: 62, top: 64, gap: 34, padX: 24, visible: () => true };
|
|
651
|
+
var HEIGHT = { card: 54, group: 54, gateway: 54, placeholder: 54, chip: 34, start: 40, end: 40, event: 40, note: 40 };
|
|
652
|
+
var LANE_PAD_TOP = 38;
|
|
653
|
+
var LANE_PAD_BOTTOM = 14;
|
|
654
|
+
function computeGeometry(m, opt = {}) {
|
|
655
|
+
const o = { ...DEFAULTS, ...opt };
|
|
656
|
+
const xs = [];
|
|
657
|
+
let x = o.padX;
|
|
658
|
+
for (const c of m.columns) {
|
|
659
|
+
xs.push(x);
|
|
660
|
+
x += c.width + o.gap;
|
|
661
|
+
}
|
|
662
|
+
const world = { w: Math.max(1, x - o.gap + o.padX), h: 1 };
|
|
663
|
+
const nodes = {};
|
|
664
|
+
for (const n of m.nodes) {
|
|
665
|
+
const h = HEIGHT[n.kind] ?? 54;
|
|
666
|
+
const first = Math.min(Math.max(0, n.col), m.columns.length - 1);
|
|
667
|
+
const last = Math.min(m.columns.length - 1, first + (n.colSpan ?? 1) - 1);
|
|
668
|
+
const x0 = xs[first] ?? o.padX;
|
|
669
|
+
const x1 = (xs[last] ?? x0) + (m.columns[last]?.width ?? 0);
|
|
670
|
+
nodes[n.id] = { id: n.id, x: x0, y: o.top + 40 + n.row * o.pitch - h / 2, w: Math.max(1, x1 - x0), h };
|
|
671
|
+
}
|
|
672
|
+
const lanes = {};
|
|
673
|
+
for (const l of m.lanes) {
|
|
674
|
+
const mem = m.nodes.filter((n) => n.lane === l.id && o.visible(n.id)).map((n) => nodes[n.id]);
|
|
675
|
+
if (!mem.length) continue;
|
|
676
|
+
const y1 = Math.min(...mem.map((p) => p.y)) - LANE_PAD_TOP;
|
|
677
|
+
const y2 = Math.max(...mem.map((p) => p.y + p.h)) + LANE_PAD_BOTTOM;
|
|
678
|
+
lanes[l.id] = { x: 8, y: y1, w: world.w - 16, h: y2 - y1 };
|
|
679
|
+
}
|
|
680
|
+
const all = Object.values(lanes);
|
|
681
|
+
let bounds = { x: 0, y: 0, w: world.w, h: 1 };
|
|
682
|
+
if (all.length) {
|
|
683
|
+
const top = Math.min(...all.map((r) => r.y));
|
|
684
|
+
bounds = { x: 8, y: top, w: world.w - 16, h: Math.max(...all.map((r) => r.y + r.h)) - top };
|
|
685
|
+
}
|
|
686
|
+
world.h = Math.max(1, bounds.y + bounds.h + 70);
|
|
687
|
+
return { xs, nodes, lanes, bounds, world };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/routing.ts
|
|
691
|
+
function elbow(x1, y1, x2, y2, tx) {
|
|
692
|
+
const r = 8;
|
|
693
|
+
if (Math.abs(y2 - y1) < 1) return `M${x1},${y1} H${x2}`;
|
|
694
|
+
if (Math.abs(y2 - y1) < 2 * r + 2) return `M${x1},${y1} C${tx},${y1} ${tx},${y2} ${x2},${y2}`;
|
|
695
|
+
const dir = y2 > y1 ? 1 : -1;
|
|
696
|
+
return `M${x1},${y1} H${tx - r} Q${tx},${y1} ${tx},${y1 + dir * r} V${y2 - dir * r} Q${tx},${y2} ${tx + r},${y2} H${x2}`;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/render.ts
|
|
700
|
+
function nodeHtml(ctx, n) {
|
|
701
|
+
const m = ctx.model;
|
|
702
|
+
const sid = nodeStateId(m, ctx.mode, n);
|
|
703
|
+
const word = m.states[sid]?.word ?? sid;
|
|
704
|
+
const bucket = bucketOf(m, sid);
|
|
705
|
+
const sub = n.subtitle ?? m.columns[n.col]?.title ?? "";
|
|
706
|
+
const weight = Math.max(1, n.weight ?? 1);
|
|
707
|
+
const showWord = n.kind !== "placeholder" && n.kind !== "note";
|
|
708
|
+
const bar = weight > 1 && n.kind !== "chip" ? `<div class="bar"><i class="${["good", "neutral", "gap"].includes(bucket) ? bucket : "gap"}" style="width:100%"></i></div>` : "";
|
|
709
|
+
const improved = isImproved(m, ctx.mode, n) ? '<span class="cor up" title="Gap filled in this view">\u25B2</span>' : "";
|
|
710
|
+
const upgrade = n.upgrade && bucket !== "good" ? `<span class="cor meas" title="${esc("Can improve: " + n.upgrade)}">\u2197</span>` : "";
|
|
711
|
+
const arrow = n.kind === "chip" ? '<span aria-hidden="true">\u2191 </span>' : "";
|
|
712
|
+
const lead = showWord ? `<b class="ev st-${cssId(sid)}">${esc(word)}</b> \xB7 ` : "";
|
|
713
|
+
return `<div class="t"><span class="newdot" title="Added in this view"></span><span class="name">${arrow}${esc(n.title)}</span></div><div class="s" title="${esc(sub)}">${lead}${esc(sub)}</div>${bar}${improved}${upgrade}`;
|
|
714
|
+
}
|
|
715
|
+
function build(ctx) {
|
|
716
|
+
const { model, els } = ctx;
|
|
717
|
+
ctx.geom = computeGeometry(model);
|
|
718
|
+
els.world.style.width = ctx.geom.world.w + "px";
|
|
719
|
+
els.world.style.height = ctx.geom.world.h + "px";
|
|
720
|
+
els.svg.setAttribute("width", String(ctx.geom.world.w));
|
|
721
|
+
els.svg.setAttribute("height", String(ctx.geom.world.h));
|
|
722
|
+
els.world.querySelectorAll(".node, .band, .elabel").forEach((n) => n.remove());
|
|
723
|
+
els.layer.innerHTML = "";
|
|
724
|
+
ctx.nodeEls.clear();
|
|
725
|
+
ctx.laneEls.clear();
|
|
726
|
+
ctx.edgeEls = [];
|
|
727
|
+
ctx.labelEls = [];
|
|
728
|
+
for (const l of model.lanes) {
|
|
729
|
+
const d = el("div", "band" + (l.gap ? " gap" : ""));
|
|
730
|
+
d.id = "lane_" + l.id;
|
|
731
|
+
const badge = l.badge ? `<span class="bd t-${["ok", "warn", "muted"].includes(l.badgeTone ?? "") ? l.badgeTone : "muted"}">${esc(l.badge)}</span>` : "";
|
|
732
|
+
const note = l.note ? `<span class="bn">${esc(l.note)}</span>` : "";
|
|
733
|
+
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>`;
|
|
734
|
+
const label = d.firstElementChild;
|
|
735
|
+
on(ctx, label, "click", (e) => {
|
|
736
|
+
e.stopPropagation();
|
|
737
|
+
ctx.fn.zoomToBand(l.id);
|
|
738
|
+
});
|
|
739
|
+
on(ctx, label, "keydown", (e) => {
|
|
740
|
+
if (e.key === "Enter") ctx.fn.zoomToBand(l.id);
|
|
741
|
+
});
|
|
742
|
+
els.world.insertBefore(d, els.svg.parentElement === els.world ? els.svg : null);
|
|
743
|
+
ctx.laneEls.set(l.id, d);
|
|
744
|
+
}
|
|
745
|
+
for (const n of model.nodes) {
|
|
746
|
+
const p = ctx.geom.nodes[n.id];
|
|
747
|
+
if (!p) continue;
|
|
748
|
+
const d = el("div", "node" + (n.kind === "chip" ? " ref" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : ""));
|
|
749
|
+
d.id = "n_" + n.id;
|
|
750
|
+
d.tabIndex = 0;
|
|
751
|
+
d.setAttribute("role", "button");
|
|
752
|
+
d.style.setProperty("--c", String(n.col));
|
|
753
|
+
d.style.left = p.x + "px";
|
|
754
|
+
d.style.top = p.y + "px";
|
|
755
|
+
d.style.width = p.w + "px";
|
|
756
|
+
d.style.height = p.h + "px";
|
|
757
|
+
on(ctx, d, "click", () => {
|
|
758
|
+
if (ctx.moved > 4) return;
|
|
759
|
+
ctx.fn.select(canonOf(ctx.nodes, n.id));
|
|
760
|
+
});
|
|
761
|
+
on(ctx, d, "dblclick", (e) => {
|
|
762
|
+
e.stopPropagation();
|
|
763
|
+
ctx.fn.select(canonOf(ctx.nodes, n.id));
|
|
764
|
+
ctx.fn.zoomToPath();
|
|
765
|
+
});
|
|
766
|
+
on(ctx, d, "keydown", (e) => {
|
|
767
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
768
|
+
e.preventDefault();
|
|
769
|
+
ctx.fn.select(canonOf(ctx.nodes, n.id));
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
on(ctx, d, "mouseenter", () => ctx.fn.trace(n.id));
|
|
773
|
+
on(ctx, d, "mouseleave", () => ctx.fn.trace(ctx.selected));
|
|
774
|
+
on(ctx, d, "focus", () => ctx.fn.trace(n.id));
|
|
775
|
+
on(ctx, d, "blur", () => ctx.fn.trace(ctx.selected));
|
|
776
|
+
els.world.appendChild(d);
|
|
777
|
+
ctx.nodeEls.set(n.id, d);
|
|
778
|
+
}
|
|
779
|
+
const gap = DEFAULTS.gap;
|
|
780
|
+
ctx.links.forEach((l, i) => {
|
|
781
|
+
const A = ctx.geom.nodes[l.from], B = ctx.geom.nodes[l.to];
|
|
782
|
+
if (!A || !B) {
|
|
783
|
+
console.warn(`flow-studio: link ${l.id} skipped (an end is missing)`);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const x1 = A.x + A.w, y1 = A.y + A.h / 2, x2 = B.x, y2 = B.y + B.h / 2, tx = x1 + gap / 2;
|
|
787
|
+
const path = sv("path", { d: elbow(x1, y1, x2 - 1, y2, tx), class: "edge", "data-i": String(i), "data-a": l.from, "data-b": l.to, "marker-end": "url(#arr)" });
|
|
788
|
+
els.layer.appendChild(path);
|
|
789
|
+
ctx.edgeEls[i] = path;
|
|
790
|
+
if (l.label) {
|
|
791
|
+
const lab = el("div", "elabel", esc(l.label));
|
|
792
|
+
lab.dataset.i = String(i);
|
|
793
|
+
lab.dataset.a = l.from;
|
|
794
|
+
lab.dataset.b = l.to;
|
|
795
|
+
lab.style.left = (Math.abs(y2 - y1) < 1 ? (x1 + x2) / 2 : tx) + "px";
|
|
796
|
+
lab.style.top = (y1 + y2) / 2 + "px";
|
|
797
|
+
els.world.appendChild(lab);
|
|
798
|
+
ctx.labelEls[i] = lab;
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function paint(ctx, fromSwitch = false) {
|
|
803
|
+
const { model } = ctx;
|
|
804
|
+
const mode = ctx.mode;
|
|
805
|
+
ctx.geom = computeGeometry(model, { visible: (id) => {
|
|
806
|
+
const n = ctx.nodes.get(id);
|
|
807
|
+
return !!n && nodeVisible(model, mode, n);
|
|
808
|
+
} });
|
|
809
|
+
document.body.classList.toggle("alt", !!model.views && mode !== baselineView(model));
|
|
810
|
+
for (const n of model.nodes) {
|
|
811
|
+
const d = ctx.nodeEls.get(n.id);
|
|
812
|
+
if (!d) continue;
|
|
813
|
+
const sid = nodeStateId(model, mode, n);
|
|
814
|
+
const show = nodeVisible(model, mode, n);
|
|
815
|
+
const bucket = bucketOf(model, sid);
|
|
816
|
+
const cors = (isImproved(model, mode, n) ? 1 : 0) + (n.upgrade && bucket !== "good" ? 1 : 0);
|
|
817
|
+
const fresh = isNew(model, mode, n);
|
|
818
|
+
d.className = "node st-" + cssId(sid) + (n.kind === "chip" ? " ref" : "") + ((n.colSpan ?? 1) > 1 ? " wide" : "") + (n.kind === "placeholder" ? " placeholder" : "") + (show ? "" : " off") + (fresh ? " isnew" : "") + (cors ? cors > 1 ? " hascor2" : " hascor" : "") + (ctx.selected && canonOf(ctx.nodes, n.id) === ctx.selected ? " sel" : "");
|
|
819
|
+
d.innerHTML = nodeHtml(ctx, n);
|
|
820
|
+
d.setAttribute("aria-hidden", show ? "false" : "true");
|
|
821
|
+
d.tabIndex = show ? 0 : -1;
|
|
822
|
+
const word = model.states[sid]?.word ?? sid;
|
|
823
|
+
const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
|
|
824
|
+
d.setAttribute("aria-label", n.kind === "chip" ? `${n.title}: a reference to the node in the ${lane} lane. Press Enter to select it.` : `${n.title}. ${n.subtitle ?? model.columns[n.col]?.title ?? ""}, ${word}. Press Enter to inspect.`);
|
|
825
|
+
if (fromSwitch && fresh && !ctx.reduced) {
|
|
826
|
+
d.style.transitionDelay = n.col * 60 + "ms";
|
|
827
|
+
d.classList.add("pulse");
|
|
828
|
+
window.setTimeout(() => {
|
|
829
|
+
d.style.transitionDelay = "";
|
|
830
|
+
d.classList.remove("pulse");
|
|
831
|
+
}, 2200);
|
|
832
|
+
} else {
|
|
833
|
+
d.style.transitionDelay = "";
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
ctx.links.forEach((l, i) => {
|
|
837
|
+
const p = ctx.edgeEls[i];
|
|
838
|
+
if (!p) return;
|
|
839
|
+
const vis = linkVisible(model, mode, l, ctx.nodes);
|
|
840
|
+
p.setAttribute("class", "edge st-" + cssId(linkStateId(model, mode, l)) + (vis ? "" : " off"));
|
|
841
|
+
ctx.labelEls[i]?.classList.toggle("off", !vis);
|
|
842
|
+
});
|
|
843
|
+
for (const l of model.lanes) {
|
|
844
|
+
const d = ctx.laneEls.get(l.id);
|
|
845
|
+
const r = ctx.geom.lanes[l.id];
|
|
846
|
+
if (!d) continue;
|
|
847
|
+
d.classList.toggle("off", !r);
|
|
848
|
+
if (r) {
|
|
849
|
+
d.style.left = r.x + "px";
|
|
850
|
+
d.style.top = r.y + "px";
|
|
851
|
+
d.style.width = r.w + "px";
|
|
852
|
+
d.style.height = r.h + "px";
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
ctx.fn.buildChrome();
|
|
856
|
+
ctx.fn.trace(ctx.selected);
|
|
857
|
+
ctx.fn.renderInspector();
|
|
858
|
+
ctx.fn.stickLabels();
|
|
859
|
+
ctx.fn.placeHeaders();
|
|
860
|
+
}
|
|
861
|
+
function install2(ctx) {
|
|
862
|
+
ctx.fn.buildWorld = () => build(ctx);
|
|
863
|
+
ctx.fn.paint = (fromSwitch) => paint(ctx, fromSwitch);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/viewport.ts
|
|
867
|
+
var K_MIN = 0.25;
|
|
868
|
+
var K_MAX = 2.5;
|
|
869
|
+
var clampK = (k) => Number.isFinite(k) ? Math.min(K_MAX, Math.max(K_MIN, k)) : 1;
|
|
870
|
+
function zoomAt(v, factor, cx, cy) {
|
|
871
|
+
const base = Number.isFinite(v.k) && v.k > 0 ? v.k : 1;
|
|
872
|
+
const k = clampK(base * factor);
|
|
873
|
+
const r = k / base;
|
|
874
|
+
return { k, x: cx - (cx - v.x) * r, y: cy - (cy - v.y) * r };
|
|
875
|
+
}
|
|
876
|
+
function fitRect(rect, size, pad, top) {
|
|
877
|
+
if (!(rect.w > 0 && rect.h > 0)) return { x: 0, y: 0, k: 1 };
|
|
878
|
+
const aw = Math.max(1, size.w - 2 * pad);
|
|
879
|
+
const ah = Math.max(1, size.h - 2 * pad - top);
|
|
880
|
+
const k = Math.min(1, clampK(Math.min(aw / rect.w, ah / rect.h)));
|
|
881
|
+
return { k, x: pad + (aw - rect.w * k) / 2 - rect.x * k, y: pad + top + (ah - rect.h * k) / 2 - rect.y * k };
|
|
882
|
+
}
|
|
883
|
+
function fitWidth(rect, size, pad, top) {
|
|
884
|
+
if (!(rect.w > 0)) return { x: 0, y: 0, k: 1 };
|
|
885
|
+
const k = Math.min(1, clampK((Math.max(1, size.w) - 2 * pad) / rect.w));
|
|
886
|
+
return { k, x: pad - rect.x * k, y: top - rect.y * k };
|
|
887
|
+
}
|
|
888
|
+
function clampPan(v, bounds, size) {
|
|
889
|
+
const keep = 80;
|
|
890
|
+
const minX = keep - (bounds.x + bounds.w) * v.k;
|
|
891
|
+
const maxX = size.w - keep - bounds.x * v.k;
|
|
892
|
+
const minY = keep - (bounds.y + bounds.h) * v.k;
|
|
893
|
+
const maxY = size.h - keep - bounds.y * v.k;
|
|
894
|
+
return { k: v.k, x: Math.min(maxX, Math.max(minX, v.x)), y: Math.min(maxY, Math.max(minY, v.y)) };
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/controls.ts
|
|
898
|
+
var HEAD = 44;
|
|
899
|
+
var ICON = {
|
|
900
|
+
select: '<path d="M5 3l12 6.5-5.2 1.6L9.5 17z"/>',
|
|
901
|
+
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"/>',
|
|
902
|
+
plus: '<path d="M12 5v14M5 12h14"/>',
|
|
903
|
+
minus: '<path d="M5 12h14"/>',
|
|
904
|
+
fit: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>',
|
|
905
|
+
one: '<path d="M9 8l3-2v12"/><path d="M5 20h14"/>',
|
|
906
|
+
sel: '<circle cx="12" cy="12" r="3.2"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3"/>',
|
|
907
|
+
full: '<path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/>'
|
|
908
|
+
};
|
|
909
|
+
function install3(ctx) {
|
|
910
|
+
const { els, model } = ctx;
|
|
911
|
+
const vp = els.vp;
|
|
912
|
+
let cached = { w: vp.clientWidth, h: vp.clientHeight };
|
|
913
|
+
const size = () => cached;
|
|
914
|
+
let vpRect = vp.getBoundingClientRect();
|
|
915
|
+
const refreshRect = () => {
|
|
916
|
+
vpRect = vp.getBoundingClientRect();
|
|
917
|
+
};
|
|
918
|
+
let animTimer = 0;
|
|
919
|
+
let raf = 0;
|
|
920
|
+
function applyView() {
|
|
921
|
+
const v = ctx.view;
|
|
922
|
+
els.world.style.transform = `translate(${v.x}px,${v.y}px) scale(${v.k})`;
|
|
923
|
+
vp.style.setProperty("--inv", Math.max(1, 0.9 / v.k).toFixed(3));
|
|
924
|
+
vp.style.setProperty("--k", v.k.toFixed(4));
|
|
925
|
+
vp.classList.toggle("lod1", v.k < 0.62);
|
|
926
|
+
vp.classList.toggle("lod2", v.k < 0.4);
|
|
927
|
+
els.zoom.textContent = Math.round(v.k * 100) + "%";
|
|
928
|
+
ctx.fn.stickLabels();
|
|
929
|
+
ctx.fn.placeHeaders();
|
|
930
|
+
if (!raf) raf = requestAnimationFrame(() => {
|
|
931
|
+
raf = 0;
|
|
932
|
+
if (document.body.classList.contains("notes")) ctx.fn.placeMarkers();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function setView(v, animate = false) {
|
|
936
|
+
const c = clampPan(v, ctx.geom.bounds, size());
|
|
937
|
+
ctx.view = [c.x, c.y, c.k].every(Number.isFinite) ? c : { x: 0, y: 0, k: 1 };
|
|
938
|
+
const glide = animate && !ctx.reduced;
|
|
939
|
+
vp.classList.toggle("anim", glide);
|
|
940
|
+
if (glide) {
|
|
941
|
+
clearTimeout(animTimer);
|
|
942
|
+
animTimer = window.setTimeout(() => vp.classList.remove("anim"), 330);
|
|
943
|
+
}
|
|
944
|
+
applyView();
|
|
945
|
+
}
|
|
946
|
+
function fit(animate = false, rect, inset = 0) {
|
|
947
|
+
const r = rect ?? ctx.geom.bounds;
|
|
948
|
+
if (!(r.w > 0)) return;
|
|
949
|
+
const s = size();
|
|
950
|
+
setView(fitRect(r, { w: s.w - inset, h: s.h }, 28, HEAD + 8), animate);
|
|
951
|
+
}
|
|
952
|
+
function fitW(animate = false) {
|
|
953
|
+
setView(fitWidth(ctx.geom.bounds, size(), 28, HEAD + 8), animate);
|
|
954
|
+
}
|
|
955
|
+
function zoomBy(f) {
|
|
956
|
+
const s = size();
|
|
957
|
+
ctx.userMoved = true;
|
|
958
|
+
setView(zoomAt(ctx.view, f, s.w / 2, s.h / 2), true);
|
|
959
|
+
ctx.fn.announce("Zoom " + Math.round(ctx.view.k * 100) + " percent");
|
|
960
|
+
}
|
|
961
|
+
function placeHeaders() {
|
|
962
|
+
const cols = els.cols;
|
|
963
|
+
if (!cols.firstChild) {
|
|
964
|
+
model.columns.forEach((c, i2) => {
|
|
965
|
+
const h = el("div", "hd");
|
|
966
|
+
h.textContent = (c.short ?? c.title).toUpperCase();
|
|
967
|
+
h.title = c.title;
|
|
968
|
+
h.dataset.i = String(i2);
|
|
969
|
+
cols.appendChild(h);
|
|
970
|
+
});
|
|
971
|
+
let i = 0;
|
|
972
|
+
while (i < model.columns.length) {
|
|
973
|
+
const name = model.columns[i]?.phase;
|
|
974
|
+
let j = i;
|
|
975
|
+
while (j + 1 < model.columns.length && model.columns[j + 1]?.phase === name) j++;
|
|
976
|
+
if (name) {
|
|
977
|
+
const p = el("div", "ph");
|
|
978
|
+
p.innerHTML = "<span></span><i></i>";
|
|
979
|
+
p.firstElementChild.textContent = name;
|
|
980
|
+
p.dataset.a = String(i);
|
|
981
|
+
p.dataset.b = String(j);
|
|
982
|
+
cols.appendChild(p);
|
|
983
|
+
}
|
|
984
|
+
i = j + 1;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const v = ctx.view, xs = ctx.geom.xs;
|
|
988
|
+
cols.querySelectorAll(".hd").forEach((h) => {
|
|
989
|
+
const i = +(h.dataset.i ?? 0);
|
|
990
|
+
h.style.left = v.x + (xs[i] ?? 0) * v.k + "px";
|
|
991
|
+
h.style.width = Math.max(0, (model.columns[i]?.width ?? 0) * v.k - 6) + "px";
|
|
992
|
+
});
|
|
993
|
+
cols.querySelectorAll(".ph").forEach((p) => {
|
|
994
|
+
const a = +(p.dataset.a ?? 0), b = +(p.dataset.b ?? 0);
|
|
995
|
+
p.style.left = v.x + (xs[a] ?? 0) * v.k + "px";
|
|
996
|
+
p.style.width = Math.max(40, ((xs[b] ?? 0) + (model.columns[b]?.width ?? 0) - (xs[a] ?? 0)) * v.k) + "px";
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
function stickLabels() {
|
|
1000
|
+
const wl = (52 - ctx.view.x) / ctx.view.k;
|
|
1001
|
+
for (const l of model.lanes) {
|
|
1002
|
+
const r = ctx.geom.lanes[l.id], d = ctx.laneEls.get(l.id);
|
|
1003
|
+
if (!r || !d?.firstElementChild) continue;
|
|
1004
|
+
d.firstElementChild.style.left = Math.min(Math.max(12, wl - r.x), Math.max(12, r.w - 300)) + "px";
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const pointers = /* @__PURE__ */ new Map();
|
|
1008
|
+
let drag = null;
|
|
1009
|
+
let pinch = null;
|
|
1010
|
+
on(ctx, vp, "pointerdown", (e) => {
|
|
1011
|
+
if (e.target.closest("#tools, .bandlabel")) return;
|
|
1012
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
1013
|
+
ctx.moved = 0;
|
|
1014
|
+
if (pointers.size === 2) {
|
|
1015
|
+
const [a, b] = [...pointers.values()];
|
|
1016
|
+
pinch = { d: Math.hypot(a.x - b.x, a.y - b.y), view: { ...ctx.view } };
|
|
1017
|
+
drag = null;
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
const onNode = !!e.target.closest(".node");
|
|
1021
|
+
if (e.button === 1 || ctx.tool === "hand" || ctx.spaceDown || !onNode) {
|
|
1022
|
+
drag = { x: e.clientX, y: e.clientY, vx: ctx.view.x, vy: ctx.view.y };
|
|
1023
|
+
try {
|
|
1024
|
+
vp.setPointerCapture(e.pointerId);
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
vp.classList.add("panning");
|
|
1028
|
+
if (e.button === 1) e.preventDefault();
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
on(ctx, vp, "pointermove", (e) => {
|
|
1032
|
+
if (pointers.has(e.pointerId)) pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
1033
|
+
if (pinch && pointers.size === 2) {
|
|
1034
|
+
const [a, b] = [...pointers.values()];
|
|
1035
|
+
const d = Math.hypot(a.x - b.x, a.y - b.y), r = vpRect;
|
|
1036
|
+
ctx.userMoved = true;
|
|
1037
|
+
setView(zoomAt(pinch.view, d / pinch.d, (a.x + b.x) / 2 - r.left, (a.y + b.y) / 2 - r.top), false);
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (!drag) return;
|
|
1041
|
+
const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
|
|
1042
|
+
ctx.moved = Math.max(ctx.moved, Math.hypot(dx, dy));
|
|
1043
|
+
if (ctx.moved > 4) {
|
|
1044
|
+
ctx.userMoved = true;
|
|
1045
|
+
setView({ k: ctx.view.k, x: drag.vx + dx, y: drag.vy + dy }, false);
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
const endDrag = (e) => {
|
|
1049
|
+
pointers.delete(e.pointerId);
|
|
1050
|
+
if (pointers.size < 2) pinch = null;
|
|
1051
|
+
if (drag) {
|
|
1052
|
+
drag = null;
|
|
1053
|
+
vp.classList.remove("panning");
|
|
1054
|
+
try {
|
|
1055
|
+
vp.releasePointerCapture(e.pointerId);
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
window.setTimeout(() => {
|
|
1059
|
+
ctx.moved = 0;
|
|
1060
|
+
}, 0);
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
on(ctx, vp, "pointerup", endDrag);
|
|
1064
|
+
on(ctx, vp, "pointercancel", endDrag);
|
|
1065
|
+
on(ctx, vp, "wheel", (e) => {
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
const r = vpRect, unit = e.deltaMode === 1 ? 16 : 1;
|
|
1068
|
+
ctx.userMoved = true;
|
|
1069
|
+
setView(zoomAt(ctx.view, Math.exp(-e.deltaY * unit * (e.ctrlKey ? 0.01 : 12e-4)), e.clientX - r.left, e.clientY - r.top), false);
|
|
1070
|
+
}, { passive: false });
|
|
1071
|
+
on(ctx, vp, "dblclick", (e) => {
|
|
1072
|
+
if (!e.target.closest(".node, .bandlabel, #tools")) {
|
|
1073
|
+
ctx.userMoved = false;
|
|
1074
|
+
fit(true);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
on(ctx, vp, "click", (e) => {
|
|
1078
|
+
if (!e.target.closest(".node, .bandlabel, #tools") && ctx.moved <= 4) ctx.fn.select(null);
|
|
1079
|
+
});
|
|
1080
|
+
const ro = new ResizeObserver(() => {
|
|
1081
|
+
cached = { w: vp.clientWidth, h: vp.clientHeight };
|
|
1082
|
+
refreshRect();
|
|
1083
|
+
if (!ctx.userMoved) fitW(false);
|
|
1084
|
+
else setView(ctx.view, false);
|
|
1085
|
+
});
|
|
1086
|
+
ro.observe(vp);
|
|
1087
|
+
on(ctx, vp, "pointerenter", refreshRect);
|
|
1088
|
+
on(ctx, window, "scroll", refreshRect, { passive: true });
|
|
1089
|
+
ctx.disposers.push(() => ro.disconnect());
|
|
1090
|
+
function setTool(id) {
|
|
1091
|
+
ctx.tool = id;
|
|
1092
|
+
vp.classList.toggle("hand", id === "hand");
|
|
1093
|
+
els.tools.querySelectorAll("[data-id='select'], [data-id='hand']").forEach((b) => b.setAttribute("aria-pressed", String(b.dataset.id === id)));
|
|
1094
|
+
ctx.fn.announce(id === "hand" ? "Hand tool: drag to pan" : "Select tool");
|
|
1095
|
+
}
|
|
1096
|
+
function toggleFull() {
|
|
1097
|
+
const ws = els.workspace;
|
|
1098
|
+
if (document.fullscreenElement) void document.exitFullscreen();
|
|
1099
|
+
else if (ws.requestFullscreen) ws.requestFullscreen().catch(() => ws.classList.toggle("fs"));
|
|
1100
|
+
else ws.classList.toggle("fs");
|
|
1101
|
+
}
|
|
1102
|
+
const TOOLS = [
|
|
1103
|
+
["select", "Select (V)", "tool"],
|
|
1104
|
+
["hand", "Hand: drag to pan (H)", "tool"],
|
|
1105
|
+
["-"],
|
|
1106
|
+
["plus", "Zoom in (+)", () => zoomBy(1.25)],
|
|
1107
|
+
["minus", "Zoom out (-)", () => zoomBy(1 / 1.25)],
|
|
1108
|
+
["fit", "Fit to screen (0)", () => {
|
|
1109
|
+
ctx.userMoved = false;
|
|
1110
|
+
fit(true);
|
|
1111
|
+
ctx.fn.announce("Fit to screen");
|
|
1112
|
+
}],
|
|
1113
|
+
["one", "Actual size (1)", () => {
|
|
1114
|
+
const s = size();
|
|
1115
|
+
ctx.userMoved = true;
|
|
1116
|
+
setView(zoomAt(ctx.view, 1 / ctx.view.k, s.w / 2, s.h / 2), true);
|
|
1117
|
+
}],
|
|
1118
|
+
["sel", "Zoom to selection (F)", () => ctx.fn.zoomToPath()],
|
|
1119
|
+
["-"],
|
|
1120
|
+
["full", "Fullscreen", "full"]
|
|
1121
|
+
];
|
|
1122
|
+
els.tools.innerHTML = "";
|
|
1123
|
+
for (const t of TOOLS) {
|
|
1124
|
+
if (t[0] === "-") {
|
|
1125
|
+
els.tools.appendChild(el("div", "sep"));
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const [id, label, act] = t;
|
|
1129
|
+
const b = el("button", "tb", `<svg viewBox="0 0 24 24" aria-hidden="true">${ICON[id] ?? ""}</svg>`);
|
|
1130
|
+
b.type = "button";
|
|
1131
|
+
b.title = label;
|
|
1132
|
+
b.setAttribute("aria-label", label);
|
|
1133
|
+
b.dataset.id = id;
|
|
1134
|
+
if (act === "tool") {
|
|
1135
|
+
b.setAttribute("aria-pressed", String(ctx.tool === id));
|
|
1136
|
+
on(ctx, b, "click", () => setTool(id));
|
|
1137
|
+
} else if (act === "full") on(ctx, b, "click", toggleFull);
|
|
1138
|
+
else on(ctx, b, "click", act);
|
|
1139
|
+
els.tools.appendChild(b);
|
|
1140
|
+
}
|
|
1141
|
+
requestAnimationFrame(() => {
|
|
1142
|
+
const r = els.tools.getBoundingClientRect(), v = vp.getBoundingClientRect();
|
|
1143
|
+
els.zoom.style.top = r.bottom - v.top + 6 + "px";
|
|
1144
|
+
});
|
|
1145
|
+
on(ctx, document, "fullscreenchange", () => window.setTimeout(() => {
|
|
1146
|
+
if (!ctx.userMoved) fitW(false);
|
|
1147
|
+
}, 60));
|
|
1148
|
+
on(ctx, document, "keydown", (e) => {
|
|
1149
|
+
const typing = !!e.target && /INPUT|TEXTAREA/.test(e.target.tagName);
|
|
1150
|
+
if (typing) {
|
|
1151
|
+
if (e.key === "Escape") e.target.blur();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
1155
|
+
if (e.key === " " && document.activeElement === vp) {
|
|
1156
|
+
ctx.spaceDown = true;
|
|
1157
|
+
e.preventDefault();
|
|
1158
|
+
vp.classList.add("hand");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const k = e.key, step = e.shiftKey ? 240 : 60;
|
|
1162
|
+
if (k === "/") {
|
|
1163
|
+
e.preventDefault();
|
|
1164
|
+
els.q.focus();
|
|
1165
|
+
} else if (k === "n" || k === "N") ctx.fn.toggleNotes();
|
|
1166
|
+
else if (k === "v" || k === "V") setTool("select");
|
|
1167
|
+
else if (k === "h" || k === "H") setTool("hand");
|
|
1168
|
+
else if (k === "+" || k === "=") zoomBy(1.25);
|
|
1169
|
+
else if (k === "-" || k === "_") zoomBy(1 / 1.25);
|
|
1170
|
+
else if (k === "0") {
|
|
1171
|
+
ctx.userMoved = false;
|
|
1172
|
+
fit(true);
|
|
1173
|
+
} else if (k === "1") {
|
|
1174
|
+
const s = size();
|
|
1175
|
+
ctx.userMoved = true;
|
|
1176
|
+
setView(zoomAt(ctx.view, 1 / ctx.view.k, s.w / 2, s.h / 2), true);
|
|
1177
|
+
} else if (k === "f" || k === "F") ctx.fn.zoomToPath();
|
|
1178
|
+
else if (k.startsWith("Arrow") && (document.activeElement === vp || document.activeElement === document.body)) {
|
|
1179
|
+
e.preventDefault();
|
|
1180
|
+
ctx.userMoved = true;
|
|
1181
|
+
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);
|
|
1182
|
+
} else if (k === "Escape") ctx.fn.escape();
|
|
1183
|
+
});
|
|
1184
|
+
on(ctx, document, "keyup", (e) => {
|
|
1185
|
+
if (e.key === " ") {
|
|
1186
|
+
ctx.spaceDown = false;
|
|
1187
|
+
if (ctx.tool !== "hand") vp.classList.remove("hand");
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
ctx.fn.setView = setView;
|
|
1191
|
+
ctx.fn.fit = fit;
|
|
1192
|
+
ctx.fn.fitWidth = fitW;
|
|
1193
|
+
ctx.fn.zoomBy = zoomBy;
|
|
1194
|
+
ctx.fn.stickLabels = stickLabels;
|
|
1195
|
+
ctx.fn.placeHeaders = placeHeaders;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/inspector.ts
|
|
1199
|
+
var BUCKET_COLOR = { good: "ok", neutral: "muted", gap: "warn" };
|
|
1200
|
+
var PILL = { ok: "ok", warn: "warn", crit: "warn", info: "info", accent: "info", muted: "muted" };
|
|
1201
|
+
function install4(ctx) {
|
|
1202
|
+
const { els, model } = ctx;
|
|
1203
|
+
const matchesFilter = (n, f) => !f || (f.col === void 0 || n.col === f.col) && (f.kind === void 0 || n.kind === f.kind);
|
|
1204
|
+
function wire(box) {
|
|
1205
|
+
box.querySelector("#hideInsp")?.addEventListener("click", () => {
|
|
1206
|
+
if (ctx.selected) ctx.fn.select(null);
|
|
1207
|
+
else {
|
|
1208
|
+
ctx.dismissed = true;
|
|
1209
|
+
ctx.fn.renderInspector();
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
box.querySelectorAll("[data-hl]").forEach((b) => b.addEventListener("click", () => {
|
|
1213
|
+
ctx.hl = ctx.hl === b.dataset.hl ? null : b.dataset.hl ?? null;
|
|
1214
|
+
ctx.fn.trace(ctx.selected);
|
|
1215
|
+
ctx.fn.renderInspector();
|
|
1216
|
+
}));
|
|
1217
|
+
box.querySelectorAll("[data-g]").forEach((b) => b.addEventListener("click", () => {
|
|
1218
|
+
ctx.fn.select(b.dataset.g ?? null);
|
|
1219
|
+
ctx.fn.zoomToPath();
|
|
1220
|
+
}));
|
|
1221
|
+
box.querySelectorAll("[data-act]").forEach((b) => b.addEventListener("click", () => ctx.fn.simulate(b.dataset.act ?? "")));
|
|
1222
|
+
box.querySelector("#simReset")?.addEventListener("click", () => {
|
|
1223
|
+
ctx.fn.clearSim();
|
|
1224
|
+
ctx.fn.renderInspector();
|
|
1225
|
+
ctx.fn.announce("Simulation reset.");
|
|
1226
|
+
});
|
|
1227
|
+
box.querySelector("#zPath")?.addEventListener("click", () => ctx.fn.zoomToPath());
|
|
1228
|
+
box.querySelector("#zBand")?.addEventListener("click", () => {
|
|
1229
|
+
const n = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
|
|
1230
|
+
if (n) ctx.fn.zoomToBand(n.lane);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
function renderInspector() {
|
|
1234
|
+
const box = els.inspector, ws = els.workspace;
|
|
1235
|
+
const sel = ctx.selected ? ctx.nodes.get(ctx.selected) : void 0;
|
|
1236
|
+
const head = `<div class="insp-h"><span>Inspector</span><button type="button" id="hideInsp" aria-label="Close the inspector">Close \u2715</button></div>`;
|
|
1237
|
+
if (!sel) {
|
|
1238
|
+
const found = ctx.query && !ctx.dismissed ? model.nodes.filter((n) => nodeVisible(model, ctx.mode, n) && matches(ctx, n)) : [];
|
|
1239
|
+
const open = found.length > 0;
|
|
1240
|
+
box.classList.toggle("open", open);
|
|
1241
|
+
ws.classList.toggle("inspopen", open);
|
|
1242
|
+
box.setAttribute("aria-hidden", String(!open));
|
|
1243
|
+
if (!open) return;
|
|
1244
|
+
const rows = found.map((f) => {
|
|
1245
|
+
const b = bucketOf(model, nodeStateId(model, ctx.mode, f));
|
|
1246
|
+
const hits = (f.members ?? []).filter((m) => m.toLowerCase().includes(ctx.query)).length;
|
|
1247
|
+
return `<button type="button" class="dr" data-g="${esc(f.id)}"><span class="dbar" style="background:var(--${BUCKET_COLOR[b]})"></span><span class="dtx"><span class="dn">${esc(f.title)}</span><span class="dk">${hits} match \xB7 ${esc(model.columns[f.col]?.title ?? "")}</span></span></button>`;
|
|
1248
|
+
}).join("");
|
|
1249
|
+
box.innerHTML = head + `<div class="insp-b"><div class="insp-sec"><b>${found.length}</b> group${found.length === 1 ? "" : "s"} match \u201C${esc(ctx.query)}\u201D</div><div class="drill">${rows}</div></div>`;
|
|
1250
|
+
wire(box);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
box.classList.add("open");
|
|
1254
|
+
ws.classList.add("inspopen");
|
|
1255
|
+
box.setAttribute("aria-hidden", "false");
|
|
1256
|
+
const sid = nodeStateId(model, ctx.mode, sel);
|
|
1257
|
+
const st = model.states[sid];
|
|
1258
|
+
const bucket = bucketOf(model, sid);
|
|
1259
|
+
const lane = model.lanes.find((l) => l.id === sel.lane);
|
|
1260
|
+
const counts = relationsOf(ctx).slice(0, 3).map((r) => {
|
|
1261
|
+
const set = relationSet(ctx, r, sel.id);
|
|
1262
|
+
let n = 0;
|
|
1263
|
+
for (const id of set) {
|
|
1264
|
+
const x = ctx.nodes.get(id);
|
|
1265
|
+
if (!x) continue;
|
|
1266
|
+
if (r.filter) {
|
|
1267
|
+
if (matchesFilter(x, r.filter)) n += Math.max(1, x.weight ?? 1);
|
|
1268
|
+
} else if (id !== sel.id) n += 1;
|
|
1269
|
+
}
|
|
1270
|
+
const cls = r.id === "up" ? "cn--up" : r.id === "down" ? "cn--down" : "";
|
|
1271
|
+
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>`;
|
|
1272
|
+
}).join("");
|
|
1273
|
+
const pills = `<span class="pill ${PILL[st?.tone ?? "muted"]}">${esc(st?.word ?? sid)}</span>` + (isImproved(model, ctx.mode, sel) ? '<span class="pill info">gap filled</span>' : "") + (isNew(model, ctx.mode, sel) ? '<span class="pill muted">new in this view</span>' : "");
|
|
1274
|
+
const actions = (model.inspector?.actions ?? []).map((a) => `<button type="button" class="danger" data-act="${esc(a.id)}">\u25B6 ${esc(a.label)}</button>`).join("");
|
|
1275
|
+
const members = sel.members ?? [];
|
|
1276
|
+
box.innerHTML = head + `<div class="counts">${counts}</div>
|
|
1277
|
+
<div class="insp-b"><div class="insp-sec"><p class="nname">${esc(sel.title)}</p><div class="nkind">${esc(model.columns[sel.col]?.title ?? "")} \xB7 ${esc(lane?.title ?? sel.lane)}</div><div class="pillrow">${pills}</div>${sel.subtitle ? `<p class="note">${esc(sel.subtitle)}</p>` : ""}</div>
|
|
1278
|
+
<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>
|
|
1279
|
+
${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>`}
|
|
1280
|
+
<div class="acts"><button type="button" id="zPath">Zoom to path</button><button type="button" id="zBand">Go to lane</button></div>
|
|
1281
|
+
${actions ? `<div class="acts">${actions}<button type="button" id="simReset"${ctx.simActive ? "" : " hidden"}>Reset</button></div>` : ""}</div>`;
|
|
1282
|
+
wire(box);
|
|
1283
|
+
const mq = box.querySelector("#mq");
|
|
1284
|
+
const draw = () => {
|
|
1285
|
+
const f = (mq?.value ?? "").toLowerCase();
|
|
1286
|
+
const rows = members.filter((m) => m.toLowerCase().includes(f));
|
|
1287
|
+
const list = box.querySelector("#mlist");
|
|
1288
|
+
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("");
|
|
1289
|
+
const cnt = box.querySelector("#mcount");
|
|
1290
|
+
const weight = sel.weight ?? 0;
|
|
1291
|
+
if (cnt) cnt.textContent = `${Math.min(14, rows.length)} of ${rows.length}${weight > members.length ? ` named (${weight} members in all)` : ""}`;
|
|
1292
|
+
};
|
|
1293
|
+
if (mq) {
|
|
1294
|
+
mq.addEventListener("input", draw);
|
|
1295
|
+
draw();
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
ctx.fn.renderInspector = renderInspector;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/chrome.ts
|
|
1302
|
+
var DEFAULT_LABELS = {
|
|
1303
|
+
tileGood: "links in a good state",
|
|
1304
|
+
tileNeutral: "links in a neutral state",
|
|
1305
|
+
tileGap: "groups with a gap: click to show",
|
|
1306
|
+
tileUpgrade: "groups you can improve: click to show",
|
|
1307
|
+
ledgerTitle: "What this view could not establish",
|
|
1308
|
+
tableTitle: "Table view"
|
|
1309
|
+
};
|
|
1310
|
+
function install5(ctx) {
|
|
1311
|
+
const { els, model } = ctx;
|
|
1312
|
+
const label = (k) => model.meta.labels?.[k] ?? DEFAULT_LABELS[k] ?? k;
|
|
1313
|
+
const visible = (id) => {
|
|
1314
|
+
const n = ctx.nodes.get(id);
|
|
1315
|
+
return !!n && nodeVisible(model, ctx.mode, n);
|
|
1316
|
+
};
|
|
1317
|
+
function countUp(node, to, ms = 700) {
|
|
1318
|
+
if (ctx.reduced) {
|
|
1319
|
+
node.textContent = String(to);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
const t0 = performance.now();
|
|
1323
|
+
const step = (t) => {
|
|
1324
|
+
const k = Math.min(1, (t - t0) / ms), e = 1 - Math.pow(1 - k, 3);
|
|
1325
|
+
node.textContent = String(Math.round(to * e));
|
|
1326
|
+
if (k < 1) requestAnimationFrame(step);
|
|
1327
|
+
};
|
|
1328
|
+
requestAnimationFrame(step);
|
|
1329
|
+
}
|
|
1330
|
+
function buildStrip() {
|
|
1331
|
+
const box = els.strip;
|
|
1332
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, model.columns.length)}, minmax(0, 1fr))`;
|
|
1333
|
+
if (!box.firstChild) {
|
|
1334
|
+
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("");
|
|
1335
|
+
}
|
|
1336
|
+
box.querySelectorAll(".lcell").forEach((cell, i) => {
|
|
1337
|
+
cell.style.setProperty("--i", String(i));
|
|
1338
|
+
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1339
|
+
for (const n of model.nodes) {
|
|
1340
|
+
if (n.col !== i || n.kind === "chip") continue;
|
|
1341
|
+
const w = Math.max(1, n.weight ?? 1);
|
|
1342
|
+
tot += w;
|
|
1343
|
+
if (!visible(n.id)) continue;
|
|
1344
|
+
const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
|
|
1345
|
+
if (b === "good") good += w;
|
|
1346
|
+
else if (b === "neutral") neutral += w;
|
|
1347
|
+
else gap += w;
|
|
1348
|
+
}
|
|
1349
|
+
const t = tot || 1, bars = cell.querySelectorAll("i");
|
|
1350
|
+
requestAnimationFrame(() => {
|
|
1351
|
+
if (bars[0]) bars[0].style.width = good / t * 100 + "%";
|
|
1352
|
+
if (bars[1]) bars[1].style.width = neutral / t * 100 + "%";
|
|
1353
|
+
if (bars[2]) bars[2].style.width = gap / t * 100 + "%";
|
|
1354
|
+
});
|
|
1355
|
+
const lv = cell.querySelector(".lv");
|
|
1356
|
+
if (lv) lv.textContent = `${good + neutral} / ${tot}`;
|
|
1357
|
+
cell.title = `${model.columns[i]?.title ?? ""}: ${good} good, ${neutral} neutral, ${gap} gap`;
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
function buildTiles() {
|
|
1361
|
+
const k = els.tiles;
|
|
1362
|
+
let good = 0, neutral = 0;
|
|
1363
|
+
for (const l of ctx.links) {
|
|
1364
|
+
if (!linkVisible(model, ctx.mode, l, ctx.nodes)) continue;
|
|
1365
|
+
const b = bucketOf(model, linkStateId(model, ctx.mode, l));
|
|
1366
|
+
if (b === "good") good++;
|
|
1367
|
+
else if (b === "neutral") neutral++;
|
|
1368
|
+
}
|
|
1369
|
+
let gaps = 0, upgrades = 0;
|
|
1370
|
+
for (const n of model.nodes) {
|
|
1371
|
+
if (!visible(n.id) || n.kind === "chip") continue;
|
|
1372
|
+
const b = bucketOf(model, nodeStateId(model, ctx.mode, n));
|
|
1373
|
+
if (b === "gap") gaps += 1;
|
|
1374
|
+
if (n.upgrade && b !== "good") upgrades++;
|
|
1375
|
+
}
|
|
1376
|
+
k.innerHTML = "";
|
|
1377
|
+
const tile = (cls, n, text, filter) => {
|
|
1378
|
+
const b = el(filter ? "button" : "div", "tile " + cls, `<span class="tn">0</span><span class="tl">${esc(text)}</span>`);
|
|
1379
|
+
b.style.setProperty("--i", String(k.children.length + 9));
|
|
1380
|
+
if (filter) {
|
|
1381
|
+
b.type = "button";
|
|
1382
|
+
b.setAttribute("aria-pressed", String(ctx.filter === filter));
|
|
1383
|
+
b.addEventListener("click", () => {
|
|
1384
|
+
ctx.filter = ctx.filter === filter ? null : filter;
|
|
1385
|
+
ctx.fn.select(null);
|
|
1386
|
+
buildTiles();
|
|
1387
|
+
ctx.fn.trace(null);
|
|
1388
|
+
ctx.fn.announce(ctx.filter ? "Filter on" : "Filter off");
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
k.appendChild(b);
|
|
1392
|
+
countUp(b.querySelector(".tn"), n);
|
|
1393
|
+
};
|
|
1394
|
+
tile("ok", good, label("tileGood"));
|
|
1395
|
+
tile("", neutral, label("tileNeutral"));
|
|
1396
|
+
tile("warn", gaps, label("tileGap"), "gap");
|
|
1397
|
+
tile("", upgrades, label("tileUpgrade"), "upgrade");
|
|
1398
|
+
}
|
|
1399
|
+
function buildLegend() {
|
|
1400
|
+
const parts = Object.entries(model.states).map(([id, s]) => {
|
|
1401
|
+
const cap = s.edge === "dot" ? ' stroke-linecap="round"' : "";
|
|
1402
|
+
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>`;
|
|
1403
|
+
});
|
|
1404
|
+
parts.push('<span id="hintline">scroll = zoom \xB7 drag = pan \xB7 double-click = fit</span>');
|
|
1405
|
+
els.legend.innerHTML = parts.join("");
|
|
1406
|
+
}
|
|
1407
|
+
function buildLedger() {
|
|
1408
|
+
let gaps = 0, total = 0;
|
|
1409
|
+
for (const n of model.nodes) {
|
|
1410
|
+
if (n.kind === "chip" || !visible(n.id)) continue;
|
|
1411
|
+
total++;
|
|
1412
|
+
if (bucketOf(model, nodeStateId(model, ctx.mode, n)) === "gap") gaps++;
|
|
1413
|
+
}
|
|
1414
|
+
const noLane = model.lanes.filter((l) => l.gap).length;
|
|
1415
|
+
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>`;
|
|
1416
|
+
}
|
|
1417
|
+
function buildTable() {
|
|
1418
|
+
const rows = model.nodes.filter((n) => visible(n.id) && n.kind !== "chip").map((n) => {
|
|
1419
|
+
const sid = nodeStateId(model, ctx.mode, n);
|
|
1420
|
+
const lane = model.lanes.find((l) => l.id === n.lane)?.title ?? n.lane;
|
|
1421
|
+
return `<tr><th scope="row">${esc(model.columns[n.col]?.title ?? "")}</th><td>${esc(lane)}</td><td>${esc(n.title)}</td><td>${esc(model.states[sid]?.word ?? sid)}</td><td>${Math.max(1, n.weight ?? 1)}</td><td>${esc(n.subtitle ?? "")}</td><td>${esc(n.upgrade ?? "")}</td></tr>`;
|
|
1422
|
+
}).join("");
|
|
1423
|
+
els.table.innerHTML = `<thead><tr><th scope="col">Column</th><th scope="col">Lane</th><th scope="col">Group</th><th scope="col">State</th><th scope="col">Members</th><th scope="col">Detail</th><th scope="col">Can improve</th></tr></thead><tbody>${rows}</tbody>`;
|
|
1424
|
+
}
|
|
1425
|
+
ctx.fn.buildChrome = () => {
|
|
1426
|
+
buildStrip();
|
|
1427
|
+
buildTiles();
|
|
1428
|
+
buildTable();
|
|
1429
|
+
buildLedger();
|
|
1430
|
+
};
|
|
1431
|
+
document.title = model.meta.title;
|
|
1432
|
+
els.title.textContent = model.meta.title;
|
|
1433
|
+
els.help.textContent = model.meta.help;
|
|
1434
|
+
els.readat.textContent = model.meta.asOf ? `as of ${model.meta.asOf}` : "";
|
|
1435
|
+
els.footer.textContent = model.meta.source;
|
|
1436
|
+
buildLegend();
|
|
1437
|
+
const views = model.views ?? [];
|
|
1438
|
+
els.modeSwitch.hidden = views.length < 2;
|
|
1439
|
+
els.modeSwitch.innerHTML = "";
|
|
1440
|
+
for (const v of views) {
|
|
1441
|
+
const b = el("button", "seg");
|
|
1442
|
+
b.type = "button";
|
|
1443
|
+
b.setAttribute("role", "tab");
|
|
1444
|
+
b.dataset.mode = v.id;
|
|
1445
|
+
b.setAttribute("aria-selected", String(v.id === ctx.mode));
|
|
1446
|
+
b.textContent = v.label;
|
|
1447
|
+
on(ctx, b, "click", () => {
|
|
1448
|
+
els.modeSwitch.querySelectorAll("button").forEach((x) => x.setAttribute("aria-selected", String(x === b)));
|
|
1449
|
+
ctx.fn.setMode(v.id);
|
|
1450
|
+
});
|
|
1451
|
+
els.modeSwitch.appendChild(b);
|
|
1452
|
+
}
|
|
1453
|
+
on(ctx, els.q, "input", (e) => {
|
|
1454
|
+
ctx.query = e.target.value.trim().toLowerCase();
|
|
1455
|
+
ctx.dismissed = false;
|
|
1456
|
+
const n = model.nodes.filter((x) => visible(x.id) && matches(ctx, x)).length;
|
|
1457
|
+
els.matchinfo.textContent = ctx.query ? `${n} group${n === 1 ? "" : "s"} match` : "";
|
|
1458
|
+
if (!ctx.selected) {
|
|
1459
|
+
ctx.fn.trace(null);
|
|
1460
|
+
ctx.fn.renderInspector();
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
on(ctx, els.q, "keydown", (e) => {
|
|
1464
|
+
if (e.key !== "Enter" || !ctx.query) return;
|
|
1465
|
+
const ids = model.nodes.filter((x) => visible(x.id) && matches(ctx, x)).map((x) => x.id);
|
|
1466
|
+
const r = ctx.fn.rectOf(ids);
|
|
1467
|
+
if (r) {
|
|
1468
|
+
ctx.userMoved = true;
|
|
1469
|
+
ctx.fn.fit(true, r, els.inspector.classList.contains("open") ? 340 : 0);
|
|
1470
|
+
ctx.fn.announce(`Zoomed to ${ids.length} matching groups`);
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
on(ctx, els.theme, "click", () => {
|
|
1474
|
+
const r = document.documentElement;
|
|
1475
|
+
const dark = r.dataset.theme ? r.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
1476
|
+
r.dataset.theme = dark ? "light" : "dark";
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/notes.ts
|
|
1481
|
+
function install6(ctx) {
|
|
1482
|
+
const { els, model } = ctx;
|
|
1483
|
+
const notes = ctx.notes.map((n, i) => ({ id: i + 1, ...n }));
|
|
1484
|
+
const card = (id) => els.notelist.querySelector(`[data-n="${id}"]`);
|
|
1485
|
+
const anchorOf = (sel) => {
|
|
1486
|
+
try {
|
|
1487
|
+
return document.querySelector(sel);
|
|
1488
|
+
} catch {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
function clearMarkers() {
|
|
1493
|
+
document.querySelectorAll(".marker").forEach((m) => m.remove());
|
|
1494
|
+
document.querySelectorAll(".anchored").forEach((a) => a.classList.remove("anchored"));
|
|
1495
|
+
}
|
|
1496
|
+
function placeMarkers() {
|
|
1497
|
+
clearMarkers();
|
|
1498
|
+
for (const n of notes) {
|
|
1499
|
+
const a = anchorOf(n.anchor);
|
|
1500
|
+
if (!a) continue;
|
|
1501
|
+
let r = a.getBoundingClientRect();
|
|
1502
|
+
if (!r.width && !r.height) continue;
|
|
1503
|
+
if (a.id === "inspector" && !a.classList.contains("open")) {
|
|
1504
|
+
const vr = els.vp.getBoundingClientRect();
|
|
1505
|
+
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: () => ({}) };
|
|
1506
|
+
}
|
|
1507
|
+
const v = els.vp.getBoundingClientRect();
|
|
1508
|
+
if (a.closest("#world") && (r.right < v.left || r.left > v.right || r.bottom < v.top || r.top > v.bottom)) continue;
|
|
1509
|
+
const m = el("button", "marker", String(n.id));
|
|
1510
|
+
m.type = "button";
|
|
1511
|
+
m.dataset.n = String(n.id);
|
|
1512
|
+
m.title = n.title;
|
|
1513
|
+
m.setAttribute("aria-label", `Note ${n.id}: ${n.title}`);
|
|
1514
|
+
m.style.left = r.left + scrollX - 10 + "px";
|
|
1515
|
+
m.style.top = r.top + scrollY - 10 + "px";
|
|
1516
|
+
m.addEventListener("mouseenter", () => {
|
|
1517
|
+
a.classList.add("anchored");
|
|
1518
|
+
card(n.id)?.classList.add("on");
|
|
1519
|
+
});
|
|
1520
|
+
m.addEventListener("mouseleave", () => {
|
|
1521
|
+
a.classList.remove("anchored");
|
|
1522
|
+
card(n.id)?.classList.remove("on");
|
|
1523
|
+
});
|
|
1524
|
+
m.addEventListener("click", () => card(n.id)?.scrollIntoView({ block: "center", behavior: ctx.reduced ? "auto" : "smooth" }));
|
|
1525
|
+
document.body.appendChild(m);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
function toggleNotes(next) {
|
|
1529
|
+
const on_ = next === void 0 ? !document.body.classList.contains("notes") : next;
|
|
1530
|
+
document.body.classList.toggle("notes", on_);
|
|
1531
|
+
els.notesBtn.setAttribute("aria-pressed", String(on_));
|
|
1532
|
+
if (on_) window.setTimeout(placeMarkers, ctx.reduced ? 30 : 340);
|
|
1533
|
+
else clearMarkers();
|
|
1534
|
+
}
|
|
1535
|
+
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>';
|
|
1536
|
+
els.notelist.querySelectorAll(".ncard").forEach((c) => {
|
|
1537
|
+
c.addEventListener("mouseenter", () => {
|
|
1538
|
+
const n = notes.find((x) => String(x.id) === c.dataset.n);
|
|
1539
|
+
const a = n && anchorOf(n.anchor);
|
|
1540
|
+
if (a) a.classList.add("anchored");
|
|
1541
|
+
});
|
|
1542
|
+
c.addEventListener("mouseleave", () => document.querySelectorAll(".anchored").forEach((a) => a.classList.remove("anchored")));
|
|
1543
|
+
});
|
|
1544
|
+
on(ctx, els.notesBtn, "click", () => toggleNotes());
|
|
1545
|
+
on(ctx, window, "scroll", () => {
|
|
1546
|
+
if (document.body.classList.contains("notes")) placeMarkers();
|
|
1547
|
+
}, { passive: true });
|
|
1548
|
+
const markdown = () => {
|
|
1549
|
+
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
1550
|
+
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}`);
|
|
1551
|
+
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
1552
|
+
out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
1553
|
+
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
1554
|
+
if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
|
|
1555
|
+
return out.join("\n");
|
|
1556
|
+
};
|
|
1557
|
+
const rows = (items) => `<ul>${items.map((i) => `<li>${i}</li>`).join("")}</ul>`;
|
|
1558
|
+
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>
|
|
1559
|
+
<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)}`))}
|
|
1560
|
+
<h3>Columns</h3>${rows(model.columns.map((c) => `${esc(c.title)}${c.phase ? ` (${esc(c.phase)})` : ""}`))}
|
|
1561
|
+
<h3>Lanes</h3>${rows(model.lanes.map((l) => `${esc(l.title)}${l.badge ? ` [${esc(l.badge)}]` : ""}${l.note ? `: ${esc(l.note)}` : ""}`))}
|
|
1562
|
+
<h3>Size</h3><p>${model.nodes.length} nodes, ${model.links.length} links.</p>`;
|
|
1563
|
+
const msg = (t) => {
|
|
1564
|
+
const m = document.getElementById("cpMsg");
|
|
1565
|
+
if (m) m.textContent = t;
|
|
1566
|
+
};
|
|
1567
|
+
const copy = async (text, what) => {
|
|
1568
|
+
try {
|
|
1569
|
+
await navigator.clipboard.writeText(text);
|
|
1570
|
+
msg(`${what} copied.`);
|
|
1571
|
+
} catch {
|
|
1572
|
+
const ta = document.createElement("textarea");
|
|
1573
|
+
ta.value = text;
|
|
1574
|
+
document.body.appendChild(ta);
|
|
1575
|
+
ta.select();
|
|
1576
|
+
try {
|
|
1577
|
+
document.execCommand("copy");
|
|
1578
|
+
msg(`${what} copied.`);
|
|
1579
|
+
} catch {
|
|
1580
|
+
msg("Copy is blocked here: select the text instead.");
|
|
1581
|
+
}
|
|
1582
|
+
ta.remove();
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
document.getElementById("cpMd")?.addEventListener("click", () => void copy(markdown(), "Markdown"));
|
|
1586
|
+
document.getElementById("cpJson")?.addEventListener("click", () => void copy(JSON.stringify(model, null, 2), "JSON"));
|
|
1587
|
+
ctx.fn.toggleNotes = toggleNotes;
|
|
1588
|
+
ctx.fn.placeMarkers = placeMarkers;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/engine.ts
|
|
1592
|
+
var IDS = {
|
|
1593
|
+
vp: "viewport",
|
|
1594
|
+
world: "world",
|
|
1595
|
+
svg: "edges",
|
|
1596
|
+
layer: "edgeLayer",
|
|
1597
|
+
cols: "cols",
|
|
1598
|
+
tools: "tools",
|
|
1599
|
+
zoom: "zoomLabel",
|
|
1600
|
+
inspector: "inspector",
|
|
1601
|
+
workspace: "workspace",
|
|
1602
|
+
live: "live",
|
|
1603
|
+
strip: "layerstrip",
|
|
1604
|
+
tiles: "kpis",
|
|
1605
|
+
legend: "legend",
|
|
1606
|
+
ledger: "ledger",
|
|
1607
|
+
table: "tbl",
|
|
1608
|
+
q: "q",
|
|
1609
|
+
matchinfo: "matchinfo",
|
|
1610
|
+
modeSwitch: "modeSwitch",
|
|
1611
|
+
drawer: "drawer",
|
|
1612
|
+
notelist: "notelist",
|
|
1613
|
+
notesBtn: "notesBtn",
|
|
1614
|
+
theme: "theme",
|
|
1615
|
+
specbody: "specbody",
|
|
1616
|
+
spec: "spec",
|
|
1617
|
+
title: "title",
|
|
1618
|
+
readat: "readat",
|
|
1619
|
+
help: "help",
|
|
1620
|
+
footer: "footer"
|
|
1621
|
+
};
|
|
1622
|
+
function findEls(root) {
|
|
1623
|
+
const out = {};
|
|
1624
|
+
for (const [key, id] of Object.entries(IDS)) {
|
|
1625
|
+
const e = root.querySelector("#" + id);
|
|
1626
|
+
if (!e) throw new Error(`flow-studio: the page has no element #${id}`);
|
|
1627
|
+
out[key] = e;
|
|
1628
|
+
}
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
function mount(root, model, opts = {}) {
|
|
1632
|
+
if (!model || !Array.isArray(model.columns) || !Array.isArray(model.lanes) || !Array.isArray(model.nodes) || !Array.isArray(model.links) || !model.states) {
|
|
1633
|
+
throw new Error("flow-studio: the model needs columns, lanes, states, nodes and links");
|
|
1634
|
+
}
|
|
1635
|
+
const els = findEls(root);
|
|
1636
|
+
const issues = validateModel(model);
|
|
1637
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
1638
|
+
for (const i of issues) (i.level === "error" ? console.error : console.warn)(`flow-studio ${i.code} ${i.where}: ${i.message}`);
|
|
1639
|
+
if (errors.length) els.help.textContent = `${errors.length} model error(s). See the console. The page shows what it can.`;
|
|
1640
|
+
const rm = matchMedia("(prefers-reduced-motion: reduce)");
|
|
1641
|
+
const ctx = {
|
|
1642
|
+
model,
|
|
1643
|
+
root,
|
|
1644
|
+
els,
|
|
1645
|
+
notes: opts.notes && opts.notes.length ? opts.notes : model.notes ?? [],
|
|
1646
|
+
nodes: new Map(model.nodes.map((n) => [n.id, n])),
|
|
1647
|
+
links: model.links,
|
|
1648
|
+
geom: { xs: [], nodes: {}, lanes: {}, bounds: { x: 0, y: 0, w: 1, h: 1 }, world: { w: 1, h: 1 } },
|
|
1649
|
+
view: { x: 0, y: 0, k: 1 },
|
|
1650
|
+
reduced: rm.matches,
|
|
1651
|
+
mode: defaultView(model),
|
|
1652
|
+
selected: null,
|
|
1653
|
+
hl: null,
|
|
1654
|
+
query: "",
|
|
1655
|
+
filter: null,
|
|
1656
|
+
tool: "select",
|
|
1657
|
+
spaceDown: false,
|
|
1658
|
+
userMoved: false,
|
|
1659
|
+
dismissed: false,
|
|
1660
|
+
simActive: false,
|
|
1661
|
+
moved: 0,
|
|
1662
|
+
nodeEls: /* @__PURE__ */ new Map(),
|
|
1663
|
+
laneEls: /* @__PURE__ */ new Map(),
|
|
1664
|
+
edgeEls: [],
|
|
1665
|
+
labelEls: [],
|
|
1666
|
+
overlays: [],
|
|
1667
|
+
simTimers: [],
|
|
1668
|
+
disposers: [],
|
|
1669
|
+
fn: {}
|
|
1670
|
+
};
|
|
1671
|
+
const onRm = (e) => {
|
|
1672
|
+
ctx.reduced = e.matches;
|
|
1673
|
+
};
|
|
1674
|
+
rm.addEventListener("change", onRm);
|
|
1675
|
+
ctx.disposers.push(() => rm.removeEventListener("change", onRm));
|
|
1676
|
+
let style = document.getElementById("state-css");
|
|
1677
|
+
if (!style) {
|
|
1678
|
+
style = document.createElement("style");
|
|
1679
|
+
style.id = "state-css";
|
|
1680
|
+
document.head.prepend(style);
|
|
1681
|
+
}
|
|
1682
|
+
style.textContent = stateCss(model.states);
|
|
1683
|
+
install(ctx);
|
|
1684
|
+
install2(ctx);
|
|
1685
|
+
install3(ctx);
|
|
1686
|
+
install4(ctx);
|
|
1687
|
+
install5(ctx);
|
|
1688
|
+
install6(ctx);
|
|
1689
|
+
document.body.classList.add("intro");
|
|
1690
|
+
window.setTimeout(() => document.body.classList.remove("intro"), 2100);
|
|
1691
|
+
ctx.fn.buildWorld();
|
|
1692
|
+
ctx.fn.paint(false);
|
|
1693
|
+
ctx.fn.fitWidth(false);
|
|
1694
|
+
window.setTimeout(() => ctx.fn.fitWidth(false), 120);
|
|
1695
|
+
const api = {
|
|
1696
|
+
model,
|
|
1697
|
+
geometry: () => ctx.geom,
|
|
1698
|
+
view: () => ({ ...ctx.view }),
|
|
1699
|
+
setView: (v, animate) => ctx.fn.setView(v, animate),
|
|
1700
|
+
fit: (animate) => {
|
|
1701
|
+
ctx.userMoved = false;
|
|
1702
|
+
ctx.fn.fit(animate);
|
|
1703
|
+
},
|
|
1704
|
+
select: (id) => ctx.fn.select(id),
|
|
1705
|
+
selected: () => ctx.selected,
|
|
1706
|
+
mode: () => ctx.mode,
|
|
1707
|
+
setMode: (id) => ctx.fn.setMode(id),
|
|
1708
|
+
simulate: (id) => ctx.fn.simulate(id),
|
|
1709
|
+
resetSimulation: () => {
|
|
1710
|
+
ctx.fn.clearSim();
|
|
1711
|
+
ctx.fn.renderInspector();
|
|
1712
|
+
},
|
|
1713
|
+
destroy: () => {
|
|
1714
|
+
ctx.fn.clearSim();
|
|
1715
|
+
ctx.disposers.forEach((d) => d());
|
|
1716
|
+
ctx.disposers = [];
|
|
1717
|
+
style?.remove();
|
|
1718
|
+
if (window.FlowStudio === api) delete window.FlowStudio;
|
|
1719
|
+
}
|
|
1720
|
+
};
|
|
1721
|
+
window.FlowStudio = api;
|
|
1722
|
+
return api;
|
|
1723
|
+
}
|
|
1724
|
+
function auto() {
|
|
1725
|
+
const m = document.getElementById("flow-model");
|
|
1726
|
+
if (!m) return;
|
|
1727
|
+
try {
|
|
1728
|
+
const model = JSON.parse(m.textContent ?? "{}");
|
|
1729
|
+
const n = document.getElementById("flow-notes");
|
|
1730
|
+
const notes = n ? JSON.parse(n.textContent ?? "[]") : void 0;
|
|
1731
|
+
mount(document, model, { notes });
|
|
1732
|
+
} catch (e) {
|
|
1733
|
+
console.error(e);
|
|
1734
|
+
const h = document.getElementById("help");
|
|
1735
|
+
if (h) h.textContent = "This page could not be drawn: " + (e instanceof Error ? e.message : String(e));
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
window.flowStudioMount = mount;
|
|
1739
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", auto);
|
|
1740
|
+
else auto();
|
|
1741
|
+
})();
|
|
1742
|
+
</script>
|
|
1743
|
+
</body>
|
|
1744
|
+
</html>
|