Give success, warning and info the 2025 spec and three contrast levels

The three state colours are DynamicColors on their own tonal palette now, built
exactly as Google builds error/on-error/error-container/on-error-container in both
specs, so the scheme's contrast level, dark tones, spec version and platform reach
them as they reach every other role; --harmonize (and a profile's harmonize) pulls
each source towards the seed, off by default. material:scheme also writes M3's
medium (0.5) and high (1.0) levels for both themes and every profile, keyed on
data-contrast and never on a media query, and --contrast now moves the standard
block alone. Scheme::load() takes the level; the mail theme stays on standard.
Plan: docs/plans/material-3-alignment.md, steps 6 and 7 (core C1, C2, C15).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:30:48 +02:00
co-authored by Claude Fable 5.1
parent b1fc0c9cfa
commit 4263982369
11 changed files with 3697 additions and 372 deletions
+28
View File
@@ -19,6 +19,11 @@ function writeProfiles(string $path, array $primaries, string $default): void
'contrast' => 0,
'light' => ['primary' => $primary, 'surface' => '#fafafa'],
'dark' => ['primary' => '#eeeeee', 'surface' => '#111111'],
'contrast' => [
'standard' => 0,
'medium' => ['light' => ['primary' => '#332f8a'], 'dark' => ['surface' => '#0a0a0a']],
'high' => ['light' => ['primary' => '#000000'], 'dark' => ['primary' => '#ffffff', 'surface' => '#000000']],
],
])->all();
File::put($path, json_encode([...collect($profiles[$default])->except('label')->all(), 'default' => $default, 'profiles' => $profiles]));
@@ -69,6 +74,29 @@ it('loads a profile by name, whatever is active', function () {
->and(Scheme::light($this->path, 'ocean')['primary'])->toBe('#00897b');
});
it('loads a contrast level, falling back to the standard roles it does not name', function () {
expect(Scheme::load($this->path, 'teal', 'high')['light']['primary'])->toBe('#000000')
->and(Scheme::load($this->path, 'teal', 'high')['dark']['surface'])->toBe('#000000')
// The level names no surface in light, so the standard one stands.
->and(Scheme::light($this->path, 'teal', 'high')['surface'])->toBe('#fafafa')
->and(Scheme::load($this->path, 'teal', 'medium')['light']['primary'])->toBe('#332f8a')
->and(Scheme::load($this->path, 'teal')['light']['primary'])->toBe('#00897b')
// A level nobody generated, and a name that is not one, are the standard scheme.
->and(Scheme::load($this->path, 'teal', 'extreme')['light']['primary'])->toBe('#00897b')
->and(Scheme::profiles($this->path, 'high')['rose']['light']['primary'])->toBe('#000000')
->and(Scheme::profiles($this->path)['rose']['light']['primary'])->toBe('#c2185b');
});
it('draws a high contrast level from the package default for a scheme file without one', function () {
File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => ['primary' => '#abcdef']]));
$package = json_decode(File::get(__DIR__.'/../../resources/css/tokens/scheme.json'), true);
expect(Scheme::light($this->path, null, 'high')['primary'])->toBe('#123456')
->and(Scheme::light($this->path, null, 'high')['surface'])->toBe($package['contrast']['high']['light']['surface'])
->and(Scheme::light($this->path)['surface'])->toBe($package['light']['surface']);
});
it('has no profile for a single scheme, and loads it as before', function () {
File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => ['primary' => '#abcdef']]));
Scheme::resolveProfileUsing(fn (): string => 'teal');