@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,34 @@
1
+ /**
2
+ * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,
3
+ * and the graph filling the rest of the viewport.
4
+ *
5
+ * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all
6
+ * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a "?"
7
+ * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right
8
+ * (graph-navigation.ts) and the responsibilities open in a right-hand panel.
9
+ *
10
+ * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element
11
+ * carries a `style=` attribute — every look is a class.
12
+ */
13
+ /** Everything the shell frames, already rendered by its owner. */
14
+ export declare class ShellParts {
15
+ readonly title: string;
16
+ readonly lockControl: string;
17
+ readonly legend: string;
18
+ readonly edgeKey: string;
19
+ readonly filterStatus: string;
20
+ readonly snapshot: string;
21
+ readonly responsibilities: string;
22
+ constructor(title: string, lockControl: string, legend: string, edgeKey: string, filterStatus: string, snapshot: string, responsibilities: string);
23
+ }
24
+ export declare class GraphPageShell {
25
+ body(parts: ShellParts): string;
26
+ private drawer;
27
+ /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */
28
+ private modeItem;
29
+ private topbar;
30
+ private overlays;
31
+ /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */
32
+ private help;
33
+ private brandIcon;
34
+ }
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,
4
+ * and the graph filling the rest of the viewport.
5
+ *
6
+ * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all
7
+ * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a "?"
8
+ * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right
9
+ * (graph-navigation.ts) and the responsibilities open in a right-hand panel.
10
+ *
11
+ * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element
12
+ * carries a `style=` attribute — every look is a class.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.GraphPageShell = exports.ShellParts = void 0;
16
+ const graph_color_modes_1 = require("./graph-color-modes");
17
+ /** Everything the shell frames, already rendered by its owner. */
18
+ class ShellParts {
19
+ title;
20
+ lockControl;
21
+ legend;
22
+ edgeKey;
23
+ filterStatus;
24
+ snapshot;
25
+ responsibilities;
26
+ constructor(title, lockControl, legend, edgeKey, filterStatus, snapshot, responsibilities) {
27
+ this.title = title;
28
+ this.lockControl = lockControl;
29
+ this.legend = legend;
30
+ this.edgeKey = edgeKey;
31
+ this.filterStatus = filterStatus;
32
+ this.snapshot = snapshot;
33
+ this.responsibilities = responsibilities;
34
+ }
35
+ }
36
+ exports.ShellParts = ShellParts;
37
+ class GraphPageShell {
38
+ body(parts) {
39
+ return `<div class="wp-shell" id="wp-shell">
40
+ ${this.drawer(parts)}
41
+ <main class="wp-main">
42
+ ${this.topbar(parts)}
43
+ <div id="graph"></div>
44
+ ${this.overlays(parts)}
45
+ </main>
46
+ </div>`;
47
+ }
48
+ drawer(parts) {
49
+ return `<aside class="wp-side" id="wp-side" aria-label="Graph controls">
50
+ <div class="wp-brand">${this.brandIcon()}<span>${parts.title}</span></div>
51
+ <div class="wp-group" role="group" aria-label="Color mode">
52
+ <div class="wp-label">Color by</div>
53
+ <div class="wp-modes">${graph_color_modes_1.GRAPH_MODES.map((info, index) => this.modeItem(info, index)).join('')}</div>
54
+ <p class="wp-mode-reason" id="wp-impact-reason" hidden></p>
55
+ </div>
56
+ <div class="wp-group">
57
+ <label class="wp-label" for="wp-lock">Focus</label>
58
+ ${parts.lockControl}
59
+ <button type="button" class="wp-toggle" id="wp-filter-toggle" aria-pressed="false">Hide unconnected <span class="wp-sw" aria-hidden="true"></span></button>
60
+ </div>
61
+ <div class="wp-group">
62
+ <div class="wp-label wp-label-row">Legend <button type="button" class="wp-icon wp-icon-mini wp-icon-dark" id="wp-legend-pop" title="Pop the legend out over the graph" aria-label="Pop the legend out over the graph">⇱</button></div>
63
+ ${parts.legend}
64
+ </div>
65
+ <div class="wp-group">
66
+ <div class="wp-label">Edges</div>
67
+ ${parts.edgeKey}
68
+ </div>
69
+ <div class="wp-foot">
70
+ <button type="button" class="wp-link" id="wp-resp-open" aria-expanded="false">Responsibilities</button>
71
+ <button type="button" class="wp-link" id="wp-snapshot-open" aria-expanded="false">Saved snapshot</button>
72
+ </div>
73
+ </aside>`;
74
+ }
75
+ /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */
76
+ modeItem(info, index) {
77
+ const pressed = info.mode === graph_color_modes_1.GraphMode.RUNTIME ? 'true' : 'false';
78
+ return (`<button type="button" class="wp-mode" data-wp-mode="${info.mode}" aria-pressed="${pressed}" title="Key ${index + 1}">` +
79
+ `<span class="wp-mode-name">${info.name}</span><span class="wp-mode-sub">${info.subtitle}</span></button>`);
80
+ }
81
+ topbar(parts) {
82
+ return `<div class="wp-topbar">
83
+ <button type="button" class="wp-icon" id="wp-collapse" aria-label="Toggle the controls drawer" aria-controls="wp-side" aria-expanded="true">☰</button>
84
+ <div class="wp-crumb" id="wp-crumb">Mode: <b>Runtime</b></div>
85
+ ${parts.filterStatus}
86
+ <button type="button" class="wp-icon" id="wp-help-btn" aria-label="How to read this graph" aria-controls="wp-help" aria-expanded="false">?</button>
87
+ </div>`;
88
+ }
89
+ overlays(parts) {
90
+ return `<div class="wp-pop wp-legend-popout" id="wp-legend-popout" hidden>
91
+ <header><span id="wp-legend-pop-title">Legend · Runtime</span><button type="button" class="wp-icon wp-icon-mini" id="wp-legend-close" aria-label="Close the legend">✕</button></header>
92
+ ${parts.legend}
93
+ </div>
94
+ ${this.help()}
95
+ <div class="wp-pop wp-snapshot" id="wp-snapshot" hidden>${parts.snapshot}</div>
96
+ <aside class="wp-panel" id="wp-resp-panel" aria-label="Responsibilities" hidden>
97
+ <header><span>Responsibilities</span><button type="button" class="wp-icon wp-icon-mini" id="wp-resp-close" aria-label="Close responsibilities">✕</button></header>
98
+ ${parts.responsibilities}
99
+ </aside>`;
100
+ }
101
+ /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */
102
+ help() {
103
+ return `<div class="wp-pop wp-help" id="wp-help" role="dialog" aria-label="How to read this graph" hidden>
104
+ <header><span>Reading the graph</span><button type="button" class="wp-icon wp-icon-mini" id="wp-help-close" aria-label="Close help">✕</button></header>
105
+ <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>
106
+ <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row) and <strong>Mode ▸</strong>.</p>
107
+ <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>
108
+ <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>
109
+ <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
110
+ </div>`;
111
+ }
112
+ brandIcon() {
113
+ return ('<svg width="22" height="22" viewBox="0 0 22 22" aria-hidden="true">' +
114
+ '<rect x="1" y="1" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
115
+ '<rect x="13" y="1" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
116
+ '<rect x="7" y="15" width="8" height="6" rx="1.5" fill="none" stroke="#e9eaf0" stroke-width="1.6"/>' +
117
+ '<path d="M5 7v4h12V7M11 11v4" fill="none" stroke="#e9eaf0" stroke-width="1.6"/></svg>');
118
+ }
119
+ }
120
+ exports.GraphPageShell = GraphPageShell;
121
+ //# sourceMappingURL=graph-page-shell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAEH,2DAAuE;AAEvE,kEAAkE;AAClE,MAAa,UAAU;IAEC;IACA;IACA;IACA;IACA;IACA;IACA;IAPpB,YACoB,KAAa,EACb,WAAmB,EACnB,MAAc,EACd,OAAe,EACf,YAAoB,EACpB,QAAgB,EAChB,gBAAwB;QANxB,UAAK,GAAL,KAAK,CAAQ;QACb,gBAAW,GAAX,WAAW,CAAQ;QACnB,WAAM,GAAN,MAAM,CAAQ;QACd,YAAO,GAAP,OAAO,CAAQ;QACf,iBAAY,GAAZ,YAAY,CAAQ;QACpB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,qBAAgB,GAAhB,gBAAgB,CAAQ;IACzC,CAAC;CACP;AAVD,gCAUC;AAED,MAAa,cAAc;IACvB,IAAI,CAAC,KAAiB;QAClB,OAAO;MACT,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;;UAEd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;;UAElB,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;;OAEvB,CAAC;IACJ,CAAC;IAEO,MAAM,CAAC,KAAiB;QAC5B,OAAO;gCACiB,IAAI,CAAC,SAAS,EAAE,SAAS,KAAK,CAAC,KAAK;;;oCAGhC,+BAAW,CAAC,GAAG,CAAC,CAAC,IAAc,EAAE,KAAa,EAAU,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;;;;;cAKrH,KAAK,CAAC,WAAW;;;;;cAKjB,KAAK,CAAC,MAAM;;;;cAIZ,KAAK,CAAC,OAAO;;;;;;aAMd,CAAC;IACV,CAAC;IAED,gGAAgG;IACxF,QAAQ,CAAC,IAAc,EAAE,KAAa;QAC1C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,KAAK,6BAAS,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QACnE,OAAO,CACH,uDAAuD,IAAI,CAAC,IAAI,mBAAmB,OAAO,gBAAgB,KAAK,GAAG,CAAC,IAAI;YACvH,8BAA8B,IAAI,CAAC,IAAI,oCAAoC,IAAI,CAAC,QAAQ,kBAAkB,CAC7G,CAAC;IACN,CAAC;IAEO,MAAM,CAAC,KAAiB;QAC5B,OAAO;;;cAGD,KAAK,CAAC,YAAY;;eAEjB,CAAC;IACZ,CAAC;IAEO,QAAQ,CAAC,KAAiB;QAC9B,OAAO;;cAED,KAAK,CAAC,MAAM;;UAEhB,IAAI,CAAC,IAAI,EAAE;kEAC6C,KAAK,CAAC,QAAQ;;;cAGlE,KAAK,CAAC,gBAAgB;iBACnB,CAAC;IACd,CAAC;IAED,6FAA6F;IACrF,IAAI;QACR,OAAO;;;;;;;eAOA,CAAC;IACZ,CAAC;IAEO,SAAS;QACb,OAAO,CACH,qEAAqE;YACrE,mGAAmG;YACnG,oGAAoG;YACpG,oGAAoG;YACpG,uFAAuF,CAC1F,CAAC;IACN,CAAC;CACJ;AA5FD,wCA4FC","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { GRAPH_MODES, GraphMode, ModeInfo } from './graph-color-modes';\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n <div class=\"wp-modes\">${GRAPH_MODES.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /** A stacked two-line item: the mode's name, then its grey subtitle. Keys 1/2/3 select them. */\n private modeItem(info: ModeInfo, index: number): string {\n const pressed = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" data-wp-mode=\"${info.mode}\" aria-pressed=\"${pressed}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n private topbar(parts: ShellParts): string {\n return `<div class=\"wp-topbar\">\n <button type=\"button\" class=\"wp-icon\" id=\"wp-collapse\" aria-label=\"Toggle the controls drawer\" aria-controls=\"wp-side\" aria-expanded=\"true\">☰</button>\n <div class=\"wp-crumb\" id=\"wp-crumb\">Mode: <b>Runtime</b></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help()}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(): string {\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row) and <strong>Mode ▸</strong>.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The architecture page's stylesheet: the dark drawer, the graph filling the viewport, the floating
3
+ * popovers and the responsibilities panel. Emitted AFTER the shared menu/filter/navigation styles so
4
+ * its `#graph` rules (fill the viewport, no 75vh cap) win.
5
+ */
6
+ export declare class GraphPageStyles {
7
+ css(): string;
8
+ private base;
9
+ private drawer;
10
+ private controls;
11
+ private main;
12
+ private overlays;
13
+ /**
14
+ * The graph fills what the topbar leaves. A Runtime box with a specialization is an HTML-like
15
+ * table, so its cells are extra polygons inside the node: the hover glow and the lock outline are
16
+ * kept on the box's own outline (the FIRST polygon) instead of stroking every cell.
17
+ */
18
+ private graph;
19
+ private cards;
20
+ }
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GraphPageStyles = void 0;
4
+ /**
5
+ * The architecture page's stylesheet: the dark drawer, the graph filling the viewport, the floating
6
+ * popovers and the responsibilities panel. Emitted AFTER the shared menu/filter/navigation styles so
7
+ * its `#graph` rules (fill the viewport, no 75vh cap) win.
8
+ */
9
+ class GraphPageStyles {
10
+ css() {
11
+ return this.base() + this.drawer() + this.controls() + this.main() + this.overlays() + this.graph() + this.cards();
12
+ }
13
+ base() {
14
+ return `
15
+ :root {
16
+ --wp-side-bg: #0c0d12; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;
17
+ --wp-side-hover: #1f2129; --wp-side-line: #2a2c36; --wp-accent: #3559d6; --wp-accent-dark: #8aa4ff;
18
+ --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #fbfbfd;
19
+ }
20
+ html, body { height: 100%; }
21
+ /* A class that sets display (the legend lists are grids) must not un-hide a [hidden] element. */
22
+ [hidden] { display: none !important; }
23
+ body { margin: 0; padding: 0; overflow: hidden; background: var(--wp-canvas); color: var(--wp-fg);
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; font-size: 14px; }
25
+ kbd { font-family: ui-monospace, Menlo, monospace; font-size: 12px; border: 1px solid var(--wp-line);
26
+ border-radius: 4px; padding: 0 5px; background: #f3f4f7; }
27
+ .wp-shell { display: grid; grid-template-columns: 272px minmax(0, 1fr); height: 100vh; }
28
+ .wp-shell.wp-collapsed { grid-template-columns: 0 minmax(0, 1fr); }
29
+ .wp-shell.wp-collapsed .wp-side { visibility: hidden; }
30
+ @media (max-width: 760px) {
31
+ .wp-shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, auto) minmax(0, 1fr); }
32
+ .wp-side { max-height: 42vh; }
33
+ .wp-shell.wp-collapsed { grid-template-columns: minmax(0, 1fr); }
34
+ .wp-shell.wp-collapsed .wp-side { display: none; }
35
+ .wp-main { height: auto; min-height: 0; }
36
+ }
37
+ @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }`;
38
+ }
39
+ drawer() {
40
+ return `
41
+ .wp-side { background: var(--wp-side-bg); color: var(--wp-side-fg); overflow-y: auto; overflow-x: hidden;
42
+ display: flex; flex-direction: column; padding: 12px 0 10px; min-width: 0; font-size: 13px; }
43
+ .wp-brand { display: flex; align-items: center; gap: 10px; padding: 0 16px 10px; font-weight: 600; font-size: 14px; }
44
+ .wp-brand svg { flex: none; }
45
+ .wp-group { padding: 8px 14px; border-top: 1px solid var(--wp-side-line); display: grid; gap: 6px; }
46
+ .wp-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--wp-side-muted);
47
+ font-weight: 600; padding-left: 4px; }
48
+ .wp-label-row { display: flex; justify-content: space-between; align-items: center; }
49
+ .wp-modes { display: grid; gap: 1px; }
50
+ .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 5px 10px; border-radius: 8px; }
51
+ .wp-mode:hover { background: var(--wp-side-hover); }
52
+ .wp-mode[aria-pressed="true"] { background: #262833; box-shadow: inset 3px 0 0 var(--wp-accent-dark); }
53
+ .wp-mode:disabled { cursor: not-allowed; opacity: 0.45; }
54
+ .wp-mode-name { display: block; font-size: 14px; font-weight: 600; line-height: 1.3; }
55
+ .wp-mode-sub { display: block; font-size: 12px; color: var(--wp-side-muted); line-height: 1.3; }
56
+ .wp-mode-reason { margin: 2px 4px 0; font-size: 11.5px; line-height: 1.35; color: var(--wp-side-muted); }
57
+ .wp-legend-list { display: grid; gap: 5px; padding-left: 4px; }
58
+ .wp-legend-row { display: flex; align-items: center; gap: 10px; line-height: 1.25; }
59
+ .wp-legend-row svg { flex: none; }
60
+ .wp-legend-row small { display: block; color: var(--wp-side-muted); font-size: 11.5px; }
61
+ .wp-impact-note { margin: 2px 0 0; font-size: 11.5px; color: var(--wp-side-muted); }
62
+ .wp-impact-note:empty { display: none; }
63
+ .wp-foot { margin-top: auto; padding: 10px 18px 0; border-top: 1px solid var(--wp-side-line); display: flex; gap: 14px; flex-wrap: wrap; }`;
64
+ }
65
+ controls() {
66
+ return `
67
+ .wp-side input[type="search"] { width: 100%; box-sizing: border-box; background: #17181f; color: var(--wp-side-fg);
68
+ border: 1px solid var(--wp-side-line); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }
69
+ .wp-toggle { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;
70
+ align-items: center; padding: 6px 10px; border-radius: 8px; font-size: 13px; }
71
+ .wp-toggle:hover { background: var(--wp-side-hover); }
72
+ .wp-toggle:disabled { cursor: not-allowed; opacity: 0.45; }
73
+ .wp-sw { width: 30px; height: 18px; border-radius: 9px; background: #3a3d4a; position: relative; transition: background 0.15s; }
74
+ .wp-sw::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;
75
+ background: #fff; transition: transform 0.15s; }
76
+ .wp-toggle[aria-pressed="true"] .wp-sw { background: var(--wp-accent); }
77
+ .wp-toggle[aria-pressed="true"] .wp-sw::after { transform: translateX(12px); }
78
+ .wp-link { all: unset; cursor: pointer; color: var(--wp-side-muted); font-size: 12px; text-decoration: underline; }
79
+ .wp-link:hover { color: var(--wp-side-fg); }
80
+ .wp-icon { all: unset; box-sizing: border-box; cursor: pointer; display: inline-grid; place-items: center; flex: none;
81
+ width: 34px; height: 34px; border-radius: 8px; background: var(--wp-surface); border: 1px solid var(--wp-line);
82
+ color: var(--wp-fg); font-size: 15px; }
83
+ .wp-icon:hover { border-color: var(--wp-accent); }
84
+ .wp-icon-mini { width: 24px; height: 24px; font-size: 12px; }
85
+ .wp-icon-dark { background: transparent; border-color: var(--wp-side-line); color: var(--wp-side-fg); }
86
+ .wp-mode:focus-visible, .wp-toggle:focus-visible, .wp-side input:focus-visible, .wp-icon:focus-visible,
87
+ .wp-link:focus-visible { outline: 2px solid var(--wp-accent-dark); outline-offset: 2px; }`;
88
+ }
89
+ main() {
90
+ return `
91
+ .wp-main { position: relative; min-width: 0; height: 100vh; display: flex; flex-direction: column; }
92
+ .wp-topbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 12px;
93
+ border-bottom: 1px solid var(--wp-line); background: var(--wp-surface); }
94
+ .wp-crumb { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--wp-muted); overflow: hidden;
95
+ text-overflow: ellipsis; white-space: nowrap; }
96
+ .wp-crumb b { color: var(--wp-fg); }
97
+ .wp-topbar #wp-filter-status, .wp-topbar #wp-graph-error { margin: 0; font-size: 13px; }`;
98
+ }
99
+ overlays() {
100
+ return `
101
+ .wp-pop { position: absolute; z-index: 30; background: var(--wp-surface); border: 1px solid var(--wp-line);
102
+ border-radius: 10px; box-shadow: 0 8px 24px #0002; padding: 12px 14px; font-size: 13px; box-sizing: border-box; }
103
+ .wp-pop header { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }
104
+ .wp-legend-popout { left: 12px; bottom: 12px; width: 270px; max-height: 60vh; overflow: auto; }
105
+ .wp-legend-popout .wp-legend-row small, .wp-legend-popout .wp-impact-note { color: var(--wp-muted); }
106
+ .wp-help { right: 12px; top: 56px; width: min(380px, calc(100% - 24px)); line-height: 1.5; }
107
+ .wp-help p { margin: 0 0 8px; }
108
+ .wp-snapshot { left: 12px; bottom: 12px; width: min(440px, calc(100% - 24px)); }
109
+ .wp-snapshot .hint { margin: 0; }
110
+ .wp-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 40; width: min(540px, 100%); box-sizing: border-box;
111
+ overflow-y: auto; padding: 0 16px 16px; background: #f5f6f9; border-left: 1px solid var(--wp-line); box-shadow: -8px 0 24px #0002; }
112
+ .wp-panel > header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center;
113
+ padding: 12px 0; background: #f5f6f9; font-weight: 600; font-size: 15px; }`;
114
+ }
115
+ /**
116
+ * The graph fills what the topbar leaves. A Runtime box with a specialization is an HTML-like
117
+ * table, so its cells are extra polygons inside the node: the hover glow and the lock outline are
118
+ * kept on the box's own outline (the FIRST polygon) instead of stroking every cell.
119
+ */
120
+ graph() {
121
+ return `
122
+ #graph { flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; padding: 16px 16px 64px;
123
+ box-sizing: border-box; background: var(--wp-canvas); }
124
+ #graph g.wp-node-clickable:hover polygon:not(:first-of-type) { stroke: transparent; stroke-width: 0; filter: none; }
125
+ #graph g.node.wp-locked > polygon:first-of-type,
126
+ #graph g.node.wp-locked > ellipse,
127
+ #graph g.node.wp-locked > path { stroke: #b26a00; stroke-width: 3; }`;
128
+ }
129
+ cards() {
130
+ return `
131
+ #wp-responsibilities { margin: 0; }
132
+ #wp-responsibilities > h2 { display: none; }
133
+ #wp-responsibilities > .hint { color: var(--wp-muted); margin: 0 0 8px; }
134
+ .wp-resp-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 0; padding: 10px 15px; }
135
+ .wp-resp-card > summary { cursor: pointer; color: #333; }
136
+ .wp-resp-level { display: inline-block; min-width: 26px; padding: 1px 6px; margin-right: 6px; border-radius: 4px;
137
+ background: #eef; font-size: 12px; font-weight: bold; text-align: center; }
138
+ .wp-resp-body { margin-top: 8px; color: #444; }
139
+ .wp-resp-body code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }
140
+ .wp-hidden { display: none; }`;
141
+ }
142
+ }
143
+ exports.GraphPageStyles = GraphPageStyles;
144
+ //# sourceMappingURL=graph-page-styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-page-styles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-styles.ts"],"names":[],"mappings":";;;AAAA;;;;GAIG;AACH,MAAa,eAAe;IACxB,GAAG;QACC,OAAO,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACvH,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;;;;;;;;;;uFAuBwE,CAAC;IACpF,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;mJAuBoI,CAAC;IAChJ,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;;;;;;;;;kGAqBmF,CAAC;IAC/F,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;iGAOkF,CAAC;IAC9F,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;uFAawE,CAAC;IACpF,CAAC;IAED;;;;OAIG;IACK,KAAK;QACT,OAAO;;;;;;6EAM8D,CAAC;IAC1E,CAAC;IAEO,KAAK;QACT,OAAO;;;;;;;;;;sCAUuB,CAAC;IACnC,CAAC;CACJ;AA5ID,0CA4IC","sourcesContent":["/**\n * The architecture page's stylesheet: the dark drawer, the graph filling the viewport, the floating\n * popovers and the responsibilities panel. Emitted AFTER the shared menu/filter/navigation styles so\n * its `#graph` rules (fill the viewport, no 75vh cap) win.\n */\nexport class GraphPageStyles {\n css(): string {\n return this.base() + this.drawer() + this.controls() + this.main() + this.overlays() + this.graph() + this.cards();\n }\n\n private base(): string {\n return `\n :root {\n --wp-side-bg: #0c0d12; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;\n --wp-side-hover: #1f2129; --wp-side-line: #2a2c36; --wp-accent: #3559d6; --wp-accent-dark: #8aa4ff;\n --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #fbfbfd;\n }\n html, body { height: 100%; }\n /* A class that sets display (the legend lists are grids) must not un-hide a [hidden] element. */\n [hidden] { display: none !important; }\n body { margin: 0; padding: 0; overflow: hidden; background: var(--wp-canvas); color: var(--wp-fg);\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Arial, sans-serif; font-size: 14px; }\n kbd { font-family: ui-monospace, Menlo, monospace; font-size: 12px; border: 1px solid var(--wp-line);\n border-radius: 4px; padding: 0 5px; background: #f3f4f7; }\n .wp-shell { display: grid; grid-template-columns: 272px minmax(0, 1fr); height: 100vh; }\n .wp-shell.wp-collapsed { grid-template-columns: 0 minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { visibility: hidden; }\n @media (max-width: 760px) {\n .wp-shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, auto) minmax(0, 1fr); }\n .wp-side { max-height: 42vh; }\n .wp-shell.wp-collapsed { grid-template-columns: minmax(0, 1fr); }\n .wp-shell.wp-collapsed .wp-side { display: none; }\n .wp-main { height: auto; min-height: 0; }\n }\n @media (prefers-reduced-motion: reduce) { * { transition: none !important; } }`;\n }\n\n private drawer(): string {\n return `\n .wp-side { background: var(--wp-side-bg); color: var(--wp-side-fg); overflow-y: auto; overflow-x: hidden;\n display: flex; flex-direction: column; padding: 12px 0 10px; min-width: 0; font-size: 13px; }\n .wp-brand { display: flex; align-items: center; gap: 10px; padding: 0 16px 10px; font-weight: 600; font-size: 14px; }\n .wp-brand svg { flex: none; }\n .wp-group { padding: 8px 14px; border-top: 1px solid var(--wp-side-line); display: grid; gap: 6px; }\n .wp-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em; color: var(--wp-side-muted);\n font-weight: 600; padding-left: 4px; }\n .wp-label-row { display: flex; justify-content: space-between; align-items: center; }\n .wp-modes { display: grid; gap: 1px; }\n .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 5px 10px; border-radius: 8px; }\n .wp-mode:hover { background: var(--wp-side-hover); }\n .wp-mode[aria-pressed=\"true\"] { background: #262833; box-shadow: inset 3px 0 0 var(--wp-accent-dark); }\n .wp-mode:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-mode-name { display: block; font-size: 14px; font-weight: 600; line-height: 1.3; }\n .wp-mode-sub { display: block; font-size: 12px; color: var(--wp-side-muted); line-height: 1.3; }\n .wp-mode-reason { margin: 2px 4px 0; font-size: 11.5px; line-height: 1.35; color: var(--wp-side-muted); }\n .wp-legend-list { display: grid; gap: 5px; padding-left: 4px; }\n .wp-legend-row { display: flex; align-items: center; gap: 10px; line-height: 1.25; }\n .wp-legend-row svg { flex: none; }\n .wp-legend-row small { display: block; color: var(--wp-side-muted); font-size: 11.5px; }\n .wp-impact-note { margin: 2px 0 0; font-size: 11.5px; color: var(--wp-side-muted); }\n .wp-impact-note:empty { display: none; }\n .wp-foot { margin-top: auto; padding: 10px 18px 0; border-top: 1px solid var(--wp-side-line); display: flex; gap: 14px; flex-wrap: wrap; }`;\n }\n\n private controls(): string {\n return `\n .wp-side input[type=\"search\"] { width: 100%; box-sizing: border-box; background: #17181f; color: var(--wp-side-fg);\n border: 1px solid var(--wp-side-line); border-radius: 8px; padding: 7px 10px; font: inherit; font-size: 13px; }\n .wp-toggle { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 6px 10px; border-radius: 8px; font-size: 13px; }\n .wp-toggle:hover { background: var(--wp-side-hover); }\n .wp-toggle:disabled { cursor: not-allowed; opacity: 0.45; }\n .wp-sw { width: 30px; height: 18px; border-radius: 9px; background: #3a3d4a; position: relative; transition: background 0.15s; }\n .wp-sw::after { content: \"\"; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px; border-radius: 50%;\n background: #fff; transition: transform 0.15s; }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw { background: var(--wp-accent); }\n .wp-toggle[aria-pressed=\"true\"] .wp-sw::after { transform: translateX(12px); }\n .wp-link { all: unset; cursor: pointer; color: var(--wp-side-muted); font-size: 12px; text-decoration: underline; }\n .wp-link:hover { color: var(--wp-side-fg); }\n .wp-icon { all: unset; box-sizing: border-box; cursor: pointer; display: inline-grid; place-items: center; flex: none;\n width: 34px; height: 34px; border-radius: 8px; background: var(--wp-surface); border: 1px solid var(--wp-line);\n color: var(--wp-fg); font-size: 15px; }\n .wp-icon:hover { border-color: var(--wp-accent); }\n .wp-icon-mini { width: 24px; height: 24px; font-size: 12px; }\n .wp-icon-dark { background: transparent; border-color: var(--wp-side-line); color: var(--wp-side-fg); }\n .wp-mode:focus-visible, .wp-toggle:focus-visible, .wp-side input:focus-visible, .wp-icon:focus-visible,\n .wp-link:focus-visible { outline: 2px solid var(--wp-accent-dark); outline-offset: 2px; }`;\n }\n\n private main(): string {\n return `\n .wp-main { position: relative; min-width: 0; height: 100vh; display: flex; flex-direction: column; }\n .wp-topbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; padding: 7px 12px;\n border-bottom: 1px solid var(--wp-line); background: var(--wp-surface); }\n .wp-crumb { flex: 1 1 auto; min-width: 0; font-size: 13px; color: var(--wp-muted); overflow: hidden;\n text-overflow: ellipsis; white-space: nowrap; }\n .wp-crumb b { color: var(--wp-fg); }\n .wp-topbar #wp-filter-status, .wp-topbar #wp-graph-error { margin: 0; font-size: 13px; }`;\n }\n\n private overlays(): string {\n return `\n .wp-pop { position: absolute; z-index: 30; background: var(--wp-surface); border: 1px solid var(--wp-line);\n border-radius: 10px; box-shadow: 0 8px 24px #0002; padding: 12px 14px; font-size: 13px; box-sizing: border-box; }\n .wp-pop header { display: flex; justify-content: space-between; align-items: center; gap: 8px; font-weight: 600; margin-bottom: 8px; }\n .wp-legend-popout { left: 12px; bottom: 12px; width: 270px; max-height: 60vh; overflow: auto; }\n .wp-legend-popout .wp-legend-row small, .wp-legend-popout .wp-impact-note { color: var(--wp-muted); }\n .wp-help { right: 12px; top: 56px; width: min(380px, calc(100% - 24px)); line-height: 1.5; }\n .wp-help p { margin: 0 0 8px; }\n .wp-snapshot { left: 12px; bottom: 12px; width: min(440px, calc(100% - 24px)); }\n .wp-snapshot .hint { margin: 0; }\n .wp-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 40; width: min(540px, 100%); box-sizing: border-box;\n overflow-y: auto; padding: 0 16px 16px; background: #f5f6f9; border-left: 1px solid var(--wp-line); box-shadow: -8px 0 24px #0002; }\n .wp-panel > header { position: sticky; top: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center;\n padding: 12px 0; background: #f5f6f9; font-weight: 600; font-size: 15px; }`;\n }\n\n /**\n * The graph fills what the topbar leaves. A Runtime box with a specialization is an HTML-like\n * table, so its cells are extra polygons inside the node: the hover glow and the lock outline are\n * kept on the box's own outline (the FIRST polygon) instead of stroking every cell.\n */\n private graph(): string {\n return `\n #graph { flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; padding: 16px 16px 64px;\n box-sizing: border-box; background: var(--wp-canvas); }\n #graph g.wp-node-clickable:hover polygon:not(:first-of-type) { stroke: transparent; stroke-width: 0; filter: none; }\n #graph g.node.wp-locked > polygon:first-of-type,\n #graph g.node.wp-locked > ellipse,\n #graph g.node.wp-locked > path { stroke: #b26a00; stroke-width: 3; }`;\n }\n\n private cards(): string {\n return `\n #wp-responsibilities { margin: 0; }\n #wp-responsibilities > h2 { display: none; }\n #wp-responsibilities > .hint { color: var(--wp-muted); margin: 0 0 8px; }\n .wp-resp-card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin: 10px 0; padding: 10px 15px; }\n .wp-resp-card > summary { cursor: pointer; color: #333; }\n .wp-resp-level { display: inline-block; min-width: 26px; padding: 1px 6px; margin-right: 6px; border-radius: 4px;\n background: #eef; font-size: 12px; font-weight: bold; text-align: center; }\n .wp-resp-body { margin-top: 8px; color: #444; }\n .wp-resp-body code { background: #f2f2f2; padding: 1px 4px; border-radius: 3px; font-family: monospace; }\n .wp-hidden { display: none; }`;\n }\n}\n"]}
@@ -1,9 +1,17 @@
1
1
  import { LevelBand } from './graph-level-bands';
2
- /** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */
2
+ import { LegendFacts, NodeModeDots } from './graph-color-modes';
3
+ /**
4
+ * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.
5
+ *
6
+ * `dot` is the statement `fullDot` carries. `modes` is set only by the architecture graph, whose page
7
+ * switches color modes by re-rendering each box from its per-mode statement; the runtime graph has
8
+ * one look and leaves it null.
9
+ */
3
10
  export declare class RenderNode {
4
11
  readonly id: string;
5
12
  readonly dot: string;
6
- constructor(id: string, dot: string);
13
+ readonly modes: NodeModeDots | null;
14
+ constructor(id: string, dot: string, modes: NodeModeDots | null);
7
15
  }
8
16
  export declare class RenderEdge {
9
17
  readonly from: string;
@@ -20,7 +28,11 @@ export declare class GraphRenderModel {
20
28
  fullDot: string;
21
29
  header: string;
22
30
  footer: string;
31
+ /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */
32
+ legend: LegendFacts;
23
33
  node(id: string, dot: string): string;
34
+ /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */
35
+ styledNode(id: string, modes: NodeModeDots): string;
24
36
  completeEndpoints(): void;
25
37
  edge(from: string, to: string, dot: string): string;
26
38
  }
@@ -2,13 +2,22 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.GraphFilterAssets = exports.GraphRenderModel = exports.RenderEdge = exports.RenderNode = void 0;
4
4
  const dot_syntax_1 = require("./dot-syntax");
5
- /** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */
5
+ const graph_color_modes_1 = require("./graph-color-modes");
6
+ /**
7
+ * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.
8
+ *
9
+ * `dot` is the statement `fullDot` carries. `modes` is set only by the architecture graph, whose page
10
+ * switches color modes by re-rendering each box from its per-mode statement; the runtime graph has
11
+ * one look and leaves it null.
12
+ */
6
13
  class RenderNode {
7
14
  id;
8
15
  dot;
9
- constructor(id, dot) {
16
+ modes;
17
+ constructor(id, dot, modes) {
10
18
  this.id = id;
11
19
  this.dot = dot;
20
+ this.modes = modes;
12
21
  }
13
22
  }
14
23
  exports.RenderNode = RenderNode;
@@ -33,10 +42,17 @@ class GraphRenderModel {
33
42
  fullDot = '';
34
43
  header = '';
35
44
  footer = '';
45
+ /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */
46
+ legend = new graph_color_modes_1.LegendFacts();
36
47
  node(id, dot) {
37
- this.nodes.push(new RenderNode(id, dot));
48
+ this.nodes.push(new RenderNode(id, dot, null));
38
49
  return dot;
39
50
  }
51
+ /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */
52
+ styledNode(id, modes) {
53
+ this.nodes.push(new RenderNode(id, modes.runtime, modes));
54
+ return modes.runtime;
55
+ }
40
56
  completeEndpoints() {
41
57
  const ids = new Set(this.nodes.map((node) => node.id));
42
58
  for (const edge of this.edges) {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":";;;AAAA,6CAAwC;AAGxC,8FAA8F;AAC9F,MAAa,UAAU;IAEC;IACA;IAFpB,YACoB,EAAU,EACV,GAAW;QADX,OAAE,GAAF,EAAE,CAAQ;QACV,QAAG,GAAH,GAAG,CAAQ;IAC5B,CAAC;CACP;AALD,gCAKC;AAED,MAAa,UAAU;IAEC;IACA;IACA;IACA;IAJpB,YACoB,IAAY,EACZ,EAAU,EACV,GAAW,EACX,EAAU;QAHV,SAAI,GAAJ,IAAI,CAAQ;QACZ,OAAE,GAAF,EAAE,CAAQ;QACV,QAAG,GAAH,GAAG,CAAQ;QACX,OAAE,GAAF,EAAE,CAAQ;IAC3B,CAAC;CACP;AAPD,gCAOC;AAED,mEAAmE;AACnE,MAAa,gBAAgB;IAChB,KAAK,GAAiB,EAAE,CAAC;IACzB,KAAK,GAAiB,EAAE,CAAC;IAClC,KAAK,GAAgB,EAAE,CAAC;IACxB,OAAO,GAAG,EAAE,CAAC;IACb,MAAM,GAAG,EAAE,CAAC;IACZ,MAAM,GAAG,EAAE,CAAC;IAEZ,IAAI,CAAC,EAAU,EAAE,GAAW;QACxB,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC;QACzC,OAAO,GAAG,CAAC;IACf,CAAC;IAED,iBAAiB;QACb,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;QACvD,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YAC5B,KAAK,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;gBACpC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;oBAAE,SAAS;gBAC1B,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBACZ,IAAI,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,IAAA,qBAAQ,EAAC,EAAE,CAAC,MAAM,CAAC,CAAC;YAC5C,CAAC;QACL,CAAC;IACL,CAAC;IAED,IAAI,CAAC,IAAY,EAAE,EAAU,EAAE,GAAW;QACtC,MAAM,EAAE,GAAG,gBAAgB,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;QAC/C,MAAM,SAAS,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC;QACxD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QACzD,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AA9BD,4CA8BC;AAED,qFAAqF;AACrF,MAAa,iBAAiB;IAC1B,IAAI,CAAI,KAAQ;QACZ,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC1D,CAAC;IAED,IAAI;QACA,OAAO,CACH,qFAAqF;YACrF,uEAAuE;YACvE,iDAAiD,CACpD,CAAC;IACN,CAAC;IAED,MAAM;QACF,OAAO,CACH,8EAA8E;YAC9E,mEAAmE;YACnE,sEAAsE,CACzE,CAAC;IACN,CAAC;CACJ;AApBD,8CAoBC","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\n\n/** Renderer-owned records, captured while emitting DOT, never recovered by parsing labels. */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n\n node(id: string, dot: string): string {\n this.nodes.push(new RenderNode(id, dot));\n return dot;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n edge(from: string, to: string, dot: string): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const statement = `${dot.slice(0, -2)} [id=\"${id}\"];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id));\n return statement;\n }\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#1565c0; stroke-width:3; }'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"graph-render-model.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-render-model.ts"],"names":[],"mappings":"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","sourcesContent":["import { dotValue } from './dot-syntax';\nimport { LevelBand } from './graph-level-bands';\nimport { LegendFacts, NodeModeDots } from './graph-color-modes';\n\n/**\n * Renderer-owned records, captured while emitting DOT, never recovered by parsing labels.\n *\n * `dot` is the statement `fullDot` carries. `modes` is set only by the architecture graph, whose page\n * switches color modes by re-rendering each box from its per-mode statement; the runtime graph has\n * one look and leaves it null.\n */\nexport class RenderNode {\n constructor(\n public readonly id: string,\n public readonly dot: string,\n public readonly modes: NodeModeDots | null,\n ) {}\n}\n\nexport class RenderEdge {\n constructor(\n public readonly from: string,\n public readonly to: string,\n public readonly dot: string,\n public readonly id: string,\n ) {}\n}\n\n/** The immutable drawable snapshot carried by a generated page. */\nexport class GraphRenderModel {\n readonly nodes: RenderNode[] = [];\n readonly edges: RenderEdge[] = [];\n bands: LevelBand[] = [];\n fullDot = '';\n header = '';\n footer = '';\n /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */\n legend = new LegendFacts();\n\n node(id: string, dot: string): string {\n this.nodes.push(new RenderNode(id, dot, null));\n return dot;\n }\n\n /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */\n styledNode(id: string, modes: NodeModeDots): string {\n this.nodes.push(new RenderNode(id, modes.runtime, modes));\n return modes.runtime;\n }\n\n completeEndpoints(): void {\n const ids = new Set(this.nodes.map((node) => node.id));\n for (const edge of this.edges) {\n for (const id of [edge.from, edge.to]) {\n if (ids.has(id)) continue;\n ids.add(id);\n this.node(id, ` \"${dotValue(id)}\";\\n`);\n }\n }\n }\n\n edge(from: string, to: string, dot: string): string {\n const id = `wp-real-edge-${this.edges.length}`;\n const statement = `${dot.slice(0, -2)} [id=\"${id}\"];\\n`;\n this.edges.push(new RenderEdge(from, to, statement, id));\n return statement;\n }\n}\n\n/** Shared filter chrome and script-safe JSON for pages opened directly from disk. */\nexport class GraphFilterAssets {\n json<T>(value: T): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n }\n\n html(): string {\n return (\n '<p id=\"wp-filter-status\" hidden>Filtering: <strong id=\"wp-filter-anchor\"></strong> ' +\n '<button id=\"wp-filter-off\" type=\"button\">Turn off Filter</button></p>' +\n '<p id=\"wp-graph-error\" role=\"alert\" hidden></p>'\n );\n }\n\n styles(): string {\n return (\n '#wp-filter-status { text-align:center; } #wp-graph-error { color:#b71c1c; } ' +\n '#graph .wp-filter-anchor polygon, #graph .wp-filter-anchor path, ' +\n '#graph .wp-filter-anchor ellipse { stroke:#1565c0; stroke-width:3; }'\n );\n }\n}\n"]}
@@ -51,7 +51,7 @@ class ResponsibilitiesRenderer {
51
51
  }
52
52
  return (`<section id="wp-responsibilities">` +
53
53
  `<h2>Responsibilities (level high → low)</h2>` +
54
- `<p class="hint">Lock a box above to narrow this list to just that box's dependency chain.</p>` +
54
+ `<p class="hint">Lock a box (the Focus field, or a box's menu) to narrow this list to just that box's dependency chain.</p>` +
55
55
  cards.join('\n') +
56
56
  `</section>`);
57
57
  }
@@ -1 +1 @@
1
- {"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<projectKey>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box above to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n // data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the\n // heading stays the pretty short name. Keying the attribute on the short name is what let\n // two same-named projects share one card's visibility — see graph-names.ts.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(nodeId)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n}\n"]}
1
+ {"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<projectKey>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box (the Focus field, or a box's menu) to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n // data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the\n // heading stays the pretty short name. Keying the attribute on the short name is what let\n // two same-named projects share one card's visibility — see graph-names.ts.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(nodeId)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n }\n}\n"]}