@webpieces/nx-webpieces-rules 0.4.859 → 0.4.860

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/nx-webpieces-rules",
3
- "version": "0.4.859",
3
+ "version": "0.4.860",
4
4
  "description": "Nx-specific webpieces validation rules, graph tooling, executors, and inference plugin.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -20,12 +20,12 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
- "@webpieces/api-doc-model": "0.4.859",
24
- "@webpieces/core-util": "0.4.859",
25
- "@webpieces/eslint-rules": "0.4.859",
26
- "@webpieces/rules-config": "0.4.859",
23
+ "@webpieces/api-doc-model": "0.4.860",
24
+ "@webpieces/core-util": "0.4.860",
25
+ "@webpieces/eslint-rules": "0.4.860",
26
+ "@webpieces/rules-config": "0.4.860",
27
27
  "madge": "8.0.0",
28
- "@webpieces/rules-sdk": "0.4.859"
28
+ "@webpieces/rules-sdk": "0.4.860"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@nx/devkit": ">=18.0.0"
@@ -41,7 +41,7 @@
41
41
  "access": "public"
42
42
  },
43
43
  "devDependencies": {
44
- "@webpieces/tooling-testkit": "0.4.859"
44
+ "@webpieces/tooling-testkit": "0.4.860"
45
45
  },
46
46
  "types": "./src/index.d.ts",
47
47
  "bin": {
@@ -117,7 +117,9 @@ class WpFilterPage {
117
117
  // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value
118
118
  .catch((err) => this.error(err instanceof Error ? err : new Error(String(err))));
119
119
  }
120
- captureBinding(_svg) { return () => { }; }
120
+ captureBinding(_svg) {
121
+ return () => { };
122
+ }
121
123
  wireControls() { }
122
124
  prepareSvg(_svg) { }
123
125
  filterItem(name) {
@@ -135,6 +137,7 @@ class WpFilterPage {
135
137
  const previousAnchor = this.anchor;
136
138
  const previousRetained = this.retained;
137
139
  const previousSvg = host.querySelector('svg');
140
+ const focusName = WpNodeMenu.focusedName(previousSvg);
138
141
  const rollback = this.captureBinding(previousSvg);
139
142
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure
140
143
  try {
@@ -149,6 +152,7 @@ class WpFilterPage {
149
152
  host.replaceChildren(svg);
150
153
  this.wireSvg(svg);
151
154
  this.indicator();
155
+ WpNodeMenu.restoreFocus(svg, focusName);
152
156
  this.error(null);
153
157
  // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary
154
158
  }
@@ -161,6 +165,8 @@ class WpFilterPage {
161
165
  else
162
166
  host.replaceChildren();
163
167
  rollback();
168
+ if (previousSvg !== null)
169
+ WpNodeMenu.restoreFocus(previousSvg, focusName);
164
170
  this.error(err instanceof Error ? err : new Error(String(err)));
165
171
  }
166
172
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n render(retained: Set<string>): string {\n const nodes = this.model.nodes.filter((node) => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node) => node.dot).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void { return (): void => {}; }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained);\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
1
+ {"version":3,"file":"graph-filter.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-filter.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Plain browser helpers shared by architecture and runtime; no CommonJS imports. */\nclass WpGraphChain {\n readonly inNodes = new Map<string, Set<string>>();\n readonly outNodes = new Map<string, Set<string>>();\n\n constructor(private readonly model: RenderModelJson) {\n const ids = new Set(model.nodes.map((node) => node.id));\n for (const edge of model.edges) {\n if (!ids.has(edge.from) || !ids.has(edge.to)) continue;\n this.add(this.outNodes, edge.from, edge.to);\n this.add(this.inNodes, edge.to, edge.from);\n }\n }\n\n private add(map: Map<string, Set<string>>, from: string, to: string): void {\n if (!map.has(from)) map.set(from, new Set());\n map.get(from)?.add(to);\n }\n\n nodes(anchor: string): Set<string> {\n const retained = new Set<string>();\n if (!this.model.nodes.some((node) => node.id === anchor)) return retained;\n for (const direction of [this.inNodes, this.outNodes]) {\n const visited = new Set([anchor]);\n const stack = [anchor];\n while (stack.length > 0) {\n const id = stack.pop() as string;\n retained.add(id);\n for (const next of direction.get(id) ?? []) {\n if (visited.has(next)) continue;\n visited.add(next);\n stack.push(next);\n }\n }\n }\n return retained;\n }\n}\n\nclass WpRenderBand {\n constructor(\n readonly level: number,\n readonly nodeNames: string[],\n ) {}\n}\n\n/** Original records plus fresh ranks: removed nodes cannot reappear via a rank/edge. */\nclass WpFilteredDot {\n constructor(private readonly model: RenderModelJson) {}\n\n render(retained: Set<string>): string {\n const nodes = this.model.nodes.filter((node) => retained.has(node.id));\n const edges = this.model.edges.filter(\n (edge) => retained.has(edge.from) && retained.has(edge.to),\n );\n const bands = this.model.bands\n .map(\n (band) =>\n new WpRenderBand(\n band.level,\n band.nodeNames.filter((id) => retained.has(id)),\n ),\n )\n .filter((band) => band.nodeNames.length > 0);\n return (\n this.model.header +\n nodes.map((node) => node.dot).join('') +\n this.bandDot(bands) +\n edges.map((edge) => edge.dot).join('') +\n this.model.footer\n );\n }\n\n private bandDot(bands: WpRenderBand[]): string {\n const nodeAttrs =\n '[style=invis, shape=point, width=0.01, height=0.01, label=\"\", class=\"wp-layout\"]';\n const edgeAttrs = '[style=invis, class=\"wp-layout\"]';\n let dot = '';\n for (const band of bands) {\n const anchor = `__wp_layout_L${band.level}`;\n dot += ` \"${anchor}\" ${nodeAttrs};\\n { rank=same; \"${anchor}\"; `;\n dot += band.nodeNames.map((id) => `${JSON.stringify(id)}; `).join('') + '}\\n';\n }\n for (let i = 0; i + 1 < bands.length; i++) {\n const upper = bands[i];\n const lower = bands[i + 1];\n const from = `__wp_layout_L${upper.level}`;\n const to = `__wp_layout_L${lower.level}`;\n if (upper.nodeNames.length <= 10 && lower.nodeNames.length <= 10) {\n dot += ` \"${from}\" -> \"${to}\" ${edgeAttrs};\\n`;\n continue;\n }\n const spacer = `__wp_layout_spacer_L${upper.level}_L${lower.level}`;\n dot += ` \"${spacer}\" ${nodeAttrs};\\n { rank=same; \"${spacer}\"; }\\n`;\n dot += ` \"${from}\" -> \"${spacer}\" ${edgeAttrs};\\n \"${spacer}\" -> \"${to}\" ${edgeAttrs};\\n`;\n }\n return dot;\n }\n}\n\n/** Page-owned state survives replacements; rendering failures leave the usable SVG intact. */\nabstract class WpFilterPage {\n protected readonly chain: WpGraphChain;\n protected locked: string | null = null;\n protected anchor: string | null = null;\n protected retained: Set<string>;\n private viz: VizInstance | null = null;\n\n constructor(protected readonly model: RenderModelJson) {\n this.chain = new WpGraphChain(model);\n this.retained = new Set(model.nodes.map((node) => node.id));\n }\n\n render(): void {\n document\n .getElementById('wp-filter-off')\n ?.addEventListener('click', () => this.filter(null));\n this.wireControls();\n Viz.instance()\n .then((viz) => {\n this.viz = viz;\n this.filter(null);\n })\n // webpieces-disable no-any-unknown -- promise rejections may carry any JavaScript value\n .catch((err: unknown): void =>\n this.error(err instanceof Error ? err : new Error(String(err))),\n );\n }\n\n protected captureBinding(_svg: SVGSVGElement | null): () => void {\n return (): void => {};\n }\n protected wireControls(): void {}\n protected abstract wireSvg(svg: SVGSVGElement): void;\n protected prepareSvg(_svg: SVGSVGElement): void {}\n\n filterItem(name: string): WpNodeMenuItem {\n return new WpNodeMenuItem(\n this.anchor === null ? 'Filter Unconnected' : 'Turn off Filter',\n () => this.filter(this.anchor === null ? name : null),\n );\n }\n\n filter(anchor: string | null): void {\n if (this.viz === null) return;\n const host = document.getElementById('graph');\n if (host === null) return;\n const retained =\n anchor === null\n ? new Set(this.model.nodes.map((node) => node.id))\n : this.chain.nodes(anchor);\n const previousAnchor = this.anchor;\n const previousRetained = this.retained;\n const previousSvg = host.querySelector('svg');\n const focusName = WpNodeMenu.focusedName(previousSvg);\n const rollback = this.captureBinding(previousSvg);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- browser transaction restores the previous usable graph on failure\n try {\n const dot =\n anchor === null\n ? this.model.fullDot\n : new WpFilteredDot(this.model).render(retained);\n const svg = this.viz.renderSVGElement(dot);\n this.prepareSvg(svg);\n WpNodeMenu.close();\n this.anchor = anchor;\n this.retained = retained;\n host.replaceChildren(svg);\n this.wireSvg(svg);\n this.indicator();\n WpNodeMenu.restoreFocus(svg, focusName);\n this.error(null);\n // webpieces-disable no-any-unknown -- JavaScript may throw any value at this browser boundary\n } catch (err: unknown) {\n //const error = toError(err);\n this.anchor = previousAnchor;\n this.retained = previousRetained;\n if (previousSvg !== null) host.replaceChildren(previousSvg);\n else host.replaceChildren();\n rollback();\n if (previousSvg !== null) WpNodeMenu.restoreFocus(previousSvg, focusName);\n this.error(err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n private indicator(): void {\n const status = document.getElementById('wp-filter-status');\n const label = document.getElementById('wp-filter-anchor');\n if (status !== null) status.hidden = this.anchor === null;\n if (label !== null) label.textContent = this.anchor;\n if (this.anchor !== null) document.getElementById('wp-filter-off')?.focus();\n else document.querySelector<SVGGElement>('#graph g.wp-node-clickable')?.focus();\n }\n\n private error(err: Error | null): void {\n const el = document.getElementById('wp-graph-error');\n if (el === null) return;\n el.hidden = err === null;\n el.textContent =\n err === null\n ? ''\n : `Graph rendering failed: ${err.message}. Try Turn off Filter or reload the page.`;\n }\n}\n"]}
@@ -45,6 +45,9 @@ export declare class GraphNodeMenu {
45
45
  * menu. Opening a menu closes any other, so at most one is ever on screen.
46
46
  */
47
47
  private menuScript;
48
+ private positionScript;
49
+ /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */
50
+ private focusScript;
48
51
  private keyboardScript;
49
52
  /**
50
53
  * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
@@ -65,7 +65,12 @@ class GraphNodeMenu {
65
65
  cursor: pointer;
66
66
  }
67
67
  .wp-node-menu-item:hover { background: #E3F2FD; }
68
- g.wp-node-clickable { cursor: pointer; }
68
+ g.wp-node-clickable { cursor: pointer; outline: none; }
69
+ /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */
70
+ g.wp-node-clickable.wp-keyboard-focus {
71
+ outline: 2px dashed #7b1fa2;
72
+ outline-offset: 2px;
73
+ }
69
74
  /* Every shape Graphviz (or the runtime page's own queue-cylinder redraw) can emit for a node
70
75
  * body: box/record -> polygon, circle -> ellipse, cylinder and the redrawn queue -> path. A
71
76
  * shape left out of this list would be a box that is clickable but never looks it. */
@@ -111,12 +116,11 @@ class GraphNodeMenu {
111
116
  constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }
112
117
  }
113
118
  class WpNodeMenu {
114
- static close() {
115
- const open = document.getElementById('wp-node-menu');
116
- if (open !== null) open.remove();
117
- }
119
+ ${this.focusScript()}
118
120
  static open(nodeEl, name, items) {
119
121
  WpNodeMenu.close();
122
+ WpNodeMenu.owner = nodeEl;
123
+ WpNodeMenu.ownerKeyboard = WpNodeMenu.keyboard;
120
124
  const menu = document.createElement('div');
121
125
  menu.id = 'wp-node-menu';
122
126
  menu.className = 'wp-node-menu';
@@ -128,6 +132,7 @@ class GraphNodeMenu {
128
132
  for (const item of items) menu.appendChild(WpNodeMenu.button(item));
129
133
  document.body.appendChild(menu);
130
134
  WpNodeMenu.place(menu, nodeEl);
135
+ menu.querySelector('button')?.focus({ preventScroll: true });
131
136
  }
132
137
  static button(item) {
133
138
  const button = document.createElement('button');
@@ -137,20 +142,12 @@ class GraphNodeMenu {
137
142
  button.addEventListener('click', function (ev) {
138
143
  ev.preventDefault();
139
144
  ev.stopPropagation();
140
- WpNodeMenu.close();
145
+ WpNodeMenu.close(true);
141
146
  item.onSelect();
142
147
  });
143
148
  return button;
144
149
  }
145
- static place(menu, nodeEl) {
146
- const box = nodeEl.getBoundingClientRect();
147
- const own = menu.getBoundingClientRect();
148
- const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;
149
- let left = box.left + window.scrollX;
150
- if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);
151
- menu.style.left = left + 'px';
152
- menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
153
- }
150
+ ${this.positionScript()}
154
151
  ${this.keyboardScript()}
155
152
  static wire(svg, itemsFor) {
156
153
  svg.querySelectorAll('g.node').forEach(function (node) {
@@ -159,6 +156,14 @@ class GraphNodeMenu {
159
156
  ? '' : title.textContent.trim();
160
157
  if (name === '') return;
161
158
  if (node.classList.contains('wp-layout')) return;
159
+ if (WpNodeMenu.wired.has(node)) return;
160
+ WpNodeMenu.wired.add(node);
161
+ node.addEventListener('focus', function () {
162
+ node.classList.toggle('wp-keyboard-focus', WpNodeMenu.keyboard);
163
+ });
164
+ node.addEventListener('blur', function () {
165
+ node.classList.remove('wp-keyboard-focus');
166
+ });
162
167
  node.classList.add('wp-node-clickable');
163
168
  node.setAttribute('tabindex', '0');
164
169
  node.setAttribute('role', 'button');
@@ -172,14 +177,61 @@ class GraphNodeMenu {
172
177
  }
173
178
  }`;
174
179
  }
180
+ positionScript() {
181
+ return ` static place(menu, nodeEl) {
182
+ const box = nodeEl.getBoundingClientRect();
183
+ const own = menu.getBoundingClientRect();
184
+ const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;
185
+ let left = box.left + window.scrollX;
186
+ if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);
187
+ menu.style.left = left + 'px';
188
+ menu.style.top = (box.bottom + window.scrollY + 4) + 'px';
189
+ }
190
+ `;
191
+ }
192
+ /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */
193
+ focusScript() {
194
+ return ` static keyboard = false;
195
+ static owner = null;
196
+ static ownerKeyboard = false;
197
+ static wired = new WeakSet();
198
+ static close(returnFocus = false) {
199
+ const open = document.getElementById('wp-node-menu');
200
+ const owner = WpNodeMenu.owner;
201
+ const keyboard = WpNodeMenu.ownerKeyboard;
202
+ WpNodeMenu.owner = null;
203
+ if (open === null) return;
204
+ open.remove();
205
+ if (returnFocus && keyboard && owner?.isConnected) {
206
+ WpNodeMenu.keyboard = true;
207
+ owner.focus({ preventScroll: true });
208
+ }
209
+ }
210
+ static focusedName(svg) {
211
+ const active = document.activeElement;
212
+ const menu = document.getElementById('wp-node-menu');
213
+ const node = menu?.contains(active) && WpNodeMenu.ownerKeyboard
214
+ ? WpNodeMenu.owner : active;
215
+ return svg?.contains(node) && (node === WpNodeMenu.owner || node?.classList.contains('wp-keyboard-focus'))
216
+ ? node.querySelector('title')?.textContent.trim() : null;
217
+ }
218
+ static restoreFocus(svg, name) {
219
+ if (name == null) return;
220
+ const nodes = Array.from(svg.querySelectorAll('g.wp-node-clickable'));
221
+ const node = nodes.find(node => node.querySelector('title')?.textContent.trim() === name);
222
+ WpNodeMenu.keyboard = true;
223
+ (node || nodes[0])?.focus({ preventScroll: true });
224
+ }
225
+ `;
226
+ }
175
227
  keyboardScript() {
176
228
  return `static wireKeyboard(node, name, itemsFor) {
177
229
  node.addEventListener('keydown', function (ev) {
178
230
  if (ev.key !== 'Enter' && ev.key !== ' ') return;
179
231
  ev.preventDefault();
180
232
  ev.stopPropagation();
233
+ WpNodeMenu.keyboard = true;
181
234
  WpNodeMenu.open(node, name, itemsFor(name, node));
182
- document.querySelector('#wp-node-menu button')?.focus();
183
235
  });
184
236
  }`;
185
237
  }
@@ -227,10 +279,20 @@ class GraphNodeMenu {
227
279
  /** Outside click and Escape both dismiss — wired once, on the document. */
228
280
  dismissScript() {
229
281
  return `
282
+ document.addEventListener('pointerdown', function () {
283
+ WpNodeMenu.keyboard = false;
284
+ document.querySelectorAll('.wp-keyboard-focus').forEach(function (node) {
285
+ node.classList.remove('wp-keyboard-focus');
286
+ });
287
+ }, true);
230
288
  document.addEventListener('click', function () { WpNodeMenu.close(); });
231
289
  document.addEventListener('keydown', function (ev) {
232
- if (ev.key === 'Escape') WpNodeMenu.close();
233
- });`;
290
+ if (ev.altKey || ev.ctrlKey || ev.metaKey) return;
291
+ WpNodeMenu.keyboard = true;
292
+ const active = document.activeElement;
293
+ if (active?.classList.contains('wp-node-clickable')) active.classList.add('wp-keyboard-focus');
294
+ if (ev.key === 'Escape') WpNodeMenu.close(true);
295
+ }, true);`;
234
296
  }
235
297
  }
236
298
  exports.GraphNodeMenu = GraphNodeMenu;
@@ -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":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAa,aAAa;IACtB;;;;OAIG;IACH,MAAM;QACF,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UA8CL,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6CD,IAAI,CAAC,cAAc,EAAE;;;;;;;;;;;;;;;;;;;UAmBzB,CAAC;IACP,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;;;;YAIH,CAAC;IACT,CAAC;CACJ;AArND,sCAqNC","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` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown 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; }\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 menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n static close() {\n const open = document.getElementById('wp-node-menu');\n if (open !== null) open.remove();\n }\n static open(nodeEl, name, items) {\n WpNodeMenu.close();\n const menu = document.createElement('div');\n menu.id = 'wp-node-menu';\n menu.className = 'wp-node-menu';\n menu.addEventListener('click', function (ev) { ev.stopPropagation(); });\n const heading = document.createElement('div');\n heading.className = 'wp-node-menu-title';\n heading.textContent = name;\n menu.appendChild(heading);\n for (const item of items) menu.appendChild(WpNodeMenu.button(item));\n document.body.appendChild(menu);\n WpNodeMenu.place(menu, nodeEl);\n }\n static button(item) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close();\n item.onSelect();\n });\n return button;\n }\n static place(menu, nodeEl) {\n const box = nodeEl.getBoundingClientRect();\n const own = menu.getBoundingClientRect();\n const maxLeft = window.scrollX + document.documentElement.clientWidth - own.width - 8;\n let left = box.left + window.scrollX;\n if (left > maxLeft) left = Math.max(window.scrollX + 8, maxLeft);\n menu.style.left = left + 'px';\n menu.style.top = (box.bottom + window.scrollY + 4) + 'px';\n }\n ${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 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 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.open(node, name, itemsFor(name, node));\n document.querySelector('#wp-node-menu button')?.focus();\n });\n }`;\n }\n\n /**\n * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown 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('click', function () { WpNodeMenu.close(); });\n document.addEventListener('keydown', function (ev) {\n if (ev.key === 'Escape') WpNodeMenu.close();\n });`;\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;;;;;cAKD,IAAI,CAAC,WAAW,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA+BlB,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;AArRD,sCAqRC","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` dropdown backed by GraphHighlighter (chain\n * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;\n * - a design page and the runtime page have neither dropdown 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: #1976d2;\n stroke-width: 5;\n filter: drop-shadow(0 0 6px rgba(25, 118, 210, 0.85));\n }`;\n }\n\n /**\n * The dim/undim rules a lock (or a hover) toggles, scoped to whatever element holds the rendered\n * SVG on this page (`#graph` on the architecture page, `.graph` on a design page).\n *\n * We ONLY dim: the lit subgraph keeps its exact normal look. The un-dim rules repeat\n * `svg.wp-dim` so they out-specify the dim rule, which carries an extra type selector.\n */\n dimStyles(scope: string): string {\n return `\n ${scope} .node, ${scope} .edge { transition: opacity 0.12s ease; }\n ${scope} svg.wp-dim .node,\n ${scope} svg.wp-dim .edge { opacity: 0.15; }\n ${scope} svg.wp-dim .node.wp-focus,\n ${scope} svg.wp-dim .node.wp-neighbor,\n ${scope} svg.wp-dim .edge.wp-hl { opacity: 1; }`;\n }\n\n /** The whole browser side: the menu, the design-page lock, and the global dismiss handlers. */\n script(): string {\n return `${this.menuScript()}\\n${this.lockScript()}\\n${this.dismissScript()}`;\n }\n\n /**\n * `WpNodeMenuItem` + `WpNodeMenu`. Anchored at the node's own bounding box (bottom-left corner,\n * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole\n * menu. Opening a menu closes any other, so at most one is ever on screen.\n */\n private menuScript(): string {\n return `\n class WpNodeMenuItem {\n constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }\n }\n class WpNodeMenu {\n ${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) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'wp-node-menu-item';\n button.textContent = item.label;\n button.addEventListener('click', function (ev) {\n ev.preventDefault();\n ev.stopPropagation();\n WpNodeMenu.close(true);\n item.onSelect();\n });\n return button;\n }\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 dropdown 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"]}