@webpieces/nx-webpieces-rules 0.4.874 → 0.4.876

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.
@@ -1 +1 @@
1
- {"version":3,"file":"graph-impact.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-impact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Impact mode's data: what THIS BRANCH's build looks like, in three shades (plus untouched grey).\n *\n * - BUILD INPUT is every transitive dependency of the affected set that is not itself affected:\n * `ci` dependsOn `^build`, so they must exist as build output though their inputs did not change.\n * nx restores them from cache when it has them and compiles them otherwise — which one happens is\n * not knowable here, so nothing claims it. A pure downward walk over the graph's own edges.\n *\n * - AFFECTED is `nx show projects --affected --base=<fork point> --json`, with NO `--head`, so\n * uncommitted and untracked work counts. nx already gets right everything a hand-rolled diff gets\n * wrong: lockfile and package.json changes resolved per project, root/global inputs, namedInputs,\n * implicitDependencies, nested roots and deleted files. This uses its public CLI surface only, never\n * nx internals.\n * - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project\n * ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)\n * — never a path-prefix guess. A project whose only change is a DELETED file is not in that map\n * (the file is gone), so it shows as affected rather than touched.\n *\n * The base is the repo's existing merge-base computation (`DiffScope.detectBase` in rules-config:\n * `git merge-base HEAD origin/main`, falling back to `main`) — never NX_BASE/NX_HEAD, whose value\n * depends on whoever launched the process. The changed-file list is `DiffScope.getChangedFiles`, the\n * same base→working-tree diff (with untracked files) every code rule uses.\n *\n * The answer changes per branch, so it never goes into the committed dependencies.html or\n * dependencies.json (both stay byte-stable across branches). It goes into a SIDECAR,\n * `architecture/.impact/dependencies.impact.js`: a plain script setting one window global, loaded\n * with `<script src>` — the one way a page opened straight from disk (file://) can read a\n * neighbouring file. The directory carries its own `.gitignore` containing `*`, which ignores the\n * directory and everything in it, so a consumer repo needs no gitignore edit and the PR gate's\n * \"build left nothing uncommitted\" check never sees it.\n *\n * Both `architecture:generate` and `architecture:visualize` refresh it: the answer changes with the\n * branch, not with the architecture, so viewing the SAVED graph re-scans Impact against it without\n * regenerating anything else (#1158).\n *\n * Impact is a convenience. When nx or git cannot answer, NO sidecar is written (a stale one is\n * removed) and the page disables Impact with a one-line reason. It never fails generation.\n */\n\nimport { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectFileMapUsingProjectGraph, createProjectGraphAsync } from '@nx/devkit';\nimport { DiffScope, ChangedFilesOptions } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { GraphNames } from './graph-names';\nimport { toError } from '../toError';\n\n/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */\nexport const IMPACT_SIDECAR_DIR = '.impact';\nexport const IMPACT_SIDECAR_FILE = 'dependencies.impact.js';\nexport const IMPACT_SIDECAR_SRC = `${IMPACT_SIDECAR_DIR}/${IMPACT_SIDECAR_FILE}`;\n/** The nx call is capped; a slow or wedged nx disables Impact instead of stalling generation. */\nconst NX_TIMEOUT_MS = 15000;\n\n/** What the sidecar carries; the page reads it as `window.__WP_IMPACT__`. */\nexport class ImpactReport {\n constructor(\n public readonly available: boolean,\n public readonly reason: string,\n public readonly base: string,\n /** Affected AND owning a changed file (node ids). */\n public readonly touched: string[],\n /** Affected but owning no changed file (node ids): their ci re-runs. */\n public readonly affected: string[],\n /**\n * Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn\n * `^build`, so they must exist as build output although their inputs did not change: nx\n * restores them from cache when it has them and compiles them otherwise. Which of the two\n * happens is not knowable here, so the page claims neither.\n */\n public readonly buildInputs: string[],\n public readonly changedFiles: number,\n /**\n * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the\n * Filter popover's \"Changed + what they use\". (\"Changed + what uses them\" is touched +\n * affected, which nx already computed as the dependents whose tests re-run.)\n */\n public readonly dependencies: string[],\n /**\n * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as\n * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the\n * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.\n */\n public readonly globalFiles: string[],\n ) {}\n}\n\n/** A scan that could not produce an answer: no sidecar is written, only this reason printed. */\nexport class ImpactUnavailable {\n constructor(public readonly reason: string) {}\n}\n\n/** Runs the workspace's own nx CLI. Injected so specs never spawn nx. */\nexport class NxCli {\n /** stdout, or null when nx cannot be found, exits non-zero or exceeds the timeout. */\n run(workspaceRoot: string, args: string[], timeoutMs: number): string | null {\n const bin = this.binary(workspaceRoot);\n if (bin === null) return null;\n const result = spawnSync(process.execPath, [bin, ...args], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n timeout: timeoutMs,\n stdio: 'pipe',\n maxBuffer: 16 * 1024 * 1024,\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n\n /** Resolved the way node would from the workspace, so a linked worktree finds its main tree's nx. */\n private binary(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a workspace without nx resolves nothing; that is an answer, not an error\n try {\n return require.resolve('nx/bin/nx.js', { paths: [workspaceRoot] });\n } catch (err: unknown) {\n const error = toError(err);\n void error; // no nx installed here — Impact is simply unavailable\n return null;\n }\n }\n}\n\n/** nx's own file → project ownership, from the public devkit file map. Injected for specs. */\nexport class ProjectFileOwners {\n async owners(): Promise<Map<string, string>> {\n const fileMap = await createProjectFileMapUsingProjectGraph(await createProjectGraphAsync());\n const owners = new Map<string, string>();\n for (const project of Object.keys(fileMap)) {\n for (const data of fileMap[project]) owners.set(data.file, project);\n }\n return owners;\n }\n}\n\nexport class NxImpactScanner {\n private readonly names = new GraphNames();\n\n constructor(\n private readonly diffScope: DiffScope = new DiffScope(),\n private readonly nx: NxCli = new NxCli(),\n private readonly ownership: ProjectFileOwners = new ProjectFileOwners(),\n ) {}\n\n async scan(workspaceRoot: string, graph: EnhancedGraph): Promise<ImpactReport | ImpactUnavailable> {\n const base = this.diffScope.detectBase(workspaceRoot);\n if (base === null) return new ImpactUnavailable('no fork point: neither origin/main nor main resolves (or git is unavailable)');\n const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);\n if (json === null) return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`);\n const affectedSet = this.parse(json, graph);\n if (affectedSet === null) return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list');\n const options = new ChangedFilesOptions();\n options.tsOnly = false;\n options.includeDeletions = true;\n const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);\n const short = base.slice(0, 7);\n if (affectedSet.length === 0)\n return new ImpactReport(false, `nothing affected since the fork point ${short}`, short, [], [], [], files.length, [], []);\n const owners = await this.owners();\n const touched = this.touched(affectedSet, files, owners);\n const rest = affectedSet.filter((project: string): boolean => !touched.includes(project));\n const inputs = this.buildInputs(affectedSet, graph);\n return new ImpactReport(\n true,\n '',\n short,\n this.ids(touched),\n this.ids(rest),\n this.ids(inputs),\n files.length,\n this.ids(this.buildInputs(touched, graph)),\n this.globalFiles(files, owners),\n );\n }\n\n /** Changed files no project owns; none are claimed when ownership itself was unavailable. */\n globalFiles(files: string[], owners: Map<string, string>): string[] {\n if (owners.size === 0) return [];\n return files.filter((file: string): boolean => !owners.has(file)).sort();\n }\n\n /**\n * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over\n * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of\n * the touched set, \"what they use\".\n */\n buildInputs(roots: string[], graph: EnhancedGraph): string[] {\n const inSet = new Set<string>(roots);\n const seen = new Set<string>(roots);\n const stack = [...roots];\n const inputs: string[] = [];\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 if (!inSet.has(dep) && graph[dep] !== undefined) inputs.push(dep);\n }\n }\n return inputs.sort();\n }\n\n /** Affected projects that own one of the changed files, by nx's own ownership. */\n touched(affected: string[], files: string[], owners: Map<string, string>): string[] {\n const owning = new Set<string>();\n for (const file of files) {\n const owner = owners.get(file);\n if (owner !== undefined) owning.add(owner);\n }\n return affected.filter((project: string): boolean => owning.has(project));\n }\n\n /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */\n parse(json: string, graph: EnhancedGraph): string[] | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason\n try {\n // webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below\n const parsed: unknown = JSON.parse(json);\n if (!Array.isArray(parsed)) return null;\n return parsed\n .filter((name: unknown): name is string => typeof name === 'string' && graph[name] !== undefined)\n .sort();\n } catch (err: unknown) {\n const error = toError(err);\n void error; // not JSON — the caller reports it\n return null;\n }\n }\n\n /** Ownership failing is not fatal: the set then shows in one shade, all \"affected\". */\n private async owners(): Promise<Map<string, string>> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional\n try {\n return await this.ownership.owners();\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);\n return new Map<string, string>();\n }\n }\n\n private ids(projects: string[]): string[] {\n return projects.map((project: string): string => this.names.getNodeId(project));\n }\n}\n\n/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */\nexport class ImpactSidecar {\n /** Never throws: a sidecar that cannot be written just leaves Impact disabled on the page. */\n write(architectureDir: string, report: ImpactReport): string | null {\n const dir = path.join(architectureDir, IMPACT_SIDECAR_DIR);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation\n try {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n path.join(dir, '.gitignore'),\n '# Written by architecture:generate and architecture:visualize. Per-branch data: never commit it.\\n*\\n',\n 'utf-8',\n );\n const file = path.join(dir, IMPACT_SIDECAR_FILE);\n fs.writeFileSync(file, this.script(report), 'utf-8');\n return file;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not write the impact sidecar: ${error.message}`);\n return null;\n }\n }\n\n /** A stale sidecar from an earlier branch must never be shown as this branch's impact. */\n remove(architectureDir: string): void {\n fs.rmSync(path.join(architectureDir, IMPACT_SIDECAR_DIR, IMPACT_SIDECAR_FILE), { force: true });\n }\n\n script(report: ImpactReport): string {\n const json = JSON.stringify(report).replace(/</g, '\\\\u003c');\n return `// Generated by architecture:generate or architecture:visualize for one branch. Gitignored; do not commit.\\nwindow.__WP_IMPACT__ = ${json};\\n`;\n }\n}\n\n/** The generator's one call: scan, write, and report the result and its cost in one line. */\nexport class ImpactRefresh {\n constructor(\n private readonly scanner: NxImpactScanner = new NxImpactScanner(),\n private readonly sidecar: ImpactSidecar = new ImpactSidecar(),\n ) {}\n\n /** Never throws — Impact is optional, generation is not. */\n async run(architectureDir: string, workspaceRoot: string, graph: EnhancedGraph): Promise<string> {\n const started = Date.now();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation\n try {\n const report = await this.scanner.scan(workspaceRoot, graph);\n const cost = `${Date.now() - started} ms`;\n if (report instanceof ImpactUnavailable) {\n this.sidecar.remove(architectureDir);\n return `ℹ️ Impact disabled, no sidecar written: ${report.reason} (${cost})`;\n }\n const file = this.sidecar.write(architectureDir, report);\n if (file === null) return `⚠️ Impact sidecar not written (${cost})`;\n if (!report.available) return `ℹ️ Impact disabled on the page: ${report.reason} (${cost})`;\n return (\n `✅ Impact vs ${report.base}: ${report.touched.length} touched · ${report.affected.length} affected · ` +\n `${report.buildInputs.length} build inputs, from ${report.changedFiles} changed file(s) (${cost}) → ${file}`\n );\n } catch (err: unknown) {\n const error = toError(err);\n return `⚠️ Impact skipped: ${error.message}`;\n }\n }\n}\n"]}
1
+ {"version":3,"file":"graph-impact.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-impact.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Impact mode's data: what one COMPARISON's build looks like, in three shades (plus untouched grey).\n *\n * There are two comparisons, and only two (#1163). Both end at the WORKING TREE, so uncommitted and\n * untracked work counts in either:\n *\n * - BRANCH, \"Changed on this branch\": the fork point (`DiffScope.detectBase` in rules-config:\n * `git merge-base HEAD origin/main`, falling back to `main`) → working tree.\n * - COMMIT, \"Last commit\": `HEAD^` → working tree. To see what another commit changed, check it out\n * (`git checkout <hash>`) and run `pnpm arch:visualize`: nx computes `--affected` against the code\n * that is actually on disk, so nothing here ever jumps to another commit.\n *\n * Which of them exist is decided by `ImpactScopeResolver` from where HEAD is. On `main` (by BRANCH\n * NAME, so a local main with unpushed commits still counts as main) and on a detached HEAD only the\n * last commit exists — the fork point IS HEAD there, so the branch comparison would always be empty.\n * On a feature branch with commits past its fork point both exist, the branch one first. On a FRESH\n * feature branch (fork point = HEAD) only the branch one exists: falling through to \"last commit\"\n * would show main's last PR as if it were the branch's own work.\n *\n * Per comparison:\n * - BUILD INPUT is every transitive dependency of the affected set that is not itself affected:\n * `ci` dependsOn `^build`, so they must exist as build output though their inputs did not change.\n * nx restores them from cache when it has them and compiles them otherwise — which one happens is\n * not knowable here, so nothing claims it. A pure downward walk over the graph's own edges.\n * - AFFECTED is `nx show projects --affected --base=<the comparison's base> --json`, with NO\n * `--head`, so uncommitted and untracked work counts. nx already gets right everything a\n * hand-rolled diff gets wrong: lockfile and package.json changes resolved per project, root/global\n * inputs, namedInputs, implicitDependencies, nested roots and deleted files. This uses its public\n * CLI surface only, never nx internals, and never NX_BASE/NX_HEAD, whose value depends on whoever\n * launched the process.\n * - TOUCHED is the subset of that set which OWNS a changed file, by nx's own file-to-project\n * ownership (the project file map from `createProjectFileMapUsingProjectGraph`, public devkit API)\n * — never a path-prefix guess. The ownership map is computed ONCE and shared by every comparison.\n * A project whose only change is a DELETED file is not in that map (the file is gone), so it shows\n * as affected rather than touched. The changed-file list is `DiffScope.getChangedFiles`, the same\n * base→working-tree diff (with untracked files) every code rule uses.\n *\n * The answer changes per checkout, so it never goes into the committed dependencies.html or\n * dependencies.json (both stay byte-stable). It goes into a SIDECAR,\n * `architecture/.impact/dependencies.impact.js`: a plain script setting one window global,\n * `window.__WP_IMPACT__ = { scans, defaultKind }`, loaded with `<script src>` — the one way a page\n * opened straight from disk (file://) can read a neighbouring file. The directory carries its own\n * `.gitignore` containing `*`, which ignores the directory and everything in it, so a consumer repo\n * needs no gitignore edit and the PR gate's \"build left nothing uncommitted\" check never sees it.\n *\n * `architecture:visualize` scans every comparison that exists. `architecture:generate` refreshes\n * ONLY the branch comparison (`GENERATE_IMPACT_KINDS`) and never scans the last commit; on main or a\n * detached HEAD that leaves nothing to write, so it removes the sidecar and the page says to run\n * `pnpm arch:visualize`.\n *\n * Impact is a convenience. When nx or git cannot answer any comparison, NO sidecar is written (a\n * stale one is removed) and the page disables Impact with a one-line reason. It never fails\n * generation.\n */\n\nimport { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { createProjectFileMapUsingProjectGraph, createProjectGraphAsync } from '@nx/devkit';\nimport { DiffScope, ChangedFilesOptions } from '@webpieces/rules-config';\nimport type { EnhancedGraph } from './graph-sorter';\nimport { GraphNames } from './graph-names';\nimport { toError } from '../toError';\n\n/** Relative to the architecture/ directory the page lives in; the page's `<script src>`. */\nexport const IMPACT_SIDECAR_DIR = '.impact';\nexport const IMPACT_SIDECAR_FILE = 'dependencies.impact.js';\nexport const IMPACT_SIDECAR_SRC = `${IMPACT_SIDECAR_DIR}/${IMPACT_SIDECAR_FILE}`;\n/** Each nx call is capped; a slow or wedged nx disables that comparison instead of stalling generation. */\nconst NX_TIMEOUT_MS = 15000;\n/** The branch whose name means \"there is no branch comparison here, only the last commit\". */\nconst MAIN_BRANCH = 'main';\n\nexport const NOTHING_CHANGED_ON_BRANCH = 'Nothing changed on this branch yet';\nexport const ROOT_COMMIT_REASON =\n \"HEAD is the repository's first commit; there is no previous commit to compare against\";\nexport const UNCOMMITTED_SUFFIX = ' + uncommitted changes';\n\n/** Which of the two comparisons a scan answers. */\nexport enum ImpactKind {\n /** Fork point → working tree: \"Changed on this branch\". */\n BRANCH = 'branch',\n /** HEAD^ → working tree: \"Last commit\". */\n COMMIT = 'commit',\n}\n\n/** `architecture:visualize` scans every comparison that exists. */\nexport const VISUALIZE_IMPACT_KINDS: readonly ImpactKind[] = [ImpactKind.BRANCH, ImpactKind.COMMIT];\n/** `architecture:generate` refreshes the branch comparison only and never scans the last commit. */\nexport const GENERATE_IMPACT_KINDS: readonly ImpactKind[] = [ImpactKind.BRANCH];\n\n/** One scan as the sidecar carries it; the page reads `window.__WP_IMPACT__.scans`. */\nexport class ImpactReport {\n constructor(\n public readonly kind: ImpactKind,\n /** What the page says this scan compares, e.g. \"Changed on this branch (since abc1234)\". */\n public readonly label: string,\n /** The working tree had uncommitted or untracked changes (they count in the scan). */\n public readonly dirty: boolean,\n public readonly available: boolean,\n /** Why the scan is unavailable; for an available but EMPTY scan, the note the page shows. */\n public readonly reason: string,\n /** The base's short sha ('' when there is none). */\n public readonly base: string,\n /** Affected AND owning a changed file (node ids). */\n public readonly touched: string[],\n /** Affected but owning no changed file (node ids): their ci re-runs. */\n public readonly affected: string[],\n /**\n * Transitive dependencies of the affected set that are NOT affected (node ids). `ci` dependsOn\n * `^build`, so they must exist as build output although their inputs did not change: nx\n * restores them from cache when it has them and compiles them otherwise. Which of the two\n * happens is not knowable here, so the page claims neither.\n */\n public readonly buildInputs: string[],\n public readonly changedFiles: number,\n /**\n * Transitive dependencies of the TOUCHED set that are not touched themselves (node ids): the\n * Filter popover's \"Changed + what they use\". (\"Changed + what uses them\" is touched +\n * affected, which nx already computed as the dependents whose tests re-run.)\n */\n public readonly dependencies: string[],\n /**\n * Changed files no project owns (repo-relative, sorted): workspace-global inputs such as\n * pnpm-lock.yaml. When nothing is touched they are WHY everything is affected, and the\n * legend names them. Empty when nx's file ownership was unavailable — then nothing is known.\n */\n public readonly globalFiles: string[],\n ) {}\n}\n\n/** The sidecar's one global: every scan, and the one the page shows first. */\nexport class ImpactSidecarData {\n constructor(\n public readonly scans: ImpactReport[],\n public readonly defaultKind: ImpactKind,\n ) {}\n}\n\n/** A scan (or a whole plan) that could not produce an answer. */\nexport class ImpactUnavailable {\n constructor(\n public readonly reason: string,\n public readonly label: string = 'Impact',\n ) {}\n}\n\n/** One comparison to scan. `base` is a full sha; null when it does not exist (`reason` says why). */\nexport class ImpactComparison {\n constructor(\n public readonly kind: ImpactKind,\n public readonly base: string | null,\n public readonly label: string,\n public readonly dirty: boolean,\n public readonly reason: string,\n ) {}\n}\n\n/** The comparisons that exist where HEAD is, and which one the page shows first. */\nexport class ImpactPlan {\n constructor(\n public readonly comparisons: ImpactComparison[],\n public readonly defaultKind: ImpactKind,\n ) {}\n}\n\n/** The few git facts the resolver needs. Injected so specs never spawn git. */\nexport class ImpactGit {\n /** The checked-out branch's short name; null on a detached HEAD (or without git). */\n branch(root: string): string | null {\n return this.run(root, ['symbolic-ref', '--short', '-q', 'HEAD']);\n }\n\n /** The full sha `ref` names; null when it does not resolve (e.g. `HEAD^` on the first commit). */\n sha(root: string, ref: string): string | null {\n return this.run(root, ['rev-parse', '--verify', '-q', `${ref}^{commit}`]);\n }\n\n /** `git status --porcelain` is non-empty: uncommitted or untracked work. */\n dirty(root: string): boolean {\n return (this.run(root, ['status', '--porcelain']) ?? '') !== '';\n }\n\n /** HEAD's subject line ('' when unknown). */\n subject(root: string): string {\n return this.run(root, ['log', '-1', '--format=%s', 'HEAD']) ?? '';\n }\n\n /** Trimmed stdout, or null on any failure. */\n private run(root: string, args: string[]): string | null {\n const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', stdio: 'pipe' });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout.trim();\n }\n}\n\n/**\n * Decides which comparisons exist from where HEAD is (#1163):\n *\n * | where | comparisons | default |\n * |---|---|---|\n * | `main` (by branch NAME) | last commit | last commit |\n * | detached HEAD | last commit | last commit |\n * | feature branch with commits past the fork point | branch, last commit | branch |\n * | fresh feature branch (fork point = HEAD) | branch | branch |\n *\n * HEAD with no parent leaves the last commit unavailable, with `ROOT_COMMIT_REASON`.\n */\nexport class ImpactScopeResolver {\n constructor(\n private readonly git: ImpactGit = new ImpactGit(),\n private readonly diffScope: DiffScope = new DiffScope(),\n ) {}\n\n resolve(root: string): ImpactPlan | ImpactUnavailable {\n const head = this.git.sha(root, 'HEAD');\n if (head === null) return new ImpactUnavailable('git cannot resolve HEAD (not a git checkout, or git is unavailable)');\n const branch = this.git.branch(root);\n const dirty = this.git.dirty(root);\n const commit = this.lastCommit(root, dirty);\n // On main the decision is the branch NAME, never merge-base: unpushed commits still mean \"last commit\".\n if (branch === null || branch === MAIN_BRANCH) return new ImpactPlan([commit], ImpactKind.COMMIT);\n const fork = this.diffScope.detectBase(root);\n if (fork === null) {\n const reason = 'no fork point: neither origin/main nor main resolves';\n return new ImpactPlan(\n [new ImpactComparison(ImpactKind.BRANCH, null, 'Changed on this branch', dirty, reason)],\n ImpactKind.BRANCH,\n );\n }\n const onBranch = new ImpactComparison(\n ImpactKind.BRANCH,\n fork,\n `Changed on this branch (since ${this.short(fork)})${dirty ? UNCOMMITTED_SUFFIX : ''}`,\n dirty,\n '',\n );\n // A fresh branch has no commit of its own: \"last commit\" would be main's last PR, not its work.\n if (this.sameCommit(fork, head)) return new ImpactPlan([onBranch], ImpactKind.BRANCH);\n return new ImpactPlan([onBranch, commit], ImpactKind.BRANCH);\n }\n\n private lastCommit(root: string, dirty: boolean): ImpactComparison {\n const parent = this.git.sha(root, 'HEAD^');\n if (parent === null) return new ImpactComparison(ImpactKind.COMMIT, null, 'Last commit', dirty, ROOT_COMMIT_REASON);\n const subject = this.git.subject(root);\n const label =\n `Changed since ${this.short(parent)}${subject === '' ? '' : ` — ${subject}`}` +\n (dirty ? UNCOMMITTED_SUFFIX : '');\n return new ImpactComparison(ImpactKind.COMMIT, parent, label, dirty, '');\n }\n\n /** detectBase may hand back a full or an abbreviated sha; compare on the shorter of the two. */\n private sameCommit(a: string, b: string): boolean {\n const length = Math.min(a.length, b.length);\n return length > 0 && a.slice(0, length) === b.slice(0, length);\n }\n\n private short(sha: string): string {\n return sha.slice(0, 7);\n }\n}\n\n/** Runs the workspace's own nx CLI. Injected so specs never spawn nx. */\nexport class NxCli {\n /** stdout, or null when nx cannot be found, exits non-zero or exceeds the timeout. */\n run(workspaceRoot: string, args: string[], timeoutMs: number): string | null {\n const bin = this.binary(workspaceRoot);\n if (bin === null) return null;\n const result = spawnSync(process.execPath, [bin, ...args], {\n cwd: workspaceRoot,\n encoding: 'utf8',\n timeout: timeoutMs,\n stdio: 'pipe',\n maxBuffer: 16 * 1024 * 1024,\n });\n if (result.error !== undefined || result.status !== 0) return null;\n return result.stdout;\n }\n\n /** Resolved the way node would from the workspace, so a linked worktree finds its main tree's nx. */\n private binary(workspaceRoot: string): string | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- a workspace without nx resolves nothing; that is an answer, not an error\n try {\n return require.resolve('nx/bin/nx.js', { paths: [workspaceRoot] });\n } catch (err: unknown) {\n const error = toError(err);\n void error; // no nx installed here — Impact is simply unavailable\n return null;\n }\n }\n}\n\n/** nx's own file → project ownership, from the public devkit file map. Injected for specs. */\nexport class ProjectFileOwners {\n async owners(): Promise<Map<string, string>> {\n const fileMap = await createProjectFileMapUsingProjectGraph(await createProjectGraphAsync());\n const owners = new Map<string, string>();\n for (const project of Object.keys(fileMap)) {\n for (const data of fileMap[project]) owners.set(data.file, project);\n }\n return owners;\n }\n}\n\n/**\n * nx's ownership map, computed at most ONCE however many comparisons ask for it. Failing is not\n * fatal: the set then shows in one shade, all \"affected\".\n */\nexport class SharedOwners {\n private pending: Promise<Map<string, string>> | null = null;\n\n constructor(private readonly ownership: ProjectFileOwners) {}\n\n get(): Promise<Map<string, string>> {\n if (this.pending === null) this.pending = this.load();\n return this.pending;\n }\n\n private async load(): Promise<Map<string, string>> {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- the second shade is optional\n try {\n return await this.ownership.owners();\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Impact: nx file ownership unavailable (${error.message}); showing one shade`);\n return new Map<string, string>();\n }\n }\n}\n\n/** The sets one scan found; an empty one is still an answer. */\nclass ImpactSets {\n constructor(\n public readonly touched: string[] = [],\n public readonly affected: string[] = [],\n public readonly buildInputs: string[] = [],\n public readonly dependencies: string[] = [],\n public readonly globalFiles: string[] = [],\n ) {}\n}\n\nexport class NxImpactScanner {\n private readonly names = new GraphNames();\n\n constructor(\n private readonly diffScope: DiffScope = new DiffScope(),\n private readonly nx: NxCli = new NxCli(),\n private readonly ownership: ProjectFileOwners = new ProjectFileOwners(),\n ) {}\n\n /** Every comparison, in order, sharing one ownership map. */\n async scanAll(\n workspaceRoot: string,\n graph: EnhancedGraph,\n comparisons: ImpactComparison[],\n ): Promise<(ImpactReport | ImpactUnavailable)[]> {\n const owners = new SharedOwners(this.ownership);\n const results: (ImpactReport | ImpactUnavailable)[] = [];\n for (const comparison of comparisons) results.push(await this.scan(workspaceRoot, graph, comparison, owners));\n return results;\n }\n\n /** One comparison: nx affected from ITS base, with NO `--head`, so uncommitted work counts. */\n async scan(\n workspaceRoot: string,\n graph: EnhancedGraph,\n comparison: ImpactComparison,\n owners: SharedOwners = new SharedOwners(this.ownership),\n ): Promise<ImpactReport | ImpactUnavailable> {\n const base = comparison.base;\n if (base === null) return this.report(comparison, false, comparison.reason, '', 0, new ImpactSets());\n const json = this.nx.run(workspaceRoot, ['show', 'projects', '--affected', `--base=${base}`, '--json'], NX_TIMEOUT_MS);\n if (json === null)\n return new ImpactUnavailable(`nx show projects --affected failed or took over ${NX_TIMEOUT_MS / 1000}s`, comparison.label);\n const affectedSet = this.parse(json, graph);\n if (affectedSet === null)\n return new ImpactUnavailable('nx show projects --affected printed something other than a JSON list', comparison.label);\n const options = new ChangedFilesOptions();\n options.tsOnly = false;\n options.includeDeletions = true;\n const files = this.diffScope.getChangedFiles(workspaceRoot, base, undefined, options);\n const short = base.slice(0, 7);\n if (affectedSet.length === 0)\n return this.report(comparison, true, this.emptyNote(comparison, files.length, short), short, files.length, new ImpactSets());\n const map = await owners.get();\n const touched = this.touched(affectedSet, files, map);\n const rest = affectedSet.filter((project: string): boolean => !touched.includes(project));\n const sets = new ImpactSets(\n this.ids(touched),\n this.ids(rest),\n this.ids(this.buildInputs(affectedSet, graph)),\n this.ids(this.buildInputs(touched, graph)),\n this.globalFiles(files, map),\n );\n return this.report(comparison, true, '', short, files.length, sets);\n }\n\n /**\n * An empty scan is still an ANSWER (available, zero sets): a fresh branch with a clean tree has\n * simply changed nothing yet, and the page says so instead of disabling Impact.\n */\n emptyNote(comparison: ImpactComparison, changedFiles: number, short: string): string {\n if (changedFiles === 0 && comparison.kind === ImpactKind.BRANCH) return NOTHING_CHANGED_ON_BRANCH;\n if (changedFiles === 0) return `Nothing changed since ${short}`;\n return `Nothing affected since ${short}`;\n }\n\n /** Changed files no project owns; none are claimed when ownership itself was unavailable. */\n globalFiles(files: string[], owners: Map<string, string>): string[] {\n if (owners.size === 0) return [];\n return files.filter((file: string): boolean => !owners.has(file)).sort();\n }\n\n /**\n * Every transitive dependency of `roots` that is not itself one of them: a pure downward walk over\n * the graph's own dependsOn edges, no nx call. Of the affected set these are the build inputs; of\n * the touched set, \"what they use\".\n */\n buildInputs(roots: string[], graph: EnhancedGraph): string[] {\n const inSet = new Set<string>(roots);\n const seen = new Set<string>(roots);\n const stack = [...roots];\n const inputs: string[] = [];\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 if (!inSet.has(dep) && graph[dep] !== undefined) inputs.push(dep);\n }\n }\n return inputs.sort();\n }\n\n /** Affected projects that own one of the changed files, by nx's own ownership. */\n touched(affected: string[], files: string[], owners: Map<string, string>): string[] {\n const owning = new Set<string>();\n for (const file of files) {\n const owner = owners.get(file);\n if (owner !== undefined) owning.add(owner);\n }\n return affected.filter((project: string): boolean => owning.has(project));\n }\n\n /** The JSON list, narrowed to projects the graph draws; null when it is not a list of names. */\n parse(json: string, graph: EnhancedGraph): string[] | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- untrusted CLI output; anything unparseable is reported as a reason\n try {\n // webpieces-disable no-any-unknown -- CLI JSON is untrusted until narrowed below\n const parsed: unknown = JSON.parse(json);\n if (!Array.isArray(parsed)) return null;\n return parsed\n .filter((name: unknown): name is string => typeof name === 'string' && graph[name] !== undefined)\n .sort();\n } catch (err: unknown) {\n const error = toError(err);\n void error; // not JSON — the caller reports it\n return null;\n }\n }\n\n private report(\n comparison: ImpactComparison,\n available: boolean,\n reason: string,\n base: string,\n changedFiles: number,\n sets: ImpactSets,\n ): ImpactReport {\n return new ImpactReport(\n comparison.kind,\n comparison.label,\n comparison.dirty,\n available,\n reason,\n base,\n sets.touched,\n sets.affected,\n sets.buildInputs,\n changedFiles,\n sets.dependencies,\n sets.globalFiles,\n );\n }\n\n private ids(projects: string[]): string[] {\n return projects.map((project: string): string => this.names.getNodeId(project));\n }\n}\n\n/** Writes (or removes) the sidecar, self-ignored, beside architecture/dependencies.html. */\nexport class ImpactSidecar {\n /** Never throws: a sidecar that cannot be written just leaves Impact disabled on the page. */\n write(architectureDir: string, data: ImpactSidecarData): string | null {\n const dir = path.join(architectureDir, IMPACT_SIDECAR_DIR);\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; a write failure must never fail generation\n try {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(\n path.join(dir, '.gitignore'),\n '# Written by architecture:generate and architecture:visualize. Per-checkout data: never commit it.\\n*\\n',\n 'utf-8',\n );\n const file = path.join(dir, IMPACT_SIDECAR_FILE);\n fs.writeFileSync(file, this.script(data), 'utf-8');\n return file;\n } catch (err: unknown) {\n const error = toError(err);\n console.warn(`⚠️ Could not write the impact sidecar: ${error.message}`);\n return null;\n }\n }\n\n /** A stale sidecar from an earlier checkout must never be shown as this one's impact. */\n remove(architectureDir: string): void {\n fs.rmSync(path.join(architectureDir, IMPACT_SIDECAR_DIR, IMPACT_SIDECAR_FILE), { force: true });\n }\n\n script(data: ImpactSidecarData): string {\n const json = JSON.stringify(data).replace(/</g, '\\\\u003c');\n return `// Generated by architecture:generate or architecture:visualize for one checkout. Gitignored; do not commit.\\nwindow.__WP_IMPACT__ = ${json};\\n`;\n }\n}\n\n/** The executors' one call: resolve, scan each comparison, write, and report one line per comparison. */\nexport class ImpactRefresh {\n constructor(\n private readonly scanner: NxImpactScanner = new NxImpactScanner(),\n private readonly sidecar: ImpactSidecar = new ImpactSidecar(),\n private readonly resolver: ImpactScopeResolver = new ImpactScopeResolver(),\n ) {}\n\n /**\n * Scans the comparisons in `kinds` that exist here (`VISUALIZE_IMPACT_KINDS` /\n * `GENERATE_IMPACT_KINDS`). Never throws — Impact is optional, generation is not.\n */\n async run(\n architectureDir: string,\n workspaceRoot: string,\n graph: EnhancedGraph,\n kinds: readonly ImpactKind[],\n ): Promise<string> {\n const started = Date.now();\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- impact is optional; any failure becomes a reason, never a failed generation\n try {\n const plan = this.resolver.resolve(workspaceRoot);\n if (plan instanceof ImpactUnavailable) return this.none(architectureDir, plan.reason, started);\n const comparisons = plan.comparisons.filter(\n (comparison: ImpactComparison): boolean => kinds.includes(comparison.kind),\n );\n if (comparisons.length === 0)\n return this.none(\n architectureDir,\n 'no Changed-on-this-branch comparison on main or a detached HEAD; pnpm arch:visualize shows the last commit',\n started,\n );\n const results = await this.scanner.scanAll(workspaceRoot, graph, comparisons);\n const lines = results.map((result: ImpactReport | ImpactUnavailable): string => this.line(result));\n const scans = results.filter(\n (result: ImpactReport | ImpactUnavailable): result is ImpactReport => result instanceof ImpactReport,\n );\n const cost = `${Date.now() - started} ms`;\n if (scans.length === 0) {\n this.sidecar.remove(architectureDir);\n return [...lines, `ℹ️ Impact disabled, no sidecar written (${cost})`].join('\\n');\n }\n const file = this.sidecar.write(architectureDir, new ImpactSidecarData(scans, this.defaultKind(scans, plan.defaultKind)));\n return [...lines, file === null ? `⚠️ Impact sidecar not written (${cost})` : `→ ${file} (${cost})`].join('\\n');\n } catch (err: unknown) {\n const error = toError(err);\n return `⚠️ Impact skipped: ${error.message}`;\n }\n }\n\n /** The plan's default when that scan is available, else the first available one. */\n private defaultKind(scans: ImpactReport[], preferred: ImpactKind): ImpactKind {\n const usable = scans.filter((scan: ImpactReport): boolean => scan.available);\n if (usable.length === 0 || usable.some((scan: ImpactReport): boolean => scan.kind === preferred)) return preferred;\n return usable[0].kind;\n }\n\n private none(architectureDir: string, reason: string, started: number): string {\n this.sidecar.remove(architectureDir);\n return `ℹ️ Impact disabled, no sidecar written: ${reason} (${Date.now() - started} ms)`;\n }\n\n private line(result: ImpactReport | ImpactUnavailable): string {\n if (result instanceof ImpactUnavailable || !result.available)\n return `ℹ️ Impact · ${result.label}: unavailable — ${result.reason}`;\n if (result.reason !== '') return `✅ Impact · ${result.label}: ${result.reason}`;\n return (\n `✅ Impact · ${result.label}: ${result.touched.length} touched · ${result.affected.length} affected · ` +\n `${result.buildInputs.length} build inputs, from ${result.changedFiles} changed file(s)`\n );\n }\n}\n"]}
@@ -66,7 +66,7 @@ class GraphLegend {
66
66
  }
67
67
  /** Which box is which is only known from the branch's sidecar, so all four rows always show. */
68
68
  impactRows() {
69
- return (this.row('touched', this.solid(graph_color_modes_1.IMPACT_TOUCHED, '', ''), 'touched', 'files changed on this branch') +
69
+ return (this.row('touched', this.solid(graph_color_modes_1.IMPACT_TOUCHED, '', ''), 'touched', 'its files changed') +
70
70
  this.row('affected', this.solid(graph_color_modes_1.IMPACT_AFFECTED, graph_color_modes_1.IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +
71
71
  this.row('build-input', this.solid(graph_color_modes_1.IMPACT_BUILD_INPUT, graph_color_modes_1.IMPACT_BUILD_INPUT_BORDER, '3 2'), 'build input', 'compiled or restored from cache, unchanged') +
72
72
  this.row('untouched', this.solid(graph_color_modes_1.IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +
@@ -1 +1 @@
1
- {"version":3,"file":"graph-legend.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-legend.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;;AAEH,oDAAwE;AACxE,2DAc6B;AAE7B,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,KAAK,GAAG,SAAS,CAAC;AAExB,MAAa,WAAW;IACpB,+FAA+F;IAC/F,QAAQ,CAAC,KAAkB;QACvB,OAAO,CACH,IAAI,CAAC,OAAO,CAAC,6BAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,6BAAS,CAAC,YAAY,EAAE,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;YACxE,IAAI,CAAC,OAAO,CAAC,6BAAS,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,CAC1D,CAAC;IACN,CAAC;IAED,uFAAuF;IACvF,OAAO;QACH,OAAO,CACH,8BAA8B;YAC9B,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,0CAA0C,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,6CAA6C,CAAC;YAC3F,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,KAAK,EAAE,mBAAmB,EAAE,wCAAwC,CAAC;YAC7F,QAAQ,CACX,CAAC;IACN,CAAC;IAEO,OAAO,CAAC,IAAe,EAAE,IAAY,EAAE,MAAe;QAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;QACrC,OAAO,oDAAoD,IAAI,IAAI,IAAI,IAAI,IAAI,QAAQ,CAAC;IAC5F,CAAC;IAED,kGAAkG;IAClG,WAAW,CAAC,KAAkB;QAC1B,MAAM,OAAO,GAAG,CAAC,SAAyB,EAAW,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAC7F,MAAM,OAAO,GAAqB;YAC9B,GAAG,8BAAU,CAAC,GAAG,CAAC,CAAC,IAAoB,EAAkB,EAAE,CAAC,oCAAgB,CAAC,IAAI,CAAC,CAAC;YACnF,GAAG,4BAAgB,CAAC,GAAG,CAAC,CAAC,CAAiB,EAAkB,EAAE,CAAC,oCAAgB,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CACtF,CAAC,KAAqB,EAAW,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,CACtD;SACJ,CAAC;QACF,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC;gBAAE,SAAS;YACxC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;QACjG,CAAC;QACD,IAAI,KAAK,CAAC,YAAY;YAClB,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,CAAC,aAAa,EAAE,EAAE,kBAAkB,EAAE,8BAA8B,CAAC,CAAC;QACxG,IAAI,KAAK,CAAC,SAAS;YACf,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,oCAAgB,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,kBAAkB,EAAE,uBAAuB,CAAC,CAAC;QAChH,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gBAAgB,CAAC,KAAkB;QAC/B,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,+BAAW,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YAChD,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;QACxF,CAAC;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gGAAgG;IACxF,UAAU;QACd,OAAO,CACH,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,kCAAc,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,SAAS,EAAE,8BAA8B,CAAC;YAClG,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,mCAAe,EAAE,kCAAc,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,wBAAwB,CAAC;YAC3G,IAAI,CAAC,GAAG,CACJ,aAAa,EACb,IAAI,CAAC,KAAK,CAAC,sCAAkB,EAAE,6CAAyB,EAAE,KAAK,CAAC,EAChE,aAAa,EACb,4CAA4C,CAC/C;YACD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,oCAAgB,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,WAAW,EAAE,wBAAwB,CAAC;YAClG,oDAAoD,CACvD,CAAC;IACN,CAAC;IAEO,GAAG,CAAC,GAAW,EAAE,MAAc,EAAE,KAAa,EAAE,MAAc;QAClE,OAAO,CACH,8CAA8C,GAAG,KAAK,MAAM,EAAE;YAC9D,SAAS,KAAK,UAAU,MAAM,uBAAuB,CACxD,CAAC;IACN,CAAC;IAEO,IAAI;QACR,OAAO,eAAe,QAAQ,aAAa,QAAQ,kBAAkB,QAAQ,IAAI,QAAQ,uBAAuB,CAAC;IACrH,CAAC;IAEO,KAAK;QACT,OAAO,gCAAgC,QAAQ,GAAG,CAAC,aAAa,QAAQ,GAAG,CAAC,yBAAyB,KAAK,WAAW,CAAC;IAC1H,CAAC;IAED,gFAAgF;IACxE,SAAS,CAAC,KAAqB;QACnC,MAAM,IAAI,GAAG,gBAAgB,QAAQ,aAAa,QAAQ,WAAW,oCAAgB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC;QAC7G,IAAI,KAAK,CAAC,MAAM,EAAE;YAAE,OAAO,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,4BAA4B,QAAQ,GAAG,CAAC,aAAa,QAAQ,GAAG,CAAC,kBAAkB,KAAK,CAAC,KAAK,KAAK,CAAC;QAClH,OAAO,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACrD,CAAC;IAEO,aAAa;QACjB,MAAM,KAAK,GAAG,QAAQ,GAAG,8BAAU,CAAC,MAAM,CAAC;QAC3C,MAAM,OAAO,GAAG,8BAAU,CAAC,GAAG,CAC1B,CAAC,IAAoB,EAAE,KAAa,EAAU,EAAE,CAC5C,YAAY,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,QAAQ,WAAW,oCAAgB,CAAC,IAAI,CAAC,CAAC,KAAK,KAAK,CAC1I,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACX,OAAO,IAAI,CAAC,IAAI,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IAChD,CAAC;IAEO,UAAU,CAAC,KAAgB;QAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,kFAAkF;IAC1E,KAAK,CAAC,IAAY,EAAE,OAAe,EAAE,IAAY;QACrD,MAAM,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,sBAAsB,IAAI,GAAG,CAAC;QAChE,MAAM,IAAI,GACN,OAAO,KAAK,EAAE;YACV,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,4BAA4B,QAAQ,GAAG,CAAC,aAAa,QAAQ,GAAG,CAAC,yBAAyB,OAAO,qBAAqB,MAAM,IAAI,CAAC;QAC3I,OAAO,IAAI,CAAC,IAAI,EAAE,GAAG,gBAAgB,QAAQ,aAAa,QAAQ,WAAW,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;IACjH,CAAC;IAEO,OAAO,CAAC,KAAa,EAAE,IAAY,EAAE,KAAa,EAAE,MAAc;QACtE,MAAM,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,sBAAsB,IAAI,GAAG,CAAC;QAChE,MAAM,IAAI,GAAG,6FAA6F,KAAK,uBAAuB,MAAM,UAAU,CAAC;QACvJ,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;CACJ;AA3HD,kCA2HC","sourcesContent":["/**\n * The architecture graph's legend, generated per color mode FROM THE PROJECTS ACTUALLY DRAWN.\n *\n * The hand-written legend this replaces listed five frameworks when six existed and never mentioned\n * the `app` and `bundle` borders the renderer drew (#1155). Generating the rows from the same tables\n * the renderer colors with (graph-color-modes.ts) means a color in use always has a row, and a color\n * nothing uses never does.\n *\n * Swatches are inline SVG drawn with presentation attributes, never `style=`, so the page carries\n * no inline CSS.\n */\n\nimport { KNOWN_FRAMEWORKS, KnownFramework } from '@webpieces/rules-sdk';\nimport {\n BASE_ORDER,\n FRAMEWORK_STYLES,\n FrameworkStyle,\n GraphMode,\n IMPACT_AFFECTED,\n IMPACT_BUILD_INPUT,\n IMPACT_BUILD_INPUT_BORDER,\n IMPACT_TOUCHED,\n IMPACT_UNTOUCHED,\n LegendFacts,\n NO_RUNTIME_COLOR,\n ROLE_STYLES,\n RoleStyle,\n} from './graph-color-modes';\n\nconst SWATCH_W = 26;\nconst SWATCH_H = 16;\nconst FRAME = '#3a3c46';\n\nexport class GraphLegend {\n /** One section per mode; the page shows the section of the current mode and hides the rest. */\n sections(facts: LegendFacts): string {\n return (\n this.section(GraphMode.RUNTIME, this.runtimeRows(facts), false) +\n this.section(GraphMode.ARCHITECTURE, this.architectureRows(facts), true) +\n this.section(GraphMode.IMPACT, this.impactRows(), true)\n );\n }\n\n /** The edge key: why a line exists. Static — every graph can carry every edge kind. */\n edgeKey(): string {\n return (\n '<div class=\"wp-legend-list\">' +\n this.edgeRow('#e9eaf0', '', 'uses', 'calls the API, or a plain library import') +\n this.edgeRow('#e9eaf0', '4 3', 'implements', 'serves the API (labeled with its contracts)') +\n this.edgeRow('#7aa2ff', '4 3', 'uses + implements', 'implements some contracts, uses others') +\n '</div>'\n );\n }\n\n private section(mode: GraphMode, rows: string, hidden: boolean): string {\n const hide = hidden ? ' hidden' : '';\n return `<div class=\"wp-legend-list\" data-wp-legend-mode=\"${mode}\"${hide}>${rows}</div>`;\n }\n\n /** Base runtimes first (stripe order), then specializations, in the shared vocabulary's order. */\n runtimeRows(facts: LegendFacts): string {\n const present = (framework: KnownFramework): boolean => facts.frameworks.includes(framework);\n const ordered: FrameworkStyle[] = [\n ...BASE_ORDER.map((base: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[base]),\n ...KNOWN_FRAMEWORKS.map((f: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[f]).filter(\n (style: FrameworkStyle): boolean => !style.isBase(),\n ),\n ];\n let rows = '';\n for (const style of ordered) {\n if (!present(style.framework)) continue;\n rows += this.row(style.framework, this.envSwatch(style), style.framework, style.description);\n }\n if (facts.multiRuntime)\n rows += this.row('multi', this.stripesSwatch(), 'several runtimes', 'one equal stripe per runtime');\n if (facts.noRuntime)\n rows += this.row('none', this.solid(NO_RUNTIME_COLOR, '', ''), 'no framework tag', 'runs nowhere declared');\n return rows;\n }\n\n architectureRows(facts: LegendFacts): string {\n let rows = '';\n for (const style of ROLE_STYLES) {\n if (!facts.roles.includes(style.role)) continue;\n rows += this.row(style.role, this.roleSwatch(style), style.role, style.description);\n }\n return rows;\n }\n\n /** Which box is which is only known from the branch's sidecar, so all four rows always show. */\n private impactRows(): string {\n return (\n this.row('touched', this.solid(IMPACT_TOUCHED, '', ''), 'touched', 'files changed on this branch') +\n this.row('affected', this.solid(IMPACT_AFFECTED, IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +\n this.row(\n 'build-input',\n this.solid(IMPACT_BUILD_INPUT, IMPACT_BUILD_INPUT_BORDER, '3 2'),\n 'build input',\n 'compiled or restored from cache, unchanged',\n ) +\n this.row('untouched', this.solid(IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +\n '<p class=\"wp-impact-note\" data-wp-impact-note></p>'\n );\n }\n\n private row(key: string, swatch: string, title: string, detail: string): string {\n return (\n `<div class=\"wp-legend-row\" data-wp-legend=\"${key}\">${swatch}` +\n `<span>${title}<small>${detail}</small></span></div>`\n );\n }\n\n private open(): string {\n return `<svg width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" viewBox=\"0 0 ${SWATCH_W} ${SWATCH_H}\" aria-hidden=\"true\">`;\n }\n\n private frame(): string {\n return `<rect x=\"0.5\" y=\"0.5\" width=\"${SWATCH_W - 1}\" height=\"${SWATCH_H - 1}\" fill=\"none\" stroke=\"${FRAME}\"/></svg>`;\n }\n\n /** A base runtime is solid; a specialization is its inset on the base color. */\n private envSwatch(style: FrameworkStyle): string {\n const base = `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[style.base].color}\"/>`;\n if (style.isBase()) return this.open() + base + this.frame();\n const inset = `<rect x=\"4\" y=\"4\" width=\"${SWATCH_W - 8}\" height=\"${SWATCH_H - 8}\" rx=\"2\" fill=\"${style.color}\"/>`;\n return this.open() + base + inset + this.frame();\n }\n\n private stripesSwatch(): string {\n const width = SWATCH_W / BASE_ORDER.length;\n const stripes = BASE_ORDER.map(\n (base: KnownFramework, index: number): string =>\n `<rect x=\"${(index * width).toFixed(2)}\" width=\"${width.toFixed(2)}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[base].color}\"/>`,\n ).join('');\n return this.open() + stripes + this.frame();\n }\n\n private roleSwatch(style: RoleStyle): string {\n return this.solid(style.fill, '', '');\n }\n\n /** A filled swatch, optionally with an inner ring (dashed when `dash` is set). */\n private solid(fill: string, outline: string, dash: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const ring =\n outline === ''\n ? ''\n : `<rect x=\"2\" y=\"2\" width=\"${SWATCH_W - 4}\" height=\"${SWATCH_H - 4}\" fill=\"none\" stroke=\"${outline}\" stroke-width=\"2\"${dashed}/>`;\n return this.open() + `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${fill}\"/>` + ring + this.frame();\n }\n\n private edgeRow(color: string, dash: string, title: string, detail: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const line = `<svg width=\"28\" height=\"10\" aria-hidden=\"true\"><line x1=\"0\" y1=\"5\" x2=\"28\" y2=\"5\" stroke=\"${color}\" stroke-width=\"1.6\"${dashed}/></svg>`;\n return this.row(title, line, title, detail);\n }\n}\n"]}
1
+ {"version":3,"file":"graph-legend.js","sourceRoot":"","sources":["../../../../../../packages/tooling/nx-webpieces-rules/src/lib/graph-legend.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The architecture graph's legend, generated per color mode FROM THE PROJECTS ACTUALLY DRAWN.\n *\n * The hand-written legend this replaces listed five frameworks when six existed and never mentioned\n * the `app` and `bundle` borders the renderer drew (#1155). Generating the rows from the same tables\n * the renderer colors with (graph-color-modes.ts) means a color in use always has a row, and a color\n * nothing uses never does.\n *\n * Swatches are inline SVG drawn with presentation attributes, never `style=`, so the page carries\n * no inline CSS.\n */\n\nimport { KNOWN_FRAMEWORKS, KnownFramework } from '@webpieces/rules-sdk';\nimport {\n BASE_ORDER,\n FRAMEWORK_STYLES,\n FrameworkStyle,\n GraphMode,\n IMPACT_AFFECTED,\n IMPACT_BUILD_INPUT,\n IMPACT_BUILD_INPUT_BORDER,\n IMPACT_TOUCHED,\n IMPACT_UNTOUCHED,\n LegendFacts,\n NO_RUNTIME_COLOR,\n ROLE_STYLES,\n RoleStyle,\n} from './graph-color-modes';\n\nconst SWATCH_W = 26;\nconst SWATCH_H = 16;\nconst FRAME = '#3a3c46';\n\nexport class GraphLegend {\n /** One section per mode; the page shows the section of the current mode and hides the rest. */\n sections(facts: LegendFacts): string {\n return (\n this.section(GraphMode.RUNTIME, this.runtimeRows(facts), false) +\n this.section(GraphMode.ARCHITECTURE, this.architectureRows(facts), true) +\n this.section(GraphMode.IMPACT, this.impactRows(), true)\n );\n }\n\n /** The edge key: why a line exists. Static — every graph can carry every edge kind. */\n edgeKey(): string {\n return (\n '<div class=\"wp-legend-list\">' +\n this.edgeRow('#e9eaf0', '', 'uses', 'calls the API, or a plain library import') +\n this.edgeRow('#e9eaf0', '4 3', 'implements', 'serves the API (labeled with its contracts)') +\n this.edgeRow('#7aa2ff', '4 3', 'uses + implements', 'implements some contracts, uses others') +\n '</div>'\n );\n }\n\n private section(mode: GraphMode, rows: string, hidden: boolean): string {\n const hide = hidden ? ' hidden' : '';\n return `<div class=\"wp-legend-list\" data-wp-legend-mode=\"${mode}\"${hide}>${rows}</div>`;\n }\n\n /** Base runtimes first (stripe order), then specializations, in the shared vocabulary's order. */\n runtimeRows(facts: LegendFacts): string {\n const present = (framework: KnownFramework): boolean => facts.frameworks.includes(framework);\n const ordered: FrameworkStyle[] = [\n ...BASE_ORDER.map((base: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[base]),\n ...KNOWN_FRAMEWORKS.map((f: KnownFramework): FrameworkStyle => FRAMEWORK_STYLES[f]).filter(\n (style: FrameworkStyle): boolean => !style.isBase(),\n ),\n ];\n let rows = '';\n for (const style of ordered) {\n if (!present(style.framework)) continue;\n rows += this.row(style.framework, this.envSwatch(style), style.framework, style.description);\n }\n if (facts.multiRuntime)\n rows += this.row('multi', this.stripesSwatch(), 'several runtimes', 'one equal stripe per runtime');\n if (facts.noRuntime)\n rows += this.row('none', this.solid(NO_RUNTIME_COLOR, '', ''), 'no framework tag', 'runs nowhere declared');\n return rows;\n }\n\n architectureRows(facts: LegendFacts): string {\n let rows = '';\n for (const style of ROLE_STYLES) {\n if (!facts.roles.includes(style.role)) continue;\n rows += this.row(style.role, this.roleSwatch(style), style.role, style.description);\n }\n return rows;\n }\n\n /** Which box is which is only known from the branch's sidecar, so all four rows always show. */\n private impactRows(): string {\n return (\n this.row('touched', this.solid(IMPACT_TOUCHED, '', ''), 'touched', 'its files changed') +\n this.row('affected', this.solid(IMPACT_AFFECTED, IMPACT_TOUCHED, ''), 'affected', 'tests and build re-run') +\n this.row(\n 'build-input',\n this.solid(IMPACT_BUILD_INPUT, IMPACT_BUILD_INPUT_BORDER, '3 2'),\n 'build input',\n 'compiled or restored from cache, unchanged',\n ) +\n this.row('untouched', this.solid(IMPACT_UNTOUCHED, '', ''), 'untouched', 'not part of this build') +\n '<p class=\"wp-impact-note\" data-wp-impact-note></p>'\n );\n }\n\n private row(key: string, swatch: string, title: string, detail: string): string {\n return (\n `<div class=\"wp-legend-row\" data-wp-legend=\"${key}\">${swatch}` +\n `<span>${title}<small>${detail}</small></span></div>`\n );\n }\n\n private open(): string {\n return `<svg width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" viewBox=\"0 0 ${SWATCH_W} ${SWATCH_H}\" aria-hidden=\"true\">`;\n }\n\n private frame(): string {\n return `<rect x=\"0.5\" y=\"0.5\" width=\"${SWATCH_W - 1}\" height=\"${SWATCH_H - 1}\" fill=\"none\" stroke=\"${FRAME}\"/></svg>`;\n }\n\n /** A base runtime is solid; a specialization is its inset on the base color. */\n private envSwatch(style: FrameworkStyle): string {\n const base = `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[style.base].color}\"/>`;\n if (style.isBase()) return this.open() + base + this.frame();\n const inset = `<rect x=\"4\" y=\"4\" width=\"${SWATCH_W - 8}\" height=\"${SWATCH_H - 8}\" rx=\"2\" fill=\"${style.color}\"/>`;\n return this.open() + base + inset + this.frame();\n }\n\n private stripesSwatch(): string {\n const width = SWATCH_W / BASE_ORDER.length;\n const stripes = BASE_ORDER.map(\n (base: KnownFramework, index: number): string =>\n `<rect x=\"${(index * width).toFixed(2)}\" width=\"${width.toFixed(2)}\" height=\"${SWATCH_H}\" fill=\"${FRAMEWORK_STYLES[base].color}\"/>`,\n ).join('');\n return this.open() + stripes + this.frame();\n }\n\n private roleSwatch(style: RoleStyle): string {\n return this.solid(style.fill, '', '');\n }\n\n /** A filled swatch, optionally with an inner ring (dashed when `dash` is set). */\n private solid(fill: string, outline: string, dash: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const ring =\n outline === ''\n ? ''\n : `<rect x=\"2\" y=\"2\" width=\"${SWATCH_W - 4}\" height=\"${SWATCH_H - 4}\" fill=\"none\" stroke=\"${outline}\" stroke-width=\"2\"${dashed}/>`;\n return this.open() + `<rect width=\"${SWATCH_W}\" height=\"${SWATCH_H}\" fill=\"${fill}\"/>` + ring + this.frame();\n }\n\n private edgeRow(color: string, dash: string, title: string, detail: string): string {\n const dashed = dash === '' ? '' : ` stroke-dasharray=\"${dash}\"`;\n const line = `<svg width=\"28\" height=\"10\" aria-hidden=\"true\"><line x1=\"0\" y1=\"5\" x2=\"28\" y2=\"5\" stroke=\"${color}\" stroke-width=\"1.6\"${dashed}/></svg>`;\n return this.row(title, line, title, detail);\n }\n}\n"]}
@@ -56,6 +56,12 @@ export declare class GraphPageShell {
56
56
  * (when any is selected). Nothing applies until "Show N projects".
57
57
  */
58
58
  private filterPopover;
59
+ /**
60
+ * Which Impact scan the page shows (#1163): "Changed on this branch" (fork point → working tree)
61
+ * or "Last commit" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;
62
+ * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.
63
+ */
64
+ private impactKinds;
59
65
  private topbar;
60
66
  private overlays;
61
67
  /** The two hint paragraphs that used to sit above the graph, plus the keyboard shortcuts. */
@@ -82,6 +82,7 @@ class GraphPageShell {
82
82
  <div class="wp-label">Color by</div>
83
83
  ${this.modePulldown()}
84
84
  <p class="wp-mode-reason" id="wp-impact-reason" hidden></p>
85
+ ${this.impactKinds('drawer')}
85
86
  </div>
86
87
  <div class="wp-group">
87
88
  <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>
@@ -137,12 +138,22 @@ class GraphPageShell {
137
138
  .join('');
138
139
  return `<div class="wp-pop wp-filter-pop" id="wp-filter-pop" role="dialog" aria-label="Filter projects" hidden>
139
140
  <header><span>Filter</span><button type="button" class="wp-icon wp-icon-mini" id="wp-filter-close" aria-label="Close the filter">✕</button></header>
140
- <fieldset class="wp-fgroup" id="wp-scope-group"><legend>Changes on this branch</legend>${scopes}<p class="wp-scope-reason" id="wp-scope-reason" hidden></p></fieldset>
141
+ <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>
141
142
  <fieldset class="wp-fgroup"><legend>Runtime <small>any match</small></legend><div class="wp-chips">${chips('runtime', graph_color_modes_1.FRAMEWORK_ORDER)}</div></fieldset>
142
143
  <fieldset class="wp-fgroup"><legend>Role <small>any match</small></legend><div class="wp-chips">${chips('role', graph_color_modes_1.ROLE_STYLES.map((style) => style.role))}</div></fieldset>
143
144
  <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>
144
145
  </div>`;
145
146
  }
147
+ /**
148
+ * Which Impact scan the page shows (#1163): "Changed on this branch" (fork point → working tree)
149
+ * or "Last commit" (HEAD^ → working tree). Hidden until the page finds TWO scans in the sidecar;
150
+ * `where` tells the drawer's copy (shown only in Impact mode) from the Filter popover's.
151
+ */
152
+ impactKinds(where) {
153
+ return (`<div class="wp-chips wp-impact-kinds" data-wp-impact-kinds="${where}" role="group" aria-label="Compare" hidden>` +
154
+ '<button type="button" class="wp-chip" data-wp-impact-kind="branch" aria-pressed="false">Changed on this branch</button>' +
155
+ '<button type="button" class="wp-chip" data-wp-impact-kind="commit" aria-pressed="false">Last commit</button></div>');
156
+ }
146
157
  topbar(parts) {
147
158
  return `<div class="wp-topbar">
148
159
  <button type="button" class="wp-icon" id="wp-collapse" aria-label="Toggle the controls drawer" aria-controls="wp-side" aria-expanded="true">☰</button>
@@ -171,9 +182,9 @@ class GraphPageShell {
171
182
  <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>
172
183
  <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>
173
184
  <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>
174
- <p><strong>Filter</strong> narrows the graph by what this branch changed, 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>
185
+ <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>
175
186
  <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>
176
- <p><strong>Runtime</strong>: a box may depend on a box carrying every color it has. <strong>Architecture</strong>: what each box is in the system. <strong>Impact</strong>: what this branch changed (amber) and what depends on it (amber outline).</p>
187
+ <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>
177
188
  <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>
178
189
  </div>`;
179
190
  }
@@ -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":"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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';\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 </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>Changes on this branch</legend>${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 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 this branch changed, 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 this branch changed (amber) and what depends on it (amber outline).</p>\n <p><kbd>/</kbd> focus the lock search · <kbd>1</kbd> <kbd>2</kbd> <kbd>3</kbd> switch mode · <kbd>Esc</kbd> unlock</p>\n </div>`;\n }\n\n private brandIcon(): string {\n return (\n '<svg width=\"22\" height=\"22\" viewBox=\"0 0 22 22\" aria-hidden=\"true\">' +\n '<rect x=\"1\" y=\"1\" width=\"8\" height=\"6\" rx=\"1.5\" fill=\"none\" stroke=\"#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":";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"]}
@@ -146,6 +146,9 @@ class GraphPageStyles {
146
146
  border: 1px solid var(--wp-line); background: var(--wp-surface); font-size: 12.5px; }
147
147
  .wp-chip:hover { border-color: var(--wp-accent-hover); }
148
148
  .wp-chip[aria-pressed="true"] { background: var(--wp-accent); border-color: var(--wp-accent); color: #fff; }
149
+ .wp-chip:disabled { opacity: 0.45; cursor: default; }
150
+ .wp-impact-kinds { margin: 6px 0 4px; }
151
+ .wp-side .wp-chip:not([aria-pressed="true"]) { background: var(--wp-side-control); border-color: var(--wp-side-line); }
149
152
  .wp-filter-pop footer { display: flex; justify-content: space-between; align-items: center; gap: 8px;
150
153
  padding-top: 10px; border-top: 1px solid var(--wp-line); }
151
154
  .wp-text-btn { all: unset; cursor: pointer; color: var(--wp-muted); font-size: 12.5px; text-decoration: underline; }
@@ -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;;;;;;;;;;;;;;;;;;;;;;;kEAuBmD,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;AArND,0CAqNC","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-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;;;;;;;;;;;;;;;;;;;;;;;;;;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"]}
@@ -5,26 +5,39 @@
5
5
  * one) and remembered per viewer in localStorage. Storage can be missing or throw (a private window,
6
6
  * blocked site data, a file:// page in some browsers), so every access is wrapped and the page works
7
7
  * without it.
8
+ *
9
+ * Impact can carry two scans (#1163): "Changed on this branch" (kind `branch`) and "Last commit"
10
+ * (kind `commit`). The ACTIVE one starts as the sidecar's `defaultKind` (or the viewer's remembered
11
+ * choice, when that scan exists and is available) and is what every Impact color, note and Filter
12
+ * set reads.
8
13
  */
9
14
  declare class WpModeState {
10
15
  static readonly MODES: string[];
11
16
  static readonly NAMES: string[];
12
17
  static readonly SUBTITLES: string[];
13
18
  static readonly STORAGE_KEY = "wp-architecture-graph-mode";
14
- readonly impact: ImpactJson | null;
19
+ static readonly KIND_STORAGE_KEY = "wp-architecture-graph-impact-kind";
20
+ readonly scans: ImpactJson[];
21
+ kind: string;
15
22
  mode: string;
16
23
  constructor();
24
+ /** The active scan; null when the page has no Impact data. */
25
+ get impact(): ImpactJson | null;
17
26
  /** '' when Impact can be shown, else the one line the drawer and the Filter popover say instead. */
18
27
  impactReason(): string;
28
+ /** Switch the active scan; false (and nothing changes) for a missing or unavailable one. */
29
+ setKind(kind: string): boolean;
30
+ kindAvailable(kind: string): boolean;
31
+ private initialKind;
19
32
  available(mode: string): boolean;
20
33
  name(mode: string): string;
21
34
  subtitle(mode: string): string;
22
35
  /** False (and nothing changes) for an unknown or unavailable mode. */
23
36
  set(mode: string): boolean;
24
- /** Which impact shade a box gets. */
37
+ /** Which impact shade a box gets, from the ACTIVE scan. */
25
38
  status(id: string): 'touched' | 'affected' | 'buildInput' | 'untouched';
26
39
  private initial;
27
- private stored;
40
+ private read;
28
41
  }
29
42
  /**
30
43
  * One state of the Filter popover: a change scope plus the selected Runtime and Role chips. The page
@@ -81,6 +94,8 @@ declare class GraphFilterPopover {
81
94
  constructor(page: GraphPage);
82
95
  wire(): void;
83
96
  isOpen(): boolean;
97
+ /** The active Impact scan changed: an open popover's counts and labels follow it. */
98
+ refresh(): void;
84
99
  open(): void;
85
100
  close(): void;
86
101
  /** Repaint the controls from the draft: checked scope, pressed chips, counts and the footer. */
@@ -98,6 +113,10 @@ declare class GraphDrawer {
98
113
  wire(): void;
99
114
  /** "Color by": the pill trigger opens the menu of three; picking one switches and closes it. */
100
115
  private wireModeMenu;
116
+ /** The "Changed on this branch" / "Last commit" toggle, in the drawer and in the Filter popover. */
117
+ private wireImpactKinds;
118
+ /** Shown only when two scans exist; the drawer's copy only while Impact colors the graph. */
119
+ private syncImpactKinds;
101
120
  private closeModeMenu;
102
121
  /** Type-to-search lock: an exact project id (or a unique short name) locks, empty or Esc unlocks. */
103
122
  private wireLock;
@@ -150,6 +169,8 @@ declare class GraphPage extends WpFilterPage {
150
169
  resolveName(text: string): string | null;
151
170
  setLock(name: string | null): void;
152
171
  setMode(mode: string): void;
172
+ /** Switch the active Impact scan: Impact recolours and an active change-scope filter re-applies. */
173
+ setImpactKind(kind: string): void;
153
174
  /** The drawer's "Hide unconnected": filter to the locked box's chain, or turn the filter off. */
154
175
  toggleFilter(): void;
155
176
  appliedFilter(): WpFilterChoice;