@life-cockpit/angular-ui-kit 2.15.0 → 2.16.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.
|
@@ -13581,7 +13581,7 @@ function layoutTreeH(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
|
|
|
13581
13581
|
path: `M ${sx} ${sy} C ${mx} ${sy}, ${mx} ${ty}, ${tx} ${ty}`,
|
|
13582
13582
|
labelX: mx, labelY: (sy + ty) / 2,
|
|
13583
13583
|
color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
|
|
13584
|
-
marker: 'depends',
|
|
13584
|
+
arrow: false, marker: 'depends',
|
|
13585
13585
|
});
|
|
13586
13586
|
}
|
|
13587
13587
|
let childY = y;
|
|
@@ -13614,7 +13614,7 @@ function layoutTreeV(measure, x, y, parentId, nodes, edges, allNodes, nodeMap) {
|
|
|
13614
13614
|
path: `M ${sx} ${sy} C ${sx} ${my}, ${tx} ${my}, ${tx} ${ty}`,
|
|
13615
13615
|
labelX: (sx + tx) / 2, labelY: my,
|
|
13616
13616
|
color: 'var(--color-neutral-300)', dashed: false, isCrossRef: false,
|
|
13617
|
-
marker: 'depends',
|
|
13617
|
+
arrow: false, marker: 'depends',
|
|
13618
13618
|
});
|
|
13619
13619
|
}
|
|
13620
13620
|
let childX = x;
|
|
@@ -13784,7 +13784,8 @@ function createCrossRefEdges(nodeMap, allOriginal, dir, backEdges) {
|
|
|
13784
13784
|
path: geo.path, relation: dep.relation,
|
|
13785
13785
|
label: dep.relationLabel ?? style.label,
|
|
13786
13786
|
labelX: geo.labelX, labelY: geo.labelY,
|
|
13787
|
-
color: style.color, dashed: style.dashed, isCrossRef: true,
|
|
13787
|
+
color: style.color, dashed: style.dashed, isCrossRef: true,
|
|
13788
|
+
arrow: true, marker: relation,
|
|
13788
13789
|
});
|
|
13789
13790
|
}
|
|
13790
13791
|
}
|
|
@@ -13802,11 +13803,367 @@ function createCrossRefEdges(nodeMap, allOriginal, dir, backEdges) {
|
|
|
13802
13803
|
edges.push({
|
|
13803
13804
|
id: `${back.from}↺${back.to}`, sourceId: back.from, targetId: back.to,
|
|
13804
13805
|
path: geo.path, labelX: geo.labelX, labelY: geo.labelY,
|
|
13805
|
-
color: style.color, dashed: true, isCrossRef: true,
|
|
13806
|
+
color: style.color, dashed: true, isCrossRef: true,
|
|
13807
|
+
arrow: true, marker: 'depends',
|
|
13806
13808
|
});
|
|
13807
13809
|
}
|
|
13808
13810
|
return edges;
|
|
13809
13811
|
}
|
|
13812
|
+
const push = (map, key, value) => {
|
|
13813
|
+
const bucket = map.get(key);
|
|
13814
|
+
if (bucket)
|
|
13815
|
+
bucket.push(value);
|
|
13816
|
+
else
|
|
13817
|
+
map.set(key, [value]);
|
|
13818
|
+
};
|
|
13819
|
+
/**
|
|
13820
|
+
* Flattens the input into the node set and the precedence links the layered
|
|
13821
|
+
* layout works on.
|
|
13822
|
+
*
|
|
13823
|
+
* Both link kinds are precedence: `dependsOn` says "this comes after that", and a
|
|
13824
|
+
* `children` link says the same of a parent and its child. Layering over their
|
|
13825
|
+
* union is what keeps a `children` edge pointing *with* the flow instead of
|
|
13826
|
+
* doubling back across the graph, and means nothing in the input goes undrawn.
|
|
13827
|
+
* A pair joined both ways is one edge, styled by its `dependsOn` relation.
|
|
13828
|
+
*
|
|
13829
|
+
* A self-link is dropped: it constrains a node to sit after itself, which no
|
|
13830
|
+
* layering can satisfy, and it has nowhere to be drawn.
|
|
13831
|
+
*/
|
|
13832
|
+
function collectGraph(roots) {
|
|
13833
|
+
const nodes = new Map();
|
|
13834
|
+
const order = [];
|
|
13835
|
+
const edges = new Map();
|
|
13836
|
+
const walk = (node) => {
|
|
13837
|
+
if (!nodes.has(node.id)) {
|
|
13838
|
+
nodes.set(node.id, node);
|
|
13839
|
+
order.push(node.id);
|
|
13840
|
+
}
|
|
13841
|
+
for (const child of node.children || []) {
|
|
13842
|
+
const fresh = !nodes.has(child.id);
|
|
13843
|
+
if (node.id !== child.id) {
|
|
13844
|
+
edges.set(`${node.id}→${child.id}`, {
|
|
13845
|
+
from: node.id, to: child.id, structural: true, back: false,
|
|
13846
|
+
});
|
|
13847
|
+
}
|
|
13848
|
+
// Guards the recursion: a cycle or a second parent revisits a node that is
|
|
13849
|
+
// already collected, and there is nothing left to collect from it.
|
|
13850
|
+
if (fresh)
|
|
13851
|
+
walk(child);
|
|
13852
|
+
}
|
|
13853
|
+
};
|
|
13854
|
+
for (const root of roots)
|
|
13855
|
+
walk(root);
|
|
13856
|
+
for (const id of order) {
|
|
13857
|
+
for (const dep of nodes.get(id)?.dependsOn || []) {
|
|
13858
|
+
if (dep.id === id || !nodes.has(dep.id))
|
|
13859
|
+
continue;
|
|
13860
|
+
const style = RELATION_STYLES[resolveRelation(dep.relation)];
|
|
13861
|
+
edges.set(`${dep.id}→${id}`, {
|
|
13862
|
+
from: dep.id, to: id, relation: dep.relation,
|
|
13863
|
+
label: dep.relationLabel ?? style.label,
|
|
13864
|
+
structural: false, back: false,
|
|
13865
|
+
});
|
|
13866
|
+
}
|
|
13867
|
+
}
|
|
13868
|
+
return { order, nodes, edges: [...edges.values()] };
|
|
13869
|
+
}
|
|
13870
|
+
/**
|
|
13871
|
+
* Longest-path layering: a node sits one layer past the deepest thing it depends
|
|
13872
|
+
* on, so every forward edge points strictly onwards and a node with several
|
|
13873
|
+
* predecessors clears *all* of them. Nodes with no predecessor land in layer 0.
|
|
13874
|
+
*
|
|
13875
|
+
* A cycle can't be layered — some link in the loop has to point backwards. A DFS
|
|
13876
|
+
* picks those out (a link to a node still on the stack) and marks them `back`;
|
|
13877
|
+
* the layering then runs on what's left, which is acyclic by construction. So a
|
|
13878
|
+
* cyclic input lays out fine, with the loop-closing links drawn as return edges.
|
|
13879
|
+
*/
|
|
13880
|
+
function assignLayers(order, edges) {
|
|
13881
|
+
const out = new Map();
|
|
13882
|
+
for (const edge of edges)
|
|
13883
|
+
push(out, edge.from, edge);
|
|
13884
|
+
// Iterative DFS: a recursive one overflows the stack on a long chain, and a
|
|
13885
|
+
// process-order graph is mostly chain.
|
|
13886
|
+
const UNSEEN = 0, ON_STACK = 1, DONE = 2;
|
|
13887
|
+
const state = new Map();
|
|
13888
|
+
for (const start of order) {
|
|
13889
|
+
if (state.get(start))
|
|
13890
|
+
continue;
|
|
13891
|
+
state.set(start, ON_STACK);
|
|
13892
|
+
const stack = [{ id: start, next: 0 }];
|
|
13893
|
+
while (stack.length) {
|
|
13894
|
+
const frame = stack[stack.length - 1];
|
|
13895
|
+
const outgoing = out.get(frame.id) ?? [];
|
|
13896
|
+
if (frame.next >= outgoing.length) {
|
|
13897
|
+
state.set(frame.id, DONE);
|
|
13898
|
+
stack.pop();
|
|
13899
|
+
continue;
|
|
13900
|
+
}
|
|
13901
|
+
const edge = outgoing[frame.next++];
|
|
13902
|
+
const seen = state.get(edge.to) ?? UNSEEN;
|
|
13903
|
+
if (seen === ON_STACK)
|
|
13904
|
+
edge.back = true; // closes a loop
|
|
13905
|
+
if (seen !== UNSEEN)
|
|
13906
|
+
continue;
|
|
13907
|
+
state.set(edge.to, ON_STACK);
|
|
13908
|
+
stack.push({ id: edge.to, next: 0 });
|
|
13909
|
+
}
|
|
13910
|
+
}
|
|
13911
|
+
// Longest path over what's left, walked in topological order.
|
|
13912
|
+
const indegree = new Map(order.map(id => [id, 0]));
|
|
13913
|
+
for (const edge of edges) {
|
|
13914
|
+
if (!edge.back)
|
|
13915
|
+
indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
|
|
13916
|
+
}
|
|
13917
|
+
const layer = new Map(order.map(id => [id, 0]));
|
|
13918
|
+
const queue = order.filter(id => !indegree.get(id));
|
|
13919
|
+
for (let i = 0; i < queue.length; i++) {
|
|
13920
|
+
for (const edge of out.get(queue[i]) ?? []) {
|
|
13921
|
+
if (edge.back)
|
|
13922
|
+
continue;
|
|
13923
|
+
layer.set(edge.to, Math.max(layer.get(edge.to) ?? 0, (layer.get(queue[i]) ?? 0) + 1));
|
|
13924
|
+
const remaining = (indegree.get(edge.to) ?? 0) - 1;
|
|
13925
|
+
indegree.set(edge.to, remaining);
|
|
13926
|
+
if (!remaining)
|
|
13927
|
+
queue.push(edge.to);
|
|
13928
|
+
}
|
|
13929
|
+
}
|
|
13930
|
+
return layer;
|
|
13931
|
+
}
|
|
13932
|
+
/** Median of the neighbour positions, or -1 when a node has no neighbours to follow. */
|
|
13933
|
+
function median(values) {
|
|
13934
|
+
if (!values.length)
|
|
13935
|
+
return -1;
|
|
13936
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
13937
|
+
const mid = sorted.length >> 1;
|
|
13938
|
+
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
13939
|
+
}
|
|
13940
|
+
/** Row index of every node within its own layer. */
|
|
13941
|
+
function positionIndex(layers) {
|
|
13942
|
+
const index = new Map();
|
|
13943
|
+
for (const layer of layers)
|
|
13944
|
+
layer.forEach((id, i) => index.set(id, i));
|
|
13945
|
+
return index;
|
|
13946
|
+
}
|
|
13947
|
+
/**
|
|
13948
|
+
* Edges cross when their endpoints are ordered oppositely in the two layers they
|
|
13949
|
+
* span. Counted per layer *pair* rather than per adjacent layer, so an edge
|
|
13950
|
+
* skipping a layer is scored too — this layout has no dummy nodes, and in a
|
|
13951
|
+
* dependency graph the long edges are exactly the ones that tangle.
|
|
13952
|
+
*/
|
|
13953
|
+
function countCrossings(layers, edges, layerOf) {
|
|
13954
|
+
const index = positionIndex(layers);
|
|
13955
|
+
const groups = new Map();
|
|
13956
|
+
for (const edge of edges) {
|
|
13957
|
+
if (edge.back)
|
|
13958
|
+
continue;
|
|
13959
|
+
const from = index.get(edge.from);
|
|
13960
|
+
const to = index.get(edge.to);
|
|
13961
|
+
if (from === undefined || to === undefined)
|
|
13962
|
+
continue;
|
|
13963
|
+
push(groups, `${layerOf.get(edge.from)}:${layerOf.get(edge.to)}`, [from, to]);
|
|
13964
|
+
}
|
|
13965
|
+
let crossings = 0;
|
|
13966
|
+
for (const pairs of groups.values()) {
|
|
13967
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
13968
|
+
for (let j = i + 1; j < pairs.length; j++) {
|
|
13969
|
+
if ((pairs[i][0] - pairs[j][0]) * (pairs[i][1] - pairs[j][1]) < 0)
|
|
13970
|
+
crossings++;
|
|
13971
|
+
}
|
|
13972
|
+
}
|
|
13973
|
+
}
|
|
13974
|
+
return crossings;
|
|
13975
|
+
}
|
|
13976
|
+
/** Sweeps to run over the layers; each one reorders against the previous result. */
|
|
13977
|
+
const ORDER_SWEEPS = 8;
|
|
13978
|
+
/**
|
|
13979
|
+
* Crossing minimisation, median heuristic (Sugiyama's second phase): a node wants
|
|
13980
|
+
* to sit level with the middle of its neighbours, and ordering every layer that
|
|
13981
|
+
* way repeatedly untangles the edges between them.
|
|
13982
|
+
*
|
|
13983
|
+
* Sweeps alternate direction — downward passes order a layer by its predecessors,
|
|
13984
|
+
* upward passes by its successors — because settling one side alone just moves the
|
|
13985
|
+
* tangle to the other. The heuristic isn't monotonic, so the best ordering seen is
|
|
13986
|
+
* kept rather than the last one.
|
|
13987
|
+
*/
|
|
13988
|
+
function orderLayers(layers, edges, layerOf) {
|
|
13989
|
+
const predecessors = new Map();
|
|
13990
|
+
const successors = new Map();
|
|
13991
|
+
for (const edge of edges) {
|
|
13992
|
+
if (edge.back)
|
|
13993
|
+
continue;
|
|
13994
|
+
push(predecessors, edge.to, edge.from);
|
|
13995
|
+
push(successors, edge.from, edge.to);
|
|
13996
|
+
}
|
|
13997
|
+
const current = layers.map(layer => [...layer]);
|
|
13998
|
+
let best = current.map(layer => [...layer]);
|
|
13999
|
+
let bestCrossings = countCrossings(current, edges, layerOf);
|
|
14000
|
+
for (let sweep = 0; sweep < ORDER_SWEEPS && bestCrossings > 0; sweep++) {
|
|
14001
|
+
const downward = sweep % 2 === 0;
|
|
14002
|
+
const reference = downward ? predecessors : successors;
|
|
14003
|
+
const index = positionIndex(current);
|
|
14004
|
+
// The first layer of a downward sweep (and the last of an upward one) has no
|
|
14005
|
+
// reference layer, so it stays as it is and anchors the sweep.
|
|
14006
|
+
const targets = current.map((_, i) => i);
|
|
14007
|
+
for (const i of downward ? targets.slice(1) : targets.slice(0, -1).reverse()) {
|
|
14008
|
+
current[i] = [...current[i]]
|
|
14009
|
+
.map((id, row) => {
|
|
14010
|
+
const neighbours = (reference.get(id) ?? [])
|
|
14011
|
+
.map(n => index.get(n))
|
|
14012
|
+
.filter((p) => p !== undefined);
|
|
14013
|
+
const m = median(neighbours);
|
|
14014
|
+
// No neighbours to follow: hold the current row, so a free node doesn't
|
|
14015
|
+
// get swept to the top of the layer on every pass.
|
|
14016
|
+
return { id, row, key: m < 0 ? row : m };
|
|
14017
|
+
})
|
|
14018
|
+
.sort((a, b) => a.key - b.key || a.row - b.row)
|
|
14019
|
+
.map(entry => entry.id);
|
|
14020
|
+
}
|
|
14021
|
+
const crossings = countCrossings(current, edges, layerOf);
|
|
14022
|
+
if (crossings < bestCrossings) {
|
|
14023
|
+
bestCrossings = crossings;
|
|
14024
|
+
best = current.map(layer => [...layer]);
|
|
14025
|
+
}
|
|
14026
|
+
}
|
|
14027
|
+
return best;
|
|
14028
|
+
}
|
|
14029
|
+
/** Alignment passes run after ordering, to straighten the edges it left behind. */
|
|
14030
|
+
const ALIGN_PASSES = 4;
|
|
14031
|
+
/**
|
|
14032
|
+
* Secondary-axis coordinates: with the row *order* fixed, slide each node towards
|
|
14033
|
+
* the middle of its neighbours so chains run straight instead of stair-stepping.
|
|
14034
|
+
*
|
|
14035
|
+
* Order and spacing are invariants here — the ordering phase chose them, and this
|
|
14036
|
+
* must not undo its work. So each layer is placed by a forward cascade that can
|
|
14037
|
+
* only push nodes further along, then shifted back by the average push: a uniform
|
|
14038
|
+
* shift keeps every gap and every relative position, and cancels the drift the
|
|
14039
|
+
* cascade would otherwise accumulate down the layer.
|
|
14040
|
+
*/
|
|
14041
|
+
function alignSecondary(layers, edges, step) {
|
|
14042
|
+
const predecessors = new Map();
|
|
14043
|
+
const successors = new Map();
|
|
14044
|
+
for (const edge of edges) {
|
|
14045
|
+
if (edge.back)
|
|
14046
|
+
continue;
|
|
14047
|
+
push(predecessors, edge.to, edge.from);
|
|
14048
|
+
push(successors, edge.from, edge.to);
|
|
14049
|
+
}
|
|
14050
|
+
const pos = new Map();
|
|
14051
|
+
for (const layer of layers)
|
|
14052
|
+
layer.forEach((id, i) => pos.set(id, i * step));
|
|
14053
|
+
for (let pass = 0; pass < ALIGN_PASSES; pass++) {
|
|
14054
|
+
const downward = pass % 2 === 0;
|
|
14055
|
+
const reference = downward ? predecessors : successors;
|
|
14056
|
+
const indices = layers.map((_, i) => i);
|
|
14057
|
+
for (const i of downward ? indices.slice(1) : indices.slice(0, -1).reverse()) {
|
|
14058
|
+
const layer = layers[i];
|
|
14059
|
+
if (!layer.length)
|
|
14060
|
+
continue;
|
|
14061
|
+
const wanted = layer.map(id => {
|
|
14062
|
+
const neighbours = (reference.get(id) ?? [])
|
|
14063
|
+
.map(n => pos.get(n))
|
|
14064
|
+
.filter((p) => p !== undefined);
|
|
14065
|
+
return neighbours.length ? median(neighbours) : (pos.get(id) ?? 0);
|
|
14066
|
+
});
|
|
14067
|
+
const placed = [];
|
|
14068
|
+
for (let k = 0; k < layer.length; k++) {
|
|
14069
|
+
placed[k] = k === 0 ? wanted[k] : Math.max(wanted[k], placed[k - 1] + step);
|
|
14070
|
+
}
|
|
14071
|
+
const drift = placed.reduce((sum, p, k) => sum + (p - wanted[k]), 0) / placed.length;
|
|
14072
|
+
layer.forEach((id, k) => pos.set(id, placed[k] - drift));
|
|
14073
|
+
}
|
|
14074
|
+
}
|
|
14075
|
+
// Normalise so the graph starts at the origin, like the tree layout does.
|
|
14076
|
+
const min = Math.min(...pos.values());
|
|
14077
|
+
for (const [id, value] of pos)
|
|
14078
|
+
pos.set(id, value - min);
|
|
14079
|
+
return pos;
|
|
14080
|
+
}
|
|
14081
|
+
/**
|
|
14082
|
+
* Places the nodes of a dependency graph in layers running left→right
|
|
14083
|
+
* (`horizontal`) or top→bottom (`vertical`).
|
|
14084
|
+
*
|
|
14085
|
+
* Unlike the tree layout, the column a node lands in is decided by the *graph*, not
|
|
14086
|
+
* by the shape of the `children` nesting it happened to arrive in: layer = longest
|
|
14087
|
+
* dependency path to it. Every link is drawn, including the ones a tree layout has
|
|
14088
|
+
* to discard to keep a single parent per node.
|
|
14089
|
+
*/
|
|
14090
|
+
function layoutLayered(roots, dir, allNodes) {
|
|
14091
|
+
const { order, nodes: originals, edges } = collectGraph(roots);
|
|
14092
|
+
if (!order.length) {
|
|
14093
|
+
return { nodes: [], edges: [], nodeMap: new Map(), width: 0, height: 0 };
|
|
14094
|
+
}
|
|
14095
|
+
const layerOf = assignLayers(order, edges);
|
|
14096
|
+
const depth = Math.max(...layerOf.values());
|
|
14097
|
+
const layers = Array.from({ length: depth + 1 }, () => []);
|
|
14098
|
+
for (const id of order)
|
|
14099
|
+
layers[layerOf.get(id) ?? 0].push(id);
|
|
14100
|
+
const ordered = orderLayers(layers, edges, layerOf);
|
|
14101
|
+
const primarySize = dir === 'horizontal' ? NODE_WIDTH : NODE_HEIGHT;
|
|
14102
|
+
const secondarySize = dir === 'horizontal' ? NODE_HEIGHT : NODE_WIDTH;
|
|
14103
|
+
const secondary = alignSecondary(ordered, edges, secondarySize + V_GAP);
|
|
14104
|
+
const nodes = [];
|
|
14105
|
+
const nodeMap = new Map();
|
|
14106
|
+
for (const layer of ordered) {
|
|
14107
|
+
for (const id of layer) {
|
|
14108
|
+
const source = originals.get(id);
|
|
14109
|
+
if (!source)
|
|
14110
|
+
continue;
|
|
14111
|
+
const original = allNodes.get(id);
|
|
14112
|
+
const primary = (layerOf.get(id) ?? 0) * (primarySize + H_GAP);
|
|
14113
|
+
const across = secondary.get(id) ?? 0;
|
|
14114
|
+
const laid = {
|
|
14115
|
+
id, label: source.label, description: source.description, icon: source.icon,
|
|
14116
|
+
status: source.status || 'default', type: source.type, moreCount: source.moreCount,
|
|
14117
|
+
x: dir === 'horizontal' ? primary : across,
|
|
14118
|
+
y: dir === 'horizontal' ? across : primary,
|
|
14119
|
+
width: NODE_WIDTH, height: NODE_HEIGHT,
|
|
14120
|
+
depth: layerOf.get(id) ?? 0,
|
|
14121
|
+
// Layers come from the whole graph, not from one parent — there is no
|
|
14122
|
+
// single node this one hangs off, so nothing to report as its parent.
|
|
14123
|
+
parentId: null,
|
|
14124
|
+
incomingCount: original?.dependsOn?.length ?? 0,
|
|
14125
|
+
outgoingCount: original?.children?.length ?? 0,
|
|
14126
|
+
};
|
|
14127
|
+
nodes.push(laid);
|
|
14128
|
+
nodeMap.set(id, laid);
|
|
14129
|
+
}
|
|
14130
|
+
}
|
|
14131
|
+
const laidOut = [...nodeMap.values()];
|
|
14132
|
+
const layoutEdges = [];
|
|
14133
|
+
for (const edge of edges) {
|
|
14134
|
+
const source = nodeMap.get(edge.from);
|
|
14135
|
+
const target = nodeMap.get(edge.to);
|
|
14136
|
+
if (!source || !target)
|
|
14137
|
+
continue;
|
|
14138
|
+
const relation = resolveRelation(edge.relation);
|
|
14139
|
+
const style = RELATION_STYLES[relation];
|
|
14140
|
+
// A back edge runs against the flow by definition, so the direct route would
|
|
14141
|
+
// double back through its own layers — it always bows clear instead.
|
|
14142
|
+
const geo = routeCrossRef(source, target, dir, laidOut, /* allowDirect */ !edge.back);
|
|
14143
|
+
layoutEdges.push({
|
|
14144
|
+
id: `${edge.from}${edge.back ? '↺' : '⇢'}${edge.to}`,
|
|
14145
|
+
sourceId: edge.from, targetId: edge.to,
|
|
14146
|
+
path: geo.path, relation: edge.relation, label: edge.label,
|
|
14147
|
+
labelX: geo.labelX, labelY: geo.labelY,
|
|
14148
|
+
color: edge.structural ? 'var(--color-neutral-400)' : style.color,
|
|
14149
|
+
dashed: edge.back || (!edge.structural && style.dashed),
|
|
14150
|
+
// Everything here is a real dependency, so nothing is dimmed as secondary
|
|
14151
|
+
// except the return edges — which the layering had to break to lay out.
|
|
14152
|
+
isCrossRef: edge.back,
|
|
14153
|
+
arrow: true,
|
|
14154
|
+
marker: relation,
|
|
14155
|
+
});
|
|
14156
|
+
}
|
|
14157
|
+
const width = (depth + 1) * primarySize + depth * H_GAP;
|
|
14158
|
+
const across = Math.max(...laidOut.map(n => (dir === 'horizontal' ? n.y : n.x))) + secondarySize;
|
|
14159
|
+
return {
|
|
14160
|
+
nodes,
|
|
14161
|
+
edges: layoutEdges,
|
|
14162
|
+
nodeMap,
|
|
14163
|
+
width: dir === 'horizontal' ? width : across,
|
|
14164
|
+
height: dir === 'horizontal' ? across : width,
|
|
14165
|
+
};
|
|
14166
|
+
}
|
|
13810
14167
|
// ── Status colors ────────────────────────────────────────────────────────────
|
|
13811
14168
|
// Labels read from the theme's semantic ink, not from a `--color-<status>-700`.
|
|
13812
14169
|
// Those resolve to dark ink in both themes (error-700 is #6b0909), which on the
|
|
@@ -13861,6 +14218,8 @@ function readableInk(color) {
|
|
|
13861
14218
|
* Dependency viewer component for visualizing hierarchical and cross-cutting relationships.
|
|
13862
14219
|
*
|
|
13863
14220
|
* Features:
|
|
14221
|
+
* - Tree layout (depth in the `children` nesting) and layered/DAG layout (depth in
|
|
14222
|
+
* the dependency graph), via `layout`
|
|
13864
14223
|
* - Horizontal (left-to-right) and vertical (top-to-bottom) layout directions
|
|
13865
14224
|
* - Bidirectional dependencies: children (right/down) and dependsOn (cross-references)
|
|
13866
14225
|
* - Relationship types: depends, blocks, references, requires, extends, implements, uses
|
|
@@ -13869,7 +14228,8 @@ function readableInk(color) {
|
|
|
13869
14228
|
* - Color-coded edges per relationship type
|
|
13870
14229
|
* - Node status colors (default, active, success, warning, error, muted)
|
|
13871
14230
|
* - Pan and zoom controls with mouse wheel support
|
|
13872
|
-
* -
|
|
14231
|
+
* - Fitting (`fitMode` / `autoFit`, `minNodeSize`) and a static, non-navigable
|
|
14232
|
+
* mode (`interactive: false`)
|
|
13873
14233
|
* - Collapsible sub-trees
|
|
13874
14234
|
* - Interactive node selection with detail panel
|
|
13875
14235
|
* - Legend showing active relationship types
|
|
@@ -13878,11 +14238,24 @@ function readableInk(color) {
|
|
|
13878
14238
|
*
|
|
13879
14239
|
* ## Feeding it a graph
|
|
13880
14240
|
*
|
|
13881
|
-
* `root`
|
|
13882
|
-
*
|
|
13883
|
-
*
|
|
13884
|
-
*
|
|
13885
|
-
*
|
|
14241
|
+
* `root` takes a single node (a tree) or an array (a forest — the shape to use for
|
|
14242
|
+
* a flat set of items related only by `dependsOn`). Either way the input is treated
|
|
14243
|
+
* as a graph: a link back to a node that already has a place in the layout — a
|
|
14244
|
+
* cycle, or a node reached from a second parent — is drawn as a cross-reference
|
|
14245
|
+
* arrow instead of being followed. Every node is laid out exactly once, and no
|
|
14246
|
+
* input can make the layout recurse forever.
|
|
14247
|
+
*
|
|
14248
|
+
* ## Picking a layout
|
|
14249
|
+
*
|
|
14250
|
+
* `tree` (default) answers "what contains what": the column is the depth in the
|
|
14251
|
+
* `children` nesting, and `dependsOn` links are drawn over the top without moving
|
|
14252
|
+
* anything.
|
|
14253
|
+
*
|
|
14254
|
+
* `layered` answers "what has to happen first": the column is the depth in the
|
|
14255
|
+
* dependency graph, so a node always sits past everything it depends on. Reach for
|
|
14256
|
+
* it whenever the question is about order or impact rather than containment —
|
|
14257
|
+
* a tree layout can only approximate that by first reducing the graph to a
|
|
14258
|
+
* spanning tree, which throws away every link that doesn't fit one parent per node.
|
|
13886
14259
|
*
|
|
13887
14260
|
* For incremental exploration, hand in a wider `root` per step and let
|
|
13888
14261
|
* `nodeExpand` drive the loading. Pan, zoom, collapse state and selection all
|
|
@@ -13894,6 +14267,17 @@ function readableInk(color) {
|
|
|
13894
14267
|
* <lc-dependency-viewer [root]="specTree" direction="horizontal" />
|
|
13895
14268
|
* ```
|
|
13896
14269
|
*
|
|
14270
|
+
* @example Order of work — a flat set of items, laid out by their dependencies
|
|
14271
|
+
* ```html
|
|
14272
|
+
* <lc-dependency-viewer
|
|
14273
|
+
* [root]="items()"
|
|
14274
|
+
* layout="layered"
|
|
14275
|
+
* direction="horizontal"
|
|
14276
|
+
* fitMode="fit-width"
|
|
14277
|
+
* (nodeSelect)="openItem($event)"
|
|
14278
|
+
* />
|
|
14279
|
+
* ```
|
|
14280
|
+
*
|
|
13897
14281
|
* @example Static overview — the whole graph at a glance, click-through only
|
|
13898
14282
|
* ```html
|
|
13899
14283
|
* <lc-dependency-viewer
|
|
@@ -13917,9 +14301,28 @@ function readableInk(color) {
|
|
|
13917
14301
|
* ```
|
|
13918
14302
|
*/
|
|
13919
14303
|
class DependencyViewerComponent {
|
|
14304
|
+
/**
|
|
14305
|
+
* The graph to draw. A single node is the root of a tree; an array is a forest,
|
|
14306
|
+
* which is how you feed a flat set of items that has no natural root — e.g. the
|
|
14307
|
+
* specs of a plan, related only by `dependsOn`.
|
|
14308
|
+
*/
|
|
13920
14309
|
root = input.required(...(ngDevMode ? [{ debugName: "root" }] : /* istanbul ignore next */ []));
|
|
13921
14310
|
direction = input('horizontal', ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
13922
14311
|
height = input('500px', ...(ngDevMode ? [{ debugName: "height" }] : /* istanbul ignore next */ []));
|
|
14312
|
+
/**
|
|
14313
|
+
* `tree` (default) places nodes by their depth in the `children` nesting, and
|
|
14314
|
+
* draws `dependsOn` links over the top as cross-references.
|
|
14315
|
+
*
|
|
14316
|
+
* `layered` places them by their depth in the *dependency* graph: a node always
|
|
14317
|
+
* sits past everything it depends on, transitively, and nodes that depend on
|
|
14318
|
+
* nothing start the first layer. That makes `direction="horizontal"` read as an
|
|
14319
|
+
* order of work — left to right — which a tree layout can only approximate by
|
|
14320
|
+
* first throwing away every link that doesn't fit a single-parent hierarchy.
|
|
14321
|
+
*
|
|
14322
|
+
* Use `layered` for process, sequencing and impact views; `tree` for containment
|
|
14323
|
+
* hierarchies, where the nesting *is* the structure.
|
|
14324
|
+
*/
|
|
14325
|
+
layout = input('tree', ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
13923
14326
|
showToolbar = input(true, ...(ngDevMode ? [{ debugName: "showToolbar" }] : /* istanbul ignore next */ []));
|
|
13924
14327
|
showEdgeLabels = input(true, ...(ngDevMode ? [{ debugName: "showEdgeLabels" }] : /* istanbul ignore next */ []));
|
|
13925
14328
|
edgeWidth = input(1.5, ...(ngDevMode ? [{ debugName: "edgeWidth" }] : /* istanbul ignore next */ []));
|
|
@@ -13932,8 +14335,29 @@ class DependencyViewerComponent {
|
|
|
13932
14335
|
* natural size instead of ballooning to fill the frame.
|
|
13933
14336
|
*
|
|
13934
14337
|
* Pair with `interactive: false` for a purely static display.
|
|
14338
|
+
*
|
|
14339
|
+
* Shorthand for `fitMode="contain"`, which supersedes it.
|
|
13935
14340
|
*/
|
|
13936
14341
|
autoFit = input(false, ...(ngDevMode ? [{ debugName: "autoFit" }] : /* istanbul ignore next */ []));
|
|
14342
|
+
/**
|
|
14343
|
+
* How the graph is scaled into the frame. Defaults to `autoFit`'s setting —
|
|
14344
|
+
* `contain` when it is on, `none` when it isn't — and overrides it when set.
|
|
14345
|
+
*
|
|
14346
|
+
* `contain` fits both axes, so everything is visible at once but a big graph
|
|
14347
|
+
* shrinks to get there. `fit-width` fits the width only and lets the viewer grow
|
|
14348
|
+
* as tall as the graph needs, so the nodes keep a readable size and the *page*
|
|
14349
|
+
* scrolls; the graph itself never scrolls internally. Past roughly 20 nodes a
|
|
14350
|
+
* process view generally wants `fit-width`.
|
|
14351
|
+
*/
|
|
14352
|
+
fitMode = input(null, ...(ngDevMode ? [{ debugName: "fitMode" }] : /* istanbul ignore next */ []));
|
|
14353
|
+
/**
|
|
14354
|
+
* Smallest node width, in px, that fitting may shrink to. Below it the fit stops
|
|
14355
|
+
* scaling down and lets the graph overflow instead — which `fit-width` turns into
|
|
14356
|
+
* page scroll, and `contain` into clipping. Pair it with `fit-width`.
|
|
14357
|
+
*
|
|
14358
|
+
* Nodes are 160px wide unscaled, so e.g. `80` allows shrinking to half size.
|
|
14359
|
+
*/
|
|
14360
|
+
minNodeSize = input(null, ...(ngDevMode ? [{ debugName: "minNodeSize" }] : /* istanbul ignore next */ []));
|
|
13937
14361
|
/**
|
|
13938
14362
|
* Viewport navigation: drag-to-pan, wheel-zoom and the toolbar's zoom buttons.
|
|
13939
14363
|
* `false` freezes the viewport — node selection, expansion and collapse keep
|
|
@@ -13968,37 +14392,42 @@ class DependencyViewerComponent {
|
|
|
13968
14392
|
canvasRef = viewChild('canvas', ...(ngDevMode ? [{ debugName: "canvasRef" }] : /* istanbul ignore next */ []));
|
|
13969
14393
|
/** Last known layout position of the anchor, to compensate pan after a relayout. */
|
|
13970
14394
|
anchorPos = null;
|
|
13971
|
-
|
|
14395
|
+
roots = computed(() => {
|
|
14396
|
+
const root = this.root();
|
|
14397
|
+
return Array.isArray(root) ? root : [root];
|
|
14398
|
+
}, ...(ngDevMode ? [{ debugName: "roots" }] : /* istanbul ignore next */ []));
|
|
14399
|
+
effectiveRoots = computed(() => {
|
|
14400
|
+
const collapsed = this.collapsedIds();
|
|
14401
|
+
// One `visited` set across the whole forest: a node reachable from two roots
|
|
14402
|
+
// is the same node, and must be pruned identically wherever it turns up.
|
|
14403
|
+
const visited = new Set();
|
|
14404
|
+
return this.roots().map(root => this.pruneCollapsed(root, collapsed, visited));
|
|
14405
|
+
}, ...(ngDevMode ? [{ debugName: "effectiveRoots" }] : /* istanbul ignore next */ []));
|
|
13972
14406
|
allOriginalNodes = computed(() => {
|
|
13973
14407
|
const map = new Map();
|
|
13974
|
-
|
|
14408
|
+
for (const root of this.roots())
|
|
14409
|
+
collectAllNodes(root, map);
|
|
13975
14410
|
return map;
|
|
13976
14411
|
}, ...(ngDevMode ? [{ debugName: "allOriginalNodes" }] : /* istanbul ignore next */ []));
|
|
13977
|
-
|
|
13978
|
-
|
|
14412
|
+
/**
|
|
14413
|
+
* The laid-out graph. Named `graph` rather than `layout` because `layout` is the
|
|
14414
|
+
* input that picks *how* it gets laid out.
|
|
14415
|
+
*/
|
|
14416
|
+
graph = computed(() => {
|
|
13979
14417
|
const dir = this.direction();
|
|
13980
14418
|
const hiddenTypes = new Set(this.hiddenTypes());
|
|
13981
14419
|
const hiddenRelations = new Set(this.hiddenRelations());
|
|
13982
|
-
const backEdges = [];
|
|
13983
|
-
const measured = measureTree(root, 0, dir, new Set(), backEdges);
|
|
13984
|
-
const nodes = [];
|
|
13985
|
-
const nodeMap = new Map();
|
|
13986
|
-
const treeEdges = [];
|
|
13987
14420
|
const allNodes = this.allOriginalNodes();
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
layoutTreeV(measured, 0, 0, null, nodes, treeEdges, allNodes, nodeMap);
|
|
13993
|
-
}
|
|
13994
|
-
const crossEdges = createCrossRefEdges(nodeMap, allNodes, dir, backEdges);
|
|
13995
|
-
const edges = [...treeEdges, ...crossEdges].filter(e => !(e.relation && hiddenRelations.has(e.relation)));
|
|
14421
|
+
const placed = this.layout() === 'layered'
|
|
14422
|
+
? layoutLayered(this.effectiveRoots(), dir, allNodes)
|
|
14423
|
+
: this.layoutTree(dir, allNodes);
|
|
14424
|
+
const edges = placed.edges.filter(e => !(e.relation && hiddenRelations.has(e.relation)));
|
|
13996
14425
|
// Type filtering hides nodes and anything still pointing at them. It runs
|
|
13997
14426
|
// after layout so the remaining nodes keep the positions they'd have anyway —
|
|
13998
14427
|
// toggling a filter must not reshuffle the whole graph.
|
|
13999
14428
|
const visibleNodes = hiddenTypes.size
|
|
14000
|
-
? nodes.filter(n => !(n.type && hiddenTypes.has(n.type)))
|
|
14001
|
-
: nodes;
|
|
14429
|
+
? placed.nodes.filter(n => !(n.type && hiddenTypes.has(n.type)))
|
|
14430
|
+
: placed.nodes;
|
|
14002
14431
|
const visibleIds = new Set(visibleNodes.map(n => n.id));
|
|
14003
14432
|
const visibleEdges = hiddenTypes.size
|
|
14004
14433
|
? edges.filter(e => visibleIds.has(e.sourceId) && visibleIds.has(e.targetId))
|
|
@@ -14006,11 +14435,66 @@ class DependencyViewerComponent {
|
|
|
14006
14435
|
return {
|
|
14007
14436
|
nodes: visibleNodes,
|
|
14008
14437
|
edges: visibleEdges,
|
|
14438
|
+
nodeMap: placed.nodeMap,
|
|
14439
|
+
width: placed.width,
|
|
14440
|
+
height: placed.height,
|
|
14441
|
+
};
|
|
14442
|
+
}, ...(ngDevMode ? [{ debugName: "graph" }] : /* istanbul ignore next */ []));
|
|
14443
|
+
/**
|
|
14444
|
+
* Tree layout over the forest: each root gets its own band along the secondary
|
|
14445
|
+
* axis. A `visited` set shared by all of them keeps a node that hangs off two
|
|
14446
|
+
* roots from being laid out twice — the second link becomes a cross-reference,
|
|
14447
|
+
* exactly as a second parent within one tree does.
|
|
14448
|
+
*/
|
|
14449
|
+
layoutTree(dir, allNodes) {
|
|
14450
|
+
const backEdges = [];
|
|
14451
|
+
const visited = new Set();
|
|
14452
|
+
const measured = this.effectiveRoots().map(root => measureTree(root, 0, dir, visited, backEdges));
|
|
14453
|
+
const nodes = [];
|
|
14454
|
+
const nodeMap = new Map();
|
|
14455
|
+
const treeEdges = [];
|
|
14456
|
+
let offset = 0;
|
|
14457
|
+
for (const measure of measured) {
|
|
14458
|
+
if (dir === 'horizontal') {
|
|
14459
|
+
layoutTreeH(measure, 0, offset, null, nodes, treeEdges, allNodes, nodeMap);
|
|
14460
|
+
}
|
|
14461
|
+
else {
|
|
14462
|
+
layoutTreeV(measure, offset, 0, null, nodes, treeEdges, allNodes, nodeMap);
|
|
14463
|
+
}
|
|
14464
|
+
offset += measure.secondary + V_GAP;
|
|
14465
|
+
}
|
|
14466
|
+
const primary = measured.length ? Math.max(...measured.map(m => m.primary)) : 0;
|
|
14467
|
+
const secondary = Math.max(0, offset - V_GAP);
|
|
14468
|
+
return {
|
|
14469
|
+
nodes,
|
|
14470
|
+
edges: [...treeEdges, ...createCrossRefEdges(nodeMap, allNodes, dir, backEdges)],
|
|
14009
14471
|
nodeMap,
|
|
14010
|
-
width: dir === 'horizontal' ?
|
|
14011
|
-
height: dir === 'horizontal' ?
|
|
14472
|
+
width: dir === 'horizontal' ? primary : secondary,
|
|
14473
|
+
height: dir === 'horizontal' ? secondary : primary,
|
|
14012
14474
|
};
|
|
14013
|
-
}
|
|
14475
|
+
}
|
|
14476
|
+
/**
|
|
14477
|
+
* Height `fit-width` derived from the graph, or `null` under any other mode.
|
|
14478
|
+
*
|
|
14479
|
+
* `fit-width` scales to the frame's width and lets the height follow the graph,
|
|
14480
|
+
* so a tall graph runs past the frame and the *page* scrolls instead of the nodes
|
|
14481
|
+
* shrinking out of legibility. The height is therefore always the graph's, never
|
|
14482
|
+
* the frame's — including when the graph is the shorter of the two. Deriving it
|
|
14483
|
+
* unconditionally is also what keeps the fit stable: this height becomes the
|
|
14484
|
+
* canvas's, so a fit that consulted the canvas height would be reading back its
|
|
14485
|
+
* own last output and could flip between two answers forever.
|
|
14486
|
+
*/
|
|
14487
|
+
contentHeight = signal(null, ...(ngDevMode ? [{ debugName: "contentHeight" }] : /* istanbul ignore next */ []));
|
|
14488
|
+
effectiveFitMode = computed(() => this.fitMode() ?? (this.autoFit() ? 'contain' : 'none'), ...(ngDevMode ? [{ debugName: "effectiveFitMode" }] : /* istanbul ignore next */ []));
|
|
14489
|
+
/** The host only gives up its configured height when the fit made it grow. */
|
|
14490
|
+
hostHeight = computed(() => (this.contentHeight() === null ? this.height() : 'auto'), ...(ngDevMode ? [{ debugName: "hostHeight" }] : /* istanbul ignore next */ []));
|
|
14491
|
+
/** `minNodeSize` expressed as the scale it forbids fitting to go below. */
|
|
14492
|
+
minFitScale = computed(() => {
|
|
14493
|
+
const min = this.minNodeSize();
|
|
14494
|
+
if (!min || min <= 0)
|
|
14495
|
+
return MIN_FIT_SCALE;
|
|
14496
|
+
return Math.min(MAX_FIT_SCALE, Math.max(MIN_FIT_SCALE, min / NODE_WIDTH));
|
|
14497
|
+
}, ...(ngDevMode ? [{ debugName: "minFitScale" }] : /* istanbul ignore next */ []));
|
|
14014
14498
|
constructor() {
|
|
14015
14499
|
// Keep the anchor node pinned to its screen position across relayouts. A new
|
|
14016
14500
|
// `root` (an expand step) re-runs the tree layout from scratch, which recentres
|
|
@@ -14018,11 +14502,11 @@ class DependencyViewerComponent {
|
|
|
14018
14502
|
// graph slides underneath it. Compensating the pan by the anchor's own delta is
|
|
14019
14503
|
// what makes incremental expansion feel stable.
|
|
14020
14504
|
effect(() => {
|
|
14021
|
-
const nodeMap = this.
|
|
14505
|
+
const nodeMap = this.graph().nodeMap;
|
|
14022
14506
|
const anchorId = this.anchorNodeId() ?? this.selectedNodeId();
|
|
14023
|
-
//
|
|
14507
|
+
// Fitting recentres the whole graph on every relayout, which subsumes
|
|
14024
14508
|
// holding one node still. Running both would have them fight over the pan.
|
|
14025
|
-
if (this.
|
|
14509
|
+
if (this.effectiveFitMode() !== 'none' || !anchorId) {
|
|
14026
14510
|
this.anchorPos = null;
|
|
14027
14511
|
return;
|
|
14028
14512
|
}
|
|
@@ -14050,24 +14534,31 @@ class DependencyViewerComponent {
|
|
|
14050
14534
|
// resolves after the initial render, which re-triggers this effect at the
|
|
14051
14535
|
// point the canvas finally has a measurable size.
|
|
14052
14536
|
effect(() => {
|
|
14053
|
-
if (
|
|
14537
|
+
if (this.effectiveFitMode() === 'none') {
|
|
14538
|
+
// Leaving fit-width behind: the grown height was this fit's doing, so it
|
|
14539
|
+
// goes with it, or the host keeps a size nothing is maintaining any more.
|
|
14540
|
+
untracked(() => this.contentHeight.set(null));
|
|
14541
|
+
return;
|
|
14542
|
+
}
|
|
14543
|
+
if (!this.canvasRef())
|
|
14054
14544
|
return;
|
|
14055
|
-
this.
|
|
14545
|
+
this.graph();
|
|
14546
|
+
this.minFitScale();
|
|
14056
14547
|
untracked(() => this.fit());
|
|
14057
14548
|
});
|
|
14058
14549
|
// …and when the container changes size. ResizeObserver delivers an initial
|
|
14059
14550
|
// callback on observe, so this covers the first fit in a browser too.
|
|
14060
14551
|
effect(onCleanup => {
|
|
14061
14552
|
const el = this.canvasRef()?.nativeElement;
|
|
14062
|
-
if (
|
|
14553
|
+
if (this.effectiveFitMode() === 'none' || !el || typeof ResizeObserver === 'undefined')
|
|
14063
14554
|
return;
|
|
14064
14555
|
const observer = new ResizeObserver(() => this.fit());
|
|
14065
14556
|
observer.observe(el);
|
|
14066
14557
|
onCleanup(() => observer.disconnect());
|
|
14067
14558
|
});
|
|
14068
14559
|
}
|
|
14069
|
-
svgWidth = computed(() => this.
|
|
14070
|
-
svgHeight = computed(() => this.
|
|
14560
|
+
svgWidth = computed(() => this.graph().width + 80, ...(ngDevMode ? [{ debugName: "svgWidth" }] : /* istanbul ignore next */ []));
|
|
14561
|
+
svgHeight = computed(() => this.graph().height + 80, ...(ngDevMode ? [{ debugName: "svgHeight" }] : /* istanbul ignore next */ []));
|
|
14071
14562
|
viewBox = computed(() => `0 0 ${this.svgWidth()} ${this.svgHeight()}`, ...(ngDevMode ? [{ debugName: "viewBox" }] : /* istanbul ignore next */ []));
|
|
14072
14563
|
transform = computed(() => {
|
|
14073
14564
|
const z = this.zoom() / 100;
|
|
@@ -14080,7 +14571,7 @@ class DependencyViewerComponent {
|
|
|
14080
14571
|
const id = this.selectedNodeId();
|
|
14081
14572
|
if (!id)
|
|
14082
14573
|
return null;
|
|
14083
|
-
return this.
|
|
14574
|
+
return this.graph().nodes.find(n => n.id === id) ?? null;
|
|
14084
14575
|
}, ...(ngDevMode ? [{ debugName: "selectedNode" }] : /* istanbul ignore next */ []));
|
|
14085
14576
|
selectedDependsOn = computed(() => {
|
|
14086
14577
|
const id = this.selectedNodeId();
|
|
@@ -14093,8 +14584,10 @@ class DependencyViewerComponent {
|
|
|
14093
14584
|
// of built-in relation types they borrow their colour from.
|
|
14094
14585
|
legendItems = computed(() => {
|
|
14095
14586
|
const seen = new Map();
|
|
14096
|
-
for (const edge of this.
|
|
14097
|
-
|
|
14587
|
+
for (const edge of this.graph().edges) {
|
|
14588
|
+
// Keyed off the label, not `isCrossRef`: in the layered layout the labelled
|
|
14589
|
+
// dependency edges are the main flow, not cross-references.
|
|
14590
|
+
if (!edge.label || seen.has(edge.label))
|
|
14098
14591
|
continue;
|
|
14099
14592
|
seen.set(edge.label, { label: edge.label, color: edge.color, dashed: edge.dashed });
|
|
14100
14593
|
}
|
|
@@ -14103,7 +14596,7 @@ class DependencyViewerComponent {
|
|
|
14103
14596
|
typeLegendItems = computed(() => {
|
|
14104
14597
|
const colors = this.typeColors();
|
|
14105
14598
|
const seen = new Map();
|
|
14106
|
-
for (const node of this.
|
|
14599
|
+
for (const node of this.graph().nodes) {
|
|
14107
14600
|
if (!node.type || seen.has(node.type) || !colors[node.type])
|
|
14108
14601
|
continue;
|
|
14109
14602
|
seen.set(node.type, { type: node.type, color: colors[node.type] });
|
|
@@ -14171,7 +14664,7 @@ class DependencyViewerComponent {
|
|
|
14171
14664
|
// expand/collapse. Mirrors lc-tree-view's expandAll()/collapseAll() surface.
|
|
14172
14665
|
/** Centres the viewport on a node. No-op for an id that isn't laid out. */
|
|
14173
14666
|
focusNode(id) {
|
|
14174
|
-
const node = this.
|
|
14667
|
+
const node = this.graph().nodeMap.get(id);
|
|
14175
14668
|
if (!node)
|
|
14176
14669
|
return;
|
|
14177
14670
|
const z = this.zoom() / 100;
|
|
@@ -14184,13 +14677,16 @@ class DependencyViewerComponent {
|
|
|
14184
14677
|
this.anchorPos = { id, x: node.x, y: node.y };
|
|
14185
14678
|
}
|
|
14186
14679
|
/**
|
|
14187
|
-
* Scales and centres the graph
|
|
14188
|
-
*
|
|
14680
|
+
* Scales and centres the graph to fit the canvas, per `fitMode`. Called for you
|
|
14681
|
+
* when fitting is on; public so a caller can fit on demand without it.
|
|
14189
14682
|
*
|
|
14190
14683
|
* No-op while the canvas has no size (before the first render, or in a detached
|
|
14191
|
-
* / `display: none` container) — the
|
|
14684
|
+
* / `display: none` container) — the fit observers re-run once it does.
|
|
14192
14685
|
*/
|
|
14193
14686
|
fit() {
|
|
14687
|
+
const mode = this.effectiveFitMode();
|
|
14688
|
+
if (mode === 'none')
|
|
14689
|
+
return;
|
|
14194
14690
|
const el = this.canvasEl();
|
|
14195
14691
|
const viewW = el?.clientWidth ?? 0;
|
|
14196
14692
|
const viewH = el?.clientHeight ?? 0;
|
|
@@ -14199,12 +14695,30 @@ class DependencyViewerComponent {
|
|
|
14199
14695
|
const box = this.contentBox();
|
|
14200
14696
|
if (box.width <= 0 || box.height <= 0)
|
|
14201
14697
|
return;
|
|
14202
|
-
|
|
14698
|
+
// `fit-width` deliberately ignores the height: it is the mode that lets the
|
|
14699
|
+
// graph be taller than the frame. That is also what keeps it stable — the
|
|
14700
|
+
// height it produces feeds back in as the canvas's own height, and a scale
|
|
14701
|
+
// derived from that height would chase itself.
|
|
14702
|
+
const natural = mode === 'fit-width'
|
|
14703
|
+
? viewW / box.width
|
|
14704
|
+
: Math.min(viewW / box.width, viewH / box.height);
|
|
14705
|
+
const scale = Math.min(MAX_FIT_SCALE, Math.max(this.minFitScale(), natural));
|
|
14203
14706
|
this.zoom.set(scale * 100);
|
|
14204
14707
|
// Invert screen = pan + scale * layout for the box's top-left corner, so the
|
|
14205
14708
|
// box lands centred in the canvas.
|
|
14206
14709
|
this.panX.set((viewW - box.width * scale) / 2 - box.x * scale);
|
|
14207
|
-
|
|
14710
|
+
const scaledHeight = box.height * scale;
|
|
14711
|
+
if (mode === 'fit-width') {
|
|
14712
|
+
// The viewer takes the graph's height and pins it to the top. Past the frame
|
|
14713
|
+
// that means the page scrolls through a readable graph rather than the frame
|
|
14714
|
+
// clipping one that was shrunk to fit.
|
|
14715
|
+
this.contentHeight.set(Math.ceil(scaledHeight));
|
|
14716
|
+
this.panY.set(-box.y * scale);
|
|
14717
|
+
}
|
|
14718
|
+
else {
|
|
14719
|
+
this.contentHeight.set(null);
|
|
14720
|
+
this.panY.set((viewH - scaledHeight) / 2 - box.y * scale);
|
|
14721
|
+
}
|
|
14208
14722
|
// The camera just moved deliberately; a stale anchor baseline would have the
|
|
14209
14723
|
// next relayout "correct" a position this fit had already chosen.
|
|
14210
14724
|
this.anchorPos = null;
|
|
@@ -14214,7 +14728,7 @@ class DependencyViewerComponent {
|
|
|
14214
14728
|
* default zoom and pan.
|
|
14215
14729
|
*/
|
|
14216
14730
|
resetView() {
|
|
14217
|
-
if (this.
|
|
14731
|
+
if (this.effectiveFitMode() !== 'none') {
|
|
14218
14732
|
this.fit();
|
|
14219
14733
|
return;
|
|
14220
14734
|
}
|
|
@@ -14268,7 +14782,7 @@ class DependencyViewerComponent {
|
|
|
14268
14782
|
* apex of the bow — the only part of it that reaches outside them.
|
|
14269
14783
|
*/
|
|
14270
14784
|
contentBox() {
|
|
14271
|
-
const { nodes, edges } = this.
|
|
14785
|
+
const { nodes, edges } = this.graph();
|
|
14272
14786
|
if (!nodes.length)
|
|
14273
14787
|
return { x: 0, y: 0, width: 0, height: 0 };
|
|
14274
14788
|
let minX = Infinity;
|
|
@@ -14335,12 +14849,12 @@ class DependencyViewerComponent {
|
|
|
14335
14849
|
return typeColor ? readableInk(typeColor) : STATUS_COLORS[node.status].text;
|
|
14336
14850
|
}
|
|
14337
14851
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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 });
|
|
14852
|
+
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 }, layout: { classPropertyName: "layout", publicName: "layout", 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 }, fitMode: { classPropertyName: "fitMode", publicName: "fitMode", isSignal: true, isRequired: false, transformFunction: null }, minNodeSize: { classPropertyName: "minNodeSize", publicName: "minNodeSize", 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": "hostHeight()" } }, 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 [class.dep-viewer__canvas--sized]=\"contentHeight() !== null\"\n [style.height.px]=\"contentHeight()\"\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 graph().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.arrow ? '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 graph().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 graph().edges; track edge.id) {\n @if (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__canvas--sized{flex:0 0 auto}.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 });
|
|
14339
14853
|
}
|
|
14340
14854
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: DependencyViewerComponent, decorators: [{
|
|
14341
14855
|
type: Component,
|
|
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: [{
|
|
14856
|
+
args: [{ selector: 'lc-dependency-viewer', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, host: { '[class]': '"dep-viewer"', '[style.height]': 'hostHeight()' }, 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 [class.dep-viewer__canvas--sized]=\"contentHeight() !== null\"\n [style.height.px]=\"contentHeight()\"\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 graph().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.arrow ? '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 graph().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 graph().edges; track edge.id) {\n @if (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__canvas--sized{flex:0 0 auto}.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"] }]
|
|
14857
|
+
}], 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 }] }], layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", 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 }] }], fitMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "fitMode", required: false }] }], minNodeSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "minNodeSize", 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: [{
|
|
14344
14858
|
type: HostListener,
|
|
14345
14859
|
args: ['document:mousemove', ['$event']]
|
|
14346
14860
|
}], onMouseUp: [{
|