@webpieces/nx-webpieces-rules 0.4.655 → 0.4.657

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.
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphNodeMenu = void 0;
4
+ /**
5
+ * The floating per-node menu, shared by BOTH generated graphs.
6
+ *
7
+ * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the
8
+ * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and
9
+ * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating
10
+ * it into the two emitters is what this module exists to prevent.
11
+ *
12
+ * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`
13
+ * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the
14
+ * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a
15
+ * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one
16
+ * copy reachable by both emitters with no build-order coupling.
17
+ *
18
+ * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:
19
+ * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
20
+ * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
21
+ * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —
22
+ * dim every other box in that graph, light the locked one.
23
+ * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
24
+ */
25
+ class GraphNodeMenu {
26
+ /**
27
+ * Menu chrome + the "this box is clickable" affordance. Every node is clickable now (the menu
28
+ * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with
29
+ * a design page.
30
+ */
31
+ styles() {
32
+ return `
33
+ .wp-node-menu {
34
+ position: absolute;
35
+ z-index: 1000;
36
+ min-width: 150px;
37
+ padding: 4px 0;
38
+ background: white;
39
+ border: 1px solid #cfcfcf;
40
+ border-radius: 6px;
41
+ box-shadow: 0 4px 14px rgba(0,0,0,0.22);
42
+ font-family: Arial, sans-serif;
43
+ font-size: 14px;
44
+ }
45
+ .wp-node-menu-title {
46
+ padding: 4px 14px 6px;
47
+ color: #777;
48
+ font-size: 12px;
49
+ font-family: monospace;
50
+ border-bottom: 1px solid #eee;
51
+ margin-bottom: 4px;
52
+ }
53
+ .wp-node-menu-item {
54
+ display: block;
55
+ width: 100%;
56
+ padding: 7px 14px;
57
+ border: 0;
58
+ background: none;
59
+ color: #1565C0;
60
+ font: inherit;
61
+ text-align: left;
62
+ cursor: pointer;
63
+ }
64
+ .wp-node-menu-item:hover { background: #E3F2FD; }
65
+ g.wp-node-clickable { cursor: pointer; }
66
+ g.wp-node-clickable polygon,
67
+ g.wp-node-clickable ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }
68
+ g.wp-node-clickable:hover polygon,
69
+ g.wp-node-clickable:hover ellipse {
70
+ stroke: #1976d2;
71
+ stroke-width: 5;
72
+ filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));
73
+ }`;
74
+ }
75
+ /**
76
+ * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered
77
+ * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).
78
+ *
79
+ * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat
80
+ * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.
81
+ */
82
+ dimStyles(scope) {
83
+ return `
84
+ ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }
85
+ ${scope} svg.wp-dim .node,
86
+ ${scope} svg.wp-dim .edge { opacity: 0.15; }
87
+ ${scope} svg.wp-dim .node.wp-focus,
88
+ ${scope} svg.wp-dim .node.wp-neighbor,
89
+ ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;
90
+ }
91
+ /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */
92
+ script() {
93
+ return `${this.menuScript()}\n${this.lockScript()}\n${this.dismissScript()}`;
94
+ }
95
+ /**
96
+ * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,
97
+ * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole
98
+ * menu. Opening a menu closes any other, so at most one is ever on screen.
99
+ */
100
+ menuScript() {
101
+ return `
102
+ class WpNodeMenuItem {
103
+ constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }
104
+ }
105
+ class WpNodeMenu {
106
+ static close() {
107
+ const open = document.getElementById('wp-node-menu');
108
+ if (open !== null) open.remove();
109
+ }
110
+ static open(nodeEl, name, items) {
111
+ WpNodeMenu.close();
112
+ const menu = document.createElement('div');
113
+ menu.id = 'wp-node-menu';
114
+ menu.className = 'wp-node-menu';
115
+ menu.addEventListener('click', function (ev) { ev.stopPropagation(); });
116
+ const heading = document.createElement('div');
117
+ heading.className = 'wp-node-menu-title';
118
+ heading.textContent = name;
119
+ menu.appendChild(heading);
120
+ for (const item of items) menu.appendChild(WpNodeMenu.button(item));
121
+ document.body.appendChild(menu);
122
+ WpNodeMenu.place(menu, nodeEl);
123
+ }
124
+ static button(item) {
125
+ const button = document.createElement('button');
126
+ button.type = 'button';
127
+ button.className = 'wp-node-menu-item';
128
+ button.textContent = item.label;
129
+ button.addEventListener('click', function (ev) {
130
+ ev.preventDefault();
131
+ ev.stopPropagation();
132
+ WpNodeMenu.close();
133
+ item.onSelect();
134
+ });
135
+ return button;
136
+ }
137
+ static place(menu, nodeEl) {
138
+ const box = nodeEl.getBoundingClientRect();
139
+ const own = menu.getBoundingClientRect();
140
+ const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;
141
+ let left = box.left + window.scrollX;
142
+ if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);
143
+ menu.style.left = left + 'px';
144
+ menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
145
+ }
146
+ static wire(svg, itemsFor) {
147
+ svg.querySelectorAll('g.node').forEach(function (node) {
148
+ const title = node.querySelector('title');
149
+ const name = title === null || title.textContent === null
150
+ ? '' : title.textContent.trim();
151
+ if (name === '') return;
152
+ node.classList.add('wp-node-clickable');
153
+ node.addEventListener('click', function (ev) {
154
+ ev.preventDefault();
155
+ ev.stopPropagation();
156
+ WpNodeMenu.open(node, name, itemsFor(name, node));
157
+ });
158
+ });
159
+ }
160
+ }`;
161
+ }
162
+ /**
163
+ * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class
164
+ * box dims every other box in that design's graph and lights the locked one.
165
+ */
166
+ lockScript() {
167
+ return `
168
+ class WpNodeLock {
169
+ constructor(svg) { this.svg = svg; this.locked = null; }
170
+ isLocked(name) { return this.locked === name; }
171
+ toggle(name, nodeEl) {
172
+ if (this.locked === name) this.clear();
173
+ else this.lock(name, nodeEl);
174
+ }
175
+ lock(name, nodeEl) {
176
+ this.clear();
177
+ this.locked = name;
178
+ this.svg.classList.add('wp-dim');
179
+ nodeEl.classList.add('wp-focus');
180
+ }
181
+ clear() {
182
+ this.locked = null;
183
+ this.svg.classList.remove('wp-dim');
184
+ this.svg.querySelectorAll('.wp-focus').forEach(function (el) {
185
+ el.classList.remove('wp-focus');
186
+ });
187
+ }
188
+ }`;
189
+ }
190
+ /** Outside click and Escape both dismiss — wired once, on the document. */
191
+ dismissScript() {
192
+ return `
193
+ document.addEventListener('click', function () { WpNodeMenu.close(); });
194
+ document.addEventListener('keydown', function (ev) {
195
+ if (ev.key === 'Escape') WpNodeMenu.close();
196
+ });`;
197
+ }
198
+ }
199
+ exports.GraphNodeMenu = GraphNodeMenu;
200
+ //# sourceMappingURL=graph-node-menu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAyCL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2DL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;UAqBL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;YAIH,CAAC;IACT,CAAC;CACJ;AAlLD,sCAkLC","sourcesContent":["/**\n * The floating per-node menu, shared by BOTH generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and both `architecture/dependencies.html` (graph-visualizer.ts) and\n * every project's `design.html` (di-graph/design-visualizer.ts) inline these same bytes. Duplicating\n * it into the two emitters is what this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by both emitters with no build-order coupling.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the two pages genuinely differ:\n * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page has neither dropdown nor responsibilities, so it uses `WpNodeLock` below —\n * dim every other box in that graph, light the locked one.\n * Both spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; }\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n node.classList.add('wp-node-clickable');\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n /**\n * The lock a DESIGN page uses: no dropdown and no responsibilities exist there, so locking a class\n * box dims every other box in that design's graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\n }\n}\n"]}
@@ -37,7 +37,7 @@ declare class GraphHighlighter {
37
37
  */
38
38
  private locked;
39
39
  constructor(svg: SVGSVGElement);
40
- /** Index the SVG, then wire hover and the lock dropdown. */
40
+ /** Index the SVG, then wire hover, the lock dropdown, and the per-node floating menu. */
41
41
  wire(): void;
42
42
  private indexNodes;
43
43
  private indexEdges;
@@ -54,6 +54,22 @@ declare class GraphHighlighter {
54
54
  private relight;
55
55
  private wireHover;
56
56
  private wireLock;
57
+ private lockSelect;
58
+ /** True when `name` is the box the page is currently locked on — what the menu labels itself by. */
59
+ isLocked(name: string): boolean;
60
+ /**
61
+ * THE one lock entry point, so the dropdown and the node menu can never disagree: it sets the
62
+ * dropdown's selection, re-highlights, and re-filters the cards whichever of the two asked.
63
+ * `null` is "All (no lock)".
64
+ */
65
+ setLock(name: string | null): void;
66
+ private applyLock;
67
+ /**
68
+ * Every box opens the floating menu (the shared one both generated graphs use). "View Design" is
69
+ * only built when that project actually HAS a committed design.html; the bottom item is always
70
+ * Lock or Unlock, whichever applies to this box right now.
71
+ */
72
+ private wireMenu;
57
73
  /**
58
74
  * Filter the responsibility cards to the locked box's chain by READING BACK the
59
75
  * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and
@@ -13,9 +13,13 @@
13
13
  * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`
14
14
  * was asking for all along; nothing needed a disable.
15
15
  *
16
- * Behaviour, unchanged: after Viz renders the SVG, hovering a box dims the rest and lights its full
16
+ * Behaviour: after Viz renders the SVG, hovering a box dims the rest and lights its full
17
17
  * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)
18
18
  * and filters the responsibilities cards below the graph to just that chain. "All" clears the lock.
19
+ *
20
+ * CLICKING a box opens the shared floating node menu (graph-node-menu.ts, the same menu every
21
+ * project's design.html uses) — "View Design" only for a project that HAS a design.html, then
22
+ * Lock/Unlock. Menu and dropdown are one lock: both go through setLock(), so each reflects the other.
19
23
  */
20
24
  // The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both
21
25
  // scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token
@@ -64,12 +68,13 @@ class GraphHighlighter {
64
68
  constructor(svg) {
65
69
  this.svg = svg;
66
70
  }
67
- /** Index the SVG, then wire hover and the lock dropdown. */
71
+ /** Index the SVG, then wire hover, the lock dropdown, and the per-node floating menu. */
68
72
  wire() {
69
73
  this.indexNodes();
70
74
  this.indexEdges();
71
75
  this.wireHover();
72
76
  this.wireLock();
77
+ this.wireMenu();
73
78
  }
74
79
  indexNodes() {
75
80
  this.svg.querySelectorAll('g.node').forEach((g) => {
@@ -177,20 +182,63 @@ class GraphHighlighter {
177
182
  });
178
183
  }
179
184
  wireLock() {
180
- const lockSelect = document.getElementById('wp-lock');
185
+ const lockSelect = this.lockSelect();
181
186
  if (lockSelect === null)
182
187
  return;
183
188
  lockSelect.addEventListener('change', () => {
184
- this.locked = lockSelect.value === '' ? null : lockSelect.value;
185
- if (this.locked === null) {
186
- this.clear();
187
- }
188
- else {
189
- const g = this.nodeByName.get(this.locked);
190
- if (g !== undefined)
191
- this.highlight(this.locked, g);
189
+ this.applyLock(lockSelect.value === '' ? null : lockSelect.value);
190
+ });
191
+ }
192
+ lockSelect() {
193
+ return document.getElementById('wp-lock');
194
+ }
195
+ /** True when `name` is the box the page is currently locked on — what the menu labels itself by. */
196
+ isLocked(name) {
197
+ return this.locked === name;
198
+ }
199
+ /**
200
+ * THE one lock entry point, so the dropdown and the node menu can never disagree: it sets the
201
+ * dropdown's selection, re-highlights, and re-filters the cards whichever of the two asked.
202
+ * `null` is "All (no lock)".
203
+ */
204
+ setLock(name) {
205
+ const lockSelect = this.lockSelect();
206
+ if (lockSelect !== null)
207
+ lockSelect.value = name === null ? '' : name;
208
+ this.applyLock(name);
209
+ }
210
+ applyLock(name) {
211
+ this.locked = name;
212
+ if (name === null) {
213
+ this.clear();
214
+ }
215
+ else {
216
+ const g = this.nodeByName.get(name);
217
+ if (g !== undefined)
218
+ this.highlight(name, g);
219
+ }
220
+ this.filterCards();
221
+ }
222
+ /**
223
+ * Every box opens the floating menu (the shared one both generated graphs use). "View Design" is
224
+ * only built when that project actually HAS a committed design.html; the bottom item is always
225
+ * Lock or Unlock, whichever applies to this box right now.
226
+ */
227
+ wireMenu() {
228
+ const designs = new Map();
229
+ for (const link of __DESIGN_LINKS__)
230
+ designs.set(link.nodeId, link.href);
231
+ WpNodeMenu.wire(this.svg, (name) => {
232
+ const items = [];
233
+ const href = designs.get(name);
234
+ if (href !== undefined) {
235
+ items.push(new WpNodeMenuItem('View Design', () => { window.open(href, '_blank'); }));
192
236
  }
193
- this.filterCards();
237
+ const locked = this.isLocked(name);
238
+ items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => {
239
+ this.setLock(locked ? null : name);
240
+ }));
241
+ return items;
194
242
  });
195
243
  }
196
244
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for architecture/dependencies.html.\n *\n * graph-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded Graphviz DOT, and inlines the result into a <script> tag. So it\n * runs in a browser, never in node — but it is ordinary TypeScript, compiled in place by tsc exactly\n * like the package's bin entry points, and linted like every other file here.\n *\n * IT USED TO BE A COMMITTED .js ASSET, exempted from `no-js-files`, and its own header said why: so the\n * dim/highlight/lock logic \"can define ordinary browser functions without tripping the TypeScript lint\n * rules\". That is a file existing to dodge the rules — the same anti-pattern as the bin shims this repo\n * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`\n * was asking for all along; nothing needed a disable.\n *\n * Behaviour, unchanged: after Viz renders the SVG, hovering a box dims the rest and lights its full\n * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)\n * and filters the responsibilities cards below the graph to just that chain. \"All\" clears the lock.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/**\n * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering\n * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the\n * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box\n * would light boxes it has nothing to do with.\n *\n * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),\n * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the\n * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides\n * what is scaffolding, and it says so with this class.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** One traversal direction: the edges to light, and the nodes to walk on to. */\nclass Direction {\n constructor(\n readonly nodes: Map<string, Set<string>>,\n readonly edges: Map<string, Set<Element>>,\n ) {}\n}\n\n/**\n * Indexes the rendered SVG and drives the dim/highlight/lock interaction.\n *\n * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,\n * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks\n * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.\n */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private readonly inEdges = new Map<string, Set<Element>>();\n private readonly outEdges = new Map<string, Set<Element>>();\n private readonly inNodes = new Map<string, Set<string>>();\n private readonly outNodes = new Map<string, Set<string>>();\n\n /**\n * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box\n * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you\n * scroll down to its responsibility cards.\n */\n private locked: string | null = null;\n\n constructor(private readonly svg: SVGSVGElement) {}\n\n /** Index the SVG, then wire hover and the lock dropdown. */\n wire(): void {\n this.indexNodes();\n this.indexEdges();\n this.wireHover();\n this.wireLock();\n }\n\n private indexNodes(): void {\n this.svg.querySelectorAll('g.node').forEach((g: Element): void => {\n if (g.classList.contains(LAYOUT_CLASS)) return;\n const title = g.querySelector('title');\n if (title !== null && title.textContent !== null) {\n this.nodeByName.set(title.textContent.trim(), g as SVGGElement);\n }\n });\n }\n\n private indexEdges(): void {\n this.svg.querySelectorAll('g.edge').forEach((edge: Element): void => {\n if (edge.classList.contains(LAYOUT_CLASS)) return;\n const title = edge.querySelector('title');\n const text = title === null ? null : title.textContent;\n if (text === null) return;\n const idx = text.indexOf('->');\n if (idx < 0) return;\n const from = text.slice(0, idx).trim();\n const to = text.slice(idx + 2).trim();\n this.ensureEdges(this.outEdges, from).add(edge);\n this.ensureEdges(this.inEdges, to).add(edge);\n this.ensureNodes(this.outNodes, from).add(to);\n this.ensureNodes(this.inNodes, to).add(from);\n });\n }\n\n private ensureEdges(map: Map<string, Set<Element>>, key: string): Set<Element> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<Element>();\n map.set(key, created);\n return created;\n }\n\n private ensureNodes(map: Map<string, Set<string>>, key: string): Set<string> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<string>();\n map.set(key, created);\n return created;\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach((el: Element): void => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n }\n\n /**\n * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get\n * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.\n */\n private highlight(name: string, focusEl: SVGGElement): void {\n this.clear();\n this.svg.classList.add('wp-dim');\n focusEl.classList.add('wp-focus');\n const directions = [\n new Direction(this.inNodes, this.inEdges),\n new Direction(this.outNodes, this.outEdges),\n ];\n for (const dir of directions) this.walk(name, dir);\n }\n\n private walk(start: string, dir: Direction): void {\n const visited = new Set<string>();\n const stack = [start];\n while (stack.length > 0) {\n const cur = stack.pop() as string;\n const edges = dir.edges.get(cur);\n if (edges !== undefined) {\n edges.forEach((e: Element): void => { e.classList.add('wp-hl'); });\n }\n const next = dir.nodes.get(cur);\n if (next === undefined) continue;\n next.forEach((name: string): void => {\n if (visited.has(name)) return;\n visited.add(name);\n stack.push(name);\n const g = this.nodeByName.get(name);\n if (g !== undefined) g.classList.add('wp-neighbor');\n });\n }\n }\n\n /** Leaving a box restores the locked view rather than clearing it outright. */\n private relight(): void {\n if (this.locked === null) {\n this.clear();\n return;\n }\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string): void => {\n g.addEventListener('mouseenter', (): void => { this.highlight(name, g); });\n g.addEventListener('mouseleave', (): void => { this.relight(); });\n });\n }\n\n private wireLock(): void {\n const lockSelect = document.getElementById('wp-lock') as HTMLSelectElement | null;\n if (lockSelect === null) return;\n lockSelect.addEventListener('change', (): void => {\n this.locked = lockSelect.value === '' ? null : lockSelect.value;\n if (this.locked === null) {\n this.clear();\n } else {\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n this.filterCards();\n });\n }\n\n /**\n * Filter the responsibility cards to the locked box's chain by READING BACK the\n * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and\n * the cards can never disagree with the picture.\n */\n private filterCards(): void {\n const lit = new Set<string>();\n if (this.locked !== null) {\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach((el: Element): void => {\n const title = el.querySelector('title');\n if (title !== null && title.textContent !== null) lit.add(title.textContent.trim());\n });\n }\n document.querySelectorAll('.wp-resp-card').forEach((card: Element): void => {\n const name = card.getAttribute('data-node');\n const visible = this.locked === null || (name !== null && lit.has(name));\n card.classList.toggle('wp-hidden', !visible);\n });\n }\n}\n\n/** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */\nclass GraphPage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n new GraphHighlighter(element).wire();\n })\n // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced\n .catch((err: unknown): void => {\n console.error(err);\n const host = document.getElementById('graph');\n if (host !== null) host.innerHTML = '<pre>' + String(err) + '</pre>';\n });\n }\n}\n\nnew GraphPage().render();\n"]}
1
+ {"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Browser-side script for architecture/dependencies.html.\n *\n * graph-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT\n * placeholder (see viz-client-globals.d.ts) with the JSON-encoded Graphviz DOT, and inlines the result into a <script> tag. So it\n * runs in a browser, never in node — but it is ordinary TypeScript, compiled in place by tsc exactly\n * like the package's bin entry points, and linted like every other file here.\n *\n * IT USED TO BE A COMMITTED .js ASSET, exempted from `no-js-files`, and its own header said why: so the\n * dim/highlight/lock logic \"can define ordinary browser functions without tripping the TypeScript lint\n * rules\". That is a file existing to dodge the rules — the same anti-pattern as the bin shims this repo\n * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`\n * was asking for all along; nothing needed a disable.\n *\n * Behaviour: after Viz renders the SVG, hovering a box dims the rest and lights its full\n * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)\n * and filters the responsibilities cards below the graph to just that chain. \"All\" clears the lock.\n *\n * CLICKING a box opens the shared floating node menu (graph-node-menu.ts, the same menu every\n * project's design.html uses) — \"View Design\" only for a project that HAS a design.html, then\n * Lock/Unlock. Menu and dropdown are one lock: both go through setLock(), so each reflects the other.\n */\n\n// The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both\n// scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token\n// in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.\n\n/**\n * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering\n * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the\n * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box\n * would light boxes it has nothing to do with.\n *\n * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),\n * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the\n * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides\n * what is scaffolding, and it says so with this class.\n */\nconst LAYOUT_CLASS = 'wp-layout';\n\n/** One traversal direction: the edges to light, and the nodes to walk on to. */\nclass Direction {\n constructor(\n readonly nodes: Map<string, Set<string>>,\n readonly edges: Map<string, Set<Element>>,\n ) {}\n}\n\n/**\n * Indexes the rendered SVG and drives the dim/highlight/lock interaction.\n *\n * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,\n * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks\n * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.\n */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private readonly inEdges = new Map<string, Set<Element>>();\n private readonly outEdges = new Map<string, Set<Element>>();\n private readonly inNodes = new Map<string, Set<string>>();\n private readonly outNodes = new Map<string, Set<string>>();\n\n /**\n * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box\n * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you\n * scroll down to its responsibility cards.\n */\n private locked: string | null = null;\n\n constructor(private readonly svg: SVGSVGElement) {}\n\n /** Index the SVG, then wire hover, the lock dropdown, and the per-node floating menu. */\n wire(): void {\n this.indexNodes();\n this.indexEdges();\n this.wireHover();\n this.wireLock();\n this.wireMenu();\n }\n\n private indexNodes(): void {\n this.svg.querySelectorAll('g.node').forEach((g: Element): void => {\n if (g.classList.contains(LAYOUT_CLASS)) return;\n const title = g.querySelector('title');\n if (title !== null && title.textContent !== null) {\n this.nodeByName.set(title.textContent.trim(), g as SVGGElement);\n }\n });\n }\n\n private indexEdges(): void {\n this.svg.querySelectorAll('g.edge').forEach((edge: Element): void => {\n if (edge.classList.contains(LAYOUT_CLASS)) return;\n const title = edge.querySelector('title');\n const text = title === null ? null : title.textContent;\n if (text === null) return;\n const idx = text.indexOf('->');\n if (idx < 0) return;\n const from = text.slice(0, idx).trim();\n const to = text.slice(idx + 2).trim();\n this.ensureEdges(this.outEdges, from).add(edge);\n this.ensureEdges(this.inEdges, to).add(edge);\n this.ensureNodes(this.outNodes, from).add(to);\n this.ensureNodes(this.inNodes, to).add(from);\n });\n }\n\n private ensureEdges(map: Map<string, Set<Element>>, key: string): Set<Element> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<Element>();\n map.set(key, created);\n return created;\n }\n\n private ensureNodes(map: Map<string, Set<string>>, key: string): Set<string> {\n const existing = map.get(key);\n if (existing !== undefined) return existing;\n const created = new Set<string>();\n map.set(key, created);\n return created;\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl').forEach((el: Element): void => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n }\n\n /**\n * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get\n * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.\n */\n private highlight(name: string, focusEl: SVGGElement): void {\n this.clear();\n this.svg.classList.add('wp-dim');\n focusEl.classList.add('wp-focus');\n const directions = [\n new Direction(this.inNodes, this.inEdges),\n new Direction(this.outNodes, this.outEdges),\n ];\n for (const dir of directions) this.walk(name, dir);\n }\n\n private walk(start: string, dir: Direction): void {\n const visited = new Set<string>();\n const stack = [start];\n while (stack.length > 0) {\n const cur = stack.pop() as string;\n const edges = dir.edges.get(cur);\n if (edges !== undefined) {\n edges.forEach((e: Element): void => { e.classList.add('wp-hl'); });\n }\n const next = dir.nodes.get(cur);\n if (next === undefined) continue;\n next.forEach((name: string): void => {\n if (visited.has(name)) return;\n visited.add(name);\n stack.push(name);\n const g = this.nodeByName.get(name);\n if (g !== undefined) g.classList.add('wp-neighbor');\n });\n }\n }\n\n /** Leaving a box restores the locked view rather than clearing it outright. */\n private relight(): void {\n if (this.locked === null) {\n this.clear();\n return;\n }\n const g = this.nodeByName.get(this.locked);\n if (g !== undefined) this.highlight(this.locked, g);\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string): void => {\n g.addEventListener('mouseenter', (): void => { this.highlight(name, g); });\n g.addEventListener('mouseleave', (): void => { this.relight(); });\n });\n }\n\n private wireLock(): void {\n const lockSelect = this.lockSelect();\n if (lockSelect === null) return;\n lockSelect.addEventListener('change', (): void => {\n this.applyLock(lockSelect.value === '' ? null : lockSelect.value);\n });\n }\n\n private lockSelect(): HTMLSelectElement | null {\n return document.getElementById('wp-lock') as HTMLSelectElement | null;\n }\n\n /** True when `name` is the box the page is currently locked on — what the menu labels itself by. */\n isLocked(name: string): boolean {\n return this.locked === name;\n }\n\n /**\n * THE one lock entry point, so the dropdown and the node menu can never disagree: it sets the\n * dropdown's selection, re-highlights, and re-filters the cards whichever of the two asked.\n * `null` is \"All (no lock)\".\n */\n setLock(name: string | null): void {\n const lockSelect = this.lockSelect();\n if (lockSelect !== null) lockSelect.value = name === null ? '' : name;\n this.applyLock(name);\n }\n\n private applyLock(name: string | null): void {\n this.locked = name;\n if (name === null) {\n this.clear();\n } else {\n const g = this.nodeByName.get(name);\n if (g !== undefined) this.highlight(name, g);\n }\n this.filterCards();\n }\n\n /**\n * Every box opens the floating menu (the shared one both generated graphs use). \"View Design\" is\n * only built when that project actually HAS a committed design.html; the bottom item is always\n * Lock or Unlock, whichever applies to this box right now.\n */\n private wireMenu(): void {\n const designs = new Map<string, string>();\n for (const link of __DESIGN_LINKS__) designs.set(link.nodeId, link.href);\n WpNodeMenu.wire(this.svg, (name: string): WpNodeMenuItem[] => {\n const items: WpNodeMenuItem[] = [];\n const href = designs.get(name);\n if (href !== undefined) {\n items.push(new WpNodeMenuItem('View Design', (): void => { window.open(href, '_blank'); }));\n }\n const locked = this.isLocked(name);\n items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', (): void => {\n this.setLock(locked ? null : name);\n }));\n return items;\n });\n }\n\n /**\n * Filter the responsibility cards to the locked box's chain by READING BACK the\n * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and\n * the cards can never disagree with the picture.\n */\n private filterCards(): void {\n const lit = new Set<string>();\n if (this.locked !== null) {\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach((el: Element): void => {\n const title = el.querySelector('title');\n if (title !== null && title.textContent !== null) lit.add(title.textContent.trim());\n });\n }\n document.querySelectorAll('.wp-resp-card').forEach((card: Element): void => {\n const name = card.getAttribute('data-node');\n const visible = this.locked === null || (name !== null && lit.has(name));\n card.classList.toggle('wp-hidden', !visible);\n });\n }\n}\n\n/** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */\nclass GraphPage {\n render(): void {\n Viz.instance()\n .then((viz: VizInstance): void => {\n const element = viz.renderSVGElement(__DOT__);\n const host = document.getElementById('graph');\n if (host === null) return;\n host.appendChild(element);\n new GraphHighlighter(element).wire();\n })\n // webpieces-disable no-any-unknown -- a promise rejection reason is untyped BY THE LANGUAGE (any value can be thrown), and this browser script cannot import the repo's toError helper; it is stringified, never dereferenced\n .catch((err: unknown): void => {\n console.error(err);\n const host = document.getElementById('graph');\n if (host !== null) host.innerHTML = '<pre>' + String(err) + '</pre>';\n });\n }\n}\n\nnew GraphPage().render();\n"]}
@@ -10,6 +10,18 @@
10
10
  * graph can't reach.
11
11
  */
12
12
  import type { EnhancedGraph } from './graph-sorter';
13
+ /**
14
+ * One node's design page, handed to the browser so the node menu can offer "View Design" for the
15
+ * boxes that HAVE one and omit the item entirely for the boxes that do not.
16
+ *
17
+ * The link is data on the PAGE now, not a `URL=` attribute in the DOT: a URL made viz.js wrap the
18
+ * box in an `<a>` and clicking navigated straight out, which the menu replaces.
19
+ */
20
+ export declare class DesignLink {
21
+ readonly nodeId: string;
22
+ readonly href: string;
23
+ constructor(nodeId: string, href: string);
24
+ }
13
25
  export declare class VisualizationPaths {
14
26
  htmlPath: string;
15
27
  constructor(htmlPath: string);
@@ -20,6 +32,8 @@ export declare class VisualizationPaths {
20
32
  * replaced by the entire DOT too, bloating every generated page.
21
33
  */
22
34
  export declare const CLIENT_DOT_PLACEHOLDER: string;
35
+ /** Same contract as CLIENT_DOT_PLACEHOLDER, for the node → design.html links the menu offers. */
36
+ export declare const CLIENT_DESIGN_LINKS_PLACEHOLDER: string;
23
37
  /**
24
38
  * Read a compiled browser client sitting beside this file.
25
39
  *
@@ -34,6 +48,8 @@ export declare class GraphVisualizer {
34
48
  private readonly responsibilities;
35
49
  private readonly bandLayout;
36
50
  private readonly cycles;
51
+ /** The ONE floating-node-menu implementation, shared with the per-project design pages. */
52
+ private readonly nodeMenu;
37
53
  /**
38
54
  * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
39
55
  * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
@@ -91,11 +107,16 @@ export declare class GraphVisualizer {
91
107
  */
92
108
  private labelledApis;
93
109
  /**
94
- * Click-through href for a node: the project's committed design.html, made
110
+ * Href for a node's design page: the project's committed design.html, made
95
111
  * relative to architecture/dependencies.html. Returns null when the project
96
- * has no generated DI design (no design.json → no clickable design page).
112
+ * has no generated DI design (no design.json → no design page exists).
97
113
  */
98
114
  private designHtmlHref;
115
+ /**
116
+ * The design pages that EXIST, one entry per visible node that has one. A node absent from this
117
+ * list gets no "View Design" item at all — the item is never rendered dead or greyed out.
118
+ */
119
+ designLinks(graph: EnhancedGraph): DesignLink[];
99
120
  /**
100
121
  * Generate Graphviz DOT format from the graph
101
122
  */
@@ -122,7 +143,7 @@ export declare class GraphVisualizer {
122
143
  /**
123
144
  * Generate interactive HTML with embedded SVG using viz.js
124
145
  */
125
- generateHTML(dot: string, title?: string, lockControl?: string, responsibilitiesHtml?: string): string;
146
+ generateHTML(dot: string, links: DesignLink[], title?: string, lockControl?: string, responsibilitiesHtml?: string): string;
126
147
  /**
127
148
  * The lock control (a single-select dropdown, rendered below the legend).
128
149
  * Picking a module LOCKS the graph into that box's hover view — its full
@@ -154,8 +175,9 @@ export declare class GraphVisualizer {
154
175
  * architecture/dependencies.html, next to dependencies.json.
155
176
  *
156
177
  * This is a checked-in artifact, regenerated deterministically by
157
- * architecture:generate so the boxes stay clickable into each project's
158
- * design.html. The DOT is embedded in the HTML (rendered client-side by
178
+ * architecture:generate so every box keeps its menu, and the "View Design"
179
+ * item keeps pointing at each project's committed design.html as designs
180
+ * come and go. The DOT is embedded in the HTML (rendered client-side by
159
181
  * viz.js). Output is deterministic (sorted graph in → same bytes out) so git
160
182
  * only shows a diff when the architecture actually changed.
161
183
  */