design: Themes/Tokens.xaml — one place a raw value may live, guarded by DESIGN.md

Tokens.xaml carries every colour, type size, spacing step, radius and motion duration from
DESIGN.md; App.xaml merges it BEFORE Controls.xaml so styles can resolve tokens.

Guard (DesignTokensTests, 4 facts): the test PARSES DESIGN.md — the colour tables under
"## Colour" are the contract — and fails if a documented token has no brush, if the hex
disagrees, if the meter bands drift, if Tokens ever merges after Controls, or if the retired
brand red #e94560 comes back as a token. Adding a colour to the standard now requires
implementing it, and changing the theme without the document is caught.

Inter/JetBrains Mono are declared as token families with system fallbacks; the bundled font
assets are the next unit.

No surface consumes tokens yet — this unit is the foundation only.
This commit is contained in:
2026-10-30 07:43:17 -07:00
parent 565b8dbd6f
commit f2bf577f34
4 changed files with 238 additions and 17 deletions
+3
View File
@@ -6,6 +6,9 @@
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<!-- Design tokens first: every style below resolves its colours, sizes and
radii from them (DESIGN.md is the source of truth). -->
<ResourceDictionary Source="Themes/Tokens.xaml"/>
<ResourceDictionary Source="Themes/Controls.xaml"/> <ResourceDictionary Source="Themes/Controls.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
+8 -4
View File
@@ -171,7 +171,11 @@ rejected in favour of the values above.
## Implementation status ## Implementation status
- **Not yet wired.** `Themes/Controls.xaml` still carries the legacy palette and the dozens of - **`Themes/Tokens.xaml` exists and is merged first in `App.xaml`** (2026-10-30). Guard:
hardcoded hexes; `Themes/Tokens.xaml` lands next, then surfaces are converted one at a time. `ytLive.Tests/DesignTokensTests.cs` — it parses this document and fails if a documented token has
- Target end state: every colour, radius and spacing value resolves from `Tokens.xaml`; a guard no brush, if a value disagrees, or if the retired `#e94560` returns as a token.
test fails on any raw `#hex` outside it. - **Still to sweep:** `Themes/Controls.xaml` and the dozens of hardcoded hexes in XAML/C# still use
the legacy palette. Surfaces convert one at a time; the end state is that every colour, radius and
spacing value resolves from `Tokens.xaml` and a guard fails on any raw `#hex` outside it.
- Fonts (Inter, JetBrains Mono) are declared as token families with system fallbacks; the bundled
font assets land next.
+81
View File
@@ -0,0 +1,81 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<!-- ============================================================================
DESIGN TOKENS — the only place a raw value may live.
Source of truth: DESIGN.md (normative). Guard: ytLive.Tests/DesignTokensTests.cs,
which fails if a documented token has no brush here, or the values disagree.
Merge order: App.xaml merges this BEFORE Themes/Controls.xaml, so every style can
reference a token instead of a literal.
============================================================================ -->
<!-- Surfaces (dark navy scale) -->
<SolidColorBrush x:Key="Brush.Background" Color="#0b1326"/>
<SolidColorBrush x:Key="Brush.SurfaceContainerLowest" Color="#060e20"/>
<SolidColorBrush x:Key="Brush.SurfaceContainerLow" Color="#131b2e"/>
<SolidColorBrush x:Key="Brush.SurfaceContainer" Color="#171f33"/>
<SolidColorBrush x:Key="Brush.SurfaceContainerHigh" Color="#222a3d"/>
<SolidColorBrush x:Key="Brush.SurfaceContainerHighest" Color="#2d3449"/>
<SolidColorBrush x:Key="Brush.SurfaceBright" Color="#31394d"/>
<SolidColorBrush x:Key="Brush.Outline" Color="#334155"/>
<SolidColorBrush x:Key="Brush.OutlineVariant" Color="#1e293b"/>
<!-- Text -->
<SolidColorBrush x:Key="Brush.OnSurface" Color="#dae2fd"/>
<SolidColorBrush x:Key="Brush.OnSurfaceVariant" Color="#94a3b8"/>
<SolidColorBrush x:Key="Brush.OnSurfaceDisabled" Color="#5b6478"/>
<!-- Accent roles: one per meaning, never mixed -->
<SolidColorBrush x:Key="Brush.Tertiary" Color="#38bdf8"/>
<SolidColorBrush x:Key="Brush.OnTertiary" Color="#0b1326"/>
<SolidColorBrush x:Key="Brush.Primary" Color="#ff5451"/>
<SolidColorBrush x:Key="Brush.PrimaryContainer" Color="#b91a24"/>
<SolidColorBrush x:Key="Brush.Secondary" Color="#ffb95f"/>
<SolidColorBrush x:Key="Brush.SecondaryContainer" Color="#ee9800"/>
<SolidColorBrush x:Key="Brush.Attention" Color="#ff8c3a"/>
<SolidColorBrush x:Key="Brush.Error" Color="#ef4444"/>
<SolidColorBrush x:Key="Brush.ErrorContainer" Color="#93000a"/>
<SolidColorBrush x:Key="Brush.OnError" Color="#ffdad6"/>
<!-- Meter bands (measured levels, never gain settings) -->
<SolidColorBrush x:Key="Brush.Meter.Nominal" Color="#38bdf8"/>
<SolidColorBrush x:Key="Brush.Meter.Caution" Color="#ffb95f"/>
<SolidColorBrush x:Key="Brush.Meter.Peak" Color="#ff5451"/>
<!-- Typography. Inter/JetBrains Mono are bundled; the fallbacks keep the app correct
on a machine where the asset failed to load. -->
<FontFamily x:Key="FontFamily.Sans">Inter, Segoe UI</FontFamily>
<FontFamily x:Key="FontFamily.Mono">JetBrains Mono, Cascadia Mono, Consolas</FontFamily>
<sys:Double x:Key="FontSize.HeadlineLg">32</sys:Double>
<sys:Double x:Key="FontSize.HeadlineMd">24</sys:Double>
<sys:Double x:Key="FontSize.HeadlineSm">18</sys:Double>
<sys:Double x:Key="FontSize.BodyLg">15</sys:Double>
<sys:Double x:Key="FontSize.BodyMd">13</sys:Double>
<sys:Double x:Key="FontSize.BodySm">12</sys:Double>
<sys:Double x:Key="FontSize.LabelLg">13</sys:Double>
<sys:Double x:Key="FontSize.LabelMd">11</sys:Double>
<sys:Double x:Key="FontSize.LabelSm">10</sys:Double>
<!-- Spacing -->
<sys:Double x:Key="Space.Xs">4</sys:Double>
<sys:Double x:Key="Space.Sm">8</sys:Double>
<sys:Double x:Key="Space.Md">12</sys:Double>
<sys:Double x:Key="Space.Lg">16</sys:Double>
<sys:Double x:Key="Space.Xl">24</sys:Double>
<sys:Double x:Key="Space.Gutter">12</sys:Double>
<sys:Double x:Key="Space.Margin">16</sys:Double>
<sys:Double x:Key="Space.MarginDocked">8</sys:Double>
<!-- Shape -->
<CornerRadius x:Key="Radius.Sm">4</CornerRadius>
<CornerRadius x:Key="Radius.Md">8</CornerRadius>
<CornerRadius x:Key="Radius.Lg">16</CornerRadius>
<!-- Motion -->
<Duration x:Key="Motion.Fast">0:0:0.09</Duration>
<Duration x:Key="Motion.Normal">0:0:0.15</Duration>
</ResourceDictionary>
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
namespace ytLive.Tests;
/// <summary>DESIGN.md became the visual standard on 2026-10-30. This is its guard: the document
/// is the contract, `Themes/Tokens.xaml` is the implementation, and a token documented in one but
/// missing (or different) in the other fails here — so the palette cannot drift into two truths
/// the way the 2026-10-26 and 2026-10-29 Stitch exports did.
///
/// <para>Deliberately parses the markdown rather than duplicating a list: adding a colour to the
/// standard requires implementing it, and changing a value in the theme without the document is a
/// lie the gate catches.</para></summary>
public class DesignTokensTests
{
private static readonly XNamespace Xaml = "http://schemas.microsoft.com/winfx/2006/xaml";
[Fact]
public void Every_colour_in_the_standard_exists_as_a_token_brush_with_the_documented_value()
{
var documented = DocumentedColours();
Assert.True(documented.Count >= 20,
$"parsed only {documented.Count} colour tokens out of DESIGN.md — the table format changed");
var brushes = Brushes();
foreach (var (token, hex) in documented)
{
var key = "Brush." + Pascal(token);
Assert.True(brushes.ContainsKey(key),
$"DESIGN.md documents `{token}` ({hex}) but Themes/Tokens.xaml has no `{key}` brush");
Assert.Equal(hex, brushes[key], ignoreCase: true);
}
}
[Fact]
public void The_meter_bands_are_the_standard_three()
{
var brushes = Brushes();
// DESIGN.md -> "Meter thresholds": nominal to -12 dB, caution to -3 dB, red above.
Assert.Equal("#38bdf8", brushes["Brush.Meter.Nominal"], ignoreCase: true);
Assert.Equal("#ffb95f", brushes["Brush.Meter.Caution"], ignoreCase: true);
Assert.Equal("#ff5451", brushes["Brush.Meter.Peak"], ignoreCase: true);
}
[Fact]
public void Tokens_merge_before_the_theme_so_styles_can_resolve_them()
{
var app = XDocument.Load(SourceAssert.RepoPath("App.xaml"));
var sources = app.Descendants()
.Where(e => e.Name.LocalName == "ResourceDictionary")
.Select(e => (string?)e.Attribute("Source"))
.Where(s => s != null)
.ToList();
var tokens = sources.FindIndex(s => s!.EndsWith("Themes/Tokens.xaml", StringComparison.Ordinal));
var controls = sources.FindIndex(s => s!.EndsWith("Themes/Controls.xaml", StringComparison.Ordinal));
Assert.True(tokens >= 0, "App.xaml does not merge Themes/Tokens.xaml");
Assert.True(controls >= 0, "App.xaml no longer merges Themes/Controls.xaml");
Assert.True(tokens < controls,
"Themes/Tokens.xaml must merge BEFORE Themes/Controls.xaml, or styles cannot resolve a token");
}
[Fact]
public void The_retired_brand_red_is_not_a_token()
{
// #e94560 was the general accent in 15 files. It is retired (DESIGN.md, 2026-10-30):
// blue is interactive, red means live/record. Re-introducing it as a token would undo
// the rule, so it must not appear anywhere in the token sheet.
Assert.DoesNotContain("#e94560", File.ReadAllText(SourceAssert.RepoPath("Themes", "Tokens.xaml")),
StringComparison.OrdinalIgnoreCase);
}
/// <summary>The `| Token | Hex |` tables under "## Colour", stopping at "## Typography".
/// The "Rejected from the export" table lives further down and must not count.</summary>
private static Dictionary<string, string> DocumentedColours()
{
var result = new Dictionary<string, string>(StringComparer.Ordinal);
var inColour = false;
foreach (var raw in File.ReadAllLines(SourceAssert.RepoPath("DESIGN.md")))
{
var line = raw.Trim();
if (line.StartsWith("## ", StringComparison.Ordinal))
{
inColour = line.StartsWith("## Colour", StringComparison.Ordinal);
continue;
}
if (!inColour || !line.StartsWith("| `", StringComparison.Ordinal)) continue;
var cells = line.Split('|').Select(c => c.Trim()).Where(c => c.Length > 0).ToArray();
if (cells.Length < 2) continue;
// 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;
result[token] = hex;
}
return result;
}
private static Dictionary<string, string> Brushes()
{
var dict = XDocument.Load(SourceAssert.RepoPath("Themes", "Tokens.xaml"));
return dict.Descendants()
.Where(e => e.Name.LocalName == "SolidColorBrush")
.Select(e => ((string?)e.Attribute(Xaml + "Key"), (string?)e.Attribute("Color")))
.Where(p => p.Item1 != null && p.Item2 != null)
.ToDictionary(p => p.Item1!, p => p.Item2!, StringComparer.Ordinal);
}
/// <summary>The first `backticked` run in a markdown cell, or the trimmed cell itself.</summary>
private static string Backticked(string cell)
{
var start = cell.IndexOf('`');
if (start < 0) return cell.Trim();
var end = cell.IndexOf('`', start + 1);
return end < 0 ? cell.Trim() : cell[(start + 1)..end].Trim();
}
/// <summary>`surface-container-lowest` → `SurfaceContainerLowest`.</summary>
private static string Pascal(string token) =>
string.Concat(token.Split('-', StringSplitOptions.RemoveEmptyEntries)
.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
}