@webpieces/nx-webpieces-rules 0.4.870 → 0.4.872
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/package.json +7 -7
- package/src/executors/generate/executor.d.ts +3 -1
- package/src/executors/generate/executor.js +9 -1
- package/src/executors/generate/executor.js.map +1 -1
- package/src/index.d.ts +1 -0
- package/src/index.js +5 -1
- package/src/index.js.map +1 -1
- package/src/lib/di-graph/runtime-client-bindings.d.ts +5 -1
- package/src/lib/di-graph/runtime-client-bindings.js +21 -5
- package/src/lib/di-graph/runtime-client-bindings.js.map +1 -1
- package/src/lib/dot-syntax.d.ts +9 -0
- package/src/lib/dot-syntax.js +17 -0
- package/src/lib/dot-syntax.js.map +1 -1
- package/src/lib/graph-color-modes.d.ts +136 -0
- package/src/lib/graph-color-modes.js +299 -0
- package/src/lib/graph-color-modes.js.map +1 -0
- package/src/lib/graph-filter.client.d.ts +11 -1
- package/src/lib/graph-filter.client.js +25 -4
- package/src/lib/graph-filter.client.js.map +1 -1
- package/src/lib/graph-impact.d.ts +119 -0
- package/src/lib/graph-impact.js +295 -0
- package/src/lib/graph-impact.js.map +1 -0
- package/src/lib/graph-legend.d.ts +34 -0
- package/src/lib/graph-legend.js +116 -0
- package/src/lib/graph-legend.js.map +1 -0
- package/src/lib/graph-names.d.ts +2 -2
- package/src/lib/graph-names.js +2 -2
- package/src/lib/graph-names.js.map +1 -1
- package/src/lib/graph-navigation.d.ts +10 -0
- package/src/lib/graph-navigation.js +47 -12
- package/src/lib/graph-navigation.js.map +1 -1
- package/src/lib/graph-node-menu.d.ts +4 -3
- package/src/lib/graph-node-menu.js +137 -20
- package/src/lib/graph-node-menu.js.map +1 -1
- package/src/lib/graph-page-shell.d.ts +34 -0
- package/src/lib/graph-page-shell.js +121 -0
- package/src/lib/graph-page-shell.js.map +1 -0
- package/src/lib/graph-page-styles.d.ts +20 -0
- package/src/lib/graph-page-styles.js +144 -0
- package/src/lib/graph-page-styles.js.map +1 -0
- package/src/lib/graph-render-model.d.ts +14 -2
- package/src/lib/graph-render-model.js +19 -3
- package/src/lib/graph-render-model.js.map +1 -1
- package/src/lib/graph-responsibilities.js +1 -1
- package/src/lib/graph-responsibilities.js.map +1 -1
- package/src/lib/graph-visualizer.client.d.ts +65 -0
- package/src/lib/graph-visualizer.client.js +311 -7
- package/src/lib/graph-visualizer.client.js.map +1 -1
- package/src/lib/graph-visualizer.d.ts +19 -31
- package/src/lib/graph-visualizer.js +50 -286
- package/src/lib/graph-visualizer.js.map +1 -1
- package/src/lib/runtime-html-page.js +2 -2
- package/src/lib/runtime-html-page.js.map +1 -1
- package/src/lib/runtime-wiring/source-extractor.d.ts +2 -1
- package/src/lib/runtime-wiring/source-extractor.js +3 -2
- package/src/lib/runtime-wiring/source-extractor.js.map +1 -1
- package/src/lib/runtime-wiring/wiring-format.js +2 -2
- package/src/lib/runtime-wiring/wiring-format.js.map +1 -1
- package/src/rule-help.js +1 -1
- package/src/rule-help.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;;AAEH,+CAAyB;AACzB,mDAA6B;AAE7B,+CAA2C;AAE3C;;;;GAIG;AACH,MAAM,iBAAiB;IACnB,MAAM,GAAG,KAAK,CAAC;CAClB;AAED,MAAa,wBAAwB;IAChB,KAAK,GAAG,IAAI,wBAAU,EAAE,CAAC;IAE1C;;;;OAIG;IACH,eAAe,CAAC,KAAoB,EAAE,aAAqB;QACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAS,EAAE,CAAS,EAAU,EAAE;YAC3C,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;YAClD,IAAI,SAAS,KAAK,CAAC;gBAAE,OAAO,SAAS,CAAC;YACtC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC7B,qEAAqE;YACrE,4CAA4C;YAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,WAAW,KAAK,KAAK;gBAAE,SAAS;YACnD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC;QACxE,CAAC;QAED,OAAO,CACH,oCAAoC;YACpC,8CAA8C;YAC9C,+FAA+F;YAC/F,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC;YAChB,YAAY,CACf,CAAC;IACN,CAAC;IAEO,UAAU,CAAC,OAAe,EAAE,KAAiB,EAAE,aAAqB;QACxE,yFAAyF;QACzF,0FAA0F;QAC1F,4EAA4E;QAC5E,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,KAAK,CAAC,gBAAgB,IAAI,EAAE,CAAC;QAC7C,MAAM,WAAW,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,CAAC;QACjD,OAAO,CACH,4CAA4C,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI;YACvE,yCAAyC,KAAK,CAAC,KAAK,UAAU;YAC9D,WAAW,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,YAAY,WAAW,YAAY;YACxE,6BAA6B,IAAI,QAAQ;YACzC,YAAY,CACf,CAAC;IACN,CAAC;IAED;;;;OAIG;IACK,QAAQ,CAAC,KAAiB,EAAE,aAAqB;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,oBAAoB,CAAC;QACxC,IAAI,CAAC,IAAI;YAAE,OAAO,kEAAkE,CAAC;QACrF,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC/B,OAAO,kBAAkB,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,YAAY,CAAC;QAC/D,CAAC;QACD,OAAO,IAAI,CAAC,cAAc,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;;;OAMG;IACK,cAAc,CAAC,QAAgB;QACnC,MAAM,GAAG,GAAa,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,iBAAiB,EAAE,CAAC;QACtC,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAChD,CAAC;QACD,IAAI,KAAK,CAAC,MAAM;YAAE,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpC,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAEO,UAAU,CAAC,IAAY,EAAE,GAAa,EAAE,KAAwB;QACpE,MAAM,OAAO,GAAG,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpB,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;YAAC,CAAC;YAC9D,OAAO;QACX,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACV,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;YAAC,CAAC;YAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,+BAA+B;YACjF,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC;YACrF,OAAO;QACX,CAAC;QACD,IAAI,MAAM,EAAE,CAAC;YACT,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC;YAAC,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;YACtE,OAAO;QACX,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAAC,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;QAAC,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC;IACnE,CAAC;IAED;;;OAGG;IACK,YAAY,CAAC,OAAe;QAChC,OAAO,OAAO;aACT,OAAO,CAAC,YAAY,EAAE,iBAAiB,CAAC;aACxC,OAAO,CAAC,kBAAkB,EAAE,qBAAqB,CAAC;aAClD,OAAO,CAAC,sBAAsB,EAAE,eAAe,CAAC,CAAC;IAC1D,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAY;QAC3B,OAAO,IAAI;aACN,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;aACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;IACjC,CAAC;CACJ;AA9HD,4DA8HC","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<projectKey>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box above to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n // data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the\n // heading stays the pretty short name. Keying the attribute on the short name is what let\n // two same-named projects share one card's visibility — see graph-names.ts.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(nodeId)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"graph-responsibilities.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-responsibilities.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Responsibilities Section\n *\n * Renders every module's full `responsibilities.md` as a collapsible card below\n * the dependency graph in architecture/dependencies.html. Cards are ordered by\n * dependency level HIGH → LOW (top-level apps first, deepest libs last) so a\n * reader scrolls from the runnable servers/clients down into the libraries.\n *\n * Each card carries `data-node=\"<projectKey>\"` matching the graph box's title, so\n * the page script can filter the list to just the locked box's chain (see\n * graph-visualizer.client.ts). The summary paragraph is the `shortDescription`\n * already on each graph entry; the expanded body is the rendered markdown file.\n */\n\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport type { EnhancedGraph, GraphEntry } from './graph-sorter';\nimport { GraphNames } from './graph-names';\n\n/**\n * Mutable cursor state threaded through the markdown line renderer: whether we\n * are currently inside an open `<ul>` (so the next blank line / heading / non-\n * bullet closes it).\n */\nclass MarkdownListState {\n inList = false;\n}\n\nexport class ResponsibilitiesRenderer {\n private readonly names = new GraphNames();\n\n /**\n * Build the responsibilities section HTML: one collapsible card per module,\n * sorted by level descending (tie-break by name ascending). Returned as a\n * <section> that GraphVisualizer injects below the graph.\n */\n generateSection(graph: EnhancedGraph, workspaceRoot: string): string {\n const projects = Object.keys(graph);\n projects.sort((a: string, b: string): number => {\n const levelDiff = graph[b].level - graph[a].level;\n if (levelDiff !== 0) return levelDiff;\n return a.localeCompare(b);\n });\n\n const cards: string[] = [];\n for (const project of projects) {\n // Hidden projects (drawOnGraph:false) are omitted from the graph, so\n // they get no responsibilities card either.\n if (graph[project].drawOnGraph === false) continue;\n cards.push(this.renderCard(project, graph[project], workspaceRoot));\n }\n\n return (\n `<section id=\"wp-responsibilities\">` +\n `<h2>Responsibilities (level high → low)</h2>` +\n `<p class=\"hint\">Lock a box (the Focus field, or a box's menu) to narrow this list to just that box's dependency chain.</p>` +\n cards.join('\\n') +\n `</section>`\n );\n }\n\n private renderCard(project: string, entry: GraphEntry, workspaceRoot: string): string {\n // data-node is the graph box's IDENTITY (the project key, matching the SVG <title>); the\n // heading stays the pretty short name. Keying the attribute on the short name is what let\n // two same-named projects share one card's visibility — see graph-names.ts.\n const nodeId = this.names.getNodeId(project);\n const shortName = this.names.getShortName(project);\n const summary = entry.shortDescription ?? '';\n const summaryHtml = summary ? ` — ${this.escapeHtml(summary)}` : '';\n const body = this.readBody(entry, workspaceRoot);\n return (\n `<details class=\"wp-resp-card\" data-node=\"${this.escapeHtml(nodeId)}\">` +\n `<summary><span class=\"wp-resp-level\">L${entry.level}</span> ` +\n `<strong>${this.escapeHtml(shortName)}</strong>${summaryHtml}</summary>` +\n `<div class=\"wp-resp-body\">${body}</div>` +\n `</details>`\n );\n }\n\n /**\n * Read a module's responsibilities.md body. Generation already guarantees the\n * file exists (metadata validation throws otherwise); a fallback line guards\n * the edge case where the graph JSON is stale relative to disk.\n */\n private readBody(entry: GraphEntry, workspaceRoot: string): string {\n const file = entry.responsibilitiesFile;\n if (!file) return '<p><em>No responsibilities.md recorded for this module.</em></p>';\n const absolutePath = path.join(workspaceRoot, file);\n if (!fs.existsSync(absolutePath)) {\n return `<p><em>Missing ${this.escapeHtml(file)}.</em></p>`;\n }\n return this.renderMarkdown(fs.readFileSync(absolutePath, 'utf-8'));\n }\n\n /**\n * Minimal markdown → HTML for a responsibilities.md body. Handles ATX headings\n * (`#`..`######`), `-`/`*` bullet lists, and blank-line-separated paragraphs.\n * Everything is HTML-escaped first; only a small inline set is re-marked. This\n * is deliberately tiny — responsibilities.md files are short and structured,\n * and the repo has no markdown dependency.\n */\n private renderMarkdown(markdown: string): string {\n const out: string[] = [];\n const state = new MarkdownListState();\n for (const rawLine of markdown.split('\\n')) {\n this.renderLine(rawLine.trim(), out, state);\n }\n if (state.inList) out.push('</ul>');\n return out.join('\\n');\n }\n\n private renderLine(line: string, out: string[], state: MarkdownListState): void {\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line);\n const bullet = /^[-*]\\s+(.*)$/.exec(line);\n if (line.length === 0) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n return;\n }\n if (heading) {\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n const level = Math.min(heading[1].length + 3, 6); // # → h4, ## → h5, deeper → h6\n out.push(`<h${level}>${this.renderInline(this.escapeHtml(heading[2]))}</h${level}>`);\n return;\n }\n if (bullet) {\n if (!state.inList) { out.push('<ul>'); state.inList = true; }\n out.push(`<li>${this.renderInline(this.escapeHtml(bullet[1]))}</li>`);\n return;\n }\n if (state.inList) { out.push('</ul>'); state.inList = false; }\n out.push(`<p>${this.renderInline(this.escapeHtml(line))}</p>`);\n }\n\n /**\n * Render inline markdown spans within an already HTML-escaped line: `code` →\n * <code>, **bold** → <strong>, *italic* → <em>.\n */\n private renderInline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/(^|[^*])\\*([^*]+)\\*/g, '$1<em>$2</em>');\n }\n\n /**\n * Escape a string for safe embedding in HTML text/attributes.\n */\n private escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n }\n}\n"]}
|
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
|
|
3
|
+
*
|
|
4
|
+
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered
|
|
5
|
+
* one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,
|
|
6
|
+
* blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
|
|
7
|
+
* without it.
|
|
8
|
+
*/
|
|
9
|
+
declare class WpModeState {
|
|
10
|
+
static readonly MODES: string[];
|
|
11
|
+
static readonly NAMES: string[];
|
|
12
|
+
static readonly STORAGE_KEY = "wp-architecture-graph-mode";
|
|
13
|
+
readonly impact: ImpactJson | null;
|
|
14
|
+
mode: string;
|
|
15
|
+
constructor();
|
|
16
|
+
/** '' when Impact can be shown, else the one line the drawer and the menu say instead. */
|
|
17
|
+
impactReason(): string;
|
|
18
|
+
available(mode: string): boolean;
|
|
19
|
+
name(mode: string): string;
|
|
20
|
+
/** False (and nothing changes) for an unknown or unavailable mode. */
|
|
21
|
+
set(mode: string): boolean;
|
|
22
|
+
/** Which impact shade a box gets. */
|
|
23
|
+
status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched';
|
|
24
|
+
private initial;
|
|
25
|
+
private stored;
|
|
26
|
+
}
|
|
1
27
|
/** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
|
|
2
28
|
declare class GraphHighlighter {
|
|
3
29
|
private readonly svg;
|
|
@@ -13,12 +39,51 @@ declare class GraphHighlighter {
|
|
|
13
39
|
private wireHover;
|
|
14
40
|
private wireMenu;
|
|
15
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* The drawer and its floating panels: every control outside the graph itself. Holds no graph state —
|
|
44
|
+
* each control calls back into the page, and `sync()` repaints the controls from the page's state.
|
|
45
|
+
*/
|
|
46
|
+
declare class GraphDrawer {
|
|
47
|
+
private readonly page;
|
|
48
|
+
constructor(page: GraphPage);
|
|
49
|
+
wire(): void;
|
|
50
|
+
/** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
|
|
51
|
+
private wireLock;
|
|
52
|
+
/**
|
|
53
|
+
* Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the
|
|
54
|
+
* menu's own Escape handler closes it: Esc with a menu open only closes the menu.
|
|
55
|
+
*/
|
|
56
|
+
private onKey;
|
|
57
|
+
/** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */
|
|
58
|
+
private toggles;
|
|
59
|
+
/** Repaint every control from the page's state. */
|
|
60
|
+
sync(modes: WpModeState, locked: string | null, filtering: boolean): void;
|
|
61
|
+
private syncFilterToggle;
|
|
62
|
+
private impactNote;
|
|
63
|
+
/** "Mode: <b>Runtime</b> · Locked: <b>x</b>", built with text nodes so no name is parsed as HTML. */
|
|
64
|
+
private crumb;
|
|
65
|
+
private lockInput;
|
|
66
|
+
private byId;
|
|
67
|
+
}
|
|
16
68
|
declare class GraphPage extends WpFilterPage {
|
|
17
69
|
private highlighter;
|
|
70
|
+
private readonly modes;
|
|
71
|
+
private readonly drawer;
|
|
18
72
|
protected captureBinding(_svg: SVGSVGElement | null): () => void;
|
|
19
73
|
protected wireControls(): void;
|
|
74
|
+
protected nodeDot(node: RenderNodeJson): string;
|
|
75
|
+
protected usesFullDot(): boolean;
|
|
20
76
|
protected wireSvg(svg: SVGSVGElement): void;
|
|
21
77
|
lockSelection(): string | null;
|
|
78
|
+
hasNode(id: string): boolean;
|
|
79
|
+
/** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
|
|
80
|
+
resolveName(text: string): string | null;
|
|
22
81
|
setLock(name: string | null): void;
|
|
82
|
+
setMode(mode: string): void;
|
|
83
|
+
/** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
|
|
84
|
+
toggleFilter(): void;
|
|
85
|
+
/** The node menu's "Mode ▸" submenu: one radio item per mode, Impact disabled with its reason. */
|
|
86
|
+
modeItem(): WpNodeMenuItem;
|
|
87
|
+
private syncControls;
|
|
23
88
|
private filterCards;
|
|
24
89
|
}
|
|
@@ -1,4 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.
|
|
4
|
+
*
|
|
5
|
+
* The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered
|
|
6
|
+
* one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,
|
|
7
|
+
* blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
|
|
8
|
+
* without it.
|
|
9
|
+
*/
|
|
10
|
+
class WpModeState {
|
|
11
|
+
static MODES = ['runtime', 'architecture', 'impact'];
|
|
12
|
+
static NAMES = ['Runtime', 'Architecture', 'Impact'];
|
|
13
|
+
static STORAGE_KEY = 'wp-architecture-graph-mode';
|
|
14
|
+
impact = window.__WP_IMPACT__ ?? null;
|
|
15
|
+
mode = 'runtime';
|
|
16
|
+
constructor() {
|
|
17
|
+
this.mode = this.initial();
|
|
18
|
+
}
|
|
19
|
+
/** '' when Impact can be shown, else the one line the drawer and the menu say instead. */
|
|
20
|
+
impactReason() {
|
|
21
|
+
if (this.impact === null)
|
|
22
|
+
return 'No impact data for this branch. Run pnpm nx run architecture:generate to compute it.';
|
|
23
|
+
return this.impact.available ? '' : `Impact unavailable: ${this.impact.reason}.`;
|
|
24
|
+
}
|
|
25
|
+
available(mode) {
|
|
26
|
+
return WpModeState.MODES.includes(mode) && (mode !== 'impact' || this.impactReason() === '');
|
|
27
|
+
}
|
|
28
|
+
name(mode) {
|
|
29
|
+
return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;
|
|
30
|
+
}
|
|
31
|
+
/** False (and nothing changes) for an unknown or unavailable mode. */
|
|
32
|
+
set(mode) {
|
|
33
|
+
if (!this.available(mode))
|
|
34
|
+
return false;
|
|
35
|
+
this.mode = mode;
|
|
36
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(WpModeState.STORAGE_KEY, mode);
|
|
39
|
+
if (location.hash !== `#${mode}`)
|
|
40
|
+
history.replaceState(null, '', `#${mode}`);
|
|
41
|
+
// webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
//const error = toError(err);
|
|
45
|
+
void err;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
/** Which impact shade a box gets. */
|
|
50
|
+
status(id) {
|
|
51
|
+
if (this.impact?.touched.includes(id))
|
|
52
|
+
return 'touched';
|
|
53
|
+
if (this.impact?.affected.includes(id))
|
|
54
|
+
return 'affected';
|
|
55
|
+
if (this.impact?.buildInputs.includes(id))
|
|
56
|
+
return 'buildInput';
|
|
57
|
+
return 'untouched';
|
|
58
|
+
}
|
|
59
|
+
initial() {
|
|
60
|
+
const hashed = location.hash.slice(1);
|
|
61
|
+
if (this.available(hashed))
|
|
62
|
+
return hashed;
|
|
63
|
+
const stored = this.stored();
|
|
64
|
+
if (stored !== null && this.available(stored))
|
|
65
|
+
return stored;
|
|
66
|
+
return 'runtime';
|
|
67
|
+
}
|
|
68
|
+
stored() {
|
|
69
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as "nothing stored"
|
|
70
|
+
try {
|
|
71
|
+
return localStorage.getItem(WpModeState.STORAGE_KEY);
|
|
72
|
+
// webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
//const error = toError(err);
|
|
76
|
+
void err;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
2
81
|
/** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */
|
|
3
82
|
class GraphHighlighter {
|
|
4
83
|
svg;
|
|
@@ -70,7 +149,9 @@ class GraphHighlighter {
|
|
|
70
149
|
});
|
|
71
150
|
}
|
|
72
151
|
wireMenu() {
|
|
73
|
-
const designs = new Map(
|
|
152
|
+
const designs = new Map();
|
|
153
|
+
for (const link of __DESIGN_LINKS__)
|
|
154
|
+
designs.set(link.nodeId, link.href);
|
|
74
155
|
WpNodeMenu.wire(this.svg, (name) => {
|
|
75
156
|
const items = [];
|
|
76
157
|
const href = designs.get(name);
|
|
@@ -79,24 +160,210 @@ class GraphHighlighter {
|
|
|
79
160
|
const locked = this.page.lockSelection() === name;
|
|
80
161
|
items.push(new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () => this.page.setLock(locked ? null : name)));
|
|
81
162
|
items.push(this.page.filterItem(name));
|
|
163
|
+
items.push(this.page.modeItem());
|
|
82
164
|
return items;
|
|
83
165
|
});
|
|
84
166
|
}
|
|
85
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The drawer and its floating panels: every control outside the graph itself. Holds no graph state —
|
|
170
|
+
* each control calls back into the page, and `sync()` repaints the controls from the page's state.
|
|
171
|
+
*/
|
|
172
|
+
class GraphDrawer {
|
|
173
|
+
page;
|
|
174
|
+
constructor(page) {
|
|
175
|
+
this.page = page;
|
|
176
|
+
}
|
|
177
|
+
wire() {
|
|
178
|
+
document.querySelectorAll('.wp-mode[data-wp-mode]').forEach((button) => {
|
|
179
|
+
button.addEventListener('click', () => this.page.setMode(button.dataset['wpMode'] ?? 'runtime'));
|
|
180
|
+
});
|
|
181
|
+
this.wireLock();
|
|
182
|
+
this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());
|
|
183
|
+
this.toggles('wp-collapse', 'wp-shell', true);
|
|
184
|
+
this.toggles('wp-help-btn', 'wp-help', false);
|
|
185
|
+
this.toggles('wp-help-close', 'wp-help', false);
|
|
186
|
+
this.toggles('wp-legend-pop', 'wp-legend-popout', false);
|
|
187
|
+
this.toggles('wp-legend-close', 'wp-legend-popout', false);
|
|
188
|
+
this.toggles('wp-resp-open', 'wp-resp-panel', false);
|
|
189
|
+
this.toggles('wp-resp-close', 'wp-resp-panel', false);
|
|
190
|
+
this.toggles('wp-snapshot-open', 'wp-snapshot', false);
|
|
191
|
+
window.addEventListener('keydown', (ev) => this.onKey(ev), true);
|
|
192
|
+
window.addEventListener('hashchange', () => this.page.setMode(location.hash.slice(1)));
|
|
193
|
+
}
|
|
194
|
+
/** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
|
|
195
|
+
wireLock() {
|
|
196
|
+
const input = this.lockInput();
|
|
197
|
+
if (input === null)
|
|
198
|
+
return;
|
|
199
|
+
input.addEventListener('input', () => {
|
|
200
|
+
const value = input.value.trim();
|
|
201
|
+
if (value === '')
|
|
202
|
+
this.page.setLock(null);
|
|
203
|
+
else if (this.page.hasNode(value))
|
|
204
|
+
this.page.setLock(value);
|
|
205
|
+
});
|
|
206
|
+
input.addEventListener('change', () => {
|
|
207
|
+
const match = this.page.resolveName(input.value.trim());
|
|
208
|
+
if (match !== null)
|
|
209
|
+
this.page.setLock(match);
|
|
210
|
+
else
|
|
211
|
+
input.value = this.page.lockSelection() ?? '';
|
|
212
|
+
});
|
|
213
|
+
input.addEventListener('keydown', (ev) => {
|
|
214
|
+
if (ev.key !== 'Escape')
|
|
215
|
+
return;
|
|
216
|
+
ev.preventDefault();
|
|
217
|
+
input.value = '';
|
|
218
|
+
this.page.setLock(null);
|
|
219
|
+
input.blur();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the
|
|
224
|
+
* menu's own Escape handler closes it: Esc with a menu open only closes the menu.
|
|
225
|
+
*/
|
|
226
|
+
onKey(ev) {
|
|
227
|
+
if (ev.altKey || ev.ctrlKey || ev.metaKey)
|
|
228
|
+
return;
|
|
229
|
+
const target = ev.target;
|
|
230
|
+
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)
|
|
231
|
+
return;
|
|
232
|
+
const menuOpen = document.getElementById('wp-node-menu') !== null;
|
|
233
|
+
if (ev.key === '/') {
|
|
234
|
+
ev.preventDefault();
|
|
235
|
+
this.lockInput()?.focus();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const index = ['1', '2', '3'].indexOf(ev.key);
|
|
239
|
+
if (index >= 0 && !menuOpen) {
|
|
240
|
+
this.page.setMode(WpModeState.MODES[index]);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (ev.key === 'Escape' && !menuOpen && this.page.lockSelection() !== null)
|
|
244
|
+
this.page.setLock(null);
|
|
245
|
+
}
|
|
246
|
+
/** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */
|
|
247
|
+
toggles(buttonId, targetId, collapse) {
|
|
248
|
+
const button = this.byId(buttonId);
|
|
249
|
+
const target = this.byId(targetId);
|
|
250
|
+
if (button === null || target === null)
|
|
251
|
+
return;
|
|
252
|
+
button.addEventListener('click', (ev) => {
|
|
253
|
+
ev.stopPropagation();
|
|
254
|
+
let expanded;
|
|
255
|
+
if (collapse) {
|
|
256
|
+
expanded = !target.classList.toggle('wp-collapsed');
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
target.hidden = !target.hidden;
|
|
260
|
+
expanded = !target.hidden;
|
|
261
|
+
}
|
|
262
|
+
document.querySelectorAll(`[aria-controls="${targetId}"], #${buttonId}[aria-expanded]`).forEach((el) => el.setAttribute('aria-expanded', String(expanded)));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
/** Repaint every control from the page's state. */
|
|
266
|
+
sync(modes, locked, filtering) {
|
|
267
|
+
const reason = modes.impactReason();
|
|
268
|
+
document.querySelectorAll('.wp-mode[data-wp-mode]').forEach((button) => {
|
|
269
|
+
const mode = button.dataset['wpMode'] ?? '';
|
|
270
|
+
button.setAttribute('aria-pressed', String(mode === modes.mode));
|
|
271
|
+
button.disabled = !modes.available(mode);
|
|
272
|
+
if (mode === 'impact')
|
|
273
|
+
button.title = reason === '' ? 'Key 3' : reason;
|
|
274
|
+
});
|
|
275
|
+
const reasonEl = this.byId('wp-impact-reason');
|
|
276
|
+
if (reasonEl !== null) {
|
|
277
|
+
reasonEl.textContent = reason;
|
|
278
|
+
reasonEl.hidden = reason === '';
|
|
279
|
+
}
|
|
280
|
+
document.querySelectorAll('[data-wp-legend-mode]').forEach((section) => {
|
|
281
|
+
section.hidden = section.dataset['wpLegendMode'] !== modes.mode;
|
|
282
|
+
});
|
|
283
|
+
document.querySelectorAll('[data-wp-impact-note]').forEach((note) => {
|
|
284
|
+
note.textContent = this.impactNote(modes);
|
|
285
|
+
});
|
|
286
|
+
const title = this.byId('wp-legend-pop-title');
|
|
287
|
+
if (title !== null)
|
|
288
|
+
title.textContent = `Legend · ${modes.name(modes.mode)}`;
|
|
289
|
+
const input = this.lockInput();
|
|
290
|
+
if (input !== null && document.activeElement !== input)
|
|
291
|
+
input.value = locked ?? '';
|
|
292
|
+
this.syncFilterToggle(locked, filtering);
|
|
293
|
+
this.crumb(modes.name(modes.mode), locked);
|
|
294
|
+
}
|
|
295
|
+
syncFilterToggle(locked, filtering) {
|
|
296
|
+
const toggle = this.byId('wp-filter-toggle');
|
|
297
|
+
if (toggle === null)
|
|
298
|
+
return;
|
|
299
|
+
toggle.setAttribute('aria-pressed', String(filtering));
|
|
300
|
+
toggle.disabled = !filtering && locked === null;
|
|
301
|
+
toggle.title = toggle.disabled ? 'Lock a project first' : '';
|
|
302
|
+
}
|
|
303
|
+
impactNote(modes) {
|
|
304
|
+
const impact = modes.impact;
|
|
305
|
+
if (impact === null || !impact.available)
|
|
306
|
+
return '';
|
|
307
|
+
return (`${impact.touched.length} touched · ${impact.affected.length} affected · ` +
|
|
308
|
+
`${impact.buildInputs.length} build inputs — nx affected since fork point ${impact.base}, ` +
|
|
309
|
+
`${impact.changedFiles} changed files.`);
|
|
310
|
+
}
|
|
311
|
+
/** "Mode: <b>Runtime</b> · Locked: <b>x</b>", built with text nodes so no name is parsed as HTML. */
|
|
312
|
+
crumb(mode, locked) {
|
|
313
|
+
const crumb = this.byId('wp-crumb');
|
|
314
|
+
if (crumb === null)
|
|
315
|
+
return;
|
|
316
|
+
const bold = (text) => {
|
|
317
|
+
const b = document.createElement('b');
|
|
318
|
+
b.textContent = text;
|
|
319
|
+
return b;
|
|
320
|
+
};
|
|
321
|
+
const parts = ['Mode: ', bold(mode)];
|
|
322
|
+
if (locked !== null)
|
|
323
|
+
parts.push(' · Locked: ', bold(locked));
|
|
324
|
+
crumb.replaceChildren(...parts);
|
|
325
|
+
}
|
|
326
|
+
lockInput() {
|
|
327
|
+
return document.getElementById('wp-lock');
|
|
328
|
+
}
|
|
329
|
+
byId(id) {
|
|
330
|
+
return document.getElementById(id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
86
333
|
class GraphPage extends WpFilterPage {
|
|
87
334
|
highlighter = null;
|
|
335
|
+
modes = new WpModeState();
|
|
336
|
+
drawer = new GraphDrawer(this);
|
|
88
337
|
captureBinding(_svg) {
|
|
89
338
|
const previous = this.highlighter;
|
|
90
|
-
return () => {
|
|
339
|
+
return () => {
|
|
340
|
+
this.highlighter = previous;
|
|
341
|
+
this.filterCards();
|
|
342
|
+
this.syncControls();
|
|
343
|
+
};
|
|
91
344
|
}
|
|
92
345
|
wireControls() {
|
|
93
|
-
|
|
94
|
-
|
|
346
|
+
this.drawer.wire();
|
|
347
|
+
this.syncControls();
|
|
348
|
+
}
|
|
349
|
+
nodeDot(node) {
|
|
350
|
+
const modes = node.modes;
|
|
351
|
+
if (modes === null)
|
|
352
|
+
return node.dot;
|
|
353
|
+
if (this.modes.mode === 'architecture')
|
|
354
|
+
return modes.architecture;
|
|
355
|
+
if (this.modes.mode === 'impact')
|
|
356
|
+
return modes[this.modes.status(node.id)];
|
|
357
|
+
return modes.runtime;
|
|
358
|
+
}
|
|
359
|
+
usesFullDot() {
|
|
360
|
+
return this.modes.mode === 'runtime';
|
|
95
361
|
}
|
|
96
362
|
wireSvg(svg) {
|
|
97
363
|
this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);
|
|
98
364
|
this.highlighter.wire();
|
|
99
365
|
this.filterCards();
|
|
366
|
+
this.syncControls();
|
|
100
367
|
svg.querySelectorAll('g.node').forEach((node) => {
|
|
101
368
|
if (node.querySelector('title')?.textContent === this.anchor)
|
|
102
369
|
node.classList.add('wp-filter-anchor');
|
|
@@ -105,13 +372,50 @@ class GraphPage extends WpFilterPage {
|
|
|
105
372
|
lockSelection() {
|
|
106
373
|
return this.locked;
|
|
107
374
|
}
|
|
375
|
+
hasNode(id) {
|
|
376
|
+
return this.model.nodes.some((node) => node.id === id);
|
|
377
|
+
}
|
|
378
|
+
/** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */
|
|
379
|
+
resolveName(text) {
|
|
380
|
+
if (text === '')
|
|
381
|
+
return null;
|
|
382
|
+
if (this.hasNode(text))
|
|
383
|
+
return text;
|
|
384
|
+
const matches = this.model.nodes.filter((node) => node.id.split('/').pop() === text);
|
|
385
|
+
return matches.length === 1 ? matches[0].id : null;
|
|
386
|
+
}
|
|
108
387
|
setLock(name) {
|
|
109
388
|
this.locked = name;
|
|
110
|
-
const select = document.getElementById('wp-lock');
|
|
111
|
-
if (select !== null)
|
|
112
|
-
select.value = name ?? '';
|
|
113
389
|
this.highlighter?.relight();
|
|
114
390
|
this.filterCards();
|
|
391
|
+
this.syncControls();
|
|
392
|
+
}
|
|
393
|
+
setMode(mode) {
|
|
394
|
+
if (mode === this.modes.mode || !this.modes.set(mode))
|
|
395
|
+
return;
|
|
396
|
+
this.syncControls();
|
|
397
|
+
this.redraw();
|
|
398
|
+
}
|
|
399
|
+
/** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
|
|
400
|
+
toggleFilter() {
|
|
401
|
+
if (this.anchor !== null)
|
|
402
|
+
this.filter(null);
|
|
403
|
+
else if (this.locked !== null)
|
|
404
|
+
this.filter(this.locked);
|
|
405
|
+
}
|
|
406
|
+
/** The node menu's "Mode ▸" submenu: one radio item per mode, Impact disabled with its reason. */
|
|
407
|
+
modeItem() {
|
|
408
|
+
const children = WpModeState.MODES.map((mode) => {
|
|
409
|
+
const item = new WpNodeMenuItem(this.modes.name(mode), () => this.setMode(mode));
|
|
410
|
+
item.checked = mode === this.modes.mode;
|
|
411
|
+
if (!this.modes.available(mode))
|
|
412
|
+
item.disabledReason = this.modes.impactReason();
|
|
413
|
+
return item;
|
|
414
|
+
});
|
|
415
|
+
return new WpNodeMenuItem('Mode', () => { }, children);
|
|
416
|
+
}
|
|
417
|
+
syncControls() {
|
|
418
|
+
this.drawer.sync(this.modes, this.locked, this.anchor !== null);
|
|
115
419
|
}
|
|
116
420
|
filterCards() {
|
|
117
421
|
const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private hovered: string | null = null;\n\n constructor(\n private readonly svg: SVGSVGElement,\n private readonly chain: WpGraphChain,\n private readonly model: RenderModelJson,\n private readonly page: GraphPage,\n ) {}\n\n wire(): void {\n this.svg.querySelectorAll<SVGGElement>('g.node').forEach((g) => {\n if (g.classList.contains('wp-layout')) return;\n const name = g.querySelector('title')?.textContent;\n if (name !== null && name !== undefined) this.nodeByName.set(name, g);\n });\n this.wireHover();\n this.wireMenu();\n this.relight();\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl, .wp-locked').forEach((el: Element) => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl', 'wp-locked');\n });\n }\n\n relight(): void {\n this.clear();\n const locked = this.page.lockSelection();\n if (locked !== null) this.nodeByName.get(locked)?.classList.add('wp-locked');\n // A filtered-out Lock is suspended, including its otherwise visible relatives.\n const anchors = [locked, this.hovered].filter(\n (name: string | null): name is string => name !== null && this.nodeByName.has(name),\n );\n if (anchors.length === 0) return;\n const lit = new Set<string>();\n for (const name of anchors) {\n this.nodeByName.get(name)?.classList.add('wp-focus');\n for (const id of this.chain.nodes(name)) {\n if (this.nodeByName.has(id)) lit.add(id);\n }\n }\n this.svg.classList.add('wp-dim');\n for (const id of lit) this.nodeByName.get(id)?.classList.add('wp-neighbor');\n for (const edge of this.model.edges) {\n if (lit.has(edge.from) && lit.has(edge.to)) {\n this.svg.querySelector(`[id=\"${edge.id}\"]`)?.classList.add('wp-hl');\n }\n }\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string) => {\n g.addEventListener('mouseenter', () => {\n this.hovered = name;\n this.relight();\n });\n g.addEventListener('mouseleave', () => {\n this.hovered = null;\n this.relight();\n });\n });\n }\n\n private wireMenu(): void {\n const designs = new Map(__DESIGN_LINKS__.map((link) => [link.nodeId, link.href]));\n WpNodeMenu.wire(this.svg, (name) => {\n const items: WpNodeMenuItem[] = [];\n const href = designs.get(name);\n if (href !== undefined)\n items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));\n const locked = this.page.lockSelection() === name;\n items.push(\n new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () =>\n this.page.setLock(locked ? null : name),\n ),\n );\n items.push(this.page.filterItem(name));\n return items;\n });\n }\n}\n\nclass GraphPage extends WpFilterPage {\n private highlighter: GraphHighlighter | null = null;\n\n protected override captureBinding(_svg: SVGSVGElement | null): () => void {\n const previous = this.highlighter;\n return (): void => { this.highlighter = previous; this.filterCards(); };\n }\n\n protected override wireControls(): void {\n const select = document.getElementById('wp-lock') as HTMLSelectElement | null;\n select?.addEventListener('change', () =>\n this.setLock(select.value === '' ? null : select.value),\n );\n }\n\n protected wireSvg(svg: SVGSVGElement): void {\n this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);\n this.highlighter.wire();\n this.filterCards();\n svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => {\n if (node.querySelector('title')?.textContent === this.anchor)\n node.classList.add('wp-filter-anchor');\n });\n }\n\n lockSelection(): string | null {\n return this.locked;\n }\n\n setLock(name: string | null): void {\n this.locked = name;\n const select = document.getElementById('wp-lock') as HTMLSelectElement | null;\n if (select !== null) select.value = name ?? '';\n this.highlighter?.relight();\n this.filterCards();\n }\n\n private filterCards(): void {\n const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);\n document.querySelectorAll('.wp-resp-card').forEach((card) => {\n const name = card.getAttribute('data-node');\n card.classList.toggle(\n 'wp-hidden',\n name === null || !this.retained.has(name) || !lit.has(name),\n );\n });\n }\n}\n\nnew GraphPage(__RENDER_MODEL__).render();\n"]}
|
|
1
|
+
{"version":3,"file":"graph-visualizer.client.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-visualizer.client.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The three color modes (graph-color-modes.ts), the viewer's remembered choice, and Impact's data.\n *\n * The mode is deep-linkable (`#runtime`, `#architecture`, `#impact`, which win over the remembered\n * one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,\n * blocked site data, a file:// page in some browsers), so every access is wrapped and the page works\n * without it.\n */\nclass WpModeState {\n static readonly MODES = ['runtime', 'architecture', 'impact'];\n static readonly NAMES = ['Runtime', 'Architecture', 'Impact'];\n static readonly STORAGE_KEY = 'wp-architecture-graph-mode';\n readonly impact: ImpactJson | null = window.__WP_IMPACT__ ?? null;\n mode = 'runtime';\n\n constructor() {\n this.mode = this.initial();\n }\n\n /** '' when Impact can be shown, else the one line the drawer and the menu say instead. */\n impactReason(): string {\n if (this.impact === null)\n return 'No impact data for this branch. Run pnpm nx run architecture:generate to compute it.';\n return this.impact.available ? '' : `Impact unavailable: ${this.impact.reason}.`;\n }\n\n available(mode: string): boolean {\n return WpModeState.MODES.includes(mode) && (mode !== 'impact' || this.impactReason() === '');\n }\n\n name(mode: string): string {\n return WpModeState.NAMES[WpModeState.MODES.indexOf(mode)] ?? mode;\n }\n\n /** False (and nothing changes) for an unknown or unavailable mode. */\n set(mode: string): boolean {\n if (!this.available(mode)) return false;\n this.mode = mode;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store must not break the page\n try {\n localStorage.setItem(WpModeState.STORAGE_KEY, mode);\n if (location.hash !== `#${mode}`) history.replaceState(null, '', `#${mode}`);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n }\n return true;\n }\n\n /** Which impact shade a box gets. */\n status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched' {\n if (this.impact?.touched.includes(id)) return 'touched';\n if (this.impact?.affected.includes(id)) return 'affected';\n if (this.impact?.buildInputs.includes(id)) return 'buildInput';\n return 'untouched';\n }\n\n private initial(): string {\n const hashed = location.hash.slice(1);\n if (this.available(hashed)) return hashed;\n const stored = this.stored();\n if (stored !== null && this.available(stored)) return stored;\n return 'runtime';\n }\n\n private stored(): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- storage is a per-viewer convenience; a blocked store reads as \"nothing stored\"\n try {\n return localStorage.getItem(WpModeState.STORAGE_KEY);\n // webpieces-disable no-any-unknown -- browsers may throw any value from blocked storage\n } catch (err: unknown) {\n //const error = toError(err);\n void err;\n return null;\n }\n }\n}\n\n/** Architecture interactions bind to each new SVG; logical Lock/Filter state belongs to the page. */\nclass GraphHighlighter {\n private readonly nodeByName = new Map<string, SVGGElement>();\n private hovered: string | null = null;\n\n constructor(\n private readonly svg: SVGSVGElement,\n private readonly chain: WpGraphChain,\n private readonly model: RenderModelJson,\n private readonly page: GraphPage,\n ) {}\n\n wire(): void {\n this.svg.querySelectorAll<SVGGElement>('g.node').forEach((g: SVGGElement) => {\n if (g.classList.contains('wp-layout')) return;\n const name = g.querySelector('title')?.textContent;\n if (name !== null && name !== undefined) this.nodeByName.set(name, g);\n });\n this.wireHover();\n this.wireMenu();\n this.relight();\n }\n\n private clear(): void {\n this.svg.classList.remove('wp-dim');\n this.svg.querySelectorAll('.wp-focus, .wp-neighbor, .wp-hl, .wp-locked').forEach((el: Element) => {\n el.classList.remove('wp-focus', 'wp-neighbor', 'wp-hl', 'wp-locked');\n });\n }\n\n relight(): void {\n this.clear();\n const locked = this.page.lockSelection();\n if (locked !== null) this.nodeByName.get(locked)?.classList.add('wp-locked');\n // A filtered-out Lock is suspended, including its otherwise visible relatives.\n const anchors = [locked, this.hovered].filter(\n (name: string | null): name is string => name !== null && this.nodeByName.has(name),\n );\n if (anchors.length === 0) return;\n const lit = new Set<string>();\n for (const name of anchors) {\n this.nodeByName.get(name)?.classList.add('wp-focus');\n for (const id of this.chain.nodes(name)) {\n if (this.nodeByName.has(id)) lit.add(id);\n }\n }\n this.svg.classList.add('wp-dim');\n for (const id of lit) this.nodeByName.get(id)?.classList.add('wp-neighbor');\n for (const edge of this.model.edges) {\n if (lit.has(edge.from) && lit.has(edge.to)) {\n this.svg.querySelector(`[id=\"${edge.id}\"]`)?.classList.add('wp-hl');\n }\n }\n }\n\n private wireHover(): void {\n this.nodeByName.forEach((g: SVGGElement, name: string) => {\n g.addEventListener('mouseenter', () => {\n this.hovered = name;\n this.relight();\n });\n g.addEventListener('mouseleave', () => {\n this.hovered = null;\n this.relight();\n });\n });\n }\n\n private wireMenu(): void {\n const designs = new Map<string, string>();\n for (const link of __DESIGN_LINKS__) designs.set(link.nodeId, link.href);\n WpNodeMenu.wire(this.svg, (name: string): WpNodeMenuItem[] => {\n const items: WpNodeMenuItem[] = [];\n const href = designs.get(name);\n if (href !== undefined)\n items.push(new WpNodeMenuItem('View Design', () => window.open(href, '_blank')));\n const locked = this.page.lockSelection() === name;\n items.push(\n new WpNodeMenuItem(locked ? 'Unlock' : 'Lock', () =>\n this.page.setLock(locked ? null : name),\n ),\n );\n items.push(this.page.filterItem(name));\n items.push(this.page.modeItem());\n return items;\n });\n }\n}\n\n/**\n * The drawer and its floating panels: every control outside the graph itself. Holds no graph state —\n * each control calls back into the page, and `sync()` repaints the controls from the page's state.\n */\nclass GraphDrawer {\n constructor(private readonly page: GraphPage) {}\n\n wire(): void {\n document.querySelectorAll<HTMLButtonElement>('.wp-mode[data-wp-mode]').forEach((button: HTMLButtonElement) => {\n button.addEventListener('click', () => this.page.setMode(button.dataset['wpMode'] ?? 'runtime'));\n });\n this.wireLock();\n this.byId('wp-filter-toggle')?.addEventListener('click', () => this.page.toggleFilter());\n this.toggles('wp-collapse', 'wp-shell', true);\n this.toggles('wp-help-btn', 'wp-help', false);\n this.toggles('wp-help-close', 'wp-help', false);\n this.toggles('wp-legend-pop', 'wp-legend-popout', false);\n this.toggles('wp-legend-close', 'wp-legend-popout', false);\n this.toggles('wp-resp-open', 'wp-resp-panel', false);\n this.toggles('wp-resp-close', 'wp-resp-panel', false);\n this.toggles('wp-snapshot-open', 'wp-snapshot', false);\n window.addEventListener('keydown', (ev: KeyboardEvent) => this.onKey(ev), true);\n window.addEventListener('hashchange', () => this.page.setMode(location.hash.slice(1)));\n }\n\n /** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */\n private wireLock(): void {\n const input = this.lockInput();\n if (input === null) return;\n input.addEventListener('input', () => {\n const value = input.value.trim();\n if (value === '') this.page.setLock(null);\n else if (this.page.hasNode(value)) this.page.setLock(value);\n });\n input.addEventListener('change', () => {\n const match = this.page.resolveName(input.value.trim());\n if (match !== null) this.page.setLock(match);\n else input.value = this.page.lockSelection() ?? '';\n });\n input.addEventListener('keydown', (ev: KeyboardEvent) => {\n if (ev.key !== 'Escape') return;\n ev.preventDefault();\n input.value = '';\n this.page.setLock(null);\n input.blur();\n });\n }\n\n /**\n * Page keys, in the CAPTURE phase so the open/closed state of the node menu is read before the\n * menu's own Escape handler closes it: Esc with a menu open only closes the menu.\n */\n private onKey(ev: KeyboardEvent): void {\n if (ev.altKey || ev.ctrlKey || ev.metaKey) return;\n const target = ev.target;\n if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) return;\n const menuOpen = document.getElementById('wp-node-menu') !== null;\n if (ev.key === '/') {\n ev.preventDefault();\n this.lockInput()?.focus();\n return;\n }\n const index = ['1', '2', '3'].indexOf(ev.key);\n if (index >= 0 && !menuOpen) {\n this.page.setMode(WpModeState.MODES[index]);\n return;\n }\n if (ev.key === 'Escape' && !menuOpen && this.page.lockSelection() !== null) this.page.setLock(null);\n }\n\n /** `button` shows/hides `target`; for the drawer itself it toggles the collapsed class instead. */\n private toggles(buttonId: string, targetId: string, collapse: boolean): void {\n const button = this.byId(buttonId);\n const target = this.byId(targetId);\n if (button === null || target === null) return;\n button.addEventListener('click', (ev: MouseEvent) => {\n ev.stopPropagation();\n let expanded: boolean;\n if (collapse) {\n expanded = !target.classList.toggle('wp-collapsed');\n } else {\n target.hidden = !target.hidden;\n expanded = !target.hidden;\n }\n document.querySelectorAll(`[aria-controls=\"${targetId}\"], #${buttonId}[aria-expanded]`).forEach(\n (el: Element) => el.setAttribute('aria-expanded', String(expanded)),\n );\n });\n }\n\n /** Repaint every control from the page's state. */\n sync(modes: WpModeState, locked: string | null, filtering: boolean): void {\n const reason = modes.impactReason();\n document.querySelectorAll<HTMLButtonElement>('.wp-mode[data-wp-mode]').forEach((button: HTMLButtonElement) => {\n const mode = button.dataset['wpMode'] ?? '';\n button.setAttribute('aria-pressed', String(mode === modes.mode));\n button.disabled = !modes.available(mode);\n if (mode === 'impact') button.title = reason === '' ? 'Key 3' : reason;\n });\n const reasonEl = this.byId('wp-impact-reason');\n if (reasonEl !== null) {\n reasonEl.textContent = reason;\n reasonEl.hidden = reason === '';\n }\n document.querySelectorAll<HTMLElement>('[data-wp-legend-mode]').forEach((section: HTMLElement) => {\n section.hidden = section.dataset['wpLegendMode'] !== modes.mode;\n });\n document.querySelectorAll<HTMLElement>('[data-wp-impact-note]').forEach((note: HTMLElement) => {\n note.textContent = this.impactNote(modes);\n });\n const title = this.byId('wp-legend-pop-title');\n if (title !== null) title.textContent = `Legend · ${modes.name(modes.mode)}`;\n const input = this.lockInput();\n if (input !== null && document.activeElement !== input) input.value = locked ?? '';\n this.syncFilterToggle(locked, filtering);\n this.crumb(modes.name(modes.mode), locked);\n }\n\n private syncFilterToggle(locked: string | null, filtering: boolean): void {\n const toggle = this.byId('wp-filter-toggle') as HTMLButtonElement | null;\n if (toggle === null) return;\n toggle.setAttribute('aria-pressed', String(filtering));\n toggle.disabled = !filtering && locked === null;\n toggle.title = toggle.disabled ? 'Lock a project first' : '';\n }\n\n private impactNote(modes: WpModeState): string {\n const impact = modes.impact;\n if (impact === null || !impact.available) return '';\n return (\n `${impact.touched.length} touched · ${impact.affected.length} affected · ` +\n `${impact.buildInputs.length} build inputs — nx affected since fork point ${impact.base}, ` +\n `${impact.changedFiles} changed files.`\n );\n }\n\n /** \"Mode: <b>Runtime</b> · Locked: <b>x</b>\", built with text nodes so no name is parsed as HTML. */\n private crumb(mode: string, locked: string | null): void {\n const crumb = this.byId('wp-crumb');\n if (crumb === null) return;\n const bold = (text: string): HTMLElement => {\n const b = document.createElement('b');\n b.textContent = text;\n return b;\n };\n const parts: (string | Node)[] = ['Mode: ', bold(mode)];\n if (locked !== null) parts.push(' · Locked: ', bold(locked));\n crumb.replaceChildren(...parts);\n }\n\n private lockInput(): HTMLInputElement | null {\n return document.getElementById('wp-lock') as HTMLInputElement | null;\n }\n\n private byId(id: string): HTMLElement | null {\n return document.getElementById(id);\n }\n}\n\nclass GraphPage extends WpFilterPage {\n private highlighter: GraphHighlighter | null = null;\n private readonly modes = new WpModeState();\n private readonly drawer = new GraphDrawer(this);\n\n protected override captureBinding(_svg: SVGSVGElement | null): () => void {\n const previous = this.highlighter;\n return (): void => {\n this.highlighter = previous;\n this.filterCards();\n this.syncControls();\n };\n }\n\n protected override wireControls(): void {\n this.drawer.wire();\n this.syncControls();\n }\n\n protected override nodeDot(node: RenderNodeJson): string {\n const modes = node.modes;\n if (modes === null) return node.dot;\n if (this.modes.mode === 'architecture') return modes.architecture;\n if (this.modes.mode === 'impact') return modes[this.modes.status(node.id)];\n return modes.runtime;\n }\n\n protected override usesFullDot(): boolean {\n return this.modes.mode === 'runtime';\n }\n\n protected wireSvg(svg: SVGSVGElement): void {\n this.highlighter = new GraphHighlighter(svg, this.chain, this.model, this);\n this.highlighter.wire();\n this.filterCards();\n this.syncControls();\n svg.querySelectorAll<SVGGElement>('g.node').forEach((node: SVGGElement) => {\n if (node.querySelector('title')?.textContent === this.anchor)\n node.classList.add('wp-filter-anchor');\n });\n }\n\n lockSelection(): string | null {\n return this.locked;\n }\n\n hasNode(id: string): boolean {\n return this.model.nodes.some((node: RenderNodeJson): boolean => node.id === id);\n }\n\n /** An exact id, else the one id whose short name (after any scope) is `text`; null if none or several. */\n resolveName(text: string): string | null {\n if (text === '') return null;\n if (this.hasNode(text)) return text;\n const matches = this.model.nodes.filter(\n (node: RenderNodeJson): boolean => node.id.split('/').pop() === text,\n );\n return matches.length === 1 ? matches[0].id : null;\n }\n\n setLock(name: string | null): void {\n this.locked = name;\n this.highlighter?.relight();\n this.filterCards();\n this.syncControls();\n }\n\n setMode(mode: string): void {\n if (mode === this.modes.mode || !this.modes.set(mode)) return;\n this.syncControls();\n this.redraw();\n }\n\n /** The drawer's \"Hide unconnected\": filter to the locked box's chain, or turn the filter off. */\n toggleFilter(): void {\n if (this.anchor !== null) this.filter(null);\n else if (this.locked !== null) this.filter(this.locked);\n }\n\n /** The node menu's \"Mode ▸\" submenu: one radio item per mode, Impact disabled with its reason. */\n modeItem(): WpNodeMenuItem {\n const children = WpModeState.MODES.map((mode: string): WpNodeMenuItem => {\n const item = new WpNodeMenuItem(this.modes.name(mode), (): void => this.setMode(mode));\n item.checked = mode === this.modes.mode;\n if (!this.modes.available(mode)) item.disabledReason = this.modes.impactReason();\n return item;\n });\n return new WpNodeMenuItem('Mode', (): void => {}, children);\n }\n\n private syncControls(): void {\n this.drawer.sync(this.modes, this.locked, this.anchor !== null);\n }\n\n private filterCards(): void {\n const lit = this.locked === null ? this.retained : this.chain.nodes(this.locked);\n document.querySelectorAll('.wp-resp-card').forEach((card: Element) => {\n const name = card.getAttribute('data-node');\n card.classList.toggle(\n 'wp-hidden',\n name === null || !this.retained.has(name) || !lit.has(name),\n );\n });\n }\n}\n\nnew GraphPage(__RENDER_MODEL__).render();\n"]}
|