Offer the contrast level where the theme is chosen

<x-theme-toggle mode="contrast"> is the picker's row for M3's three levels, marking
the one in force so the operating system's setting shows while the choice is system.
The scheme picker's dots follow the level on screen instead of always promising the
standard colours, and the showcase's colour section puts the three levels side by
side — data-scheme beside data-contrast, the way a profile's level blocks key.
Plan: docs/plans/material-3-alignment.md, step 7 (core C2).

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:38:31 +02:00
co-authored by Claude Fable 5.1
parent 7084aac788
commit 57d9e0aa6f
5 changed files with 152 additions and 9 deletions
+31 -3
View File
@@ -35,11 +35,37 @@ it('draws a radio per generated profile, bound, named and previewing on change',
it('puts its other attributes on the group and the binding on the radios', function () {
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" data-test="color-profile" class="mt-4" />');
expect($html)->toMatch('/<fieldset x-data data-scheme-picker class="min-w-0 mt-4" data-test="color-profile">/')
expect($html)->toMatch('/<fieldset\s+x-data="\{ swatches: [^"]+\}"\s+data-scheme-picker\s+class="min-w-0 mt-4" data-test="color-profile"\s*>/')
->and(substr_count($html, 'wire:model="colorProfile"'))->toBe(2)
->and(substr_count($html, 'data-test="color-profile"'))->toBe(1);
});
it('swaps the dots to the contrast level on screen', function () {
File::put($this->path, json_encode([
'default' => 'indigo',
'profiles' => [
'indigo' => [
'label' => 'Indigo',
'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'],
'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7'],
'contrast' => ['high' => ['light' => ['primary' => '#241bb0'], 'dark' => ['primary' => '#eeeaff']]],
],
],
]));
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" />');
expect($html)
// Standard is the inline pair, so a dot is painted before Alpine runs.
->toContain('style="--swatch-light: #4f46e5; --swatch-dark: #c0c1ff"')
->toContain('\u0022standard\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022')
->toContain('\u0022high\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#241bb0\u0022')
// A level the file does not carry repeats the standard colours rather than none.
->toContain('\u0022medium\u0022:{\u0022light\u0022:{\u0022primary\u0022:\u0022#4f46e5\u0022')
->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].light[\'primary\']')
->toContain('swatches[\'indigo\'][$store.theme.resolvedContrast].dark[\'tertiary\']');
});
it('keeps its inline styles to the scheme file\'s checked colours', function () {
File::put($this->path, json_encode([
'default' => 'indigo',
@@ -48,10 +74,12 @@ it('keeps its inline styles to the scheme file\'s checked colours', function ()
$html = (string) $this->blade('<x-scheme-picker wire:model="colorProfile" />');
preg_match_all('/style="([^"]*)"/', $html, $styles);
preg_match_all('/\sstyle="([^"]*)"/', $html, $styles);
expect($styles[1])->not->toBeEmpty()
->each->toMatch('/^--swatch-light: #[0-9a-fA-F]{6}; --swatch-dark: #[0-9a-fA-F]{6}$/');
->each->toMatch('/^--swatch-light: #[0-9a-fA-F]{6}; --swatch-dark: #[0-9a-fA-F]{6}$/')
// The colours Alpine swaps in are the same filtered ones, never the file's own text.
->and($html)->not->toContain('url(');
});
it('shows a validation message for the bound property instead of the hint', function () {