138 lines
4.0 KiB
TypeScript
138 lines
4.0 KiB
TypeScript
'use client'
|
||
|
||
import { useCallback } from 'react'
|
||
import {
|
||
getSmoothStepPath,
|
||
EdgeLabelRenderer,
|
||
BaseEdge,
|
||
type EdgeProps,
|
||
} from '@xyflow/react'
|
||
import type { AllocationEdgeData } from '@/lib/types'
|
||
|
||
export default function AllocationEdge({
|
||
id,
|
||
sourceX,
|
||
sourceY,
|
||
targetX,
|
||
targetY,
|
||
sourcePosition,
|
||
targetPosition,
|
||
data,
|
||
style,
|
||
markerEnd,
|
||
}: EdgeProps) {
|
||
const edgeData = data as AllocationEdgeData | undefined
|
||
|
||
const [edgePath, labelX, labelY] = getSmoothStepPath({
|
||
sourceX,
|
||
sourceY,
|
||
targetX,
|
||
targetY,
|
||
sourcePosition,
|
||
targetPosition,
|
||
})
|
||
|
||
const allocation = edgeData?.allocation ?? 0
|
||
const color = edgeData?.color ?? '#94a3b8'
|
||
const hasSiblings = (edgeData?.siblingCount ?? 1) > 1
|
||
|
||
const handleAdjust = useCallback(
|
||
(delta: number) => {
|
||
if (!edgeData?.onAdjust || !edgeData.sourceId || !edgeData.targetId || !edgeData.edgeType) return
|
||
edgeData.onAdjust(edgeData.sourceId, edgeData.targetId, edgeData.edgeType, delta)
|
||
},
|
||
[edgeData]
|
||
)
|
||
|
||
return (
|
||
<>
|
||
<BaseEdge id={id} path={edgePath} style={style} markerEnd={markerEnd} />
|
||
<EdgeLabelRenderer>
|
||
<div
|
||
className="nodrag nopan"
|
||
style={{
|
||
position: 'absolute',
|
||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||
pointerEvents: 'all',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '2px',
|
||
}}
|
||
>
|
||
{hasSiblings && (
|
||
<button
|
||
onClick={() => handleAdjust(-5)}
|
||
disabled={allocation <= 5}
|
||
style={{
|
||
width: 18,
|
||
height: 18,
|
||
borderRadius: 4,
|
||
border: `1.5px solid ${color}`,
|
||
background: 'white',
|
||
color,
|
||
fontSize: 13,
|
||
fontWeight: 700,
|
||
lineHeight: '14px',
|
||
cursor: allocation <= 5 ? 'not-allowed' : 'pointer',
|
||
opacity: allocation <= 5 ? 0.4 : 0.85,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 0,
|
||
transition: 'opacity 0.15s',
|
||
}}
|
||
onMouseEnter={(e) => { if (allocation > 5) e.currentTarget.style.opacity = '1' }}
|
||
onMouseLeave={(e) => { e.currentTarget.style.opacity = allocation <= 5 ? '0.4' : '0.85' }}
|
||
>
|
||
−
|
||
</button>
|
||
)}
|
||
<span
|
||
style={{
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
color,
|
||
background: 'rgba(255,255,255,0.92)',
|
||
padding: '1px 4px',
|
||
borderRadius: 4,
|
||
minWidth: 32,
|
||
textAlign: 'center',
|
||
userSelect: 'none',
|
||
}}
|
||
>
|
||
{Math.round(allocation)}%
|
||
</span>
|
||
{hasSiblings && (
|
||
<button
|
||
onClick={() => handleAdjust(5)}
|
||
disabled={allocation >= 95}
|
||
style={{
|
||
width: 18,
|
||
height: 18,
|
||
borderRadius: 4,
|
||
border: `1.5px solid ${color}`,
|
||
background: 'white',
|
||
color,
|
||
fontSize: 13,
|
||
fontWeight: 700,
|
||
lineHeight: '14px',
|
||
cursor: allocation >= 95 ? 'not-allowed' : 'pointer',
|
||
opacity: allocation >= 95 ? 0.4 : 0.85,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 0,
|
||
transition: 'opacity 0.15s',
|
||
}}
|
||
onMouseEnter={(e) => { if (allocation < 95) e.currentTarget.style.opacity = '1' }}
|
||
onMouseLeave={(e) => { e.currentTarget.style.opacity = allocation >= 95 ? '0.4' : '0.85' }}
|
||
>
|
||
+
|
||
</button>
|
||
)}
|
||
</div>
|
||
</EdgeLabelRenderer>
|
||
</>
|
||
)
|
||
}
|