chore: auto-commit before merge (loop primary)

This commit is contained in:
2026-02-16 15:06:20 +00:00
parent aca57714e4
commit e9a7581aa5
20 changed files with 305 additions and 470 deletions
@@ -42,6 +42,7 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
const highlightedNodeIdRef = useRef<string | null>(highlightedNodeId ?? null)
const [dimensions, setDimensions] = useState({ width: 800, height: MIN_HEIGHT, scaleFactor: 1 })
const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null)
const [chartInView, setChartInView] = useState(true)
callbacksRef.current = { onRoleClick, onSkillClick, onNodeHover }
@@ -49,6 +50,18 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
highlightedNodeIdRef.current = highlightedNodeId ?? null
}, [highlightedNodeId])
// Track chart visibility for play/pause button
useEffect(() => {
const container = containerRef.current
if (!container) return
const observer = new IntersectionObserver(
([entry]) => setChartInView(entry.isIntersecting),
{ threshold: 0.1 },
)
observer.observe(container)
return () => observer.disconnect()
}, [])
useEffect(() => {
const container = containerRef.current
if (!container) return
@@ -235,6 +248,7 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
isPlaying={animation.isPlaying}
onToggle={animation.togglePlayPause}
isMobile={isMobile}
visible={chartInView}
/>
)}
@@ -16,39 +16,60 @@ export const ConstellationLegend: React.FC<ConstellationLegendProps> = ({ isTouc
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '12px',
padding: '6px 12px',
fontFamily: 'var(--font-geist-mono)',
fontSize: '10px',
color: 'var(--text-tertiary)',
lineHeight: '24px',
flexDirection: 'column',
gap: '2px',
padding: '8px 12px',
pointerEvents: 'none',
}}
>
{items.map((item, i) => (
<React.Fragment key={item.label}>
{i > 0 && (
<span style={{ color: 'var(--border)', userSelect: 'none' }} aria-hidden="true">·</span>
)}
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
<span
style={{
display: 'inline-block',
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: item.color,
flexShrink: 0,
}}
/>
{item.label}{domainCounts?.[item.domain] != null ? ` (${domainCounts[item.domain]})` : ''}
</span>
</React.Fragment>
))}
<span style={{ color: 'var(--border)', userSelect: 'none' }} aria-hidden="true">·</span>
<span style={{ opacity: 0.7 }}>{isTouch || supportsCoarsePointer ? 'Tap to explore connections' : 'Hover to explore connections'}</span>
<div
style={{
fontSize: '12px',
fontFamily: 'var(--font-ui)',
color: 'var(--text-secondary)',
opacity: 1,
}}
>
{isTouch || supportsCoarsePointer ? 'Tap to explore connections' : 'Hover to explore connections'}
</div>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '12px',
fontFamily: 'var(--font-geist-mono)',
fontSize: '12px',
color: 'var(--text-tertiary)',
lineHeight: '24px',
}}
>
{items.map((item, i) => (
<React.Fragment key={item.label}>
{i > 0 && (
<span style={{ color: 'var(--border)', userSelect: 'none' }} aria-hidden="true">·</span>
)}
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px' }}>
<span
style={{
display: 'inline-block',
width: '6px',
height: '6px',
borderRadius: '50%',
backgroundColor: item.color,
flexShrink: 0,
}}
/>
{item.label}{domainCounts?.[item.domain] != null ? ` (${domainCounts[item.domain]})` : ''}
</span>
</React.Fragment>
))}
</div>
</div>
)
}
@@ -4,9 +4,10 @@ interface PlayPauseButtonProps {
isPlaying: boolean
onToggle: () => void
isMobile: boolean
visible?: boolean
}
export const PlayPauseButton: React.FC<PlayPauseButtonProps> = ({ isPlaying, onToggle, isMobile }) => {
export const PlayPauseButton: React.FC<PlayPauseButtonProps> = ({ isPlaying, onToggle, isMobile, visible = true }) => {
const size = isMobile ? 44 : 36
const offset = isMobile ? 8 : 12
@@ -16,22 +17,25 @@ export const PlayPauseButton: React.FC<PlayPauseButtonProps> = ({ isPlaying, onT
aria-label={isPlaying ? 'Pause animation' : 'Play animation'}
style={{
position: 'absolute',
bottom: offset,
right: offset,
left: offset,
top: '50%',
transform: 'translateY(-50%)',
width: size,
height: size,
borderRadius: '50%',
border: '1px solid var(--border-light)',
border: '1.5px solid var(--border)',
background: 'var(--surface)',
boxShadow: '0 1px 4px rgba(26,43,42,0.10)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
opacity: 0.6,
opacity: visible ? 0.85 : 0,
pointerEvents: visible ? 'auto' : 'none',
transition: 'opacity 150ms ease',
}}
onMouseEnter={e => (e.currentTarget.style.opacity = '1')}
onMouseLeave={e => (e.currentTarget.style.opacity = '0.6')}
onMouseLeave={e => { if (visible) e.currentTarget.style.opacity = '0.85' }}
>
{isPlaying ? (
<svg width="14" height="14" viewBox="0 0 14 14" fill="var(--text-secondary)">
+2 -2
View File
@@ -20,8 +20,8 @@ export const LABEL_REST_OPACITY = 0.5
// Link visual params
export const LINK_BASE_WIDTH = 0.5
export const LINK_STRENGTH_WIDTH_FACTOR = 1.5
export const LINK_BASE_OPACITY = 0.08
export const LINK_STRENGTH_OPACITY_FACTOR = 0.12
export const LINK_BASE_OPACITY = 0.04
export const LINK_STRENGTH_OPACITY_FACTOR = 0.06
export const LINK_HIGHLIGHT_BASE_WIDTH = 1
export const LINK_HIGHLIGHT_STRENGTH_WIDTH_FACTOR = 2
export const LINK_BEZIER_VERTICAL_OFFSET = 0.15