diff --git a/client/src/components/DirectoryTreemap.tsx b/client/src/components/DirectoryTreemap.tsx index 8e57e62..0f8792d 100644 --- a/client/src/components/DirectoryTreemap.tsx +++ b/client/src/components/DirectoryTreemap.tsx @@ -140,6 +140,22 @@ export default function DirectoryTreemap({ data, onPathSelect, currentPath, owne return '#8b949e'; } + // Tooltip (created early so dirG and leafG can both reference it) + const tooltip = d3 + .select(container) + .append('div') + .style('position', 'absolute') + .style('background', '#161b22') + .style('border', '1px solid #30363d') + .style('border-radius', '6px') + .style('padding', '8px 12px') + .style('font-size', '0.8125rem') + .style('color', '#dfe2eb') + .style('pointer-events', 'none') + .style('opacity', '0') + .style('z-index', '10') + .style('max-width', '400px'); + // Render directory group headers (non-leaf nodes with depth 1+) const dirGroups = root.descendants().filter((d) => d.children && d.depth >= 1); @@ -166,6 +182,12 @@ export default function DirectoryTreemap({ data, onPathSelect, currentPath, owne .on('contextmenu', (event: MouseEvent, d) => { event.preventDefault(); window.open(`https://github.com/${owner}/${repo}/tree/HEAD/${d.data.path}`, '_blank'); + }) + .on('mouseover', function () { + d3.select(this).attr('stroke', '#58a6ff').attr('stroke-width', 1); + }) + .on('mouseout', function () { + d3.select(this).attr('stroke', '#30363d').attr('stroke-width', 1); }); // Directory header bar @@ -178,6 +200,14 @@ export default function DirectoryTreemap({ data, onPathSelect, currentPath, owne .style('cursor', 'pointer') .on('click', (_event, d) => { onPathSelect(d.data.path); + }) + .on('mouseover', function () { + const parentG = d3.select(this.parentNode as SVGGElement); + parentG.select('rect').attr('stroke', '#58a6ff').attr('stroke-width', 1); + }) + .on('mouseout', function () { + const parentG = d3.select(this.parentNode as SVGGElement); + parentG.select('rect').attr('stroke', '#30363d').attr('stroke-width', 1); }); // Square bottom corners of header (overlap with body) @@ -205,6 +235,23 @@ export default function DirectoryTreemap({ data, onPathSelect, currentPath, owne return name.length > maxChars ? name.slice(0, maxChars - 1) + 'โ€ฆ' : name; }); + // Directory tooltip on hover + dirG + .on('mousemove', (event: MouseEvent, d) => { + tooltip + .style('opacity', '1') + .style('left', `${Math.min(event.offsetX + 12, width - 300)}px`) + .style('top', `${event.offsetY - 8}px`) + .html( + `
๐Ÿ“ ${d.data.path}
` + + `
+${d.data.additions.toLocaleString()}
` + + `
-${d.data.deletions.toLocaleString()}
` + + `
ยฑ${(d.value ?? 0).toLocaleString()} total changes
` + + `
Click to drill down ยท Right-click โ†’ GitHub
` + ); + }) + .on('mouseleave', () => tooltip.style('opacity', '0')); + // Directory change count in header (right-aligned) dirG .filter((d) => (d.x1 - d.x0) > 100) @@ -291,22 +338,6 @@ export default function DirectoryTreemap({ data, onPathSelect, currentPath, owne .attr('pointer-events', 'none') .text((d) => `ยฑ${d.data.value.toLocaleString()}`); - // Tooltip - const tooltip = d3 - .select(container) - .append('div') - .style('position', 'absolute') - .style('background', '#161b22') - .style('border', '1px solid #30363d') - .style('border-radius', '6px') - .style('padding', '8px 12px') - .style('font-size', '0.8125rem') - .style('color', '#dfe2eb') - .style('pointer-events', 'none') - .style('opacity', '0') - .style('z-index', '10') - .style('max-width', '400px'); - leafG .on('mousemove', (event: MouseEvent, d) => { const icon = d.data.isDir ? '๐Ÿ“' : '๐Ÿ“„';