Merge branch 'worktree-agent-a9537eabbf38e0608'

# Conflicts:
#	resources/views/components/scheme-picker.blade.php
#	resources/views/components/theme-script.blade.php
#	resources/views/showcase/sections/colour.blade.php
#	tests/Feature/Components/ThemeScriptTest.php
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 05:43:37 +02:00
23 changed files with 4086 additions and 408 deletions
+50 -21
View File
@@ -18,6 +18,11 @@ use Throwable;
* the role existed) is taken from the default, and a value that is not a #rrggbb hex is
* refused, so nothing but a colour ever reaches a stylesheet.
*
* The roles at the top level are the standard contrast level; M3's medium and high levels sit
* under `contrast` and are asked for by name (`load($path, $profile, 'high')`). A file written
* before the levels existed — or one whose level is missing a role — falls back to the standard
* roles, so a mail or an error page never draws a hole.
*
* A file generated from `livewire-material.profiles` holds every profile under `profiles`, its
* default under `default`, and the default's roles at the top level as a single scheme does. Which
* profile is active is the application's to say, through resolveProfileUsing(); the resolver is
@@ -30,6 +35,14 @@ use Throwable;
*/
class Scheme
{
/**
* M3's contrast levels, as `material:scheme` writes them and as <html data-contrast> names
* them. `standard` is the scheme's own level and has no attribute.
*
* @var list<string>
*/
public const LEVELS = ['standard', 'medium', 'high'];
/**
* @var (Closure(): ?string)|null
*/
@@ -46,14 +59,15 @@ class Scheme
}
/**
* The roles to draw: the given profile's, the active profile's, or the single scheme's.
* The roles to draw: the given profile's, the active profile's, or the single scheme's, at
* the given contrast level (`standard`, `medium` or `high`; anything else is standard).
*
* @return array{light: array<string, string>, dark: array<string, string>}
*/
public static function load(?string $path = null, ?string $profile = null): array
public static function load(?string $path = null, ?string $profile = null, string $contrast = 'standard'): array
{
$data = static::data($path);
$profiles = static::profilesFrom($data);
$profiles = static::profilesFrom($data, $contrast);
if ($profiles !== []) {
$name = $profile !== null && isset($profiles[$profile]) ? $profile : static::activeFrom($data, $profiles);
@@ -61,13 +75,7 @@ class Scheme
return ['light' => $profiles[$name]['light'], 'dark' => $profiles[$name]['dark']];
}
$default = static::defaultScheme();
$scheme = ['light' => static::roles($data['light'] ?? null), 'dark' => static::roles($data['dark'] ?? null)];
return [
'light' => [...$default['light'], ...$scheme['light']],
'dark' => [...$default['dark'], ...$scheme['dark']],
];
return static::scheme($data, $contrast);
}
/**
@@ -75,9 +83,9 @@ class Scheme
*
* @return array<string, string>
*/
public static function light(?string $path = null, ?string $profile = null): array
public static function light(?string $path = null, ?string $profile = null, string $contrast = 'standard'): array
{
return static::load($path, $profile)['light'];
return static::load($path, $profile, $contrast)['light'];
}
/**
@@ -85,9 +93,9 @@ class Scheme
*
* @return array<string, array{label: string, light: array<string, string>, dark: array<string, string>}>
*/
public static function profiles(?string $path = null): array
public static function profiles(?string $path = null, string $contrast = 'standard'): array
{
return static::profilesFrom(static::data($path));
return static::profilesFrom(static::data($path), $contrast);
}
/**
@@ -116,13 +124,12 @@ class Scheme
* @param array<mixed> $data
* @return array<string, array{label: string, light: array<string, string>, dark: array<string, string>}>
*/
protected static function profilesFrom(array $data): array
protected static function profilesFrom(array $data, string $contrast = 'standard'): array
{
if (! is_array($data['profiles'] ?? null)) {
return [];
}
$default = static::defaultScheme();
$profiles = [];
foreach ($data['profiles'] as $name => $profile) {
@@ -132,14 +139,32 @@ class Scheme
$profiles[$name] = [
'label' => is_string($profile['label'] ?? null) ? $profile['label'] : Str::headline($name),
'light' => [...$default['light'], ...static::roles($profile['light'] ?? null)],
'dark' => [...$default['dark'], ...static::roles($profile['dark'] ?? null)],
...static::scheme($profile, $contrast),
];
}
return $profiles;
}
/**
* One scheme's roles at one contrast level: the level's own, over the standard ones it does
* not name, over the package's default. A scheme file written before the levels existed has
* only the standard roles, and draws them at every level rather than nothing.
*
* @param array<mixed> $data
* @return array{light: array<string, string>, dark: array<string, string>}
*/
protected static function scheme(array $data, string $contrast): array
{
$default = static::defaultScheme($contrast);
$level = is_array($data['contrast'][$contrast] ?? null) ? $data['contrast'][$contrast] : [];
return [
'light' => [...$default['light'], ...static::roles($data['light'] ?? null), ...static::roles($level['light'] ?? null)],
'dark' => [...$default['dark'], ...static::roles($data['dark'] ?? null), ...static::roles($level['dark'] ?? null)],
];
}
/**
* @param array<mixed> $data
* @param non-empty-array<string, mixed> $profiles
@@ -162,15 +187,19 @@ class Scheme
}
/**
* The package's own scheme, which fills any role a file lacks.
* The package's own scheme at one contrast level, which fills any role a file lacks.
*
* @return array{light: array<string, string>, dark: array<string, string>}
*/
protected static function defaultScheme(): array
protected static function defaultScheme(string $contrast = 'standard'): array
{
$data = json_decode((string) file_get_contents(dirname(__DIR__, 2).'/resources/css/tokens/scheme.json'), true);
$level = is_array($data['contrast'][$contrast] ?? null) ? $data['contrast'][$contrast] : [];
return ['light' => static::roles($data['light'] ?? null), 'dark' => static::roles($data['dark'] ?? null)];
return [
'light' => [...static::roles($data['light'] ?? null), ...static::roles($level['light'] ?? null)],
'dark' => [...static::roles($data['dark'] ?? null), ...static::roles($level['dark'] ?? null)],
];
}
/**