public/layout/New-UiButtonCard.ps1

function New-UiButtonCard {
    <#
    .SYNOPSIS
        Creates a button card with icon, header, description, and an action button.
    .DESCRIPTION
        Creates a styled card/groupbox containing an icon, header text, optional description,
        and an action button. The button executes asynchronously by default with full
        output streaming support. Use New-UiActionCard for the same card with -NoOutput
        baked in.
         
        Use -Action to provide an inline scriptblock, or -File to run an external
        script file. These parameters are mutually exclusive.
    .PARAMETER Header
        The card title/header text.
    .PARAMETER Description
        Optional description text shown below the header.
    .PARAMETER Icon
        Icon name from Segoe MDL2 Assets (e.g., 'Play', 'Save', 'Processing').
    .PARAMETER ButtonText
        Text shown on the action button. Defaults to 'Go'.
    .PARAMETER Action
        The scriptblock to execute when the button is clicked. Mutually exclusive with -File.
    .PARAMETER File
        Path to a script file to execute when clicked. Supports .ps1, .bat, .cmd,
        .vbs, and .exe files. Mutually exclusive with -Action.
    .PARAMETER ArgumentList
        Hashtable of arguments to pass to the script file.
    .PARAMETER Accent
        If specified, the button uses accent color styling.
    .PARAMETER FullWidth
        If specified, the card spans the full width of its container.
    .PARAMETER NoAsync
        Execute synchronously on the UI thread (blocks UI).
    .PARAMETER NoWait
        Execute async with output window, but don't block the parent window.
        Other buttons remain clickable while this action runs.
    .PARAMETER NoOutput
        Execute async but don't show output window.
    .PARAMETER HideEmptyOutput
        Show output window only when there's actual content.
    .PARAMETER ResultActions
        Hashtable array defining actions for DataGrid results.
    .PARAMETER SingleSelect
        If specified, ResultActions work with single selection.
    .PARAMETER LinkedVariables
        Variable names to capture from caller's scope.
    .PARAMETER LinkedFunctions
        Function names to capture from caller's scope.
    .PARAMETER LinkedModules
        Module paths to import in the async runspace.
    .PARAMETER Capture
        Variable names to capture from the runspace after execution completes.
        Captured variables are stored in the session and available to subsequent
        button actions via hydration, eliminating the need for Get-UiSession.
    .PARAMETER Parameters
        Hashtable of parameters to pass to the action.
    .PARAMETER Variables
        Hashtable of variables to inject into the action.
    .PARAMETER Variable
        Optional name to register the button for -SubmitButton lookups.
        When specified, inputs using -SubmitButton with this name will trigger
        the button's click event when Enter is pressed.
    .PARAMETER WPFProperties
        Hashtable of WPF properties to apply to the card container.
    .EXAMPLE
        New-UiButtonCard -Header "Get Processes" -Icon "Processing" -Action { Get-Process }
    .EXAMPLE
        New-UiButtonCard -Header "Save Data" -Description "Saves current state" -Icon "Save" -ButtonText "SAVE" -Accent -Action { Save-Data }
    .EXAMPLE
        New-UiButtonCard -Header "Run Deploy" -Icon "Deploy" -File "C:\Scripts\Deploy.ps1" -ArgumentList @{ Env = 'Prod' }
    #>

    [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
    param(
        [Parameter(Mandatory)]
        [string]$Header,

        [string]$Description,

        [string]$ButtonText = 'Go',

        [Parameter(Mandatory, ParameterSetName = 'ScriptBlock')]
        [scriptblock]$Action,

        [Parameter(Mandatory, ParameterSetName = 'File')]
        [string]$File,

        [Parameter(ParameterSetName = 'File')]
        [hashtable]$ArgumentList,

        [switch]$Accent,

        [switch]$FullWidth,

        # Action execution parameters (passed to New-UiButton)
        [switch]$NoAsync,
        [switch]$NoWait,
        [switch]$NoOutput,
        [switch]$HideEmptyOutput,
        [hashtable[]]$ResultActions,
        [switch]$SingleSelect,
        [string[]]$LinkedVariables,
        [string[]]$LinkedFunctions,
        [string[]]$LinkedModules,
        [string[]]$Capture,
        [hashtable]$Parameters,
        [hashtable]$Variables,

        [Parameter()]
        [string]$Variable,

        [Parameter()]
        [hashtable]$WPFProperties
    )

    DynamicParam {
        Get-IconDynamicParameter -ParameterName 'Icon'
    }

    begin {
        $Icon = $PSBoundParameters['Icon']
    }

    process {

    # Can't use both - pick one
    if ($NoOutput -and $HideEmptyOutput) {
        throw "Parameters -NoOutput and -HideEmptyOutput are mutually exclusive. Use only one."
    }

    $session = Assert-UiSession -CallerName 'New-UiButtonCard'
    $colors  = Get-ThemeColors
    $parent  = $session.CurrentParent
    Write-Debug "Header='$Header', Icon='$Icon', Accent=$($Accent.IsPresent), Parent: $($parent.GetType().Name)"

    $groupBox = [PsUi.ControlFactory]::CreateGroupBox($null, 0)
    $groupBox.Height = [System.Double]::NaN

    # Apply responsive constraints (function checks if parent is WrapPanel)
    Set-ResponsiveConstraints -Control $groupBox -FullWidth:$FullWidth

    $grid = [System.Windows.Controls.Grid]::new()
    $grid.MinHeight = 40

    [void]$grid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]::new())
    [void]$grid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]::new())
    [void]$grid.ColumnDefinitions.Add([System.Windows.Controls.ColumnDefinition]::new())
    $grid.ColumnDefinitions[1].Width = [System.Windows.GridLength]::new(1, 'Star')
    $grid.ColumnDefinitions[2].Width = [System.Windows.GridLength]::new(128)

    # Column 0: Icon (if specified)
    $iconText = [PsUi.ModuleContext]::GetIcon($Icon)
    if ($Icon -and $iconText) {
        $grid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(48)
        $iconBlock = [System.Windows.Controls.TextBlock]@{
            Text = $iconText
            FontFamily = [System.Windows.Media.FontFamily]::new('Segoe MDL2 Assets')
            FontSize = 24
            HorizontalAlignment = 'Center'
            VerticalAlignment = 'Center'
            Tag = 'AccentBrush'
        }
        $iconBlock.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'AccentBrush')
        if ([PsUi.ModuleContext]::IsInitialized) {
            [PsUi.ThemeEngine]::RegisterElement($iconBlock)
        }
        [void]$grid.Children.Add($iconBlock)
    }
    else {
        $grid.ColumnDefinitions[0].Width = [System.Windows.GridLength]::new(8)
    }

    # Column 1: Header and description text
    $textPanel = [System.Windows.Controls.StackPanel]@{
        VerticalAlignment = 'Center'
    }
    [System.Windows.Controls.Grid]::SetColumn($textPanel, 1)

    $headerBlock = [System.Windows.Controls.TextBlock]@{
        Text         = $Header
        FontFamily   = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI')
        FontSize     = 14
        FontWeight   = 'Medium'
        Foreground   = ConvertTo-UiBrush $colors.ControlFg
        TextWrapping = 'Wrap'
        Tag          = 'ControlFgBrush'
    }
    [PsUi.ThemeEngine]::RegisterElement($headerBlock)
    [void]$textPanel.Children.Add($headerBlock)

    if ($Description) {
        $descriptionBlock = [System.Windows.Controls.TextBlock]@{
            Text         = $Description
            FontFamily   = [System.Windows.Media.FontFamily]::new('Segoe UI Variable, Segoe UI')
            FontSize     = 12
            Foreground   = ConvertTo-UiBrush $colors.SecondaryText
            TextWrapping = 'Wrap'
            ToolTip      = $Description
            Tag          = 'SecondaryTextBrush'
        }
        [PsUi.ThemeEngine]::RegisterElement($descriptionBlock)
        [void]$textPanel.Children.Add($descriptionBlock)
    }
    [void]$grid.Children.Add($textPanel)

    # Column 2: Action button
    # Temporarily set the grid as current parent so the button gets added there
    $originalParent = $session.CurrentParent
    $session.CurrentParent = $grid

    # Build splat for New-UiButton based on parameter set
    $buttonParams = @{
        Text       = $ButtonText
        Accent     = $Accent
        Width      = 120
        Height     = 32
        GridColumn = 2
    }

    # Use either Action or File based on parameter set
    if ($PSCmdlet.ParameterSetName -eq 'File') {
        $buttonParams['File'] = $File
        if ($ArgumentList) { $buttonParams['ArgumentList'] = $ArgumentList }
    }
    else {
        $buttonParams['Action'] = $Action
    }

    # Pass through action execution parameters
    if ($NoAsync) { $buttonParams['NoAsync'] = $true }
    if ($NoWait) { $buttonParams['NoWait'] = $true }
    if ($NoOutput) { $buttonParams['NoOutput'] = $true }
    if ($HideEmptyOutput) { $buttonParams['HideEmptyOutput'] = $true }
    if ($ResultActions) { $buttonParams['ResultActions'] = $ResultActions }
    if ($SingleSelect) { $buttonParams['SingleSelect'] = $true }
    if ($LinkedVariables) { $buttonParams['LinkedVariables'] = $LinkedVariables }
    if ($LinkedFunctions) { $buttonParams['LinkedFunctions'] = $LinkedFunctions }
    if ($LinkedModules) { $buttonParams['LinkedModules'] = $LinkedModules }
    if ($Capture) { $buttonParams['Capture'] = $Capture }
    if ($Parameters) { $buttonParams['Parameters'] = $Parameters }
    if ($Variables) { $buttonParams['Variables'] = $Variables }
    if ($Variable) { $buttonParams['Variable'] = $Variable }

    # Store Header in context for output window title
    $buttonParams['OutputTitle'] = $Header

    $button = New-UiButton @buttonParams

    # Restore original parent
    $session.CurrentParent = $originalParent

    # Attach grid to groupbox and add to parent
    $groupBox.Content = $grid

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

    # Add to parent - don't return if added (to avoid pipeline output)
    Write-Debug "Adding button card '$Header' to parent"
    $addedToParent = $false
    if ($parent -is [System.Windows.Controls.Panel]) {
        [void]$parent.Children.Add($groupBox)
        $addedToParent = $true
    }
    elseif ($parent -is [System.Windows.Controls.ItemsControl]) {
        [void]$parent.Items.Add($groupBox)
        $addedToParent = $true
    }
    elseif ($parent -is [System.Windows.Controls.ContentControl]) {
        $parent.Content = $groupBox
        $addedToParent = $true
    }

    if (Get-Command New-UniqueControlName -ErrorAction SilentlyContinue) {
        Register-UiControl -Name (New-UniqueControlName -Prefix 'ButtonCard') -Control $groupBox
    }

    # Only return if not added to parent (for manual layout scenarios)
    if (!$addedToParent) {
        return $groupBox
    }
    }
}