refactor: decompose CareerConstellation monolith into focused modules
Break 1102-line CareerConstellation.tsx into: - constellation/constants.ts: sizing, opacity, domain color tokens - constellation/types.ts: SimNode, SimLink, LayoutParams interfaces - hooks/useForceSimulation.ts: D3 simulation lifecycle - hooks/useConstellationHighlight.ts: highlight/dim logic - hooks/useConstellationInteraction.ts: mouse/touch/pin handlers - constellation/MobileAccordion.tsx: tap-to-expand role details - constellation/ConstellationLegend.tsx: domain legend - constellation/AccessibleNodeOverlay.tsx: keyboard navigation buttons - constellation/CareerConstellation.tsx: 288-line orchestrator All existing behaviour preserved. Quality gates pass.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
import { supportsCoarsePointer } from '@/components/constellation/constants'
|
||||
import type { SimNode, ConstellationCallbacks } from '@/components/constellation/types'
|
||||
|
||||
export function useConstellationInteraction(deps: {
|
||||
highlightGraphRef: React.MutableRefObject<((id: string | null) => void) | null>
|
||||
nodeSelectionRef: React.MutableRefObject<d3.Selection<SVGGElement, SimNode, SVGGElement, unknown> | null>
|
||||
svgRef: React.RefObject<SVGSVGElement | null>
|
||||
callbacksRef: React.MutableRefObject<ConstellationCallbacks>
|
||||
resolveGraphFallback: () => string | null
|
||||
resolveRoleFallback: () => string | null
|
||||
dimensionsTrigger: number
|
||||
}) {
|
||||
const [pinnedNodeId, setPinnedNodeId] = useState<string | null>(null)
|
||||
const pinnedNodeIdRef = useRef<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
pinnedNodeIdRef.current = pinnedNodeId
|
||||
}, [pinnedNodeId])
|
||||
|
||||
const bindEvents = useCallback(() => {
|
||||
const nodeSelection = deps.nodeSelectionRef.current
|
||||
const svgEl = deps.svgRef.current
|
||||
if (!nodeSelection || !svgEl) return
|
||||
|
||||
const svg = d3.select(svgEl)
|
||||
|
||||
svg.select('.bg-rect').on('click.interaction', () => {
|
||||
if (supportsCoarsePointer) {
|
||||
setPinnedNodeId(null)
|
||||
pinnedNodeIdRef.current = null
|
||||
deps.highlightGraphRef.current?.(null)
|
||||
deps.callbacksRef.current.onNodeHover?.(null)
|
||||
}
|
||||
})
|
||||
|
||||
nodeSelection.on('mouseenter.interaction', function(_event: MouseEvent, d: SimNode) {
|
||||
if (supportsCoarsePointer) return
|
||||
deps.highlightGraphRef.current?.(d.id)
|
||||
if (d.type === 'role') {
|
||||
deps.callbacksRef.current.onNodeHover?.(d.id)
|
||||
}
|
||||
})
|
||||
|
||||
nodeSelection.on('mouseleave.interaction', function() {
|
||||
if (supportsCoarsePointer) return
|
||||
deps.highlightGraphRef.current?.(deps.resolveGraphFallback())
|
||||
deps.callbacksRef.current.onNodeHover?.(deps.resolveRoleFallback())
|
||||
})
|
||||
|
||||
nodeSelection.on('click.interaction', function(_event: MouseEvent, d: SimNode) {
|
||||
if (supportsCoarsePointer) {
|
||||
if (pinnedNodeIdRef.current === d.id) {
|
||||
setPinnedNodeId(null)
|
||||
pinnedNodeIdRef.current = null
|
||||
deps.highlightGraphRef.current?.(null)
|
||||
deps.callbacksRef.current.onNodeHover?.(null)
|
||||
} else {
|
||||
setPinnedNodeId(d.id)
|
||||
pinnedNodeIdRef.current = d.id
|
||||
deps.highlightGraphRef.current?.(d.id)
|
||||
deps.callbacksRef.current.onNodeHover?.(d.type === 'role' ? d.id : deps.resolveRoleFallback())
|
||||
}
|
||||
}
|
||||
|
||||
if (d.type === 'role') {
|
||||
deps.callbacksRef.current.onRoleClick(d.id)
|
||||
} else {
|
||||
deps.callbacksRef.current.onSkillClick(d.id)
|
||||
}
|
||||
})
|
||||
}, [deps])
|
||||
|
||||
// Re-bind events whenever selections change (triggered by simulation re-creation)
|
||||
useEffect(() => {
|
||||
bindEvents()
|
||||
}, [deps.dimensionsTrigger, bindEvents])
|
||||
|
||||
return {
|
||||
pinnedNodeId,
|
||||
setPinnedNodeId,
|
||||
pinnedNodeIdRef,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user