public/output/Out-CSVDataGrid.ps1

function Out-CSVDataGrid {
    <#
    .SYNOPSIS
        Opens CSV files for viewing and editing. Save writes back to disk.
    .DESCRIPTION
        A bulk CSV editor. Point it at one or more CSVs and you get a window that
        opens, sorts, filters, and edits them. Saves go back to the file they came
        from, or to a new path if you use Save As.
 
        Internals use $script: scoped state, so opening two CSV grids from the same
        session at the same time can step on itself. Modal dialogs keep this from
        happening in normal use. Avoid calling this in parallel runspaces sharing the
        module.
    .PARAMETER CSVDirectory
        Folder to load every .csv file from. Top level only, no recursion - pipe
        Get-ChildItem -Recurse output to -CSVFiles for that.
    .PARAMETER CSVFiles
        One or more CSV file paths. Also takes pipeline input from Get-ChildItem.
    .PARAMETER TitleText
        Window title.
    .PARAMETER IsFilterable
        Kept for v2.x call sites. The filter box is always on now, so this switch
        changes nothing.
    .PARAMETER IsResizeable
        Adds the corner resize grip. The window resizes either way - kept for v2.x
        call sites.
    .PARAMETER ColumnsToPopupOnSelection
        Column names that pop a separate viewer when clicked. Use for long values
        you can't easily edit inline.
    .PARAMETER ColumnComboBoxes
        Columns you want edited via a dropdown instead of a textbox. Key is the
        column name, value is either an array of allowed values or a hashtable
        @{ Values = @(...); DefaultValue = '...' }.
 
        Example:
            @{
                Status = @('Active', 'Inactive')
                Priority = @{ Values = @('Low', 'High'); DefaultValue = 'Low' }
            }
 
        A column can't be in both this and ColumnsToPopupOnSelection - that throws at startup.
    .PARAMETER ReadOnlyColumns
        Columns that can't be edited. If a column is also in ColumnsToPopupOnSelection,
        the popup viewer goes read-only too.
    .PARAMETER ForceTextWrap
        Wrap text inside cells instead of clipping it.
    .PARAMETER Width
        Window width in pixels (400-2000). Defaults to 1000.
    .PARAMETER Height
        Window height in pixels (300-1500). Defaults to 700.
    .PARAMETER Theme
        Color theme. See Get-UiThemeTemplate for the list. When not given, follows
        the session's active theme if one is loaded, otherwise Light.
    .PARAMETER Delimiter
        Column separator character. Defaults to comma. Use ';' for semicolon delimited
        files or "`t" for tab-delimited files.
    .PARAMETER NoHeader
        Treat the first row as data, not headers. Columns get named Column1..ColumnN.
    .PARAMETER IconFont
        Which icon font to use for the toolbar: Inherit (default), Auto, SegoeMDL2 (Win10),
        or SegoeFluentIcons (Win11). Only matters when this is the top-level window. When
        hosted inside another PsUi window, that window's font wins.
    .PARAMETER NoIconFontFallback
        Don't borrow glyphs from the other Segoe icon font when the active one is
        missing them. Absent toolbar icons render as empty boxes instead.
    .EXAMPLE
        Out-CSVDataGrid -CSVDirectory 'C:\Data' -IsFilterable -IsResizeable
    .EXAMPLE
        Get-ChildItem C:\Logs -Filter *.csv | Out-CSVDataGrid -IsFilterable
    .EXAMPLE
        Out-CSVDataGrid -CSVFiles 'inventory.csv' -ColumnComboBoxes @{
            Status = @('Active', 'Inactive', 'Pending')
            Priority = @('Low', 'Medium', 'High', 'Critical')
        }
    #>

    [CmdletBinding(DefaultParameterSetName = 'Directory')]
    param(
        [Parameter(ParameterSetName = 'Directory')]
        [string]$CSVDirectory,

        [Parameter(ParameterSetName = 'Files', ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('FullName')]
        [string[]]$CSVFiles,

        [Alias('Title')]
        [string]$TitleText = 'CSV Editor',

        [switch]$IsFilterable,

        [switch]$IsResizeable,

        [string[]]$ColumnsToPopupOnSelection,

        [Parameter()]
        [hashtable]$ColumnComboBoxes,

        [Parameter()]
        [string[]]$ReadOnlyColumns,

        [switch]$ForceTextWrap,

        [ValidateRange(400, 2000)]
        [int]$Width = 1000,

        [ValidateRange(300, 1500)]
        [int]$Height = 700,

        [ArgumentCompleter({ [PsUi.ThemeEngine]::GetAvailableThemes() })]
        [string]$Theme = 'Light',

        [char]$Delimiter = ',',

        [switch]$NoHeader,

        # Default 'Inherit' (rather than leaving unbound) so $IconFont always satisfies the ValidateSet. Otherwise any .GetNewClosure() inside the function would explode trying to carry an unbound "" value through the attribute.
        [ValidateSet('Inherit', 'Auto', 'SegoeMDL2', 'SegoeFluentIcons')]
        [string]$IconFont = 'Inherit',

        [switch]$NoIconFontFallback
    )


    begin {
        # Out-CSVDataGrid calls ShowDialog on the host thread, so an async button action (already on a worker thread) deadlocks waiting on the UI thread.
        # Block early until the file list path is rebuilt on top of New-UiWindow the way Out-Datagrid is.
        if ([PsUi.AsyncExecutor]::CurrentExecutor) {
            Write-Error 'Out-CSVDataGrid cannot be called from an async button action (ShowDialog requires the UI thread). Use -NoAsync on the button, or call Out-Datagrid instead if you only need to view tabular data.'
            return
        }

        Write-Debug "Starting with Title='$TitleText', Theme='$Theme', Delimiter='$Delimiter'"
        $allFiles = [System.Collections.Generic.List[string]]::new()

        function Show-ThemedDialog {
            param(
                [string]$Title,
                [string]$Message,
                [string]$Buttons = 'OK',
                [string]$Icon = 'Info'
            )
            # $colors gets hydrated into this scope by the async runspace - no need to resolve it again.
            Show-UiMessageDialog -Title $Title -Message $Message -Buttons $Buttons -Icon $Icon -ThemeColors $colors
        }

        function Get-DataGridEditStyle {
            $editStyle = [System.Windows.Style]::new([System.Windows.Controls.TextBox])

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::BackgroundProperty, (ConvertTo-UiBrush $colors.ControlBg)))
            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::ForegroundProperty, (ConvertTo-UiBrush $colors.ControlFg)))
            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::BorderBrushProperty, (ConvertTo-UiBrush $colors.Accent)))

            # No border - keeps the row height stable when editing starts.
            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::BorderThicknessProperty, [System.Windows.Thickness]::new(0)))
            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::PaddingProperty, [System.Windows.Thickness]::new(2, 0, 2, 0)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::MarginProperty, [System.Windows.Thickness]::new(0)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.TextBox]::VerticalContentAlignmentProperty, [System.Windows.VerticalAlignment]::Center))

            return $editStyle
        }

        function Get-DataGridComboBoxEditStyle {
            $editStyle = [System.Windows.Style]::new([System.Windows.Controls.ComboBox])

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::BackgroundProperty, (ConvertTo-UiBrush $colors.ControlBg)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::ForegroundProperty, (ConvertTo-UiBrush $colors.ControlFg)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::BorderBrushProperty, (ConvertTo-UiBrush $colors.Accent)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::BorderThicknessProperty, [System.Windows.Thickness]::new(0)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::PaddingProperty, [System.Windows.Thickness]::new(2, 0, 2, 0)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::MarginProperty, [System.Windows.Thickness]::new(0)))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Stretch))

            [void]$editStyle.Setters.Add([System.Windows.Setter]::new([System.Windows.Controls.ComboBox]::VerticalContentAlignmentProperty, [System.Windows.VerticalAlignment]::Center))

            return $editStyle
        }
    }

    process {
        if ($CSVFiles) {
            foreach ($file in $CSVFiles) {
                if (Test-Path $file -PathType Leaf) {
                    [void]$allFiles.Add($file)
                }
            }
        }
    }

    end {
        # Outermost net: stray NullRefs from close time handlers otherwise ride up to the parent button -Action catch and pop an "Error: Open Out-CSVDataGrid" dialog on the way out. Real errors hit the inner try/catch sites.
        try {
            $script:filterTimer = $null
            $windowShown = $false

            # A column can't be both a ComboBox and a PopupOnSelection - that's nonsense
            if ($ColumnComboBoxes -and $ColumnsToPopupOnSelection) {
                Write-Debug "Checking for ComboBox/PopupOnSelection overlap"
                $overlap = $ColumnComboBoxes.Keys | Where-Object { $_ -in $ColumnsToPopupOnSelection }
                if ($overlap) {
                    Write-Debug "Found overlapping columns: $($overlap -join ', ')"
                    Write-Warning "Columns cannot use both ComboBox and PopupOnSelection: $($overlap -join ', ')"
                    throw "Parameter conflict: Remove overlapping columns from either ColumnComboBoxes or ColumnsToPopupOnSelection"
                }
            }

            if ($CSVDirectory -and (Test-Path $CSVDirectory -PathType Container)) {
                Get-ChildItem $CSVDirectory -Filter '*.csv' -File | ForEach-Object {
                    [void]$allFiles.Add($_.FullName)
                }
            }

            if ($allFiles.Count -eq 0) {
                Write-Debug "No CSV files found in provided paths"
                Write-Warning 'No CSV files found'
                return
            }

            Write-Debug "Found $($allFiles.Count) CSV file(s) to load"

            $isStandalone = !(Test-Path variable:__WPFThemeColors)

            # __WPFThemeColors isn't injected into a -NoAsync button action, so $isStandalone reads $true even for a nested launch from a live window. A session already current at entry means a parent owns it - capture that so the close handler never disposes someone else's session.
            $hasParentSession = [PsUi.SessionManager]::CurrentSessionId -ne [System.Guid]::Empty

            # RowContextMenu actions invoke without __WPFThemeColors in scope - fall back to ActiveTheme, otherwise the default 'Light' below flips the parent window via Initialize-UITheme.
            if (!$PSBoundParameters.ContainsKey('Theme')) {
                $active = [PsUi.ModuleContext]::ActiveTheme
                if (![string]::IsNullOrWhiteSpace($active)) { $Theme = $active }
            }

            if ($isStandalone) {
                # Create the Application on this (ShowDialog) thread before Initialize-UITheme - ThemeEngine no longer creates it, and a cross thread App loses content theming. Does nothing if one exists.
                [void][PsUi.ThemeEngine]::EnsureApplication()
                $colors = Initialize-UITheme -Theme $Theme
            }
            else {
                $colors = Get-Variable -Name __WPFThemeColors -ValueOnly -ErrorAction SilentlyContinue
            }

            # Last resort fallback if the lookups above all came back empty.
            if (!$colors) {
                $colors = Initialize-UITheme -Theme $Theme
            }

            # Push -IconFont override when standalone. Returns $null when inside a parent (parent's font wins) or when no override params were supplied. Restore runs after ShowDialog on the success path. The trap below covers throws between here and there.
            $overrideParams = @{
                IsStandalone       = $isStandalone
                BoundParameters    = $PSBoundParameters
                IconFont           = $IconFont
                NoIconFontFallback = [bool]$NoIconFontFallback
            }
            $iconFontSnap = Push-UiIconFontOverride @overrideParams

            # A throw between the push and the post ShowDialog restore would leak the override.
            # trap catches it at function scope, restores, then break rethrows.
            trap {
                if ($iconFontSnap) {
                    [PsUi.ModuleContext]::RestoreIconFontState($iconFontSnap)
                    $iconFontSnap = $null
                }
                break
            }

            $Script:CurrentCSVData = @{}
            $Script:HasChanges = $false

            foreach ($filePath in $allFiles) {
                $baseName = [System.IO.Path]::GetFileName($filePath)
                try {
                    $importParams = @{
                        Path      = $filePath
                        Delimiter = $Delimiter
                    }
                    if ($NoHeader) {
                        # No header row to read from - make up column names from the column count.
                        $firstLine = Get-Content $filePath -First 1
                        $columnCount = ($firstLine -split [regex]::Escape($Delimiter)).Count
                        $headers = 1..$columnCount | ForEach-Object { "Column$_" }
                        $importParams['Header'] = $headers
                    }
                    $csvData = Import-Csv @importParams

                    # Two files sharing a basename (e.g. -Recurse over folders each holding config.csv) collapsed to one key - the earlier vanished from the combo and Save misfired to the survivor's path. Tag the parent folder on collision so each file keeps its own entry and Path. The key is opaque: it's the combo label and the save lookup key, nothing rebuilds it from the path.
                    $fileName = $baseName
                    if ($Script:CurrentCSVData.ContainsKey($fileName)) {
                        $parentDir = Split-Path -Leaf (Split-Path -Parent $filePath)
                        $fileName  = '{0} ({1})' -f $baseName, $parentDir
                        $dupIndex  = 2
                        while ($Script:CurrentCSVData.ContainsKey($fileName)) {
                            $fileName = '{0} ({1} {2})' -f $baseName, $parentDir, $dupIndex
                            $dupIndex++
                        }
                    }
                    $Script:CurrentCSVData[$fileName] = @{
                        Path      = $filePath
                        Data      = [System.Collections.ArrayList]@($csvData)
                        Modified  = $false
                        Delimiter = $Delimiter
                    }
                }
                catch {
                    Write-Warning "Failed to load $baseName`: $_"
                }
            }

            if ($Script:CurrentCSVData.Count -eq 0) {
                Write-Warning 'No valid CSV files loaded'
                return
            }

            $window = [System.Windows.Window]@{
                Title                 = $TitleText
                Width                 = $Width
                Height                = $Height
                MinWidth              = 400
                MinHeight             = 300
                WindowStartupLocation = 'CenterScreen'
                FontFamily            = [System.Windows.Media.FontFamily]::new('Segoe UI')
                ResizeMode            = if ($IsResizeable) { 'CanResizeWithGrip' } else { 'CanResize' }
            }

            # Owner chain so an Alt+Tab back to the parent window brings this one with it.
            $null = Set-WindowOwner -Window $window
            $window.SetResourceReference([System.Windows.Window]::BackgroundProperty, 'WindowBackgroundBrush')
            $window.SetResourceReference([System.Windows.Window]::ForegroundProperty, 'ControlForegroundBrush')

            Set-UIResources -Window $window -Colors $colors

            $appId = "PsUi.CSVEditor." + [Guid]::NewGuid().ToString("N").Substring(0, 8)
            [PsUi.WindowManager]::SetWindowAppId($window, $appId)

            $csvWindowIcon = $null
            try {
                $csvWindowIcon = New-WindowIcon -Colors $colors
                if ($csvWindowIcon) { window.Icon = $csvWindowIcon   }
            }
            catch {
                Write-Verbose "Failed to create window icon: $_"
            }

            $overlayIcon = $null
            try {
                $csvGlyph = [PsUi.ModuleContext]::GetIcon('Document')
                $overlayIcon = New-TaskbarOverlayIcon -GlyphChar $csvGlyph -Color $colors.Accent
                # Stash the glyph in Resources so the theme handler can redraw it on theme switch.
                $window.Resources['OverlayGlyph'] = $csvGlyph
            }
            catch { Write-Debug "Taskbar overlay failed: $_" }

            $capturedCsvWindow = $window
            $capturedCsvIcon   = $csvWindowIcon
            $capturedOverlay   = $overlayIcon

            $window.Add_Loaded({
                if ($capturedCsvIcon) { [PsUi.WindowManager]::SetTaskbarIcon($capturedCsvWindow, $capturedCsvIcon) }
                if ($capturedOverlay) {    [PsUi.WindowManager]::SetTaskbarOverlay($capturedCsvWindow, $capturedOverlay, 'CSV')  }
            }.GetNewClosure())

            $mainPanel = [System.Windows.Controls.DockPanel]@{
                LastChildFill = $true
            }
            $window.Content = $mainPanel

            # Header bar
            $headerBorder = [System.Windows.Controls.Border]@{
                Padding    = [System.Windows.Thickness]::new(16, 12, 16, 12)
                Tag        = 'HeaderBorder'
            }
            $headerBorder.SetResourceReference([System.Windows.Controls.Border]::BackgroundProperty, 'HeaderBackgroundBrush')
            [System.Windows.Controls.DockPanel]::SetDock($headerBorder, 'Top')

            $headerGrid = [System.Windows.Controls.Grid]::new()
            $col1 = [System.Windows.Controls.ColumnDefinition]@{  Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) }
            $col2 = [System.Windows.Controls.ColumnDefinition]@{  Width = [System.Windows.GridLength]::Auto }
            [void]$headerGrid.ColumnDefinitions.Add($col1)
            [void]$headerGrid.ColumnDefinitions.Add($col2)

            $headerStack = [System.Windows.Controls.StackPanel]@{ Orientation = 'Horizontal' }
            [System.Windows.Controls.Grid]::SetColumn($headerStack, 0)

            $headerIcon = [System.Windows.Controls.TextBlock]@{
                Text              = [PsUi.ModuleContext]::GetIcon('Document')
                FontFamily        = [PsUi.ModuleContext]::ActiveIconFontFamily
                FontSize          = 24
                VerticalAlignment = 'Center'
                Width             = 32
                TextAlignment     = 'Center'
                Margin            = [System.Windows.Thickness]::new(0, 0, 12, 0)
                Tag               = 'HeaderText'
            }
            $headerIcon.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
            [void]$headerStack.Children.Add($headerIcon)

            $headerTitle = [System.Windows.Controls.TextBlock]@{
                Text              = $TitleText
                FontSize          = 18
                FontWeight        = [System.Windows.FontWeights]::SemiBold
                VerticalAlignment = 'Center'
                Tag               = 'HeaderText'
            }
            $headerTitle.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'HeaderForegroundBrush')
           
            [void]$headerStack.Children.Add($headerTitle)
            [void]$headerGrid.Children.Add($headerStack)

            # Add theme button to header but only when running standalone
            if ($isStandalone) {
                $themeButtonData = New-ThemePopupButton -Container $window -CurrentTheme $Theme
                [System.Windows.Controls.Grid]::SetColumn($themeButtonData.Button, 1)
                [void]$headerGrid.Children.Add($themeButtonData.Button)
            }

            $headerBorder.Child = $headerGrid
            [void]$mainPanel.Children.Add($headerBorder)

            $contentPanel = [System.Windows.Controls.DockPanel]@{
                Margin        = [System.Windows.Thickness]::new(12)
                LastChildFill = $true
            }
            [void]$mainPanel.Children.Add($contentPanel)

            $toolbar = [System.Windows.Controls.Grid]@{   Margin = [System.Windows.Thickness]::new(0, 0, 0, 8) }
            [System.Windows.Controls.DockPanel]::SetDock($toolbar, 'Top')

            $col1 = [System.Windows.Controls.ColumnDefinition]@{  Width = [System.Windows.GridLength]::new(1, [System.Windows.GridUnitType]::Star) }
            $col2 = [System.Windows.Controls.ColumnDefinition]@{ Width = [System.Windows.GridLength]::Auto }
            [void]$toolbar.ColumnDefinitions.Add($col1)
            [void]$toolbar.ColumnDefinitions.Add($col2)

            $leftPanel = [System.Windows.Controls.StackPanel]@{  Orientation = 'Horizontal' }
            [System.Windows.Controls.Grid]::SetColumn($leftPanel, 0)

            $fileLabel = [System.Windows.Controls.TextBlock]@{
                Text              = 'File:'
                VerticalAlignment = 'Center'
                Margin            = [System.Windows.Thickness]::new(0, 0, 8, 0)
            }

            $fileLabel.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'ControlForegroundBrush')
            [void]$leftPanel.Children.Add($fileLabel)

            $fileCombo = [System.Windows.Controls.ComboBox]::new()
            $fileCombo.Width = 200
            $fileCombo.Margin = [System.Windows.Thickness]::new(0, 0, 16, 0)
            
            foreach ($fileName in $Script:CurrentCSVData.Keys) {  [void]$fileCombo.Items.Add($fileName) }
            
            $fileCombo.SelectedIndex = 0
            
            Set-ComboBoxStyle -ComboBox $fileCombo
            
            [void]$leftPanel.Children.Add($fileCombo)
            [void]$toolbar.Children.Add($leftPanel)

            $rightPanel = [System.Windows.Controls.StackPanel]::new()
            $rightPanel.Orientation = 'Horizontal'
            $rightPanel.HorizontalAlignment = 'Right'
           
            [System.Windows.Controls.Grid]::SetColumn($rightPanel, 1)

            $filterLabel = [System.Windows.Controls.TextBlock]::new()
            $filterLabel.Text = 'Filter:'
            $filterLabel.VerticalAlignment = 'Center'
            $filterLabel.Margin = [System.Windows.Thickness]::new(0, 0, 8, 0)
            $filterLabel.SetResourceReference([System.Windows.Controls.TextBlock]::ForegroundProperty, 'ControlForegroundBrush')
            [void]$rightPanel.Children.Add($filterLabel)

            $filterBoxContainer = [System.Windows.Controls.Grid]::new()
            $filterBoxContainer.Width = 200
            $filterBoxContainer.Height = 26
            $filterBoxContainer.VerticalAlignment = 'Center'

            $filterBox = [System.Windows.Controls.TextBox]::new()
            $filterBox.Height = 26
            $filterBox.Padding = [System.Windows.Thickness]::new(4, 0, 20, 0)
            Set-TextBoxStyle -TextBox $filterBox
            [void]$filterBoxContainer.Children.Add($filterBox)

            # Clear button pinned to the right edge of the filter box.
            $filterClearBtn = [System.Windows.Controls.Button]::new()
            $filterClearBtn.Content = [PsUi.ModuleContext]::GetIcon('Cancel')
            $filterClearBtn.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $filterClearBtn.FontSize = 10
            $filterClearBtn.Width = 16
            $filterClearBtn.Height = 16
            $filterClearBtn.Padding = [System.Windows.Thickness]::new(0)
            $filterClearBtn.Margin = [System.Windows.Thickness]::new(0, 0, 5, 0)
            $filterClearBtn.HorizontalAlignment = 'Right'
            $filterClearBtn.VerticalAlignment = 'Center'
            $filterClearBtn.Background = [System.Windows.Media.Brushes]::Transparent
            $filterClearBtn.BorderThickness = [System.Windows.Thickness]::new(0)
            $filterClearBtn.SetResourceReference([System.Windows.Controls.Button]::ForegroundProperty, 'SecondaryTextBrush')
            $filterClearBtn.Cursor = [System.Windows.Input.Cursors]::Hand
            $filterClearBtn.Visibility = 'Collapsed'
            $filterClearBtn.ToolTip = 'Clear'
            $filterClearBtn.Tag = $filterBox
            $filterClearBtn.Add_Click({ $this.Tag.Text = ''; $this.Tag.Focus() }.GetNewClosure())
            [void]$filterBoxContainer.Children.Add($filterClearBtn)

            $filterBox.Tag = @{ ClearButton = $filterClearBtn }
            [void]$rightPanel.Children.Add($filterBoxContainer)

            [void]$toolbar.Children.Add($rightPanel)
            [void]$contentPanel.Children.Add($toolbar)

            $rowToolbar = [System.Windows.Controls.StackPanel]::new()
            $rowToolbar.Orientation = 'Horizontal'
            $rowToolbar.Margin = [System.Windows.Thickness]::new(0, 0, 0, 8)
            [System.Windows.Controls.DockPanel]::SetDock($rowToolbar, 'Top')
            [void]$contentPanel.Children.Add($rowToolbar)

            $addRowBtn = [System.Windows.Controls.Button]::new()
            $addRowBtn.Width = 36
            $addRowBtn.Height = 28
            $addRowBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $addRowBtn.ToolTip = 'Add new row'
            $addRowBtn.Padding = [System.Windows.Thickness]::new(0)

            $addRowIcon = [System.Windows.Controls.TextBlock]::new()
            $addRowIcon.Text = [PsUi.ModuleContext]::GetIcon('Add')
            $addRowIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $addRowIcon.FontSize = 14
            $addRowIcon.HorizontalAlignment = 'Center'
            $addRowIcon.VerticalAlignment = 'Center'
            $addRowBtn.Content = $addRowIcon

            Set-ButtonStyle -Button $addRowBtn
            [void]$rowToolbar.Children.Add($addRowBtn)

            $deleteRowBtn = [System.Windows.Controls.Button]::new()
            $deleteRowBtn.Width = 36
            $deleteRowBtn.Height = 28
            $deleteRowBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $deleteRowBtn.ToolTip = 'Delete selected row(s)'
            $deleteRowBtn.Padding = [System.Windows.Thickness]::new(0)

            $deleteRowIcon = [System.Windows.Controls.TextBlock]::new()
            $deleteRowIcon.Text = [PsUi.ModuleContext]::GetIcon('Delete')
            $deleteRowIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $deleteRowIcon.FontSize = 14
            $deleteRowIcon.HorizontalAlignment = 'Center'
            $deleteRowIcon.VerticalAlignment = 'Center'
            $deleteRowBtn.Content = $deleteRowIcon

            Set-ButtonStyle -Button $deleteRowBtn
            [void]$rowToolbar.Children.Add($deleteRowBtn)

            $copyRowBtn = [System.Windows.Controls.Button]::new()
            $copyRowBtn.Width = 36
            $copyRowBtn.Height = 28
            $copyRowBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $copyRowBtn.ToolTip = 'Duplicate selected row'
            $copyRowBtn.Padding = [System.Windows.Thickness]::new(0)

            $copyRowIcon = [System.Windows.Controls.TextBlock]::new()
            $copyRowIcon.Text = [PsUi.ModuleContext]::GetIcon('Copy')
            $copyRowIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $copyRowIcon.FontSize = 14
            $copyRowIcon.HorizontalAlignment = 'Center'
            $copyRowIcon.VerticalAlignment = 'Center'
            $copyRowBtn.Content = $copyRowIcon

            Set-ButtonStyle -Button $copyRowBtn
            [void]$rowToolbar.Children.Add($copyRowBtn)

            $saveBtn = [System.Windows.Controls.Button]::new()
            $saveBtn.Width = 36
            $saveBtn.Height = 28
            $saveBtn.Margin = [System.Windows.Thickness]::new(8, 0, 4, 0)
            $saveBtn.ToolTip = 'Save (overwrite original)'
            $saveBtn.Padding = [System.Windows.Thickness]::new(0)

            $saveIcon = [System.Windows.Controls.TextBlock]::new()
            $saveIcon.Text = [PsUi.ModuleContext]::GetIcon('Save')
            $saveIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $saveIcon.FontSize = 14
            $saveIcon.HorizontalAlignment = 'Center'
            $saveIcon.VerticalAlignment = 'Center'
            $saveBtn.Content = $saveIcon

            Set-ButtonStyle -Button $saveBtn
            [void]$rowToolbar.Children.Add($saveBtn)

            $saveAllBtn = [System.Windows.Controls.Button]::new()
            $saveAllBtn.Width = 36
            $saveAllBtn.Height = 28
            $saveAllBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $saveAllBtn.ToolTip = 'Save All files'
            $saveAllBtn.Padding = [System.Windows.Thickness]::new(0)

            $saveAllIcon = [System.Windows.Controls.TextBlock]::new()
            $saveAllIcon.Text = [PsUi.ModuleContext]::GetIcon('SaveLocal')
            $saveAllIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $saveAllIcon.FontSize = 14
            $saveAllIcon.HorizontalAlignment = 'Center'
            $saveAllIcon.VerticalAlignment = 'Center'
            $saveAllBtn.Content = $saveAllIcon

            Set-ButtonStyle -Button $saveAllBtn
            [void]$rowToolbar.Children.Add($saveAllBtn)

            $saveAsBtn = [System.Windows.Controls.Button]::new()
            $saveAsBtn.Width = 36
            $saveAsBtn.Height = 28
            $saveAsBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $saveAsBtn.ToolTip = 'Save As (export to new file)'
            $saveAsBtn.Padding = [System.Windows.Thickness]::new(0)

            $saveAsIcon = [System.Windows.Controls.TextBlock]::new()
            $saveAsIcon.Text = [PsUi.ModuleContext]::GetIcon('SaveAs')
            $saveAsIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $saveAsIcon.FontSize = 14
            $saveAsIcon.HorizontalAlignment = 'Center'
            $saveAsIcon.VerticalAlignment = 'Center'
            $saveAsBtn.Content = $saveAsIcon

            Set-ButtonStyle -Button $saveAsBtn
            [void]$rowToolbar.Children.Add($saveAsBtn)

            $typeInfoBtn = [System.Windows.Controls.Button]::new()
            $typeInfoBtn.Width = 36
            $typeInfoBtn.Height = 28
            $typeInfoBtn.Margin = [System.Windows.Thickness]::new(0, 0, 4, 0)
            $typeInfoBtn.ToolTip = 'Show column type information'
            $typeInfoBtn.Padding = [System.Windows.Thickness]::new(0)

            $typeInfoIcon = [System.Windows.Controls.TextBlock]::new()
            $typeInfoIcon.Text = [PsUi.ModuleContext]::GetIcon('Info')
            $typeInfoIcon.FontFamily = [PsUi.ModuleContext]::ActiveIconFontFamily
            $typeInfoIcon.FontSize = 14
            $typeInfoIcon.HorizontalAlignment = 'Center'
            $typeInfoIcon.VerticalAlignment = 'Center'
            $typeInfoBtn.Content = $typeInfoIcon

            Set-ButtonStyle -Button $typeInfoBtn
            [void]$rowToolbar.Children.Add($typeInfoBtn)

            $dataGrid = [System.Windows.Controls.DataGrid]::new()
            Set-DataGridStyle -Grid $dataGrid -SelectionMode Extended
            $dataGrid.EnableRowVirtualization = $true
            $dataGrid.EnableColumnVirtualization = $false  # Keep false for ComboBoxes
            $dataGrid.CanUserAddRows = $false
            $dataGrid.IsReadOnly = $false
            [void]$contentPanel.Children.Add($dataGrid)

            # Striping survives both themes. One shot - $loadCSVFile reentry mustn't stack handlers.
            Add-UiDataGridAlternatingBrush -DataGrid $dataGrid

            # Store unfiltered data separately - filtering will rebuild the observable collection
            $script:unfilteredData = $null
            $script:currentFilterText = ''


            # Columns in -ColumnsToPopupOnSelection get an Out-TextEditor on edit instead of inline.
            $dataGrid.Add_BeginningEdit({
                    param($sender, $eventArgs)

                    if ($ColumnsToPopupOnSelection -and $ColumnsToPopupOnSelection.Count -gt 0) {
                        $columnHeader = $eventArgs.Column.Header

                        if ($columnHeader -in $ColumnsToPopupOnSelection) {
                            $eventArgs.Cancel = $true

                            $isColumnReadOnly = $ReadOnlyColumns -and ($columnHeader -in $ReadOnlyColumns)

                            $row = $eventArgs.Row.Item
                            $currentValue = $row.$columnHeader

                            $titlePrefix = if ($isColumnReadOnly) { 'View' } else { 'Edit' }
                            $result = Out-TextEditor -InitialText $currentValue -TitleText "${titlePrefix}: $columnHeader" -ReadOnly:$isColumnReadOnly

                            if (!$isColumnReadOnly -and $null -ne $result) {
                                $row.$columnHeader = $result

                                if ($script:currentFileName) {
                                    $Script:CurrentCSVData[$script:currentFileName].Modified = $true
                                }

                                # PSCustomObject doesn't notify on property change. Refresh + UpdateLayout + a selection bounce is the smallest combo that reliably redraws the cell.
                                if ($script:collectionView) { $script:collectionView.Refresh() }
                                $dataGrid.Items.Refresh()
                                $dataGrid.UpdateLayout()

                                $tempSelected = $dataGrid.SelectedItem
                                $dataGrid.SelectedItem = $null
                                $dataGrid.Dispatcher.Invoke([Action] {}, [System.Windows.Threading.DispatcherPriority]::Render)
                                $dataGrid.SelectedItem = $tempSelected
                                $dataGrid.ScrollIntoView($row)
                            }
                        }
                    }
                }.GetNewClosure())

            $null = New-DataGridContextMenu -DataGrid $dataGrid

            $script:currentObservable = $null
            $script:currentFileName = $null

            $loadCSVFile = {
                param($fileName)

                $script:currentFileName = $fileName
                $csvInfo = $Script:CurrentCSVData[$fileName]
                $data = $csvInfo.Data

                $dataGrid.Columns.Clear()

                if ($data.Count -gt 0) {
                    $propNames = $data[0].PSObject.Properties.Name

                    foreach ($propName in $propNames) {
                        $isInPopupList = $ColumnsToPopupOnSelection -and ($propName -in $ColumnsToPopupOnSelection)
                        $isReadOnly = $ReadOnlyColumns -and ($propName -in $ReadOnlyColumns) -and !$isInPopupList

                        if ($ColumnComboBoxes -and $ColumnComboBoxes.ContainsKey($propName)) {
                            $col = [System.Windows.Controls.DataGridComboBoxColumn]::new()
                            $col.Header = $propName
                            $col.SortMemberPath = $propName
                            $col.IsReadOnly = $isReadOnly
                            $col.SelectedItemBinding = [System.Windows.Data.Binding]::new($propName)
                            $col.SelectedItemBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
                            $col.SelectedItemBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged

                            $allowedValues = if ($ColumnComboBoxes[$propName] -is [array]) {
                                $ColumnComboBoxes[$propName]
                            }
                            else {
                                $ColumnComboBoxes[$propName].Values
                            }

                            $existingValues = $data | Select-Object -ExpandProperty $propName -Unique -ErrorAction SilentlyContinue
                            $allValues = [System.Collections.Generic.List[object]]::new()
                            foreach ($val in $allowedValues) {
                                [void]$allValues.Add($val)
                            }
                            foreach ($val in $existingValues) {
                                if ($val -and $val -notin $allValues) {
                                    [void]$allValues.Add($val)
                                }
                            }

                            $col.ItemsSource = $allValues

                            $tempCombo = [System.Windows.Controls.ComboBox]::new()
                            Set-ComboBoxStyle -ComboBox $tempCombo
                            $col.ElementStyle = $tempCombo.Style
                            $col.EditingElementStyle = Get-DataGridComboBoxEditStyle

                            if ($ForceTextWrap) {
                                $col.Width = [System.Windows.Controls.DataGridLength]::new(150)
                            }
                            else {
                                $col.Width = [System.Windows.Controls.DataGridLength]::new(1, [System.Windows.Controls.DataGridLengthUnitType]::Star)
                            }

                            [void]$dataGrid.Columns.Add($col)
                        }
                        else {
                            $col = [System.Windows.Controls.DataGridTextColumn]::new()
                            $col.Header = $propName
                            $col.SortMemberPath = $propName
                            $col.IsReadOnly = $isReadOnly
                            $col.Binding = [System.Windows.Data.Binding]::new($propName)
                            $col.Binding.Mode = [System.Windows.Data.BindingMode]::TwoWay
                            $col.Binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::LostFocus

                            $col.EditingElementStyle = Get-DataGridEditStyle

                            if ($ForceTextWrap) {
                                $col.Width = [System.Windows.Controls.DataGridLength]::new(150)
                            }
                            else {
                                $col.Width = [System.Windows.Controls.DataGridLength]::new(1, [System.Windows.Controls.DataGridLengthUnitType]::Star)
                            }

                            [void]$dataGrid.Columns.Add($col)
                        }
                    }

                    $script:currentObservable = [System.Collections.ObjectModel.ObservableCollection[object]]::new()
                    foreach ($item in $data) {
                        [void]$script:currentObservable.Add($item)
                    }

                    $script:unfilteredData = [System.Collections.Generic.List[object]]::new()
                    foreach ($item in $data) {
                        [void]$script:unfilteredData.Add($item)
                    }

                    $script:collectionView = [System.Windows.Data.CollectionViewSource]::GetDefaultView($script:currentObservable)
                    $dataGrid.ItemsSource = $script:collectionView

                    # Decorate empty cells. Swap bool text columns for glyphs. Both helpers skip already processed columns so file switches don't double up.
                    Add-UiDataGridEmptyCellDecorator -DataGrid $dataGrid -Items $data
                    Convert-UiDataGridBoolColumnsToGlyph -DataGrid $dataGrid -Items $data

                    $script:currentFilterText = ''
                }
                else {
                    $dataGrid.ItemsSource = $null
                    $script:currentObservable = $null
                }
            }

            $firstFile = @($Script:CurrentCSVData.Keys)[0]
            & $loadCSVFile $firstFile

            $fileCombo.Add_SelectionChanged({
                    $selectedFile = $this.SelectedItem
                    if ($selectedFile) {
                        & $loadCSVFile $selectedFile
                    }
                }.GetNewClosure())

            $filterBox.Add_TextChanged({
                $clearBtn = $filterBox.Tag.ClearButton
                if ($clearBtn) {
                    $clearBtn.Visibility = if ([string]::IsNullOrEmpty($filterBox.Text)) { 'Collapsed' } else { 'Visible' }
                }

                if ($script:filterTimer) {
                    $script:filterTimer.Stop()
                    $script:filterTimer = $null
                }

                $script:filterTimer = [System.Windows.Threading.DispatcherTimer]::new()
                $script:filterTimer.Interval = [TimeSpan]::FromMilliseconds(300)

                $script:filterTimer.Add_Tick({
                    $filterText = $filterBox.Text.Trim()
                    $script:currentFilterText = $filterText

                    # Rebuild instead of CollectionView.Filter - PS delegates break during WPF sorting
                    if ($script:unfilteredData -and $script:currentObservable) {
                        $sortDescriptions = @()
                        if ($script:collectionView) {
                            foreach ($sd in $script:collectionView.SortDescriptions) {
                                $sortDescriptions += $sd
                            }
                        }

                        $script:currentObservable.Clear()

                        foreach ($item in $script:unfilteredData) {
                            if ([string]::IsNullOrEmpty($filterText)) {
                                [void]$script:currentObservable.Add($item)
                            }
                            else {
                                $matches = $false
                                foreach ($prop in $item.PSObject.Properties) {
                                    $val = $prop.Value
                                    if ($val -and $val.ToString().IndexOf($filterText, [StringComparison]::OrdinalIgnoreCase) -ge 0) {
                                        $matches = $true
                                        break
                                    }
                                }
                                if ($matches) {
                                    [void]$script:currentObservable.Add($item)
                                }
                            }
                        }

                        if ($script:collectionView -and $sortDescriptions.Count -gt 0) {
                            $script:collectionView.SortDescriptions.Clear()
                            foreach ($sd in $sortDescriptions) {
                                $script:collectionView.SortDescriptions.Add($sd)
                            }
                        }
                    }

                    $script:filterTimer.Stop()
                    $script:filterTimer = $null
                })

                $script:filterTimer.Start()
            })

            $addRowBtn.Add_Click({
                    try {
                        if (!$script:currentFileName) {
                            Show-ThemedDialog -Title 'No File' -Message 'No file selected.' -Buttons OK -Icon Warning
                            return
                        }
                        if (!$script:currentObservable) {
                            Show-ThemedDialog -Title 'No Data' -Message 'No data loaded.' -Buttons OK -Icon Warning
                            return
                        }

                        $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                        if (!$csvInfo -or $csvInfo.Data.Count -eq 0) {
                            Show-ThemedDialog -Title 'No Data' -Message 'No CSV data available.' -Buttons OK -Icon Warning
                            return
                        }

                        $newRow = [PSCustomObject]@{}
                        foreach ($prop in $csvInfo.Data[0].PSObject.Properties) {
                            $defaultValue = ''

                            if ($ColumnComboBoxes -and $ColumnComboBoxes.ContainsKey($prop.Name)) {
                                $comboConfig = $ColumnComboBoxes[$prop.Name]

                                if ($comboConfig -is [array]) {
                                    if ($comboConfig.Count -gt 0) {
                                        $defaultValue = $comboConfig[0]
                                    }
                                }
                                else {
                                    if ($comboConfig.DefaultValue) {
                                        $defaultValue = $comboConfig.DefaultValue
                                    }
                                    elseif ($comboConfig.Values -and $comboConfig.Values.Count -gt 0) {
                                        $defaultValue = $comboConfig.Values[0]
                                    }
                                }
                            }

                            $newRow | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $defaultValue
                        }

                        [void]$script:currentObservable.Add($newRow)
                        [void]$csvInfo.Data.Add($newRow)

                        if ($script:unfilteredData) {
                            [void]$script:unfilteredData.Add($newRow)
                        }

                        $csvInfo.Modified = $true

                        if ($script:collectionView) {
                            $script:collectionView.Refresh()
                        }

                        $dataGrid.SelectedItem = $newRow
                        $dataGrid.ScrollIntoView($newRow)
                    }
                    catch {
                        Show-ThemedDialog -Title 'Add Failed' -Message "Failed to add row: $_" -Buttons OK -Icon Error
                        return
                    }
                })

            $deleteRowBtn.Add_Click({
                    try {
                        if (!$script:currentFileName) {
                            Show-ThemedDialog -Title 'No File' -Message 'No file selected.' -Buttons OK -Icon Warning
                            return
                        }
                        if (!$script:currentObservable) {
                            Show-ThemedDialog -Title 'No Data' -Message 'No data loaded.' -Buttons OK -Icon Warning
                            return
                        }

                        $selected = $dataGrid.SelectedItems
                        if (!$selected -or $selected.Count -eq 0) {
                            Show-ThemedDialog -Title 'No Selection' -Message 'Please select row(s) to delete.' -Buttons OK -Icon Warning
                            return
                        }

                        $count = $selected.Count
                        $result = Show-ThemedDialog -Title 'Confirm Delete' -Message "Delete $count row(s)?" -Buttons YesNo -Icon Question

                        if ($result -eq 'Yes') {
                            $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                            $toRemove = @($selected)
                            foreach ($item in $toRemove) {
                                [void]$script:currentObservable.Remove($item)
                                [void]$csvInfo.Data.Remove($item)

                                if ($script:unfilteredData) {
                                    [void]$script:unfilteredData.Remove($item)
                                }
                            }
                            $csvInfo.Modified = $true

                            if ($script:collectionView) {
                                $script:collectionView.Refresh()
                            }

                            Show-ThemedDialog -Title 'Deleted' -Message "$count row(s) deleted." -Buttons OK -Icon Info
                        }
                    }
                    catch {
                        Show-ThemedDialog -Title 'Delete Failed' -Message "Failed to delete rows: $_" -Buttons OK -Icon Error
                    }
                })

            $copyRowBtn.Add_Click({
                    try {
                        if (!$script:currentFileName) {
                            Show-ThemedDialog -Title 'No File' -Message 'No file selected.' -Buttons OK -Icon Warning
                            return
                        }
                        if (!$script:currentObservable) {
                            Show-ThemedDialog -Title 'No Data' -Message 'No data loaded.' -Buttons OK -Icon Warning
                            return
                        }

                        $selected = $dataGrid.SelectedItem
                        if (!$selected) {
                            Show-ThemedDialog -Title 'No Selection' -Message 'Please select a row to duplicate.' -Buttons OK -Icon Warning
                            return
                        }

                        $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                        $newRow = [PSCustomObject]@{}
                        foreach ($prop in $selected.PSObject.Properties) {
                            $newRow | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value
                        }

                        [void]$script:currentObservable.Add($newRow)
                        [void]$csvInfo.Data.Add($newRow)
                        $csvInfo.Modified = $true

                        if ($script:collectionView) {
                            $script:collectionView.Refresh()
                        }

                        $dataGrid.SelectedItem = $newRow
                        $dataGrid.ScrollIntoView($newRow)
                    }
                    catch {
                        Show-ThemedDialog -Title 'Copy Failed' -Message "Failed to duplicate row: $_" -Buttons OK -Icon Error
                    }
                })

            $dataGrid.Add_CellEditEnding({
                    param($sender, $eventArgs)
                    if ($script:currentFileName -and $eventArgs.EditAction -eq 'Commit') {
                        $Script:CurrentCSVData[$script:currentFileName].Modified = $true
                        Write-Debug "CellEditEnding - marked '$script:currentFileName' as modified"
                    }
                })

            $dataGrid.Add_RowEditEnding({
                    param($sender, $eventArgs)
                    if ($script:currentFileName -and $eventArgs.EditAction -eq 'Commit') {
                        $Script:CurrentCSVData[$script:currentFileName].Modified = $true
                        Write-Debug "RowEditEnding - marked '$script:currentFileName' as modified"
                    }
                })

            # ComboBox columns don't fire CellEditEnding on selection change
            $dataGrid.Add_PreparingCellForEdit({
                    param($sender, $eventArgs)
                    $editElement = $eventArgs.EditingElement
                    if ($editElement -is [System.Windows.Controls.ComboBox]) {
                        $editElement.Add_SelectionChanged({
                            if ($script:currentFileName) {
                                $Script:CurrentCSVData[$script:currentFileName].Modified = $true
                                Write-Debug "ComboBox SelectionChanged - marked as modified"
                            }
                        })
                    }
                })

            $saveBtn.Add_Click({
                    if ($script:currentFileName) {
                        try {
                            $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                            $exportParams = @{
                                Path              = $csvInfo.Path
                                NoTypeInformation = $true
                                Force             = $true
                            }
                            if ($csvInfo.Delimiter) {
                                $exportParams['Delimiter'] = $csvInfo.Delimiter
                            }
                            $csvInfo.Data | Export-Csv @exportParams
                            Show-ThemedDialog -Title 'Saved' -Message "Saved: $script:currentFileName" -Buttons OK -Icon Info
                        }
                        catch {
                            Show-ThemedDialog -Title 'Save Failed' -Message "Failed to save: $_" -Buttons OK -Icon Error
                        }
                    }
                    else {
                        Show-ThemedDialog -Title 'No File' -Message 'No file is currently selected.' -Buttons OK -Icon Warning
                    }
                })

            $saveAllBtn.Add_Click({
                    $savedCount = 0
                    $errorCount = 0
                    foreach ($fileName in $Script:CurrentCSVData.Keys) {
                        $csvInfo = $Script:CurrentCSVData[$fileName]
                        try {
                            $exportParams = @{
                                Path              = $csvInfo.Path
                                NoTypeInformation = $true
                                Force             = $true
                            }
                            if ($csvInfo.Delimiter) {
                                $exportParams['Delimiter'] = $csvInfo.Delimiter
                            }
                            $csvInfo.Data | Export-Csv @exportParams
                            $savedCount++
                        }
                        catch {
                            $errorCount++
                            Write-Warning "Failed to save $fileName`: $_"
                        }
                    }
                    $msg = "Saved $savedCount file(s)."
                    if ($errorCount -gt 0) { $msg += " $errorCount error(s)." }
                    Show-ThemedDialog -Title 'Save All' -Message $msg -Buttons OK -Icon Info
                })

            $saveAsBtn.Add_Click({
                    if ($script:currentFileName) {
                        $saveDialog = [Microsoft.Win32.SaveFileDialog]::new()
                        $saveDialog.Filter = 'CSV Files (*.csv)|*.csv|All Files (*.*)|*.*'
                        $saveDialog.DefaultExt = '.csv'
                        $saveDialog.FileName = "export_$script:currentFileName"
                        if ($saveDialog.ShowDialog()) {
                            try {
                                $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                                $exportParams = @{
                                    Path              = $saveDialog.FileName
                                    NoTypeInformation = $true
                                    Force             = $true
                                }
                                if ($csvInfo.Delimiter) {
                                    $exportParams['Delimiter'] = $csvInfo.Delimiter
                                }
                                $csvInfo.Data | Export-Csv @exportParams
                                Show-ThemedDialog -Title 'Saved' -Message "Data saved to:`n$($saveDialog.FileName)" -Buttons OK -Icon Info
                            }
                            catch {
                                Show-ThemedDialog -Title 'Save Failed' -Message "Failed to save: $_" -Buttons OK -Icon Error
                            }
                        }
                    }
                    else {
                        Show-ThemedDialog -Title 'No File' -Message 'No file is currently selected.' -Buttons OK -Icon Warning
                    }
                })

            $typeInfoBtn.Add_Click({
                    if ($script:currentFileName) {
                        $csvInfo = $Script:CurrentCSVData[$script:currentFileName]
                        if ($csvInfo.Data.Count -gt 0) {
                            $rowCount = $csvInfo.Data.Count
                            $colCount = @($csvInfo.Data[0].PSObject.Properties).Count

                            $message = "File: $script:currentFileName`nPath: $($csvInfo.Path)`nRows: $rowCount`nColumns: $colCount`nDelimiter: '$($csvInfo.Delimiter)'`nModified: $($csvInfo.Modified)"

                            Show-ThemedDialog -Title 'Data Information' -Message $message -Buttons OK -Icon Info
                        }
                    }
                    else {
                        Show-ThemedDialog -Title 'No File' -Message 'No file is currently selected.' -Buttons OK -Icon Warning
                    }
                })

            Initialize-UiWindowLoaded -Window $window -SetIcon

            $window.Add_Closed({
                # Stop the filter timer before controls dispose
                if ($script:filterTimer) {
                    $script:filterTimer.Stop()
                    $script:filterTimer = $null
                }

                $script:collectionView = $null

                if ($isStandalone -and !$hasParentSession) {
                    $sessionId = [PsUi.SessionManager]::CurrentSessionId
                    if ($sessionId -ne [Guid]::Empty) {
                        [PsUi.SessionManager]::DisposeSession($sessionId)
                    }
                }
            }.GetNewClosure())

            if (!$isStandalone) {
                Set-UiDialogPosition -Dialog $window
            }

            $windowShown = $true
            [void]$window.ShowDialog()

            # Restore the icon font if an override was pushed - nothing to restore when $iconFontSnap is $null.
            # Tail statement, not a finally: off pipeline (called from a -NoAsync click) a finally NREs on exit. The function scope trap above covers a throw from ShowDialog.
            [PsUi.ModuleContext]::RestoreIconFontState($iconFontSnap)
            $iconFontSnap = $null

            $Script:CurrentCSVData = @{}
        }
        catch {
            Write-Debug "Out-CSVDataGrid: dispatch caught: $($_.Exception.GetType().Name): $($_.Exception.Message)"
            Write-Debug "Stack: $($_.ScriptStackTrace)"
            # Before the window shows, a throw is a real setup/validation failure (e.g. the deliberate ComboBox/PopupOnSelection conflict) - surface it so bad calls stop the calling script. After it shows, a throw is close time teardown noise from an unwinding handler. Log and swallow so it doesn't pop an "Error: Open Out-CSVDataGrid" dialog.
            if (!$windowShown) { throw }
        }
    }
}