Skip to content

The light themes' Warning and Info status colors read against their own page, and text on a status fill gets a measured ink in every theme (#3609) - #3627

Merged
erikdarlingdata merged 1 commit into
devfrom
fix/3609-lane
Sep 18, 2026
Merged

erikdarlingdata merged 1 commit into
devfrom
fix/3609-lane

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Closes the palette half of what #3606 exposed: on a stock Light install the new Settings contrast readout paints two of its own status rows red, because WarningColor and InfoColor never cleared the page they sit on. Everything here is measured with the WcagContrast #3606 checked in (WCAG 2.x relative luminance), not eyeballed; no screenshots are possible from this lane.

The defect, measured

The readout measures each status color against BackgroundColor and bands it green ≥ 4.5 / amber ≥ 3 / red < 3. Stock, before this PR:

Theme Color Value vs page vs card (BackgroundLightColor) Readout
Light WarningColor #F57F17 2.47 2.65 red
Light InfoColor #2eaef1 (= AccentColor) 2.32 2.49 red
Cool Breeze WarningColor #F57F17 2.09 2.39 red

(The issue text has Light's and Cool Breeze's WarningColor figures swapped — Light is 2.47, Cool Breeze 2.09. Both are under the floor; the conclusion is unchanged.)

3:1 is the non-text floor, and it is not the bar these colors actually need. I traced every consumer of WarningBrush / InfoBrush in Lite, the viewer and PerformanceMonitor.Ui:

  • Text ink (4.5:1 against the page): the viewer paints WarningBrush as text for the overview attention count, the fleet unknown-status line, the deadlock-coverage note, the seat state, and the 22-pt warning count; InfoBrush is the "■ Blue — Info" legend run in both Settings windows.
  • Marker (3:1): the server-list status dot (Lite and viewer), the performance-calendar cells and legend, the warning card border.
  • Fill behind text: the Recommendations severity badge (Lite, viewer, Dashboard) with a hard-coded #1A1A1A ink; the viewer's tab alert badge and server-list attention badge with a hard-coded White ink.

So the fills had to reach 4.5:1 on the page and the card — and that is where the palette's structural problem shows.

Values

Theme Color Before → After vs page vs card vs well (BackgroundDark) vs alt row
Light WarningColor #F57F17 → #AE4F08 2.47 → 4.99 2.65 → 5.35 2.36 → 4.77 2.34 → 4.72
Light InfoColor #2eaef1 → #0369A1 2.32 → 5.53 2.49 → 5.93 2.22 → 5.29 2.19 → 5.23
Cool Breeze WarningColor #F57F17 → #9E4A0B 2.09 → 4.82 2.39 → 5.51 1.91 → 4.41 1.77 → 4.08
  • Light's amber is the amber-700 class (hue 26°, saturation 0.95 — an amber, not a brown); it clears AA text on all four Light surfaces.
  • Light's Info was literally the accent. The accent is fine as a fill (its ink is AccentForegroundColor, 6.79:1 — Text on an accent fill gets a measured ink in every theme, and the FinOps row marks become theme brushes Dark can actually see (#3577, bug arm) #3589), but a fill's value is the wrong value for a marker or a text tone. Decoupled to the same 201° hue, deep enough to read; AccentColor itself is untouched, so nothing about the selected tab, grid selection or accent buttons changes.
  • Cool Breeze's page is a shade darker than Light's, so its amber goes one step deeper to clear the same bar. Its well and alt-row figures are 4.41 / 4.08 — surfaces where the amber is only ever a marker (the status dot), comfortably over 3:1. Still an amber.
  • Dark is untouched (its status colors measure 5.8–12.3:1 against its page).

Each value carries its before → after beside the <Color> in XAML, tagged #3609, in the #3589 convention.

The structural problem, and why this is not a value pick

Making a status color dark enough to read against the page makes it too dark for the dark text that used to sit on it. The band is empty: 4.5:1 on Light's page needs a relative luminance ≤ 0.17; 4.5:1 under #1A1A1A needs ≥ 0.22. The old #1A1A1A ink measures 3.25:1 on the new Light amber and 2.93:1 on the new Info — and it was already 3.10:1 on the Critical badge's #C62828, a failing pair the bright amber had been hiding in plain sight.

The resolution is the one #3589 gave the accent: the fill is tuned for the page and the ink becomes a theme key. StatusForegroundBrush — the ink for text and glyphs on any status fill:

Theme Ink on Warning on Info on Success on Error
Light #FFFFFF 5.35 5.93 5.13 5.62
Cool Breeze #FFFFFF (already its accent ink) 6.11 5.39 5.13 5.62
Dark #111217 (the darkest surface tone, = its accent ink) 13.26 7.52 9.30 6.26

It is a SolidColorBrush with a literal color (the WarningTextBrush / ApexBorderBrush idiom), not a nineteenth <Color>: ThemeColorOverrideTests pins the palette at exactly eighteen colors in every file, and this is ink that should follow the fills it sits on, not a slot the operator tunes independently. It is not exposed in the Settings Colors section for the same reason.

Consumers moved onto the key:

  • Lite/Controls/RecommendationsTab.xaml and the viewer's copy of that template in MainWindow.xaml — the two #1A1A1A inks on the severity badge. Visible change on Light / Cool Breeze: the badge goes from dark text on a bright amber / cyan to white text on a deep amber / blue. On Dark the ink moves #1A1A1A → #111217, imperceptible.
  • The viewer's tab alert badge (MainWindow.AlertBadges.cs, Brushes.White → SetResourceReference) and server-list attention badge (MainWindow.xaml, Foreground="White"). These are load-bearing edits outside the theme files, and they fix a pre-existing defect of the same class that the key makes free: White on Dark's #FFD54F amber is 1.41:1 — an alert count you could not read. Leaving two badge sites on a literal White while the third used the key would have put two inks on the same fill in one theme, which is what Text on an accent fill gets a measured ink in every theme, and the FinOps row marks become theme brushes Dark can actually see (#3577, bug arm) #3589 refused to do for the accent.

Parity ports

  • Darling viewer: the three dictionaries stay 1:1 with Lite's apart from their pre-existing viewer-only block. ThemeParityLiteDarlingTests: 70 shared keys per theme (69 before — the new brush), 0 mismatches; ThemeCompletenessTests key sets equal (95 Lite / 103 Darling ×3); every theme token referenced by either app's XAML resolves in all three themes.
  • deprecated/Dashboard: the three values mirrored (its ThemeParityTests pins palette values Dashboard↔Lite — 67 shared, 0 mismatches — and PerformanceMonitor.Ui changes trip the path filter that runs it), plus the ink key and its RecommendationsContent.xaml badge, because a value-only mirror would have knowingly dropped that badge's ink to 3.25:1. Smallest faithful diff, its idiom (short parity comments).

What the readout shows now (stock palette, computed the way ThemeColorsPanel.RefreshReadouts computes it)

Theme Success Warning Error Info
Light 4.78 green 2.47 red → 4.99 green 5.24 green 2.32 red → 5.53 green
Cool Breeze 4.04 amber 2.09 red → 4.82 green 4.43 amber 4.25 amber
Dark 8.59 green 12.25 green 5.79 green 6.94 green

Light and Dark read all-green. Cool Breeze still shows three amber rows — Success 4.04, Error 4.43, Info 4.25 (its Info is its accent, #1E6FA8). Those are above the 3:1 floor the issue was opened for and were not among the values it named; they are the same design call on three more colors (and Info↔Accent coupling on Cool Breeze is the Light decision again), so they are reported below rather than folded in.

What this PR does not do

Tests

  • New — Lite.Tests/ThemeStatusContrastTests (four facts, 21 cases): every theme's four status colors ≥ 3:1 on both page surfaces (the readout's own red line, pinned for all four so the defect cannot return through a different color); WarningColor / InfoColor ≥ 4.5:1 on both surfaces (they are text — Cool Breeze's Info is exempted in a spelled-out line with its 4.25 figure, so the pin tightens the day it is fixed); StatusForegroundBrush ≥ 4.5:1 on every status fill in every theme; and the three consumers spell the key with no literal #1A1A1A / White left. Text parse via ThemeXamlRewriter.DeclaredColors + WcagContrast — the same parser and arithmetic as the readout, no WPF object constructed. Executed locally on macOS through a net10.0 harness (real WcagContrast.cs; a stub Color struct): 21/21 green on this branch, and pointed at pristine dev it fails with exactly the issue's numbers (2.47 / 2.32 / 2.09, plus the missing key).
  • ThemeCompletenessTests (both apps), ThemeParityLiteDarlingTests, Dashboard ThemeParityTests, and the {StaticResource} hygiene check were replicated line-for-line in Python against the working tree: all green (figures above). CI runs the real ones.
  • ThemeColorOverrideTests' eighteen-<Color> pins are unaffected by construction (no <Color> added).
  • dotnet build -c Release -p:EnableWindowsTargeting=true on Lite, Lite.Tests, the viewer, Darling.Tests, Dashboard and Dashboard.Tests: 0 warnings, 0 errors each; BAML regenerated with the new key and values.

Out-of-lane findings (reported, not fixed)

  • Cool Breeze Success / Error / Info at 4.04 / 4.43 / 4.25 vs page — amber in the readout. Candidates measured: Success #1B5E20 (Material green-900) 6.1:1; Error #B71C1C (red-900) 5.3:1; Info would need decoupling from AccentColor as Light's did. A palette call for the maintainer.
  • Light's ApexBorderBrush #1976D2 is 4.29:1 on the Light page — fine as the border it is (3:1), noted only because Light's Info now sits near it in hue and reads deeper.
  • The Settings legend runs ("■ Green — Success" etc.) paint SuccessBrush / InfoBrush as bold text; on Cool Breeze that is the 4.04 / 4.25 above.

Closes #3609

…wn page, and text on a status fill gets a measured ink in every theme (#3609)

Light shipped WarningColor #F57F17 at 2.47:1 and InfoColor #2eaef1 (the
accent itself) at 2.32:1 against BackgroundColor; Cool Breeze's
WarningColor was 2.09:1. All three sit under the 3:1 non-text floor the
new Settings contrast readout (#3606) paints red, and WarningBrush is
TEXT in the viewer (fleet counts, seat state, coverage note), so the bar
was 4.5:1, not 3:1.

- Light WarningColor -> #AE4F08 (4.99 page / 5.35 card / 4.77 well /
  4.72 alt row), InfoColor -> #0369A1 (5.53 / 5.93 / 5.29 / 5.23; same
  201-degree hue as the accent, decoupled from it). Cool Breeze
  WarningColor -> #9E4A0B (4.82 / 5.51; 4.41 / 4.08 where it is only a
  marker). Dark untouched.
- No amber clears 4.5:1 on Light's page AND under the #1A1A1A the
  Recommendations badge painted on it (the band is empty), so the ink is
  a theme key, StatusForegroundBrush, the way #3589 gave the accent
  AccentForegroundColor: white on the light themes, #111217 on Dark. A
  brush with a literal, not a nineteenth <Color>, so the eighteen-color
  palette pins in ThemeColorOverrideTests stand. The Recommendations
  badge (Lite, viewer, Dashboard) and the viewer's alert / attention
  badges take it; the latter were hard-coded White, 1.41:1 on Dark's
  amber.
- Values and the ink key mirrored to the Darling viewer and the
  deprecated Dashboard so ThemeParityLiteDarlingTests and the Dashboard
  ThemeParityTests hold (70 and 67 shared keys, 0 mismatches).
- Lite.Tests/ThemeStatusContrastTests pins every theme's four status
  colors at 3:1 on both page surfaces, Warning/Info at 4.5:1, the ink at
  4.5:1 on every status fill, and the three consumers on the key. Text
  parse + WcagContrast, no WPF object; executed locally, fails on dev.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 18, 2026 18:25

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — reviewed the theme XAML/C# diff (WPF, not T-SQL, so the collector-query style rules don't apply here). Spot-checked the WCAG math independently (hand-computed relative luminance for #F57F17/#AE4F08 against the Light page background: got 2.47:1 and 4.99:1, matching the PR's claimed before/after exactly). Verified: the new StatusForegroundBrush key is a literal SolidColorBrush (not a 19th <Color>) so ThemeColorOverrideTests' 18-color pin still holds; ThemeParityLiteDarlingTests/ThemeCompletenessTests/Dashboard's ThemeParityTests all compare key sets dynamically (no hardcoded counts to bump); Lite, Darling viewer, and deprecated Dashboard all got matching WarningColor/InfoColor/StatusForegroundBrush updates plus consistent Foreground swaps on their Recommendations severity badges. Darling's alert/attention badges correctly swap both the literal Brushes.White/Foreground="White" for DynamicResource StatusForegroundBrush alongside their existing WarningBrush/ErrorBrush DynamicResource fills. No correctness, parity, or security issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant