public/layout/New-UiPanel.ps1

function New-UiPanel {
    <#
    .SYNOPSIS
        Creates a panel container for organizing child controls.
    .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 hashtable defining a custom action button in the panel header.
        Requires -Header parameter to be set.
        Hashtable should contain: 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.
        Invalid properties will generate warnings but not stop 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 @{
            Icon = "Info"
            Tooltip = "Show Help"
            Action = { Show-UiMessageDialog -Title "Help" -Message "This is help text" }
        } -Content {
            New-UiLabel -Text "Panel content"
        }
    .EXAMPLE
        New-UiPanel -Content { } -WPFProperties @{
            ToolTip = "Custom tooltip"
            Cursor = "Hand"
            Opacity = 0.8
        }
    .EXAMPLE
        New-UiPanel -Content { } -WPFProperties @{
            "Grid.Row" = 1
            "Grid.Column" = 2
            Tag = "MyTag"
        }
    #>

    [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) {
            $panelMaxCols = $MaxColumns  # Capture for closure - completely independent from window
            $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)
                $minColumnWidth = 150  # Minimum width per column in wrap panel
                $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 display control (GroupBox wrapper if Header specified)
    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 = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
            $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
            $iconButton.Add_Click({ & $actionScript }.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
    }

    if ($parent -is [System.Windows.Controls.Panel]) {
        [void]$parent.Children.Add($displayControl)
    }
    elseif ($parent -is [System.Windows.Controls.ItemsControl]) {
        [void]$parent.Items.Add($displayControl)
    }
    elseif ($parent -is [System.Windows.Controls.ContentControl]) {
        $parent.Content = $displayControl
    }

    # 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
    
    Write-Debug "Content block complete. Added to: $($oldParent.GetType().Name)"
}