@webpieces/nx-webpieces-rules 0.4.871 → 0.4.872

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +36 -0
  2. package/package.json +7 -7
  3. package/src/executors/generate/executor.d.ts +3 -1
  4. package/src/executors/generate/executor.js +9 -1
  5. package/src/executors/generate/executor.js.map +1 -1
  6. package/src/index.d.ts +1 -0
  7. package/src/index.js +5 -1
  8. package/src/index.js.map +1 -1
  9. package/src/lib/dot-syntax.d.ts +9 -0
  10. package/src/lib/dot-syntax.js +17 -0
  11. package/src/lib/dot-syntax.js.map +1 -1
  12. package/src/lib/graph-color-modes.d.ts +136 -0
  13. package/src/lib/graph-color-modes.js +299 -0
  14. package/src/lib/graph-color-modes.js.map +1 -0
  15. package/src/lib/graph-filter.client.d.ts +11 -1
  16. package/src/lib/graph-filter.client.js +25 -4
  17. package/src/lib/graph-filter.client.js.map +1 -1
  18. package/src/lib/graph-impact.d.ts +119 -0
  19. package/src/lib/graph-impact.js +295 -0
  20. package/src/lib/graph-impact.js.map +1 -0
  21. package/src/lib/graph-legend.d.ts +34 -0
  22. package/src/lib/graph-legend.js +116 -0
  23. package/src/lib/graph-legend.js.map +1 -0
  24. package/src/lib/graph-names.d.ts +2 -2
  25. package/src/lib/graph-names.js +2 -2
  26. package/src/lib/graph-names.js.map +1 -1
  27. package/src/lib/graph-navigation.d.ts +10 -0
  28. package/src/lib/graph-navigation.js +47 -12
  29. package/src/lib/graph-navigation.js.map +1 -1
  30. package/src/lib/graph-node-menu.d.ts +4 -3
  31. package/src/lib/graph-node-menu.js +137 -20
  32. package/src/lib/graph-node-menu.js.map +1 -1
  33. package/src/lib/graph-page-shell.d.ts +34 -0
  34. package/src/lib/graph-page-shell.js +121 -0
  35. package/src/lib/graph-page-shell.js.map +1 -0
  36. package/src/lib/graph-page-styles.d.ts +20 -0
  37. package/src/lib/graph-page-styles.js +144 -0
  38. package/src/lib/graph-page-styles.js.map +1 -0
  39. package/src/lib/graph-render-model.d.ts +14 -2
  40. package/src/lib/graph-render-model.js +19 -3
  41. package/src/lib/graph-render-model.js.map +1 -1
  42. package/src/lib/graph-responsibilities.js +1 -1
  43. package/src/lib/graph-responsibilities.js.map +1 -1
  44. package/src/lib/graph-visualizer.client.d.ts +65 -0
  45. package/src/lib/graph-visualizer.client.js +311 -7
  46. package/src/lib/graph-visualizer.client.js.map +1 -1
  47. package/src/lib/graph-visualizer.d.ts +19 -31
  48. package/src/lib/graph-visualizer.js +50 -286
  49. package/src/lib/graph-visualizer.js.map +1 -1
  50. package/src/lib/runtime-html-page.js +2 -2
  51. package/src/lib/runtime-html-page.js.map +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-legend.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-legend.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture graph's legend, generated per color mode FROM THE PROJECTS ACTUALLY DRAWN.\n *\n * The hand-written legend this replaces listed five frameworks when six existed and never mentioned\n * the `app` and `bundle` borders the renderer drew (#1155). Generating the rows from the same tables\n * the renderer colors with (graph-color-modes.ts) means a color in use always has a row, and a color\n * nothing uses never does.\n *\n * Swatches are inline SVG drawn with presentation attributes, never `style=`, so the page carries\n * no inline CSS.\n */\n\nimport { KNOWN_FRAMEWORKS, KnownFramework } from '@webpieces/rules-sdk';\nimport {\n BASE_ORDER,\n FRAMEWORK_STYLES,\n FrameworkStyle,\n GraphMode,\n IMPACT_AFFECTED,\n IMPACT_BUILD_INPUT,\n IMPACT_BUILD_INPUT_BORDER,\n IMPACT_TOUCHED,\n IMPACT_UNTOUCHED,\n LegendFacts,\n NO_RUNTIME_COLOR,\n ROLE_STYLES,\n RoleStyle,\n} from './graph-color-modes';\n\nconst SWATCH_W = 26;\nconst SWATCH_H = 16;\nconst FRAME = '#2b2f3a';\n\nexport class GraphLegend {\n /** One section per mode; the page shows the section of the current mode and hides the rest. */\n sections(facts: LegendFacts): string {\n return (\n this.section(GraphMode.RUNTIME, this.runtimeRows(facts), false) +\n this.section(GraphMode.ARCHITECTURE, this.architectureRows(facts), true) +\n this.section(GraphMode.IMPACT, this.impactRows(), true)\n );\n }\n\n /** The edge key: why a line exists. Static — every graph can carry every edge kind. */\n edgeKey(): string {\n return (\n '<div class=\"wp-legend-list\">' +\n this.edgeRow('#e9eaf0', '', 'uses', 'calls the API, or a plain library import') +\n this.edgeRow('#e9eaf0', '4 3', 'implements', 'serves the API (labeled with its contracts)') +\n this.edgeRow('#7aa2ff', '4 3', 'uses + implements', 'implements some contracts, uses others') +\n '</div>'\n );\n }\n\n private section(mode: GraphMode, rows: string, hidden: boolean): string {\n const hide = hidden ? ' hidden' : '';\n return `<div class=\"wp-legend-list\" data-wp-legend-mode=\"${mode}\"${hide}>${rows}</div>`;\n }\n\n /** Base runtimes first (stripe order), then specializations, in the shared vocabulary's order. */\n runtimeRows(facts: LegendFacts): string {\n const present = (framework: KnownFramework): boolean => facts.frameworks.includes(framework);\n const ordered: FrameworkStyle[] = [\n ...BASE_ORDER.map((base: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[base]),\n ...KNOWN_FRAMEWORKS.map((f: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[f]).filter(\n (style: FrameworkStyle): boolean => !style.isBase(),\n ),\n ];\n let rows = '';\n for (const style of ordered) {\n if (!present(style.framework)) continue;\n rows += this.row(style.framework, this.envSwatch(style), style.framework, style.description);\n }\n if (facts.multiRuntime)\n rows += this.row('multi', this.stripesSwatch(), 'several runtimes', 'one equal stripe per runtime');\n if (facts.noRuntime)\n rows += this.row('none', this.solid(NO_RUNTIME_COLOR, '', ''), 'no framework tag', 'runs nowhere declared');\n return rows;\n }\n\n architectureRows(facts: LegendFacts): string {\n let rows = '';\n for (const style of ROLE_STYLES) {\n if (!facts.roles.includes(style.role)) continue;\n rows += this.row(style.role, this.roleSwatch(style), style.role, style.description);\n }\n return rows;\n }\n\n /** Which box is which is only known from the branch's sidecar, so all four rows always show. */\n private impactRows(): string {\n return (\n this.row('touched', this.solid(IMPACT_TOUCHED, '', ''), 'touched', 'files changed on this branch') +\n this.row('affected', this.solid(IMPACT_AFFECTED, IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +\n this.row(\n 'build-input',\n this.solid(IMPACT_BUILD_INPUT, IMPACT_BUILD_INPUT_BORDER, '3 2'),\n 'build input',\n 'compiled or restored from cache, unchanged',\n ) +\n this.row('untouched', this.solid(IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +\n '<p class=\"wp-impact-note\" data-wp-impact-note></p>'\n );\n }\n\n private row(key: string, swatch: string, title: string, detail: string): string {\n return (\n `<div class=\"wp-legend-row\" data-wp-legend=\"${key}\">${swatch}` +\n `<span>${title}<small>${detail}</small></span></div>`\n );\n }\n\n private open(): string {\n return `<svg width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" viewBox=\"0 0 ${SWATCH_W} ${SWATCH_H}\" aria-hidden=\"true\">`;\n }\n\n private frame(): string {\n return `<rect x=\"0.5\" y=\"0.5\" width=\"${SWATCH_W - 1}\" height=\"${SWATCH_H - 1}\" fill=\"none\" stroke=\"${FRAME}\"/></svg>`;\n }\n\n /** A base runtime is solid; a specialization is its inset on the base color. */\n private envSwatch(style: FrameworkStyle): string {\n const base = `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[style.base].color}\"/>`;\n if (style.isBase()) return this.open() + base + this.frame();\n const inset = `<rect x=\"4\" y=\"4\" width=\"${SWATCH_W - 8}\" height=\"${SWATCH_H - 8}\" rx=\"2\" fill=\"${style.color}\"/>`;\n return this.open() + base + inset + this.frame();\n }\n\n private stripesSwatch(): string {\n const width = SWATCH_W / BASE_ORDER.length;\n const stripes = BASE_ORDER.map(\n (base: KnownFramework, index: number): string =>\n `<rect x=\"${(index * width).toFixed(2)}\" width=\"${width.toFixed(2)}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[base].color}\"/>`,\n ).join('');\n return this.open() + stripes + this.frame();\n }\n\n private roleSwatch(style: RoleStyle): string {\n return this.solid(style.fill, '', '');\n }\n\n /** A filled swatch, optionally with an inner ring (dashed when `dash` is set). */\n private solid(fill: string, outline: string, dash: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const ring =\n outline === ''\n ? ''\n : `<rect x=\"2\" y=\"2\" width=\"${SWATCH_W - 4}\" height=\"${SWATCH_H - 4}\" fill=\"none\" stroke=\"${outline}\" stroke-width=\"2\"${dashed}/>`;\n return this.open() + `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${fill}\"/>` + ring + this.frame();\n }\n\n private edgeRow(color: string, dash: string, title: string, detail: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const line = `<svg width=\"28\" height=\"10\" aria-hidden=\"true\"><line x1=\"0\" y1=\"5\" x2=\"28\" y2=\"5\" stroke=\"${color}\" stroke-width=\"1.6\"${dashed}/></svg>`;\n return this.row(title, line, title, detail);\n }\n}\n"]}
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The two names a project has in the visualization, kept apart on purpose:
5
5
  *
6
- * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown
6
+ * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-search
7
7
  * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.
8
8
  * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.
9
9
  *
@@ -38,7 +38,7 @@ export declare class GraphNames {
38
38
  * The DOT/DOM identity of a project: its project KEY, unchanged.
39
39
  *
40
40
  * This is deliberately a named method rather than an inlined `project` at ten call sites — the
41
- * node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and
41
+ * node id is one decision, and it has to be made in one place or the DOT, the lock search and
42
42
  * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.
43
43
  */
44
44
  getNodeId(projectKey: string): string;
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * The two names a project has in the visualization, kept apart on purpose:
6
6
  *
7
- * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown
7
+ * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-search
8
8
  * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.
9
9
  * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.
10
10
  *
@@ -49,7 +49,7 @@ class GraphNames {
49
49
  * The DOT/DOM identity of a project: its project KEY, unchanged.
50
50
  *
51
51
  * This is deliberately a named method rather than an inlined `project` at ten call sites — the
52
- * node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and
52
+ * node id is one decision, and it has to be made in one place or the DOT, the lock search and
53
53
  * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.
54
54
  */
55
55
  getNodeId(projectKey) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,sFAAsF;AACtF,MAAa,WAAW;IAEA;IACA;IAFpB,YACoB,UAAkB,EAClB,KAAa;QADb,eAAU,GAAV,UAAU,CAAQ;QAClB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kCAKC;AAED,iFAAiF;AACjF,MAAa,oBAAqB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AALD,oDAKC;AAED,MAAa,UAAU;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,UAAkB;QACxB,OAAO,UAAU,CAAC;IACtB,CAAC;IAED;;;;;;OAMG;IACH,YAAY,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAED;;;;;OAKG;IACH,mBAAmB,CAAC,MAAqB;QACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;QAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,QAAQ,KAAK,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;;gBACrD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAkB,CAAC;YACtD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACjC,MAAM,KAAK,GAAG,OAAO;iBAChB,GAAG,CAAC,CAAC,KAAkB,EAAU,EAAE,CAAC,IAAI,KAAK,CAAC,UAAU,OAAO,KAAK,CAAC,KAAK,GAAG,CAAC;iBAC9E,IAAI,EAAE;iBACN,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,UAAU,CAAC,IAAI,CAAC,WAAW,MAAM,wBAAwB,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACpC,MAAM,IAAI,oBAAoB,CAC1B,uFAAuF;cACrF,oEAAoE;cACpE,UAAU,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;cAC5B,qFAAqF;cACrF,4EAA4E,CAAC,CAAC;IACxF,CAAC;CACJ;AAvDD,gCAuDC","sourcesContent":["/**\n * Graph Names\n *\n * The two names a project has in the visualization, kept apart on purpose:\n *\n * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-dropdown\n * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.\n * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.\n *\n * They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may\n * legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that\n * serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short\n * name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so\n * L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible\n * anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the\n * union says they are equal) and was discarded, and the server's dependency on the contract lib drew\n * as a self-loop on the fused box.\n *\n * So identity is the project key and nothing else — collision is structurally impossible now rather\n * than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change\n * that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.\n *\n * Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it\n * without a circular dependency between them.\n */\n\n/** One project key and the level it sits on — what a collision report has to name. */\nexport class NodeIdOwner {\n constructor(\n public readonly projectKey: string,\n public readonly level: number,\n ) {}\n}\n\n/** Thrown when two distinct project keys would be drawn as the same DOT node. */\nexport class NodeIdCollisionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NodeIdCollisionError';\n }\n}\n\nexport class GraphNames {\n /**\n * The DOT/DOM identity of a project: its project KEY, unchanged.\n *\n * This is deliberately a named method rather than an inlined `project` at ten call sites — the\n * node id is one decision, and it has to be made in one place or the DOT, the lock dropdown and\n * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.\n */\n getNodeId(projectKey: string): string {\n return projectKey;\n }\n\n /**\n * The human-facing display name: scope stripped.\n * '@scope/name' → 'name'\n * 'name' → 'name'\n *\n * LABELS ONLY. Never use this as an identity — see this file's header for what that cost.\n */\n getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n }\n\n /**\n * Assert that every project draws as its own node.\n *\n * Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it\n * guards is a graph that renders happily and says something false.\n */\n assertUniqueNodeIds(owners: NodeIdOwner[]): void {\n const byNodeId = new Map<string, NodeIdOwner[]>();\n for (const owner of owners) {\n const nodeId = this.getNodeId(owner.projectKey);\n const existing = byNodeId.get(nodeId);\n if (existing === undefined) byNodeId.set(nodeId, [owner]);\n else existing.push(owner);\n }\n const collisions: string[] = [];\n for (const nodeId of [...byNodeId.keys()].sort()) {\n const sharers = byNodeId.get(nodeId) as NodeIdOwner[];\n if (sharers.length < 2) continue;\n const named = sharers\n .map((owner: NodeIdOwner): string => `\"${owner.projectKey}\" (L${owner.level})`)\n .sort()\n .join(' and ');\n collisions.push(` node \"${nodeId}\" would be drawn for ${named}`);\n }\n if (collisions.length === 0) return;\n throw new NodeIdCollisionError(\n 'Two distinct projects map to the same graph node, so the rendered architecture graph '\n + 'would fuse them (and fuse their dependency levels into one row):\\n'\n + collisions.sort().join('\\n')\n + '\\nFix: getNodeId() must stay a 1:1 function of the project key — it is the project '\n + 'key verbatim. Only getShortName() may drop the scope, and only for LABELS.');\n }\n}\n"]}
1
+ {"version":3,"file":"graph-names.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-names.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;;;AAEH,sFAAsF;AACtF,MAAa,WAAW;IAEA;IACA;IAFpB,YACoB,UAAkB,EAClB,KAAa;QADb,eAAU,GAAV,UAAU,CAAQ;QAClB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kCAKC;AAED,iFAAiF;AACjF,MAAa,oBAAqB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACvB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACvC,CAAC;CACJ;AALD,oDAKC;AAED,MAAa,UAAU;IACnB;;;;;;OAMG;IACH,SAAS,CAAC,UAAkB;QACxB,OAAO,UAAU,CAAC;IACtB,CAAC;IAED;;;;;;OAMG;IACH,YAAY,CAAC,IAAY;QACrB,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAG,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9D,CAAC;IAED;;;;;OAKG;IACH,mBAAmB,CAAC,MAAqB;QACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;QAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACzB,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,QAAQ,KAAK,SAAS;gBAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;;gBACrD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,MAAM,UAAU,GAAa,EAAE,CAAC;QAChC,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAkB,CAAC;YACtD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,SAAS;YACjC,MAAM,KAAK,GAAG,OAAO;iBAChB,GAAG,CAAC,CAAC,KAAkB,EAAU,EAAE,CAAC,IAAI,KAAK,CAAC,UAAU,OAAO,KAAK,CAAC,KAAK,GAAG,CAAC;iBAC9E,IAAI,EAAE;iBACN,IAAI,CAAC,OAAO,CAAC,CAAC;YACnB,UAAU,CAAC,IAAI,CAAC,WAAW,MAAM,wBAAwB,KAAK,EAAE,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACpC,MAAM,IAAI,oBAAoB,CAC1B,uFAAuF;cACrF,oEAAoE;cACpE,UAAU,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;cAC5B,qFAAqF;cACrF,4EAA4E,CAAC,CAAC;IACxF,CAAC;CACJ;AAvDD,gCAuDC","sourcesContent":["/**\n * Graph Names\n *\n * The two names a project has in the visualization, kept apart on purpose:\n *\n * - its NODE ID — the identity every DOT node, edge endpoint, `rank=same` member, lock-search\n * value and responsibilities `data-node` attribute is keyed on. It is the project KEY, verbatim.\n * - its SHORT NAME — the human-facing label, with the `@scope/` prefix stripped.\n *\n * They used to be the same string, and that was a silent, badly-wrong-picture bug. A workspace may\n * legitimately hold `@scope/public-api` (an L0 contract lib) AND `public-api` (the L6 server that\n * serves it) — two DISTINCT projects that strip to the same short name. Keying the DOT on the short\n * name fused them into one node, and graphviz UNIONS any two `rank=same` sets sharing a node, so\n * L0's rank and L6's rank became one rank: the entire L0 band rendered on the L6 row, the invisible\n * anchor chain that pins level order became unsatisfiable (it demands L0 strictly below L6 while the\n * union says they are equal) and was discarded, and the server's dependency on the contract lib drew\n * as a self-loop on the fused box.\n *\n * So identity is the project key and nothing else — collision is structurally impossible now rather\n * than merely unlikely — and {@link GraphNames.assertUniqueNodeIds} asserts that, so a future change\n * that reintroduces a lossy id fails generation loudly instead of drawing a wrong graph in silence.\n *\n * Kept in its own class so GraphVisualizer and ResponsibilitiesRenderer can both depend on it\n * without a circular dependency between them.\n */\n\n/** One project key and the level it sits on — what a collision report has to name. */\nexport class NodeIdOwner {\n constructor(\n public readonly projectKey: string,\n public readonly level: number,\n ) {}\n}\n\n/** Thrown when two distinct project keys would be drawn as the same DOT node. */\nexport class NodeIdCollisionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'NodeIdCollisionError';\n }\n}\n\nexport class GraphNames {\n /**\n * The DOT/DOM identity of a project: its project KEY, unchanged.\n *\n * This is deliberately a named method rather than an inlined `project` at ten call sites — the\n * node id is one decision, and it has to be made in one place or the DOT, the lock search and\n * the responsibilities cards drift apart and the hover/lock wiring silently stops matching.\n */\n getNodeId(projectKey: string): string {\n return projectKey;\n }\n\n /**\n * The human-facing display name: scope stripped.\n * '@scope/name' → 'name'\n * 'name' → 'name'\n *\n * LABELS ONLY. Never use this as an identity — see this file's header for what that cost.\n */\n getShortName(name: string): string {\n return name.includes('/') ? name.split('/').pop()! : name;\n }\n\n /**\n * Assert that every project draws as its own node.\n *\n * Fails LOUDLY, naming both colliding project keys and their levels, because the failure mode it\n * guards is a graph that renders happily and says something false.\n */\n assertUniqueNodeIds(owners: NodeIdOwner[]): void {\n const byNodeId = new Map<string, NodeIdOwner[]>();\n for (const owner of owners) {\n const nodeId = this.getNodeId(owner.projectKey);\n const existing = byNodeId.get(nodeId);\n if (existing === undefined) byNodeId.set(nodeId, [owner]);\n else existing.push(owner);\n }\n const collisions: string[] = [];\n for (const nodeId of [...byNodeId.keys()].sort()) {\n const sharers = byNodeId.get(nodeId) as NodeIdOwner[];\n if (sharers.length < 2) continue;\n const named = sharers\n .map((owner: NodeIdOwner): string => `\"${owner.projectKey}\" (L${owner.level})`)\n .sort()\n .join(' and ');\n collisions.push(` node \"${nodeId}\" would be drawn for ${named}`);\n }\n if (collisions.length === 0) return;\n throw new NodeIdCollisionError(\n 'Two distinct projects map to the same graph node, so the rendered architecture graph '\n + 'would fuse them (and fuse their dependency levels into one row):\\n'\n + collisions.sort().join('\\n')\n + '\\nFix: getNodeId() must stay a 1:1 function of the project key — it is the project '\n + 'key verbatim. Only getShortName() may drop the scope, and only for LABELS.');\n }\n}\n"]}
@@ -1,5 +1,15 @@
1
+ /**
2
+ * Where the zoom controls sit. The runtime page keeps its toolbar ABOVE the graph; the architecture
3
+ * page gives the graph the whole viewport and floats a compact `− 1:1 ⤢ +` cluster bottom-right.
4
+ */
5
+ export declare enum NavigationLayout {
6
+ TOOLBAR = "toolbar",
7
+ FLOATING = "floating"
8
+ }
1
9
  /** Shared local-file navigation. Reset restores readable scale; Fit is an explicit overview. */
2
10
  export declare class GraphNavigation {
11
+ private readonly layout;
12
+ constructor(layout: NavigationLayout);
3
13
  styles(): string;
4
14
  script(): string;
5
15
  }
@@ -1,14 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.GraphNavigation = void 0;
3
+ exports.GraphNavigation = exports.NavigationLayout = void 0;
4
+ /**
5
+ * Where the zoom controls sit. The runtime page keeps its toolbar ABOVE the graph; the architecture
6
+ * page gives the graph the whole viewport and floats a compact `− 1:1 ⤢ +` cluster bottom-right.
7
+ */
8
+ var NavigationLayout;
9
+ (function (NavigationLayout) {
10
+ NavigationLayout["TOOLBAR"] = "toolbar";
11
+ NavigationLayout["FLOATING"] = "floating";
12
+ })(NavigationLayout || (exports.NavigationLayout = NavigationLayout = {}));
4
13
  /** Shared local-file navigation. Reset restores readable scale; Fit is an explicit overview. */
5
14
  class GraphNavigation {
15
+ layout;
16
+ constructor(layout) {
17
+ this.layout = layout;
18
+ }
6
19
  styles() {
7
20
  return `
8
21
  #graph { overflow: auto; max-height: 75vh; text-align: left; touch-action: pan-x pan-y; }
9
22
  #graph svg { max-width: none; max-height: none; display: block; }
10
23
  .wp-navigation { display: flex; gap: 8px; align-items: center; margin: 10px 0; }
11
24
  .wp-navigation button { padding: 6px 12px; cursor: pointer; }
25
+ .wp-navigation.wp-zoom { position: absolute; right: 14px; bottom: 14px; margin: 0; gap: 6px; z-index: 20; }
26
+ .wp-navigation.wp-zoom button { min-width: 34px; height: 34px; padding: 0 8px; border-radius: 8px; border: 1px solid #d9dce5; background: #fff; color: #1b1e27; font-size: 15px; }
27
+ .wp-navigation.wp-zoom button:hover { border-color: #3559d6; }
28
+ .wp-navigation.wp-zoom output { min-width: 42px; padding: 2px 6px; border-radius: 6px; background: #ffffffd9; color: #5d6475; font-size: 12px; text-align: center; }
12
29
  .wp-graph-details { position: fixed; z-index: 1100; box-sizing: border-box; max-width: min(520px, calc(100vw - 24px)); max-height: min(480px, calc(100vh - 24px)); overflow: auto; background: white; padding: 12px; border: 1px solid #aaa; border-radius: 6px; box-shadow: 0 4px 16px #0003; font: 14px/1.5 Arial, sans-serif; overflow-wrap: anywhere; }
13
30
  .wp-graph-details li { margin-bottom: 8px; }
14
31
  .wp-api-detail { cursor: pointer; text-decoration: underline; }
@@ -16,7 +33,8 @@ class GraphNavigation {
16
33
  `;
17
34
  }
18
35
  script() {
19
- return `new (${GraphNavigationController.toString()})();`;
36
+ const floating = this.layout === NavigationLayout.FLOATING;
37
+ return `new (${GraphNavigationController.toString()})(${JSON.stringify(floating)});`;
20
38
  }
21
39
  }
22
40
  exports.GraphNavigation = GraphNavigation;
@@ -27,26 +45,43 @@ class GraphNavigationController {
27
45
  status = document.createElement('output');
28
46
  svg = null;
29
47
  scale = 1;
30
- constructor() {
48
+ constructor(floating) {
31
49
  if (this.host === null)
32
50
  return;
33
- this.toolbar.className = 'wp-navigation';
51
+ this.toolbar.className = floating ? 'wp-navigation wp-zoom' : 'wp-navigation';
34
52
  this.toolbar.setAttribute('aria-label', 'Graph navigation');
35
- this.host.before(this.toolbar);
36
- this.button('Zoom in', () => this.zoom(1.25));
37
- this.button('Zoom out', () => this.zoom(0.8));
38
- this.button('Readable reset', () => this.reset());
39
- this.button('Fit overview', () => this.fit());
40
- this.toolbar.append(this.status);
53
+ if (floating)
54
+ this.floatingButtons();
55
+ else
56
+ this.toolbarButtons();
41
57
  new MutationObserver(() => this.attach()).observe(this.host, { childList: true });
42
58
  this.attach();
43
59
  window.addEventListener('resize', () => this.changed());
44
60
  this.host.addEventListener('scroll', () => this.changed());
45
61
  }
46
- button(label, action) {
62
+ toolbarButtons() {
63
+ this.host?.before(this.toolbar);
64
+ this.button('Zoom in', 'Zoom in', () => this.zoom(1.25));
65
+ this.button('Zoom out', 'Zoom out', () => this.zoom(0.8));
66
+ this.button('Readable reset', 'Readable reset', () => this.reset());
67
+ this.button('Fit overview', 'Fit overview', () => this.fit());
68
+ this.toolbar.append(this.status);
69
+ }
70
+ /** Placed AFTER the graph in the DOM, so it never sits ahead of the boxes in the tab order. */
71
+ floatingButtons() {
72
+ this.host?.after(this.toolbar);
73
+ this.toolbar.append(this.status);
74
+ this.button('−', 'Zoom out', () => this.zoom(0.8));
75
+ this.button('1:1', 'Readable reset', () => this.reset());
76
+ this.button('⤢', 'Fit overview', () => this.fit());
77
+ this.button('+', 'Zoom in', () => this.zoom(1.25));
78
+ }
79
+ button(text, label, action) {
47
80
  const button = document.createElement('button');
48
81
  button.type = 'button';
49
- button.textContent = label;
82
+ button.textContent = text;
83
+ button.title = label;
84
+ button.setAttribute('aria-label', label);
50
85
  button.addEventListener('click', action);
51
86
  this.toolbar.append(button);
52
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-navigation.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/** Shared local-file navigation. Reset restores readable scale; Fit is an explicit overview. */\nexport class GraphNavigation {\n styles(): string {\n return `\n #graph { overflow: auto; max-height: 75vh; text-align: left; touch-action: pan-x pan-y; }\n #graph svg { max-width: none; max-height: none; display: block; }\n .wp-navigation { display: flex; gap: 8px; align-items: center; margin: 10px 0; }\n .wp-navigation button { padding: 6px 12px; cursor: pointer; }\n .wp-graph-details { position: fixed; z-index: 1100; box-sizing: border-box; max-width: min(520px, calc(100vw - 24px)); max-height: min(480px, calc(100vh - 24px)); overflow: auto; background: white; padding: 12px; border: 1px solid #aaa; border-radius: 6px; box-shadow: 0 4px 16px #0003; font: 14px/1.5 Arial, sans-serif; overflow-wrap: anywhere; }\n .wp-graph-details li { margin-bottom: 8px; }\n .wp-api-detail { cursor: pointer; text-decoration: underline; }\n .wp-api-detail:focus { outline: 2px solid #1565c0; }\n `;\n }\n\n script(): string {\n return `new (${GraphNavigationController.toString()})();`;\n }\n}\n\n/** Serialized browser controller; dependencies are native DOM APIs only. */\nclass GraphNavigationController {\n private readonly host = document.getElementById('graph');\n private readonly toolbar = document.createElement('nav');\n private readonly status = document.createElement('output');\n private svg: SVGSVGElement | null = null;\n private scale = 1;\n\n constructor() {\n if (this.host === null) return;\n this.toolbar.className = 'wp-navigation';\n this.toolbar.setAttribute('aria-label', 'Graph navigation');\n this.host.before(this.toolbar);\n this.button('Zoom in', () => this.zoom(1.25));\n this.button('Zoom out', () => this.zoom(0.8));\n this.button('Readable reset', () => this.reset());\n this.button('Fit overview', () => this.fit());\n this.toolbar.append(this.status);\n new MutationObserver(() => this.attach()).observe(this.host, { childList: true });\n this.attach();\n window.addEventListener('resize', () => this.changed());\n this.host.addEventListener('scroll', () => this.changed());\n }\n\n private button(label: string, action: () => void): void {\n const button = document.createElement('button');\n button.type = 'button';\n button.textContent = label;\n button.addEventListener('click', action);\n this.toolbar.append(button);\n }\n\n private changed(): void {\n document.dispatchEvent(new Event('wp-graph-transform'));\n }\n\n private resize(): void {\n if (this.svg === null) return;\n const box = this.svg.viewBox.baseVal;\n this.svg.style.width = `${box.width * this.scale}px`;\n this.svg.style.height = `${box.height * this.scale}px`;\n this.status.textContent = `${Math.round(this.scale * 100)}%`;\n this.changed();\n }\n\n private zoom(factor: number): void {\n if (this.host === null) return;\n const before = this.scale;\n this.scale = Math.max(0.1, Math.min(4, this.scale * factor));\n const centerX = this.host.scrollLeft + this.host.clientWidth / 2;\n const centerY = this.host.scrollTop + this.host.clientHeight / 2;\n this.resize();\n this.host.scrollLeft = (centerX * this.scale) / before - this.host.clientWidth / 2;\n this.host.scrollTop = (centerY * this.scale) / before - this.host.clientHeight / 2;\n }\n\n private reset(): void {\n this.scale = 1;\n this.resize();\n this.scrollOrigin();\n }\n\n private scrollOrigin(): void {\n if (this.host === null) return;\n this.host.scrollLeft = 0;\n this.host.scrollTop = 0;\n }\n\n private fit(): void {\n if (this.svg === null || this.host === null) return;\n this.scale = Math.min(\n 1,\n this.host.clientWidth / this.svg.viewBox.baseVal.width,\n this.host.clientHeight / this.svg.viewBox.baseVal.height,\n );\n this.resize();\n this.scrollOrigin();\n }\n\n private attach(): void {\n const next = this.host?.querySelector('svg');\n if (next === null || next === undefined || next === this.svg) return;\n this.svg = next;\n this.reset();\n }\n}\n"]}
1
+ {"version":3,"file":"graph-navigation.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-navigation.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where the zoom controls sit. The runtime page keeps its toolbar ABOVE the graph; the architecture\n * page gives the graph the whole viewport and floats a compact `− 1:1 ⤢ +` cluster bottom-right.\n */\nexport enum NavigationLayout {\n TOOLBAR = 'toolbar',\n FLOATING = 'floating',\n}\n\n/** Shared local-file navigation. Reset restores readable scale; Fit is an explicit overview. */\nexport class GraphNavigation {\n constructor(private readonly layout: NavigationLayout) {}\n\n styles(): string {\n return `\n #graph { overflow: auto; max-height: 75vh; text-align: left; touch-action: pan-x pan-y; }\n #graph svg { max-width: none; max-height: none; display: block; }\n .wp-navigation { display: flex; gap: 8px; align-items: center; margin: 10px 0; }\n .wp-navigation button { padding: 6px 12px; cursor: pointer; }\n .wp-navigation.wp-zoom { position: absolute; right: 14px; bottom: 14px; margin: 0; gap: 6px; z-index: 20; }\n .wp-navigation.wp-zoom button { min-width: 34px; height: 34px; padding: 0 8px; border-radius: 8px; border: 1px solid #d9dce5; background: #fff; color: #1b1e27; font-size: 15px; }\n .wp-navigation.wp-zoom button:hover { border-color: #3559d6; }\n .wp-navigation.wp-zoom output { min-width: 42px; padding: 2px 6px; border-radius: 6px; background: #ffffffd9; color: #5d6475; font-size: 12px; text-align: center; }\n .wp-graph-details { position: fixed; z-index: 1100; box-sizing: border-box; max-width: min(520px, calc(100vw - 24px)); max-height: min(480px, calc(100vh - 24px)); overflow: auto; background: white; padding: 12px; border: 1px solid #aaa; border-radius: 6px; box-shadow: 0 4px 16px #0003; font: 14px/1.5 Arial, sans-serif; overflow-wrap: anywhere; }\n .wp-graph-details li { margin-bottom: 8px; }\n .wp-api-detail { cursor: pointer; text-decoration: underline; }\n .wp-api-detail:focus { outline: 2px solid #1565c0; }\n `;\n }\n\n script(): string {\n const floating = this.layout === NavigationLayout.FLOATING;\n return `new (${GraphNavigationController.toString()})(${JSON.stringify(floating)});`;\n }\n}\n\n/** Serialized browser controller; dependencies are native DOM APIs only. */\nclass GraphNavigationController {\n private readonly host = document.getElementById('graph');\n private readonly toolbar = document.createElement('nav');\n private readonly status = document.createElement('output');\n private svg: SVGSVGElement | null = null;\n private scale = 1;\n\n constructor(floating: boolean) {\n if (this.host === null) return;\n this.toolbar.className = floating ? 'wp-navigation wp-zoom' : 'wp-navigation';\n this.toolbar.setAttribute('aria-label', 'Graph navigation');\n if (floating) this.floatingButtons();\n else this.toolbarButtons();\n new MutationObserver(() => this.attach()).observe(this.host, { childList: true });\n this.attach();\n window.addEventListener('resize', () => this.changed());\n this.host.addEventListener('scroll', () => this.changed());\n }\n\n private toolbarButtons(): void {\n this.host?.before(this.toolbar);\n this.button('Zoom in', 'Zoom in', () => this.zoom(1.25));\n this.button('Zoom out', 'Zoom out', () => this.zoom(0.8));\n this.button('Readable reset', 'Readable reset', () => this.reset());\n this.button('Fit overview', 'Fit overview', () => this.fit());\n this.toolbar.append(this.status);\n }\n\n /** Placed AFTER the graph in the DOM, so it never sits ahead of the boxes in the tab order. */\n private floatingButtons(): void {\n this.host?.after(this.toolbar);\n this.toolbar.append(this.status);\n this.button('−', 'Zoom out', () => this.zoom(0.8));\n this.button('1:1', 'Readable reset', () => this.reset());\n this.button('⤢', 'Fit overview', () => this.fit());\n this.button('+', 'Zoom in', () => this.zoom(1.25));\n }\n\n private button(text: string, label: string, action: () => void): void {\n const button = document.createElement('button');\n button.type = 'button';\n button.textContent = text;\n button.title = label;\n button.setAttribute('aria-label', label);\n button.addEventListener('click', action);\n this.toolbar.append(button);\n }\n\n private changed(): void {\n document.dispatchEvent(new Event('wp-graph-transform'));\n }\n\n private resize(): void {\n if (this.svg === null) return;\n const box = this.svg.viewBox.baseVal;\n this.svg.style.width = `${box.width * this.scale}px`;\n this.svg.style.height = `${box.height * this.scale}px`;\n this.status.textContent = `${Math.round(this.scale * 100)}%`;\n this.changed();\n }\n\n private zoom(factor: number): void {\n if (this.host === null) return;\n const before = this.scale;\n this.scale = Math.max(0.1, Math.min(4, this.scale * factor));\n const centerX = this.host.scrollLeft + this.host.clientWidth / 2;\n const centerY = this.host.scrollTop + this.host.clientHeight / 2;\n this.resize();\n this.host.scrollLeft = (centerX * this.scale) / before - this.host.clientWidth / 2;\n this.host.scrollTop = (centerY * this.scale) / before - this.host.clientHeight / 2;\n }\n\n private reset(): void {\n this.scale = 1;\n this.resize();\n this.scrollOrigin();\n }\n\n private scrollOrigin(): void {\n if (this.host === null) return;\n this.host.scrollLeft = 0;\n this.host.scrollTop = 0;\n }\n\n private fit(): void {\n if (this.svg === null || this.host === null) return;\n this.scale = Math.min(\n 1,\n this.host.clientWidth / this.svg.viewBox.baseVal.width,\n this.host.clientHeight / this.svg.viewBox.baseVal.height,\n );\n this.resize();\n this.scrollOrigin();\n }\n\n private attach(): void {\n const next = this.host?.querySelector('svg');\n if (next === null || next === undefined || next === this.svg) return;\n this.svg = next;\n this.reset();\n }\n}\n"]}
@@ -16,9 +16,9 @@
16
16
  * ambient globals — see viz-client-globals.d.ts.
17
17
  *
18
18
  * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:
19
- * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
19
+ * - the architecture page already has a `#wp-lock` search field backed by GraphHighlighter (chain
20
20
  * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
21
- * - a design page and the runtime page have neither dropdown nor responsibilities, so they use
21
+ * - a design page and the runtime page have neither lock field nor responsibilities, so they use
22
22
  * `WpNodeLock` below — dim every other box in that graph, light the locked one.
23
23
  * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
24
24
  */
@@ -44,13 +44,14 @@ export declare class GraphNodeMenu {
44
44
  * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole
45
45
  * menu. Opening a menu closes any other, so at most one is ever on screen.
46
46
  */
47
+ private itemScript;
47
48
  private menuScript;
48
49
  private positionScript;
49
50
  /** Explicit input modality avoids Chromium's pointer-focus :focus-visible heuristics. */
50
51
  private focusScript;
51
52
  private keyboardScript;
52
53
  /**
53
- * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
54
+ * The lock the DESIGN pages and the RUNTIME page use: neither has a lock field or a responsibilities
54
55
  * list, so locking a box dims every other box in that graph and lights the locked one.
55
56
  */
56
57
  private lockScript;
@@ -19,9 +19,9 @@ exports.GraphNodeMenu = void 0;
19
19
  * ambient globals — see viz-client-globals.d.ts.
20
20
  *
21
21
  * What the menu does NOT own is the LOCK BEHAVIOUR, because the pages genuinely differ:
22
- * - the architecture page already has a `#wp-lock` dropdown backed by GraphHighlighter (chain
22
+ * - the architecture page already has a `#wp-lock` search field backed by GraphHighlighter (chain
23
23
  * highlight + responsibilities filter), and its menu item drives that, so the two stay in sync;
24
- * - a design page and the runtime page have neither dropdown nor responsibilities, so they use
24
+ * - a design page and the runtime page have neither lock field nor responsibilities, so they use
25
25
  * `WpNodeLock` below — dim every other box in that graph, light the locked one.
26
26
  * All three spell the dim with the SAME class names and the SAME CSS from `dimStyles()`.
27
27
  */
@@ -65,6 +65,15 @@ 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; }
68
77
  g.wp-node-clickable { cursor: pointer; outline: none; }
69
78
  /* DOM keyboard focus is independent of graph highlight/Lock and stays dim with its node. */
70
79
  g.wp-node-clickable.wp-keyboard-focus {
@@ -110,12 +119,24 @@ class GraphNodeMenu {
110
119
  * 4px below it) and clamped so a box at the right edge of a very wide graph still shows its whole
111
120
  * menu. Opening a menu closes any other, so at most one is ever on screen.
112
121
  */
113
- menuScript() {
122
+ itemScript() {
114
123
  return `
115
124
  class WpNodeMenuItem {
116
- constructor(label, onSelect) { this.label = label; this.onSelect = onSelect; }
117
- }
125
+ constructor(label, onSelect, children = []) {
126
+ this.label = label;
127
+ this.onSelect = onSelect;
128
+ this.children = children;
129
+ this.checked = null;
130
+ this.disabledReason = '';
131
+ }
132
+ }`;
133
+ }
134
+ menuScript() {
135
+ return `${this.itemScript()}
136
+ ${WpMenuButton.toString()}
137
+ ${WpSubmenu.toString()}
118
138
  class WpNodeMenu {
139
+ static submenu(item) { return new WpSubmenu().build(item, WpNodeMenu.button); }
119
140
  ${this.focusScript()}
120
141
  static open(nodeEl, name, items) {
121
142
  WpNodeMenu.close();
@@ -129,24 +150,15 @@ class GraphNodeMenu {
129
150
  heading.className = 'wp-node-menu-title';
130
151
  heading.textContent = name;
131
152
  menu.appendChild(heading);
132
- for (const item of items) menu.appendChild(WpNodeMenu.button(item));
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
+ }
133
157
  document.body.appendChild(menu);
134
158
  WpNodeMenu.place(menu, nodeEl);
135
159
  menu.querySelector('button')?.focus({ preventScroll: true });
136
160
  }
137
- static button(item) {
138
- const button = document.createElement('button');
139
- button.type = 'button';
140
- button.className = 'wp-node-menu-item';
141
- button.textContent = item.label;
142
- button.addEventListener('click', function (ev) {
143
- ev.preventDefault();
144
- ev.stopPropagation();
145
- WpNodeMenu.close(true);
146
- item.onSelect();
147
- });
148
- return button;
149
- }
161
+ static button(item) { return new WpMenuButton().build(item, WpNodeMenu.close.bind(WpNodeMenu)); }
150
162
  ${this.positionScript()}
151
163
  ${this.keyboardScript()}
152
164
  static wire(svg, itemsFor) {
@@ -236,7 +248,7 @@ class GraphNodeMenu {
236
248
  }`;
237
249
  }
238
250
  /**
239
- * The lock the DESIGN pages and the RUNTIME page use: neither has a dropdown or a responsibilities
251
+ * The lock the DESIGN pages and the RUNTIME page use: neither has a lock field or a responsibilities
240
252
  * list, so locking a box dims every other box in that graph and lights the locked one.
241
253
  */
242
254
  lockScript() {
@@ -296,4 +308,109 @@ class GraphNodeMenu {
296
308
  }
297
309
  }
298
310
  exports.GraphNodeMenu = GraphNodeMenu;
311
+ /**
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.
315
+ *
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.
318
+ */
319
+ class WpMenuButton {
320
+ build(item, close) {
321
+ const button = document.createElement('button');
322
+ button.type = 'button';
323
+ button.className = 'wp-node-menu-item';
324
+ 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
+ button.addEventListener('click', (ev) => {
334
+ ev.preventDefault();
335
+ ev.stopPropagation();
336
+ close(true);
337
+ item.onSelect();
338
+ });
339
+ return button;
340
+ }
341
+ }
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
+ }
299
416
  //# 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":";;;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"]}
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"]}