Private/MenuArrowAccordion.ps1

function Build-InTUIAccordionRows {
    <#
    .SYNOPSIS
        Builds a flat list of visible rows from accordion section definitions.
    .DESCRIPTION
        Computes the visible parent and child rows based on which section (if any)
        is currently expanded. Returns an array of hashtables with Type, Label,
        SectionIndex, ChildIndex, ChildCount, Expanded, and IsDirect fields.
    #>

    [CmdletBinding()]
    [OutputType([object[]])]
    param(
        [Parameter(Mandatory)]
        [object[]]$Sections,

        [Parameter()]
        [int]$ExpandedIndex = -1
    )

    $rows = [System.Collections.ArrayList]::new()

    for ($s = 0; $s -lt $Sections.Count; $s++) {
        $section = $Sections[$s]
        # Support both 'Items' (InTUI convention) and 'Children' (TB convention)
        $children = if ($section.Children) { $section.Children } elseif ($section.Items) { $section.Items } else { @() }
        $isDirect = [bool]$section.IsDirect
        $isExpanded = ($s -eq $ExpandedIndex) -and ($children.Count -gt 0)

        $null = $rows.Add(@{
            Type         = 'parent'
            SectionIndex = $s
            Label        = $section.Title
            ChildCount   = $children.Count
            Expanded     = $isExpanded
            IsDirect     = $isDirect
        })

        if ($isExpanded) {
            for ($c = 0; $c -lt $children.Count; $c++) {
                $null = $rows.Add(@{
                    Type         = 'child'
                    SectionIndex = $s
                    ChildIndex   = $c
                    Label        = $children[$c]
                })
            }
        }
    }

    return @(, $rows.ToArray())
}

function Show-InTUIMenuArrowAccordion {
    <#
    .SYNOPSIS
        Accordion-style main menu. Left/Right expand/collapse, single-expansion
        constraint. Returns selection hashtable or $null on Escape.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Title,

        [Parameter(Mandatory)]
        [array]$Sections,

        [Parameter()]
        [int]$PageSize = 20
    )

    $palette = Get-InTUIColorPalette
    $reset = $palette.Reset

    $innerWidth = Get-InTUIConsoleInnerWidth
    $border = [char]0x2502
    $hLine = [char]0x2500

    # Render top border with gradient (once, before the loop)
    $topGradient = Get-InTUIGradientLine -Character $hLine -Width $innerWidth
    Write-Host (' {0}{1}{2}{3}{4}' -f $palette.SurfaceFg, ([char]0x256D), $topGradient, ([char]0x256E), $reset)

    # Render title inside the box
    $plainTitle = Strip-InTUIMarkup -Text $Title
    $titlePadded = (' {0}' -f $plainTitle).PadRight($innerWidth)
    Write-Host (' {0}{1}{2}{3}{4}{5}{6}' -f $palette.SurfaceFg, $border, $palette.Teal, $palette.Bold, $titlePadded, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset))

    # Title underline
    $titleUnderline = (' ' + ([string]$hLine * $plainTitle.Length)).PadRight($innerWidth)
    Write-Host (' {0}{1}{2}{3}{4}{5}' -f $palette.SurfaceFg, $border, $palette.Dim, $titleUnderline, $reset, ('{0}{1}{2}' -f $palette.SurfaceFg, $border, $reset))

    # Record anchor position
    $anchorTop = [Console]::CursorTop

    $expandedIndex = -1
    $selectedIndex = 0
    $previousRowCount = 0

    $rows = Build-InTUIAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex

    # Ensure enough buffer space for the accordion at max expansion
    $maxChildren = 0
    foreach ($sec in $Sections) {
        $ch = if ($sec.Children) { $sec.Children } elseif ($sec.Items) { $sec.Items } else { @() }
        if ($ch.Count -gt $maxChildren) { $maxChildren = $ch.Count }
    }
    $anchorTop = Ensure-InTUIBufferSpace -AnchorTop $anchorTop -NeededRows ($Sections.Count + $maxChildren + 6)

    try {
        try { [Console]::CursorVisible = $false } catch { $null = $_ }

        # Initial render
        Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
        $previousRowCount = $rows.Count

        while ($true) {
            $key = [Console]::ReadKey($true)
            $itemCount = $rows.Count

            switch ($key.Key) {
                'UpArrow' {
                    if ($selectedIndex -gt 0) {
                        $selectedIndex--
                    }
                    else {
                        $selectedIndex = $itemCount - 1
                    }
                    Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
                    $previousRowCount = $rows.Count
                }
                'DownArrow' {
                    if ($selectedIndex -lt ($itemCount - 1)) {
                        $selectedIndex++
                    }
                    else {
                        $selectedIndex = 0
                    }
                    Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
                    $previousRowCount = $rows.Count
                }
                'RightArrow' {
                    $currentRow = $rows[$selectedIndex]
                    if ($currentRow.Type -eq 'parent' -and -not $currentRow.Expanded -and -not $currentRow.IsDirect -and $currentRow.ChildCount -gt 0) {
                        $expandedIndex = $currentRow.SectionIndex
                        $rows = Build-InTUIAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex

                        # Move selection to first child
                        for ($f = 0; $f -lt $rows.Count; $f++) {
                            if ($rows[$f].Type -eq 'child' -and $rows[$f].SectionIndex -eq $expandedIndex) {
                                $selectedIndex = $f
                                break
                            }
                        }

                        Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
                        $previousRowCount = $rows.Count
                    }
                }
                'LeftArrow' {
                    if ($expandedIndex -ge 0) {
                        $collapseTarget = $expandedIndex
                        $expandedIndex = -1
                        $rows = Build-InTUIAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex

                        # Move selection to the collapsed parent
                        $selectedIndex = $collapseTarget
                        if ($selectedIndex -ge $rows.Count) {
                            $selectedIndex = $rows.Count - 1
                        }

                        Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
                        $previousRowCount = $rows.Count
                    }
                }
                'Enter' {
                    $currentRow = $rows[$selectedIndex]

                    if ($currentRow.Type -eq 'child') {
                        # Resolve the child text from the section data
                        $section = $Sections[$currentRow.SectionIndex]
                        $children = if ($section.Children) { $section.Children } elseif ($section.Items) { $section.Items } else { @() }
                        return @{
                            SectionIndex = $currentRow.SectionIndex
                            ItemIndex    = $currentRow.ChildIndex
                            ItemText     = $children[$currentRow.ChildIndex]
                        }
                    }
                    elseif ($currentRow.IsDirect) {
                        # Direct-action parent: return immediately
                        return @{
                            SectionIndex = $currentRow.SectionIndex
                            ItemIndex    = -1
                            ItemText     = $currentRow.Label
                        }
                    }
                    else {
                        # Toggle expand/collapse on parent
                        if ($currentRow.Expanded) {
                            $expandedIndex = -1
                        }
                        else {
                            $expandedIndex = $currentRow.SectionIndex
                        }
                        $rows = Build-InTUIAccordionRows -Sections $Sections -ExpandedIndex $expandedIndex

                        # If expanding, move to first child; if collapsing, stay on parent
                        if ($expandedIndex -ge 0 -and $currentRow.ChildCount -gt 0) {
                            for ($f = 0; $f -lt $rows.Count; $f++) {
                                if ($rows[$f].Type -eq 'child' -and $rows[$f].SectionIndex -eq $expandedIndex) {
                                    $selectedIndex = $f
                                    break
                                }
                            }
                        }
                        else {
                            for ($f = 0; $f -lt $rows.Count; $f++) {
                                if ($rows[$f].Type -eq 'parent' -and $rows[$f].SectionIndex -eq $currentRow.SectionIndex) {
                                    $selectedIndex = $f
                                    break
                                }
                            }
                        }

                        Render-InTUIAccordionBox -Rows $rows -SelectedIndex $selectedIndex -AnchorTop $anchorTop -PreviousRowCount $previousRowCount
                        $previousRowCount = $rows.Count
                    }
                }
                'Escape' {
                    return $null
                }
            }
        }
    }
    finally {
        try { [Console]::CursorVisible = $true } catch { $null = $_ }
    }
}