private/output/Add-ErrorDetailsPanel.ps1

function Add-ErrorDetailsPanel {
    <#
    .SYNOPSIS
        The collapsible details panel under the errors grid, and the handlers for its two toolbar buttons.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [System.Windows.Controls.Grid]$Container,

        [Parameter(Mandatory)]
        [System.Windows.Controls.DataGrid]$DataGrid,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [System.Collections.ObjectModel.ObservableCollection[PSObject]]$ErrorsList,

        [Parameter(Mandatory)]
        [System.Windows.Controls.Button]$CopyButton,

        [Parameter(Mandatory)]
        [System.Windows.Controls.Button]$ExportButton
    )

    # WPF's own Expander draws a circled arrow, so this is built the way New-UiExpander builds one.
    # It's up for as long as the window is so every brush here follows the theme.
    $errorDetailsPanel = [System.Windows.Controls.Border]@{
        Visibility      = 'Collapsed'
        Margin          = [System.Windows.Thickness]::new(4)
        BorderThickness = [System.Windows.Thickness]::new(1)
        CornerRadius    = [System.Windows.CornerRadius]::new(4)
    }
    $errorDetailsPanel.SetResourceReference([System.Windows.Controls.Border]::BackgroundProperty, 'ControlBackgroundBrush')
    $errorDetailsPanel.SetResourceReference([System.Windows.Controls.Border]::BorderBrushProperty, 'BorderBrush')

    $detailsStack            = [System.Windows.Controls.StackPanel]::new()
    $errorDetailsPanel.Child = $detailsStack

    $expandIcon = [System.Windows.Controls.TextBlock]@{
        Text                  = [PsUi.ModuleContext]::GetIcon('ChevronRight')
        FontFamily            = [PsUi.ModuleContext]::ActiveIconFontFamily
        FontSize              = 12
        VerticalAlignment     = 'Center'
        Margin                = [System.Windows.Thickness]::new(0, 0, 8, 0)
        RenderTransformOrigin = '0.5,0.5'
    }
    # The glyph turns rather than swapping to a second one, so the two stay the same width and the label never shifts.
    $expandIcon.RenderTransform = [System.Windows.Media.RotateTransform]::new(0)
    $expandIcon.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'SecondaryTextBrush')

    $headerText = [System.Windows.Controls.TextBlock]@{
        Text              = 'Error Details'
        FontFamily        = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI')
        FontSize          = 13
        FontWeight        = 'SemiBold'
        VerticalAlignment = 'Center'
    }
    $headerText.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'ControlForegroundBrush')

    $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($expandIcon)
    [void]$headerPanel.Children.Add($headerText)
    [void]$detailsStack.Children.Add($headerPanel)

    $errorDetailsText = [System.Windows.Controls.TextBox]@{
        IsReadOnly                  = $true
        FontFamily                  = [System.Windows.Media.FontFamily]::new('Cascadia Code, Cascadia Mono, Consolas, Courier New')
        FontSize                    = 11
        TextWrapping                = 'Wrap'
        AcceptsReturn               = $true
        VerticalScrollBarVisibility = 'Auto'
        MaxHeight                   = 200
        Visibility                  = 'Collapsed'
        Margin                      = [System.Windows.Thickness]::new(12, 0, 12, 10)
        Padding                     = [System.Windows.Thickness]::new(4)
    }
    Set-TextBoxStyle -TextBox $errorDetailsText

    # After Set-TextBoxStyle, which wipes any Foreground set before it, and keyed so a theme switch recolours the text.
    $errorDetailsText.SetResourceReference([System.Windows.Controls.Control]::ForegroundProperty, 'ErrorBrush')
    [void]$detailsStack.Children.Add($errorDetailsText)

    $headerPanel.Add_MouseLeftButtonUp({
        param($sender, $eventArgs)
        trap { Write-Debug "Error details toggle: $_"; continue }
        if ($errorDetailsText.Visibility -eq 'Collapsed') {
            $errorDetailsText.Visibility      = 'Visible'
            $expandIcon.RenderTransform.Angle = 90
        }
        else {
            $errorDetailsText.Visibility      = 'Collapsed'
            $expandIcon.RenderTransform.Angle = 0
        }
    }.GetNewClosure())
    [System.Windows.Controls.Grid]::SetRow($errorDetailsPanel, 2)
    [void]$Container.Children.Add($errorDetailsPanel)

    $CopyButton.Add_Click({
        if ($ErrorsList.Count -gt 0) {
            $lines = $ErrorsList | ForEach-Object {
                "$($_.Time)`t$($_.LineNumber)`t$($_.Category)`t$($_.Message)"
            }
            $header = "Time`tLine`tCategory`tMessage"
            $allLines = @($header) + @($lines)
            [System.Windows.Clipboard]::SetText($allLines -join "`n")
            Start-UiButtonFeedback -Button $CopyButton -OriginalIconChar ([PsUi.ModuleContext]::GetIcon('Copy'))
        }
    }.GetNewClosure())

    $ExportButton.Add_Click({
        if ($ErrorsList.Count -gt 0) {
            $saveDialog = [Microsoft.Win32.SaveFileDialog]::new()
            $saveDialog.Filter     = 'CSV files (*.csv)|*.csv|All files (*.*)|*.*'
            $saveDialog.DefaultExt = '.csv'
            $saveDialog.FileName   = "errors_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"

            if ($saveDialog.ShowDialog()) {
                $ErrorsList | Select-Object Time, LineNumber, Category, Message, ScriptName, Line, FullyQualifiedErrorId |
                    Export-Csv -Path $saveDialog.FileName -NoTypeInformation
                Start-UiButtonFeedback -Button $ExportButton -OriginalIconChar ([PsUi.ModuleContext]::GetIcon('Export'))
            }
        }
    }.GetNewClosure())

    $DataGrid.add_SelectionChanged({
        param($sender, $eventArgs)
        $selected = $sender.SelectedItem
        if ($null -ne $selected) {
            $errorDetailsPanel.Visibility = 'Visible'
            $details = [System.Collections.Generic.List[string]]::new()

            # PSErrorRecord carries ToDetailedString, which beats assembling the fields by hand.
            $rawRec = $selected.RawRecord
            if ($null -ne $rawRec -and $rawRec.PSObject.Methods['ToDetailedString']) {
                $errorDetailsText.Text = $rawRec.ToDetailedString()
            }
            else {
                # Check and add each property from the error record (if present - they're pretty hit or miss)
                if ($selected.Message) { $details.Add("Message: $($selected.Message)") }
                if ($selected.LineNumber -and $selected.LineNumber -ne '') { $details.Add("Line: $($selected.LineNumber)") }
                if ($selected.ScriptName) { $details.Add("Script: $($selected.ScriptName)") }
                if ($selected.Line) { $details.Add("Code: $($selected.Line)") }
                if ($selected.Category) { $details.Add("Category: $($selected.Category)") }
                if ($selected.FullyQualifiedErrorId) { $details.Add("ErrorId: $($selected.FullyQualifiedErrorId)") }
                if ($selected.ScriptStackTrace) { $details.Add("`nStack Trace:`n$($selected.ScriptStackTrace)") }
                if ($selected.InnerException) { $details.Add("`nInner Exception: $($selected.InnerException)") }
                $errorDetailsText.Text = $details -join "`n"
            }
        }
        else {
            $errorDetailsPanel.Visibility = 'Collapsed'
        }
    }.GetNewClosure())
}