Skip to content

Commit 211bbb9

Browse files
committed
Implement instant click glow effects and type-specific hover highlights
- Add immediate glow effect on node click without requiring mouse movement - Implement type-specific hover effects for all 9 work item types using correct colors - Keep hover effects (white border) separate from click effects (type-colored glow) - Fix edge hover effects to use white borders consistently - Remove conflicting JavaScript hover handlers that interfered with glow effects - Combine hover highlights with instant click feedback for better UX
1 parent 3e7d935 commit 211bbb9

2 files changed

Lines changed: 113 additions & 5 deletions

File tree

packages/web/src/components/InteractiveGraphVisualization.tsx

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,63 @@ export function InteractiveGraphVisualization() {
242242
}
243243
}, [editingEdge]);
244244

245+
// Helper function to apply glow effect immediately when node is clicked
246+
const applyNodeGlowImmediately = useCallback((node: WorkItem) => {
247+
if (!svgRef.current) return;
248+
249+
const svg = d3.select(svgRef.current);
250+
const defs = svg.select('defs');
251+
252+
const nodeTypeConfig = getTypeConfig(node.type as WorkItemType);
253+
const nodeColor = nodeTypeConfig.hexColor;
254+
const filterId = `node-glow-${node.type.toLowerCase()}`;
255+
256+
// Remove existing filter and create new one with node's type color
257+
defs.select(`#${filterId}`).remove();
258+
259+
const nodeGlowFilter = defs.append('filter')
260+
.attr('id', filterId)
261+
.attr('x', '-100%')
262+
.attr('y', '-100%')
263+
.attr('width', '300%')
264+
.attr('height', '300%');
265+
266+
// Convert hex to RGB values for feColorMatrix
267+
const hexToRgb = (hex: string) => {
268+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
269+
return result ? {
270+
r: parseInt(result[1], 16) / 255,
271+
g: parseInt(result[2], 16) / 255,
272+
b: parseInt(result[3], 16) / 255
273+
} : { r: 0.06, g: 0.73, b: 0.51 }; // fallback green
274+
};
275+
276+
const rgb = hexToRgb(nodeColor);
277+
nodeGlowFilter.append('feColorMatrix')
278+
.attr('in', 'SourceGraphic')
279+
.attr('type', 'matrix')
280+
.attr('values', `0 0 0 0 ${rgb.r} 0 0 0 0 ${rgb.g} 0 0 0 0 ${rgb.b} 0 0 0 1 0`);
281+
282+
const blur = nodeGlowFilter.append('feGaussianBlur')
283+
.attr('stdDeviation', '15')
284+
.attr('result', 'coloredBlur');
285+
286+
blur.append('animate')
287+
.attr('attributeName', 'stdDeviation')
288+
.attr('values', '10;20;10')
289+
.attr('dur', '2s')
290+
.attr('repeatCount', 'indefinite');
291+
292+
const feMerge = nodeGlowFilter.append('feMerge');
293+
feMerge.append('feMergeNode').attr('in', 'coloredBlur');
294+
feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
295+
296+
// Apply the type-specific glow filter immediately
297+
svg.selectAll('.node-bg')
298+
.filter((d: any) => d && d.id === node.id)
299+
.style('filter', `url(#${filterId})`);
300+
}, []);
301+
245302
// Apply glow effect to active dialog elements after D3 renders
246303
useEffect(() => {
247304
if (!svgRef.current) return;
@@ -408,6 +465,7 @@ export function InteractiveGraphVisualization() {
408465
.style('filter', `url(#${edgeFilterId})`)
409466
.attr('stroke-width', 12); // Same thickness as relationship editing
410467
}
468+
411469
}, [nodeMenu.visible, nodeMenu.node?.id, editingEdge?.edge?.id, showEdgeDetails, selectedEdge?.id]);
412470

413471
// Level of detail thresholds
@@ -706,6 +764,9 @@ export function InteractiveGraphVisualization() {
706764
// Set selected node for the Node Actions panel
707765
setSelectedNode(node);
708766

767+
// Apply glow effect immediately without waiting for useEffect
768+
applyNodeGlowImmediately(node);
769+
709770
// Show node menu
710771
const containerRect = containerRef.current?.getBoundingClientRect();
711772
if (containerRect) {
@@ -1235,6 +1296,35 @@ export function InteractiveGraphVisualization() {
12351296
return 'url(#dialog-glow)';
12361297
}
12371298
return null;
1299+
})
1300+
.on('mouseenter', function(event: MouseEvent, d: WorkItemEdge) {
1301+
// Skip hover effect if edge has active dialog (glow is already applied)
1302+
if ((editingEdge && editingEdge.edge && editingEdge.edge.id === d.id) ||
1303+
(showEdgeDetails && selectedEdge && selectedEdge.id === d.id)) {
1304+
return;
1305+
}
1306+
1307+
// Add white border hover effect to edges
1308+
d3.select(this)
1309+
.style('stroke', '#ffffff')
1310+
.style('stroke-width', '4')
1311+
.style('stroke-opacity', '1')
1312+
.style('filter', 'drop-shadow(0 0 4px #ffffff)');
1313+
})
1314+
.on('mouseleave', function(event: MouseEvent, d: WorkItemEdge) {
1315+
// Skip hover reset if edge has active dialog (glow should remain)
1316+
if ((editingEdge && editingEdge.edge && editingEdge.edge.id === d.id) ||
1317+
(showEdgeDetails && selectedEdge && selectedEdge.id === d.id)) {
1318+
return;
1319+
}
1320+
1321+
// Restore original edge stroke
1322+
const config = getRelationshipConfig(d.type as RelationshipType);
1323+
d3.select(this)
1324+
.style('stroke', config.hexColor)
1325+
.style('stroke-width', (d.strength || 0.8) * 3)
1326+
.style('stroke-opacity', '0.7')
1327+
.style('filter', null);
12381328
});
12391329

12401330
// Create invisible thicker clickable areas for easier interaction
@@ -1318,7 +1408,7 @@ export function InteractiveGraphVisualization() {
13181408
.data(visibleNodes)
13191409
.enter()
13201410
.append('g')
1321-
.attr('class', 'node')
1411+
.attr('class', (d: WorkItem) => `node node-type-${d.type.toLowerCase()}`)
13221412
.style('cursor', 'pointer')
13231413
.call(d3.drag<any, any>()
13241414
.on('start', (event, d: any) => {

packages/web/src/index.css

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,10 +148,28 @@ svg .dialog-active-pulse,
148148
animation: pulse-green 1.0s ease-in-out infinite !important;
149149
}
150150

151-
.node:hover circle {
152-
filter: drop-shadow(0 0 6px currentColor);
153-
stroke-width: 2.5;
154-
transform: scale(1.05);
151+
/* Type-specific node hover effects - light highlight border only */
152+
.node-type-epic:hover rect,
153+
.node-type-milestone:hover rect,
154+
.node-type-outcome:hover rect,
155+
.node-type-feature:hover rect,
156+
.node-type-task:hover rect,
157+
.node-type-bug:hover rect,
158+
.node-type-idea:hover rect,
159+
.node-type-research:hover rect,
160+
.node-type-default:hover rect {
161+
stroke: #ffffff !important;
162+
stroke-width: 2 !important;
163+
stroke-opacity: 0.7 !important;
164+
transform: scale(1.02);
165+
transition: all 0.2s ease-in-out;
166+
}
167+
168+
.edge:hover {
169+
stroke: #ffffff !important;
170+
stroke-width: 4;
171+
opacity: 1;
172+
filter: drop-shadow(0 0 4px #ffffff);
155173
transition: all 0.2s ease-in-out;
156174
}
157175

0 commit comments

Comments
 (0)