SquadSkillLinkPopup.razor

Razor Panel for a tooltip-style popup that lists the skill combinations between two linked squad players, including skill badges, effect text, out-of-position warnings and bilingual labels. Each tick it positions and sizes itself next to an anchor rect in screen space.

Rough Code
@using Sandbox
@using Sandbox.UI
@using Socer
@using System
@inherits Panel

<root class="squad-link-popup localized-text @(Connection==null?"hidden":"")">
    @if(Connection is { } selected)
    {
        <div class="squad-link-tooltip" @ref="TipPanel">
            <div class="squad-link-tooltip-heading"><label>@T("Связка скиллов","Skill combination")</label>@if(Pinned){<button class="squad-link-close" onclick=@(()=>OnClose?.Invoke())>✕</button>}</div>
            <div class="squad-link-players"><label>@selected.First.Name</label><span>↔</span><label>@selected.Second.Name</label></div>
            @foreach(var pair in selected.Pairs)
            {
                <div class="squad-link-pair">
                    <div class="squad-link-pair-skills"><div><SkillBadgeView Skill="@pair.First.Skill" Grade="@pair.First.Grade" CssClass="squad-link-skill-icon" /><label>@UniqueSkills.Name(pair.First.Skill,GamePreferences.Russian)</label></div><span>+</span><div><SkillBadgeView Skill="@pair.Second.Skill" Grade="@pair.Second.Grade" CssClass="squad-link-skill-icon" /><label>@UniqueSkills.Name(pair.Second.Skill,GamePreferences.Russian)</label></div></div>
                    <label class="squad-link-effect">@pair.Effect(GamePreferences.Russian)</label>
                </div>
            }
            @if(Formation.Fit(selected.First,Career.Shape,selected.FirstSlot)<.7f || Formation.Fit(selected.Second,Career.Shape,selected.SecondSlot)<.7f){<label class="squad-link-position-note">@T("Один из игроков не на своей позиции — часть взаимодействий может не сработать.","One player is out of position — some combinations may not activate.")</label>}
            <label class="squad-link-condition">@T("Работает в подходящих эпизодах матча.","Works in suitable match situations.")</label>
        </div>
    }
</root>

@code {
    public SquadSkillConnection Connection {get;set;}
    public ClubCareer Career {get;set;}
    public bool Pinned {get;set;}
    public Action OnClose {get;set;}
    public Rect Anchor {get;set;}
    Panel TipPanel;
    Panel positionedPanel;
    float positionedLeft=float.NaN,positionedTop=float.NaN,positionedWidth=float.NaN,positionedMaxHeight=float.NaN;
    public bool TipHovered=>TipPanel?.HasHovered==true;
    static string T(string ru,string en)=>GamePreferences.Russian?ru:en;
    static Rect Bounds(Panel panel){var a=panel.PanelPositionToScreenPosition(Vector2.Zero);var b=panel.PanelPositionToScreenPosition(new Vector2(panel.Box.Rect.Width,panel.Box.Rect.Height));return new Rect(a.x,a.y,b.x-a.x,b.y-a.y);}
    public override void Tick()
    {
        base.Tick();if(TipPanel==null || Connection==null)return;
        var root=FindRootPanel();if(root==null)return;
        var bounds=Bounds(root);float scale=root.ScaleFromScreen;
        float w=bounds.Width*scale,h=bounds.Height*scale;
        float width=Math.Min(380,w-24),height=Math.Max(180,TipPanel.Box.Rect.Height*scale);
        float x=(Anchor.Right-bounds.Left)*scale+12;
        if(x+width>w-12)x=(Anchor.Left-bounds.Left)*scale-width-12;
        float y=(Anchor.Center.y-bounds.Top)*scale-30;
        x=MathF.Round(Math.Clamp(x,12,Math.Max(12,w-width-12))*4)/4;
        y=MathF.Round(Math.Clamp(y,12,Math.Max(12,h-height-12))*4)/4;
        width=MathF.Round(width*4)/4;float maxHeight=MathF.Round((h-24)*4)/4;
        bool panelChanged=!ReferenceEquals(positionedPanel,TipPanel);
        if(panelChanged || positionedLeft!=x){TipPanel.Style.Left=Length.Pixels(x);positionedLeft=x;}
        if(panelChanged || positionedTop!=y){TipPanel.Style.Top=Length.Pixels(y);positionedTop=y;}
        if(panelChanged || positionedWidth!=width){TipPanel.Style.Width=Length.Pixels(width);positionedWidth=width;}
        if(panelChanged || positionedMaxHeight!=maxHeight){TipPanel.Style.MaxHeight=Length.Pixels(maxHeight);positionedMaxHeight=maxHeight;}
        positionedPanel=TipPanel;
    }
    protected override int BuildHash()=>HashCode.Combine(Connection?.Key,Connection?.Pairs,Career?.FormationIndex,Pinned,GamePreferences.Russian);
}