Files
fusion/plugins/fusion-plugin-dependency-graph/src/edges.tsx
gsxdsm 62033e5ef7 FN-6733: render dependency graph edge styling
Render dependency graph connectors with CSS-resolvable styling so task dependencies are visible.

- Move dependency edge stroke, width, opacity, and arrowhead fill from SVG presentation attributes into inline styles.
- Cover highlighted, dimmed, empty, filtered, desktop, and mobile dependency graph edge rendering.
- Document the SVG custom-property requirement with an FNXC dependency graph comment.

Files changed:
 .../DependencyGraph.highlighting.test.tsx          | 39 ++++++++++++++++++++++
 .../src/__tests__/edges.test.tsx                   | 34 ++++++++++++++++---
 .../fusion-plugin-dependency-graph/src/edges.tsx   | 17 +++++++---
 3 files changed, 81 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-6733

Fusion-Task-Lineage: 167e7b56-f0eb-4b32-9f66-3b372ef27d60
2026-06-19 16:47:33 -07:00

80 lines
2.9 KiB
TypeScript

import type { GraphEdge, GraphPosition } from "./types.js";
import "./GraphHighlight.css";
interface GraphEdgesProps {
edges: GraphEdge[];
positions: Map<string, GraphPosition>;
nodeWidth?: number;
nodeHeight?: number;
nodeHeights?: ReadonlyMap<string, number>;
highlightedEdgeIds?: Set<string>;
}
const DEFAULT_NODE_WIDTH = 280;
const DEFAULT_NODE_HEIGHT = 100;
/**
* FNXC:DependencyGraphEdges 2026-06-19-08:59:
* Browser SVG presentation attributes do not resolve CSS custom properties, so dependency edge theme paint and widths must travel through real CSS via inline style or classes.
* Keep only literal SVG-safe values such as fill="none" as presentation attributes so dependency connector strokes and arrowheads remain visible across default, highlighted, and dimmed states.
*/
export function GraphEdges({
edges,
positions,
nodeWidth = DEFAULT_NODE_WIDTH,
nodeHeight = DEFAULT_NODE_HEIGHT,
nodeHeights,
highlightedEdgeIds,
}: GraphEdgesProps) {
const hasHighlights = Boolean(highlightedEdgeIds && highlightedEdgeIds.size > 0);
return (
<svg className="dependency-graph-edges" aria-hidden="true">
<defs>
<marker
id="dependency-graph-arrowhead"
markerWidth="10"
markerHeight="7"
refX="10"
refY="3.5"
orient="auto"
markerUnits="strokeWidth"
>
<path d="M 0 0 L 10 3.5 L 0 7 z" style={{ fill: "var(--border)" }} />
</marker>
</defs>
{edges.map((edge) => {
const source = positions.get(edge.source);
const target = positions.get(edge.target);
if (!source || !target) return null;
const edgeId = `${edge.source}->${edge.target}`;
const isActiveHighlight = hasHighlights && (highlightedEdgeIds?.has(edgeId) ?? false);
const x1 = source.x + nodeWidth / 2;
const y1 = source.y + (nodeHeights?.get(edge.source) ?? nodeHeight);
const x2 = target.x + nodeWidth / 2;
const y2 = target.y;
const controlY = y1 + (y2 - y1) / 2;
return (
<path
key={edgeId}
data-testid="dependency-edge"
data-edge-id={edgeId}
className={`dependency-graph-edge${isActiveHighlight ? " graph-edge--highlighted" : ""}${hasHighlights && !isActiveHighlight ? " graph-edge--dimmed" : ""}`}
d={`M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`}
fill="none"
markerEnd="url(#dependency-graph-arrowhead)"
style={{
opacity: hasHighlights && !isActiveHighlight ? 0.15 : 1,
stroke: isActiveHighlight ? "var(--todo)" : "var(--border)",
strokeWidth: isActiveHighlight ? "var(--space-xs)" : "var(--btn-border-width)",
transition: "opacity var(--transition-fast), stroke var(--transition-fast), stroke-width var(--transition-fast)",
}}
/>
);
})}
</svg>
);
}