design: convert Themes/Controls.xaml to tokens (surface 1 of the sweep)

The theme file that every control inherits is now token-driven: 73 hardcoded colours became
Brush.* references, and the two role changes the standard requires landed here first.

- Red leaves the chrome. The seven #e94560 sites (base button, focus borders, checked box and
  segment backgrounds, the selected-layer accent strip) were all interactive/selection roles,
  so they are blue now. Red stays for live/record only.
- The slider was green: its fill and gloss knob are blue now, with the track on
  SurfaceContainerLowest. The old green fill predated the standard.
- The pill toggle was ON = green, OFF = red — a settings control using the live colour. It is
  now OFF = neutral surface, ON = blue.
- Added scrim-soft/scrim-strong tokens (#22FFFFFF/#26FFFFFF) for the existing white washes, and
  Color.* resources for the two gradients (a GradientStop needs a Color, not a Brush).

Guard: DesignTokensTests gains Converted_surfaces_carry_no_raw_colour — a declared list of
converted files, so each sweep commit is guarded as it lands instead of waiting for one global
grep at the end. Pure white/black highlights are the only allowed literals.

No layout, spacing or behaviour change: this unit is colour only.
This commit is contained in:
2026-10-30 07:47:00 -07:00
parent 84c335a929
commit 4ef84f2a97
4 changed files with 96 additions and 83 deletions
+25 -1
View File
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Xunit;
@@ -67,6 +68,28 @@ public class DesignTokensTests
"Themes/Tokens.xaml must merge BEFORE Themes/Controls.xaml, or styles cannot resolve a token");
}
[Fact]
public void Converted_surfaces_carry_no_raw_colour()
{
// The sweep converts one surface per commit; each adds its file here, so every step is
// guarded on its own instead of waiting for a global grep at the end. Pure white and black
// are allowed (gloss highlights, knockout masks) — everything else must be a token.
string[] converted = { "Themes/Controls.xaml" };
foreach (var file in converted)
{
var text = SourceAssert.Text(file.Split('/'));
var raw = Regex.Matches(text, @"#[0-9a-fA-F]{6,8}")
.Select(m => m.Value)
.Where(v => v is not "#ffffff" and not "#000000" and not "#FFFFFF" and not "#000000")
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
Assert.True(raw.Count == 0,
$"{file} still hardcodes {string.Join(", ", raw)} — resolve a Brush.* token instead");
}
}
[Fact]
public void The_retired_brand_red_is_not_a_token()
{
@@ -100,7 +123,8 @@ public class DesignTokensTests
// The token cell may carry a note after the backticked name("`tertiary` (electric blue)").
var token = Backticked(cells[0]);
var hex = Backticked(cells[1]);
if (!hex.StartsWith("#", StringComparison.Ordinal) || hex.Length != 7) continue;
// 6-digit RGB or 8-digit ARGB (the scrim washes carry alpha).
if (!hex.StartsWith("#", StringComparison.Ordinal) || hex.Length is not (7 or 9)) continue;
result[token] = hex;
}