Extract the scheme stylesheet shape for ErrorPage to reuse
SchemeCommand::levels()/selectors()/blocks() move to a new SchemeStylesheet, and Scheme gains forStylesheet(), which resolves a scheme (and every profile) at all three contrast levels together. Plan step 40's fallback needs to draw the same colours material:scheme writes, without duplicating its selector logic. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
513e344912
commit
3b9a889e7c
@@ -7,6 +7,7 @@ use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Str;
|
||||
use JsonException;
|
||||
use NoNameWeb\LivewireMaterial\Support\SchemeStylesheet;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand(name: 'material:scheme')]
|
||||
@@ -225,7 +226,7 @@ class SchemeCommand extends Command
|
||||
$states,
|
||||
);
|
||||
|
||||
$blocks = $this->levels($scheme);
|
||||
$blocks = SchemeStylesheet::levels($scheme);
|
||||
|
||||
return <<<CSS
|
||||
/*
|
||||
@@ -288,76 +289,12 @@ class SchemeCommand extends Command
|
||||
|
||||
CSS;
|
||||
|
||||
$css .= "\n".$this->levels($profiles[$default]);
|
||||
$css .= "\n".SchemeStylesheet::levels($profiles[$default]);
|
||||
|
||||
foreach ($profiles as $name => $profile) {
|
||||
$css .= "\n".$this->levels($profile, "[data-scheme='{$name}']");
|
||||
$css .= "\n".SchemeStylesheet::levels($profile, "[data-scheme='{$name}']");
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* One scheme, whole: the standard level first, then M3's medium and high under
|
||||
* data-contrast. A level's blocks are the standard ones with one more attribute, so they
|
||||
* outrank them wherever both match, and the light blocks of a level come before its dark
|
||||
* ones as in every other pair here. The nested `[data-contrast='high'] [data-theme='light']`
|
||||
* form keeps a light panel inside a high-contrast dark page on the level the page asked for
|
||||
* — the plain `[data-theme='light']` block would otherwise take it back to standard.
|
||||
*
|
||||
* @param array{contrast: array<string, mixed>, light: array<string, string>, dark: array<string, string>} $scheme
|
||||
*/
|
||||
protected function levels(array $scheme, string $prefix = ''): string
|
||||
{
|
||||
[$light, $dark] = $this->selectors($prefix);
|
||||
|
||||
$css = $this->blocks($light, $dark, $scheme);
|
||||
|
||||
foreach (array_keys(self::LEVELS) as $level) {
|
||||
[$light, $dark] = $this->selectors($prefix."[data-contrast='{$level}']");
|
||||
|
||||
/** @var array{light: array<string, string>, dark: array<string, string>} $roles */
|
||||
$roles = $scheme['contrast'][$level];
|
||||
|
||||
$css .= "\n".$this->blocks($light, $dark, $roles);
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* The light and the dark selectors of one block, under the given prefix. Without a prefix
|
||||
* the plain blocks stand on `:root` and on the attribute alone; with one, the prefix is on
|
||||
* <html> and the theme may be on <html> or on a panel inside it, so both forms are written.
|
||||
*
|
||||
* @return array{list<string>, list<string>}
|
||||
*/
|
||||
protected function selectors(string $prefix): array
|
||||
{
|
||||
if ($prefix === '') {
|
||||
return [[':root', "[data-theme='light']"], ["[data-theme='dark']"]];
|
||||
}
|
||||
|
||||
return [
|
||||
[$prefix, "{$prefix}[data-theme='light']", "{$prefix} [data-theme='light']"],
|
||||
["{$prefix}[data-theme='dark']", "{$prefix} [data-theme='dark']"],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A light and a dark block of roles under the given selectors.
|
||||
*
|
||||
* @param list<string> $light
|
||||
* @param list<string> $dark
|
||||
* @param array{light: array<string, string>, dark: array<string, string>} $scheme
|
||||
*/
|
||||
protected function blocks(array $light, array $dark, array $scheme): string
|
||||
{
|
||||
$roles = fn (array $roles): string => collect($roles)
|
||||
->map(fn (string $hex, string $role): string => " --md-sys-color-{$role}: {$hex};")
|
||||
->implode("\n");
|
||||
|
||||
return implode(",\n", $light)." {\n color-scheme: light;\n\n".$roles($scheme['light'])."\n}\n\n"
|
||||
.implode(",\n", $dark)." {\n color-scheme: dark;\n\n".$roles($scheme['dark'])."\n}\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,52 @@ class Scheme
|
||||
return $profiles === [] ? null : static::activeFrom($data, $profiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* The scheme, and every profile once there is more than one, in the shape
|
||||
* `SchemeStylesheet::levels()` draws: light and dark roles at all three contrast levels
|
||||
* together, filled from the package's own default wherever the file or a profile is missing a
|
||||
* role — the same fallback `load()` gives one level at a time, given here for all three at
|
||||
* once, since a stylesheet has to carry every level in one pass.
|
||||
*
|
||||
* Which profile is *active* is left to the browser: by the time a page reads this, the theme
|
||||
* script has already written the resolved one to `<html data-scheme>` (`profile()`, embedded
|
||||
* server-side), so every profile is returned here, keyed by name, for `[data-scheme]` to select
|
||||
* between — `scheme` doubles as the plain, attribute-less blocks (`:root`, `[data-theme]`),
|
||||
* exactly as the file `material:scheme` generates gives its own default profile's roles twice.
|
||||
* `ErrorPage::fallbackStyles()`'s only path to a scheme: there is no build to serve a generated
|
||||
* `material-scheme.css`, so this draws the same colours from the same JSON instead.
|
||||
*
|
||||
* @return array{scheme: array{light: array<string, string>, dark: array<string, string>, contrast: array<string, array{light: array<string, string>, dark: array<string, string>}>}, profiles: array<string, array{label: string, light: array<string, string>, dark: array<string, string>, contrast: array<string, array{light: array<string, string>, dark: array<string, string>}>}>}
|
||||
*/
|
||||
public static function forStylesheet(?string $path = null): array
|
||||
{
|
||||
$data = static::data($path);
|
||||
$profiles = [];
|
||||
|
||||
if (is_array($data['profiles'] ?? null)) {
|
||||
foreach ($data['profiles'] as $name => $profile) {
|
||||
if (! is_string($name) || preg_match('/^[a-z0-9-]+$/', $name) !== 1 || ! is_array($profile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$profiles[$name] = [
|
||||
'label' => is_string($profile['label'] ?? null) ? $profile['label'] : Str::headline($name),
|
||||
...static::fullScheme($profile),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($profiles === []) {
|
||||
return ['scheme' => static::fullScheme($data), 'profiles' => []];
|
||||
}
|
||||
|
||||
$default = is_string($data['default'] ?? null) && isset($profiles[$data['default']])
|
||||
? $data['default']
|
||||
: (string) array_key_first($profiles);
|
||||
|
||||
return ['scheme' => $profiles[$default], 'profiles' => $profiles];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
@@ -165,6 +211,27 @@ class Scheme
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* One scheme (a single scheme's data, or one profile's) at every contrast level together, in
|
||||
* the shape `SchemeStylesheet::levels()` reads: `scheme()` already merges a level's own roles
|
||||
* over the standard ones it does not name, over the package's default, one level at a time —
|
||||
* this asks it for all three and nests medium and high under `contrast`, since a stylesheet's
|
||||
* blocks for one scheme need every level in a single call.
|
||||
*
|
||||
* @param array<mixed> $data
|
||||
* @return array{light: array<string, string>, dark: array<string, string>, contrast: array<string, array{light: array<string, string>, dark: array<string, string>}>}
|
||||
*/
|
||||
protected static function fullScheme(array $data): array
|
||||
{
|
||||
return [
|
||||
...static::scheme($data, 'standard'),
|
||||
'contrast' => [
|
||||
'medium' => static::scheme($data, 'medium'),
|
||||
'high' => static::scheme($data, 'high'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param non-empty-array<string, mixed> $profiles
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace NoNameWeb\LivewireMaterial\Support;
|
||||
|
||||
/**
|
||||
* The selector shape a Material 3 colour scheme is written in: the standard level on `:root` and
|
||||
* `[data-theme]` (or, under a prefix, repeated so it still reaches a theme set on `<html>` or on a
|
||||
* nested panel), the medium and high contrast levels under `[data-contrast]` beside it, and — when
|
||||
* a caller gives one — every colour profile again under its own prefix (`[data-scheme='name']`).
|
||||
*
|
||||
* `SchemeCommand::handle()` writes this to the application's `material-scheme.css`; plan step 40
|
||||
* moved the shape itself here so `ErrorPage::fallbackStyles()` can draw the same scheme, from
|
||||
* `Scheme::forStylesheet()` rather than a generated file, without a second copy of the selectors to
|
||||
* keep in step with the command's.
|
||||
*/
|
||||
final class SchemeStylesheet
|
||||
{
|
||||
/**
|
||||
* M3's two contrast levels beyond standard, keyed on `<html data-contrast>`; standard is the
|
||||
* scheme's own level and has no attribute of its own.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public const array LEVELS = ['medium', 'high'];
|
||||
|
||||
/**
|
||||
* One scheme, whole: the standard level first, under `$prefix` (or `:root`/`[data-theme]`
|
||||
* without one), then M3's medium and high levels under `[data-contrast]` beside it. A level's
|
||||
* blocks are the standard ones with one more attribute, so they outrank them wherever both
|
||||
* match, and the light blocks of a level come before its dark ones as in every other pair here.
|
||||
*
|
||||
* @param array{contrast: array<string, array{light: array<string, string>, dark: array<string, string>}>, light: array<string, string>, dark: array<string, string>} $scheme
|
||||
*/
|
||||
public static function levels(array $scheme, string $prefix = ''): string
|
||||
{
|
||||
[$light, $dark] = self::selectors($prefix);
|
||||
|
||||
$css = self::blocks($light, $dark, $scheme);
|
||||
|
||||
foreach (self::LEVELS as $level) {
|
||||
[$light, $dark] = self::selectors($prefix."[data-contrast='{$level}']");
|
||||
|
||||
/** @var array{light: array<string, string>, dark: array<string, string>} $roles */
|
||||
$roles = $scheme['contrast'][$level];
|
||||
|
||||
$css .= "\n".self::blocks($light, $dark, $roles);
|
||||
}
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* The light and the dark selectors of one block, under the given prefix. Without a prefix the
|
||||
* plain blocks stand on `:root` and on the attribute alone; with one, the prefix is on `<html>`
|
||||
* and the theme may be on `<html>` or on a panel inside it, so both forms are written.
|
||||
*
|
||||
* @return array{list<string>, list<string>}
|
||||
*/
|
||||
public static function selectors(string $prefix): array
|
||||
{
|
||||
if ($prefix === '') {
|
||||
return [[':root', "[data-theme='light']"], ["[data-theme='dark']"]];
|
||||
}
|
||||
|
||||
return [
|
||||
[$prefix, "{$prefix}[data-theme='light']", "{$prefix} [data-theme='light']"],
|
||||
["{$prefix}[data-theme='dark']", "{$prefix} [data-theme='dark']"],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A light and a dark block of roles under the given selectors.
|
||||
*
|
||||
* @param list<string> $light
|
||||
* @param list<string> $dark
|
||||
* @param array{light: array<string, string>, dark: array<string, string>} $scheme
|
||||
*/
|
||||
public static function blocks(array $light, array $dark, array $scheme): string
|
||||
{
|
||||
$roles = fn (array $roles): string => collect($roles)
|
||||
->map(fn (string $hex, string $role): string => " --md-sys-color-{$role}: {$hex};")
|
||||
->implode("\n");
|
||||
|
||||
return implode(",\n", $light)." {\n color-scheme: light;\n\n".$roles($scheme['light'])."\n}\n\n"
|
||||
.implode(",\n", $dark)." {\n color-scheme: dark;\n\n".$roles($scheme['dark'])."\n}\n";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user