Public/View/New-ElmViewport.ps1

function New-ElmViewport {
    <#
    .SYNOPSIS
        Creates a scrollable text viewport view node.

    .DESCRIPTION
        Returns a Box view node showing a fixed-height window into an array of
        text lines. The caller manages ScrollOffset in the model and increments
        or decrements it via key subscriptions.

        Only lines in the range [ScrollOffset, ScrollOffset + MaxVisible) are
        rendered. Lines outside the window are not included in the view tree.

    .PARAMETER Lines
        Array of strings to display. Required.

    .PARAMETER ScrollOffset
        Zero-based index of the first visible line. Clamped so the window
        never extends past the end of Lines. Default: 0.

    .PARAMETER MaxVisible
        Number of lines to show at once. Default: 10.

    .PARAMETER Style
        Optional Elm style applied to each line.

    .OUTPUTS
        PSCustomObject — Box view node.

    .EXAMPLE
        New-ElmViewport -Lines $model.Lines -ScrollOffset $model.Scroll -MaxVisible 20

    .EXAMPLE
        $dimStyle = New-ElmStyle -Foreground 'BrightBlack'
        New-ElmViewport -Lines $logLines -ScrollOffset $model.Top -MaxVisible 15 -Style $dimStyle

    .NOTES
        To implement scroll-to-bottom, set ScrollOffset to
        [math]::Max(0, $lines.Count - $MaxVisible) before passing to this function.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [AllowEmptyString()]
        [string[]]$Lines,

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

        [Parameter()]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$MaxVisible = 10,

        [Parameter()]
        [PSCustomObject]$Style = $null
    )

    $count = $Lines.Count

    if ($count -eq 0) {
        return [PSCustomObject]@{
            Type      = 'Box'
            Direction = 'Vertical'
            Children  = @([PSCustomObject]@{ Type = 'Text'; Content = ''; Style = $null; Width = 'Auto'; Height = 'Auto' })
            Style     = $Style
            Width     = 'Auto'
            Height    = 'Auto'
        }
    }

    # Clamp offset
    $maxOffset = [math]::Max(0, $count - $MaxVisible)
    $offset    = [math]::Max(0, [math]::Min($ScrollOffset, $maxOffset))
    $endIdx    = [math]::Min($offset + $MaxVisible, $count) - 1

    $children = [System.Collections.Generic.List[object]]::new()
    for ($i = $offset; $i -le $endIdx; $i++) {
        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $Lines[$i]
            Style   = $Style
            Width   = 'Auto'
            Height  = 'Auto'
        })
    }

    return [PSCustomObject]@{
        Type      = 'Box'
        Direction = 'Vertical'
        Children  = $children.ToArray()
        Style     = $null
        Width     = 'Auto'
        Height    = 'Auto'
    }
}