public/layout/New-UiPanel.ps1

function New-UiPanel {
    <#
    .SYNOPSIS
        Creates a panel container for organizing child controls.
    .DESCRIPTION
        The workhorse container. Stacks children vertically or horizontally, or wraps them into
        responsive columns with -LayoutStyle Wrap (-MaxColumns caps the count). -Header puts it in
        a themed GroupBox. In a horizontal or wrapped row, buttons beside a labeled input line up
        with its box.
    .PARAMETER Content
        ScriptBlock containing child controls to render inside the panel.
    .PARAMETER Header
        Optional header text. Wraps the panel in a themed GroupBox when set.
    .PARAMETER Type
        Container type: Stack (default), Tab, or Wrap.
    .PARAMETER LayoutStyle
        Child layout mode: Stack (default) or Wrap.
    .PARAMETER Orientation
        Stack direction when Type is Stack. Defaults to Vertical.
    .PARAMETER FullWidth
        Forces the panel to take full width in WrapPanel layouts.
    .PARAMETER MaxColumns
        Maximum responsive columns for Wrap layout (1-4). Children resize automatically.
    .PARAMETER HeaderAction
        Optional action button in the panel header. Requires -Header to be set.
        Pass New-UiHeaderAction output, or the legacy hashtable containing: Icon (string),
        Tooltip (string), Action (scriptblock).
    .PARAMETER ShowSourceButton
        When used with -Header, automatically adds a "View Source Code" button that displays
        the Content scriptblock in a PowerShell-styled dialog.
    .PARAMETER WPFProperties
        Hashtable of additional WPF properties to set on the control.
        Allows setting any valid WPF property not explicitly exposed as a parameter.
        Bad values warn and get skipped. A property name that does not exist on the control is
        skipped silently (-Verbose shows it). Nothing stops execution.
        Supports attached properties using dot notation (e.g., "Grid.Row").
    .EXAMPLE
        New-UiPanel -Header "Example Panel" -ShowSourceButton -Content {
            New-UiLabel -Text "This code can be viewed by clicking the button"
        }
    .EXAMPLE
        New-UiPanel -Header "Custom Action" -HeaderAction (
            New-UiHeaderAction -Icon Info -Tooltip 'Show Help' -Action { Show-UiMessageDialog -Message 'Help text' }
        ) -Content {
            New-UiLabel -Text "Panel content"
        }
 
        The legacy hashtable form still works:
        -HeaderAction @{ Icon = 'Info'; Tooltip = 'Show Help'; Action = { ... } }
    .EXAMPLE
        # Wrap layout flows children into columns, capped at two here.
        New-UiPanel -LayoutStyle Wrap -MaxColumns 2 -Content {
            New-UiToggle -Label 'Wake on LAN' -Variable 'wol'
            New-UiToggle -Label 'Remote registry' -Variable 'remoteReg'
            New-UiToggle -Label 'ICMP echo' -Variable 'icmp'
            New-UiToggle -Label 'SMB v1' -Variable 'smb1'
        }
    .EXAMPLE
        New-UiPanel -Content { } -WPFProperties @{
            "Grid.Row" = 1
            "Grid.Column" = 2
        }
    #>

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

        [string]$Header,

        [ValidateSet('Stack', 'Tab', 'Wrap')]
        [string]$Type = 'Stack',

        [ValidateSet('Stack', 'Wrap')]
        [string]$LayoutStyle = 'Stack',

        [System.Windows.Controls.Orientation]$Orientation = 'Vertical',

        [switch]$FullWidth,

        [ValidateScript({ $_ -eq 0 -or ($_ -ge 1 -and $_ -le 4) })]
        [int]$MaxColumns = 0,

        [hashtable]$HeaderAction,

        [switch]$ShowSourceButton,

        [Parameter()]
        [hashtable]$WPFProperties
    )

    $session = Assert-UiSession -CallerName 'New-UiPanel'
    Write-Debug "Header: '$Header', Type: $Type"
    $parent    = $session.CurrentParent
    $oldParent = $parent

    # Auto-generate HeaderAction for ShowSourceButton
    if ($ShowSourceButton -and $Header) {
        # Preserve indentation by using the ScriptBlock's AST extent
        # This captures the original formatting including indentation
        $sourceCode = $Content.Ast.Extent.Text

        # If AST is not available or doesn't preserve formatting, fall back to ToString()
        if (!$sourceCode) {
            $sourceCode = $Content.ToString()
        }

        # Strip outer braces and normalize indentation
        # Remove leading/trailing whitespace
        $sourceCode = $sourceCode.Trim()

        # If the code starts with { and ends with }, remove them
        if ($sourceCode -match '(?s)^\s*\{(.+)\}\s*$') {
            $sourceCode = $matches[1]
        }

        # Normalize indentation - find minimum indentation and remove it from all lines
        $lines = $sourceCode -split "`r?`n"
        $nonEmptyLines = $lines | Where-Object { $_ -match '\S' }
        if ($nonEmptyLines) {
            $minIndent = ($nonEmptyLines | ForEach-Object {
                if ($_ -match '^(\s*)') {
                    $matches[1].Length
                } else {
                    0
                }
            } | Measure-Object -Minimum).Minimum

            $normalizedLines = $lines | ForEach-Object {
                if ($_ -match '\S' -and $_.Length -ge $minIndent) {
                    $_.Substring($minIndent)
                } else {
                    $_
                }
            }
            $sourceCode = $normalizedLines -join "`n"
        }

        # Final trim to remove leading/trailing blank lines
        $sourceCode = $sourceCode.Trim()

        $HeaderAction = @{
            Icon = 'Code'
            Tooltip = 'View Source Code'
            Action = [scriptblock]::Create(@"
Show-UiMessageDialog -Title 'Source Code' -Message @'
$sourceCode
'@ -PowerShell
"@
)
        }
    }

    # Create container based on Type and LayoutStyle
    if ($Type -eq 'Tab') {
        $innerContainer = [System.Windows.Controls.TabControl]@{
            Margin = [System.Windows.Thickness]::new(0, 0, 0, 10)
        }
        $fullWidthConstraint = $true
    }
    elseif ($Type -eq 'Wrap' -or $LayoutStyle -eq 'Wrap') {
        # Use WrapPanel for wrapping child controls
        $innerContainer = [System.Windows.Controls.WrapPanel]@{
            Orientation = 'Horizontal'
            HorizontalAlignment = 'Stretch'
        }
        # When MaxColumns is specified, panel should stretch but stay in column layout
        $fullWidthConstraint = $FullWidth

        # If MaxColumns specified, add responsive sizing for child controls inside this panel
        if ($MaxColumns -gt 0) {
            # Captured for the closure, and independent of the window's own column count
            $panelMaxCols = $MaxColumns
            $innerContainer.Add_SizeChanged({
                param($sender, $eventArgs)

                $paddingBuffer = 16
                $availableWidth = $sender.ActualWidth - $paddingBuffer
                if ($availableWidth -le 0) { return }

                # Calculate column width based on this panel's MaxColumns (NOT window's)
                # Minimum width per column in the wrap panel
                $minColumnWidth = 150
                $possibleCols   = [Math]::Max(1, [Math]::Floor($availableWidth / $minColumnWidth))
                $actualCols     = [Math]::Min($possibleCols, $panelMaxCols)
                $actualCols     = [Math]::Max($actualCols, 1)

                $childWidth = [Math]::Floor(($availableWidth / $actualCols) - 8)

                # Apply width to all children that support it
                foreach ($child in $sender.Children) {
                    if ($child -is [System.Windows.FrameworkElement]) {
                        # Skip items that explicitly want full width (have Tag = 'FullWidth')
                        if ($child.Tag -eq 'FullWidth') { continue }
                        $child.Width = $childWidth
                    }
                }
            }.GetNewClosure())
        }
    }
    else {
        # Default Stack behavior
        $innerContainer = [System.Windows.Controls.StackPanel]@{
            Orientation = $Orientation
        }
        $fullWidthConstraint = $FullWidth
    }

    # Build the display control, a GroupBox around it when Header is set
    if ($Header) {
        $displayControl = [System.Windows.Controls.GroupBox]@{
            Content = $innerContainer
        }

        # When using Wrap layout, panel should stretch to fill available space
        if ($Type -eq 'Wrap' -or $LayoutStyle -eq 'Wrap') {
            $displayControl.HorizontalAlignment = 'Stretch'
        }

        # Create header with icon button if HeaderAction is provided
        if ($HeaderAction) {
            $headerGrid = [System.Windows.Controls.Grid]::new()
            $col1 = [System.Windows.Controls.ColumnDefinition]::new()
            $col1.Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star)
            $col2 = [System.Windows.Controls.ColumnDefinition]::new()
            $col2.Width = [System.Windows.GridLength]::Auto
            [void]$headerGrid.ColumnDefinitions.Add($col1)
            [void]$headerGrid.ColumnDefinitions.Add($col2)

            $headerText = [System.Windows.Controls.TextBlock]::new()
            $headerText.Text = $Header
            $headerText.VerticalAlignment = 'Center'
            $headerText.FontWeight = 'SemiBold'
            $headerText.Tag = 'ControlFgBrush'
            [PsUi.ThemeEngine]::RegisterElement($headerText)
            [System.Windows.Controls.Grid]::SetColumn($headerText, 0)
            [void]$headerGrid.Children.Add($headerText)

            $iconButton = [System.Windows.Controls.Button]::new()
            $iconButton.Width = 24
            $iconButton.Height = 24
            $iconButton.Padding = [System.Windows.Thickness]::new(0)
            $iconButton.ToolTip = $HeaderAction.Tooltip
            $iconButton.VerticalAlignment = 'Center'
            $iconButton.Margin = [System.Windows.Thickness]::new(8, 0, 0, 0)

            $iconBlock = [System.Windows.Controls.TextBlock]::new()
            $iconBlock.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $iconBlock.FontSize = 12
            $iconBlock.HorizontalAlignment = 'Center'
            $iconBlock.VerticalAlignment = 'Center'

            # Map icon name to character using ModuleContext if available
            $iconChar = $null
            if ($HeaderAction.Icon) {
                $iconText = [PsUi.ModuleContext]::GetIcon($HeaderAction.Icon)
                if ($iconText) {
                    $iconChar = $iconText
                }
                else {
                    # Fallback mapping (icons should already be in CharList but just in case)
                    $iconChar = switch ($HeaderAction.Icon) {
                        'Code' { [PsUi.ModuleContext]::GetIcon('Code') }
                        'Info' { [PsUi.ModuleContext]::GetIcon('Info') }
                        'Help' { [PsUi.ModuleContext]::GetIcon('Help') }
                        'View' { [PsUi.ModuleContext]::GetIcon('View') }
                        default { [PsUi.ModuleContext]::GetIcon('Info') }
                    }
                }
            }
            else {
                $iconChar = [PsUi.ModuleContext]::GetIcon('Info')
            }

            $iconBlock.Text = $iconChar
            $iconButton.Content = $iconBlock

            # Style and click handler
            Set-ButtonStyle -Button $iconButton -IconOnly
            $actionScript   = $HeaderAction.Action
            $ownerSessionId = $session.SessionId
            $pushSession    = ${function:Push-UiSession}
            $popSession     = ${function:Pop-UiSession}
            $invokeCallback = ${function:Invoke-UiCallback}

            # The trap's continue still reaches the pop
            $iconButton.Add_Click({
                trap { Write-Warning "New-UiPanel header action error: $_"; continue }
                $sessionToken = & $pushSession -SessionId $ownerSessionId
                $null         = & $invokeCallback -ScriptBlock $actionScript -Label 'New-UiPanel header action'
                & $popSession -Token $sessionToken
            }.GetNewClosure())

            [System.Windows.Controls.Grid]::SetColumn($iconButton, 1)
            [void]$headerGrid.Children.Add($iconButton)

            # Use grid as header
            $displayControl.Header = $headerGrid
        }
        else {
            # Simple string header (existing behavior)
            $displayControl.Header = $Header
        }

        Set-GroupBoxStyle -GroupBox $displayControl
    }
    else {
        $displayControl = $innerContainer
    }

    # Apply layout constraints
    Set-FullWidthConstraint -Control $displayControl -Parent $parent -FullWidth:$fullWidthConstraint
    Set-ResponsiveConstraints -Control $displayControl -FullWidth:$fullWidthConstraint

    # Apply custom WPF properties if specified
    if ($WPFProperties) {
        Set-UiProperties -Control $displayControl -Properties $WPFProperties
    }

    Add-UiControlToParent -Control $displayControl -Parent $parent

    # Execute content block with innerContainer as the new parent
    $session.CurrentParent = $innerContainer
    Write-Debug "Entering content block. Parent set to: $($innerContainer.GetType().Name)"
    
    # Execute content - restore parent outside try/finally for PS 5.1 closure compatibility
    try {
        Invoke-UiContent -Content $Content -CallerName 'New-UiPanel' -ErrorAction Stop
    }
    catch {
        # Restore parent before re-throwing
        $session.CurrentParent = $oldParent
        Write-Debug "Content execution failed: $($_.Exception.Message)"
        throw
    }
    
    # Restore parent after successful content execution
    $session.CurrentParent = $oldParent

    # -Type Tab builds a TabControl, which isn't a Panel
    if ($innerContainer -is [System.Windows.Controls.Panel]) { Set-UiRowAlignment -Panel $innerContainer }

    Write-Debug "Content block complete. Added to: $($oldParent.GetType().Name)"
}