@webpieces/nx-webpieces-rules 0.4.853 → 0.4.854

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,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphFilterAssets = exports.GraphRenderModel = exports.RenderEdge = exports.RenderNode = void 0;
4
+ const dot_syntax_1 = require("./dot-syntax");
5
+ /** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */
6
+ class RenderNode {
7
+ id;
8
+ dot;
9
+ constructor(id, dot) {
10
+ this.id = id;
11
+ this.dot = dot;
12
+ }
13
+ }
14
+ exports.RenderNode = RenderNode;
15
+ class RenderEdge {
16
+ from;
17
+ to;
18
+ dot;
19
+ id;
20
+ constructor(from, to, dot, id) {
21
+ this.from = from;
22
+ this.to = to;
23
+ this.dot = dot;
24
+ this.id = id;
25
+ }
26
+ }
27
+ exports.RenderEdge = RenderEdge;
28
+ /** The immutable drawable snapshot carried by a generated page. */
29
+ class GraphRenderModel {
30
+ nodes = [];
31
+ edges = [];
32
+ bands = [];
33
+ fullDot = '';
34
+ header = '';
35
+ footer = '';
36
+ node(id, dot) {
37
+ this.nodes.push(new RenderNode(id, dot));
38
+ return dot;
39
+ }
40
+ completeEndpoints() {
41
+ const ids = new Set(this.nodes.map((node) => node.id));
42
+ for (const edge of this.edges) {
43
+ for (const id of [edge.from, edge.to]) {
44
+ if (ids.has(id))
45
+ continue;
46
+ ids.add(id);
47
+ this.node(id, ` "${(0, dot_syntax_1.dotValue)(id)}";\n`);
48
+ }
49
+ }
50
+ }
51
+ edge(from, to, dot) {
52
+ const id = `wp-real-edge-${this.edges.length}`;
53
+ const statement = `${dot.slice(0, -2)} [id="${id}"];\n`;
54
+ this.edges.push(new RenderEdge(from, to, statement, id));
55
+ return statement;
56
+ }
57
+ }
58
+ exports.GraphRenderModel = GraphRenderModel;
59
+ /** Shared filter chrome and script-safe JSON for pages opened directly from disk. */
60
+ class GraphFilterAssets {
61
+ json(value) {
62
+ return JSON.stringify(value).replace(/</g, '\\u003c');
63
+ }
64
+ html() {
65
+ return ('<p id="wp-filter-status" hidden>Filtering: <strong id="wp-filter-anchor"></strong> ' +
66
+ '<button id="wp-filter-off" type="button">Turn off Filter</button></p>' +
67
+ '<p id="wp-graph-error" role="alert" hidden></p>');
68
+ }
69
+ styles() {
70
+ return ('#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +
71
+ '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +
72
+ '#graph .wp-filter-anchor ellipse { stroke:#1565c0; stroke-width:3; }');
73
+ }
74
+ }
75
+ exports.GraphFilterAssets = GraphFilterAssets;
76
+ //# sourceMappingURL=graph-render-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAGxC,8FAA8F;AAC9F,MAAa,UAAU;IAEC;IACA;IAFpB,YACoB,EAAU,EACV,GAAW;QADX,OAAE,GAAF,EAAE,CAAQ;QACV,QAAG,GAAH,GAAG,CAAQ;IAC5B,CAAC;CACP;AALD,gCAKC;AAED,MAAa,UAAU;IAEC;IACA;IACA;IACA;IAJpB,YACoB,IAAY,EACZ,EAAU,EACV,GAAW,EACX,EAAU;QAHV,SAAI,GAAJ,IAAI,CAAQ;QACZ,OAAE,GAAF,EAAE,CAAQ;QACV,QAAG,GAAH,GAAG,CAAQ;QACX,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AAPD,gCAOC;AAED,mEAAmE;AACnE,MAAa,gBAAgB;IAChB,KAAK,GAAiB,EAAE,CAAC;IACzB,KAAK,GAAiB,EAAE,CAAC;IAClC,KAAK,GAAgB,EAAE,CAAC;IACxB,OAAO,GAAG,EAAE,CAAC;IACb,MAAM,GAAG,EAAE,CAAC;IACZ,MAAM,GAAG,EAAE,CAAC;IAEZ,IAAI,CAAC,EAAU,EAAE,GAAW;QACxB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACzC,OAAO,GAAG,CAAC;IACf,CAAC;IAED,iBAAiB;QACb,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACvD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC5B,KAAK,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBACpC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,SAAS;gBAC1B,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACZ,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,IAAA,qBAAQ,EAAC,EAAE,CAAC,MAAM,CAAC,CAAC;YAC5C,CAAC;QACL,CAAC;IACL,CAAC;IAED,IAAI,CAAC,IAAY,EAAE,EAAU,EAAE,GAAW;QACtC,MAAM,EAAE,GAAG,gBAAgB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC/C,MAAM,SAAS,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AA9BD,4CA8BC;AAED,qFAAqF;AACrF,MAAa,iBAAiB;IAC1B,IAAI,CAAI,KAAQ;QACZ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI;QACA,OAAO,CACH,qFAAqF;YACrF,uEAAuE;YACvE,iDAAiD,CACpD,CAAC;IACN,CAAC;IAED,MAAM;QACF,OAAO,CACH,8EAA8E;YAC9E,mEAAmE;YACnE,sEAAsE,CACzE,CAAC;IACN,CAAC;CACJ;AApBD,8CAoBC","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\n\n/** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n\n node(id: string, dot: string): string {\n this.nodes.push(new RenderNode(id, dot));\n return dot;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n edge(from: string, to: string, dot: string): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const statement = `${dot.slice(0, -2)} [id=\"${id}\"];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id));\n return statement;\n }\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#1565c0; stroke-width:3; }'\n );\n }\n}\n"]}
@@ -1,83 +1,24 @@
1
- /**
2
- * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
3
- * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
4
- * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
5
- * would light boxes it has nothing to do with.
6
- *
7
- * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
8
- * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
9
- * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
10
- * what is scaffolding, and it says so with this class.
11
- */
12
- declare const LAYOUT_CLASS = "wp-layout";
13
- /** One traversal direction: the edges to light, and the nodes to walk on to. */
14
- declare class Direction {
15
- readonly nodes: Map<string, Set<string>>;
16
- readonly edges: Map<string, Set<Element>>;
17
- constructor(nodes: Map<string, Set<string>>, edges: Map<string, Set<Element>>);
18
- }
19
- /**
20
- * Indexes the rendered SVG and drives the dim/highlight/lock interaction.
21
- *
22
- * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,
23
- * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks
24
- * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.
25
- */
1
+ /** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
26
2
  declare class GraphHighlighter {
27
3
  private readonly svg;
4
+ private readonly chain;
5
+ private readonly model;
6
+ private readonly page;
28
7
  private readonly nodeByName;
29
- private readonly inEdges;
30
- private readonly outEdges;
31
- private readonly inNodes;
32
- private readonly outNodes;
33
- /**
34
- * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box
35
- * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you
36
- * scroll down to its responsibility cards.
37
- */
38
- private locked;
39
- constructor(svg: SVGSVGElement);
40
- /** Index the SVG, then wire hover, the lock dropdown, and the per-node floating menu. */
8
+ constructor(svg: SVGSVGElement, chain: WpGraphChain, model: RenderModelJson, page: GraphPage);
41
9
  wire(): void;
42
- private indexNodes;
43
- private indexEdges;
44
- private ensureEdges;
45
- private ensureNodes;
46
10
  private clear;
47
- /**
48
- * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get
49
- * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.
50
- */
51
11
  private highlight;
52
- private walk;
53
- /** Leaving a box restores the locked view rather than clearing it outright. */
54
- private relight;
12
+ relight(): void;
55
13
  private wireHover;
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
14
  private wireMenu;
73
- /**
74
- * Filter the responsibility cards to the locked box's chain by READING BACK the
75
- * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and
76
- * the cards can never disagree with the picture.
77
- */
78
- private filterCards;
79
15
  }
80
- /** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */
81
- declare class GraphPage {
82
- render(): void;
16
+ declare class GraphPage extends WpFilterPage {
17
+ private highlighter;
18
+ protected captureBinding(_svg: SVGSVGElement | null): () => void;
19
+ protected wireControls(): void;
20
+ protected wireSvg(svg: SVGSVGElement): void;
21
+ lockSelection(): string | null;
22
+ setLock(name: string | null): void;
23
+ private filterCards;
83
24
  }
@@ -1,125 +1,28 @@
1
1
  "use strict";
2
- /*
3
- * Browser-side script for architecture/dependencies.html.
4
- *
5
- * graph-visualizer.ts reads this file's COMPILED output with readFileSync, substitutes the DOT
6
- * placeholder (see viz-client-globals.d.ts) with the JSON-encoded Graphviz DOT, and inlines the result into a <script> tag. So it
7
- * runs in a browser, never in node — but it is ordinary TypeScript, compiled in place by tsc exactly
8
- * like the package's bin entry points, and linted like every other file here.
9
- *
10
- * IT USED TO BE A COMMITTED .js ASSET, exempted from `no-js-files`, and its own header said why: so the
11
- * dim/highlight/lock logic "can define ordinary browser functions without tripping the TypeScript lint
12
- * rules". That is a file existing to dodge the rules — the same anti-pattern as the bin shims this repo
13
- * deleted, and the same cure applies. The logic is a CLASS now, which is what `no-function-outside-class`
14
- * was asking for all along; nothing needed a disable.
15
- *
16
- * Behaviour: after Viz renders the SVG, hovering a box dims the rest and lights its full
17
- * ancestor+descendant chain; the #wp-lock dropdown PINS one box's chain (the dim survives mouse-leave)
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.
23
- */
24
- // The DOT placeholder and the `Viz` global are declared once in viz-client-globals.d.ts — both
25
- // scripts share a global scope, so they cannot each declare them. Do NOT spell the placeholder token
26
- // in a comment: the inliner is a blind split/join, so every literal occurrence gets the whole DOT.
27
- /**
28
- * The class graph-visualizer.ts stamps on the invisible rank anchors, spacer bands and ordering
29
- * edges that pin each level to its own row. They are layout scaffolding, not architecture, so the
30
- * indexes below skip them: an anchor must never be walkable as a dependency, or hovering one box
31
- * would light boxes it has nothing to do with.
32
- *
33
- * Graphviz drops `style=invis` elements from its SVG entirely (verified against the emitted SVG),
34
- * so today nothing carrying this class reaches the DOM at all. The skip is the guarantee that the
35
- * scaffolding stays inert if that ever stops being true — the DOT is the only place that decides
36
- * what is scaffolding, and it says so with this class.
37
- */
38
- const LAYOUT_CLASS = 'wp-layout';
39
- /** One traversal direction: the edges to light, and the nodes to walk on to. */
40
- class Direction {
41
- nodes;
42
- edges;
43
- constructor(nodes, edges) {
44
- this.nodes = nodes;
45
- this.edges = edges;
46
- }
47
- }
48
- /**
49
- * Indexes the rendered SVG and drives the dim/highlight/lock interaction.
50
- *
51
- * The adjacency is DIRECTED and kept as two maps per axis: `in*` is what points AT a node (ancestors,
52
- * walked upward) and `out*` is what it points to (dependencies, walked downward). Highlighting walks
53
- * both, so a hovered box lights its entire chain in both directions rather than just its neighbours.
54
- */
2
+ /** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
55
3
  class GraphHighlighter {
56
4
  svg;
5
+ chain;
6
+ model;
7
+ page;
57
8
  nodeByName = new Map();
58
- inEdges = new Map();
59
- outEdges = new Map();
60
- inNodes = new Map();
61
- outNodes = new Map();
62
- /**
63
- * The box the dropdown pinned, or null. Hover still works on top of a lock — leaving a box
64
- * restores the LOCKED view instead of clearing, so the pinned subgraph stays visible while you
65
- * scroll down to its responsibility cards.
66
- */
67
- locked = null;
68
- constructor(svg) {
9
+ constructor(svg, chain, model, page) {
69
10
  this.svg = svg;
11
+ this.chain = chain;
12
+ this.model = model;
13
+ this.page = page;
70
14
  }
71
- /** Index the SVG, then wire hover, the lock dropdown, and the per-node floating menu. */
72
15
  wire() {
73
- this.indexNodes();
74
- this.indexEdges();
75
- this.wireHover();
76
- this.wireLock();
77
- this.wireMenu();
78
- }
79
- indexNodes() {
80
16
  this.svg.querySelectorAll('g.node').forEach((g) => {
81
- if (g.classList.contains(LAYOUT_CLASS))
82
- return;
83
- const title = g.querySelector('title');
84
- if (title !== null && title.textContent !== null) {
85
- this.nodeByName.set(title.textContent.trim(), g);
86
- }
87
- });
88
- }
89
- indexEdges() {
90
- this.svg.querySelectorAll('g.edge').forEach((edge) => {
91
- if (edge.classList.contains(LAYOUT_CLASS))
17
+ if (g.classList.contains('wp-layout'))
92
18
  return;
93
- const title = edge.querySelector('title');
94
- const text = title === null ? null : title.textContent;
95
- if (text === null)
96
- return;
97
- const idx = text.indexOf('->');
98
- if (idx < 0)
99
- return;
100
- const from = text.slice(0, idx).trim();
101
- const to = text.slice(idx + 2).trim();
102
- this.ensureEdges(this.outEdges, from).add(edge);
103
- this.ensureEdges(this.inEdges, to).add(edge);
104
- this.ensureNodes(this.outNodes, from).add(to);
105
- this.ensureNodes(this.inNodes, to).add(from);
19
+ const name = g.querySelector('title')?.textContent;
20
+ if (name !== null && name !== undefined)
21
+ this.nodeByName.set(name, g);
106
22
  });
107
- }
108
- ensureEdges(map, key) {
109
- const existing = map.get(key);
110
- if (existing !== undefined)
111
- return existing;
112
- const created = new Set();
113
- map.set(key, created);
114
- return created;
115
- }
116
- ensureNodes(map, key) {
117
- const existing = map.get(key);
118
- if (existing !== undefined)
119
- return existing;
120
- const created = new Set();
121
- map.set(key, created);
122
- return created;
23
+ this.wireHover();
24
+ this.wireMenu();
25
+ this.relight();
123
26
  }
124
27
  clear() {
125
28
  this.svg.classList.remove('wp-dim');
@@ -127,161 +30,86 @@ class GraphHighlighter {
127
30
  el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');
128
31
  });
129
32
  }
130
- /**
131
- * Dim everything, then transitively light ancestors and descendants of `name`: edges reached get
132
- * `wp-hl`, boxes get `wp-neighbor`. The `visited` set is what makes a cyclic graph terminate.
133
- */
134
- highlight(name, focusEl) {
33
+ highlight(name) {
135
34
  this.clear();
35
+ const focus = this.nodeByName.get(name);
36
+ if (focus === undefined)
37
+ return;
38
+ const lit = this.chain.nodes(name);
136
39
  this.svg.classList.add('wp-dim');
137
- focusEl.classList.add('wp-focus');
138
- const directions = [
139
- new Direction(this.inNodes, this.inEdges),
140
- new Direction(this.outNodes, this.outEdges),
141
- ];
142
- for (const dir of directions)
143
- this.walk(name, dir);
144
- }
145
- walk(start, dir) {
146
- const visited = new Set();
147
- const stack = [start];
148
- while (stack.length > 0) {
149
- const cur = stack.pop();
150
- const edges = dir.edges.get(cur);
151
- if (edges !== undefined) {
152
- edges.forEach((e) => { e.classList.add('wp-hl'); });
40
+ focus.classList.add('wp-focus');
41
+ for (const id of lit)
42
+ this.nodeByName.get(id)?.classList.add('wp-neighbor');
43
+ for (const edge of this.model.edges) {
44
+ if (lit.has(edge.from) && lit.has(edge.to)) {
45
+ this.svg.querySelector(`[id="${edge.id}"]`)?.classList.add('wp-hl');
153
46
  }
154
- const next = dir.nodes.get(cur);
155
- if (next === undefined)
156
- continue;
157
- next.forEach((name) => {
158
- if (visited.has(name))
159
- return;
160
- visited.add(name);
161
- stack.push(name);
162
- const g = this.nodeByName.get(name);
163
- if (g !== undefined)
164
- g.classList.add('wp-neighbor');
165
- });
166
47
  }
167
48
  }
168
- /** Leaving a box restores the locked view rather than clearing it outright. */
169
49
  relight() {
170
- if (this.locked === null) {
50
+ const locked = this.page.lockSelection();
51
+ if (locked === null)
171
52
  this.clear();
172
- return;
173
- }
174
- const g = this.nodeByName.get(this.locked);
175
- if (g !== undefined)
176
- this.highlight(this.locked, g);
53
+ else
54
+ this.highlight(locked);
177
55
  }
178
56
  wireHover() {
179
57
  this.nodeByName.forEach((g, name) => {
180
- g.addEventListener('mouseenter', () => { this.highlight(name, g); });
181
- g.addEventListener('mouseleave', () => { this.relight(); });
58
+ g.addEventListener('mouseenter', () => this.highlight(name));
59
+ g.addEventListener('mouseleave', () => this.relight());
182
60
  });
183
61
  }
184
- wireLock() {
185
- const lockSelect = this.lockSelect();
186
- if (lockSelect === null)
187
- return;
188
- lockSelect.addEventListener('change', () => {
189
- this.applyLock(lockSelect.value === '' ? null : lockSelect.value);
62
+ wireMenu() {
63
+ const designs = new Map(__DESIGN_LINKS__.map((link) => [link.nodeId, link.href]));
64
+ WpNodeMenu.wire(this.svg, (name) => {
65
+ const items = [];
66
+ const href = designs.get(name);
67
+ if (href !== undefined)
68
+ items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));
69
+ const locked = this.page.lockSelection() === name;
70
+ items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => this.page.setLock(locked ? null : name)));
71
+ items.push(this.page.filterItem(name));
72
+ return items;
190
73
  });
191
74
  }
192
- lockSelect() {
193
- return document.getElementById('wp-lock');
75
+ }
76
+ class GraphPage extends WpFilterPage {
77
+ highlighter = null;
78
+ captureBinding(_svg) {
79
+ const previous = this.highlighter;
80
+ return () => { this.highlighter = previous; this.filterCards(); };
81
+ }
82
+ wireControls() {
83
+ const select = document.getElementById('wp-lock');
84
+ select?.addEventListener('change', () => this.setLock(select.value === '' ? null : select.value));
85
+ }
86
+ wireSvg(svg) {
87
+ this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);
88
+ this.highlighter.wire();
89
+ this.filterCards();
90
+ svg.querySelectorAll('g.node').forEach((node) => {
91
+ if (node.querySelector('title')?.textContent === this.anchor)
92
+ node.classList.add('wp-filter-anchor');
93
+ });
194
94
  }
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;
95
+ lockSelection() {
96
+ return this.locked;
198
97
  }
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
98
  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
99
  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
- }
100
+ const select = document.getElementById('wp-lock');
101
+ if (select !== null)
102
+ select.value = name ?? '';
103
+ this.highlighter?.relight();
220
104
  this.filterCards();
221
105
  }
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'); }));
236
- }
237
- const locked = this.isLocked(name);
238
- items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => {
239
- this.setLock(locked ? null : name);
240
- }));
241
- return items;
242
- });
243
- }
244
- /**
245
- * Filter the responsibility cards to the locked box's chain by READING BACK the
246
- * `.wp-focus`/`.wp-neighbor` classes highlight() just set — so there is no second graph walk and
247
- * the cards can never disagree with the picture.
248
- */
249
106
  filterCards() {
250
- const lit = new Set();
251
- if (this.locked !== null) {
252
- this.svg.querySelectorAll('.wp-focus, .wp-neighbor').forEach((el) => {
253
- const title = el.querySelector('title');
254
- if (title !== null && title.textContent !== null)
255
- lit.add(title.textContent.trim());
256
- });
257
- }
107
+ const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);
258
108
  document.querySelectorAll('.wp-resp-card').forEach((card) => {
259
109
  const name = card.getAttribute('data-node');
260
- const visible = this.locked === null || (name !== null && lit.has(name));
261
- card.classList.toggle('wp-hidden', !visible);
262
- });
263
- }
264
- }
265
- /** Renders the DOT and hands the SVG to the highlighter. Reports a failure into the page, not just the console. */
266
- class GraphPage {
267
- render() {
268
- Viz.instance()
269
- .then((viz) => {
270
- const element = viz.renderSVGElement(__DOT__);
271
- const host = document.getElementById('graph');
272
- if (host === null)
273
- return;
274
- host.appendChild(element);
275
- new GraphHighlighter(element).wire();
276
- })
277
- // 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
278
- .catch((err) => {
279
- console.error(err);
280
- const host = document.getElementById('graph');
281
- if (host !== null)
282
- host.innerHTML = '<pre>' + String(err) + '</pre>';
110
+ card.classList.toggle('wp-hidden', name === null || !this.retained.has(name) || !lit.has(name));
283
111
  });
284
112
  }
285
113
  }
286
- new GraphPage().render();
114
+ new GraphPage(__RENDER_MODEL__).render();
287
115
  //# sourceMappingURL=graph-visualizer.client.js.map
@@ -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: 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"]}
1
+ {"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":";AAAA,qGAAqG;AACrG,MAAM,gBAAgB;IAIG;IACA;IACA;IACA;IANJ,UAAU,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE7D,YACqB,GAAkB,EAClB,KAAmB,EACnB,KAAsB,EACtB,IAAe;QAHf,QAAG,GAAH,GAAG,CAAe;QAClB,UAAK,GAAL,KAAK,CAAc;QACnB,UAAK,GAAL,KAAK,CAAiB;QACtB,SAAI,GAAJ,IAAI,CAAW;IACjC,CAAC;IAEJ,IAAI;QACA,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAc,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;YAC3D,IAAI,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC;gBAAE,OAAO;YAC9C,MAAM,IAAI,GAAG,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;YACnD,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS;gBAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChB,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAEO,KAAK;QACT,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,iCAAiC,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE;YACxE,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;QAC5D,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,SAAS,CAAC,IAAY;QAC1B,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAChC,KAAK,MAAM,EAAE,IAAI,GAAG;YAAE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QAC5E,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YAClC,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBACzC,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,QAAQ,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACxE,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QACzC,IAAI,MAAM,KAAK,IAAI;YAAE,IAAI,CAAC,KAAK,EAAE,CAAC;;YAC7B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChC,CAAC;IAEO,SAAS;QACb,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;YAChC,CAAC,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YAC7D,CAAC,CAAC,gBAAgB,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;QAC3D,CAAC,CAAC,CAAC;IACP,CAAC;IAEO,QAAQ;QACZ,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClF,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE;YAC/B,MAAM,KAAK,GAAqB,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC/B,IAAI,IAAI,KAAK,SAAS;gBAClB,KAAK,CAAC,IAAI,CAAC,IAAI,cAAc,CAAC,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;YACrF,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,KAAK,IAAI,CAAC;YAClD,KAAK,CAAC,IAAI,CACN,IAAI,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,CAChD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAC1C,CACJ,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YACvC,OAAO,KAAK,CAAC;QACjB,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAED,MAAM,SAAU,SAAQ,YAAY;IACxB,WAAW,GAA4B,IAAI,CAAC;IAEjC,cAAc,CAAC,IAA0B;QACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC;QAClC,OAAO,GAAS,EAAE,GAAG,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5E,CAAC;IAEkB,YAAY;QAC3B,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAA6B,CAAC;QAC9E,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE,GAAG,EAAE,CACpC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAC1D,CAAC;IACN,CAAC;IAES,OAAO,CAAC,GAAkB;QAChC,IAAI,CAAC,WAAW,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC3E,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,CAAC,WAAW,EAAE,CAAC;QACnB,GAAG,CAAC,gBAAgB,CAAc,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACzD,IAAI,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC,MAAM;gBACxD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC/C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,aAAa;QACT,OAAO,IAAI,CAAC,MAAM,CAAC;IACvB,CAAC;IAED,OAAO,CAAC,IAAmB;QACvB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAA6B,CAAC;QAC9E,IAAI,MAAM,KAAK,IAAI;YAAE,MAAM,CAAC,KAAK,GAAG,IAAI,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,WAAW,EAAE,CAAC;IACvB,CAAC;IAEO,WAAW;QACf,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACjF,QAAQ,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACxD,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;YAC5C,IAAI,CAAC,SAAS,CAAC,MAAM,CACjB,WAAW,EACX,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAC9D,CAAC;QACN,CAAC,CAAC,CAAC;IACP,CAAC;CACJ;AAED,IAAI,SAAS,CAAC,gBAAgB,CAAC,CAAC,MAAM,EAAE,CAAC","sourcesContent":["/** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n\n constructor(\n private readonly svg: SVGSVGElement,\n private readonly chain: WpGraphChain,\n private readonly model: RenderModelJson,\n private readonly page: GraphPage,\n ) {}\n\n wire(): void {\n this.svg.querySelectorAll<SVGGElement>('g.node').forEach((g) => {\n if (g.classList.contains('wp-layout')) return;\n const name = g.querySelector('title')?.textContent;\n if (name !== null && name !== undefined) this.nodeByName.set(name, g);\n });\n this.wireHover();\n this.wireMenu();\n this.relight();\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) => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl');\n });\n }\n\n private highlight(name: string): void {\n this.clear();\n const focus = this.nodeByName.get(name);\n if (focus === undefined) return;\n const lit = this.chain.nodes(name);\n this.svg.classList.add('wp-dim');\n focus.classList.add('wp-focus');\n for (const id of lit) this.nodeByName.get(id)?.classList.add('wp-neighbor');\n for (const edge of this.model.edges) {\n if (lit.has(edge.from) && lit.has(edge.to)) {\n this.svg.querySelector(`[id=\"${edge.id}\"]`)?.classList.add('wp-hl');\n }\n }\n }\n\n relight(): void {\n const locked = this.page.lockSelection();\n if (locked === null) this.clear();\n else this.highlight(locked);\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g, name) => {\n g.addEventListener('mouseenter', () => this.highlight(name));\n g.addEventListener('mouseleave', () => this.relight());\n });\n }\n\n private wireMenu(): void {\n const designs = new Map(__DESIGN_LINKS__.map((link) => [link.nodeId, link.href]));\n WpNodeMenu.wire(this.svg, (name) => {\n const items: WpNodeMenuItem[] = [];\n const href = designs.get(name);\n if (href !== undefined)\n items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));\n const locked = this.page.lockSelection() === name;\n items.push(\n new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () =>\n this.page.setLock(locked ? null : name),\n ),\n );\n items.push(this.page.filterItem(name));\n return items;\n });\n }\n}\n\nclass GraphPage extends WpFilterPage {\n private highlighter: GraphHighlighter | null = null;\n\n protected override captureBinding(_svg: SVGSVGElement | null): () => void {\n const previous = this.highlighter;\n return (): void => { this.highlighter = previous; this.filterCards(); };\n }\n\n protected override wireControls(): void {\n const select = document.getElementById('wp-lock') as HTMLSelectElement | null;\n select?.addEventListener('change', () =>\n this.setLock(select.value === '' ? null : select.value),\n );\n }\n\n protected wireSvg(svg: SVGSVGElement): void {\n this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);\n this.highlighter.wire();\n this.filterCards();\n svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => {\n if (node.querySelector('title')?.textContent === this.anchor)\n node.classList.add('wp-filter-anchor');\n });\n }\n\n lockSelection(): string | null {\n return this.locked;\n }\n\n setLock(name: string | null): void {\n this.locked = name;\n const select = document.getElementById('wp-lock') as HTMLSelectElement | null;\n if (select !== null) select.value = name ?? '';\n this.highlighter?.relight();\n this.filterCards();\n }\n\n private filterCards(): void {\n const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);\n document.querySelectorAll('.wp-resp-card').forEach((card) => {\n const name = card.getAttribute('data-node');\n card.classList.toggle(\n 'wp-hidden',\n name === null || !this.retained.has(name) || !lit.has(name),\n );\n });\n }\n}\n\nnew GraphPage(__RENDER_MODEL__).render();\n"]}