Add the Material 3 Expressive foundation
tests / lint (push) Successful in 1m0s
tests / feature (8.4) (push) Successful in 1m0s
tests / feature (8.5) (push) Successful in 1m1s
tests / browser (safari, webkit) (push) Successful in 1m59s
tests / browser (chrome, chromium) (push) Successful in 1m49s
tests / browser (firefox, firefox) (push) Successful in 1m54s

Colour, shape, type, elevation and motion as tokens and Tailwind
utilities; `php artisan material:scheme`, which generates an app's colour
roles with Google's material-color-utilities (spec 2025); the theme head
script with light, dark and system and its Alpine store; Google Sans Flex;
every Material Symbol (4,135, outlined and filled) drawn by <x-icon>
without blade-icons; all 35 M3 Expressive shapes, ported from androidx, as
<x-shape>; the x-figure directive; the Toasts concern; DesignGuard for
applications' tests; and a showcase with every token, both themes side by
side and an icon search.

Colour utilities are `@theme inline`, so a section with its own
data-theme repaints; without it they resolve once on :root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 05:24:11 +02:00
co-authored by Claude Opus 5
parent 9b53891a8e
commit b48e879254
8364 changed files with 12720 additions and 28 deletions
+115
View File
@@ -0,0 +1,115 @@
<?php
namespace NoNameWeb\LivewireMaterial\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Facades\Process;
use JsonException;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'material:scheme')]
class SchemeCommand extends Command
{
/**
* @var string
*/
protected $signature = 'material:scheme
{seed : The source colour, as #rrggbb}
{--variant=tonal-spot : tonal-spot, vibrant, expressive, neutral, fidelity, content, monochrome, rainbow or fruit-salad}
{--contrast=0 : The contrast level, from -1 to 1}
{--success=#22a06b : The source of the success colour}
{--warning=#e2a400 : The source of the warning colour}
{--info=#1d7afc : The source of the info colour}
{--output= : The stylesheet to write, resources/css/material-scheme.css by default}';
/**
* @var string
*/
protected $description = 'Generate the application\'s Material 3 colour scheme from a seed colour';
public function handle(Filesystem $files): int
{
$stylesheet = $this->option('output') ?: resource_path('css/material-scheme.css');
$data = preg_replace('/\.css$/', '', $stylesheet).'.json';
$result = Process::run([
config('livewire-material.node', 'node'),
__DIR__.'/../../resources/node/scheme.mjs',
json_encode([
'seed' => $this->argument('seed'),
'variant' => $this->option('variant'),
'contrast' => (float) $this->option('contrast'),
'success' => $this->option('success'),
'warning' => $this->option('warning'),
'info' => $this->option('info'),
]),
]);
if ($result->failed()) {
$this->components->error(trim($result->errorOutput()) ?: 'Node could not run the scheme generator. Is `node` installed and on the PATH?');
return self::FAILURE;
}
try {
/** @var array{seed: string, variant: string, spec: string, contrast: float, light: array<string, string>, dark: array<string, string>} $scheme */
$scheme = json_decode($result->output(), true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
$this->components->error('The scheme generator answered with something other than JSON.');
return self::FAILURE;
}
$files->ensureDirectoryExists(dirname($stylesheet));
$files->put($stylesheet, $this->stylesheet($scheme));
$files->put($data, json_encode($scheme, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n");
$this->components->info("Wrote {$stylesheet} and {$data}.");
return self::SUCCESS;
}
/**
* @param array{seed: string, variant: string, spec: string, contrast: float, light: array<string, string>, dark: array<string, string>} $scheme
*/
protected function stylesheet(array $scheme): string
{
$command = sprintf(
'php artisan material:scheme "%s" --variant=%s%s',
$scheme['seed'],
$scheme['variant'],
$scheme['contrast'] != 0 ? ' --contrast='.$scheme['contrast'] : '',
);
$block = fn (array $roles): string => collect($roles)
->map(fn (string $hex, string $role): string => " --md-sys-color-{$role}: {$hex};")
->implode("\n");
return <<<CSS
/*
* Material 3 colour roles, generated by Google's material-color-utilities (spec {$scheme['spec']}).
*
* {$command}
*
* Regenerate rather than editing a value: every pair here (a role and its on-role) carries
* M3's contrast guarantee only as generated. The head script sets data-theme before the
* first paint; the light block also stands without it.
*/
:root,
[data-theme='light'] {
color-scheme: light;
{$block($scheme['light'])}
}
[data-theme='dark'] {
color-scheme: dark;
{$block($scheme['dark'])}
}
CSS;
}
}