diff --git a/.gitignore b/.gitignore
index 5ee4ab9..618ef1d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,3 +67,12 @@ evaluate/test_repos/
# Draft/duplicate files
docs/assets/marketing-diagram*
+
+# Audit/design working documents
+accessibility-audit.md
+design-critique.md
+design-handoff.md
+design-system-audit.md
+cross-audit-synthesis.md
+research-synthesis.md
+Quality-Audit-Report.docx
diff --git a/code-review-graph-vscode/src/views/graphWebview.ts b/code-review-graph-vscode/src/views/graphWebview.ts
index 5e67b36..039d102 100644
--- a/code-review-graph-vscode/src/views/graphWebview.ts
+++ b/code-review-graph-vscode/src/views/graphWebview.ts
@@ -409,25 +409,48 @@ export class GraphWebviewPanel {
cursor: pointer;
user-select: none;
border: 1px solid transparent;
- opacity: 0.45;
+ opacity: 0.55;
transition: opacity 0.15s, border-color 0.15s;
}
.edge-pill.active {
opacity: 1;
border-color: currentColor;
}
+ .edge-pill:focus-visible { outline: 2px solid var(--btn-bg, #89b4fa); outline-offset: 2px; }
.edge-pill .pill-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
+ /* Edge filter popover */
+ #edge-popover {
+ display: none;
+ position: absolute;
+ top: 100%;
+ left: 0;
+ margin-top: 4px;
+ background: var(--toolbar-bg);
+ border: 1px solid var(--toolbar-border);
+ border-radius: 6px;
+ padding: 8px 12px;
+ z-index: 100;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.3);
+ min-width: 160px;
+ }
+ #edge-popover.visible { display: flex; flex-wrap: wrap; gap: 6px; }
+ #edge-filter-wrap { position: relative; }
+
/* Depth slider */
#depth-slider {
width: 80px;
accent-color: var(--btn-bg);
cursor: pointer;
}
+ #depth-slider:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
#depth-value {
font-size: 11px;
font-family: var(--font-mono);
@@ -455,11 +478,14 @@ export class GraphWebviewPanel {
/* Node count badge */
#node-count {
+ position: absolute;
+ bottom: 8px;
+ right: 12px;
font-size: 11px;
color: var(--fg);
opacity: 0.6;
- margin-left: auto;
white-space: nowrap;
+ z-index: 5;
}
/* ------------------------------------------------------------------ */
@@ -516,12 +542,12 @@ export class GraphWebviewPanel {
#tooltip .tooltip-params {
font-family: var(--font-mono);
font-size: 11px;
- color: #a6e3a1;
+ color: var(--vscode-debugTokenExpression-string, #a6e3a1);
}
#tooltip .tooltip-return {
font-family: var(--font-mono);
font-size: 11px;
- color: #89b4fa;
+ color: var(--vscode-debugTokenExpression-number, #89b4fa);
}
/* ------------------------------------------------------------------ */
@@ -534,6 +560,9 @@ export class GraphWebviewPanel {
.pulse-ring {
animation: pulse 0.6s ease-out;
}
+
+ path.node-shape:focus { outline: none; }
+ path.node-shape:focus-visible { stroke: var(--btn-bg, #89b4fa) !important; stroke-width: 3 !important; }
@@ -541,21 +570,23 @@ export class GraphWebviewPanel {
-
+
+
+
+
+
No graph data available
+
Run Code Graph: Build Graph from the Command Palette to get started.
+
+
-
+
diff --git a/code-review-graph-vscode/src/webview/graph.ts b/code-review-graph-vscode/src/webview/graph.ts
index 0e788eb..6d214b0 100644
--- a/code-review-graph-vscode/src/webview/graph.ts
+++ b/code-review-graph-vscode/src/webview/graph.ts
@@ -70,28 +70,44 @@ interface SimLink extends d3.SimulationLinkDatum {
// ---------------------------------------------------------------------------
const NODE_RADIUS: Record = {
- File: 14,
+ File: 18,
Class: 12,
- Function: 10,
- Test: 10,
- Type: 10,
+ Function: 6,
+ Test: 6,
+ Type: 5,
};
const NODE_COLOR: Record = {
- File: "#cba6f7",
- Class: "#f9e2af",
- Function: "#a6e3a1",
- Test: "#89b4fa",
- Type: "#fab387",
+ File: "#58a6ff",
+ Class: "#f0883e",
+ Function: "#3fb950",
+ Test: "#d2a8ff",
+ Type: "#8b949e",
+};
+
+const NODE_SHAPE: Record = {
+ File: d3.symbolCircle,
+ Class: d3.symbolSquare,
+ Function: d3.symbolTriangle,
+ Test: d3.symbolDiamond,
+ Type: d3.symbolCross,
+};
+
+const NODE_AREA: Record = {
+ File: 616,
+ Class: 452,
+ Function: 314,
+ Test: 314,
+ Type: 314,
};
const EDGE_COLOR: Record = {
- CALLS: "#a6e3a1",
- IMPORTS_FROM: "#89b4fa",
- INHERITS: "#cba6f7",
+ CALLS: "#3fb950",
+ IMPORTS_FROM: "#f0883e",
+ INHERITS: "#d2a8ff",
IMPLEMENTS: "#f9e2af",
TESTED_BY: "#f38ba8",
- CONTAINS: "#585b70",
+ CONTAINS: "rgba(139,148,158,0.15)",
DEPENDS_ON: "#fab387",
};
@@ -128,7 +144,7 @@ let labelGroup: d3.Selection;
let zoomBehavior: d3.ZoomBehavior;
let linkSelection: d3.Selection;
-let nodeSelection: d3.Selection;
+let nodeSelection: d3.Selection;
let labelSelection: d3.Selection;
let currentTheme: "dark" | "light" = "dark";
@@ -185,7 +201,7 @@ function createSvg(): void {
// Initialize empty selections
linkSelection = linkGroup.selectAll("line");
- nodeSelection = nodeGroup.selectAll("circle");
+ nodeSelection = nodeGroup.selectAll("path.node-shape");
labelSelection = labelGroup.selectAll("text");
// Zoom + pan
@@ -243,7 +259,26 @@ function setData(nodes: GraphNode[], edges: GraphEdge[]): void {
depthValue.textContent = "All";
}
+ // Show/hide empty state
+ const emptyState = document.getElementById("empty-state");
+ const graphArea = document.getElementById("graph-area");
+ if (nodes.length === 0) {
+ if (emptyState) emptyState.style.display = "block";
+ if (graphArea) {
+ // Hide the SVG but keep the container
+ const svgHide = graphArea.querySelector("svg");
+ if (svgHide) svgHide.style.display = "none";
+ }
+ updateDepthSliderState();
+ return;
+ }
+ if (emptyState) emptyState.style.display = "none";
+ const svgEl = graphArea?.querySelector("svg");
+ if (svgEl) svgEl.style.display = "";
+
buildGraph();
+
+ updateDepthSliderState();
}
// ---------------------------------------------------------------------------
@@ -370,10 +405,11 @@ function buildGraph(): void {
// --- Nodes ---
nodeSelection = nodeGroup
- .selectAll("circle")
+ .selectAll("path.node-shape")
.data(nodes, (d) => d.qualifiedName)
- .join("circle")
- .attr("r", (d) => NODE_RADIUS[d.kind] ?? 10)
+ .join("path")
+ .attr("class", "node-shape")
+ .attr("d", (d) => d3.symbol().type(NODE_SHAPE[d.kind] ?? d3.symbolCircle).size(NODE_AREA[d.kind] ?? 314)()!)
.attr("fill", (d) => NODE_COLOR[d.kind] ?? "#cdd6f4")
.attr("stroke", "none")
.attr("stroke-width", 2)
@@ -408,7 +444,7 @@ function buildGraph(): void {
})
.call(
d3
- .drag()
+ .drag()
.on("start", (event, d) => {
if (!event.active) simulation?.alphaTarget(0.3).restart();
d.fx = d.x;
@@ -423,7 +459,61 @@ function buildGraph(): void {
d.fx = null;
d.fy = null;
})
- );
+ )
+ .attr("tabindex", 0)
+ .attr("role", "button")
+ .attr("aria-label", (d) => `${d.kind}: ${d.name}`)
+ .on("keydown", (event: KeyboardEvent, d: SimNode) => {
+ if (event.key === "Enter" || event.key === " ") {
+ event.preventDefault();
+ selectNode(d);
+ vscodeApi.postMessage({
+ command: "nodeClicked",
+ qualifiedName: d.qualifiedName,
+ filePath: d.filePath,
+ lineStart: d.lineStart ?? 1,
+ });
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ selectedNode = null;
+ unhighlightAll();
+ nodeSelection.attr("stroke", "none");
+ } else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) {
+ event.preventDefault();
+ const visibleNodes = nodeSelection.data();
+ let best: SimNode | null = null;
+ let bestDist = Infinity;
+ for (const n of visibleNodes) {
+ if (n.qualifiedName === d.qualifiedName || n.x == null || n.y == null || d.x == null || d.y == null) continue;
+ const dx = n.x - d.x;
+ const dy = n.y - d.y;
+ const dist = Math.sqrt(dx * dx + dy * dy);
+ let ok = false;
+ if (event.key === "ArrowRight" && dx > 0 && Math.abs(dy) < Math.abs(dx)) ok = true;
+ if (event.key === "ArrowLeft" && dx < 0 && Math.abs(dy) < Math.abs(dx)) ok = true;
+ if (event.key === "ArrowDown" && dy > 0 && Math.abs(dx) < Math.abs(dy)) ok = true;
+ if (event.key === "ArrowUp" && dy < 0 && Math.abs(dx) < Math.abs(dy)) ok = true;
+ if (ok && dist < bestDist) {
+ best = n;
+ bestDist = dist;
+ }
+ }
+ if (best) {
+ const target = nodeGroup.selectAll("path.node-shape")
+ .filter((n) => n.qualifiedName === best!.qualifiedName)
+ .node();
+ if (target) (target as HTMLElement).focus();
+ }
+ }
+ })
+ .on("focus", (_event: FocusEvent, d: SimNode) => {
+ showTooltip(d);
+ highlightConnected(d);
+ })
+ .on("blur", () => {
+ hideTooltip();
+ unhighlightAll();
+ });
// Highlight search matches
const searchInput = document.getElementById("search-input") as HTMLInputElement | null;
@@ -433,19 +523,19 @@ function buildGraph(): void {
const matches =
d.name.toLowerCase().includes(query) ||
d.qualifiedName.toLowerCase().includes(query);
- return matches ? "#f5e0dc" : "none";
+ return matches ? "#e6edf3" : "none";
});
}
// Highlight selected node
if (selectedNode) {
nodeSelection.attr("stroke", (d) => {
- if (d.qualifiedName === selectedNode!.qualifiedName) return "#f5e0dc";
+ if (d.qualifiedName === selectedNode!.qualifiedName) return "#e6edf3";
if (query.length > 0) {
const matches =
d.name.toLowerCase().includes(query) ||
d.qualifiedName.toLowerCase().includes(query);
- return matches ? "#f5e0dc" : "none";
+ return matches ? "#e6edf3" : "none";
}
return "none";
});
@@ -487,7 +577,7 @@ function buildGraph(): void {
.attr("x2", (d) => (d.target as SimNode).x!)
.attr("y2", (d) => (d.target as SimNode).y!);
- nodeSelection.attr("cx", (d) => d.x!).attr("cy", (d) => d.y!);
+ nodeSelection.attr("transform", (d) => `translate(${d.x},${d.y})`);
labelSelection.attr("x", (d) => d.x!).attr("y", (d) => d.y!);
});
@@ -506,8 +596,22 @@ function buildGraph(): void {
function selectNode(node: SimNode): void {
selectedNode = node;
nodeSelection.attr("stroke", (d) =>
- d.qualifiedName === node.qualifiedName ? "#f5e0dc" : "none"
+ d.qualifiedName === node.qualifiedName ? "#e6edf3" : "none"
);
+ updateDepthSliderState();
+}
+
+function updateDepthSliderState(): void {
+ const slider = document.getElementById("depth-slider") as HTMLInputElement | null;
+ const depthValue = document.getElementById("depth-value");
+ if (slider) {
+ if (selectedNode) {
+ slider.disabled = false;
+ } else {
+ slider.disabled = true;
+ if (depthValue) depthValue.textContent = "N/A";
+ }
+ }
}
function highlightConnected(node: SimNode): void {
@@ -613,7 +717,7 @@ function highlightNodeByName(qualifiedName: string): void {
.attr("cy", node.y ?? 0)
.attr("r", (NODE_RADIUS[node.kind] ?? 10) + 4)
.attr("fill", "none")
- .attr("stroke", "#f5e0dc")
+ .attr("stroke", "#e6edf3")
.attr("stroke-width", 3)
.attr("class", "pulse-ring");
@@ -636,7 +740,7 @@ function highlightNodeByName(qualifiedName: string): void {
.attr("cy", node.y ?? 0)
.attr("r", (NODE_RADIUS[node.kind] ?? 10) + 4)
.attr("fill", "none")
- .attr("stroke", "#f5e0dc")
+ .attr("stroke", "#e6edf3")
.attr("stroke-width", 3);
ring2
@@ -734,19 +838,40 @@ function bindToolbarEvents(): void {
for (const kind of ALL_EDGE_KINDS) {
const pill = document.getElementById(`edge-${kind}`);
if (pill) {
- pill.addEventListener("click", () => {
+ const toggle = () => {
if (visibleEdgeKinds.has(kind)) {
visibleEdgeKinds.delete(kind);
pill.classList.remove("active");
+ pill.setAttribute("aria-pressed", "false");
} else {
visibleEdgeKinds.add(kind);
pill.classList.add("active");
+ pill.setAttribute("aria-pressed", "true");
}
buildGraph();
+ };
+ pill.addEventListener("click", toggle);
+ pill.addEventListener("keydown", (ev) => {
+ if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggle(); }
});
}
}
+ // Edge filter popover toggle
+ const edgeFilterBtn = document.getElementById("btn-edge-filter");
+ const edgePopover = document.getElementById("edge-popover");
+ if (edgeFilterBtn && edgePopover) {
+ edgeFilterBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ edgePopover.classList.toggle("visible");
+ });
+ document.addEventListener("click", (e) => {
+ if (!edgePopover.contains(e.target as Node) && e.target !== edgeFilterBtn) {
+ edgePopover.classList.remove("visible");
+ }
+ });
+ }
+
// Depth slider
const depthSlider = document.getElementById("depth-slider") as HTMLInputElement | null;
if (depthSlider) {
diff --git a/code_review_graph/visualization.py b/code_review_graph/visualization.py
index 95f592b..289b4d6 100644
--- a/code_review_graph/visualization.py
+++ b/code_review_graph/visualization.py
@@ -208,24 +208,24 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
}
#legend h3 {
font-size: 11px; font-weight: 700; margin-bottom: 6px;
- color: #8b949e; text-transform: uppercase; letter-spacing: 1px;
+ color: #9eaab6; text-transform: uppercase; letter-spacing: 1px;
}
.legend-section { margin-bottom: 10px; }
.legend-section:last-child { margin-bottom: 0; }
.legend-item { display: flex; align-items: center; gap: 10px; padding: 2px 0; cursor: default; }
.legend-item[data-edge-kind] { cursor: pointer; user-select: none; }
.legend-item[data-edge-kind].dimmed { opacity: 0.3; }
- .legend-circle { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
+ .legend-item svg { flex-shrink: 0; }
.legend-line { width: 24px; height: 0; flex-shrink: 0; border-top-width: 2px; }
.l-calls { border-top: 2px solid #3fb950; }
.l-imports { border-top: 2px dashed #f0883e; }
.l-inherits { border-top: 2.5px dotted #d2a8ff; }
- .l-contains { border-top: 1.5px solid rgba(139,148,158,0.3); }
+ .l-contains { border-top: 1px solid rgba(139,148,158,0.3); }
#stats-bar {
position: absolute; bottom: 0; left: 0; right: 0;
background: rgba(13,17,23,0.95); border-top: 1px solid #21262d;
padding: 8px 24px; display: flex; gap: 32px; justify-content: center;
- font-size: 12px; color: #8b949e; backdrop-filter: blur(12px);
+ font-size: 12px; color: #9eaab6; backdrop-filter: blur(12px);
}
.stat-item { display: flex; gap: 6px; align-items: center; }
.stat-value { color: #e6edf3; font-weight: 600; }
@@ -247,7 +247,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
text-transform: uppercase; letter-spacing: 0.5px;
}
.tt-row { margin-top: 4px; }
- .tt-label { color: #8b949e; }
+ .tt-label { color: #9eaab6; }
.tt-file { color: #58a6ff; font-size: 11px; }
#controls {
position: absolute; top: 16px; right: 16px;
@@ -271,7 +271,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
outline: none; backdrop-filter: blur(12px);
}
#search:focus { border-color: #58a6ff; }
- #search::placeholder { color: #484f58; }
+ #search::placeholder { color: #6e7681; }
#search-results {
position: absolute; top: 52px; right: 16px;
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
@@ -287,28 +287,31 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
.sr-item:last-child { border-bottom: none; }
.sr-kind { font-size: 9px; padding: 2px 6px; border-radius: 8px; text-transform: uppercase; font-weight: 700; }
#detail-panel {
- position: absolute; top: 16px; right: 16px;
+ position: absolute; top: 16px; left: 16px;
width: 320px; max-height: calc(100vh - 80px);
background: rgba(22,27,34,0.97); border: 1px solid #30363d;
border-radius: 10px; padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
- backdrop-filter: blur(12px); z-index: 20;
+ backdrop-filter: blur(12px); z-index: 15;
overflow-y: auto; display: none; font-size: 12px;
}
#detail-panel.visible { display: block; }
#detail-panel h2 { font-size: 16px; color: #e6edf3; margin-bottom: 4px; word-break: break-all; }
#detail-panel .dp-close {
position: absolute; top: 12px; right: 14px;
- cursor: pointer; color: #8b949e; font-size: 18px; line-height: 1;
- border: none; background: none;
+ cursor: pointer; color: #8b949e; font-size: 14px; line-height: 1;
+ border: 1px solid #30363d; background: rgba(22,27,34,0.95);
+ border-radius: 6px; width: 28px; height: 28px;
+ display: flex; align-items: center; justify-content: center;
+ transition: all 0.15s;
}
- #detail-panel .dp-close:hover { color: #e6edf3; }
+ #detail-panel .dp-close:hover { color: #e6edf3; border-color: #8b949e; background: #30363d; }
.dp-section { margin-top: 14px; }
- .dp-section h4 { color: #8b949e; font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
+ .dp-section h4 { color: #9eaab6; font-size: 10px; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
.dp-list { list-style: none; }
.dp-list li { padding: 3px 0; color: #c9d1d9; cursor: pointer; }
.dp-list li:hover { color: #58a6ff; text-decoration: underline; }
- .dp-meta { color: #8b949e; }
+ .dp-meta { color: #9eaab6; }
.dp-meta span { color: #e6edf3; font-weight: 600; }
#filter-panel {
position: absolute; bottom: 50px; left: 16px;
@@ -319,31 +322,99 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
}
#filter-panel h3 {
font-size: 11px; font-weight: 700; margin-bottom: 8px;
- color: #8b949e; text-transform: uppercase; letter-spacing: 1px;
+ color: #9eaab6; text-transform: uppercase; letter-spacing: 1px;
}
.filter-item { display: flex; align-items: center; gap: 8px; padding: 3px 0; cursor: pointer; user-select: none; }
.filter-item input { accent-color: #58a6ff; cursor: pointer; }
+ :focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
+ .filter-item input:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
+ .dp-close:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; }
+ .filter-item:focus-within { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 4px; }
+ #help-overlay {
+ position: fixed; inset: 0; background: rgba(0,0,0,0.6);
+ display: flex; align-items: center; justify-content: center; z-index: 100;
+ backdrop-filter: blur(4px);
+ }
+ #help-overlay.hidden { display: none; }
+ .help-content {
+ position: relative; background: #161b22; border: 1px solid #30363d;
+ border-radius: 12px; padding: 28px 32px; max-width: 420px; width: 90%;
+ box-shadow: 0 16px 48px rgba(0,0,0,0.5);
+ }
+ .help-content h2 { font-size: 16px; color: #e6edf3; margin-bottom: 16px; }
+ .help-content .help-close { position: absolute; top: 12px; right: 14px; }
+ .help-content table { width: 100%; border-collapse: collapse; }
+ .help-content td { padding: 6px 8px; color: #c9d1d9; font-size: 13px; border-bottom: 1px solid #21262d; }
+ .help-content td:first-child { white-space: nowrap; width: 1%; }
+ kbd {
+ display: inline-block; background: #21262d; border: 1px solid #30363d;
+ border-radius: 5px; padding: 2px 7px; font-size: 11px; font-family: inherit;
+ color: #e6edf3; box-shadow: inset 0 -1px 0 #0d1117; line-height: 1.6;
+ }
+ .help-dismiss {
+ margin-top: 16px; display: block; text-align: center;
+ color: #8b949e; font-size: 12px; cursor: pointer;
+ }
+ .help-dismiss:hover { color: #e6edf3; }
+ #loading-overlay {
+ position: fixed; inset: 0; display: flex; align-items: center;
+ justify-content: center; z-index: 50; background: rgba(13,17,23,0.85);
+ backdrop-filter: blur(6px); transition: opacity 0.4s ease;
+ }
+ #loading-overlay.hidden { opacity: 0; pointer-events: none; }
+ .loading-spinner {
+ width: 36px; height: 36px; border: 3px solid #30363d;
+ border-top-color: #58a6ff; border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ }
+ @keyframes spin { to { transform: rotate(360deg); } }
+ .loading-text { color: #9eaab6; font-size: 13px; margin-top: 14px; text-align: center; }
+ #empty-state {
+ position: fixed; inset: 0; display: none; align-items: center;
+ justify-content: center; z-index: 50; flex-direction: column; gap: 12px;
+ }
+ #empty-state.visible { display: flex; }
+ .empty-icon { font-size: 48px; opacity: 0.4; }
+ .empty-title { color: #e6edf3; font-size: 18px; font-weight: 600; }
+ .empty-desc { color: #9eaab6; font-size: 13px; max-width: 320px; text-align: center; line-height: 1.6; }
marker { overflow: visible; }
+ g.node-g:focus { outline: none; }
+ g.node-g:focus-visible .node-shape { stroke: #58a6ff !important; stroke-width: 3 !important; }
+ g.node-g:focus-visible .glow-ring { stroke: #58a6ff !important; opacity: 0.6 !important; }
+ button.legend-edge { background: none; border: none; color: #c9d1d9; font-size: 12px; font-family: inherit; }
+ button.legend-edge:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 4px; }
+ .sr-item.sr-active { background: #30363d; }
+ .skip-link {
+ position: absolute; top: -40px; left: 16px; z-index: 100;
+ background: #1f6feb; color: #fff; padding: 8px 16px;
+ border-radius: 0 0 8px 8px; text-decoration: none; font-weight: 600;
+ transition: top 0.2s;
+ }
+ .skip-link:focus { top: 0; }
-
+
Skip to graph
+
+
Filter by Kind
@@ -353,23 +424,63 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path:
-
+
-
+
+
-
-
+
+
-
-
+
+
+
+
Graph Interactions
+
+
+ | Click a file | Expand/collapse contained symbols |
+ | Click symbol | Show detail panel with callers/callees |
+ | Shift+click file | Show detail panel without toggling collapse |
+ | Hover | Highlight connected nodes and edges |
+ | Drag | Pin a node in place |
+ | Scroll | Zoom in/out |
+ | Click+drag background | Pan the view |
+ | Search | Type to filter — matching nodes stay bright |
+ | Legend edges | Click edge types in the legend to toggle visibility |
+
+
Keyboard Shortcuts
+
+ | / | Focus search |
+ | ? | Toggle this help |
+ | Esc | Close panel / search / help |
+ | Enter / Space | Activate focused node |
+ | Arrow keys | Navigate between nodes |
+
+
Click anywhere outside to dismiss
+
+
+
+
+
🔍
+
No nodes to display
+
The graph is empty. Run code-review-graph build to index your codebase, then regenerate the visualization.
+
+