Add colour profiles

An application lists named schemes in livewire-material.profiles, and
material:scheme without a seed generates them all into one stylesheet:
the default profile as the plain blocks, every profile under
<html data-scheme>, with descendant selectors so a nested data-theme
panel keeps the page's profile. The JSON keeps the 1.0 top level.

Scheme::resolveProfileUsing() lets the application name the active
profile; it is asked on every use and falls back to the default for an
unknown name or a resolver that throws. The head script writes it before
the first paint and keeps it through wire:navigate, and the mail theme
and the error pages' fallback draw it. <x-scheme-picker> chooses one and
previews it through $store.theme.previewScheme(); the showcase previews
the Workbench's profiles.

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 14:35:49 +02:00
co-authored by Claude Opus 5
parent 7798352cfc
commit fb42f004b1
22 changed files with 2006 additions and 47 deletions
+80
View File
@@ -0,0 +1,80 @@
<?php
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use NoNameWeb\LivewireMaterial\Support\Scheme;
/**
* A scheme file with colour profiles, as `material:scheme` writes one from config.
*
* @param array<string, string> $primaries profile => light primary
*/
function writeProfiles(string $path, array $primaries, string $default): void
{
$profiles = collect($primaries)->map(fn (string $primary, string $name): array => [
'label' => Str::headline($name),
'seed' => $primary,
'variant' => 'vibrant',
'spec' => '2025',
'contrast' => 0,
'light' => ['primary' => $primary, 'surface' => '#fafafa'],
'dark' => ['primary' => '#eeeeee', 'surface' => '#111111'],
])->all();
File::put($path, json_encode([...collect($profiles[$default])->except('label')->all(), 'default' => $default, 'profiles' => $profiles]));
}
beforeEach(function () {
$this->path = sys_get_temp_dir().'/material-profiles-'.Str::random(8).'.json';
writeProfiles($this->path, ['indigo' => '#4f46e5', 'teal' => '#00897b', 'rose' => '#c2185b'], 'indigo');
});
afterEach(function () {
Scheme::resolveProfileUsing(null);
File::delete($this->path);
});
it('lists the generated profiles with their labels and roles, filling missing roles from the default', function () {
$profiles = Scheme::profiles($this->path);
expect(array_keys($profiles))->toBe(['indigo', 'teal', 'rose'])
->and($profiles['teal']['label'])->toBe('Teal')
->and($profiles['teal']['light']['primary'])->toBe('#00897b')
->and($profiles['teal']['light'])->toHaveKey('on-surface-variant')
->and(Scheme::profiles(sys_get_temp_dir().'/missing-scheme.json'))->toBe([]);
});
it('follows the resolver to the active profile, and falls back to the default', function () {
expect(Scheme::profile($this->path))->toBe('indigo');
Scheme::resolveProfileUsing(fn (): string => 'teal');
expect(Scheme::profile($this->path))->toBe('teal')
->and(Scheme::light($this->path)['primary'])->toBe('#00897b')
->and(Scheme::load($this->path)['dark']['surface'])->toBe('#111111');
Scheme::resolveProfileUsing(fn (): string => 'ocean');
expect(Scheme::profile($this->path))->toBe('indigo');
Scheme::resolveProfileUsing(fn (): ?string => null);
expect(Scheme::profile($this->path))->toBe('indigo');
Scheme::resolveProfileUsing(fn () => throw new RuntimeException('The database is down.'));
expect(Scheme::profile($this->path))->toBe('indigo');
});
it('loads a profile by name, whatever is active', function () {
Scheme::resolveProfileUsing(fn (): string => 'teal');
expect(Scheme::light($this->path, 'rose')['primary'])->toBe('#c2185b')
->and(Scheme::light($this->path, 'ocean')['primary'])->toBe('#00897b');
});
it('has no profile for a single scheme, and loads it as before', function () {
File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => ['primary' => '#abcdef']]));
Scheme::resolveProfileUsing(fn (): string => 'teal');
expect(Scheme::profile($this->path))->toBeNull()
->and(Scheme::profiles($this->path))->toBe([])
->and(Scheme::light($this->path)['primary'])->toBe('#123456')
->and(Scheme::light($this->path))->toHaveKey('on-surface');
});