private/Get-ContrastForeground.ps1

<#
.SYNOPSIS
    Picks a readable foreground color for a given background.

.DESCRIPTION
    Uses the WCAG 2.x relative-luminance and contrast-ratio formulas rather than
    a perceived-brightness threshold.

    The old version compared YIQ brightness against 0.5 and returned near-black
    or near-white. That splits colors into two buckets but never checks the
    result is actually readable, so mid-tone palette entries — the oranges,
    teals and mid greens that sit either side of the threshold — produced
    title-bar and active-tab text hovering around 3:1, which reads as washed out
    in both light and dark editor themes.

    Now both candidates are scored and the better one wins. If even the winner
    falls under the WCAG AA threshold for large text (4.5:1 is the body-text
    bar; chrome text is large/bold, so 4.5 is used here as a conservative
    target), it escalates to pure white or pure black, which is the most
    contrast a single foreground can give against that background.

.PARAMETER Hex
    Background color as '#RRGGBB' or 'RRGGBB'.

.EXAMPLE
    Get-ContrastForeground '#e05d44'
    Returns the darker or lighter foreground, whichever reads better.

.NOTES
    Depends on: nothing.
#>

function Get-WtwRelativeLuminance {
    <#
    .SYNOPSIS
        WCAG relative luminance (0..1) for an sRGB hex color.
    .DESCRIPTION
        Applies the sRGB gamma expansion the spec requires. A plain weighted
        average of the raw channel bytes — which is what "perceived brightness"
        formulas use — is not the same curve and misjudges saturated colors.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $Hex)

    $clean = $Hex.TrimStart('#')
    $channels = @(
        [convert]::ToInt32($clean.Substring(0, 2), 16)
        [convert]::ToInt32($clean.Substring(2, 2), 16)
        [convert]::ToInt32($clean.Substring(4, 2), 16)
    )

    $linear = foreach ($channel in $channels) {
        $srgb = $channel / 255
        if ($srgb -le 0.03928) { $srgb / 12.92 } else { [Math]::Pow((($srgb + 0.055) / 1.055), 2.4) }
    }

    return (0.2126 * $linear[0]) + (0.7152 * $linear[1]) + (0.0722 * $linear[2])
}

function Get-WtwContrastRatio {
    <#
    .SYNOPSIS
        WCAG contrast ratio between two hex colors (1.0 .. 21.0).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Foreground,
        [Parameter(Mandatory)] [string] $Background
    )

    $l1 = Get-WtwRelativeLuminance -Hex $Foreground
    $l2 = Get-WtwRelativeLuminance -Hex $Background
    $lighter = [Math]::Max($l1, $l2)
    $darker = [Math]::Min($l1, $l2)
    return (($lighter + 0.05) / ($darker + 0.05))
}

function Get-ContrastForeground {
    [CmdletBinding()]
    param([string] $Hex)

    $background = if ($Hex.StartsWith('#')) { $Hex } else { "#$Hex" }

    # Softened near-black / near-white first: they sit better against a colored
    # chrome than pure values, so they are preferred whenever they are readable.
    $dark = '#15202b'
    $light = '#e7e7e7'

    $darkRatio = Get-WtwContrastRatio -Foreground $dark -Background $background
    $lightRatio = Get-WtwContrastRatio -Foreground $light -Background $background

    $best = if ($darkRatio -ge $lightRatio) { $dark } else { $light }
    $bestRatio = [Math]::Max($darkRatio, $lightRatio)

    if ($bestRatio -ge 4.5) { return $best }

    # Mid-tone background: neither softened value is readable, so take the
    # maximum available contrast instead of returning something washed out.
    $blackRatio = Get-WtwContrastRatio -Foreground '#000000' -Background $background
    $whiteRatio = Get-WtwContrastRatio -Foreground '#ffffff' -Background $background
    return $(if ($blackRatio -ge $whiteRatio) { '#000000' } else { '#ffffff' })
}