@life-cockpit/angular-ui-kit 2.14.0 → 2.15.0

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.
@@ -13503,6 +13503,15 @@ const BACK_EDGE_BOW = 36;
13503
13503
  const ROUTE_SAMPLES = 24;
13504
13504
  /** Ignore grazing the very edge of a box — only a real incursion counts. */
13505
13505
  const ROUTE_INSET = 3;
13506
+ /** Breathing room around the graph's bounding box when auto-fitting, in layout units. */
13507
+ const FIT_PADDING = 24;
13508
+ /**
13509
+ * Auto-fit shrinks to fit but never enlarges: blowing a two-node graph up to fill
13510
+ * a 600px frame reads as broken, not as "fitted".
13511
+ */
13512
+ const MAX_FIT_SCALE = 1;
13513
+ /** Floor for the fit scale, so a huge graph in a tiny box stays a graph, not a smear. */
13514
+ const MIN_FIT_SCALE = 0.05;
13506
13515
  const RELATION_STYLES = {
13507
13516
  depends: { color: 'var(--color-neutral-400)', dashed: false, label: 'depends on' },
13508
13517
  blocks: { color: 'var(--color-error-default)', dashed: false, label: 'blocks' },
@@ -13860,6 +13869,7 @@ function readableInk(color) {
13860
13869
  * - Color-coded edges per relationship type
13861
13870
  * - Node status colors (default, active, success, warning, error, muted)
13862
13871
  * - Pan and zoom controls with mouse wheel support
13872
+ * - Auto-fit (`autoFit`) and a static, non-navigable mode (`interactive: false`)
13863
13873
  * - Collapsible sub-trees
13864
13874
  * - Interactive node selection with detail panel
13865
13875
  * - Legend showing active relationship types
@@ -13884,6 +13894,17 @@ function readableInk(color) {
13884
13894
  * <lc-dependency-viewer [root]="specTree" direction="horizontal" />
13885
13895
  * ```
13886
13896
  *
13897
+ * @example Static overview — the whole graph at a glance, click-through only
13898
+ * ```html
13899
+ * <lc-dependency-viewer
13900
+ * [root]="graph()"
13901
+ * [autoFit]="true"
13902
+ * [interactive]="false"
13903
+ * height="360px"
13904
+ * (nodeSelect)="openDetail($event)"
13905
+ * />
13906
+ * ```
13907
+ *
13887
13908
  * @example Incremental graph exploration
13888
13909
  * ```html
13889
13910
  * <lc-dependency-viewer
@@ -13902,6 +13923,23 @@ class DependencyViewerComponent {
13902
13923
  showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
13903
13924
  showEdgeLabels = input(true, ...(ngDevMode ? [{ debugName: "showEdgeLabels" }] : /* istanbul ignore next */ []));
13904
13925
  edgeWidth = input(1.5, ...(ngDevMode ? [{ debugName: "edgeWidth" }] : /* istanbul ignore next */ []));
13926
+ /**
13927
+ * Scales and centres the graph so that all of it fits the canvas, and re-fits
13928
+ * whenever the container or the graph changes size. Turns the viewer into an
13929
+ * overview: nothing to pan or zoom to, everything visible at once.
13930
+ *
13931
+ * Shrinks only — the fit never enlarges past 100%, so a two-node graph keeps its
13932
+ * natural size instead of ballooning to fill the frame.
13933
+ *
13934
+ * Pair with `interactive: false` for a purely static display.
13935
+ */
13936
+ autoFit = input(false, ...(ngDevMode ? [{ debugName: "autoFit" }] : /* istanbul ignore next */ []));
13937
+ /**
13938
+ * Viewport navigation: drag-to-pan, wheel-zoom and the toolbar's zoom buttons.
13939
+ * `false` freezes the viewport — node selection, expansion and collapse keep
13940
+ * working, so `nodeSelect` still fires in a static viewer.
13941
+ */
13942
+ interactive = input(true, ...(ngDevMode ? [{ debugName: "interactive" }] : /* istanbul ignore next */ []));
13905
13943
  /**
13906
13944
  * Node the viewport holds still across `root` updates. Defaults to the node the
13907
13945
  * user last selected, which keeps click-to-expand steady without any wiring.
@@ -13927,6 +13965,7 @@ class DependencyViewerComponent {
13927
13965
  lastMouseX = 0;
13928
13966
  lastMouseY = 0;
13929
13967
  hostEl = inject((ElementRef));
13968
+ canvasRef = viewChild('canvas', ...(ngDevMode ? [{ debugName: "canvasRef" }] : /* istanbul ignore next */ []));
13930
13969
  /** Last known layout position of the anchor, to compensate pan after a relayout. */
13931
13970
  anchorPos = null;
13932
13971
  effectiveRoot = computed(() => this.pruneCollapsed(this.root(), this.collapsedIds(), new Set()), ...(ngDevMode ? [{ debugName: "effectiveRoot" }] : /* istanbul ignore next */ []));
@@ -13981,7 +14020,9 @@ class DependencyViewerComponent {
13981
14020
  effect(() => {
13982
14021
  const nodeMap = this.layout().nodeMap;
13983
14022
  const anchorId = this.anchorNodeId() ?? this.selectedNodeId();
13984
- if (!anchorId) {
14023
+ // Auto-fit recentres the whole graph on every relayout, which subsumes
14024
+ // holding one node still. Running both would have them fight over the pan.
14025
+ if (this.autoFit() || !anchorId) {
13985
14026
  this.anchorPos = null;
13986
14027
  return;
13987
14028
  }
@@ -14004,6 +14045,26 @@ class DependencyViewerComponent {
14004
14045
  }
14005
14046
  this.anchorPos = { id: anchorId, x: node.x, y: node.y };
14006
14047
  });
14048
+ // Re-fit when the graph changes shape. Reading `canvasRef` here (rather than
14049
+ // only inside `fit`) is what gets the *first* fit to run: the view child
14050
+ // resolves after the initial render, which re-triggers this effect at the
14051
+ // point the canvas finally has a measurable size.
14052
+ effect(() => {
14053
+ if (!this.autoFit() || !this.canvasRef())
14054
+ return;
14055
+ this.layout();
14056
+ untracked(() => this.fit());
14057
+ });
14058
+ // …and when the container changes size. ResizeObserver delivers an initial
14059
+ // callback on observe, so this covers the first fit in a browser too.
14060
+ effect(onCleanup => {
14061
+ const el = this.canvasRef()?.nativeElement;
14062
+ if (!this.autoFit() || !el || typeof ResizeObserver === 'undefined')
14063
+ return;
14064
+ const observer = new ResizeObserver(() => this.fit());
14065
+ observer.observe(el);
14066
+ onCleanup(() => observer.disconnect());
14067
+ });
14007
14068
  }
14008
14069
  svgWidth = computed(() => this.layout().width + 80, ...(ngDevMode ? [{ debugName: "svgWidth" }] : /* istanbul ignore next */ []));
14009
14070
  svgHeight = computed(() => this.layout().height + 80, ...(ngDevMode ? [{ debugName: "svgHeight" }] : /* istanbul ignore next */ []));
@@ -14012,6 +14073,9 @@ class DependencyViewerComponent {
14012
14073
  const z = this.zoom() / 100;
14013
14074
  return `translate(${this.panX()}px, ${this.panY()}px) scale(${z})`;
14014
14075
  }, ...(ngDevMode ? [{ debugName: "transform" }] : /* istanbul ignore next */ []));
14076
+ // A fitted scale is an arbitrary fraction; the readout is not the source of
14077
+ // truth, so it rounds rather than printing "63.41999999%".
14078
+ zoomLabel = computed(() => Math.round(this.zoom()), ...(ngDevMode ? [{ debugName: "zoomLabel" }] : /* istanbul ignore next */ []));
14015
14079
  selectedNode = computed(() => {
14016
14080
  const id = this.selectedNodeId();
14017
14081
  if (!id)
@@ -14111,7 +14175,7 @@ class DependencyViewerComponent {
14111
14175
  if (!node)
14112
14176
  return;
14113
14177
  const z = this.zoom() / 100;
14114
- const el = this.hostEl.nativeElement.querySelector('.dep-viewer__canvas');
14178
+ const el = this.canvasEl();
14115
14179
  const viewW = el?.clientWidth ?? 0;
14116
14180
  const viewH = el?.clientHeight ?? 0;
14117
14181
  // Invert screen = pan + z * layout for the node's centre.
@@ -14119,8 +14183,41 @@ class DependencyViewerComponent {
14119
14183
  this.panY.set(viewH / 2 - (node.y + node.height / 2) * z);
14120
14184
  this.anchorPos = { id, x: node.x, y: node.y };
14121
14185
  }
14122
- /** Restores the initial zoom and pan. */
14186
+ /**
14187
+ * Scales and centres the graph so all of it fits the canvas. Called for you when
14188
+ * `autoFit` is set; public so a caller can fit on demand without it.
14189
+ *
14190
+ * No-op while the canvas has no size (before the first render, or in a detached
14191
+ * / `display: none` container) — the `autoFit` observers re-run once it does.
14192
+ */
14193
+ fit() {
14194
+ const el = this.canvasEl();
14195
+ const viewW = el?.clientWidth ?? 0;
14196
+ const viewH = el?.clientHeight ?? 0;
14197
+ if (!viewW || !viewH)
14198
+ return;
14199
+ const box = this.contentBox();
14200
+ if (box.width <= 0 || box.height <= 0)
14201
+ return;
14202
+ const scale = Math.min(MAX_FIT_SCALE, Math.max(MIN_FIT_SCALE, Math.min(viewW / box.width, viewH / box.height)));
14203
+ this.zoom.set(scale * 100);
14204
+ // Invert screen = pan + scale * layout for the box's top-left corner, so the
14205
+ // box lands centred in the canvas.
14206
+ this.panX.set((viewW - box.width * scale) / 2 - box.x * scale);
14207
+ this.panY.set((viewH - box.height * scale) / 2 - box.y * scale);
14208
+ // The camera just moved deliberately; a stale anchor baseline would have the
14209
+ // next relayout "correct" a position this fit had already chosen.
14210
+ this.anchorPos = null;
14211
+ }
14212
+ /**
14213
+ * Restores the initial view — the fitted one under `autoFit`, otherwise the
14214
+ * default zoom and pan.
14215
+ */
14123
14216
  resetView() {
14217
+ if (this.autoFit()) {
14218
+ this.fit();
14219
+ return;
14220
+ }
14124
14221
  this.zoom.set(100);
14125
14222
  this.panX.set(40);
14126
14223
  this.panY.set(40);
@@ -14158,8 +14255,47 @@ class DependencyViewerComponent {
14158
14255
  }
14159
14256
  this.collapsedIds.set(ids);
14160
14257
  }
14258
+ // ── Viewport measurement ───────────────────────────────────────────────────
14259
+ canvasEl() {
14260
+ return (this.canvasRef()?.nativeElement ??
14261
+ this.hostEl.nativeElement.querySelector('.dep-viewer__canvas'));
14262
+ }
14263
+ /**
14264
+ * Bounding box of everything drawn, in layout units.
14265
+ *
14266
+ * Edge label points stand in for the edges themselves. A parent→child edge stays
14267
+ * inside the node boxes anyway, and a bowed cross-reference puts its label at the
14268
+ * apex of the bow — the only part of it that reaches outside them.
14269
+ */
14270
+ contentBox() {
14271
+ const { nodes, edges } = this.layout();
14272
+ if (!nodes.length)
14273
+ return { x: 0, y: 0, width: 0, height: 0 };
14274
+ let minX = Infinity;
14275
+ let minY = Infinity;
14276
+ let maxX = -Infinity;
14277
+ let maxY = -Infinity;
14278
+ for (const n of nodes) {
14279
+ minX = Math.min(minX, n.x);
14280
+ minY = Math.min(minY, n.y);
14281
+ maxX = Math.max(maxX, n.x + n.width);
14282
+ maxY = Math.max(maxY, n.y + n.height);
14283
+ }
14284
+ for (const e of edges) {
14285
+ minX = Math.min(minX, e.labelX);
14286
+ minY = Math.min(minY, e.labelY);
14287
+ maxX = Math.max(maxX, e.labelX);
14288
+ maxY = Math.max(maxY, e.labelY);
14289
+ }
14290
+ return {
14291
+ x: minX - FIT_PADDING,
14292
+ y: minY - FIT_PADDING,
14293
+ width: maxX - minX + FIT_PADDING * 2,
14294
+ height: maxY - minY + FIT_PADDING * 2,
14295
+ };
14296
+ }
14161
14297
  onMouseDown(event) {
14162
- if (event.button !== 0)
14298
+ if (!this.interactive() || event.button !== 0)
14163
14299
  return;
14164
14300
  this.isPanning = true;
14165
14301
  this.lastMouseX = event.clientX;
@@ -14175,6 +14311,10 @@ class DependencyViewerComponent {
14175
14311
  }
14176
14312
  onMouseUp() { this.isPanning = false; }
14177
14313
  onWheel(event) {
14314
+ // Return *before* preventDefault: a static viewer must not swallow the wheel
14315
+ // event, or the page can't be scrolled past it.
14316
+ if (!this.interactive())
14317
+ return;
14178
14318
  event.preventDefault();
14179
14319
  this.zoom.update(z => event.deltaY < 0 ? Math.min(z + 10, 300) : Math.max(z - 10, 25));
14180
14320
  }
@@ -14195,12 +14335,12 @@ class DependencyViewerComponent {
14195
14335
  return typeColor ? readableInk(typeColor) : STATUS_COLORS[node.status].text;
14196
14336
  }
14197
14337
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14198
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: DependencyViewerComponent, isStandalone: true, selector: "lc-dependency-viewer", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: true, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showEdgeLabels: { classPropertyName: "showEdgeLabels", publicName: "showEdgeLabels", isSignal: true, isRequired: false, transformFunction: null }, edgeWidth: { classPropertyName: "edgeWidth", publicName: "edgeWidth", isSignal: true, isRequired: false, transformFunction: null }, anchorNodeId: { classPropertyName: "anchorNodeId", publicName: "anchorNodeId", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, hiddenRelations: { classPropertyName: "hiddenRelations", publicName: "hiddenRelations", isSignal: true, isRequired: false, transformFunction: null }, hiddenTypes: { classPropertyName: "hiddenTypes", publicName: "hiddenTypes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeSelect: "nodeSelect", nodeExpand: "nodeExpand" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()" }, properties: { "class": "\"dep-viewer\"", "style.height": "height()" } }, ngImport: i0, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14338
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: DependencyViewerComponent, isStandalone: true, selector: "lc-dependency-viewer", inputs: { root: { classPropertyName: "root", publicName: "root", isSignal: true, isRequired: true, transformFunction: null }, direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, showToolbar: { classPropertyName: "showToolbar", publicName: "showToolbar", isSignal: true, isRequired: false, transformFunction: null }, showEdgeLabels: { classPropertyName: "showEdgeLabels", publicName: "showEdgeLabels", isSignal: true, isRequired: false, transformFunction: null }, edgeWidth: { classPropertyName: "edgeWidth", publicName: "edgeWidth", isSignal: true, isRequired: false, transformFunction: null }, autoFit: { classPropertyName: "autoFit", publicName: "autoFit", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "interactive", isSignal: true, isRequired: false, transformFunction: null }, anchorNodeId: { classPropertyName: "anchorNodeId", publicName: "anchorNodeId", isSignal: true, isRequired: false, transformFunction: null }, typeColors: { classPropertyName: "typeColors", publicName: "typeColors", isSignal: true, isRequired: false, transformFunction: null }, hiddenRelations: { classPropertyName: "hiddenRelations", publicName: "hiddenRelations", isSignal: true, isRequired: false, transformFunction: null }, hiddenTypes: { classPropertyName: "hiddenTypes", publicName: "hiddenTypes", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeSelect: "nodeSelect", nodeExpand: "nodeExpand" }, host: { listeners: { "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp()" }, properties: { "class": "\"dep-viewer\"", "style.height": "height()" } }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- Toolbar. The zoom cluster is navigation, so it goes with `interactive`; the\n direction indicator is a read-out and stays. -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n @if (interactive()) {\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoomLabel() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n }\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n #canvas\n class=\"dep-viewer__canvas\"\n [class.dep-viewer__canvas--static]=\"!interactive()\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;flex:1 1 auto;min-height:0}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__canvas--static,.dep-viewer__canvas--static:active{cursor:default}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
14199
14339
  }
14200
14340
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, decorators: [{
14201
14341
  type: Component,
14202
- args: [{ selector: 'lc-dependency-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': '"dep-viewer"', '[style.height]': 'height()' }, template: "<!-- Toolbar -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoom() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n class=\"dep-viewer__canvas\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:block;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;height:100%}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"] }]
14203
- }], ctorParameters: () => [], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: true }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showEdgeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdgeLabels", required: false }] }], edgeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "edgeWidth", required: false }] }], anchorNodeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorNodeId", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], hiddenRelations: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenRelations", required: false }] }], hiddenTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenTypes", required: false }] }], nodeSelect: [{ type: i0.Output, args: ["nodeSelect"] }], nodeExpand: [{ type: i0.Output, args: ["nodeExpand"] }], onMouseMove: [{
14342
+ args: [{ selector: 'lc-dependency-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': '"dep-viewer"', '[style.height]': 'height()' }, template: "<!-- Toolbar. The zoom cluster is navigation, so it goes with `interactive`; the\n direction indicator is a read-out and stays. -->\n@if (showToolbar()) {\n <div class=\"dep-viewer__toolbar\">\n @if (interactive()) {\n <button class=\"dep-viewer__btn\" (click)=\"zoomIn()\" title=\"Zoom in\">+</button>\n <span class=\"dep-viewer__zoom\">{{ zoomLabel() }}%</span>\n <button class=\"dep-viewer__btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\u2212</button>\n <button class=\"dep-viewer__btn dep-viewer__btn--reset\" (click)=\"resetZoom()\" title=\"Reset\">\u27F2</button>\n }\n <span class=\"dep-viewer__direction-label\">{{ direction() === 'horizontal' ? '\u2192' : '\u2193' }}</span>\n </div>\n}\n\n<!-- Canvas -->\n<div\n #canvas\n class=\"dep-viewer__canvas\"\n [class.dep-viewer__canvas--static]=\"!interactive()\"\n (mousedown)=\"onMouseDown($event)\"\n (wheel)=\"onWheel($event)\"\n (click)=\"deselectNode()\"\n>\n <svg\n [attr.width]=\"svgWidth()\"\n [attr.height]=\"svgHeight()\"\n [attr.viewBox]=\"viewBox()\"\n [style.transform]=\"transform()\"\n class=\"dep-viewer__svg\"\n >\n <!-- Arrow markers for cross-reference edges -->\n <defs>\n <marker id=\"arrow-default\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n <marker id=\"arrow-blocks\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-error-default)\"/></marker>\n <marker id=\"arrow-references\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-info-default, #3b82f6)\"/></marker>\n <marker id=\"arrow-requires\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-warning-default)\"/></marker>\n <marker id=\"arrow-extends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-primary-400)\"/></marker>\n <marker id=\"arrow-implements\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-success-default)\"/></marker>\n <marker id=\"arrow-uses\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-300)\"/></marker>\n <marker id=\"arrow-depends\" markerWidth=\"8\" markerHeight=\"6\" refX=\"8\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L8,3 L0,6 Z\" fill=\"var(--color-neutral-400)\"/></marker>\n </defs>\n\n <!-- Edges. Their labels are deliberately NOT drawn here \u2014 see below. -->\n @for (edge of layout().edges; track edge.id) {\n <path\n [attr.d]=\"edge.path\"\n [attr.stroke]=\"edge.color\"\n [attr.stroke-width]=\"edgeWidth()\"\n [attr.stroke-dasharray]=\"edge.dashed ? '6 3' : 'none'\"\n [attr.marker-end]=\"edge.isCrossRef ? 'url(#arrow-' + edge.marker + ')' : ''\"\n fill=\"none\"\n class=\"dep-viewer__edge\"\n [class.dep-viewer__edge--cross-ref]=\"edge.isCrossRef\"\n />\n }\n\n <!-- Nodes -->\n @for (node of layout().nodes; track node.id) {\n <g\n class=\"dep-viewer__node\"\n [class.dep-viewer__node--selected]=\"selectedNodeId() === node.id\"\n (click)=\"selectNode(node.id, $event)\"\n (dblclick)=\"expandNode(node.id, $event)\"\n >\n <!-- Opaque backdrop. The status fills are translucent tints (in the dark\n theme --color-success-50 & co. resolve to rgba(\u2026, 0.18)), so without\n something solid beneath them the edges routed under a node show\n straight through it. This keeps the tint compositing against the\n viewer's own surface instead of against whatever passes behind. -->\n <rect\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n class=\"dep-viewer__node-backdrop\"\n />\n <rect\n class=\"dep-viewer__node-fill\"\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"node.width\"\n [attr.height]=\"node.height\"\n rx=\"6\" ry=\"6\"\n [attr.fill]=\"getNodeBg(node)\"\n [attr.stroke]=\"selectedNodeId() === node.id ? 'var(--color-primary-500)' : getNodeBorder(node)\"\n stroke-width=\"1.5\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n [attr.fill]=\"getNodeText(node)\"\n class=\"dep-viewer__node-label\"\n >{{ node.label }}</text>\n\n <!-- \"+N\" marker: this node has more neighbours than are shown -->\n @if (node.moreCount) {\n <g class=\"dep-viewer__more\">\n <rect\n [attr.x]=\"node.x + node.width - 26\"\n [attr.y]=\"node.y - 8\"\n width=\"32\" height=\"16\" rx=\"8\" ry=\"8\"\n />\n <text\n [attr.x]=\"node.x + node.width - 10\"\n [attr.y]=\"node.y + 3\"\n text-anchor=\"middle\"\n >+{{ node.moreCount }}</text>\n </g>\n }\n\n <!-- Collapse/expand toggle -->\n @if (hasChildren(node.id)) {\n <g\n class=\"dep-viewer__toggle\"\n (click)=\"toggleCollapse(node.id, $event)\"\n >\n @if (direction() === 'horizontal') {\n <circle\n [attr.cx]=\"node.x + node.width + 10\"\n [attr.cy]=\"node.y + node.height / 2\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width + 10\"\n [attr.y]=\"node.y + node.height / 2 + 4\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n } @else {\n <circle\n [attr.cx]=\"node.x + node.width / 2\"\n [attr.cy]=\"node.y + node.height + 10\"\n r=\"8\"\n fill=\"var(--color-neutral-100)\" stroke=\"var(--color-neutral-300)\" stroke-width=\"1\"\n />\n <text\n [attr.x]=\"node.x + node.width / 2\"\n [attr.y]=\"node.y + node.height + 14\"\n text-anchor=\"middle\"\n class=\"dep-viewer__toggle-text\"\n >{{ isCollapsed(node.id) ? '+' : '\u2212' }}</text>\n }\n </g>\n }\n </g>\n }\n\n <!-- Edge labels last: SVG paints in document order, so anything drawn before\n the nodes is covered by them. A label sits at the midpoint of its edge,\n which frequently lands on a node \u2014 it has to come after. The halo (see\n the stylesheet) keeps it legible over whatever it lands on. -->\n @if (showEdgeLabels()) {\n @for (edge of layout().edges; track edge.id) {\n @if (edge.isCrossRef && edge.label) {\n <text\n [attr.x]=\"edge.labelX\"\n [attr.y]=\"edge.labelY\"\n class=\"dep-viewer__edge-label\"\n [attr.fill]=\"edge.color\"\n text-anchor=\"middle\"\n >{{ edge.label }}</text>\n }\n }\n }\n </svg>\n</div>\n\n<!-- Legend -->\n@if (legendItems().length || typeLegendItems().length) {\n <div class=\"dep-viewer__legend\">\n @for (item of legendItems(); track item.label) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"24\" height=\"10\">\n <line\n x1=\"0\" y1=\"5\" x2=\"24\" y2=\"5\"\n [attr.stroke]=\"item.color\"\n stroke-width=\"2\"\n [attr.stroke-dasharray]=\"item.dashed ? '4 2' : 'none'\"\n />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.label }}</span>\n </span>\n }\n @for (item of typeLegendItems(); track item.type) {\n <span class=\"dep-viewer__legend-item\">\n <svg width=\"10\" height=\"10\">\n <rect width=\"10\" height=\"10\" rx=\"2\" ry=\"2\" [attr.fill]=\"item.color\" />\n </svg>\n <span class=\"dep-viewer__legend-text\">{{ item.type }}</span>\n </span>\n }\n </div>\n}\n\n<!-- Detail panel -->\n@if (selectedNode(); as node) {\n <div class=\"dep-viewer__detail\">\n <div class=\"dep-viewer__detail-header\">\n <span class=\"dep-viewer__detail-status dep-viewer__detail-status--{{ node.status }}\">\u25CF</span>\n <strong>{{ node.label }}</strong>\n @if (node.type) {\n <span class=\"dep-viewer__detail-type\">{{ node.type }}</span>\n }\n </div>\n @if (node.description) {\n <p class=\"dep-viewer__detail-desc\">{{ node.description }}</p>\n }\n <div class=\"dep-viewer__detail-meta\">\n <span>Depth: {{ node.depth }}</span>\n <span>Children: {{ node.outgoingCount }}</span>\n <span>Depends on: {{ node.incomingCount }}</span>\n </div>\n @if (selectedDependsOn().length) {\n <div class=\"dep-viewer__detail-deps\">\n <strong>Dependencies:</strong>\n <ul>\n @for (dep of selectedDependsOn(); track dep.id) {\n <li>\n <span class=\"dep-viewer__dep-relation\">{{\n dep.relationLabel || getRelationLabel(dep.relation || 'depends')\n }}</span>\n \u2192 {{ dep.id }}\n </li>\n }\n </ul>\n </div>\n }\n </div>\n}\n", styles: [":host{display:flex;flex-direction:column;position:relative;overflow:hidden;border:1px solid var(--color-border);border-radius:8px;background:var(--color-surface)}.dep-viewer__toolbar{display:flex;align-items:center;gap:4px;padding:8px 12px;border-bottom:1px solid var(--color-border);background:var(--color-surface-2);-webkit-user-select:none;user-select:none}.dep-viewer__btn{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border:1px solid var(--color-border);border-radius:4px;background:var(--color-surface);color:var(--color-text-secondary);cursor:pointer;font-size:14px;line-height:1}.dep-viewer__btn:hover{background:var(--color-surface-hover)}.dep-viewer__btn--reset{margin-left:4px}.dep-viewer__zoom{font-size:12px;min-width:40px;text-align:center;color:var(--color-text-secondary)}.dep-viewer__direction-label{margin-left:8px;font-size:14px;color:var(--color-text-tertiary)}.dep-viewer__canvas{cursor:grab;overflow:hidden;width:100%;flex:1 1 auto;min-height:0}.dep-viewer__canvas:active{cursor:grabbing}.dep-viewer__canvas--static,.dep-viewer__canvas--static:active{cursor:default}.dep-viewer__svg{transform-origin:0 0;transition:transform .1s ease-out}.dep-viewer__edge{pointer-events:none;transition:opacity .15s}.dep-viewer__edge--cross-ref{opacity:.8}.dep-viewer__edge-label{font-size:9px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none;dominant-baseline:central;paint-order:stroke;stroke:var(--color-surface);stroke-width:3px;stroke-linejoin:round}.dep-viewer__node{cursor:pointer;transition:filter .15s}.dep-viewer__node:hover{filter:brightness(.96)}.dep-viewer__node--selected rect{stroke-width:2.5}.dep-viewer__node-backdrop{fill:var(--color-surface)}.dep-viewer__node-label{font-size:12px;font-weight:500;pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__toggle{cursor:pointer}.dep-viewer__toggle:hover circle{fill:var(--color-surface-hover)}.dep-viewer__more{pointer-events:none}.dep-viewer__more rect{fill:var(--color-surface-2);stroke:var(--color-border-strong);stroke-width:1}.dep-viewer__more text{font-size:9px;font-weight:600;fill:var(--color-text-secondary);-webkit-user-select:none;user-select:none}.dep-viewer__toggle-text{font-size:11px;font-weight:600;fill:var(--color-text-secondary);pointer-events:none;-webkit-user-select:none;user-select:none}.dep-viewer__legend{display:flex;flex-wrap:wrap;gap:12px;padding:6px 12px;border-top:1px solid var(--color-border);background:var(--color-surface-2)}.dep-viewer__legend-item{display:flex;align-items:center;gap:4px}.dep-viewer__legend-text{font-size:11px;color:var(--color-text-secondary)}.dep-viewer__detail{position:absolute;bottom:12px;right:12px;width:240px;padding:12px;background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:8px;box-shadow:var(--elevation-1);font-size:12px;z-index:10}.dep-viewer__detail-header{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:13px}.dep-viewer__detail-type{margin-left:auto;padding:1px 6px;border-radius:3px;background:var(--color-surface-hover);color:var(--color-text-secondary);font-size:10px;font-weight:500}.dep-viewer__detail-status{font-size:10px}.dep-viewer__detail-status--default{color:var(--color-text-tertiary)}.dep-viewer__detail-status--active{color:var(--color-primary-400)}.dep-viewer__detail-status--success{color:var(--color-success-default)}.dep-viewer__detail-status--warning{color:var(--color-warning-default)}.dep-viewer__detail-status--error{color:var(--color-error-default)}.dep-viewer__detail-status--muted{color:var(--color-text-disabled)}.dep-viewer__detail-desc{color:var(--color-text-secondary);margin:0 0 6px;line-height:1.4}.dep-viewer__detail-meta{display:flex;flex-wrap:wrap;gap:8px;color:var(--color-text-tertiary);font-size:11px}.dep-viewer__detail-deps{margin-top:8px;padding-top:8px;border-top:1px solid var(--color-border)}.dep-viewer__detail-deps ul{list-style:none;margin:4px 0 0;padding:0}.dep-viewer__detail-deps li{padding:2px 0;color:var(--color-text-secondary)}.dep-viewer__dep-relation{display:inline-block;padding:1px 4px;background:var(--color-surface-hover);border-radius:3px;font-size:10px;font-weight:500;color:var(--color-text-secondary)}\n"] }]
14343
+ }], ctorParameters: () => [], propDecorators: { root: [{ type: i0.Input, args: [{ isSignal: true, alias: "root", required: true }] }], direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }], showToolbar: [{ type: i0.Input, args: [{ isSignal: true, alias: "showToolbar", required: false }] }], showEdgeLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showEdgeLabels", required: false }] }], edgeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "edgeWidth", required: false }] }], autoFit: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFit", required: false }] }], interactive: [{ type: i0.Input, args: [{ isSignal: true, alias: "interactive", required: false }] }], anchorNodeId: [{ type: i0.Input, args: [{ isSignal: true, alias: "anchorNodeId", required: false }] }], typeColors: [{ type: i0.Input, args: [{ isSignal: true, alias: "typeColors", required: false }] }], hiddenRelations: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenRelations", required: false }] }], hiddenTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "hiddenTypes", required: false }] }], nodeSelect: [{ type: i0.Output, args: ["nodeSelect"] }], nodeExpand: [{ type: i0.Output, args: ["nodeExpand"] }], canvasRef: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], onMouseMove: [{
14204
14344
  type: HostListener,
14205
14345
  args: ['document:mousemove', ['$event']]
14206
14346
  }], onMouseUp: [{