Private/UI/Render-PSMBMenuBox.ps1

function Render-PSMBMenuBox {
    <#
    .SYNOPSIS
        Renders menu items inside a rounded-corner box with in-place redraw.
    .DESCRIPTION
        Draws menu items with highlight indicator inside a Unicode box.
        Uses Console.SetCursorPosition for flicker-free in-place updates.
        Supports viewport scrolling for long lists.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string[]]$Items,

        [Parameter(Mandatory)]
        [int]$SelectedIndex,

        [Parameter(Mandatory)]
        [int]$AnchorTop,

        [Parameter()]
        [switch]$IncludeBack,

        [Parameter()]
        [switch]$IncludeQuit,

        [Parameter()]
        [int]$ViewportOffset = 0,

        [Parameter()]
        [int]$ViewportSize = 0
    )

    $palette = Get-PSMBColorPalette
    $esc = [char]27
    $reset = "${esc}[0m"

    $innerWidth = Get-PSMBConsoleInnerWidth
    $border = ([char]0x2502)
    $hLine = ([char]0x2500)

    $amberRGB = @(168, 213, 162)
    $roseRGB = @(94, 234, 212)
    $fitText = {
        param([string]$Text)
        if ($null -eq $Text) { return '' }
        if ($Text.Length -le $innerWidth) { return $Text }
        if ($innerWidth -le 3) { return $Text.Substring(0, $innerWidth) }
        return $Text.Substring(0, $innerWidth - 3) + '...'
    }

    $bufferHeight = [Console]::BufferHeight

    # Determine viewport boundaries
    $useViewport = ($ViewportSize -gt 0) -and ($ViewportSize -lt $Items.Count)
    if ($useViewport) {
        $showAbove = ($ViewportOffset -gt 0)
        $showBelow = (($ViewportOffset + $ViewportSize) -lt $Items.Count)
        $slotCount = $ViewportSize
    }
    else {
        $showAbove = $false
        $showBelow = $false
        $slotCount = $Items.Count
    }

    $row = $AnchorTop

    # Empty line inside box
    $emptyLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, $border, (' ' * $innerWidth), $border, $reset
    if ($row -lt $bufferHeight) {
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($emptyLine)
    }
    $row++

    # Render item slots
    for ($slot = 0; $slot -lt $slotCount; $slot++) {
        if ($row -ge $bufferHeight) { break }
        [Console]::SetCursorPosition(0, $row)

        # Scroll-up indicator
        if ($useViewport -and $slot -eq 0 -and $showAbove) {
            $aboveCount = $ViewportOffset
            $indicatorText = (' {0} {1} more above' -f ([char]0x25B4), $aboveCount)
            $indicatorPadded = (& $fitText $indicatorText).PadRight($innerWidth)
            $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
            [Console]::Write($line)
            $row++
            continue
        }

        # Scroll-down indicator
        if ($useViewport -and $slot -eq ($slotCount - 1) -and $showBelow) {
            $belowCount = $Items.Count - ($ViewportOffset + $ViewportSize)
            $indicatorText = (' {0} {1} more below' -f ([char]0x25BE), $belowCount)
            $indicatorPadded = (& $fitText $indicatorText).PadRight($innerWidth)
            $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $indicatorPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
            [Console]::Write($line)
            $row++
            continue
        }

        # Map slot to actual item index
        $i = if ($useViewport) { $ViewportOffset + $slot } else { $slot }
        $num = $i + 1
        $isHighlighted = ($i -eq $SelectedIndex)

        if ($isHighlighted) {
            $chevron = [char]0x276F
            $itemText = (' {0} {1} {2} {3}' -f $chevron, $num, ([char]0x25B8), $Items[$i])
            $padded = (& $fitText $itemText).PadRight($innerWidth)
            $line = ' {0}{1}{2}{3}{4}{5}{6}{7}' -f $palette.Surface, $border, $palette.BgSelect, $palette.Amber, $palette.Bold, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
        }
        else {
            $itemText = (' {0} {1} {2}' -f $num, ([char]0x25B8), $Items[$i])
            $padded = (& $fitText $itemText).PadRight($innerWidth)
            $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Text, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
        }

        [Console]::Write($line)
        $row++
    }

    # Empty line
    if ($row -lt $bufferHeight) {
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($emptyLine)
    }
    $row++

    # Separator line
    if ($row -lt $bufferHeight) {
        $sepGradient = Get-PSMBGradientLine -Character $hLine -Length $innerWidth -StartRGB $amberRGB -EndRGB $roseRGB
        $sepLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, ([char]0x251C), $sepGradient, ([char]0x2524), $reset
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($sepLine)
    }
    $row++

    # Hint line
    $hintText = ' Use arrow keys to navigate, Enter to select'
    if ($IncludeBack) { $hintText += ', Esc to go back' }
    elseif ($IncludeQuit) { $hintText += ', Esc to quit' }

    if ($row -lt $bufferHeight) {
        $hintPadded = $hintText.PadRight($innerWidth)
        $hintLine = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $palette.Dim, $hintPadded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($hintLine)
    }
    $row++

    # Bottom border
    if ($row -lt $bufferHeight) {
        $bottomGradient = Get-PSMBGradientLine -Character $hLine -Length $innerWidth -StartRGB $amberRGB -EndRGB $roseRGB
        $bottomLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, ([char]0x2570), $bottomGradient, ([char]0x256F), $reset
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($bottomLine)
    }
    $row++

    if ($row -lt $bufferHeight) {
        [Console]::SetCursorPosition(0, $row)
    }
}