@@ -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 ) => {
0 commit comments