@webpieces/nx-webpieces-rules 0.4.872 → 0.4.874

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 (35) hide show
  1. package/README.md +31 -18
  2. package/package.json +7 -7
  3. package/src/executors/visualize/executor.d.ts +17 -3
  4. package/src/executors/visualize/executor.js +53 -33
  5. package/src/executors/visualize/executor.js.map +1 -1
  6. package/src/lib/graph-color-modes.d.ts +56 -34
  7. package/src/lib/graph-color-modes.js +111 -100
  8. package/src/lib/graph-color-modes.js.map +1 -1
  9. package/src/lib/graph-filter.client.d.ts +13 -2
  10. package/src/lib/graph-filter.client.js +27 -7
  11. package/src/lib/graph-filter.client.js.map +1 -1
  12. package/src/lib/graph-impact.d.ts +37 -3
  13. package/src/lib/graph-impact.js +42 -11
  14. package/src/lib/graph-impact.js.map +1 -1
  15. package/src/lib/graph-legend.js +1 -1
  16. package/src/lib/graph-legend.js.map +1 -1
  17. package/src/lib/graph-level-bands.d.ts +8 -0
  18. package/src/lib/graph-level-bands.js +13 -1
  19. package/src/lib/graph-level-bands.js.map +1 -1
  20. package/src/lib/graph-node-menu.js +10 -108
  21. package/src/lib/graph-node-menu.js.map +1 -1
  22. package/src/lib/graph-page-shell.d.ts +31 -1
  23. package/src/lib/graph-page-shell.js +75 -7
  24. package/src/lib/graph-page-shell.js.map +1 -1
  25. package/src/lib/graph-page-styles.d.ts +14 -3
  26. package/src/lib/graph-page-styles.js +89 -18
  27. package/src/lib/graph-page-styles.js.map +1 -1
  28. package/src/lib/graph-render-model.d.ts +14 -6
  29. package/src/lib/graph-render-model.js +22 -8
  30. package/src/lib/graph-render-model.js.map +1 -1
  31. package/src/lib/graph-visualizer.client.d.ts +82 -5
  32. package/src/lib/graph-visualizer.client.js +351 -23
  33. package/src/lib/graph-visualizer.client.js.map +1 -1
  34. package/src/lib/graph-visualizer.js +1 -1
  35. package/src/lib/graph-visualizer.js.map +1 -1
@@ -65,15 +65,6 @@ class GraphNodeMenu {
65
65
  cursor: pointer;
66
66
  }
67
67
  .wp-node-menu-item:hover { background: #E3F2FD; }
68
- .wp-node-menu-item:disabled { color: #9aa0ad; cursor: not-allowed; background: none; }
69
- .wp-node-menu-item[aria-checked="true"]::after { content: "✓"; float: right; margin-left: 12px; }
70
- /* A submenu (e.g. the architecture page's "Mode ▸") opens beside its parent item, on hover,
71
- * on click and on ArrowRight; ArrowLeft returns to the parent. */
72
- .wp-node-menu-sub-host { position: relative; }
73
- .wp-node-menu-arrow { float: right; margin-left: 12px; color: #777; }
74
- .wp-node-menu .wp-node-menu-sub { display: none; left: 100%; top: -5px; }
75
- .wp-node-menu .wp-node-menu-sub.wp-flip { left: auto; right: 100%; }
76
- .wp-node-menu-sub-host.wp-open > .wp-node-menu-sub { display: block; }
77
68
  g.wp-node-clickable { cursor: pointer; outline: none; }
78
69
  /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */
79
70
  g.wp-node-clickable.wp-keyboard-focus {
@@ -89,9 +80,9 @@ class GraphNodeMenu {
89
80
  g.wp-node-clickable:hover polygon,
90
81
  g.wp-node-clickable:hover ellipse,
91
82
  g.wp-node-clickable:hover path {
92
- stroke: #1976d2;
83
+ stroke: #8b3cf0;
93
84
  stroke-width: 5;
94
- filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));
85
+ filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));
95
86
  }`;
96
87
  }
97
88
  /**
@@ -122,21 +113,16 @@ class GraphNodeMenu {
122
113
  itemScript() {
123
114
  return `
124
115
  class WpNodeMenuItem {
125
- constructor(label, onSelect, children = []) {
116
+ constructor(label, onSelect) {
126
117
  this.label = label;
127
118
  this.onSelect = onSelect;
128
- this.children = children;
129
- this.checked = null;
130
- this.disabledReason = '';
131
119
  }
132
120
  }`;
133
121
  }
134
122
  menuScript() {
135
123
  return `${this.itemScript()}
136
124
  ${WpMenuButton.toString()}
137
- ${WpSubmenu.toString()}
138
125
  class WpNodeMenu {
139
- static submenu(item) { return new WpSubmenu().build(item, WpNodeMenu.button); }
140
126
  ${this.focusScript()}
141
127
  static open(nodeEl, name, items) {
142
128
  WpNodeMenu.close();
@@ -150,10 +136,7 @@ class GraphNodeMenu {
150
136
  heading.className = 'wp-node-menu-title';
151
137
  heading.textContent = name;
152
138
  menu.appendChild(heading);
153
- for (const item of items) {
154
- const hasChildren = Array.isArray(item.children) && item.children.length > 0;
155
- menu.appendChild(hasChildren ? WpNodeMenu.submenu(item) : WpNodeMenu.button(item));
156
- }
139
+ for (const item of items) menu.appendChild(WpNodeMenu.button(item));
157
140
  document.body.appendChild(menu);
158
141
  WpNodeMenu.place(menu, nodeEl);
159
142
  menu.querySelector('button')?.focus({ preventScroll: true });
@@ -309,12 +292,13 @@ class GraphNodeMenu {
309
292
  }
310
293
  exports.GraphNodeMenu = GraphNodeMenu;
311
294
  /**
312
- * One plain menu item. Selecting it closes the menu (returning keyboard focus to the box it was
313
- * opened from) and then runs the item. A radio item (`checked` true/false) is announced as one, and
314
- * an item with a `disabledReason` is disabled with that reason as its tooltip.
295
+ * One menu item. Selecting it closes the menu (returning keyboard focus to the box it was opened
296
+ * from) and then runs the item.
315
297
  *
316
- * Serialized with `toString()`, like WpSubmenu below; `close` is handed in because WpNodeMenu, which
317
- * owns it, is declared after this class in the emitted script.
298
+ * Real TypeScript serialized into every page's menu script with `toString()` (the technique
299
+ * GraphNavigation uses), so it is type-checked here and still needs no compiled sibling file: the
300
+ * design pages render straight from a source checkout. `close` is handed in because WpNodeMenu,
301
+ * which owns it, is declared after this class in the emitted script.
318
302
  */
319
303
  class WpMenuButton {
320
304
  build(item, close) {
@@ -322,14 +306,6 @@ class WpMenuButton {
322
306
  button.type = 'button';
323
307
  button.className = 'wp-node-menu-item';
324
308
  button.textContent = item.label;
325
- if (item.checked === true || item.checked === false) {
326
- button.setAttribute('role', 'menuitemradio');
327
- button.setAttribute('aria-checked', String(item.checked));
328
- }
329
- if (item.disabledReason) {
330
- button.disabled = true;
331
- button.title = item.disabledReason;
332
- }
333
309
  button.addEventListener('click', (ev) => {
334
310
  ev.preventDefault();
335
311
  ev.stopPropagation();
@@ -339,78 +315,4 @@ class WpMenuButton {
339
315
  return button;
340
316
  }
341
317
  }
342
- /**
343
- * A menu item that opens a nested menu beside it — the architecture page's "Mode ▸".
344
- *
345
- * Real TypeScript serialized into every page's menu script with `toString()` (the technique
346
- * GraphNavigation uses), so it is type-checked here and still needs no compiled sibling file: the
347
- * design pages render straight from a source checkout. It touches nothing but the DOM and the
348
- * `button` builder it is handed, because nothing else exists where it runs.
349
- *
350
- * An item without children renders exactly as before, so the runtime page and the design pages —
351
- * which never build one — are unaffected.
352
- */
353
- class WpSubmenu {
354
- build(item, button) {
355
- const host = document.createElement('div');
356
- host.className = 'wp-node-menu-sub-host';
357
- const parent = document.createElement('button');
358
- parent.type = 'button';
359
- parent.className = 'wp-node-menu-item wp-node-menu-parent';
360
- parent.setAttribute('aria-haspopup', 'menu');
361
- parent.setAttribute('aria-expanded', 'false');
362
- parent.textContent = item.label;
363
- const arrow = document.createElement('span');
364
- arrow.className = 'wp-node-menu-arrow';
365
- arrow.setAttribute('aria-hidden', 'true');
366
- arrow.textContent = '▸';
367
- parent.append(arrow);
368
- const sub = document.createElement('div');
369
- sub.className = 'wp-node-menu wp-node-menu-sub';
370
- sub.setAttribute('role', 'menu');
371
- sub.setAttribute('aria-label', item.label);
372
- for (const child of item.children)
373
- sub.append(button(child));
374
- host.append(parent, sub);
375
- this.wire(host, parent, sub);
376
- return host;
377
- }
378
- wire(host, parent, sub) {
379
- const enter = () => {
380
- this.show(host, parent, sub, true);
381
- sub.querySelector('button:not([disabled])')?.focus({ preventScroll: true });
382
- };
383
- host.addEventListener('mouseenter', () => this.show(host, parent, sub, true));
384
- host.addEventListener('mouseleave', () => this.show(host, parent, sub, false));
385
- parent.addEventListener('click', (ev) => {
386
- ev.preventDefault();
387
- ev.stopPropagation();
388
- enter();
389
- });
390
- parent.addEventListener('keydown', (ev) => {
391
- if (ev.key !== 'ArrowRight')
392
- return;
393
- ev.preventDefault();
394
- enter();
395
- });
396
- sub.addEventListener('keydown', (ev) => {
397
- if (ev.key !== 'ArrowLeft')
398
- return;
399
- ev.preventDefault();
400
- this.show(host, parent, sub, false);
401
- parent.focus({ preventScroll: true });
402
- });
403
- }
404
- /** Opens to the right, or to the LEFT when the right would run off the viewport. */
405
- show(host, parent, sub, open) {
406
- host.classList.toggle('wp-open', open);
407
- parent.setAttribute('aria-expanded', String(open));
408
- if (!open)
409
- return;
410
- sub.classList.remove('wp-flip');
411
- const right = host.getBoundingClientRect().right + sub.offsetWidth + 8;
412
- if (right > document.documentElement.clientWidth)
413
- sub.classList.add('wp-flip');
414
- }
415
- }
416
318
  //# sourceMappingURL=graph-node-menu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` search field backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither lock field nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n .wp-node-menu-item:disabled { color: #9aa0ad; cursor: not-allowed; background: none; }\n .wp-node-menu-item[aria-checked=\"true\"]::after { content: \"✓\"; float: right; margin-left: 12px; }\n /* A submenu (e.g. the architecture page's \"Mode ▸\") opens beside its parent item, on hover,\n * on click and on ArrowRight; ArrowLeft returns to the parent. */\n .wp-node-menu-sub-host { position: relative; }\n .wp-node-menu-arrow { float: right; margin-left: 12px; color: #777; }\n .wp-node-menu .wp-node-menu-sub { display: none; left: 100%; top: -5px; }\n .wp-node-menu .wp-node-menu-sub.wp-flip { left: auto; right: 100%; }\n .wp-node-menu-sub-host.wp-open > .wp-node-menu-sub { display: block; }\n g.wp-node-clickable { cursor: pointer; outline: none; }\n /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */\n g.wp-node-clickable.wp-keyboard-focus {\n outline: 2px dashed #7b1fa2;\n outline-offset: 2px;\n }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private itemScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect, children = []) {\n this.label = label;\n this.onSelect = onSelect;\n this.children = children;\n this.checked = null;\n this.disabledReason = '';\n }\n }`;\n }\n\n private menuScript(): string {\n return `${this.itemScript()}\n ${WpMenuButton.toString()}\n ${WpSubmenu.toString()}\n class WpNodeMenu {\n static submenu(item) { return new WpSubmenu().build(item, WpNodeMenu.button); }\n ${this.focusScript()}\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n WpNodeMenu.owner = nodeEl;\n WpNodeMenu.ownerKeyboard = WpNodeMenu.keyboard;\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) {\n const hasChildren = Array.isArray(item.children) && item.children.length > 0;\n menu.appendChild(hasChildren ? WpNodeMenu.submenu(item) : WpNodeMenu.button(item));\n }\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n menu.querySelector('button')?.focus({ preventScroll: true });\n }\n static button(item) { return new WpMenuButton().build(item, WpNodeMenu.close.bind(WpNodeMenu)); }\n ${this.positionScript()}\n ${this.keyboardScript()}\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n if (node.classList.contains('wp-layout')) return;\n if (WpNodeMenu.wired.has(node)) return;\n WpNodeMenu.wired.add(node);\n node.addEventListener('focus', function () {\n node.classList.toggle('wp-keyboard-focus', WpNodeMenu.keyboard);\n });\n node.addEventListener('blur', function () {\n node.classList.remove('wp-keyboard-focus');\n });\n node.classList.add('wp-node-clickable');\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n WpNodeMenu.wireKeyboard(node, name, itemsFor);\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n private positionScript(): string {\n return ` static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n`;\n }\n\n /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */\n private focusScript(): string {\n return ` static keyboard = false;\n static owner = null;\n static ownerKeyboard = false;\n static wired = new WeakSet();\n static close(returnFocus = false) {\n const open = document.getElementById('wp-node-menu');\n const owner = WpNodeMenu.owner;\n const keyboard = WpNodeMenu.ownerKeyboard;\n WpNodeMenu.owner = null;\n if (open === null) return;\n open.remove();\n if (returnFocus && keyboard && owner?.isConnected) {\n WpNodeMenu.keyboard = true;\n owner.focus({ preventScroll: true });\n }\n }\n static focusedName(svg) {\n const active = document.activeElement;\n const menu = document.getElementById('wp-node-menu');\n const node = menu?.contains(active) && WpNodeMenu.ownerKeyboard\n ? WpNodeMenu.owner : active;\n return svg?.contains(node) && (node === WpNodeMenu.owner || node?.classList.contains('wp-keyboard-focus'))\n ? node.querySelector('title')?.textContent.trim() : null;\n }\n static restoreFocus(svg, name) {\n if (name == null) return;\n const nodes = Array.from(svg.querySelectorAll('g.wp-node-clickable'));\n const node = nodes.find(node => node.querySelector('title')?.textContent.trim() === name);\n WpNodeMenu.keyboard = true;\n (node || nodes[0])?.focus({ preventScroll: true });\n }\n`;\n }\n\n private keyboardScript(): string {\n return `static wireKeyboard(node, name, itemsFor) {\n node.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Enter' && ev.key !== ' ') return;\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.keyboard = true;\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a lock field or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n rebind(svg) {\n const name = this.locked;\n this.svg = svg;\n this.clear();\n this.locked = name;\n if (name === null) return;\n for (const node of svg.querySelectorAll('g.node')) {\n if (node.querySelector('title')?.textContent === name) {\n this.lock(name, node);\n break;\n }\n }\n }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('pointerdown', function () {\n WpNodeMenu.keyboard = false;\n document.querySelectorAll('.wp-keyboard-focus').forEach(function (node) {\n node.classList.remove('wp-keyboard-focus');\n });\n }, true);\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.altKey || ev.ctrlKey || ev.metaKey) return;\n WpNodeMenu.keyboard = true;\n const active = document.activeElement;\n if (active?.classList.contains('wp-node-clickable')) active.classList.add('wp-keyboard-focus');\n if (ev.key === 'Escape') WpNodeMenu.close(true);\n }, true);`;\n }\n}\n\n/**\n * One plain menu item. Selecting it closes the menu (returning keyboard focus to the box it was\n * opened from) and then runs the item. A radio item (`checked` true/false) is announced as one, and\n * an item with a `disabledReason` is disabled with that reason as its tooltip.\n *\n * Serialized with `toString()`, like WpSubmenu below; `close` is handed in because WpNodeMenu, which\n * owns it, is declared after this class in the emitted script.\n */\nclass WpMenuButton {\n build(item: WpNodeMenuItem, close: (returnFocus: boolean) => void): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n if (item.checked === true || item.checked === false) {\n button.setAttribute('role', 'menuitemradio');\n button.setAttribute('aria-checked', String(item.checked));\n }\n if (item.disabledReason) {\n button.disabled = true;\n button.title = item.disabledReason;\n }\n button.addEventListener('click', (ev: MouseEvent): void => {\n ev.preventDefault();\n ev.stopPropagation();\n close(true);\n item.onSelect();\n });\n return button;\n }\n}\n\n/**\n * A menu item that opens a nested menu beside it — the architecture page's \"Mode ▸\".\n *\n * Real TypeScript serialized into every page's menu script with `toString()` (the technique\n * GraphNavigation uses), so it is type-checked here and still needs no compiled sibling file: the\n * design pages render straight from a source checkout. It touches nothing but the DOM and the\n * `button` builder it is handed, because nothing else exists where it runs.\n *\n * An item without children renders exactly as before, so the runtime page and the design pages —\n * which never build one — are unaffected.\n */\nclass WpSubmenu {\n build(\n item: WpNodeMenuItem,\n button: (child: WpNodeMenuItem) => HTMLButtonElement,\n ): HTMLDivElement {\n const host = document.createElement('div');\n host.className = 'wp-node-menu-sub-host';\n const parent = document.createElement('button');\n parent.type = 'button';\n parent.className = 'wp-node-menu-item wp-node-menu-parent';\n parent.setAttribute('aria-haspopup', 'menu');\n parent.setAttribute('aria-expanded', 'false');\n parent.textContent = item.label;\n const arrow = document.createElement('span');\n arrow.className = 'wp-node-menu-arrow';\n arrow.setAttribute('aria-hidden', 'true');\n arrow.textContent = '▸';\n parent.append(arrow);\n const sub = document.createElement('div');\n sub.className = 'wp-node-menu wp-node-menu-sub';\n sub.setAttribute('role', 'menu');\n sub.setAttribute('aria-label', item.label);\n for (const child of item.children) sub.append(button(child));\n host.append(parent, sub);\n this.wire(host, parent, sub);\n return host;\n }\n\n private wire(host: HTMLDivElement, parent: HTMLButtonElement, sub: HTMLDivElement): void {\n const enter = (): void => {\n this.show(host, parent, sub, true);\n sub.querySelector<HTMLButtonElement>('button:not([disabled])')?.focus({ preventScroll: true });\n };\n host.addEventListener('mouseenter', (): void => this.show(host, parent, sub, true));\n host.addEventListener('mouseleave', (): void => this.show(host, parent, sub, false));\n parent.addEventListener('click', (ev: MouseEvent): void => {\n ev.preventDefault();\n ev.stopPropagation();\n enter();\n });\n parent.addEventListener('keydown', (ev: KeyboardEvent): void => {\n if (ev.key !== 'ArrowRight') return;\n ev.preventDefault();\n enter();\n });\n sub.addEventListener('keydown', (ev: KeyboardEvent): void => {\n if (ev.key !== 'ArrowLeft') return;\n ev.preventDefault();\n this.show(host, parent, sub, false);\n parent.focus({ preventScroll: true });\n });\n }\n\n /** Opens to the right, or to the LEFT when the right would run off the viewport. */\n private show(host: HTMLDivElement, parent: HTMLButtonElement, sub: HTMLDivElement, open: boolean): void {\n host.classList.toggle('wp-open', open);\n parent.setAttribute('aria-expanded', String(open));\n if (!open) return;\n sub.classList.remove('wp-flip');\n const right = host.getBoundingClientRect().right + sub.offsetWidth + 8;\n if (right > document.documentElement.clientWidth) sub.classList.add('wp-flip');\n }\n}\n"]}
1
+ {"version":3,"file":"graph-node-menu.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-node-menu.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmDL,CAAC;IACP,CAAC;IAED;;;;;;OAMG;IACH,SAAS,CAAC,KAAa;QACnB,OAAO;UACL,KAAK,WAAW,KAAK;UACrB,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK;UACL,KAAK,yCAAyC,CAAC;IACrD,CAAC;IAED,+FAA+F;IAC/F,MAAM;QACF,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,UAAU,EAAE,KAAK,IAAI,CAAC,aAAa,EAAE,EAAE,CAAC;IACjF,CAAC;IAED;;;;OAIG;IACK,UAAU;QACd,OAAO;;;;;;UAML,CAAC;IACP,CAAC;IAEO,UAAU;QACd,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE;UACzB,YAAY,CAAC,QAAQ,EAAE;;cAEnB,IAAI,CAAC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;cAmBlB,IAAI,CAAC,cAAc,EAAE;cACrB,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;UA2BzB,CAAC;IACP,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;;CASd,CAAC;IACE,CAAC;IAED,yFAAyF;IACjF,WAAW;QACf,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+Bd,CAAC;IACE,CAAC;IAEO,cAAc;QAClB,OAAO;;;;;;;;UAQL,CAAC;IACP,CAAC;IAED;;;OAGG;IACK,UAAU;QACd,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCL,CAAC;IACP,CAAC;IAED,2EAA2E;IACnE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;kBAcG,CAAC;IACf,CAAC;CACJ;AAjRD,sCAiRC;AAED;;;;;;;;GAQG;AACH,MAAM,YAAY;IACd,KAAK,CAAC,IAAoB,EAAE,KAAqC;QAC7D,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;QAChD,MAAM,CAAC,IAAI,GAAG,QAAQ,CAAC;QACvB,MAAM,CAAC,SAAS,GAAG,mBAAmB,CAAC;QACvC,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC;QAChC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC,EAAc,EAAQ,EAAE;YACtD,EAAE,CAAC,cAAc,EAAE,CAAC;YACpB,EAAE,CAAC,eAAe,EAAE,CAAC;YACrB,KAAK,CAAC,IAAI,CAAC,CAAC;YACZ,IAAI,CAAC,QAAQ,EAAE,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ","sourcesContent":["/**\n * The floating per-node menu, shared by ALL THREE generated graphs.\n *\n * There is exactly ONE implementation of the menu — its CSS, its positioning, its dismissal and the\n * click wiring that opens it — and `architecture/dependencies.html` (graph-visualizer.ts), every\n * project's `design.html` (di-graph/design-visualizer.ts) and `tmp/webpieces/runtime-architecture.html`\n * (runtime-visualizer.ts) all inline these same bytes. Duplicating it into the three emitters is what\n * this module exists to prevent.\n *\n * Why the browser code is emitted as a STRING here rather than living in a compiled `*.client.ts`\n * beside its emitter: `generateDesignHTML` is called straight from a source checkout (by the\n * di-graph-generate executor's unit tests) and has no injection seam for the client text, so a\n * `readCompiledClient` sibling would make the design page unbuildable from source. A string keeps one\n * copy reachable by every emitter with no build-order coupling. The two pages that DO have a compiled\n * client (`graph-visualizer.client.ts`, `runtime-visualizer.client.ts`) call into these classes as\n * ambient globals — see viz-client-globals.d.ts.\n *\n * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:\n * - the architecture page already has a `#wp-lock` search field backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither lock field nor responsibilities, so they use\n * `WpNodeLock` below — dim every other box in that graph, light the locked one.\n * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.\n */\nexport class GraphNodeMenu {\n /**\n * Menu chrome + the \"this box is clickable\" affordance. Every node is clickable now (the menu\n * replaced direct navigation), so the cursor/glow is on every box rather than only the ones with\n * a design page.\n */\n styles(): string {\n return `\n .wp-node-menu {\n position: absolute;\n z-index: 1000;\n min-width: 150px;\n padding: 4px 0;\n background: white;\n border: 1px solid #cfcfcf;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0,0,0,0.22);\n font-family: Arial, sans-serif;\n font-size: 14px;\n }\n .wp-node-menu-title {\n padding: 4px 14px 6px;\n color: #777;\n font-size: 12px;\n font-family: monospace;\n border-bottom: 1px solid #eee;\n margin-bottom: 4px;\n }\n .wp-node-menu-item {\n display: block;\n width: 100%;\n padding: 7px 14px;\n border: 0;\n background: none;\n color: #1565C0;\n font: inherit;\n text-align: left;\n cursor: pointer;\n }\n .wp-node-menu-item:hover { background: #E3F2FD; }\n g.wp-node-clickable { cursor: pointer; outline: none; }\n /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */\n g.wp-node-clickable.wp-keyboard-focus {\n outline: 2px dashed #7b1fa2;\n outline-offset: 2px;\n }\n /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node\n * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A\n * shape left out of this list would be a box that is clickable but never looks it. */\n g.wp-node-clickable polygon,\n g.wp-node-clickable ellipse,\n g.wp-node-clickable path { transition: stroke-width 0.12s ease, filter 0.12s ease; }\n g.wp-node-clickable:hover polygon,\n g.wp-node-clickable:hover ellipse,\n g.wp-node-clickable:hover path {\n stroke: #8b3cf0;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private itemScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) {\n this.label = label;\n this.onSelect = onSelect;\n }\n }`;\n }\n\n private menuScript(): string {\n return `${this.itemScript()}\n ${WpMenuButton.toString()}\n class WpNodeMenu {\n ${this.focusScript()}\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n WpNodeMenu.owner = nodeEl;\n WpNodeMenu.ownerKeyboard = WpNodeMenu.keyboard;\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n menu.querySelector('button')?.focus({ preventScroll: true });\n }\n static button(item) { return new WpMenuButton().build(item, WpNodeMenu.close.bind(WpNodeMenu)); }\n ${this.positionScript()}\n ${this.keyboardScript()}\n static wire(svg, itemsFor) {\n svg.querySelectorAll('g.node').forEach(function (node) {\n const title = node.querySelector('title');\n const name = title === null || title.textContent === null\n ? '' : title.textContent.trim();\n if (name === '') return;\n if (node.classList.contains('wp-layout')) return;\n if (WpNodeMenu.wired.has(node)) return;\n WpNodeMenu.wired.add(node);\n node.addEventListener('focus', function () {\n node.classList.toggle('wp-keyboard-focus', WpNodeMenu.keyboard);\n });\n node.addEventListener('blur', function () {\n node.classList.remove('wp-keyboard-focus');\n });\n node.classList.add('wp-node-clickable');\n node.setAttribute('tabindex', '0');\n node.setAttribute('role', 'button');\n WpNodeMenu.wireKeyboard(node, name, itemsFor);\n node.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n });\n }\n }`;\n }\n\n private positionScript(): string {\n return ` static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n`;\n }\n\n /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */\n private focusScript(): string {\n return ` static keyboard = false;\n static owner = null;\n static ownerKeyboard = false;\n static wired = new WeakSet();\n static close(returnFocus = false) {\n const open = document.getElementById('wp-node-menu');\n const owner = WpNodeMenu.owner;\n const keyboard = WpNodeMenu.ownerKeyboard;\n WpNodeMenu.owner = null;\n if (open === null) return;\n open.remove();\n if (returnFocus && keyboard && owner?.isConnected) {\n WpNodeMenu.keyboard = true;\n owner.focus({ preventScroll: true });\n }\n }\n static focusedName(svg) {\n const active = document.activeElement;\n const menu = document.getElementById('wp-node-menu');\n const node = menu?.contains(active) && WpNodeMenu.ownerKeyboard\n ? WpNodeMenu.owner : active;\n return svg?.contains(node) && (node === WpNodeMenu.owner || node?.classList.contains('wp-keyboard-focus'))\n ? node.querySelector('title')?.textContent.trim() : null;\n }\n static restoreFocus(svg, name) {\n if (name == null) return;\n const nodes = Array.from(svg.querySelectorAll('g.wp-node-clickable'));\n const node = nodes.find(node => node.querySelector('title')?.textContent.trim() === name);\n WpNodeMenu.keyboard = true;\n (node || nodes[0])?.focus({ preventScroll: true });\n }\n`;\n }\n\n private keyboardScript(): string {\n return `static wireKeyboard(node, name, itemsFor) {\n node.addEventListener('keydown', function (ev) {\n if (ev.key !== 'Enter' && ev.key !== ' ') return;\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.keyboard = true;\n WpNodeMenu.open(node, name, itemsFor(name, node));\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a lock field or a responsibilities\n * list, so locking a box dims every other box in that graph and lights the locked one.\n */\n private lockScript(): string {\n return `\n class WpNodeLock {\n constructor(svg) { this.svg = svg; this.locked = null; }\n rebind(svg) {\n const name = this.locked;\n this.svg = svg;\n this.clear();\n this.locked = name;\n if (name === null) return;\n for (const node of svg.querySelectorAll('g.node')) {\n if (node.querySelector('title')?.textContent === name) {\n this.lock(name, node);\n break;\n }\n }\n }\n isLocked(name) { return this.locked === name; }\n toggle(name, nodeEl) {\n if (this.locked === name) this.clear();\n else this.lock(name, nodeEl);\n }\n lock(name, nodeEl) {\n this.clear();\n this.locked = name;\n this.svg.classList.add('wp-dim');\n nodeEl.classList.add('wp-focus');\n }\n clear() {\n this.locked = null;\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus').forEach(function (el) {\n el.classList.remove('wp-focus');\n });\n }\n }`;\n }\n\n /** Outside click and Escape both dismiss — wired once, on the document. */\n private dismissScript(): string {\n return `\n document.addEventListener('pointerdown', function () {\n WpNodeMenu.keyboard = false;\n document.querySelectorAll('.wp-keyboard-focus').forEach(function (node) {\n node.classList.remove('wp-keyboard-focus');\n });\n }, true);\n document.addEventListener('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.altKey || ev.ctrlKey || ev.metaKey) return;\n WpNodeMenu.keyboard = true;\n const active = document.activeElement;\n if (active?.classList.contains('wp-node-clickable')) active.classList.add('wp-keyboard-focus');\n if (ev.key === 'Escape') WpNodeMenu.close(true);\n }, true);`;\n }\n}\n\n/**\n * One menu item. Selecting it closes the menu (returning keyboard focus to the box it was opened\n * from) and then runs the item.\n *\n * Real TypeScript serialized into every page's menu script with `toString()` (the technique\n * GraphNavigation uses), so it is type-checked here and still needs no compiled sibling file: the\n * design pages render straight from a source checkout. `close` is handed in because WpNodeMenu,\n * which owns it, is declared after this class in the emitted script.\n */\nclass WpMenuButton {\n build(item: WpNodeMenuItem, close: (returnFocus: boolean) => void): HTMLButtonElement {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', (ev: MouseEvent): void => {\n ev.preventDefault();\n ev.stopPropagation();\n close(true);\n item.onSelect();\n });\n return button;\n }\n}\n"]}
@@ -10,6 +10,25 @@
10
10
  * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element
11
11
  * carries a `style=` attribute — every look is a class.
12
12
  */
13
+ /**
14
+ * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
15
+ * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
16
+ * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
17
+ * affected + every build input.
18
+ */
19
+ export declare enum ChangeScope {
20
+ EVERYTHING = "everything",
21
+ CHANGED = "changed",
22
+ DEPENDENTS = "dependents",
23
+ DEPENDENCIES = "dependencies",
24
+ BUILD = "build"
25
+ }
26
+ export declare class ScopeOption {
27
+ readonly scope: ChangeScope;
28
+ readonly label: string;
29
+ constructor(scope: ChangeScope, label: string);
30
+ }
31
+ export declare const SCOPE_OPTIONS: readonly ScopeOption[];
13
32
  /** Everything the shell frames, already rendered by its owner. */
14
33
  export declare class ShellParts {
15
34
  readonly title: string;
@@ -24,8 +43,19 @@ export declare class ShellParts {
24
43
  export declare class GraphPageShell {
25
44
  body(parts: ShellParts): string;
26
45
  private drawer;
27
- /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */
46
+ /**
47
+ * "Color by": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a
48
+ * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.
49
+ */
50
+ private modePulldown;
51
+ /** One menu option, two lines: the mode's name, then its grey subtitle. */
28
52
  private modeItem;
53
+ /**
54
+ * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change
55
+ * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip
56
+ * (when any is selected). Nothing applies until "Show N projects".
57
+ */
58
+ private filterPopover;
29
59
  private topbar;
30
60
  private overlays;
31
61
  /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */
@@ -12,8 +12,38 @@
12
12
  * carries a `style=` attribute — every look is a class.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.GraphPageShell = exports.ShellParts = void 0;
15
+ exports.GraphPageShell = exports.ShellParts = exports.SCOPE_OPTIONS = exports.ScopeOption = exports.ChangeScope = void 0;
16
16
  const graph_color_modes_1 = require("./graph-color-modes");
17
+ /**
18
+ * The Filter popover's "Changes on this branch" choices. The page computes each set from the Impact
19
+ * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their
20
+ * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +
21
+ * affected + every build input.
22
+ */
23
+ var ChangeScope;
24
+ (function (ChangeScope) {
25
+ ChangeScope["EVERYTHING"] = "everything";
26
+ ChangeScope["CHANGED"] = "changed";
27
+ ChangeScope["DEPENDENTS"] = "dependents";
28
+ ChangeScope["DEPENDENCIES"] = "dependencies";
29
+ ChangeScope["BUILD"] = "build";
30
+ })(ChangeScope || (exports.ChangeScope = ChangeScope = {}));
31
+ class ScopeOption {
32
+ scope;
33
+ label;
34
+ constructor(scope, label) {
35
+ this.scope = scope;
36
+ this.label = label;
37
+ }
38
+ }
39
+ exports.ScopeOption = ScopeOption;
40
+ exports.SCOPE_OPTIONS = [
41
+ new ScopeOption(ChangeScope.EVERYTHING, 'Everything'),
42
+ new ScopeOption(ChangeScope.CHANGED, 'Changed'),
43
+ new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + what uses them'),
44
+ new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + what they use'),
45
+ new ScopeOption(ChangeScope.BUILD, 'Whole build of this branch'),
46
+ ];
17
47
  /** Everything the shell frames, already rendered by its owner. */
18
48
  class ShellParts {
19
49
  title;
@@ -50,9 +80,12 @@ class GraphPageShell {
50
80
  <div class="wp-brand">${this.brandIcon()}<span>${parts.title}</span></div>
51
81
  <div class="wp-group" role="group" aria-label="Color mode">
52
82
  <div class="wp-label">Color by</div>
53
- <div class="wp-modes">${graph_color_modes_1.GRAPH_MODES.map((info, index) => this.modeItem(info, index)).join('')}</div>
83
+ ${this.modePulldown()}
54
84
  <p class="wp-mode-reason" id="wp-impact-reason" hidden></p>
55
85
  </div>
86
+ <div class="wp-group">
87
+ <button type="button" class="wp-filter-btn" id="wp-filter-open" aria-controls="wp-filter-pop" aria-expanded="false"><span>Filter</span><span class="wp-badge" id="wp-filter-badge" hidden>0</span></button>
88
+ </div>
56
89
  <div class="wp-group">
57
90
  <label class="wp-label" for="wp-lock">Focus</label>
58
91
  ${parts.lockControl}
@@ -72,16 +105,49 @@ class GraphPageShell {
72
105
  </div>
73
106
  </aside>`;
74
107
  }
75
- /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */
108
+ /**
109
+ * "Color by": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a
110
+ * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.
111
+ */
112
+ modePulldown() {
113
+ const first = graph_color_modes_1.GRAPH_MODES[0];
114
+ return `<div class="wp-pulldown">
115
+ <button type="button" class="wp-pill-trigger" id="wp-mode-trigger" aria-haspopup="menu" aria-controls="wp-mode-menu" aria-expanded="false">
116
+ <span class="wp-pill-text"><span class="wp-mode-name" id="wp-mode-current">${first.name}</span><span class="wp-mode-sub" id="wp-mode-current-sub">${first.subtitle}</span></span><span class="wp-caret" aria-hidden="true">▾</span>
117
+ </button>
118
+ <div class="wp-menu" id="wp-mode-menu" role="menu" aria-label="Color by" hidden>${graph_color_modes_1.GRAPH_MODES.map((info, index) => this.modeItem(info, index)).join('')}</div>
119
+ </div>`;
120
+ }
121
+ /** One menu option, two lines: the mode's name, then its grey subtitle. */
76
122
  modeItem(info, index) {
77
- const pressed = info.mode === graph_color_modes_1.GraphMode.RUNTIME ? 'true' : 'false';
78
- return (`<button type="button" class="wp-mode" data-wp-mode="${info.mode}" aria-pressed="${pressed}" title="Key ${index + 1}">` +
123
+ const checked = info.mode === graph_color_modes_1.GraphMode.RUNTIME ? 'true' : 'false';
124
+ return (`<button type="button" class="wp-mode" role="menuitemradio" data-wp-mode="${info.mode}" aria-checked="${checked}" title="Key ${index + 1}">` +
79
125
  `<span class="wp-mode-name">${info.name}</span><span class="wp-mode-sub">${info.subtitle}</span></button>`);
80
126
  }
127
+ /**
128
+ * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change
129
+ * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip
130
+ * (when any is selected). Nothing applies until "Show N projects".
131
+ */
132
+ filterPopover() {
133
+ const scopes = exports.SCOPE_OPTIONS.map((option) => `<label class="wp-scope"><input type="radio" name="wp-scope" value="${option.scope}"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +
134
+ `<span class="wp-scope-name">${option.label}</span><span class="wp-count" data-wp-scope-count="${option.scope}"></span></label>`).join('');
135
+ const chips = (group, values) => values
136
+ .map((value) => `<button type="button" class="wp-chip" data-wp-chip-group="${group}" data-wp-chip="${value}" aria-pressed="false">${value}</button>`)
137
+ .join('');
138
+ return `<div class="wp-pop wp-filter-pop" id="wp-filter-pop" role="dialog" aria-label="Filter projects" hidden>
139
+ <header><span>Filter</span><button type="button" class="wp-icon wp-icon-mini" id="wp-filter-close" aria-label="Close the filter">✕</button></header>
140
+ <fieldset class="wp-fgroup" id="wp-scope-group"><legend>Changes on this branch</legend>${scopes}<p class="wp-scope-reason" id="wp-scope-reason" hidden></p></fieldset>
141
+ <fieldset class="wp-fgroup"><legend>Runtime <small>any match</small></legend><div class="wp-chips">${chips('runtime', graph_color_modes_1.FRAMEWORK_ORDER)}</div></fieldset>
142
+ <fieldset class="wp-fgroup"><legend>Role <small>any match</small></legend><div class="wp-chips">${chips('role', graph_color_modes_1.ROLE_STYLES.map((style) => style.role))}</div></fieldset>
143
+ <footer><button type="button" class="wp-text-btn" id="wp-filter-clear">Clear all</button><button type="button" class="wp-primary" id="wp-filter-apply">Show all projects</button></footer>
144
+ </div>`;
145
+ }
81
146
  topbar(parts) {
82
147
  return `<div class="wp-topbar">
83
148
  <button type="button" class="wp-icon" id="wp-collapse" aria-label="Toggle the controls drawer" aria-controls="wp-side" aria-expanded="true">☰</button>
84
149
  <div class="wp-crumb" id="wp-crumb">Mode: <b>Runtime</b></div>
150
+ <div class="wp-pills" id="wp-filter-pills" aria-label="Active filters"></div>
85
151
  ${parts.filterStatus}
86
152
  <button type="button" class="wp-icon" id="wp-help-btn" aria-label="How to read this graph" aria-controls="wp-help" aria-expanded="false">?</button>
87
153
  </div>`;
@@ -92,6 +158,7 @@ class GraphPageShell {
92
158
  ${parts.legend}
93
159
  </div>
94
160
  ${this.help()}
161
+ ${this.filterPopover()}
95
162
  <div class="wp-pop wp-snapshot" id="wp-snapshot" hidden>${parts.snapshot}</div>
96
163
  <aside class="wp-panel" id="wp-resp-panel" aria-label="Responsibilities" hidden>
97
164
  <header><span>Responsibilities</span><button type="button" class="wp-icon wp-icon-mini" id="wp-resp-close" aria-label="Close responsibilities">✕</button></header>
@@ -103,7 +170,8 @@ class GraphPageShell {
103
170
  return `<div class="wp-pop wp-help" id="wp-help" role="dialog" aria-label="How to read this graph" hidden>
104
171
  <header><span>Reading the graph</span><button type="button" class="wp-icon wp-icon-mini" id="wp-help-close" aria-label="Close help">✕</button></header>
105
172
  <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>
106
- <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row) and <strong>Mode ▸</strong>.</p>
173
+ <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>
174
+ <p><strong>Filter</strong> narrows the graph by what this branch changed, by runtime and by role; the three groups combine. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>
107
175
  <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>
108
176
  <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>
109
177
  <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
@@ -111,7 +179,7 @@ class GraphPageShell {
111
179
  }
112
180
  brandIcon() {
113
181
  return ('<svg width="22" height="22" viewBox="0 0 22 22" aria-hidden="true">' +
114
- '<rect x="1" y="1" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
182
+ '<rect x="1" y="1" width="8" height="6" rx="1.5" fill="none" stroke="#a46bf7" stroke-width="1.6"/>' +
115
183
  '<rect x="13" y="1" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
116
184
  '<rect x="7" y="15" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
117
185
  '<path d="M5 7v4h12V7M11 11v4" fill="none" stroke="#e9eaf0" stroke-width="1.6"/></svg>');
@@ -1 +1 @@
1
- {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAEH,2DAAuE;AAEvE,kEAAkE;AAClE,MAAa,UAAU;IAEC;IACA;IACA;IACA;IACA;IACA;IACA;IAPpB,YACoB,KAAa,EACb,WAAmB,EACnB,MAAc,EACd,OAAe,EACf,YAAoB,EACpB,QAAgB,EAChB,gBAAwB;QANxB,UAAK,GAAL,KAAK,CAAQ;QACb,gBAAW,GAAX,WAAW,CAAQ;QACnB,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;QACf,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,qBAAgB,GAAhB,gBAAgB,CAAQ;IACzC,CAAC;CACP;AAVD,gCAUC;AAED,MAAa,cAAc;IACvB,IAAI,CAAC,KAAiB;QAClB,OAAO;MACT,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;;UAEd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;;UAElB,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;;OAEvB,CAAC;IACJ,CAAC;IAEO,MAAM,CAAC,KAAiB;QAC5B,OAAO;gCACiB,IAAI,CAAC,SAAS,EAAE,SAAS,KAAK,CAAC,KAAK;;;oCAGhC,+BAAW,CAAC,GAAG,CAAC,CAAC,IAAc,EAAE,KAAa,EAAU,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;;cAKrH,KAAK,CAAC,WAAW;;;;;cAKjB,KAAK,CAAC,MAAM;;;;cAIZ,KAAK,CAAC,OAAO;;;;;;aAMd,CAAC;IACV,CAAC;IAED,gGAAgG;IACxF,QAAQ,CAAC,IAAc,EAAE,KAAa;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,KAAK,6BAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QACnE,OAAO,CACH,uDAAuD,IAAI,CAAC,IAAI,mBAAmB,OAAO,gBAAgB,KAAK,GAAG,CAAC,IAAI;YACvH,8BAA8B,IAAI,CAAC,IAAI,oCAAoC,IAAI,CAAC,QAAQ,kBAAkB,CAC7G,CAAC;IACN,CAAC;IAEO,MAAM,CAAC,KAAiB;QAC5B,OAAO;;;cAGD,KAAK,CAAC,YAAY;;eAEjB,CAAC;IACZ,CAAC;IAEO,QAAQ,CAAC,KAAiB;QAC9B,OAAO;;cAED,KAAK,CAAC,MAAM;;UAEhB,IAAI,CAAC,IAAI,EAAE;kEAC6C,KAAK,CAAC,QAAQ;;;cAGlE,KAAK,CAAC,gBAAgB;iBACnB,CAAC;IACd,CAAC;IAED,6FAA6F;IACrF,IAAI;QACR,OAAO;;;;;;;eAOA,CAAC;IACZ,CAAC;IAEO,SAAS;QACb,OAAO,CACH,qEAAqE;YACrE,mGAAmG;YACnG,oGAAoG;YACpG,oGAAoG;YACpG,uFAAuF,CAC1F,CAAC;IACN,CAAC;CACJ;AA5FD,wCA4FC","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { GRAPH_MODES, GraphMode, ModeInfo } from './graph-color-modes';\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n <div class=\"wp-modes\">${GRAPH_MODES.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */\n private modeItem(info: ModeInfo, index: number): string {\n const pressed = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" data-wp-mode=\"${info.mode}\" aria-pressed=\"${pressed}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n private topbar(parts: ShellParts): string {\n return `<div class=\"wp-topbar\">\n <button type=\"button\" class=\"wp-icon\" id=\"wp-collapse\" aria-label=\"Toggle the controls drawer\" aria-controls=\"wp-side\" aria-expanded=\"true\">☰</button>\n <div class=\"wp-crumb\" id=\"wp-crumb\">Mode: <b>Runtime</b></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help()}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(): string {\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row) and <strong>Mode ▸</strong>.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { FRAMEWORK_ORDER, GRAPH_MODES, GraphMode, ModeInfo, ROLE_STYLES, RoleStyle } from './graph-color-modes';\n\n/**\n * The Filter popover's \"Changes on this branch\" choices. The page computes each set from the Impact\n * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their\n * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +\n * affected + every build input.\n */\nexport enum ChangeScope {\n EVERYTHING = 'everything',\n CHANGED = 'changed',\n DEPENDENTS = 'dependents',\n DEPENDENCIES = 'dependencies',\n BUILD = 'build',\n}\n\nexport class ScopeOption {\n constructor(\n public readonly scope: ChangeScope,\n public readonly label: string,\n ) {}\n}\n\nexport const SCOPE_OPTIONS: readonly ScopeOption[] = [\n new ScopeOption(ChangeScope.EVERYTHING, 'Everything'),\n new ScopeOption(ChangeScope.CHANGED, 'Changed'),\n new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + what uses them'),\n new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + what they use'),\n new ScopeOption(ChangeScope.BUILD, 'Whole build of this branch'),\n];\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n ${this.modePulldown()}\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n </div>\n <div class=\"wp-group\">\n <button type=\"button\" class=\"wp-filter-btn\" id=\"wp-filter-open\" aria-controls=\"wp-filter-pop\" aria-expanded=\"false\"><span>Filter</span><span class=\"wp-badge\" id=\"wp-filter-badge\" hidden>0</span></button>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /**\n * \"Color by\": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a\n * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.\n */\n private modePulldown(): string {\n const first = GRAPH_MODES[0];\n return `<div class=\"wp-pulldown\">\n <button type=\"button\" class=\"wp-pill-trigger\" id=\"wp-mode-trigger\" aria-haspopup=\"menu\" aria-controls=\"wp-mode-menu\" aria-expanded=\"false\">\n <span class=\"wp-pill-text\"><span class=\"wp-mode-name\" id=\"wp-mode-current\">${first.name}</span><span class=\"wp-mode-sub\" id=\"wp-mode-current-sub\">${first.subtitle}</span></span><span class=\"wp-caret\" aria-hidden=\"true\">▾</span>\n </button>\n <div class=\"wp-menu\" id=\"wp-mode-menu\" role=\"menu\" aria-label=\"Color by\" hidden>${GRAPH_MODES.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n </div>`;\n }\n\n /** One menu option, two lines: the mode's name, then its grey subtitle. */\n private modeItem(info: ModeInfo, index: number): string {\n const checked = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" role=\"menuitemradio\" data-wp-mode=\"${info.mode}\" aria-checked=\"${checked}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n /**\n * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change\n * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip\n * (when any is selected). Nothing applies until \"Show N projects\".\n */\n private filterPopover(): string {\n const scopes = SCOPE_OPTIONS.map(\n (option: ScopeOption): string =>\n `<label class=\"wp-scope\"><input type=\"radio\" name=\"wp-scope\" value=\"${option.scope}\"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +\n `<span class=\"wp-scope-name\">${option.label}</span><span class=\"wp-count\" data-wp-scope-count=\"${option.scope}\"></span></label>`,\n ).join('');\n const chips = (group: string, values: readonly string[]): string =>\n values\n .map(\n (value: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"${group}\" data-wp-chip=\"${value}\" aria-pressed=\"false\">${value}</button>`,\n )\n .join('');\n return `<div class=\"wp-pop wp-filter-pop\" id=\"wp-filter-pop\" role=\"dialog\" aria-label=\"Filter projects\" hidden>\n <header><span>Filter</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-filter-close\" aria-label=\"Close the filter\">✕</button></header>\n <fieldset class=\"wp-fgroup\" id=\"wp-scope-group\"><legend>Changes on this branch</legend>${scopes}<p class=\"wp-scope-reason\" id=\"wp-scope-reason\" hidden></p></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Runtime <small>any match</small></legend><div class=\"wp-chips\">${chips('runtime', FRAMEWORK_ORDER)}</div></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Role <small>any match</small></legend><div class=\"wp-chips\">${chips('role', ROLE_STYLES.map((style: RoleStyle): string => style.role))}</div></fieldset>\n <footer><button type=\"button\" class=\"wp-text-btn\" id=\"wp-filter-clear\">Clear all</button><button type=\"button\" class=\"wp-primary\" id=\"wp-filter-apply\">Show all projects</button></footer>\n </div>`;\n }\n\n private topbar(parts: ShellParts): string {\n return `<div class=\"wp-topbar\">\n <button type=\"button\" class=\"wp-icon\" id=\"wp-collapse\" aria-label=\"Toggle the controls drawer\" aria-controls=\"wp-side\" aria-expanded=\"true\">☰</button>\n <div class=\"wp-crumb\" id=\"wp-crumb\">Mode: <b>Runtime</b></div>\n <div class=\"wp-pills\" id=\"wp-filter-pills\" aria-label=\"Active filters\"></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help()}\n ${this.filterPopover()}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(): string {\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>\n <p><strong>Filter</strong> narrows the graph by what this branch changed, by runtime and by role; the three groups combine. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#a46bf7\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
@@ -9,11 +9,22 @@ export declare class GraphPageStyles {
9
9
  private drawer;
10
10
  private controls;
11
11
  private main;
12
+ /** The Filter popover: three groups, chips, and the "Show N projects" footer. */
13
+ private filterPopover;
12
14
  private overlays;
13
15
  /**
14
- * The graph fills what the topbar leaves. A Runtime box with a specialization is an HTML-like
15
- * table, so its cells are extra polygons inside the node: the hover glow and the lock outline are
16
- * kept on the box's own outline (the FIRST polygon) instead of stroking every cell.
16
+ * The graph fills what the topbar leaves.
17
+ *
18
+ * The hover glow and the lock outline belong to the OUTER box only. A nested Runtime box (one
19
+ * runtime plus a specialization) is an HTML-like table, so after the node's own outline Graphviz
20
+ * emits a `<polygon>` for the base-colored cell and a `<path>` for the inner `STYLE="rounded"`
21
+ * table — matching only `polygon` here is what let the glow stroke the inner box (#1158). So the
22
+ * glow is suppressed on EVERY shape after the node's first, whatever its element type.
23
+ *
24
+ * A striped (multi-runtime) box is the one exception to "the first shape is the outline":
25
+ * Graphviz draws one filled polygon per stripe first and the outline LAST, as an unfilled polygon.
26
+ * graph-color-modes.ts stamps those boxes `wp-striped`, and for them the outline is that last,
27
+ * `fill="none"` polygon; the stripes keep their thin dividers.
17
28
  */
18
29
  private graph;
19
30
  private cards;