From 8c014d84df0d58b200edeb6c43c14c5d4f09315d Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Tue, 31 Mar 2026 13:11:28 +0100 Subject: [PATCH 01/23] docs: add accessibility audit fixes design spec Covers all 23 WCAG 2.1 AA issues across standalone HTML visualization and VS Code webview: distinct node shapes, contrast fixes, keyboard navigation, ARIA roles, edge differentiation, and minor polish. Co-Authored-By: Claude Opus 4.6 (1M context) --- ...-03-31-accessibility-audit-fixes-design.md | 227 ++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-31-accessibility-audit-fixes-design.md diff --git a/docs/superpowers/specs/2026-03-31-accessibility-audit-fixes-design.md b/docs/superpowers/specs/2026-03-31-accessibility-audit-fixes-design.md new file mode 100644 index 0000000..2286c80 --- /dev/null +++ b/docs/superpowers/specs/2026-03-31-accessibility-audit-fixes-design.md @@ -0,0 +1,227 @@ +# Accessibility Audit Fixes — Design Spec + +**Standard:** WCAG 2.1 AA | **Date:** 2026-03-31 +**Scope:** Standalone HTML visualization (`visualization.py`), VS Code webview (`graph.ts`, `graphWebview.ts`) + +--- + +## Overview + +Fix all 23 accessibility issues identified in the WCAG 2.1 AA audit across the two frontend surfaces. The VS Code tree views are already accessible via native APIs — no changes needed there. + +All changes are direct in-place edits. The two surfaces (Python string template vs TypeScript) cannot share code, so fixes are applied independently to each. + +--- + +## 1. Distinct Node Shapes (Critical — Issues #1, #19) + +**Problem:** All nodes are circles, differentiated only by color. Colorblind users (~8% of males) cannot distinguish node types. + +**Fix:** Use `d3.symbol()` to render distinct shapes per node kind: + +| Kind | Shape | D3 Symbol Type | +|----------|-----------------|------------------------| +| File | Circle | `d3.symbolCircle` | +| Class | Square | `d3.symbolSquare` | +| Function | Triangle-up | `d3.symbolTriangle` | +| Test | Diamond | `d3.symbolDiamond` | +| Type | Cross/plus | `d3.symbolCross` | + +### Standalone (`visualization.py`) + +- Replace the `` append in `updateNodes` (lines 536-547) with `` using `d3.symbol().type(KIND_SHAPE[d.kind]).size(area)` +- Size the symbol area proportional to `KIND_RADIUS` squared (to match current visual sizes) +- Keep the File glow-ring as a `` underneath the shape path +- Update `highlightConnected()` and any other code that selects `circle` elements to select `path.node-shape` instead +- Add a `KIND_SHAPE` mapping alongside `KIND_COLOR` +- Update legend dots to use matching mini SVG shapes instead of colored circles + +### VS Code (`graph.ts`) + +- Same approach: replace `` in node rendering (lines 371-380) with `` using `d3.symbol()` +- Update `NODE_RADIUS` usage to symbol area calculations +- Update any `circle` CSS selectors or D3 selections to `path.node-shape` + +--- + +## 2. Color Contrast Fixes (Major — Issues #4, #5, #6, #20) + +**Problem:** Text using `#8b949e` on dark backgrounds fails the 4.5:1 minimum contrast ratio. + +### Standalone (`visualization.py`) — Contrast fixes + +| Element | Current | New | Ratio | +|---------|---------|-----|-------| +| `.tt-label` (tooltip) | `#8b949e` | `#9eaab6` | ~4.8:1 | +| Stats bar label | `#8b949e` | `#9eaab6` | ~4.8:1 | +| `.dp-meta` (detail panel) | `#8b949e` | `#9eaab6` | ~4.8:1 | +| `.dp-close` | `#8b949e` | `#9eaab6` | ~4.8:1 | +| Search placeholder | `#484f58` | `#6e7681` | ~3.2:1 (placeholder exempt from 4.5:1 per WCAG, but improved) | + +Implementation: Find-and-replace `#8b949e` with `#9eaab6` in CSS sections where it's used as text color on dark backgrounds. Replace `#484f58` with `#6e7681` for placeholder text. + +### VS Code (`graphWebview.ts`) — Theme-compatible colors + +Replace hardcoded tooltip colors with VS Code CSS variables: +- `.tooltip-params` `#a6e3a1` → `var(--vscode-debugTokenExpression-string, #a6e3a1)` (fallback to current) +- `.tooltip-return` `#89b4fa` → `var(--vscode-debugTokenExpression-number, #89b4fa)` (fallback to current) + +This ensures contrast in all themes (light, dark, high contrast). + +--- + +## 3. Keyboard Navigation (Critical — Issues #7, #8, #12, #21) + +### Graph Nodes — Both surfaces + +Add keyboard interaction to the SVG graph nodes: + +1. **Tab into graph:** Add `tabindex="0"` to each node's `` element +2. **Enter/Space:** Trigger same action as click (select node, show detail panel) +3. **Arrow keys:** Move focus to the nearest neighbor node in that direction (use node x/y positions to determine "nearest up/down/left/right") +4. **Escape:** Deselect current node, close detail panel +5. **Focus indicator:** Add a visible focus ring (2px white outline) on `:focus-visible` for node shapes + +Implementation pattern (both surfaces): +```javascript +nodeG.attr("tabindex", 0) + .on("keydown", function(event, d) { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + // same as click handler + } else if (event.key === "Escape") { + // deselect, close detail panel + } else if (["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(event.key)) { + event.preventDefault(); + // find nearest node in direction, focus it + } + }); +``` + +### Legend Edge Toggles — Standalone (Issue #8) + +Change edge toggle items from `
` to `
@@ -369,7 +372,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: var graphData = __GRAPH_DATA__; var KIND_COLOR = { File:"#58a6ff", Class:"#f0883e", Function:"#3fb950", Test:"#d2a8ff", Type:"#8b949e" }; var KIND_RADIUS = { File:18, Class:12, Function:6, Test:6, Type:5 }; -var EDGE_COLOR = { CALLS:"#3fb950", IMPORTS_FROM:"#f0883e", INHERITS:"#d2a8ff", CONTAINS:"rgba(139,148,158,0.15)" }; +var EDGE_COLOR = { CALLS:"#3fb950", IMPORTS_FROM:"#f0883e", INHERITS:"#d2a8ff", CONTAINS:"rgba(139,148,158,0.15)", IMPLEMENTS:"#f9e2af", TESTED_BY:"#f38ba8", DEPENDS_ON:"#fab387" }; var communityColorScale = d3.scaleOrdinal(d3.schemeTableau10); var communityColoringOn = false; function escH(s) { return !s ? "" : s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'").replace(/`/g,"`"); } @@ -470,7 +473,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: var glow = defs.append("filter").attr("id","glow").attr("x","-50%").attr("y","-50%").attr("width","200%").attr("height","200%"); glow.append("feGaussianBlur").attr("stdDeviation","3").attr("result","blur"); glow.append("feComposite").attr("in","SourceGraphic").attr("in2","blur").attr("operator","over"); -[{id:"arrow-calls",color:"#3fb950"},{id:"arrow-imports",color:"#f0883e"},{id:"arrow-inherits",color:"#d2a8ff"}].forEach(function(mk) { +[{id:"arrow-calls",color:"#3fb950"},{id:"arrow-imports",color:"#f0883e"},{id:"arrow-inherits",color:"#d2a8ff"},{id:"arrow-implements",color:"#f9e2af"},{id:"arrow-tested_by",color:"#f38ba8"},{id:"arrow-depends_on",color:"#fab387"}].forEach(function(mk) { defs.append("marker").attr("id", mk.id) .attr("viewBox","0 -5 10 10").attr("refX",28).attr("refY",0) .attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto") @@ -494,6 +497,9 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: CALLS: { dash:null, width:1.5, opacity:0.7, marker:"url(#arrow-calls)" }, IMPORTS_FROM: { dash:"6,3", width:1.5, opacity:0.65, marker:"url(#arrow-imports)" }, INHERITS: { dash:"3,4", width:2, opacity:0.7, marker:"url(#arrow-inherits)" }, + IMPLEMENTS: { dash:"2,4", width:1.5, opacity:0.65, marker:"url(#arrow-implements)" }, + TESTED_BY: { dash:null, width:1.5, opacity:0.6, marker:"url(#arrow-tested_by)" }, + DEPENDS_ON: { dash:"8,3", width:1.5, opacity:0.6, marker:"url(#arrow-depends_on)" }, }; function eStyle(d) { return EDGE_CFG[d.kind] || {dash:null,width:1,opacity:0.3,marker:""}; } function eColor(d) { return EDGE_COLOR[d.kind] || "#484f58"; } @@ -561,7 +567,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: var lEnter = labelSel.enter().append("text").attr("class","node-label") .attr("text-anchor","start").attr("dy","0.35em") .text(function(d) { return d.label; }) - .attr("fill", function(d) { return d.kind === "File" ? "#e6edf3" : d.kind === "Class" ? "#f0883e" : "#8b949e"; }) + .attr("fill", function(d) { return d.kind === "File" ? "#e6edf3" : d.kind === "Class" ? "#f0883e" : "#9eaab6"; }) .attr("font-size", function(d) { return d.kind === "File" ? "12px" : d.kind === "Class" ? "11px" : "10px"; }) .attr("font-weight", function(d) { return d.kind === "File" ? 700 : d.kind === "Class" ? 600 : 400; }); labelSel = lEnter.merge(labelSel); From 9a698daed765a50f3488d6455f3c200d984565bc Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Tue, 31 Mar 2026 13:26:54 +0100 Subject: [PATCH 04/23] feat: add IMPLEMENTS, TESTED_BY, DEPENDS_ON edge types to standalone HTML Brings standalone HTML visualization to feature parity with the VS Code extension by adding 3 missing edge types: EDGE_COLOR entries, EDGE_CFG configurations, SVG arrow markers, and legend items for IMPLEMENTS, TESTED_BY, and DEPENDS_ON. Adds a regression test verifying all 7 edge types appear in generated HTML. Co-Authored-By: Claude Sonnet 4.6 --- tests/test_visualization.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 9c7872d..b11fd5b 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -218,6 +218,18 @@ def test_export_includes_communities(store_with_data): assert isinstance(data["communities"], list) +def test_generate_html_includes_all_edge_types(store_with_data, tmp_path): + """Generated HTML should define colors and legend entries for all 7 edge types.""" + from code_review_graph.visualization import generate_html + + output_path = tmp_path / "graph.html" + generate_html(store_with_data, output_path) + content = output_path.read_text() + for edge_kind in ["CALLS", "IMPORTS_FROM", "INHERITS", "CONTAINS", + "IMPLEMENTS", "TESTED_BY", "DEPENDS_ON"]: + assert edge_kind in content, f"Edge type {edge_kind} missing from HTML" + + def test_generate_html_includes_interactive_features(store_with_data, tmp_path): """Generated HTML should include new interactive features.""" from code_review_graph.visualization import generate_html From ec5c57dbc92f40e773b5a73028a55eac2879eefc Mon Sep 17 00:00:00 2001 From: Tirth Kanani Date: Tue, 31 Mar 2026 13:31:10 +0100 Subject: [PATCH 05/23] feat(a11y): use distinct d3.symbol shapes per node kind for colorblind users Co-Authored-By: Claude Opus 4.6 (1M context) --- code_review_graph/visualization.py | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/code_review_graph/visualization.py b/code_review_graph/visualization.py index 588460f..421e8b1 100644 --- a/code_review_graph/visualization.py +++ b/code_review_graph/visualization.py @@ -215,7 +215,7 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: .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; } @@ -330,11 +330,11 @@ def generate_html(store: GraphStore, output_path: str | Path) -> Path: