@webpieces/nx-webpieces-rules 0.4.882 → 0.4.883

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 (76) hide show
  1. package/README.md +40 -13
  2. package/package.json +7 -7
  3. package/src/configs/tag-truth-configs.d.ts +15 -0
  4. package/src/configs/tag-truth-configs.js +20 -1
  5. package/src/configs/tag-truth-configs.js.map +1 -1
  6. package/src/executors/generate/executor.js +3 -0
  7. package/src/executors/generate/executor.js.map +1 -1
  8. package/src/index.d.ts +6 -3
  9. package/src/index.js +15 -1
  10. package/src/index.js.map +1 -1
  11. package/src/lib/graph-color-modes.d.ts +50 -6
  12. package/src/lib/graph-color-modes.js +88 -8
  13. package/src/lib/graph-color-modes.js.map +1 -1
  14. package/src/lib/graph-comparator.js +2 -0
  15. package/src/lib/graph-comparator.js.map +1 -1
  16. package/src/lib/graph-entry-levels.d.ts +35 -0
  17. package/src/lib/graph-entry-levels.js +60 -0
  18. package/src/lib/graph-entry-levels.js.map +1 -0
  19. package/src/lib/graph-impact.d.ts +5 -5
  20. package/src/lib/graph-impact.js +5 -5
  21. package/src/lib/graph-impact.js.map +1 -1
  22. package/src/lib/graph-legend.d.ts +11 -1
  23. package/src/lib/graph-legend.js +44 -6
  24. package/src/lib/graph-legend.js.map +1 -1
  25. package/src/lib/graph-level-bands.js +3 -1
  26. package/src/lib/graph-level-bands.js.map +1 -1
  27. package/src/lib/graph-loader.d.ts +7 -1
  28. package/src/lib/graph-loader.js +10 -1
  29. package/src/lib/graph-loader.js.map +1 -1
  30. package/src/lib/graph-metadata.d.ts +2 -0
  31. package/src/lib/graph-metadata.js +19 -0
  32. package/src/lib/graph-metadata.js.map +1 -1
  33. package/src/lib/graph-page-shell.d.ts +28 -6
  34. package/src/lib/graph-page-shell.js +55 -18
  35. package/src/lib/graph-page-shell.js.map +1 -1
  36. package/src/lib/graph-page-styles.js +3 -1
  37. package/src/lib/graph-page-styles.js.map +1 -1
  38. package/src/lib/graph-products.d.ts +79 -0
  39. package/src/lib/graph-products.js +199 -0
  40. package/src/lib/graph-products.js.map +1 -0
  41. package/src/lib/graph-render-model.d.ts +13 -2
  42. package/src/lib/graph-render-model.js +36 -4
  43. package/src/lib/graph-render-model.js.map +1 -1
  44. package/src/lib/graph-sorter.d.ts +13 -0
  45. package/src/lib/graph-sorter.js.map +1 -1
  46. package/src/lib/graph-visualizer.client.d.ts +20 -10
  47. package/src/lib/graph-visualizer.client.js +67 -31
  48. package/src/lib/graph-visualizer.client.js.map +1 -1
  49. package/src/lib/graph-visualizer.js +7 -6
  50. package/src/lib/graph-visualizer.js.map +1 -1
  51. package/src/lib/product-resolver.d.ts +38 -0
  52. package/src/lib/product-resolver.js +63 -0
  53. package/src/lib/product-resolver.js.map +1 -0
  54. package/src/lib/runtime-graph-model.d.ts +7 -0
  55. package/src/lib/runtime-graph-model.js.map +1 -1
  56. package/src/lib/runtime-graph.js +3 -0
  57. package/src/lib/runtime-graph.js.map +1 -1
  58. package/src/lib/runtime-html-page.d.ts +7 -0
  59. package/src/lib/runtime-html-page.js +29 -0
  60. package/src/lib/runtime-html-page.js.map +1 -1
  61. package/src/lib/runtime-visualizer.client.d.ts +7 -0
  62. package/src/lib/runtime-visualizer.client.js +32 -0
  63. package/src/lib/runtime-visualizer.client.js.map +1 -1
  64. package/src/lib/runtime-visualizer.js +3 -1
  65. package/src/lib/runtime-visualizer.js.map +1 -1
  66. package/src/lib/saved-snapshot.js +6 -2
  67. package/src/lib/saved-snapshot.js.map +1 -1
  68. package/src/lib/tag-truth.d.ts +20 -3
  69. package/src/lib/tag-truth.js +48 -7
  70. package/src/lib/tag-truth.js.map +1 -1
  71. package/src/nx-policy-registry.js +1 -0
  72. package/src/nx-policy-registry.js.map +1 -1
  73. package/src/rule-help.js +1 -0
  74. package/src/rule-help.js.map +1 -1
  75. package/src/rule-settings.js +12 -1
  76. package/src/rule-settings.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;GAWG;;;AAEH,2DAAgH;AAEhH;;;;;GAKG;AACH,IAAY,WAMX;AAND,WAAY,WAAW;IACnB,wCAAyB,CAAA;IACzB,kCAAmB,CAAA;IACnB,wCAAyB,CAAA;IACzB,4CAA6B,CAAA;IAC7B,8BAAe,CAAA;AACnB,CAAC,EANW,WAAW,2BAAX,WAAW,QAMtB;AAED,MAAa,WAAW;IAEA;IACA;IAFpB,YACoB,KAAkB,EAClB,KAAa;QADb,UAAK,GAAL,KAAK,CAAa;QAClB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AALD,kCAKC;AAEY,QAAA,aAAa,GAA2B;IACjD,IAAI,WAAW,CAAC,WAAW,CAAC,UAAU,EAAE,YAAY,CAAC;IACrD,IAAI,WAAW,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC;IAC/C,IAAI,WAAW,CAAC,WAAW,CAAC,UAAU,EAAE,0BAA0B,CAAC;IACnE,IAAI,WAAW,CAAC,WAAW,CAAC,YAAY,EAAE,yBAAyB,CAAC;IACpE,IAAI,WAAW,CAAC,WAAW,CAAC,KAAK,EAAE,4BAA4B,CAAC;CACnE,CAAC;AAEF,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;;;cAGtD,IAAI,CAAC,YAAY,EAAE;;cAEnB,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC;;;;;;;cAO1B,KAAK,CAAC,WAAW;;;;;cAKjB,KAAK,CAAC,MAAM;;;;cAIZ,KAAK,CAAC,OAAO;;;;;;aAMd,CAAC;IACV,CAAC;IAED;;;OAGG;IACK,YAAY;QAChB,MAAM,KAAK,GAAG,+BAAW,CAAC,CAAC,CAAC,CAAC;QAC7B,OAAO;;iGAEkF,KAAK,CAAC,IAAI,6DAA6D,KAAK,CAAC,QAAQ;;kGAEpF,+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;mBAC9K,CAAC;IAChB,CAAC;IAED,2EAA2E;IACnE,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,4EAA4E,IAAI,CAAC,IAAI,mBAAmB,OAAO,gBAAgB,KAAK,GAAG,CAAC,IAAI;YAC5I,8BAA8B,IAAI,CAAC,IAAI,oCAAoC,IAAI,CAAC,QAAQ,kBAAkB,CAC7G,CAAC;IACN,CAAC;IAED;;;;OAIG;IACK,aAAa;QACjB,MAAM,MAAM,GAAG,qBAAa,CAAC,GAAG,CAC5B,CAAC,MAAmB,EAAU,EAAE,CAC5B,sEAAsE,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,KAAK,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG;YAClJ,+BAA+B,MAAM,CAAC,KAAK,sDAAsD,MAAM,CAAC,KAAK,mBAAmB,CACvI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACX,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,MAAyB,EAAU,EAAE,CAC/D,MAAM;aACD,GAAG,CACA,CAAC,KAAa,EAAU,EAAE,CACtB,6DAA6D,KAAK,mBAAmB,KAAK,0BAA0B,KAAK,WAAW,CAC3I;aACA,IAAI,CAAC,EAAE,CAAC,CAAC;QAClB,OAAO;;0HAE2G,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,GAAG,MAAM;iHAC5C,KAAK,CAAC,SAAS,EAAE,mCAAe,CAAC;8GACpC,KAAK,CAAC,MAAM,EAAE,+BAAW,CAAC,GAAG,CAAC,CAAC,KAAgB,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;;eAEvK,CAAC;IACZ,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,KAAa;QAC7B,OAAO,CACH,+DAA+D,KAAK,6CAA6C;YACjH,yHAAyH;YACzH,oHAAoH,CACvH,CAAC;IACN,CAAC;IAEO,MAAM,CAAC,KAAiB;QAC5B,OAAO;;;;cAID,KAAK,CAAC,YAAY;;eAEjB,CAAC;IACZ,CAAC;IAEO,QAAQ,CAAC,KAAiB;QAC9B,OAAO;;cAED,KAAK,CAAC,MAAM;;UAEhB,IAAI,CAAC,IAAI,EAAE;UACX,IAAI,CAAC,aAAa,EAAE;kEACoC,KAAK,CAAC,QAAQ;;;cAGlE,KAAK,CAAC,gBAAgB;iBACnB,CAAC;IACd,CAAC;IAED,6FAA6F;IACrF,IAAI;QACR,OAAO;;;;;;;;eAQA,CAAC;IACZ,CAAC;IAEO,SAAS;QACb,OAAO,CACH,qEAAqE;YACrE,mGAAmG;YACnG,oGAAoG;YACpG,oGAAoG;YACpG,uFAAuF,CAC1F,CAAC;IACN,CAAC;CACJ;AAzJD,wCAyJC","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { FRAMEWORK_ORDER, GRAPH_MODES, GraphMode, ModeInfo, ROLE_STYLES, RoleStyle } from './graph-color-modes';\n\n/**\n * The Filter popover's \"Changes on this branch\" choices. The page computes each set from the Impact\n * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their\n * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +\n * affected + every build input.\n */\nexport enum ChangeScope {\n EVERYTHING = 'everything',\n CHANGED = 'changed',\n DEPENDENTS = 'dependents',\n DEPENDENCIES = 'dependencies',\n BUILD = 'build',\n}\n\nexport class ScopeOption {\n constructor(\n public readonly scope: ChangeScope,\n public readonly label: string,\n ) {}\n}\n\nexport const SCOPE_OPTIONS: readonly ScopeOption[] = [\n new ScopeOption(ChangeScope.EVERYTHING, 'Everything'),\n new ScopeOption(ChangeScope.CHANGED, 'Changed'),\n new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + what uses them'),\n new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + what they use'),\n new ScopeOption(ChangeScope.BUILD, 'Whole build of this branch'),\n];\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n ${this.modePulldown()}\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n ${this.impactKinds('drawer')}\n </div>\n <div class=\"wp-group\">\n <button type=\"button\" class=\"wp-filter-btn\" id=\"wp-filter-open\" aria-controls=\"wp-filter-pop\" aria-expanded=\"false\"><span>Filter</span><span class=\"wp-badge\" id=\"wp-filter-badge\" hidden>0</span></button>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /**\n * \"Color by\": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a\n * menu of the three. Keys 1/2/3 and the #runtime / #architecture / #impact links still switch.\n */\n private modePulldown(): string {\n const first = GRAPH_MODES[0];\n return `<div class=\"wp-pulldown\">\n <button type=\"button\" class=\"wp-pill-trigger\" id=\"wp-mode-trigger\" aria-haspopup=\"menu\" aria-controls=\"wp-mode-menu\" aria-expanded=\"false\">\n <span class=\"wp-pill-text\"><span class=\"wp-mode-name\" id=\"wp-mode-current\">${first.name}</span><span class=\"wp-mode-sub\" id=\"wp-mode-current-sub\">${first.subtitle}</span></span><span class=\"wp-caret\" aria-hidden=\"true\">▾</span>\n </button>\n <div class=\"wp-menu\" id=\"wp-mode-menu\" role=\"menu\" aria-label=\"Color by\" hidden>${GRAPH_MODES.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n </div>`;\n }\n\n /** One menu option, two lines: the mode's name, then its grey subtitle. */\n private modeItem(info: ModeInfo, index: number): string {\n const checked = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" role=\"menuitemradio\" data-wp-mode=\"${info.mode}\" aria-checked=\"${checked}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n /**\n * The Filter popover. Its three groups INTERSECT: a box is shown when it is in the chosen change\n * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip\n * (when any is selected). Nothing applies until \"Show N projects\".\n */\n private filterPopover(): string {\n const scopes = SCOPE_OPTIONS.map(\n (option: ScopeOption): string =>\n `<label class=\"wp-scope\"><input type=\"radio\" name=\"wp-scope\" value=\"${option.scope}\"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +\n `<span class=\"wp-scope-name\">${option.label}</span><span class=\"wp-count\" data-wp-scope-count=\"${option.scope}\"></span></label>`,\n ).join('');\n const chips = (group: string, values: readonly string[]): string =>\n values\n .map(\n (value: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"${group}\" data-wp-chip=\"${value}\" aria-pressed=\"false\">${value}</button>`,\n )\n .join('');\n return `<div class=\"wp-pop wp-filter-pop\" id=\"wp-filter-pop\" role=\"dialog\" aria-label=\"Filter projects\" hidden>\n <header><span>Filter</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-filter-close\" aria-label=\"Close the filter\">✕</button></header>\n <fieldset class=\"wp-fgroup\" id=\"wp-scope-group\"><legend id=\"wp-scope-legend\">Changes on this branch</legend>${this.impactKinds('filter')}${scopes}<p class=\"wp-scope-reason\" id=\"wp-scope-reason\" hidden></p></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Runtime <small>any match</small></legend><div class=\"wp-chips\">${chips('runtime', FRAMEWORK_ORDER)}</div></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Role <small>any match</small></legend><div class=\"wp-chips\">${chips('role', ROLE_STYLES.map((style: RoleStyle): string => style.role))}</div></fieldset>\n <footer><button type=\"button\" class=\"wp-text-btn\" id=\"wp-filter-clear\">Clear all</button><button type=\"button\" class=\"wp-primary\" id=\"wp-filter-apply\">Show all projects</button></footer>\n </div>`;\n }\n\n /**\n * Which Impact scan the page shows (#1163): \"Changed on this branch\" (fork point → working tree)\n * or \"Last commit\" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;\n * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.\n */\n private impactKinds(where: string): string {\n return (\n `<div class=\"wp-chips wp-impact-kinds\" data-wp-impact-kinds=\"${where}\" role=\"group\" aria-label=\"Compare\" hidden>` +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"branch\" aria-pressed=\"false\">Changed on this branch</button>' +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"commit\" aria-pressed=\"false\">Last commit</button></div>'\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 <div class=\"wp-pills\" id=\"wp-filter-pills\" aria-label=\"Active filters\"></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help()}\n ${this.filterPopover()}\n <div class=\"wp-pop wp-snapshot\" id=\"wp-snapshot\" hidden>${parts.snapshot}</div>\n <aside class=\"wp-panel\" id=\"wp-resp-panel\" aria-label=\"Responsibilities\" hidden>\n <header><span>Responsibilities</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-resp-close\" aria-label=\"Close responsibilities\">✕</button></header>\n ${parts.responsibilities}\n </aside>`;\n }\n\n /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */\n private help(): string {\n return `<div class=\"wp-pop wp-help\" id=\"wp-help\" role=\"dialog\" aria-label=\"How to read this graph\" hidden>\n <header><span>Reading the graph</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-help-close\" aria-label=\"Close help\">✕</button></header>\n <p>Every row is one dependency level: the HIGHEST level is the top row and L0, the foundation libraries, is always the bottom row. Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>\n <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime and by role; the three groups combine. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and what depends on it (amber outline); with both available, a toggle picks which.</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#a46bf7\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
1
+ {"version":3,"file":"graph-page-shell.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-shell.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture page's shell (#1155): a collapsible dark drawer on the left holding every control,\n * and the graph filling the rest of the viewport.\n *\n * Before this, two hint paragraphs, a ~650px three-column legend, a lock dropdown and a zoom bar all\n * sat ABOVE the graph, so a laptop's first screen showed no graph at all. Now the hints are a \"?\"\n * popover, the legend lives in the drawer (and can pop out over the graph), zoom floats bottom-right\n * (graph-navigation.ts) and the responsibilities open in a right-hand panel.\n *\n * Markup only. Behaviour is graph-visualizer.client.ts; styling is graph-page-styles.ts. No element\n * carries a `style=` attribute — every look is a class.\n */\n\nimport { FRAMEWORK_ORDER, GRAPH_MODES, GraphMode, ModeInfo, ROLE_STYLES, RoleStyle } from './graph-color-modes';\nimport { ProductColor, ProductPalette } from './graph-products';\n\n/**\n * The Filter popover's \"Changes on this branch\" choices. The page computes each set from the Impact\n * sidecar (graph-impact.ts): CHANGED = touched; DEPENDENTS = touched + affected (nx affected: their\n * tests re-run); DEPENDENCIES = touched + every transitive dependency of it; BUILD = touched +\n * affected + every build input — the dependencies of the DEPENDENTS too, which is why it is more than\n * the union of the two rows above it.\n *\n * \"Dependents\" are what USE a project and \"dependencies\" what it uses (#1179). \"Dependee\" would mean\n * the thing depended ON — the opposite of the dependents row — so it is never used.\n */\nexport enum ChangeScope {\n EVERYTHING = 'everything',\n CHANGED = 'changed',\n DEPENDENTS = 'dependents',\n DEPENDENCIES = 'dependencies',\n BUILD = 'build',\n}\n\n/**\n * One change scope: its name and the grey second line under it, like the Color-by menu. These are the\n * ONE copy of the labels — the page script reads each name back from its `data-wp-scope-label` (the\n * top-bar pill), so the two cannot drift apart.\n */\nexport class ScopeOption {\n constructor(\n public readonly scope: ChangeScope,\n public readonly label: string,\n public readonly subtitle: string,\n ) {}\n}\n\nexport const SCOPE_OPTIONS: readonly ScopeOption[] = [\n new ScopeOption(ChangeScope.EVERYTHING, 'All', 'every project'),\n new ScopeOption(ChangeScope.CHANGED, 'Changed', 'owns a changed file'),\n new ScopeOption(ChangeScope.DEPENDENTS, 'Changed + dependents', '+ everything that uses them (their tests re-run)'),\n new ScopeOption(ChangeScope.DEPENDENCIES, 'Changed + dependencies', '+ everything they use'),\n new ScopeOption(ChangeScope.BUILD, 'Changed + dependents + dependencies', 'everything CI builds for this branch'),\n];\n\n/** Everything the shell frames, already rendered by its owner. */\nexport class ShellParts {\n constructor(\n public readonly title: string,\n public readonly lockControl: string,\n public readonly legend: string,\n public readonly edgeKey: string,\n public readonly filterStatus: string,\n public readonly snapshot: string,\n public readonly responsibilities: string,\n /** Every declared product, sorted (#1179); empty hides the Filter's Product group. */\n public readonly products: string[],\n ) {}\n}\n\nexport class GraphPageShell {\n body(parts: ShellParts): string {\n return `<div class=\"wp-shell\" id=\"wp-shell\">\n ${this.drawer(parts)}\n <main class=\"wp-main\">\n ${this.topbar(parts)}\n <div id=\"graph\"></div>\n ${this.overlays(parts)}\n </main>\n</div>`;\n }\n\n private drawer(parts: ShellParts): string {\n return `<aside class=\"wp-side\" id=\"wp-side\" aria-label=\"Graph controls\">\n <div class=\"wp-brand\">${this.brandIcon()}<span>${parts.title}</span></div>\n <div class=\"wp-group\" role=\"group\" aria-label=\"Color mode\">\n <div class=\"wp-label\">Color by</div>\n ${this.modePulldown()}\n <p class=\"wp-mode-reason\" id=\"wp-impact-reason\" hidden></p>\n ${this.impactKinds('drawer')}\n </div>\n <div class=\"wp-group\">\n <button type=\"button\" class=\"wp-filter-btn\" id=\"wp-filter-open\" aria-controls=\"wp-filter-pop\" aria-expanded=\"false\"><span>Filter</span><span class=\"wp-badge\" id=\"wp-filter-badge\" hidden>0</span></button>\n </div>\n <div class=\"wp-group\">\n <label class=\"wp-label\" for=\"wp-lock\">Focus</label>\n ${parts.lockControl}\n <button type=\"button\" class=\"wp-toggle\" id=\"wp-filter-toggle\" aria-pressed=\"false\">Hide unconnected <span class=\"wp-sw\" aria-hidden=\"true\"></span></button>\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label wp-label-row\">Legend <button type=\"button\" class=\"wp-icon wp-icon-mini wp-icon-dark\" id=\"wp-legend-pop\" title=\"Pop the legend out over the graph\" aria-label=\"Pop the legend out over the graph\">⇱</button></div>\n ${parts.legend}\n </div>\n <div class=\"wp-group\">\n <div class=\"wp-label\">Edges</div>\n ${parts.edgeKey}\n </div>\n <div class=\"wp-foot\">\n <button type=\"button\" class=\"wp-link\" id=\"wp-resp-open\" aria-expanded=\"false\">Responsibilities</button>\n <button type=\"button\" class=\"wp-link\" id=\"wp-snapshot-open\" aria-expanded=\"false\">Saved snapshot</button>\n </div>\n </aside>`;\n }\n\n /**\n * \"Color by\": ONE pill-shaped trigger showing the current mode (name + subtitle) that opens a\n * menu of the four. Keys 1/2/3/4 and the #runtime / #architecture / #impact / #product links still\n * switch.\n */\n private modePulldown(): string {\n const first = GRAPH_MODES[0];\n return `<div class=\"wp-pulldown\">\n <button type=\"button\" class=\"wp-pill-trigger\" id=\"wp-mode-trigger\" aria-haspopup=\"menu\" aria-controls=\"wp-mode-menu\" aria-expanded=\"false\">\n <span class=\"wp-pill-text\"><span class=\"wp-mode-name\" id=\"wp-mode-current\">${first.name}</span><span class=\"wp-mode-sub\" id=\"wp-mode-current-sub\">${first.subtitle}</span></span><span class=\"wp-caret\" aria-hidden=\"true\">▾</span>\n </button>\n <div class=\"wp-menu\" id=\"wp-mode-menu\" role=\"menu\" aria-label=\"Color by\" hidden>${GRAPH_MODES.map((info: ModeInfo, index: number): string => this.modeItem(info, index)).join('')}</div>\n </div>`;\n }\n\n /** One menu option, two lines: the mode's name, then its grey subtitle. */\n private modeItem(info: ModeInfo, index: number): string {\n const checked = info.mode === GraphMode.RUNTIME ? 'true' : 'false';\n return (\n `<button type=\"button\" class=\"wp-mode\" role=\"menuitemradio\" data-wp-mode=\"${info.mode}\" aria-checked=\"${checked}\" title=\"Key ${index + 1}\">` +\n `<span class=\"wp-mode-name\">${info.name}</span><span class=\"wp-mode-sub\">${info.subtitle}</span></button>`\n );\n }\n\n /**\n * The Filter popover. Its four groups INTERSECT: a box is shown when it is in the chosen change\n * scope AND carries any selected runtime chip (when any is selected) AND any selected role chip\n * (when any is selected) AND belongs to any selected product (when any is selected). Nothing\n * applies until \"Show N projects\".\n */\n private filterPopover(products: string[]): string {\n const scopes = SCOPE_OPTIONS.map(\n (option: ScopeOption): string =>\n `<label class=\"wp-scope\"><input type=\"radio\" name=\"wp-scope\" value=\"${option.scope}\"${option.scope === ChangeScope.EVERYTHING ? ' checked' : ''}>` +\n `<span class=\"wp-scope-text\"><span class=\"wp-scope-name\" data-wp-scope-label=\"${option.scope}\">${option.label}</span>` +\n `<span class=\"wp-scope-sub\">${option.subtitle}</span></span>` +\n `<span class=\"wp-count\" data-wp-scope-count=\"${option.scope}\"></span></label>`,\n ).join('');\n const chips = (group: string, values: readonly string[]): string =>\n values\n .map(\n (value: string): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"${group}\" data-wp-chip=\"${value}\" aria-pressed=\"false\">${value}</button>`,\n )\n .join('');\n return `<div class=\"wp-pop wp-filter-pop\" id=\"wp-filter-pop\" role=\"dialog\" aria-label=\"Filter projects\" hidden>\n <header><span>Filter</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-filter-close\" aria-label=\"Close the filter\">✕</button></header>\n <fieldset class=\"wp-fgroup\" id=\"wp-scope-group\"><legend id=\"wp-scope-legend\">Changes on this branch</legend>${this.impactKinds('filter')}${scopes}<p class=\"wp-scope-reason\" id=\"wp-scope-reason\" hidden></p></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Runtime <small>any match</small></legend><div class=\"wp-chips\">${chips('runtime', FRAMEWORK_ORDER)}</div></fieldset>\n <fieldset class=\"wp-fgroup\"><legend>Role <small>any match</small></legend><div class=\"wp-chips\">${chips('role', ROLE_STYLES.map((style: RoleStyle): string => style.role))}</div></fieldset>\n ${this.productGroup(products)}\n <footer><button type=\"button\" class=\"wp-text-btn\" id=\"wp-filter-clear\">Clear all</button><button type=\"button\" class=\"wp-primary\" id=\"wp-filter-apply\">Show all projects</button></footer>\n </div>`;\n }\n\n /**\n * The Product group (#1179): one chip per declared product, each with its palette swatch. Several\n * chips select the UNION of their closures; the group intersects with the others. Omitted when the\n * workspace declares no product.\n */\n private productGroup(products: string[]): string {\n if (products.length === 0) return '';\n const palette = new ProductPalette(products);\n const chips = palette.colors\n .map(\n (color: ProductColor): string =>\n `<button type=\"button\" class=\"wp-chip\" data-wp-chip-group=\"product\" data-wp-chip=\"${color.product}\" aria-pressed=\"false\">` +\n `<svg class=\"wp-chip-swatch\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" aria-hidden=\"true\"><circle cx=\"5\" cy=\"5\" r=\"4.5\" fill=\"${color.color}\"/></svg>` +\n `${color.product}</button>`,\n )\n .join('');\n return `<fieldset class=\"wp-fgroup\" id=\"wp-product-group\"><legend>Product <small>any match</small></legend><div class=\"wp-chips\">${chips}</div></fieldset>`;\n }\n\n /**\n * Which Impact scan the page shows (#1163): \"Changed on this branch\" (fork point → working tree)\n * or \"Last commit\" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;\n * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.\n */\n private impactKinds(where: string): string {\n return (\n `<div class=\"wp-chips wp-impact-kinds\" data-wp-impact-kinds=\"${where}\" role=\"group\" aria-label=\"Compare\" hidden>` +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"branch\" aria-pressed=\"false\">Changed on this branch</button>' +\n '<button type=\"button\" class=\"wp-chip\" data-wp-impact-kind=\"commit\" aria-pressed=\"false\">Last commit</button></div>'\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 <div class=\"wp-pills\" id=\"wp-filter-pills\" aria-label=\"Active filters\"></div>\n ${parts.filterStatus}\n <button type=\"button\" class=\"wp-icon\" id=\"wp-help-btn\" aria-label=\"How to read this graph\" aria-controls=\"wp-help\" aria-expanded=\"false\">?</button>\n </div>`;\n }\n\n private overlays(parts: ShellParts): string {\n return `<div class=\"wp-pop wp-legend-popout\" id=\"wp-legend-popout\" hidden>\n <header><span id=\"wp-legend-pop-title\">Legend · Runtime</span><button type=\"button\" class=\"wp-icon wp-icon-mini\" id=\"wp-legend-close\" aria-label=\"Close the legend\">✕</button></header>\n ${parts.legend}\n </div>\n ${this.help()}\n ${this.filterPopover(parts.products)}\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. Servers, clients and apps always form the top row (a bundle sits above the apps it aggregates). Transitive dependencies are allowed but not drawn.</p>\n <p>💡 <strong>Click any box</strong> for its menu: <strong>View Design</strong> (only where the project has a generated <strong>design.html</strong>, i.e. what the AI sees inside it), <strong>Lock/Unlock</strong> (the same lock as the Focus field), <strong>Filter Unconnected</strong> (removes unrelated boxes, keeping each L-number row).</p>\n <p><strong>Filter</strong> narrows the graph by what changed on this branch or in the last commit, by runtime, by role and by product; the groups combine. A product keeps its servers, clients and apps plus everything they depend on, so <strong>Product</strong> + <strong>Changed + dependents + dependencies</strong> is exactly what this branch builds that can affect that product. Every box keeps its own L-row, and a level left empty shows as a thin labeled band.</p>\n <p>🔦 <strong>Hover any box</strong> to trace its <em>entire</em> dependency chain — every ancestor above it and every dependency below it, with the lines between — while the rest of the graph dims.</p>\n <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what changed on this branch or in the last commit (amber) and its dependents (amber outline); with both available, a toggle picks which. <strong>Product</strong>: which products each box belongs to — one color per product, stripes for several, grey for every product, a dashed white box for none.</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> <kbd>4</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#a46bf7\" stroke-width=\"1.6\"/>' +\n '<rect x=\"13\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<rect x=\"7\" y=\"15\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/>' +\n '<path d=\"M5 7v4h12V7M11 11v4\" fill=\"none\" stroke=\"#e9eaf0\" stroke-width=\"1.6\"/></svg>'\n );\n }\n}\n"]}
@@ -138,7 +138,9 @@ class GraphPageStyles {
138
138
  .wp-scope { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }
139
139
  .wp-scope:hover { background: #f4f1fb; }
140
140
  .wp-scope input { accent-color: var(--wp-accent); margin: 0; }
141
- .wp-scope-name { flex: 1 1 auto; }
141
+ .wp-scope-text { flex: 1 1 auto; display: grid; }
142
+ .wp-scope-sub { font-size: 11.5px; color: var(--wp-muted); line-height: 1.3; }
143
+ .wp-chip-swatch { vertical-align: -1px; margin-right: 5px; }
142
144
  .wp-count { color: var(--wp-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
143
145
  .wp-scope-reason { margin: 2px 6px 0; font-size: 12px; color: var(--wp-muted); }
144
146
  .wp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -1 +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,2DAAoD;AAEpD;;;;GAIG;AACH,MAAa,eAAe;IACxB,GAAG;QACC,OAAO,CACH,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,MAAM,EAAE;YACb,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,aAAa,EAAE;YACpB,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,EAAE,CACf,CAAC;IACN,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;;;;;;;;;;uFAuBwE,CAAC;IACpF,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mJAqCoI,CAAC;IAChJ,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;;;;;;;;;;mGAsBoF,CAAC;IAChG,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;uDAcwC,CAAC;IACpD,CAAC;IAED,iFAAiF;IACzE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;kEA0BmD,CAAC;IAC/D,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;uFAawE,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,KAAK;QACT,MAAM,KAAK,GAAG,6BAA6B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,KAAK,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC;QACvC,MAAM,IAAI,GAAG,mGAAmG,CAAC;QACjH,OAAO;;;6CAG8B,KAAK;qCACb,iCAAa;qCACb,iCAAa,mCAAmC,IAAI;uCAClD,iCAAa,OAAO,KAAK;kCAC9B,iCAAa,wEAAwE,CAAC;IACpH,CAAC;IAEO,KAAK;QACT,OAAO;;;;;;;;;;sCAUuB,CAAC;IACnC,CAAC;CACJ;AAxND,0CAwNC","sourcesContent":["import { STRIPED_CLASS } from './graph-color-modes';\n\n/**\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 (\n this.base() +\n this.drawer() +\n this.controls() +\n this.main() +\n this.overlays() +\n this.filterPopover() +\n this.graph() +\n this.cards()\n );\n }\n\n private base(): string {\n return `\n :root {\n --wp-side-bg: #15161b; --wp-side-control: #1e1f25; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;\n --wp-side-hover: #26272f; --wp-side-line: #3a3c46; --wp-accent: #8b3cf0; --wp-accent-hover: #a46bf7;\n --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #faf9f5;\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-pulldown { position: relative; }\n .wp-pill-trigger { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 10px;\n width: 100%; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); }\n .wp-pill-trigger:hover, .wp-pill-trigger[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-pill-text { flex: 1 1 auto; min-width: 0; }\n .wp-caret { color: var(--wp-side-muted); }\n .wp-menu { position: absolute; z-index: 35; left: 0; right: 0; top: calc(100% + 6px); display: grid; gap: 2px; padding: 6px;\n background: var(--wp-side-control); border: 1px solid var(--wp-side-line); border-radius: 12px; box-shadow: 0 10px 28px #0008; }\n .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 6px 10px; border-radius: 8px; }\n .wp-mode:hover { background: var(--wp-side-hover); }\n .wp-mode[aria-checked=\"true\"] { background: #2a2333; box-shadow: inset 3px 0 0 var(--wp-accent); }\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-filter-btn { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 7px 12px; border-radius: 8px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); font-size: 13px; font-weight: 600; }\n .wp-filter-btn:hover, .wp-filter-btn[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-badge { min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--wp-accent); color: #fff;\n font-size: 11.5px; text-align: center; box-sizing: border-box; }\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: var(--wp-side-control); 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-pill-trigger:focus-visible, .wp-filter-btn:focus-visible, .wp-chip:focus-visible,\n .wp-primary:focus-visible, .wp-pill button: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-hover); 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 .wp-pills { display: flex; gap: 6px; flex-wrap: wrap; }\n .wp-pills:empty { display: none; }\n .wp-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; border-radius: 999px;\n background: #f1e8fe; color: #4a1d8a; border: 1px solid #d6bcfa; font-size: 12.5px; }\n .wp-pill button { all: unset; cursor: pointer; width: 18px; height: 18px; display: inline-grid; place-items: center;\n border-radius: 50%; color: #4a1d8a; }\n .wp-pill button:hover { background: #e2cffc; }`;\n }\n\n /** The Filter popover: three groups, chips, and the \"Show N projects\" footer. */\n private filterPopover(): string {\n return `\n .wp-filter-pop { left: 12px; top: 56px; width: min(380px, calc(100% - 24px)); max-height: calc(100% - 80px); overflow: auto; }\n .wp-fgroup { border: 0; margin: 0 0 12px; padding: 0; display: grid; gap: 4px; }\n .wp-fgroup legend { padding: 0; margin-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;\n color: var(--wp-muted); font-weight: 600; }\n .wp-fgroup legend small { text-transform: none; letter-spacing: 0; font-weight: 400; }\n .wp-fgroup:disabled .wp-scope { opacity: 0.45; cursor: not-allowed; }\n .wp-scope { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }\n .wp-scope:hover { background: #f4f1fb; }\n .wp-scope input { accent-color: var(--wp-accent); margin: 0; }\n .wp-scope-name { flex: 1 1 auto; }\n .wp-count { color: var(--wp-muted); font-size: 12px; font-variant-numeric: tabular-nums; }\n .wp-scope-reason { margin: 2px 6px 0; font-size: 12px; color: var(--wp-muted); }\n .wp-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n .wp-chip { all: unset; box-sizing: border-box; cursor: pointer; padding: 3px 11px; border-radius: 999px;\n border: 1px solid var(--wp-line); background: var(--wp-surface); font-size: 12.5px; }\n .wp-chip:hover { border-color: var(--wp-accent-hover); }\n .wp-chip[aria-pressed=\"true\"] { background: var(--wp-accent); border-color: var(--wp-accent); color: #fff; }\n .wp-chip:disabled { opacity: 0.45; cursor: default; }\n .wp-impact-kinds { margin: 6px 0 4px; }\n .wp-side .wp-chip:not([aria-pressed=\"true\"]) { background: var(--wp-side-control); border-color: var(--wp-side-line); }\n .wp-filter-pop footer { display: flex; justify-content: space-between; align-items: center; gap: 8px;\n padding-top: 10px; border-top: 1px solid var(--wp-line); }\n .wp-text-btn { all: unset; cursor: pointer; color: var(--wp-muted); font-size: 12.5px; text-decoration: underline; }\n .wp-primary { all: unset; box-sizing: border-box; cursor: pointer; padding: 7px 14px; border-radius: 8px;\n background: var(--wp-accent); color: #fff; font-weight: 600; font-size: 13px; }\n .wp-primary:hover { background: var(--wp-accent-hover); }`;\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.\n *\n * The hover glow and the lock outline belong to the OUTER box only. A nested Runtime box (one\n * runtime plus a specialization) is an HTML-like table, so after the node's own outline Graphviz\n * emits a `<polygon>` for the base-colored cell and a `<path>` for the inner `STYLE=\"rounded\"`\n * table — matching only `polygon` here is what let the glow stroke the inner box (#1158). So the\n * glow is suppressed on EVERY shape after the node's first, whatever its element type.\n *\n * A striped (multi-runtime) box is the one exception to \"the first shape is the outline\":\n * Graphviz draws one filled polygon per stripe first and the outline LAST, as an unfilled polygon.\n * graph-color-modes.ts stamps those boxes `wp-striped`, and for them the outline is that last,\n * `fill=\"none\"` polygon; the stripes keep their thin dividers.\n */\n private graph(): string {\n const shape = ':is(polygon, ellipse, path)';\n const later = `${shape} ~ ${shape}`;\n const first = `${shape}:not(${later})`;\n const glow = 'stroke: var(--wp-accent); stroke-width: 5; filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));';\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 > ${later} { stroke: none; stroke-width: 0; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon:not([fill=\"none\"]) { stroke: #000000; stroke-width: 0.5; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon[fill=\"none\"] { ${glow} }\n #graph g.node.wp-locked:not(.${STRIPED_CLASS}) > ${first},\n #graph g.node.wp-locked.${STRIPED_CLASS} > polygon[fill=\"none\"] { stroke: var(--wp-accent); 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
+ {"version":3,"file":"graph-page-styles.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-page-styles.ts"],"names":[],"mappings":";;;AAAA,2DAAoD;AAEpD;;;;GAIG;AACH,MAAa,eAAe;IACxB,GAAG;QACC,OAAO,CACH,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,MAAM,EAAE;YACb,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,IAAI,EAAE;YACX,IAAI,CAAC,QAAQ,EAAE;YACf,IAAI,CAAC,aAAa,EAAE;YACpB,IAAI,CAAC,KAAK,EAAE;YACZ,IAAI,CAAC,KAAK,EAAE,CACf,CAAC;IACN,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;;;;;;;;;;uFAuBwE,CAAC;IACpF,CAAC;IAEO,MAAM;QACV,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mJAqCoI,CAAC;IAChJ,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;;;;;;;;;;mGAsBoF,CAAC;IAChG,CAAC;IAEO,IAAI;QACR,OAAO;;;;;;;;;;;;;;uDAcwC,CAAC;IACpD,CAAC;IAED,iFAAiF;IACzE,aAAa;QACjB,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;kEA4BmD,CAAC;IAC/D,CAAC;IAEO,QAAQ;QACZ,OAAO;;;;;;;;;;;;;uFAawE,CAAC;IACpF,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,KAAK;QACT,MAAM,KAAK,GAAG,6BAA6B,CAAC;QAC5C,MAAM,KAAK,GAAG,GAAG,KAAK,MAAM,KAAK,EAAE,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,KAAK,QAAQ,KAAK,GAAG,CAAC;QACvC,MAAM,IAAI,GAAG,mGAAmG,CAAC;QACjH,OAAO;;;6CAG8B,KAAK;qCACb,iCAAa;qCACb,iCAAa,mCAAmC,IAAI;uCAClD,iCAAa,OAAO,KAAK;kCAC9B,iCAAa,wEAAwE,CAAC;IACpH,CAAC;IAEO,KAAK;QACT,OAAO;;;;;;;;;;sCAUuB,CAAC;IACnC,CAAC;CACJ;AA1ND,0CA0NC","sourcesContent":["import { STRIPED_CLASS } from './graph-color-modes';\n\n/**\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 (\n this.base() +\n this.drawer() +\n this.controls() +\n this.main() +\n this.overlays() +\n this.filterPopover() +\n this.graph() +\n this.cards()\n );\n }\n\n private base(): string {\n return `\n :root {\n --wp-side-bg: #15161b; --wp-side-control: #1e1f25; --wp-side-fg: #e9eaf0; --wp-side-muted: #8b8fa0;\n --wp-side-hover: #26272f; --wp-side-line: #3a3c46; --wp-accent: #8b3cf0; --wp-accent-hover: #a46bf7;\n --wp-fg: #1b1e27; --wp-muted: #5d6475; --wp-line: #d9dce5; --wp-surface: #ffffff; --wp-canvas: #faf9f5;\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-pulldown { position: relative; }\n .wp-pill-trigger { all: unset; box-sizing: border-box; cursor: pointer; display: flex; align-items: center; gap: 10px;\n width: 100%; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); }\n .wp-pill-trigger:hover, .wp-pill-trigger[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-pill-text { flex: 1 1 auto; min-width: 0; }\n .wp-caret { color: var(--wp-side-muted); }\n .wp-menu { position: absolute; z-index: 35; left: 0; right: 0; top: calc(100% + 6px); display: grid; gap: 2px; padding: 6px;\n background: var(--wp-side-control); border: 1px solid var(--wp-side-line); border-radius: 12px; box-shadow: 0 10px 28px #0008; }\n .wp-mode { all: unset; box-sizing: border-box; cursor: pointer; display: block; padding: 6px 10px; border-radius: 8px; }\n .wp-mode:hover { background: var(--wp-side-hover); }\n .wp-mode[aria-checked=\"true\"] { background: #2a2333; box-shadow: inset 3px 0 0 var(--wp-accent); }\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-filter-btn { all: unset; box-sizing: border-box; cursor: pointer; display: flex; justify-content: space-between;\n align-items: center; padding: 7px 12px; border-radius: 8px; border: 1px solid var(--wp-side-line);\n background: var(--wp-side-control); font-size: 13px; font-weight: 600; }\n .wp-filter-btn:hover, .wp-filter-btn[aria-expanded=\"true\"] { border-color: var(--wp-accent-hover); }\n .wp-badge { min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--wp-accent); color: #fff;\n font-size: 11.5px; text-align: center; box-sizing: border-box; }\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: var(--wp-side-control); 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-pill-trigger:focus-visible, .wp-filter-btn:focus-visible, .wp-chip:focus-visible,\n .wp-primary:focus-visible, .wp-pill button: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-hover); 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 .wp-pills { display: flex; gap: 6px; flex-wrap: wrap; }\n .wp-pills:empty { display: none; }\n .wp-pill { display: inline-flex; align-items: center; gap: 4px; padding: 2px 4px 2px 10px; border-radius: 999px;\n background: #f1e8fe; color: #4a1d8a; border: 1px solid #d6bcfa; font-size: 12.5px; }\n .wp-pill button { all: unset; cursor: pointer; width: 18px; height: 18px; display: inline-grid; place-items: center;\n border-radius: 50%; color: #4a1d8a; }\n .wp-pill button:hover { background: #e2cffc; }`;\n }\n\n /** The Filter popover: three groups, chips, and the \"Show N projects\" footer. */\n private filterPopover(): string {\n return `\n .wp-filter-pop { left: 12px; top: 56px; width: min(380px, calc(100% - 24px)); max-height: calc(100% - 80px); overflow: auto; }\n .wp-fgroup { border: 0; margin: 0 0 12px; padding: 0; display: grid; gap: 4px; }\n .wp-fgroup legend { padding: 0; margin-bottom: 4px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;\n color: var(--wp-muted); font-weight: 600; }\n .wp-fgroup legend small { text-transform: none; letter-spacing: 0; font-weight: 400; }\n .wp-fgroup:disabled .wp-scope { opacity: 0.45; cursor: not-allowed; }\n .wp-scope { display: flex; align-items: center; gap: 8px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }\n .wp-scope:hover { background: #f4f1fb; }\n .wp-scope input { accent-color: var(--wp-accent); margin: 0; }\n .wp-scope-text { flex: 1 1 auto; display: grid; }\n .wp-scope-sub { font-size: 11.5px; color: var(--wp-muted); line-height: 1.3; }\n .wp-chip-swatch { vertical-align: -1px; margin-right: 5px; }\n .wp-count { color: var(--wp-muted); font-size: 12px; font-variant-numeric: tabular-nums; }\n .wp-scope-reason { margin: 2px 6px 0; font-size: 12px; color: var(--wp-muted); }\n .wp-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n .wp-chip { all: unset; box-sizing: border-box; cursor: pointer; padding: 3px 11px; border-radius: 999px;\n border: 1px solid var(--wp-line); background: var(--wp-surface); font-size: 12.5px; }\n .wp-chip:hover { border-color: var(--wp-accent-hover); }\n .wp-chip[aria-pressed=\"true\"] { background: var(--wp-accent); border-color: var(--wp-accent); color: #fff; }\n .wp-chip:disabled { opacity: 0.45; cursor: default; }\n .wp-impact-kinds { margin: 6px 0 4px; }\n .wp-side .wp-chip:not([aria-pressed=\"true\"]) { background: var(--wp-side-control); border-color: var(--wp-side-line); }\n .wp-filter-pop footer { display: flex; justify-content: space-between; align-items: center; gap: 8px;\n padding-top: 10px; border-top: 1px solid var(--wp-line); }\n .wp-text-btn { all: unset; cursor: pointer; color: var(--wp-muted); font-size: 12.5px; text-decoration: underline; }\n .wp-primary { all: unset; box-sizing: border-box; cursor: pointer; padding: 7px 14px; border-radius: 8px;\n background: var(--wp-accent); color: #fff; font-weight: 600; font-size: 13px; }\n .wp-primary:hover { background: var(--wp-accent-hover); }`;\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.\n *\n * The hover glow and the lock outline belong to the OUTER box only. A nested Runtime box (one\n * runtime plus a specialization) is an HTML-like table, so after the node's own outline Graphviz\n * emits a `<polygon>` for the base-colored cell and a `<path>` for the inner `STYLE=\"rounded\"`\n * table — matching only `polygon` here is what let the glow stroke the inner box (#1158). So the\n * glow is suppressed on EVERY shape after the node's first, whatever its element type.\n *\n * A striped (multi-runtime) box is the one exception to \"the first shape is the outline\":\n * Graphviz draws one filled polygon per stripe first and the outline LAST, as an unfilled polygon.\n * graph-color-modes.ts stamps those boxes `wp-striped`, and for them the outline is that last,\n * `fill=\"none\"` polygon; the stripes keep their thin dividers.\n */\n private graph(): string {\n const shape = ':is(polygon, ellipse, path)';\n const later = `${shape} ~ ${shape}`;\n const first = `${shape}:not(${later})`;\n const glow = 'stroke: var(--wp-accent); stroke-width: 5; filter: drop-shadow(0 0 6px rgba(139, 60, 240, 0.85));';\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 > ${later} { stroke: none; stroke-width: 0; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon:not([fill=\"none\"]) { stroke: #000000; stroke-width: 0.5; filter: none; }\n #graph g.wp-node-clickable.${STRIPED_CLASS}:hover > polygon[fill=\"none\"] { ${glow} }\n #graph g.node.wp-locked:not(.${STRIPED_CLASS}) > ${first},\n #graph g.node.wp-locked.${STRIPED_CLASS} > polygon[fill=\"none\"] { stroke: var(--wp-accent); 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"]}
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Products on the architecture graphs (#1179).
3
+ *
4
+ * A monorepo can hold several PRODUCTS, each with its own clients and servers. A product is declared
5
+ * on its entry points with the nx tag `product:<name>` (product-resolver.ts); everything else a
6
+ * product consists of is DERIVED here, never declared:
7
+ *
8
+ * - ProductMembership — the dependency graph. A project belongs to every product whose tagged project
9
+ * reaches it by a downward walk over `dependsOn`. That closure is everything that can change the
10
+ * product's behaviour, so it is what a reviewer of that product reviews. It must start from EVERY
11
+ * entry point, clients and servers both: a client reaches its server only over HTTP, never through
12
+ * an nx edge, so a server is in its product only because it is tagged.
13
+ * - RuntimeProductMembership — the runtime graph. The services in a product are its seeds plus every
14
+ * service reachable over RUNTIME `dependsOn`, including through a queue to its consumers — so a
15
+ * call from one product into another product's service shows, which is exactly the cross-product
16
+ * coupling a reviewer needs to see.
17
+ * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted
18
+ * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for
19
+ * one in no product.
20
+ *
21
+ * Membership is stable per checkout (it depends on tags and edges, never on a diff), so it is written
22
+ * into the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
23
+ */
24
+ import type { EnhancedGraph } from './graph-sorter';
25
+ import type { RuntimeGraph } from './runtime-graph-model';
26
+ /** One product's color, and the text color that reads on it. Data-only. */
27
+ export declare class ProductColor {
28
+ readonly product: string;
29
+ readonly color: string;
30
+ readonly fontColor: string;
31
+ /** False for a product past the palette's eight, which is painted with the neutral fill. */
32
+ readonly inPalette: boolean;
33
+ constructor(product: string, color: string, fontColor: string,
34
+ /** False for a product past the palette's eight, which is painted with the neutral fill. */
35
+ inPalette: boolean);
36
+ }
37
+ /** How many products get a color of their own; any further product uses the neutral fill. */
38
+ export declare const PRODUCT_PALETTE_SIZE: number;
39
+ /** A box shared by EVERY product, and every product past the palette's eight. */
40
+ export declare const PRODUCT_SHARED_FILL = "#c9ced8";
41
+ /** A box in no product: white, with a dashed border. */
42
+ export declare const PRODUCT_NONE_FILL = "#ffffff";
43
+ export declare const PRODUCT_NONE_BORDER = "#8a90a0";
44
+ export declare class ProductPalette {
45
+ /** Every declared product, sorted, each with its color. */
46
+ readonly colors: ProductColor[];
47
+ constructor(products: readonly string[]);
48
+ /** Every product carried by a project of the graph, as one palette. */
49
+ static fromGraph(graph: EnhancedGraph): ProductPalette;
50
+ names(): string[];
51
+ colorOf(product: string): ProductColor;
52
+ /** True for a box carrying every product of a workspace that declares two or more. */
53
+ sharedByAll(products: readonly string[]): boolean;
54
+ /** True when some product is past the palette's eight, so the legend must say it is neutral. */
55
+ overflows(): boolean;
56
+ }
57
+ /** The architecture graph's product membership: declared seeds walked DOWN over `dependsOn`. */
58
+ export declare class ProductMembership {
59
+ /**
60
+ * @param seeds project → the products it DECLARES (its `product:` tags)
61
+ * @returns project → the sorted products whose tagged projects reach it; a project in no product
62
+ * is absent, so with no tag anywhere the map is empty
63
+ */
64
+ compute(seeds: ReadonlyMap<string, readonly string[]>, graph: EnhancedGraph): Map<string, string[]>;
65
+ /** `seed` plus every project it reaches through `dependsOn`. */
66
+ private closure;
67
+ }
68
+ /** The runtime graph's product membership: tagged services walked over RUNTIME `dependsOn`. */
69
+ export declare class RuntimeProductMembership {
70
+ /**
71
+ * Stamp `products` onto every service of `graph` that some product reaches. Seeds are the
72
+ * services whose dependencies.json entry carries `products`; a `queue:<Api.method>` dependency is
73
+ * followed to that queue's consumers. A service in no product gets no field.
74
+ */
75
+ apply(graph: RuntimeGraph, projects: EnhancedGraph): void;
76
+ private closure;
77
+ /** The services one service calls: direct ones, and the consumers behind every queue it feeds. */
78
+ private callees;
79
+ }
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ /**
3
+ * Products on the architecture graphs (#1179).
4
+ *
5
+ * A monorepo can hold several PRODUCTS, each with its own clients and servers. A product is declared
6
+ * on its entry points with the nx tag `product:<name>` (product-resolver.ts); everything else a
7
+ * product consists of is DERIVED here, never declared:
8
+ *
9
+ * - ProductMembership — the dependency graph. A project belongs to every product whose tagged project
10
+ * reaches it by a downward walk over `dependsOn`. That closure is everything that can change the
11
+ * product's behaviour, so it is what a reviewer of that product reviews. It must start from EVERY
12
+ * entry point, clients and servers both: a client reaches its server only over HTTP, never through
13
+ * an nx edge, so a server is in its product only because it is tagged.
14
+ * - RuntimeProductMembership — the runtime graph. The services in a product are its seeds plus every
15
+ * service reachable over RUNTIME `dependsOn`, including through a queue to its consumers — so a
16
+ * call from one product into another product's service shows, which is exactly the cross-product
17
+ * coupling a reviewer needs to see.
18
+ * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted
19
+ * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for
20
+ * one in no product.
21
+ *
22
+ * Membership is stable per checkout (it depends on tags and edges, never on a diff), so it is written
23
+ * into the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.RuntimeProductMembership = exports.ProductMembership = exports.ProductPalette = exports.PRODUCT_NONE_BORDER = exports.PRODUCT_NONE_FILL = exports.PRODUCT_SHARED_FILL = exports.PRODUCT_PALETTE_SIZE = exports.ProductColor = void 0;
27
+ // The same two text colors graph-color-modes.ts writes on every fill. Spelled here rather than
28
+ // imported because graph-color-modes.ts imports THIS file (for the palette), and a file-import cycle
29
+ // is refused by `no-file-import-cycles`.
30
+ const DARK_TEXT = '#1a1c22';
31
+ const LIGHT_TEXT = '#ffffff';
32
+ /** One product's color, and the text color that reads on it. Data-only. */
33
+ class ProductColor {
34
+ product;
35
+ color;
36
+ fontColor;
37
+ inPalette;
38
+ constructor(product, color, fontColor,
39
+ /** False for a product past the palette's eight, which is painted with the neutral fill. */
40
+ inPalette) {
41
+ this.product = product;
42
+ this.color = color;
43
+ this.fontColor = fontColor;
44
+ this.inPalette = inPalette;
45
+ }
46
+ }
47
+ exports.ProductColor = ProductColor;
48
+ /** The eight product colors, in assignment order. Distinct from every runtime and role color. */
49
+ const PRODUCT_COLORS = [
50
+ ['#3f6fb5', LIGHT_TEXT],
51
+ ['#f28e2b', DARK_TEXT],
52
+ ['#c0392b', LIGHT_TEXT],
53
+ ['#5cb8b2', DARK_TEXT],
54
+ ['#3d8b3d', LIGHT_TEXT],
55
+ ['#edc948', DARK_TEXT],
56
+ ['#8e5ea2', LIGHT_TEXT],
57
+ ['#ff9da7', DARK_TEXT],
58
+ ];
59
+ /** How many products get a color of their own; any further product uses the neutral fill. */
60
+ exports.PRODUCT_PALETTE_SIZE = PRODUCT_COLORS.length;
61
+ /** A box shared by EVERY product, and every product past the palette's eight. */
62
+ exports.PRODUCT_SHARED_FILL = '#c9ced8';
63
+ /** A box in no product: white, with a dashed border. */
64
+ exports.PRODUCT_NONE_FILL = '#ffffff';
65
+ exports.PRODUCT_NONE_BORDER = '#8a90a0';
66
+ class ProductPalette {
67
+ /** Every declared product, sorted, each with its color. */
68
+ colors;
69
+ constructor(products) {
70
+ const sorted = [...new Set(products)].sort();
71
+ this.colors = sorted.map((product, index) => {
72
+ const entry = PRODUCT_COLORS[index];
73
+ if (entry === undefined)
74
+ return new ProductColor(product, exports.PRODUCT_SHARED_FILL, DARK_TEXT, false);
75
+ return new ProductColor(product, entry[0], entry[1], true);
76
+ });
77
+ }
78
+ /** Every product carried by a project of the graph, as one palette. */
79
+ // webpieces-disable no-function-outside-class -- static factory of this class
80
+ static fromGraph(graph) {
81
+ const all = [];
82
+ for (const entry of Object.values(graph))
83
+ all.push(...(entry.products ?? []));
84
+ return new ProductPalette(all);
85
+ }
86
+ names() {
87
+ return this.colors.map((color) => color.product);
88
+ }
89
+ colorOf(product) {
90
+ return (this.colors.find((color) => color.product === product) ??
91
+ new ProductColor(product, exports.PRODUCT_SHARED_FILL, DARK_TEXT, false));
92
+ }
93
+ /** True for a box carrying every product of a workspace that declares two or more. */
94
+ sharedByAll(products) {
95
+ return this.colors.length > 1 && this.colors.every((color) => products.includes(color.product));
96
+ }
97
+ /** True when some product is past the palette's eight, so the legend must say it is neutral. */
98
+ overflows() {
99
+ return this.colors.some((color) => !color.inPalette);
100
+ }
101
+ }
102
+ exports.ProductPalette = ProductPalette;
103
+ /** The architecture graph's product membership: declared seeds walked DOWN over `dependsOn`. */
104
+ class ProductMembership {
105
+ /**
106
+ * @param seeds project → the products it DECLARES (its `product:` tags)
107
+ * @returns project → the sorted products whose tagged projects reach it; a project in no product
108
+ * is absent, so with no tag anywhere the map is empty
109
+ */
110
+ compute(seeds, graph) {
111
+ const members = new Map();
112
+ seeds.forEach((products, seed) => {
113
+ if (products.length === 0 || graph[seed] === undefined)
114
+ return;
115
+ for (const project of this.closure(seed, graph)) {
116
+ const set = members.get(project) ?? new Set();
117
+ for (const product of products)
118
+ set.add(product);
119
+ members.set(project, set);
120
+ }
121
+ });
122
+ const result = new Map();
123
+ members.forEach((set, project) => {
124
+ result.set(project, [...set].sort());
125
+ });
126
+ return result;
127
+ }
128
+ /** `seed` plus every project it reaches through `dependsOn`. */
129
+ closure(seed, graph) {
130
+ const seen = new Set([seed]);
131
+ const stack = [seed];
132
+ while (stack.length > 0) {
133
+ const project = stack.pop();
134
+ for (const dep of graph[project]?.dependsOn ?? []) {
135
+ if (seen.has(dep))
136
+ continue;
137
+ seen.add(dep);
138
+ stack.push(dep);
139
+ }
140
+ }
141
+ return seen;
142
+ }
143
+ }
144
+ exports.ProductMembership = ProductMembership;
145
+ /** The runtime graph's product membership: tagged services walked over RUNTIME `dependsOn`. */
146
+ class RuntimeProductMembership {
147
+ /**
148
+ * Stamp `products` onto every service of `graph` that some product reaches. Seeds are the
149
+ * services whose dependencies.json entry carries `products`; a `queue:<Api.method>` dependency is
150
+ * followed to that queue's consumers. A service in no product gets no field.
151
+ */
152
+ apply(graph, projects) {
153
+ const members = new Map();
154
+ for (const name of Object.keys(graph.services)) {
155
+ const seeded = projects[name]?.products ?? [];
156
+ if (seeded.length === 0)
157
+ continue;
158
+ for (const reached of this.closure(name, graph)) {
159
+ const set = members.get(reached) ?? new Set();
160
+ for (const product of seeded)
161
+ set.add(product);
162
+ members.set(reached, set);
163
+ }
164
+ }
165
+ members.forEach((set, name) => {
166
+ const service = graph.services[name];
167
+ if (service !== undefined)
168
+ service.products = [...set].sort();
169
+ });
170
+ }
171
+ closure(seed, graph) {
172
+ const seen = new Set([seed]);
173
+ const stack = [seed];
174
+ while (stack.length > 0) {
175
+ const name = stack.pop();
176
+ for (const next of this.callees(name, graph)) {
177
+ if (seen.has(next) || graph.services[next] === undefined)
178
+ continue;
179
+ seen.add(next);
180
+ stack.push(next);
181
+ }
182
+ }
183
+ return seen;
184
+ }
185
+ /** The services one service calls: direct ones, and the consumers behind every queue it feeds. */
186
+ callees(name, graph) {
187
+ const callees = [];
188
+ for (const dep of graph.services[name]?.dependsOn ?? []) {
189
+ if (!dep.startsWith('queue:')) {
190
+ callees.push(dep);
191
+ continue;
192
+ }
193
+ callees.push(...(graph.queues[dep.slice('queue:'.length)]?.consumedBy ?? []));
194
+ }
195
+ return callees;
196
+ }
197
+ }
198
+ exports.RuntimeProductMembership = RuntimeProductMembership;
199
+ //# sourceMappingURL=graph-products.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph-products.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-products.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Products on the architecture graphs (#1179).\n *\n * A monorepo can hold several PRODUCTS, each with its own clients and servers. A product is declared\n * on its entry points with the nx tag `product:<name>` (product-resolver.ts); everything else a\n * product consists of is DERIVED here, never declared:\n *\n * - ProductMembership — the dependency graph. A project belongs to every product whose tagged project\n * reaches it by a downward walk over `dependsOn`. That closure is everything that can change the\n * product's behaviour, so it is what a reviewer of that product reviews. It must start from EVERY\n * entry point, clients and servers both: a client reaches its server only over HTTP, never through\n * an nx edge, so a server is in its product only because it is tagged.\n * - RuntimeProductMembership — the runtime graph. The services in a product are its seeds plus every\n * service reachable over RUNTIME `dependsOn`, including through a queue to its consumers — so a\n * call from one product into another product's service shows, which is exactly the cross-product\n * coupling a reviewer needs to see.\n * - ProductPalette — the Product color mode's colors: a fixed palette of eight assigned in sorted\n * product-name order, a neutral fill for a box shared by EVERY product, and a dashed white box for\n * one in no product.\n *\n * Membership is stable per checkout (it depends on tags and edges, never on a diff), so it is written\n * into the committed dependencies.json / runtime-dependencies.json rather than into a sidecar.\n */\n\nimport type { EnhancedGraph } from './graph-sorter';\nimport type { RuntimeGraph } from './runtime-graph-model';\n\n// The same two text colors graph-color-modes.ts writes on every fill. Spelled here rather than\n// imported because graph-color-modes.ts imports THIS file (for the palette), and a file-import cycle\n// is refused by `no-file-import-cycles`.\nconst DARK_TEXT = '#1a1c22';\nconst LIGHT_TEXT = '#ffffff';\n\n/** One product's color, and the text color that reads on it. Data-only. */\nexport class ProductColor {\n constructor(\n public readonly product: string,\n public readonly color: string,\n public readonly fontColor: string,\n /** False for a product past the palette's eight, which is painted with the neutral fill. */\n public readonly inPalette: boolean,\n ) {}\n}\n\n/** The eight product colors, in assignment order. Distinct from every runtime and role color. */\nconst PRODUCT_COLORS: readonly (readonly [string, string])[] = [\n ['#3f6fb5', LIGHT_TEXT],\n ['#f28e2b', DARK_TEXT],\n ['#c0392b', LIGHT_TEXT],\n ['#5cb8b2', DARK_TEXT],\n ['#3d8b3d', LIGHT_TEXT],\n ['#edc948', DARK_TEXT],\n ['#8e5ea2', LIGHT_TEXT],\n ['#ff9da7', DARK_TEXT],\n];\n\n/** How many products get a color of their own; any further product uses the neutral fill. */\nexport const PRODUCT_PALETTE_SIZE = PRODUCT_COLORS.length;\n\n/** A box shared by EVERY product, and every product past the palette's eight. */\nexport const PRODUCT_SHARED_FILL = '#c9ced8';\n/** A box in no product: white, with a dashed border. */\nexport const PRODUCT_NONE_FILL = '#ffffff';\nexport const PRODUCT_NONE_BORDER = '#8a90a0';\n\nexport class ProductPalette {\n /** Every declared product, sorted, each with its color. */\n readonly colors: ProductColor[];\n\n constructor(products: readonly string[]) {\n const sorted = [...new Set(products)].sort();\n this.colors = sorted.map((product: string, index: number): ProductColor => {\n const entry = PRODUCT_COLORS[index];\n if (entry === undefined) return new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false);\n return new ProductColor(product, entry[0], entry[1], true);\n });\n }\n\n /** Every product carried by a project of the graph, as one palette. */\n // webpieces-disable no-function-outside-class -- static factory of this class\n static fromGraph(graph: EnhancedGraph): ProductPalette {\n const all: string[] = [];\n for (const entry of Object.values(graph)) all.push(...(entry.products ?? []));\n return new ProductPalette(all);\n }\n\n names(): string[] {\n return this.colors.map((color: ProductColor): string => color.product);\n }\n\n colorOf(product: string): ProductColor {\n return (\n this.colors.find((color: ProductColor): boolean => color.product === product) ??\n new ProductColor(product, PRODUCT_SHARED_FILL, DARK_TEXT, false)\n );\n }\n\n /** True for a box carrying every product of a workspace that declares two or more. */\n sharedByAll(products: readonly string[]): boolean {\n return this.colors.length > 1 && this.colors.every((color: ProductColor): boolean => products.includes(color.product));\n }\n\n /** True when some product is past the palette's eight, so the legend must say it is neutral. */\n overflows(): boolean {\n return this.colors.some((color: ProductColor): boolean => !color.inPalette);\n }\n}\n\n/** The architecture graph's product membership: declared seeds walked DOWN over `dependsOn`. */\nexport class ProductMembership {\n /**\n * @param seeds project → the products it DECLARES (its `product:` tags)\n * @returns project → the sorted products whose tagged projects reach it; a project in no product\n * is absent, so with no tag anywhere the map is empty\n */\n compute(seeds: ReadonlyMap<string, readonly string[]>, graph: EnhancedGraph): Map<string, string[]> {\n const members = new Map<string, Set<string>>();\n seeds.forEach((products: readonly string[], seed: string): void => {\n if (products.length === 0 || graph[seed] === undefined) return;\n for (const project of this.closure(seed, graph)) {\n const set = members.get(project) ?? new Set<string>();\n for (const product of products) set.add(product);\n members.set(project, set);\n }\n });\n const result = new Map<string, string[]>();\n members.forEach((set: Set<string>, project: string): void => {\n result.set(project, [...set].sort());\n });\n return result;\n }\n\n /** `seed` plus every project it reaches through `dependsOn`. */\n private closure(seed: string, graph: EnhancedGraph): Set<string> {\n const seen = new Set<string>([seed]);\n const stack = [seed];\n while (stack.length > 0) {\n const project = stack.pop() as string;\n for (const dep of graph[project]?.dependsOn ?? []) {\n if (seen.has(dep)) continue;\n seen.add(dep);\n stack.push(dep);\n }\n }\n return seen;\n }\n}\n\n/** The runtime graph's product membership: tagged services walked over RUNTIME `dependsOn`. */\nexport class RuntimeProductMembership {\n /**\n * Stamp `products` onto every service of `graph` that some product reaches. Seeds are the\n * services whose dependencies.json entry carries `products`; a `queue:<Api.method>` dependency is\n * followed to that queue's consumers. A service in no product gets no field.\n */\n apply(graph: RuntimeGraph, projects: EnhancedGraph): void {\n const members = new Map<string, Set<string>>();\n for (const name of Object.keys(graph.services)) {\n const seeded = projects[name]?.products ?? [];\n if (seeded.length === 0) continue;\n for (const reached of this.closure(name, graph)) {\n const set = members.get(reached) ?? new Set<string>();\n for (const product of seeded) set.add(product);\n members.set(reached, set);\n }\n }\n members.forEach((set: Set<string>, name: string): void => {\n const service = graph.services[name];\n if (service !== undefined) service.products = [...set].sort();\n });\n }\n\n private closure(seed: string, graph: RuntimeGraph): Set<string> {\n const seen = new Set<string>([seed]);\n const stack = [seed];\n while (stack.length > 0) {\n const name = stack.pop() as string;\n for (const next of this.callees(name, graph)) {\n if (seen.has(next) || graph.services[next] === undefined) continue;\n seen.add(next);\n stack.push(next);\n }\n }\n return seen;\n }\n\n /** The services one service calls: direct ones, and the consumers behind every queue it feeds. */\n private callees(name: string, graph: RuntimeGraph): string[] {\n const callees: string[] = [];\n for (const dep of graph.services[name]?.dependsOn ?? []) {\n if (!dep.startsWith('queue:')) {\n callees.push(dep);\n continue;\n }\n callees.push(...(graph.queues[dep.slice('queue:'.length)]?.consumedBy ?? []));\n }\n return callees;\n }\n}\n"]}
@@ -6,13 +6,17 @@ import { LegendFacts, NodeFacts, NodeModeDots } from './graph-color-modes';
6
6
  * `dot` is the statement `fullDot` carries. `modes` and `tags` are set only by the architecture
7
7
  * graph, whose page switches color modes by re-rendering each box from its per-mode statement and
8
8
  * filters boxes by their role and framework tags; the runtime graph has one look and leaves both null.
9
+ *
10
+ * `products` is set on BOTH graphs (#1179): the products a box belongs to, which each page's product
11
+ * filter matches on. Empty for a box in no product.
9
12
  */
10
13
  export declare class RenderNode {
11
14
  readonly id: string;
12
15
  readonly dot: string;
13
16
  readonly modes: NodeModeDots | null;
14
17
  readonly tags: NodeTags | null;
15
- constructor(id: string, dot: string, modes: NodeModeDots | null, tags: NodeTags | null);
18
+ readonly products: string[];
19
+ constructor(id: string, dot: string, modes: NodeModeDots | null, tags: NodeTags | null, products: string[]);
16
20
  }
17
21
  /** What the architecture page's Filter matches a box on: its level, role and framework tags. */
18
22
  export declare class NodeTags {
@@ -38,7 +42,14 @@ export declare class GraphRenderModel {
38
42
  footer: string;
39
43
  /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */
40
44
  legend: LegendFacts;
41
- node(id: string, dot: string): string;
45
+ node(id: string, dot: string, products?: string[]): string;
46
+ /**
47
+ * Every product a declared box (a queue, a cron trigger, an external system) inherits from the
48
+ * services its edges touch (#1179): it belongs to whichever product's services it is attached to.
49
+ * Called by the runtime graph once every edge is emitted; RenderNode is immutable, so a node gains
50
+ * its products by being replaced.
51
+ */
52
+ attachProducts(isService: (id: string) => boolean): void;
42
53
  /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */
43
54
  styledNode(facts: NodeFacts, modes: NodeModeDots): string;
44
55
  completeEndpoints(): void;
@@ -9,17 +9,22 @@ const graph_color_modes_1 = require("./graph-color-modes");
9
9
  * `dot` is the statement `fullDot` carries. `modes` and `tags` are set only by the architecture
10
10
  * graph, whose page switches color modes by re-rendering each box from its per-mode statement and
11
11
  * filters boxes by their role and framework tags; the runtime graph has one look and leaves both null.
12
+ *
13
+ * `products` is set on BOTH graphs (#1179): the products a box belongs to, which each page's product
14
+ * filter matches on. Empty for a box in no product.
12
15
  */
13
16
  class RenderNode {
14
17
  id;
15
18
  dot;
16
19
  modes;
17
20
  tags;
18
- constructor(id, dot, modes, tags) {
21
+ products;
22
+ constructor(id, dot, modes, tags, products) {
19
23
  this.id = id;
20
24
  this.dot = dot;
21
25
  this.modes = modes;
22
26
  this.tags = tags;
27
+ this.products = products;
23
28
  }
24
29
  }
25
30
  exports.RenderNode = RenderNode;
@@ -58,13 +63,40 @@ class GraphRenderModel {
58
63
  footer = '';
59
64
  /** The colors and roles in use, so the page's legend lists exactly those (architecture graph). */
60
65
  legend = new graph_color_modes_1.LegendFacts();
61
- node(id, dot) {
62
- this.nodes.push(new RenderNode(id, dot, null, null));
66
+ node(id, dot, products = []) {
67
+ this.nodes.push(new RenderNode(id, dot, null, null, products));
63
68
  return dot;
64
69
  }
70
+ /**
71
+ * Every product a declared box (a queue, a cron trigger, an external system) inherits from the
72
+ * services its edges touch (#1179): it belongs to whichever product's services it is attached to.
73
+ * Called by the runtime graph once every edge is emitted; RenderNode is immutable, so a node gains
74
+ * its products by being replaced.
75
+ */
76
+ attachProducts(isService) {
77
+ const byId = new Map();
78
+ for (const node of this.nodes)
79
+ byId.set(node.id, node);
80
+ for (let index = 0; index < this.nodes.length; index++) {
81
+ const node = this.nodes[index];
82
+ if (isService(node.id))
83
+ continue;
84
+ const inherited = new Set();
85
+ for (const edge of this.edges) {
86
+ const other = edge.from === node.id ? edge.to : edge.to === node.id ? edge.from : null;
87
+ if (other === null || !isService(other))
88
+ continue;
89
+ for (const product of byId.get(other)?.products ?? [])
90
+ inherited.add(product);
91
+ }
92
+ if (inherited.size === 0)
93
+ continue;
94
+ this.nodes[index] = new RenderNode(node.id, node.dot, node.modes, node.tags, [...inherited].sort());
95
+ }
96
+ }
65
97
  /** A box drawn differently per color mode; `fullDot` carries its RUNTIME statement. */
66
98
  styledNode(facts, modes) {
67
- this.nodes.push(new RenderNode(facts.nodeId, modes.runtime, modes, new NodeTags(facts.level, facts.role, facts.frameworks)));
99
+ this.nodes.push(new RenderNode(facts.nodeId, modes.runtime, modes, new NodeTags(facts.level, facts.role, facts.frameworks), facts.products));
68
100
  return modes.runtime;
69
101
  }
70
102
  completeEndpoints() {