@caddis/cli 0.4.44 → 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.
Files changed (92) hide show
  1. package/bundles/antigravity-plugin/plugin.json +1 -1
  2. package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +15 -3
  3. package/bundles/antigravity-plugin/scripts/claude-oss.sh +12 -1
  4. package/bundles/antigravity-plugin/skills/_registry.md +1 -0
  5. package/bundles/antigravity-plugin/skills/flow-studio/README.md +53 -0
  6. package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +103 -0
  7. package/bundles/antigravity-plugin/skills/flow-studio/examples/README.md +11 -0
  8. package/bundles/antigravity-plugin/skills/flow-studio/examples/hostile.json +1783 -0
  9. package/bundles/antigravity-plugin/skills/flow-studio/examples/lineage.json +1778 -0
  10. package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +123 -0
  11. package/bundles/antigravity-plugin/skills/flow-studio/package.json +11 -0
  12. package/bundles/antigravity-plugin/skills/flow-studio/references/patterns.md +79 -0
  13. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build-engine.mjs +55 -0
  14. package/bundles/antigravity-plugin/skills/flow-studio/scripts/build.mjs +37 -0
  15. package/bundles/antigravity-plugin/skills/flow-studio/scripts/check.mjs +388 -0
  16. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/build-engine.test.mjs +26 -0
  17. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/check-selftest.test.mjs +75 -0
  18. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/cli.test.mjs +64 -0
  19. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.mjs +24 -0
  20. package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/embed.test.mjs +29 -0
  21. package/bundles/antigravity-plugin/skills/flow-studio/scripts/validate.mjs +38 -0
  22. package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +171 -0
  23. package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +231 -0
  24. package/bundles/antigravity-plugin/skills/flow-studio/src/ctx.ts +118 -0
  25. package/bundles/antigravity-plugin/skills/flow-studio/src/engine.ts +142 -0
  26. package/bundles/antigravity-plugin/skills/flow-studio/src/inspector.ts +108 -0
  27. package/bundles/antigravity-plugin/skills/flow-studio/src/interact.ts +240 -0
  28. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.test.ts +69 -0
  29. package/bundles/antigravity-plugin/skills/flow-studio/src/layout.ts +57 -0
  30. package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +107 -0
  31. package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +165 -0
  32. package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +92 -0
  33. package/bundles/antigravity-plugin/skills/flow-studio/src/render.ts +155 -0
  34. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.test.ts +13 -0
  35. package/bundles/antigravity-plugin/skills/flow-studio/src/routing.ts +11 -0
  36. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +88 -0
  37. package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +35 -0
  38. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.test.ts +66 -0
  39. package/bundles/antigravity-plugin/skills/flow-studio/src/stateCss.ts +44 -0
  40. package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +157 -0
  41. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.test.ts +20 -0
  42. package/bundles/antigravity-plugin/skills/flow-studio/src/trace.ts +22 -0
  43. package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.test.ts +55 -0
  44. package/bundles/antigravity-plugin/skills/flow-studio/src/viewport.ts +45 -0
  45. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +1744 -0
  46. package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.shell.html +61 -0
  47. package/bundles/antigravity-plugin/skills/flow-studio/tsconfig.json +17 -0
  48. package/bundles/codex/.codex/skills/frontend/flow-studio/README.md +53 -0
  49. package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +103 -0
  50. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/README.md +11 -0
  51. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/hostile.json +1783 -0
  52. package/bundles/codex/.codex/skills/frontend/flow-studio/examples/lineage.json +1778 -0
  53. package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +123 -0
  54. package/bundles/codex/.codex/skills/frontend/flow-studio/package.json +11 -0
  55. package/bundles/codex/.codex/skills/frontend/flow-studio/references/patterns.md +79 -0
  56. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build-engine.mjs +55 -0
  57. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/build.mjs +37 -0
  58. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/check.mjs +388 -0
  59. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/build-engine.test.mjs +26 -0
  60. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/check-selftest.test.mjs +75 -0
  61. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/cli.test.mjs +64 -0
  62. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.mjs +24 -0
  63. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/embed.test.mjs +29 -0
  64. package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/validate.mjs +38 -0
  65. package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +171 -0
  66. package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +231 -0
  67. package/bundles/codex/.codex/skills/frontend/flow-studio/src/ctx.ts +118 -0
  68. package/bundles/codex/.codex/skills/frontend/flow-studio/src/engine.ts +142 -0
  69. package/bundles/codex/.codex/skills/frontend/flow-studio/src/inspector.ts +108 -0
  70. package/bundles/codex/.codex/skills/frontend/flow-studio/src/interact.ts +240 -0
  71. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.test.ts +69 -0
  72. package/bundles/codex/.codex/skills/frontend/flow-studio/src/layout.ts +57 -0
  73. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +107 -0
  74. package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +165 -0
  75. package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +92 -0
  76. package/bundles/codex/.codex/skills/frontend/flow-studio/src/render.ts +155 -0
  77. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.test.ts +13 -0
  78. package/bundles/codex/.codex/skills/frontend/flow-studio/src/routing.ts +11 -0
  79. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +88 -0
  80. package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +35 -0
  81. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.test.ts +66 -0
  82. package/bundles/codex/.codex/skills/frontend/flow-studio/src/stateCss.ts +44 -0
  83. package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +157 -0
  84. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.test.ts +20 -0
  85. package/bundles/codex/.codex/skills/frontend/flow-studio/src/trace.ts +22 -0
  86. package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.test.ts +55 -0
  87. package/bundles/codex/.codex/skills/frontend/flow-studio/src/viewport.ts +45 -0
  88. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +1744 -0
  89. package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.shell.html +61 -0
  90. package/bundles/codex/.codex/skills/frontend/flow-studio/tsconfig.json +17 -0
  91. package/bundles/manifest.json +2 -2
  92. 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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
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>