@webpieces/nx-webpieces-rules 0.4.871 → 0.4.872

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +36 -0
  2. package/package.json +7 -7
  3. package/src/executors/generate/executor.d.ts +3 -1
  4. package/src/executors/generate/executor.js +9 -1
  5. package/src/executors/generate/executor.js.map +1 -1
  6. package/src/index.d.ts +1 -0
  7. package/src/index.js +5 -1
  8. package/src/index.js.map +1 -1
  9. package/src/lib/dot-syntax.d.ts +9 -0
  10. package/src/lib/dot-syntax.js +17 -0
  11. package/src/lib/dot-syntax.js.map +1 -1
  12. package/src/lib/graph-color-modes.d.ts +136 -0
  13. package/src/lib/graph-color-modes.js +299 -0
  14. package/src/lib/graph-color-modes.js.map +1 -0
  15. package/src/lib/graph-filter.client.d.ts +11 -1
  16. package/src/lib/graph-filter.client.js +25 -4
  17. package/src/lib/graph-filter.client.js.map +1 -1
  18. package/src/lib/graph-impact.d.ts +119 -0
  19. package/src/lib/graph-impact.js +295 -0
  20. package/src/lib/graph-impact.js.map +1 -0
  21. package/src/lib/graph-legend.d.ts +34 -0
  22. package/src/lib/graph-legend.js +116 -0
  23. package/src/lib/graph-legend.js.map +1 -0
  24. package/src/lib/graph-names.d.ts +2 -2
  25. package/src/lib/graph-names.js +2 -2
  26. package/src/lib/graph-names.js.map +1 -1
  27. package/src/lib/graph-navigation.d.ts +10 -0
  28. package/src/lib/graph-navigation.js +47 -12
  29. package/src/lib/graph-navigation.js.map +1 -1
  30. package/src/lib/graph-node-menu.d.ts +4 -3
  31. package/src/lib/graph-node-menu.js +137 -20
  32. package/src/lib/graph-node-menu.js.map +1 -1
  33. package/src/lib/graph-page-shell.d.ts +34 -0
  34. package/src/lib/graph-page-shell.js +121 -0
  35. package/src/lib/graph-page-shell.js.map +1 -0
  36. package/src/lib/graph-page-styles.d.ts +20 -0
  37. package/src/lib/graph-page-styles.js +144 -0
  38. package/src/lib/graph-page-styles.js.map +1 -0
  39. package/src/lib/graph-render-model.d.ts +14 -2
  40. package/src/lib/graph-render-model.js +19 -3
  41. package/src/lib/graph-render-model.js.map +1 -1
  42. package/src/lib/graph-responsibilities.js +1 -1
  43. package/src/lib/graph-responsibilities.js.map +1 -1
  44. package/src/lib/graph-visualizer.client.d.ts +65 -0
  45. package/src/lib/graph-visualizer.client.js +311 -7
  46. package/src/lib/graph-visualizer.client.js.map +1 -1
  47. package/src/lib/graph-visualizer.d.ts +19 -31
  48. package/src/lib/graph-visualizer.js +50 -286
  49. package/src/lib/graph-visualizer.js.map +1 -1
  50. package/src/lib/runtime-html-page.js +2 -2
  51. package/src/lib/runtime-html-page.js.map +1 -1
@@ -1,3 +1,29 @@
1
+ /**
2
+ * The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
3
+ *
4
+ * The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered
5
+ * one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,
6
+ * blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
7
+ * without it.
8
+ */
9
+ declare class WpModeState {
10
+ static readonly MODES: string[];
11
+ static readonly NAMES: string[];
12
+ static readonly STORAGE_KEY = "wp-architecture-graph-mode";
13
+ readonly impact: ImpactJson | null;
14
+ mode: string;
15
+ constructor();
16
+ /** '' when Impact can be shown, else the one line the drawer and the menu say instead. */
17
+ impactReason(): string;
18
+ available(mode: string): boolean;
19
+ name(mode: string): string;
20
+ /** False (and nothing changes) for an unknown or unavailable mode. */
21
+ set(mode: string): boolean;
22
+ /** Which impact shade a box gets. */
23
+ status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched';
24
+ private initial;
25
+ private stored;
26
+ }
1
27
  /** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
2
28
  declare class GraphHighlighter {
3
29
  private readonly svg;
@@ -13,12 +39,51 @@ declare class GraphHighlighter {
13
39
  private wireHover;
14
40
  private wireMenu;
15
41
  }
42
+ /**
43
+ * The drawer and its floating panels: every control outside the graph itself. Holds no graph state —
44
+ * each control calls back into the page, and `sync()` repaints the controls from the page's state.
45
+ */
46
+ declare class GraphDrawer {
47
+ private readonly page;
48
+ constructor(page: GraphPage);
49
+ wire(): void;
50
+ /** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
51
+ private wireLock;
52
+ /**
53
+ * Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the
54
+ * menu's own Escape handler closes it: Esc with a menu open only closes the menu.
55
+ */
56
+ private onKey;
57
+ /** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */
58
+ private toggles;
59
+ /** Repaint every control from the page's state. */
60
+ sync(modes: WpModeState, locked: string | null, filtering: boolean): void;
61
+ private syncFilterToggle;
62
+ private impactNote;
63
+ /** "Mode: <b>Runtime</b> · Locked: <b>x</b>", built with text nodes so no name is parsed as HTML. */
64
+ private crumb;
65
+ private lockInput;
66
+ private byId;
67
+ }
16
68
  declare class GraphPage extends WpFilterPage {
17
69
  private highlighter;
70
+ private readonly modes;
71
+ private readonly drawer;
18
72
  protected captureBinding(_svg: SVGSVGElement | null): () => void;
19
73
  protected wireControls(): void;
74
+ protected nodeDot(node: RenderNodeJson): string;
75
+ protected usesFullDot(): boolean;
20
76
  protected wireSvg(svg: SVGSVGElement): void;
21
77
  lockSelection(): string | null;
78
+ hasNode(id: string): boolean;
79
+ /** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
80
+ resolveName(text: string): string | null;
22
81
  setLock(name: string | null): void;
82
+ setMode(mode: string): void;
83
+ /** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
84
+ toggleFilter(): void;
85
+ /** The node menu's "Mode ▸" submenu: one radio item per mode, Impact disabled with its reason. */
86
+ modeItem(): WpNodeMenuItem;
87
+ private syncControls;
23
88
  private filterCards;
24
89
  }
@@ -1,4 +1,83 @@
1
1
  "use strict";
2
+ /**
3
+ * The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
4
+ *
5
+ * The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered
6
+ * one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,
7
+ * blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
8
+ * without it.
9
+ */
10
+ class WpModeState {
11
+ static MODES = ['runtime', 'architecture', 'impact'];
12
+ static NAMES = ['Runtime', 'Architecture', 'Impact'];
13
+ static STORAGE_KEY = 'wp-architecture-graph-mode';
14
+ impact = window.__WP_IMPACT__ ?? null;
15
+ mode = 'runtime';
16
+ constructor() {
17
+ this.mode = this.initial();
18
+ }
19
+ /** '' when Impact can be shown, else the one line the drawer and the menu say instead. */
20
+ impactReason() {
21
+ if (this.impact === null)
22
+ return 'No impact data for this branch. Run pnpm nx run architecture:generate to compute it.';
23
+ return this.impact.available ? '' : `Impact unavailable: ${this.impact.reason}.`;
24
+ }
25
+ available(mode) {
26
+ return WpModeState.MODES.includes(mode) && (mode !== 'impact' || this.impactReason() === '');
27
+ }
28
+ name(mode) {
29
+ return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;
30
+ }
31
+ /** False (and nothing changes) for an unknown or unavailable mode. */
32
+ set(mode) {
33
+ if (!this.available(mode))
34
+ return false;
35
+ this.mode = mode;
36
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page
37
+ try {
38
+ localStorage.setItem(WpModeState.STORAGE_KEY, mode);
39
+ if (location.hash !== `#${mode}`)
40
+ history.replaceState(null, '', `#${mode}`);
41
+ // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
42
+ }
43
+ catch (err) {
44
+ //const error = toError(err);
45
+ void err;
46
+ }
47
+ return true;
48
+ }
49
+ /** Which impact shade a box gets. */
50
+ status(id) {
51
+ if (this.impact?.touched.includes(id))
52
+ return 'touched';
53
+ if (this.impact?.affected.includes(id))
54
+ return 'affected';
55
+ if (this.impact?.buildInputs.includes(id))
56
+ return 'buildInput';
57
+ return 'untouched';
58
+ }
59
+ initial() {
60
+ const hashed = location.hash.slice(1);
61
+ if (this.available(hashed))
62
+ return hashed;
63
+ const stored = this.stored();
64
+ if (stored !== null && this.available(stored))
65
+ return stored;
66
+ return 'runtime';
67
+ }
68
+ stored() {
69
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as "nothing stored"
70
+ try {
71
+ return localStorage.getItem(WpModeState.STORAGE_KEY);
72
+ // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
73
+ }
74
+ catch (err) {
75
+ //const error = toError(err);
76
+ void err;
77
+ return null;
78
+ }
79
+ }
80
+ }
2
81
  /** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
3
82
  class GraphHighlighter {
4
83
  svg;
@@ -70,7 +149,9 @@ class GraphHighlighter {
70
149
  });
71
150
  }
72
151
  wireMenu() {
73
- const designs = new Map(__DESIGN_LINKS__.map((link) => [link.nodeId, link.href]));
152
+ const designs = new Map();
153
+ for (const link of __DESIGN_LINKS__)
154
+ designs.set(link.nodeId, link.href);
74
155
  WpNodeMenu.wire(this.svg, (name) => {
75
156
  const items = [];
76
157
  const href = designs.get(name);
@@ -79,24 +160,210 @@ class GraphHighlighter {
79
160
  const locked = this.page.lockSelection() === name;
80
161
  items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => this.page.setLock(locked ? null : name)));
81
162
  items.push(this.page.filterItem(name));
163
+ items.push(this.page.modeItem());
82
164
  return items;
83
165
  });
84
166
  }
85
167
  }
168
+ /**
169
+ * The drawer and its floating panels: every control outside the graph itself. Holds no graph state —
170
+ * each control calls back into the page, and `sync()` repaints the controls from the page's state.
171
+ */
172
+ class GraphDrawer {
173
+ page;
174
+ constructor(page) {
175
+ this.page = page;
176
+ }
177
+ wire() {
178
+ document.querySelectorAll('.wp-mode[data-wp-mode]').forEach((button) => {
179
+ button.addEventListener('click', () => this.page.setMode(button.dataset['wpMode'] ?? 'runtime'));
180
+ });
181
+ this.wireLock();
182
+ this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());
183
+ this.toggles('wp-collapse', 'wp-shell', true);
184
+ this.toggles('wp-help-btn', 'wp-help', false);
185
+ this.toggles('wp-help-close', 'wp-help', false);
186
+ this.toggles('wp-legend-pop', 'wp-legend-popout', false);
187
+ this.toggles('wp-legend-close', 'wp-legend-popout', false);
188
+ this.toggles('wp-resp-open', 'wp-resp-panel', false);
189
+ this.toggles('wp-resp-close', 'wp-resp-panel', false);
190
+ this.toggles('wp-snapshot-open', 'wp-snapshot', false);
191
+ window.addEventListener('keydown', (ev) => this.onKey(ev), true);
192
+ window.addEventListener('hashchange', () => this.page.setMode(location.hash.slice(1)));
193
+ }
194
+ /** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
195
+ wireLock() {
196
+ const input = this.lockInput();
197
+ if (input === null)
198
+ return;
199
+ input.addEventListener('input', () => {
200
+ const value = input.value.trim();
201
+ if (value === '')
202
+ this.page.setLock(null);
203
+ else if (this.page.hasNode(value))
204
+ this.page.setLock(value);
205
+ });
206
+ input.addEventListener('change', () => {
207
+ const match = this.page.resolveName(input.value.trim());
208
+ if (match !== null)
209
+ this.page.setLock(match);
210
+ else
211
+ input.value = this.page.lockSelection() ?? '';
212
+ });
213
+ input.addEventListener('keydown', (ev) => {
214
+ if (ev.key !== 'Escape')
215
+ return;
216
+ ev.preventDefault();
217
+ input.value = '';
218
+ this.page.setLock(null);
219
+ input.blur();
220
+ });
221
+ }
222
+ /**
223
+ * Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the
224
+ * menu's own Escape handler closes it: Esc with a menu open only closes the menu.
225
+ */
226
+ onKey(ev) {
227
+ if (ev.altKey || ev.ctrlKey || ev.metaKey)
228
+ return;
229
+ const target = ev.target;
230
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
231
+ return;
232
+ const menuOpen = document.getElementById('wp-node-menu') !== null;
233
+ if (ev.key === '/') {
234
+ ev.preventDefault();
235
+ this.lockInput()?.focus();
236
+ return;
237
+ }
238
+ const index = ['1', '2', '3'].indexOf(ev.key);
239
+ if (index >= 0 && !menuOpen) {
240
+ this.page.setMode(WpModeState.MODES[index]);
241
+ return;
242
+ }
243
+ if (ev.key === 'Escape' && !menuOpen && this.page.lockSelection() !== null)
244
+ this.page.setLock(null);
245
+ }
246
+ /** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */
247
+ toggles(buttonId, targetId, collapse) {
248
+ const button = this.byId(buttonId);
249
+ const target = this.byId(targetId);
250
+ if (button === null || target === null)
251
+ return;
252
+ button.addEventListener('click', (ev) => {
253
+ ev.stopPropagation();
254
+ let expanded;
255
+ if (collapse) {
256
+ expanded = !target.classList.toggle('wp-collapsed');
257
+ }
258
+ else {
259
+ target.hidden = !target.hidden;
260
+ expanded = !target.hidden;
261
+ }
262
+ document.querySelectorAll(`[aria-controls="${targetId}"], #${buttonId}[aria-expanded]`).forEach((el) => el.setAttribute('aria-expanded', String(expanded)));
263
+ });
264
+ }
265
+ /** Repaint every control from the page's state. */
266
+ sync(modes, locked, filtering) {
267
+ const reason = modes.impactReason();
268
+ document.querySelectorAll('.wp-mode[data-wp-mode]').forEach((button) => {
269
+ const mode = button.dataset['wpMode'] ?? '';
270
+ button.setAttribute('aria-pressed', String(mode === modes.mode));
271
+ button.disabled = !modes.available(mode);
272
+ if (mode === 'impact')
273
+ button.title = reason === '' ? 'Key 3' : reason;
274
+ });
275
+ const reasonEl = this.byId('wp-impact-reason');
276
+ if (reasonEl !== null) {
277
+ reasonEl.textContent = reason;
278
+ reasonEl.hidden = reason === '';
279
+ }
280
+ document.querySelectorAll('[data-wp-legend-mode]').forEach((section) => {
281
+ section.hidden = section.dataset['wpLegendMode'] !== modes.mode;
282
+ });
283
+ document.querySelectorAll('[data-wp-impact-note]').forEach((note) => {
284
+ note.textContent = this.impactNote(modes);
285
+ });
286
+ const title = this.byId('wp-legend-pop-title');
287
+ if (title !== null)
288
+ title.textContent = `Legend · ${modes.name(modes.mode)}`;
289
+ const input = this.lockInput();
290
+ if (input !== null && document.activeElement !== input)
291
+ input.value = locked ?? '';
292
+ this.syncFilterToggle(locked, filtering);
293
+ this.crumb(modes.name(modes.mode), locked);
294
+ }
295
+ syncFilterToggle(locked, filtering) {
296
+ const toggle = this.byId('wp-filter-toggle');
297
+ if (toggle === null)
298
+ return;
299
+ toggle.setAttribute('aria-pressed', String(filtering));
300
+ toggle.disabled = !filtering && locked === null;
301
+ toggle.title = toggle.disabled ? 'Lock a project first' : '';
302
+ }
303
+ impactNote(modes) {
304
+ const impact = modes.impact;
305
+ if (impact === null || !impact.available)
306
+ return '';
307
+ return (`${impact.touched.length} touched · ${impact.affected.length} affected · ` +
308
+ `${impact.buildInputs.length} build inputs — nx affected since fork point ${impact.base}, ` +
309
+ `${impact.changedFiles} changed files.`);
310
+ }
311
+ /** "Mode: <b>Runtime</b> · Locked: <b>x</b>", built with text nodes so no name is parsed as HTML. */
312
+ crumb(mode, locked) {
313
+ const crumb = this.byId('wp-crumb');
314
+ if (crumb === null)
315
+ return;
316
+ const bold = (text) => {
317
+ const b = document.createElement('b');
318
+ b.textContent = text;
319
+ return b;
320
+ };
321
+ const parts = ['Mode: ', bold(mode)];
322
+ if (locked !== null)
323
+ parts.push(' · Locked: ', bold(locked));
324
+ crumb.replaceChildren(...parts);
325
+ }
326
+ lockInput() {
327
+ return document.getElementById('wp-lock');
328
+ }
329
+ byId(id) {
330
+ return document.getElementById(id);
331
+ }
332
+ }
86
333
  class GraphPage extends WpFilterPage {
87
334
  highlighter = null;
335
+ modes = new WpModeState();
336
+ drawer = new GraphDrawer(this);
88
337
  captureBinding(_svg) {
89
338
  const previous = this.highlighter;
90
- return () => { this.highlighter = previous; this.filterCards(); };
339
+ return () => {
340
+ this.highlighter = previous;
341
+ this.filterCards();
342
+ this.syncControls();
343
+ };
91
344
  }
92
345
  wireControls() {
93
- const select = document.getElementById('wp-lock');
94
- select?.addEventListener('change', () => this.setLock(select.value === '' ? null : select.value));
346
+ this.drawer.wire();
347
+ this.syncControls();
348
+ }
349
+ nodeDot(node) {
350
+ const modes = node.modes;
351
+ if (modes === null)
352
+ return node.dot;
353
+ if (this.modes.mode === 'architecture')
354
+ return modes.architecture;
355
+ if (this.modes.mode === 'impact')
356
+ return modes[this.modes.status(node.id)];
357
+ return modes.runtime;
358
+ }
359
+ usesFullDot() {
360
+ return this.modes.mode === 'runtime';
95
361
  }
96
362
  wireSvg(svg) {
97
363
  this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);
98
364
  this.highlighter.wire();
99
365
  this.filterCards();
366
+ this.syncControls();
100
367
  svg.querySelectorAll('g.node').forEach((node) => {
101
368
  if (node.querySelector('title')?.textContent === this.anchor)
102
369
  node.classList.add('wp-filter-anchor');
@@ -105,13 +372,50 @@ class GraphPage extends WpFilterPage {
105
372
  lockSelection() {
106
373
  return this.locked;
107
374
  }
375
+ hasNode(id) {
376
+ return this.model.nodes.some((node) => node.id === id);
377
+ }
378
+ /** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
379
+ resolveName(text) {
380
+ if (text === '')
381
+ return null;
382
+ if (this.hasNode(text))
383
+ return text;
384
+ const matches = this.model.nodes.filter((node) => node.id.split('/').pop() === text);
385
+ return matches.length === 1 ? matches[0].id : null;
386
+ }
108
387
  setLock(name) {
109
388
  this.locked = name;
110
- const select = document.getElementById('wp-lock');
111
- if (select !== null)
112
- select.value = name ?? '';
113
389
  this.highlighter?.relight();
114
390
  this.filterCards();
391
+ this.syncControls();
392
+ }
393
+ setMode(mode) {
394
+ if (mode === this.modes.mode || !this.modes.set(mode))
395
+ return;
396
+ this.syncControls();
397
+ this.redraw();
398
+ }
399
+ /** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
400
+ toggleFilter() {
401
+ if (this.anchor !== null)
402
+ this.filter(null);
403
+ else if (this.locked !== null)
404
+ this.filter(this.locked);
405
+ }
406
+ /** The node menu's "Mode ▸" submenu: one radio item per mode, Impact disabled with its reason. */
407
+ modeItem() {
408
+ const children = WpModeState.MODES.map((mode) => {
409
+ const item = new WpNodeMenuItem(this.modes.name(mode), () => this.setMode(mode));
410
+ item.checked = mode === this.modes.mode;
411
+ if (!this.modes.available(mode))
412
+ item.disabledReason = this.modes.impactReason();
413
+ return item;
414
+ });
415
+ return new WpNodeMenuItem('Mode', () => { }, children);
416
+ }
417
+ syncControls() {
418
+ this.drawer.sync(this.modes, this.locked, this.anchor !== null);
115
419
  }
116
420
  filterCards() {
117
421
  const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);
@@ -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":["/** 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 private hovered: string | null = null;\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, .wp-locked').forEach((el: Element) => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl', 'wp-locked');\n });\n }\n\n relight(): void {\n this.clear();\n const locked = this.page.lockSelection();\n if (locked !== null) this.nodeByName.get(locked)?.classList.add('wp-locked');\n // A filtered-out Lock is suspended, including its otherwise visible relatives.\n const anchors = [locked, this.hovered].filter(\n (name: string | null): name is string => name !== null && this.nodeByName.has(name),\n );\n if (anchors.length === 0) return;\n const lit = new Set<string>();\n for (const name of anchors) {\n this.nodeByName.get(name)?.classList.add('wp-focus');\n for (const id of this.chain.nodes(name)) {\n if (this.nodeByName.has(id)) lit.add(id);\n }\n }\n this.svg.classList.add('wp-dim');\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 private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string) => {\n g.addEventListener('mouseenter', () => {\n this.hovered = name;\n this.relight();\n });\n g.addEventListener('mouseleave', () => {\n this.hovered = null;\n this.relight();\n });\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"]}
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 * The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.\n *\n * The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered\n * one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,\n * blocked site data, a file:// page in some browsers), so every access is wrapped and the page works\n * without it.\n */\nclass WpModeState {\n static readonly MODES = ['runtime', 'architecture', 'impact'];\n static readonly NAMES = ['Runtime', 'Architecture', 'Impact'];\n static readonly STORAGE_KEY = 'wp-architecture-graph-mode';\n readonly impact: ImpactJson | null = window.__WP_IMPACT__ ?? null;\n mode = 'runtime';\n\n constructor() {\n this.mode = this.initial();\n }\n\n /** '' when Impact can be shown, else the one line the drawer and the menu say instead. */\n impactReason(): string {\n if (this.impact === null)\n return 'No impact data for this branch. Run pnpm nx run architecture:generate to compute it.';\n return this.impact.available ? '' : `Impact unavailable: ${this.impact.reason}.`;\n }\n\n available(mode: string): boolean {\n return WpModeState.MODES.includes(mode) && (mode !== 'impact' || this.impactReason() === '');\n }\n\n name(mode: string): string {\n return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;\n }\n\n /** False (and nothing changes) for an unknown or unavailable mode. */\n set(mode: string): boolean {\n if (!this.available(mode)) return false;\n this.mode = mode;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(WpModeState.STORAGE_KEY, mode);\n if (location.hash !== `#${mode}`) history.replaceState(null, '', `#${mode}`);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n /** Which impact shade a box gets. */\n status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched' {\n if (this.impact?.touched.includes(id)) return 'touched';\n if (this.impact?.affected.includes(id)) return 'affected';\n if (this.impact?.buildInputs.includes(id)) return 'buildInput';\n return 'untouched';\n }\n\n private initial(): string {\n const hashed = location.hash.slice(1);\n if (this.available(hashed)) return hashed;\n const stored = this.stored();\n if (stored !== null && this.available(stored)) return stored;\n return 'runtime';\n }\n\n private stored(): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as \"nothing stored\"\n try {\n return localStorage.getItem(WpModeState.STORAGE_KEY);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return null;\n }\n }\n}\n\n/** 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 private hovered: string | null = null;\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: SVGGElement) => {\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, .wp-locked').forEach((el: Element) => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl', 'wp-locked');\n });\n }\n\n relight(): void {\n this.clear();\n const locked = this.page.lockSelection();\n if (locked !== null) this.nodeByName.get(locked)?.classList.add('wp-locked');\n // A filtered-out Lock is suspended, including its otherwise visible relatives.\n const anchors = [locked, this.hovered].filter(\n (name: string | null): name is string => name !== null && this.nodeByName.has(name),\n );\n if (anchors.length === 0) return;\n const lit = new Set<string>();\n for (const name of anchors) {\n this.nodeByName.get(name)?.classList.add('wp-focus');\n for (const id of this.chain.nodes(name)) {\n if (this.nodeByName.has(id)) lit.add(id);\n }\n }\n this.svg.classList.add('wp-dim');\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 private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string) => {\n g.addEventListener('mouseenter', () => {\n this.hovered = name;\n this.relight();\n });\n g.addEventListener('mouseleave', () => {\n this.hovered = null;\n this.relight();\n });\n });\n }\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', () => 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 items.push(this.page.modeItem());\n return items;\n });\n }\n}\n\n/**\n * The drawer and its floating panels: every control outside the graph itself. Holds no graph state —\n * each control calls back into the page, and `sync()` repaints the controls from the page's state.\n */\nclass GraphDrawer {\n constructor(private readonly page: GraphPage) {}\n\n wire(): void {\n document.querySelectorAll<HTMLButtonElement>('.wp-mode[data-wp-mode]').forEach((button: HTMLButtonElement) => {\n button.addEventListener('click', () => this.page.setMode(button.dataset['wpMode'] ?? 'runtime'));\n });\n this.wireLock();\n this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());\n this.toggles('wp-collapse', 'wp-shell', true);\n this.toggles('wp-help-btn', 'wp-help', false);\n this.toggles('wp-help-close', 'wp-help', false);\n this.toggles('wp-legend-pop', 'wp-legend-popout', false);\n this.toggles('wp-legend-close', 'wp-legend-popout', false);\n this.toggles('wp-resp-open', 'wp-resp-panel', false);\n this.toggles('wp-resp-close', 'wp-resp-panel', false);\n this.toggles('wp-snapshot-open', 'wp-snapshot', false);\n window.addEventListener('keydown', (ev: KeyboardEvent) => this.onKey(ev), true);\n window.addEventListener('hashchange', () => this.page.setMode(location.hash.slice(1)));\n }\n\n /** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */\n private wireLock(): void {\n const input = this.lockInput();\n if (input === null) return;\n input.addEventListener('input', () => {\n const value = input.value.trim();\n if (value === '') this.page.setLock(null);\n else if (this.page.hasNode(value)) this.page.setLock(value);\n });\n input.addEventListener('change', () => {\n const match = this.page.resolveName(input.value.trim());\n if (match !== null) this.page.setLock(match);\n else input.value = this.page.lockSelection() ?? '';\n });\n input.addEventListener('keydown', (ev: KeyboardEvent) => {\n if (ev.key !== 'Escape') return;\n ev.preventDefault();\n input.value = '';\n this.page.setLock(null);\n input.blur();\n });\n }\n\n /**\n * Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the\n * menu's own Escape handler closes it: Esc with a menu open only closes the menu.\n */\n private onKey(ev: KeyboardEvent): void {\n if (ev.altKey || ev.ctrlKey || ev.metaKey) return;\n const target = ev.target;\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return;\n const menuOpen = document.getElementById('wp-node-menu') !== null;\n if (ev.key === '/') {\n ev.preventDefault();\n this.lockInput()?.focus();\n return;\n }\n const index = ['1', '2', '3'].indexOf(ev.key);\n if (index >= 0 && !menuOpen) {\n this.page.setMode(WpModeState.MODES[index]);\n return;\n }\n if (ev.key === 'Escape' && !menuOpen && this.page.lockSelection() !== null) this.page.setLock(null);\n }\n\n /** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */\n private toggles(buttonId: string, targetId: string, collapse: boolean): void {\n const button = this.byId(buttonId);\n const target = this.byId(targetId);\n if (button === null || target === null) return;\n button.addEventListener('click', (ev: MouseEvent) => {\n ev.stopPropagation();\n let expanded: boolean;\n if (collapse) {\n expanded = !target.classList.toggle('wp-collapsed');\n } else {\n target.hidden = !target.hidden;\n expanded = !target.hidden;\n }\n document.querySelectorAll(`[aria-controls=\"${targetId}\"], #${buttonId}[aria-expanded]`).forEach(\n (el: Element) => el.setAttribute('aria-expanded', String(expanded)),\n );\n });\n }\n\n /** Repaint every control from the page's state. */\n sync(modes: WpModeState, locked: string | null, filtering: boolean): void {\n const reason = modes.impactReason();\n document.querySelectorAll<HTMLButtonElement>('.wp-mode[data-wp-mode]').forEach((button: HTMLButtonElement) => {\n const mode = button.dataset['wpMode'] ?? '';\n button.setAttribute('aria-pressed', String(mode === modes.mode));\n button.disabled = !modes.available(mode);\n if (mode === 'impact') button.title = reason === '' ? 'Key 3' : reason;\n });\n const reasonEl = this.byId('wp-impact-reason');\n if (reasonEl !== null) {\n reasonEl.textContent = reason;\n reasonEl.hidden = reason === '';\n }\n document.querySelectorAll<HTMLElement>('[data-wp-legend-mode]').forEach((section: HTMLElement) => {\n section.hidden = section.dataset['wpLegendMode'] !== modes.mode;\n });\n document.querySelectorAll<HTMLElement>('[data-wp-impact-note]').forEach((note: HTMLElement) => {\n note.textContent = this.impactNote(modes);\n });\n const title = this.byId('wp-legend-pop-title');\n if (title !== null) title.textContent = `Legend · ${modes.name(modes.mode)}`;\n const input = this.lockInput();\n if (input !== null && document.activeElement !== input) input.value = locked ?? '';\n this.syncFilterToggle(locked, filtering);\n this.crumb(modes.name(modes.mode), locked);\n }\n\n private syncFilterToggle(locked: string | null, filtering: boolean): void {\n const toggle = this.byId('wp-filter-toggle') as HTMLButtonElement | null;\n if (toggle === null) return;\n toggle.setAttribute('aria-pressed', String(filtering));\n toggle.disabled = !filtering && locked === null;\n toggle.title = toggle.disabled ? 'Lock a project first' : '';\n }\n\n private impactNote(modes: WpModeState): string {\n const impact = modes.impact;\n if (impact === null || !impact.available) return '';\n return (\n `${impact.touched.length} touched · ${impact.affected.length} affected · ` +\n `${impact.buildInputs.length} build inputs — nx affected since fork point ${impact.base}, ` +\n `${impact.changedFiles} changed files.`\n );\n }\n\n /** \"Mode: <b>Runtime</b> · Locked: <b>x</b>\", built with text nodes so no name is parsed as HTML. */\n private crumb(mode: string, locked: string | null): void {\n const crumb = this.byId('wp-crumb');\n if (crumb === null) return;\n const bold = (text: string): HTMLElement => {\n const b = document.createElement('b');\n b.textContent = text;\n return b;\n };\n const parts: (string | Node)[] = ['Mode: ', bold(mode)];\n if (locked !== null) parts.push(' · Locked: ', bold(locked));\n crumb.replaceChildren(...parts);\n }\n\n private lockInput(): HTMLInputElement | null {\n return document.getElementById('wp-lock') as HTMLInputElement | null;\n }\n\n private byId(id: string): HTMLElement | null {\n return document.getElementById(id);\n }\n}\n\nclass GraphPage extends WpFilterPage {\n private highlighter: GraphHighlighter | null = null;\n private readonly modes = new WpModeState();\n private readonly drawer = new GraphDrawer(this);\n\n protected override captureBinding(_svg: SVGSVGElement | null): () => void {\n const previous = this.highlighter;\n return (): void => {\n this.highlighter = previous;\n this.filterCards();\n this.syncControls();\n };\n }\n\n protected override wireControls(): void {\n this.drawer.wire();\n this.syncControls();\n }\n\n protected override nodeDot(node: RenderNodeJson): string {\n const modes = node.modes;\n if (modes === null) return node.dot;\n if (this.modes.mode === 'architecture') return modes.architecture;\n if (this.modes.mode === 'impact') return modes[this.modes.status(node.id)];\n return modes.runtime;\n }\n\n protected override usesFullDot(): boolean {\n return this.modes.mode === 'runtime';\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 this.syncControls();\n svg.querySelectorAll<SVGGElement>('g.node').forEach((node: SVGGElement) => {\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 hasNode(id: string): boolean {\n return this.model.nodes.some((node: RenderNodeJson): boolean => node.id === id);\n }\n\n /** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */\n resolveName(text: string): string | null {\n if (text === '') return null;\n if (this.hasNode(text)) return text;\n const matches = this.model.nodes.filter(\n (node: RenderNodeJson): boolean => node.id.split('/').pop() === text,\n );\n return matches.length === 1 ? matches[0].id : null;\n }\n\n setLock(name: string | null): void {\n this.locked = name;\n this.highlighter?.relight();\n this.filterCards();\n this.syncControls();\n }\n\n setMode(mode: string): void {\n if (mode === this.modes.mode || !this.modes.set(mode)) return;\n this.syncControls();\n this.redraw();\n }\n\n /** The drawer's \"Hide unconnected\": filter to the locked box's chain, or turn the filter off. */\n toggleFilter(): void {\n if (this.anchor !== null) this.filter(null);\n else if (this.locked !== null) this.filter(this.locked);\n }\n\n /** The node menu's \"Mode ▸\" submenu: one radio item per mode, Impact disabled with its reason. */\n modeItem(): WpNodeMenuItem {\n const children = WpModeState.MODES.map((mode: string): WpNodeMenuItem => {\n const item = new WpNodeMenuItem(this.modes.name(mode), (): void => this.setMode(mode));\n item.checked = mode === this.modes.mode;\n if (!this.modes.available(mode)) item.disabledReason = this.modes.impactReason();\n return item;\n });\n return new WpNodeMenuItem('Mode', (): void => {}, children);\n }\n\n private syncControls(): void {\n this.drawer.sync(this.modes, this.locked, this.anchor !== null);\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: Element) => {\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"]}
@@ -43,6 +43,11 @@ export declare class GraphVisualizer {
43
43
  private readonly cycles;
44
44
  /** The ONE floating-node-menu implementation, shared with the per-project design pages. */
45
45
  private readonly nodeMenu;
46
+ private readonly styler;
47
+ private readonly legend;
48
+ private readonly shell;
49
+ private readonly pageStyles;
50
+ private readonly navigation;
46
51
  /**
47
52
  * How to obtain the browser client's text. Injected so HTML generation does not depend on BUILD
48
53
  * ORDER: the default reads the compiled sibling, which exists in the tarball and in dist but NOT in
@@ -52,27 +57,10 @@ export declare class GraphVisualizer {
52
57
  constructor(clientJs?: () => string, filterJs?: () => string);
53
58
  /**
54
59
  * A project tagged `drawOnGraph:false` is hidden from the rendered graph —
55
- * its node, its rank placement, its dropdown option, its responsibilities
60
+ * its node, its rank placement, its lock-search option, its responsibilities
56
61
  * card, and every edge touching it are all omitted. It stays in the JSON.
57
62
  */
58
63
  private isHidden;
59
- /**
60
- * Fill color for an env set — the color of the first env in the set that has
61
- * a known color, else the default.
62
- */
63
- private frameworkColor;
64
- /**
65
- * Role border styling — fill stays keyed on framework; the border shows a
66
- * project's ROLE at a glance. Server and client are the top-level runnable
67
- * nodes, so they get bold, colored borders to stand out:
68
- * server → thick GREEN border (a runnable server app)
69
- * app → thick BLUE border (a runnable non-HTTP app, e.g. a tooling app)
70
- * bundle → thick PURPLE border (an aggregator that bundles several apps)
71
- * client → thick RED border (a client app, e.g. angular)
72
- * designed-lib → bold border (a library with a generated @DocumentDesign design)
73
- * lib / other → plain thin border
74
- */
75
- private roleBorderAttrs;
76
64
  /**
77
65
  * Edge styling by API-relation kind (why the edge exists):
78
66
  * implements → BLACK dashed (a controller serves this api-lib's contract)
@@ -135,26 +123,26 @@ export declare class GraphVisualizer {
135
123
  private dotNodes;
136
124
  private dotEdges;
137
125
  /**
138
- * Generate interactive HTML with embedded SVG using viz.js
126
+ * Generate the interactive page: the drawer shell around a viz.js-rendered graph.
127
+ *
128
+ * The Impact sidecar is a plain `<script src>` loaded BEFORE the page scripts, so the page knows
129
+ * the branch's impact when it first draws. It is per-branch and gitignored (graph-impact.ts):
130
+ * when it is absent the browser skips it and Impact is disabled with its reason.
139
131
  */
140
132
  generateHTML(model: GraphRenderModel, links: DesignLink[], title?: string, lockControl?: string, responsibilitiesHtml?: string): string;
133
+ /** Shared menu/dim/filter/zoom rules first, then the page's own, so its `#graph` rules win. */
134
+ private styles;
141
135
  /**
142
- * The lock control (a single-select dropdown, rendered below the legend).
143
- * Picking a module LOCKS the graph into that box's hover view — its full
144
- * ancestor + descendant chain stays lit while everything else stays dimmed —
145
- * and narrows the responsibilities list below the graph to just that chain.
146
- * The first option, "All", is the default and clears the lock. Hover still
147
- * works on top of a lock; leaving a box returns to the locked view.
136
+ * The drawer's type-to-search lock field, replacing a `<select>` that listed every project and
137
+ * was unusable at hundreds of options. Picking a project LOCKS the graph into that box's hover
138
+ * view — its full ancestor + descendant chain stays lit while everything else dims — and narrows
139
+ * the responsibilities panel to that chain. Clearing the field, or Esc, unlocks.
148
140
  *
141
+ * Each option's VALUE is the node id the SVG is keyed on; its label is the level and short name.
149
142
  * Options are ordered by level DESCENDING to match the responsibilities cards.
150
143
  */
151
144
  lockControl(graph: EnhancedGraph): string;
152
- private styles;
153
- private componentStyles;
154
- private legend;
155
- private fillItems;
156
- private borderItems;
157
- private edgeItems;
145
+ private escapeHtml;
158
146
  /**
159
147
  * The page script. The browser code lives in graph-visualizer.client.ts — real, linted TypeScript
160
148
  * that tsc compiles in place, so what this inlines is its COMPILED .js sitting beside this file.