public/controls/display/New-UiExpander.ps1

function New-UiExpander {
    <#
    .SYNOPSIS
        Creates a collapsible expander section with a header and content area.
    .DESCRIPTION
        Creates a theme-aware collapsible section. Click the header to toggle visibility
        of the content. Built from primitives for proper dark theme support.
    .PARAMETER Header
        The text displayed in the expander header.
    .PARAMETER Content
        Scriptblock containing the UI elements to show when expanded.
    .PARAMETER IsExpanded
        Start with the expander open. Default is collapsed.
    .PARAMETER Variable
        Variable name for accessing this control in button actions.
    .PARAMETER EnabledWhen
        Conditional enabling based on another control's state. Accepts either:
        - A control proxy (e.g., $toggleControl) - enables when that control is truthy
        - A scriptblock (e.g., { $toggle -and $userName }) - enables when expression is true
 
        Truthy values: CheckBox=checked, TextBox=non-empty, ComboBox=has selection.
    .PARAMETER ClearIfDisabled
        Accepted alongside -EnabledWhen for consistency, but an expander has nothing to
        clear: disabling leaves it exactly as it was.
    .PARAMETER WPFProperties
        Hashtable of additional WPF properties to set on the control.
    .EXAMPLE
        New-UiExpander -Header 'Advanced Options' -Content {
            New-UiToggle -Label 'Enable logging' -Variable 'enableLog'
            New-UiToggle -Label 'Verbose mode' -Variable 'verbose'
        }
    .EXAMPLE
        New-UiExpander -Header 'Details' -IsExpanded -Content {
            New-UiLabel -Text 'This section starts open'
        }
    #>

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

        [Parameter(Mandatory)]
        [scriptblock]$Content,

        [switch]$IsExpanded,

        [Parameter()]
        [string]$Variable,

        [Parameter()]
        [object]$EnabledWhen,

        [switch]$ClearIfDisabled,

        [Parameter()]
        [hashtable]$WPFProperties
    )

    # Grab session and theme context
    $session = Assert-UiSession -CallerName 'New-UiExpander'
    $colors  = Get-ThemeColors
    $parent  = $session.CurrentParent

    # Build the outer container with themed border
    $container = [System.Windows.Controls.Border]@{
        BorderThickness = [System.Windows.Thickness]::new(1)
        BorderBrush     = ConvertTo-UiBrush $colors.Border
        Background      = ConvertTo-UiBrush $colors.ControlBg
        CornerRadius    = [System.Windows.CornerRadius]::new(4)
        Margin          = [System.Windows.Thickness]::new(4)
        Tag             = 'ControlBgBrush'
    }

    # Stack panel holds header row and collapsible content
    $outerStack      = [System.Windows.Controls.StackPanel]::new()
    $container.Child = $outerStack

    # Chevron glyph rotates 90deg when content is visible
    $chevron = [System.Windows.Controls.TextBlock]@{
        Text                  = [PsUi.ModuleContext]::GetIcon('ChevronRight')
        FontFamily            = [PsUi.ModuleContext]::ActiveIconFontFamily
        FontSize              = 12
        Foreground            = ConvertTo-UiBrush $colors.SecondaryText
        VerticalAlignment     = 'Center'
        Margin                = [System.Windows.Thickness]::new(0, 0, 8, 0)
        RenderTransformOrigin = '0.5,0.5'
    }
    $chevron.RenderTransform = [System.Windows.Media.RotateTransform]::new(0)

    # Header label with semibold weight
    $headerText = [System.Windows.Controls.TextBlock]@{
        Text              = $Header
        FontFamily        = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI')
        FontSize          = 13
        FontWeight        = 'SemiBold'
        Foreground        = ConvertTo-UiBrush $colors.ControlFg
        VerticalAlignment = 'Center'
        Tag               = 'ControlFgBrush'
    }

    # Clickable header row with chevron and label
    $headerPanel = [System.Windows.Controls.StackPanel]@{
        Orientation = 'Horizontal'
        Cursor      = 'Hand'
        Background  = [System.Windows.Media.Brushes]::Transparent
        Margin      = [System.Windows.Thickness]::new(10, 8, 10, 8)
    }
    [void]$headerPanel.Children.Add($chevron)
    [void]$headerPanel.Children.Add($headerText)
    [void]$outerStack.Children.Add($headerPanel)

    # Content area
    $contentPanel = [System.Windows.Controls.StackPanel]@{
        Margin     = [System.Windows.Thickness]::new(12, 0, 12, 10)
        Visibility = if ($IsExpanded) { 'Visible' } else { 'Collapsed' }
    }
    [void]$outerStack.Children.Add($contentPanel)

    if ($IsExpanded) { $chevron.RenderTransform.Angle = 90 }

    # Toggle visibility and rotate chevron on click
    $headerPanel.Add_MouseLeftButtonUp({
        param($sender, $eventArgs)
        $outerStack   = $sender.Parent
        $contentPanel = $outerStack.Children[1]
        $chevronGlyph = $sender.Children[0]

        if ($contentPanel.Visibility -eq 'Collapsed') {
            $contentPanel.Visibility = 'Visible'
            $chevronGlyph.RenderTransform.Angle = 90
        }
        else {
            $contentPanel.Visibility = 'Collapsed'
            $chevronGlyph.RenderTransform.Angle = 0
        }
    })

    # Hover feedback on header row
    $headerPanel.Add_MouseEnter({ param($sender, $eventArgs) $sender.Opacity = 0.7 })
    $headerPanel.Add_MouseLeave({ param($sender, $eventArgs) $sender.Opacity = 1.0 })

    # Register control for variable hydration if name provided
    if ($Variable) { $session.AddControlSafe($Variable, $container) }

    # Register themed elements for dynamic switching
    [PsUi.ThemeEngine]::RegisterElement($container)
    [PsUi.ThemeEngine]::RegisterElement($headerText)

    Add-UiControlToParent -Control $container -Parent $parent

    # New-UiStatusBar reads the depth to keep a bar in here
    $previousParent = $session.CurrentParent
    $session.CurrentParent = $contentPanel
    $script:TabOrExpanderDepth = [int]$script:TabOrExpanderDepth + 1

    # 5.1 NREs on a finally from a -NoAsync click or a row details template. The do loop keeps a break or continue in -Content from skipping the restores.
    try { do { & $Content } while ($false) }
    catch {
        $session.CurrentParent = $previousParent
        $script:TabOrExpanderDepth--
        throw
    }

    $session.CurrentParent = $previousParent
    $script:TabOrExpanderDepth--

    if ($WPFProperties) {
        Set-UiProperties -Control $container -Properties $WPFProperties
    }

    if ($EnabledWhen) {
        Register-UiCondition -TargetControl $container -Condition $EnabledWhen -ClearIfDisabled:$ClearIfDisabled
    }
}