Public/View/New-ElmList.ps1

function New-ElmList {
    <#
    .SYNOPSIS
        Creates a scrollable, selectable list view node.

    .DESCRIPTION
        Returns a Box view node rendering a list of string items with one item
        highlighted as selected. The visible window auto-scrolls to keep the
        selected item in view.

        Rendered example (MaxVisible=4, SelectedIndex=2):

            Item A
            Item B
          > Item C <- selected
            Item D

        The caller is responsible for tracking SelectedIndex in the model and
        updating it via key subscriptions (UpArrow/DownArrow).

    .PARAMETER Items
        Array of strings to display. Required.

    .PARAMETER SelectedIndex
        Zero-based index of the currently selected item. Clamped to valid range.
        Default: 0.

    .PARAMETER MaxVisible
        Maximum number of items to show at once. Default: 10.

    .PARAMETER Prefix
        String prepended to the selected item. Default: '> '.

    .PARAMETER UnselectedPrefix
        String prepended to unselected items. Should match -Prefix in length
        for alignment. Default: ' '.

    .PARAMETER Style
        Base style applied to all unselected items.

    .PARAMETER SelectedStyle
        Style applied to the selected item. When omitted, defaults to bold.

    .OUTPUTS
        PSCustomObject - Box view node.

    .EXAMPLE
        New-ElmList -Items @('Apple', 'Banana', 'Cherry') -SelectedIndex $model.Index

    .EXAMPLE
        $selStyle = New-ElmStyle -Foreground 'BrightYellow' -Bold
        New-ElmList -Items $model.Items -SelectedIndex $model.Cursor `
                    -MaxVisible 8 -SelectedStyle $selStyle

    .NOTES
        The visible window is calculated automatically: if SelectedIndex is
        outside the current window, the window shifts to keep it visible.
    #>

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

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

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

        [Parameter()]
        [string]$Prefix = '> ',

        [Parameter()]
        [string]$UnselectedPrefix = ' ',

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

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

    # Default selected style: bold
    $resolvedSelectedStyle = if ($null -ne $SelectedStyle) {
        $SelectedStyle
    } else {
        [PSCustomObject]@{
            Foreground    = $null
            Background    = $null
            Bold          = $true
            Italic        = $false
            Underline     = $false
            Strikethrough = $false
            Border        = 'None'
            PaddingTop    = 0
            PaddingRight  = 0
            PaddingBottom = 0
            PaddingLeft   = 0
            MarginTop     = 0
            MarginRight   = 0
            MarginBottom  = 0
            MarginLeft    = 0
            Align         = 'Left'
            Width         = $null
            Height        = $null
        }
    }

    $count = $Items.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 selected index
    $sel = [math]::Max(0, [math]::Min($SelectedIndex, $count - 1))

    # Calculate scroll offset so selected item stays visible
    $scrollOffset = 0
    if ($sel -ge $MaxVisible) {
        $scrollOffset = $sel - $MaxVisible + 1
    }
    $endIdx = [math]::Min($scrollOffset + $MaxVisible, $count) - 1

    $children = [System.Collections.Generic.List[object]]::new()
    for ($i = $scrollOffset; $i -le $endIdx; $i++) {
        $isSelected = ($i -eq $sel)
        $itemPrefix = if ($isSelected) { $Prefix } else { $UnselectedPrefix }
        $content    = $itemPrefix + $Items[$i]
        $itemStyle  = if ($isSelected) { $resolvedSelectedStyle } else { $Style }

        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $content
            Style   = $itemStyle
            Width   = 'Auto'
            Height  = 'Auto'
        })
    }

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