private/Controls/New-UiDataGridTextColumn.ps1

function New-UiDataGridTextColumn {
    <#
    .SYNOPSIS
        Builds a DataGrid column. Probes row values to pick Text/CheckBox/ComboBox/DatePicker when editable.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [object]$FirstItem,

        [hashtable]$Column,

        [switch]$Editable,

        # Extra rows Build-UiDataGridColumns hands in so a column whose first row is null doesn't fall back to readonly Text just because row 0 happens to be empty.
        [object[]]$SampleItems
    )

    $header = if ($Column -and $Column.Header) { [string]$Column.Header } else { $Name }

    # ReadOnly = $true wins outright. Otherwise per column Editable (bool, scriptblock, or property name) overrides the grid wide -Editable. Anything nonboolean will be editable here. The actual percell check runs later.
    $perColRO = $Column -and $Column.ContainsKey('ReadOnly') -and $Column['ReadOnly']
    $isEditable = [bool]$Editable
    
    if ($Column -and $Column.ContainsKey('Editable')) {
        $editVal = $Column['Editable']
        if ($editVal -is [bool])         { $isEditable = $editVal }
        elseif ($editVal -is [scriptblock] -or $editVal -is [string]) { $isEditable = $true }
    }

    if ($perColRO) { $isEditable = $false }
    $editorType = if ($Column -and $Column.EditorType) { [string]$Column.EditorType } else { 'Auto' }

    # Probe rows to pick an editor type. Only runs when EditorType='Auto'. First non null wins, so an enum/datetime column with a null in row 0 still resolves correctly. Cap at 10 rows so a 100k row seed doesn't pay a column build cost proportional to the data size.
    $valueType = $null

    $probeSet  = @(if ($SampleItems) { $SampleItems } else { $FirstItem })
    $probeMax  = [Math]::Min(10, $probeSet.Count)
    for ($i = 0; $i -lt $probeMax; $i++) {

        $row = $probeSet[$i]
        if ($null -eq $row) { continue }
        try {
            $prop = $row.PSObject.Properties[$Name]
            if ($null -eq $prop) { continue }
            $sampleVal = $prop.Value
            if ($null -ne $sampleVal) { $valueType = $sampleVal.GetType(); break }
        }
        catch { Write-Debug "Type probe failed for '$Name' at row $i`: $_" }
    }

    $resolved = $editorType
    
    if ($isEditable -and $editorType -eq 'Auto') {
        if ($valueType -eq [bool])                       { $resolved = 'CheckBox' }
        elseif ($valueType -and $valueType.IsEnum)       { $resolved = 'ComboBox' }
        elseif ($valueType -eq [datetime])               { $resolved = 'DatePicker' }
        elseif ($valueType -eq [string] -or $valueType -eq [decimal] -or ($valueType -and $valueType.IsPrimitive)) { $resolved = 'Text' }
        else {
            # Complex object, null first row, or just an unsupported type? downgrade to readonly.
            # Pass EditorType='Text' on the column hashtable to override and force an editable text cell.
            Write-Debug "Column '$Name' has unsupported edit type ($valueType); forcing read-only text."
            $resolved   = 'Text'
            $isEditable = $false
        }
    }

    # Alias columns (Add-Member AliasProperty rows) display through the referenced member, but a SortMemberPath pointing at the alias name matches nothing and the sort silently dies. Resolve once here so every branch binds and sorts the real path.
    $bindPath = $Name
    try {
        $prop = $FirstItem.PSObject.Properties[$Name]
        if ($prop -is [System.Management.Automation.PSAliasProperty]) { $bindPath = $prop.ReferencedMemberName }
    }
    catch { Write-Debug "Alias check failed for '$Name': $_" }

    switch ($resolved) {

        'CheckBox' {
            $col = [System.Windows.Controls.DataGridCheckBoxColumn]::new()
            $col.Header         = $header
            $col.SortMemberPath = $bindPath
            $binding = [System.Windows.Data.Binding]::new($bindPath)
            $binding.Mode = if ($isEditable) { [System.Windows.Data.BindingMode]::TwoWay } else { [System.Windows.Data.BindingMode]::OneWay }

            # $null in a bool column renders the indeterminate dash, which reads as a tristate.
            # TargetNullValue collapses null to false at the binding. IsThreeState=$false stops the click cycle from reintroducing it.
            $binding.TargetNullValue = $false
            $col.Binding      = $binding
            $col.IsReadOnly   = !$isEditable
            $col.IsThreeState = $false

            # Default DataGridCheckBoxColumn renders the raw OS checkbox - looks straight out of 2003 next to themed controls. Reskin with the same template Set-CheckBoxStyle uses.
            $cbStyle = Get-UiDataGridCheckBoxStyle
            $col.ElementStyle        = $cbStyle
            $col.EditingElementStyle = $cbStyle
        }

        'ComboBox' {
            $col = [System.Windows.Controls.DataGridComboBoxColumn]::new()
            $col.Header         = $header
            $col.SortMemberPath = $bindPath
            $col.IsReadOnly     = !$isEditable

            $choices = if ($Column -and $Column.Choices) { @($Column.Choices) }
                       elseif ($valueType -and $valueType.IsEnum) { [enum]::GetValues($valueType) }
                       else { @() }
            $col.ItemsSource = $choices

            # SelectedValueBinding (not SelectedItemBinding) so a string Choices array against a typed property doesn't write a string back. For enums the default SelectedValuePath is fine. ComboBox handles the value coercion.
            $sel = [System.Windows.Data.Binding]::new($bindPath)
            $sel.Mode = if ($isEditable) { [System.Windows.Data.BindingMode]::TwoWay } else { [System.Windows.Data.BindingMode]::OneWay }
            $sel.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::PropertyChanged
            $col.SelectedValueBinding = $sel

            # Enums are self coercing through SelectedValuePath. Flat string Choices against a typed prop write a string back unless you pass @{Value=;Display=} hashtables and set the paths.
            if ($Column -and $Column.Choices -and !($valueType -and $valueType.IsEnum)) {
                Write-Debug "Column '$Name': flat Choices array. Pass @{Value=...; Display=...} hashtables + SelectedValuePath/SelectedDisplayMemberPath if the prop is typed."
            }

            if ($isEditable) {
                $col.EditingElementStyle = Get-UiDataGridTextEditorStyle -EditorKind ComboBox
            }
        }

        'DatePicker' {
            $col = [System.Windows.Controls.DataGridTemplateColumn]::new()
            $col.Header         = $header
            $col.SortMemberPath = $bindPath

            $displayTpl     = [System.Windows.DataTemplate]::new()
            $displayFactory = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.TextBlock])
            $displayBinding = [System.Windows.Data.Binding]::new($bindPath)
            $displayBinding.Mode = [System.Windows.Data.BindingMode]::OneWay

            if ($Column -and $Column.Format) { $displayBinding.StringFormat = [string]$Column.Format }
            $displayFactory.SetBinding([System.Windows.Controls.TextBlock]::TextProperty, $displayBinding)
            $displayFactory.SetValue([System.Windows.Controls.TextBlock]::VerticalAlignmentProperty, [System.Windows.VerticalAlignment]::Center)
            $displayTpl.VisualTree = $displayFactory
            $col.CellTemplate = $displayTpl

            if ($isEditable) {
                $editTpl       = [System.Windows.DataTemplate]::new()
                $editFactory   = [System.Windows.FrameworkElementFactory]::new([System.Windows.Controls.DatePicker])
                $editBinding   = [System.Windows.Data.Binding]::new($bindPath)
                $editBinding.Mode = [System.Windows.Data.BindingMode]::TwoWay
                $editBinding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::LostFocus
                $editFactory.SetBinding([System.Windows.Controls.DatePicker]::SelectedDateProperty, $editBinding)
                $dpStyle = Get-UiDataGridTextEditorStyle -EditorKind DatePicker
                $editFactory.SetValue([System.Windows.Controls.DatePicker]::StyleProperty, $dpStyle)
                $editTpl.VisualTree = $editFactory
                $col.CellEditingTemplate = $editTpl
            }
            $col.IsReadOnly = !$isEditable
        }

        default {
            $col = [System.Windows.Controls.DataGridTextColumn]::new()
            $col.Header         = $header
            $col.SortMemberPath = $bindPath

            if ($bindPath -match '[.\[]') {
                Write-Warning "Column '$Name': property name contains '.' or '[' which WPF Binding interprets as a path. The column will render blank. Rename the property or pre-flatten it."
            }

            # Plain binding. The upstream snapshot in New-UiDataGrid flattens objects into PSCustomObjects, so PowerShell added properties show up like regular ones, no per cell cost.
            $binding = [System.Windows.Data.Binding]::new($bindPath)
            $binding.Mode = if ($isEditable) { [System.Windows.Data.BindingMode]::TwoWay } else { [System.Windows.Data.BindingMode]::OneWay }
            if ($isEditable) { $binding.UpdateSourceTrigger = [System.Windows.Data.UpdateSourceTrigger]::LostFocus }
            if ($Column -and $Column.Format) { $binding.StringFormat = [string]$Column.Format }
            $col.Binding    = $binding
            $col.IsReadOnly = !$isEditable

            if ($isEditable) { $col.EditingElementStyle = Get-UiDataGridTextEditorStyle }
        }
    }

    $col.Width = if ($Column -and $Column.Width) { ConvertTo-UiDataGridLength $Column.Width } else { [System.Windows.Controls.DataGridLength]::Auto }

    # MinWidth floor. Keeps the header text from truncating into ellipses AND keeps editable cells large enough to land a click on. Matches Add-DataGridColumns. $Column.MinWidth overrides if you want something else.
    $headerMin    = [Math]::Max(60, ($header.Length * 7) + 30)
    $col.MinWidth = if ($Column -and $Column.MinWidth) { [double]$Column.MinWidth } else { $headerMin }

    return $col
}