Private/Interactive/Render-TBMenuBox.ps1

function Render-TBMenuBox {
    <#
    .SYNOPSIS
        Renders menu items inside a rounded-corner box with in-place redraw.
    .DESCRIPTION
        Draws menu items with optional highlight indicator and checkboxes
        inside a Unicode box. Uses Console.SetCursorPosition for flicker-free
        in-place updates. Supports both single-select and multi-select modes.
        When ViewportSize is set and the item list exceeds it, only a scrollable
        window of items is rendered with scroll indicators.
    .PARAMETER Items
        Array of menu item display strings.
    .PARAMETER SelectedIndex
        The currently highlighted item index.
    .PARAMETER AnchorTop
        The console row to start rendering from.
    .PARAMETER Checked
        Optional boolean array for multi-select checkbox state.
    .PARAMETER IncludeBack
        If set, shows 'Esc to go back' in the hint line.
    .PARAMETER IncludeQuit
        If set, shows 'Esc to quit' in the hint line.
    .PARAMETER MultiSelect
        If set, renders checkboxes and shows multi-select hints.
    .PARAMETER ViewportOffset
        First item index in the visible window. Defaults to 0.
    .PARAMETER ViewportSize
        Number of item slots in the viewport. 0 means show all items (no viewport).
    #>

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

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

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

        [Parameter()]
        [bool[]]$Checked,

        [Parameter()]
        [switch]$IncludeBack,

        [Parameter()]
        [switch]$IncludeQuit,

        [Parameter()]
        [switch]$MultiSelect,

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

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

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

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

    $blueRGB  = @(137, 180, 250)
    $mauveRGB = @(203, 166, 247)
    $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 in the first slot
        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 in the last slot
        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 ($MultiSelect -and $Checked) {
            if ($Checked[$i]) {
                $checkChar = [char]0x2611  # checked box
            }
            else {
                $checkChar = [char]0x2610  # unchecked box
            }

            if ($isHighlighted) {
                $chevron = [char]0x276F  # heavy chevron
                $itemText = (' {0} {1} {2} {3} {4}' -f $chevron, $checkChar, $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.Mauve, $palette.Bold, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
            }
            else {
                $checkColor = if ($Checked[$i]) { $palette.Green } else { $palette.Dim }
                $itemText = (' {0} {1} {2} {3}' -f $checkChar, $num, ([char]0x25B8), $Items[$i])
                $padded = (& $fitText $itemText).PadRight($innerWidth)
                $line = ' {0}{1}{2}{3}{4}{5}' -f $palette.Surface, $border, $checkColor, $padded, $reset, ('{0}{1}{2}' -f $palette.Surface, $border, $reset)
            }
        }
        else {
            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.Mauve, $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-TBGradientLine -Character $hLine -Length $innerWidth -StartRGB $blueRGB -EndRGB $mauveRGB
        $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
    if ($MultiSelect) {
        $hintText = ' Space to toggle, A for all, Enter to confirm'
    }
    else {
        $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-TBGradientLine -Character $hLine -Length $innerWidth -StartRGB $blueRGB -EndRGB $mauveRGB
        $bottomLine = ' {0}{1}{2}{3}{4}' -f $palette.Surface, ([char]0x2570), $bottomGradient, ([char]0x256F), $reset
        [Console]::SetCursorPosition(0, $row)
        [Console]::Write($bottomLine)
    }
    $row++

    # Move cursor below the box
    if ($row -lt $bufferHeight) {
        [Console]::SetCursorPosition(0, $row)
    }
}