Public/View/New-ElmPaginator.ps1

function New-ElmPaginator {
    <#
    .SYNOPSIS
        Creates a paginator or tab bar view node.

    .DESCRIPTION
        Returns a view node representing a numeric page indicator, a dot row, or
        a named tab bar, depending on the parameter set used.

        Numeric mode (default):

            < 3 / 7 >

        The `<` and `>` indicators are replaced with spaces when at the first or
        last page respectively, giving a stable width.

        Dots mode (-Dots switch):

            ○ ○ ● ○ ○ ○ ○

        Each page is a separate Text child inside a horizontal Box. The active
        page uses FilledDot; others use EmptyDot. Chars are configurable for
        terminals that do not support Unicode.

        Named-tab mode (-Tabs):

            Tab1 | [Tab2] | Tab3

        Each tab label is a separate Text child node inside a horizontal Box. The
        active tab is wrapped in brackets and receives ActiveStyle; inactive tabs
        receive Style.

    .PARAMETER CurrentPage
        One-based current page number. Clamped to [1, PageCount]. Used in
        Numeric and Dots modes.

    .PARAMETER PageCount
        Total number of pages. Minimum 1. Used in Numeric and Dots modes.

    .PARAMETER Dots
        Switch that selects Dots mode. Required to disambiguate from Numeric
        when no dot-specific parameters are passed.

    .PARAMETER FilledDot
        Character string for the active page dot. Default: '●' (U+25CF).
        Use ASCII alternatives (e.g. '*') for terminals without Unicode support.

    .PARAMETER EmptyDot
        Character string for inactive page dots. Default: '○' (U+25CB).
        Use ASCII alternatives (e.g. '-') for terminals without Unicode support.

    .PARAMETER Separator
        String inserted between each dot. Default: ' '.

    .PARAMETER Tabs
        Array of tab label strings. Used in named-tab mode.

    .PARAMETER ActiveTab
        Zero-based index of the active tab. Clamped to [0, Tabs.Count-1]. Used
        in named-tab mode.

    .PARAMETER Style
        Elm style applied to the full text node (Numeric), inactive dots (Dots),
        or inactive tab labels (Tabs).

    .PARAMETER ActiveStyle
        Elm style applied to the active element. Falls back to Style when null.

    .OUTPUTS
        PSCustomObject - Text node (Numeric), Box/Horizontal (Dots or Tabs).

    .EXAMPLE
        # Numeric pagination
        New-ElmPaginator -CurrentPage $model.Page -PageCount $model.TotalPages

    .EXAMPLE
        # Dot pagination with defaults
        New-ElmPaginator -Dots -CurrentPage $model.Page -PageCount $model.TotalPages

    .EXAMPLE
        # Dot pagination - ASCII-safe for Windows PowerShell 5.1
        New-ElmPaginator -Dots -CurrentPage 3 -PageCount 5 -FilledDot '*' -EmptyDot '-'

    .EXAMPLE
        # Named tabs
        $activeStyle = New-ElmStyle -Foreground 'BrightWhite' -Bold
        New-ElmPaginator -Tabs @('Overview','Details','Logs') `
                         -ActiveTab $model.Tab `
                         -ActiveStyle $activeStyle

    .NOTES
        In Dots mode, UTF-8 console encoding is required for the default Unicode
        dot chars to render correctly. New-ElmTerminalDriver sets this automatically.

        In named-tab mode, a ' | ' Text node is inserted between each tab as a
        separator. The caller manages ActiveTab index via key subscriptions.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Numeric')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Numeric')]
        [Parameter(Mandatory, ParameterSetName = 'Dots')]
        [int]$CurrentPage,

        [Parameter(Mandatory, ParameterSetName = 'Numeric')]
        [Parameter(Mandatory, ParameterSetName = 'Dots')]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$PageCount,

        [Parameter(Mandatory, ParameterSetName = 'Dots')]
        [switch]$Dots,

        [Parameter(ParameterSetName = 'Dots')]
        [ValidateNotNullOrEmpty()]
        [string]$FilledDot = ([char]0x25CF).ToString(),   # ●

        [Parameter(ParameterSetName = 'Dots')]
        [ValidateNotNullOrEmpty()]
        [string]$EmptyDot = ([char]0x25CB).ToString(),    # ○

        [Parameter(ParameterSetName = 'Dots')]
        [string]$Separator = ' ',

        [Parameter(Mandatory, ParameterSetName = 'Tabs')]
        [ValidateCount(1, [int]::MaxValue)]
        [string[]]$Tabs,

        [Parameter(Mandatory, ParameterSetName = 'Tabs')]
        [int]$ActiveTab,

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

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

    $resolvedActiveStyle = if ($null -ne $ActiveStyle) { $ActiveStyle } else { $Style }

    if ($PSCmdlet.ParameterSetName -eq 'Numeric') {
        $page = [math]::Max(1, [math]::Min($CurrentPage, $PageCount))

        $left    = if ($page -gt 1)          { '<' } else { ' ' }
        $right   = if ($page -lt $PageCount) { '>' } else { ' ' }
        $content = "$left $page / $PageCount $right"

        return [PSCustomObject]@{
            Type    = 'Text'
            Content = $content
            Style   = $resolvedActiveStyle
            Width   = 'Auto'
            Height  = 'Auto'
        }
    }

    if ($PSCmdlet.ParameterSetName -eq 'Dots') {
        $page     = [math]::Max(1, [math]::Min($CurrentPage, $PageCount))
        $children = [System.Collections.Generic.List[object]]::new()

        for ($i = 1; $i -le $PageCount; $i++) {
            if ($i -gt 1) {
                $children.Add([PSCustomObject]@{
                    Type    = 'Text'
                    Content = $Separator
                    Style   = $Style
                    Width   = 'Auto'
                    Height  = 'Auto'
                })
            }

            $isActive = $i -eq $page
            $children.Add([PSCustomObject]@{
                Type    = 'Text'
                Content = if ($isActive) { $FilledDot } else { $EmptyDot }
                Style   = if ($isActive) { $resolvedActiveStyle } else { $Style }
                Width   = 'Auto'
                Height  = 'Auto'
            })
        }

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

    # Named-tab mode
    $clampedTab = [math]::Max(0, [math]::Min($ActiveTab, $Tabs.Count - 1))

    $children = [System.Collections.Generic.List[object]]::new()
    for ($i = 0; $i -lt $Tabs.Count; $i++) {
        if ($i -gt 0) {
            $children.Add([PSCustomObject]@{
                Type    = 'Text'
                Content = ' | '
                Style   = $Style
                Width   = 'Auto'
                Height  = 'Auto'
            })
        }

        $isActive = $i -eq $clampedTab
        $label    = if ($isActive) { "[$($Tabs[$i])]" } else { $Tabs[$i] }
        $children.Add([PSCustomObject]@{
            Type    = 'Text'
            Content = $label
            Style   = if ($isActive) { $resolvedActiveStyle } else { $Style }
            Width   = 'Auto'
            Height  = 'Auto'
        })
    }

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