Public/View/New-ElmTable.ps1

function New-ElmTable {
    <#
    .SYNOPSIS
        Creates a data table view node.

    .DESCRIPTION
        Returns a Box (Vertical) view node rendering rows of tabular data with
        optional column headers and a selected-row highlight. Columns are padded
        to uniform widths and separated by ' | '. A separator row of dashes is
        rendered between the header and data rows when headers are present.

        Rendered example (3 columns):

            Name | Age | City
            -----------+-----+----------
            Alice | 30 | New York
          > Bob | 25 | London

        Column widths are auto-calculated from the longest content in each column
        unless -ColumnWidths is supplied.

    .PARAMETER Headers
        Array of column header strings. When empty or omitted, no header row or
        separator row is rendered.

    .PARAMETER Rows
        Array of rows; each row is an array of strings (one per column). Required.

    .PARAMETER SelectedRow
        Zero-based index of the selected data row. -1 means no selection.
        Default: -1.

    .PARAMETER ColumnWidths
        Optional array of explicit column widths (one per column). When omitted,
        widths are calculated from the maximum content length in each column.

    .PARAMETER Style
        Elm style applied to all unselected data rows.

    .PARAMETER HeaderStyle
        Elm style applied to the header row and separator row.

    .PARAMETER SelectedStyle
        Elm style applied to the selected row. When omitted, defaults to bold.

    .OUTPUTS
        PSCustomObject - Box (Vertical) view node.

    .EXAMPLE
        New-ElmTable -Headers @('Name','Age','City') `
                     -Rows @(@('Alice','30','New York'),@('Bob','25','London')) `
                     -SelectedRow $model.TableCursor

    .EXAMPLE
        $hs = New-ElmStyle -Foreground 'BrightCyan' -Bold
        $ss = New-ElmStyle -Foreground 'BrightYellow'
        New-ElmTable -Headers @('Key','Value') -Rows $model.Pairs `
                     -SelectedRow $model.Cursor `
                     -HeaderStyle $hs -SelectedStyle $ss

    .NOTES
        The caller is responsible for tracking SelectedRow and updating it via
        key subscriptions. Rows with fewer cells than the column count are padded
        with empty strings. Extra cells beyond the column count are ignored.
    #>

    [CmdletBinding()]
    param(
        [Parameter()]
        [AllowEmptyCollection()]
        [string[]]$Headers = @(),

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]]$Rows,

        [Parameter()]
        [int]$SelectedRow = -1,

        [Parameter()]
        [AllowEmptyCollection()]
        [int[]]$ColumnWidths = @(),

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

        [Parameter()]
        [PSCustomObject]$HeaderStyle = $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
        }
    }

    # Determine column count
    $hasHeaders = $Headers.Count -gt 0
    $headerCount = $Headers.Count
    $rowCount    = $Rows.Count

    # Find max columns across headers and rows
    $colCount = $headerCount
    foreach ($row in $Rows) {
        $rowArr = @($row)
        if ($rowArr.Count -gt $colCount) { $colCount = $rowArr.Count }
    }

    if ($colCount -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'
        }
    }

    # Calculate column widths
    $resolvedWidths = [int[]]::new($colCount)
    for ($c = 0; $c -lt $colCount; $c++) {
        $w = if ($c -lt $Headers.Count) { $Headers[$c].Length } else { 0 }
        foreach ($row in $Rows) {
            $rowArr = @($row)
            if ($c -lt $rowArr.Count) {
                $cellLen = ([string]$rowArr[$c]).Length
                if ($cellLen -gt $w) { $w = $cellLen }
            }
        }
        $resolvedWidths[$c] = $w
    }

    # Override with explicit widths if provided and counts match
    if ($ColumnWidths.Count -eq $colCount) {
        $resolvedWidths = $ColumnWidths
    }

    # Helper: render a row of cells as padded string
    $renderRow = {
        param([string[]]$cells)
        $parts = for ($c = 0; $c -lt $colCount; $c++) {
            $cell = if ($c -lt $cells.Count) { $cells[$c] } else { '' }
            $cell.PadRight($resolvedWidths[$c])
        }
        $parts -join ' | '
    }

    # Helper: render separator row
    $separatorParts = for ($c = 0; $c -lt $colCount; $c++) {
        '-' * $resolvedWidths[$c]
    }
    $separator = $separatorParts -join '-+-'

    $children = [System.Collections.Generic.List[object]]::new()

    # Header row
    if ($hasHeaders) {
        $paddedHeaders = [string[]]::new($colCount)
        for ($c = 0; $c -lt $colCount; $c++) {
            $paddedHeaders[$c] = if ($c -lt $Headers.Count) { $Headers[$c] } else { '' }
        }
        $headerContent = & $renderRow $paddedHeaders
        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $headerContent
            Style   = $HeaderStyle
            Width   = 'Auto'
            Height  = 'Auto'
        })
        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $separator
            Style   = $HeaderStyle
            Width   = 'Auto'
            Height  = 'Auto'
        })
    }

    # Data rows
    for ($r = 0; $r -lt $rowCount; $r++) {
        $rowArr  = [string[]](@($Rows[$r]) | ForEach-Object { [string]$_ })
        $content = & $renderRow $rowArr
        $rowStyle = if ($r -eq $SelectedRow) { $resolvedSelectedStyle } else { $Style }
        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $content
            Style   = $rowStyle
            Width   = 'Auto'
            Height  = 'Auto'
        })
    }

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