Files
livewire-material/src/Support/SchemeStylesheet.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 3b9a889e7c 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
2026-09-15 07:29:50 +02:00

88 lines
3.7 KiB
PHP

<?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";
}
}