diff --git a/README.md b/README.md index ba0e8a82..fbdd19d0 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ php artisan material:scheme "#4f46e5" --variant=tonal-spot Variants: `tonal-spot`, `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--contrast` runs from -1 to 1; `--success`, `--warning` and `--info` seed the state colours. Regenerate instead of editing the file. +#### Colour profiles + +To let an installation switch between several schemes, list them as `profiles` in the config (name ⇒ `label`, `seed`, `variant`) and run `php artisan material:scheme` without a seed: every profile lands in the same stylesheet under ``. Tell the package which one is active — `Scheme::resolveProfileUsing(fn () => Setting::get('color_profile'))` in a service provider — and the head script, mails and error pages follow it. `` lets someone choose, previewing each profile on the page. + ### Configuration ```bash @@ -82,6 +86,7 @@ php artisan vendor:publish --tag=livewire-material-config - `prefix` — components are ``, ``… Set `'m'` when a name clashes with the application's own components, and they become ``. `` always works. - `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys` (an earlier toggle's localStorage keys, adopted once). +- `profiles`, `profile` — colour profiles and the default one (see Colour profiles). - `fields.variant` — text fields `outlined` (default) or `filled`. - `pagination` — draw Laravel's and Livewire's paginators in M3 (default `true`). - `showcase.enabled`, `showcase.path`, `showcase.middleware`, `showcase.vite`. diff --git a/config/livewire-material.php b/config/livewire-material.php index fb9fb7a5..9ad0609a 100644 --- a/config/livewire-material.php +++ b/config/livewire-material.php @@ -107,6 +107,24 @@ return [ 'scheme' => resource_path('css/material-scheme.json'), + /* + |-------------------------------------------------------------------------- + | Colour profiles + |-------------------------------------------------------------------------- + | + | Named schemes an installation can switch between. Each one is a 'label', + | a 'seed' (#rrggbb), a 'variant' and an optional 'contrast'. Without a + | seed, `php artisan material:scheme` generates every profile into one + | stylesheet keyed by . 'profile' names the default one + | (else the first); the application says which is active with + | Scheme::resolveProfileUsing(). Regenerate after changing either. + | + */ + + 'profiles' => [], + + 'profile' => null, + /* |-------------------------------------------------------------------------- | Mail diff --git a/resources/boost/guidelines/core.blade.php b/resources/boost/guidelines/core.blade.php index 14a0310f..c7aae6a9 100644 --- a/resources/boost/guidelines/core.blade.php +++ b/resources/boost/guidelines/core.blade.php @@ -5,7 +5,7 @@ This application uses `nonameweb/livewire-material`: Material 3 Expressive compo - Components are anonymous Blade components, unprefixed unless `config/livewire-material.php` sets a `prefix`. Before writing or changing a view that uses them, activate the `livewire-material-development` skill for the props, slots and traps of each component. - Never write maryUI tags (``) or daisyUI classes (`btn`, `card`, `badge`, `bg-base-200`, `text-base-content`…). They compile to nothing and fail silently. -- Every layout includes `` in `` before `@vite`. The colour scheme is generated with `php artisan material:scheme` — never edit `resources/css/material-scheme.css` by hand. +- Every layout includes `` in `` before `@vite`. The colour scheme is generated with `php artisan material:scheme` — never edit `resources/css/material-scheme.css` by hand. With colour profiles (`livewire-material.profiles`), run it without a seed after changing them; the active profile comes from `Scheme::resolveProfileUsing()`. - While the application runs locally, every token and component renders in the application's own scheme at `/material` (the showcase). - HTTP error pages and the Markdown mail theme come from the package. Change error wording by publishing `--tag=livewire-material-errors`; select the mail theme with `MAIL_MARKDOWN_THEME=livewire-material::mail.theme`. @endverbatim diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 60f44f28..a294a87a 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -46,6 +46,34 @@ php artisan material:scheme "#4f46e5" --variant=tonal-spot Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--success`, `--warning` and `--info` set the source of the state colours; `--contrast` goes from -1 to 1. The command also writes `material-scheme.json` beside the stylesheet. +### Colour profiles + +An installation that switches between several schemes lists them in `config/livewire-material.php` and runs the command without a seed, which generates every profile into the same stylesheet, keyed by ``: + +```php +'profiles' => [ + 'indigo' => ['label' => 'Indigo', 'seed' => '#4f46e5', 'variant' => 'vibrant'], + 'teal' => ['label' => 'Teal', 'seed' => '#00897b', 'variant' => 'vibrant'], +], +'profile' => 'indigo', // the default; else the first +``` + +```bash +php artisan material:scheme +``` + +- Names are lowercase letters, digits and dashes. Regenerate after changing the list; only generated profiles exist for the picker, the resolver and the stylesheet. +- The application says which profile is active, once, in a service provider. The closure runs every time a colour is drawn (head script, mail, error page), so it may read the database; a name that is not a generated profile, or a closure that throws, falls back to the default: + +```php +use NoNameWeb\LivewireMaterial\Support\Scheme; + +Scheme::resolveProfileUsing(fn (): ?string => Setting::get('color_profile')); +``` + +- `` writes the active profile to `` before the first paint; mails and error pages draw it too. `Scheme::profiles()` lists the generated profiles (name ⇒ label, light and dark roles) and `Scheme::profile()` names the active one — validate a stored choice with `Rule::in(array_keys(Scheme::profiles()))`. +- Choose with `` (see Components). Never set `data-scheme` on an element inside the page expecting a different profile there: profiles key on ``. + ## Tokens Tailwind's default palette is cleared: every colour class names an M3 role. `text-red-600`, `bg-base-200` or `text-gray-500` compile to nothing. @@ -62,7 +90,7 @@ Tailwind's default palette is cleared: every colour class names an M3 role. `tex ## Theme -`config/livewire-material.php` → `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys`. In Alpine, `$store.theme` holds `choice` (what the visitor picked), `resolved` (`light` or `dark`, what shows), `set('light'|'dark'|'system')` and `toggle()`; `x-model="$store.theme.value"` binds a control. +`config/livewire-material.php` → `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys`. In Alpine, `$store.theme` holds `choice` (what the visitor picked), `resolved` (`light` or `dark`, what shows), `set('light'|'dark'|'system')` and `toggle()`; `x-model="$store.theme.value"` binds a control. With colour profiles it also holds `scheme` (the profile on screen) and `previewScheme(name)`, which shows another profile on this page without storing anything. ## Toasts @@ -724,6 +752,14 @@ An avatar that opens a menu: `name`, `email`, `avatar` (image URL or initials; d Switches `$store.theme`: `mode="toggle"` (default, light/dark icon button), `cycle` (light → dark → system), `picker` (segmented buttons for settings pages). Every toggle on a page shares the store. +### `` + +A choice of colour profile (see Colour profiles): a swatch per generated profile — its name and its primary, secondary and tertiary colour — over native radios. `wire:model` or `x-model` (with `name`) binds the chosen name; choosing previews it on the page at once; storing it is the application's. `label`, `hint`, `name`, `profiles` (default `Scheme::profiles()`). A validation error for the bound property replaces the hint. Without profiles it renders nothing. + +```blade + +``` + ### ``, `` A data table: write plain ``, ``, ``, `` inside `` (`size="xs"` for a dense one); cell utilities (`text-end`, `whitespace-nowrap`) always win. Scrolling is yours: wrap it in `
`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`. diff --git a/resources/js/theme.js b/resources/js/theme.js index e234f378..f81dadb8 100644 --- a/resources/js/theme.js +++ b/resources/js/theme.js @@ -7,6 +7,10 @@ * * `value` is an accessor, so binding a control with `x-model="$store.theme.value"` goes * through the same write as `set()` and `toggle()`: the attributes, then localStorage. + * + * `scheme` is the colour profile on screen (, which the server chose), and + * `previewScheme(name)` shows another one on this page without storing anything — the + * application saves a choice itself, and the next full load draws what it saved. */ document.addEventListener('alpine:init', () => { const root = document.documentElement @@ -18,6 +22,7 @@ document.addEventListener('alpine:init', () => { window.Alpine.store('theme', { choice: choices.includes(root.dataset.themeChoice) ? root.dataset.themeChoice : 'system', resolved: root.dataset.theme === 'dark' ? 'dark' : 'light', + scheme: root.dataset.scheme || null, get value() { return this.choice @@ -48,6 +53,15 @@ document.addEventListener('alpine:init', () => { toggle() { this.set(this.resolved === 'dark' ? 'light' : 'dark') }, + + previewScheme(name) { + if (typeof name !== 'string' || !/^[a-z0-9-]+$/.test(name)) { + return + } + + this.scheme = name + root.dataset.scheme = name + }, }) // The head script repaints on an OS change while the choice is `system`; this keeps the diff --git a/resources/views/components/scheme-picker.blade.php b/resources/views/components/scheme-picker.blade.php new file mode 100644 index 00000000..09ad1c0b --- /dev/null +++ b/resources/views/components/scheme-picker.blade.php @@ -0,0 +1,76 @@ +{{-- A choice of colour profile: one swatch per profile `material:scheme` generated from + `livewire-material.profiles`, each its name and its primary, secondary and tertiary colour. + + + + Native radios under the swatches, so `wire:model` and `x-model` bind as on any input and the + arrow keys move the choice. Choosing one shows it on the page at once + (`$store.theme.previewScheme`); storing it — and telling `Scheme::resolveProfileUsing()` — is + the application's. The dots are the only colours not drawn from tokens: they show other + profiles than the page's, so they are custom properties set inline from the scheme file's + checked hexes, light or dark with the page. Without profiles it renders nothing. + + Props: `label`, `hint`, `name` (needed with `x-model`), `profiles` (default: every generated + profile). A validation message for the bound property replaces the hint. --}} + +@props([ + 'label' => null, + 'hint' => null, + 'name' => null, + 'profiles' => null, +]) + +@php + $profiles ??= \NoNameWeb\LivewireMaterial\Support\Scheme::profiles(); + $model = $attributes->whereStartsWith('wire:model')->first(); + $name ??= $model ?: 'scheme'; + $errorKey = $model ?: (filled($attributes->get('name')) ? (string) $attributes->get('name') : null); + $messages = $errorKey !== null && isset($errors) ? \Illuminate\Support\Arr::flatten($errors->get($errorKey)) : []; +@endphp + +@if ($profiles !== []) +
only(['class', 'wire:key'])->class('min-w-0') }}> + @if (filled($label)) + {{ $label }} + @endif + +
+ @foreach ($profiles as $profile => $scheme) + + @endforeach +
+ + @if ($messages !== []) + @foreach ($messages as $message) +

{{ $message }}

+ @endforeach + @elseif (filled($hint)) +

{{ $hint }}

+ @endif +
+@endif diff --git a/resources/views/components/theme-script.blade.php b/resources/views/components/theme-script.blade.php index 2df86b1d..f8d73a84 100644 --- a/resources/views/components/theme-script.blade.php +++ b/resources/views/components/theme-script.blade.php @@ -11,6 +11,10 @@ JSON-encoded (maryUI's `"dark"`) — is adopted once and removed. Nothing is written for a visitor who never chose, so changing `theme.default` later reaches them too. + With colour profiles (`livewire-material.profiles`, generated by `material:scheme`), the + active one — `Scheme::profile()`, which asks the application's resolver — is written to + , which the generated stylesheet keys each profile on. + The rail rides along for the theme's reason: is `expanded` or `collapsed` (`livewire-material.rail.storage_key`, falling back to `rail.default`), and a collapsible rail's width is CSS keyed on it (the `rail-collapsed:` variant). Set any later, a collapsed @@ -27,6 +31,7 @@ $rail = config('livewire-material.rail'); $settings = [ + 'scheme' => \NoNameWeb\LivewireMaterial\Support\Scheme::profile(), 'default' => in_array($theme['default'] ?? null, ['light', 'dark', 'system'], true) ? $theme['default'] : 'system', 'key' => $theme['storage_key'] ?? 'material-theme', 'legacy' => array_values($theme['legacy_keys'] ?? []), @@ -78,6 +83,10 @@ root.setAttribute('data-theme', current === 'system' ? (media.matches ? 'dark' : 'light') : current); }; + if (settings.scheme) { + root.setAttribute('data-scheme', settings.scheme); + } + root.setAttribute('data-theme-key', settings.key); root.setAttribute('data-theme-choice', choice); root.setAttribute('data-rail-key', settings.rail.key); @@ -87,7 +96,7 @@ media.addEventListener('change', apply); document.addEventListener('livewire:navigating', function (event) { - var kept = ['data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-key'].map(function (name) { + var kept = ['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-key'].map(function (name) { return [name, root.getAttribute(name)]; }); diff --git a/resources/views/showcase/layout.blade.php b/resources/views/showcase/layout.blade.php index 0ebdd824..8ab85e58 100644 --- a/resources/views/showcase/layout.blade.php +++ b/resources/views/showcase/layout.blade.php @@ -132,6 +132,19 @@
+ {{-- With colour profiles, a preview of each on this page; nothing is stored. --}} + @if (($profiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles()) !== []) + + + + + + @foreach ($profiles as $name => $profile) + + @endforeach + + @endif + diff --git a/resources/views/showcase/sections/colour.blade.php b/resources/views/showcase/sections/colour.blade.php index 58491f21..cef49cfc 100644 --- a/resources/views/showcase/sections/colour.blade.php +++ b/resources/views/showcase/sections/colour.blade.php @@ -10,6 +10,14 @@ 'Surface' => ['bg-surface', 'bg-surface-dim', 'bg-surface-bright', 'bg-surface-container-lowest', 'bg-surface-container-low', 'bg-surface-container', 'bg-surface-container-high', 'bg-surface-container-highest', 'bg-on-surface', 'bg-on-surface-variant', 'bg-inverse-surface', 'bg-inverse-on-surface', 'bg-outline', 'bg-outline-variant'], 'Ink and lines' => ['bg-body', 'bg-meta', 'bg-quiet', 'bg-structure', 'bg-chrome', 'bg-divider'], ]; + + $examples = [ + 'Colour profiles' => <<<'BLADE' +
+ +
+ BLADE, + ]; @endphp
@@ -20,6 +28,16 @@ php artisan material:scheme. Both themes side by side, whatever the page is showing.

+

+ With colour profiles in livewire-material.profiles, the command generates each one under + <html data-scheme>, and <x-scheme-picker> chooses between them. Without + profiles the picker draws nothing. +

+ + @foreach ($examples as $title => $code) + + @endforeach +
@foreach (['light', 'dark'] as $theme)
diff --git a/src/Console/SchemeCommand.php b/src/Console/SchemeCommand.php index 1af683f6..8d730df2 100644 --- a/src/Console/SchemeCommand.php +++ b/src/Console/SchemeCommand.php @@ -5,6 +5,7 @@ namespace NoNameWeb\LivewireMaterial\Console; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Str; use JsonException; use Symfony\Component\Console\Attribute\AsCommand; @@ -15,7 +16,7 @@ class SchemeCommand extends Command * @var string */ protected $signature = 'material:scheme - {seed : The source colour, as #rrggbb} + {seed? : The source colour, as #rrggbb; without it, every profile in livewire-material.profiles} {--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} @@ -26,20 +27,73 @@ class SchemeCommand extends Command /** * @var string */ - protected $description = 'Generate the application\'s Material 3 colour scheme from a seed colour'; + protected $description = 'Generate the application\'s Material 3 colour scheme from a seed colour, or every configured colour profile'; public function handle(Filesystem $files): int { $stylesheet = $this->option('output') ?: resource_path('css/material-scheme.css'); $data = preg_replace('/\.css$/', '', $stylesheet).'.json'; + if (filled($this->argument('seed'))) { + $scheme = $this->generate((string) $this->argument('seed'), (string) $this->option('variant'), (float) $this->option('contrast')); + + if ($scheme === null) { + return self::FAILURE; + } + + return $this->write($files, $stylesheet, $data, $this->stylesheet($scheme), $scheme); + } + + $profiles = config('livewire-material.profiles'); + + if (! is_array($profiles) || $profiles === []) { + $this->components->error('Give a seed colour (php artisan material:scheme "#4f46e5"), or list colour profiles in livewire-material.profiles.'); + + return self::FAILURE; + } + + $generated = []; + + foreach ($profiles as $name => $profile) { + if (! is_string($name) || preg_match('/^[a-z0-9-]+$/', $name) !== 1) { + $this->components->error("A profile's name is lowercase letters, digits and dashes; \"{$name}\" is not."); + + return self::FAILURE; + } + + $scheme = $this->generate((string) ($profile['seed'] ?? ''), (string) ($profile['variant'] ?? 'tonal-spot'), (float) ($profile['contrast'] ?? 0), "Profile \"{$name}\": "); + + if ($scheme === null) { + return self::FAILURE; + } + + $generated[$name] = ['label' => (string) ($profile['label'] ?? Str::headline($name)), ...$scheme]; + } + + $configured = config('livewire-material.profile'); + $default = is_string($configured) && isset($generated[$configured]) ? $configured : array_key_first($generated); + + return $this->write($files, $stylesheet, $data, $this->profilesStylesheet($generated, $default), [ + ...collect($generated[$default])->except('label')->all(), + 'default' => $default, + 'profiles' => $generated, + ]); + } + + /** + * One scheme from Google's colour utilities, or null once the reason has been shown. + * + * @return array{seed: string, variant: string, spec: string, contrast: float, light: array, dark: array}|null + */ + protected function generate(string $seed, string $variant, float $contrast, string $context = ''): ?array + { $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'), + 'seed' => $seed, + 'variant' => $variant, + 'contrast' => $contrast, 'success' => $this->option('success'), 'warning' => $this->option('warning'), 'info' => $this->option('info'), @@ -47,22 +101,27 @@ class SchemeCommand extends Command ]); if ($result->failed()) { - $this->components->error(trim($result->errorOutput()) ?: 'Node could not run the scheme generator. Is `node` installed and on the PATH?'); + $this->components->error($context.(trim($result->errorOutput()) ?: 'Node could not run the scheme generator. Is `node` installed and on the PATH?')); - return self::FAILURE; + return null; } try { - /** @var array{seed: string, variant: string, spec: string, contrast: float, light: array, dark: array} $scheme */ - $scheme = json_decode($result->output(), true, flags: JSON_THROW_ON_ERROR); + return json_decode($result->output(), true, flags: JSON_THROW_ON_ERROR); } catch (JsonException) { - $this->components->error('The scheme generator answered with something other than JSON.'); + $this->components->error($context.'The scheme generator answered with something other than JSON.'); - return self::FAILURE; + return null; } + } + /** + * @param array $scheme + */ + protected function write(Filesystem $files, string $stylesheet, string $data, string $css, array $scheme): int + { $files->ensureDirectoryExists(dirname($stylesheet)); - $files->put($stylesheet, $this->stylesheet($scheme)); + $files->put($stylesheet, $css); $files->put($data, json_encode($scheme, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n"); $this->components->info("Wrote {$stylesheet} and {$data}."); @@ -82,9 +141,7 @@ class SchemeCommand extends Command $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"); + $blocks = $this->blocks([':root', "[data-theme='light']"], ["[data-theme='dark']"], $scheme); return <<, dark: array}> $profiles + */ + protected function profilesStylesheet(array $profiles, string $default): string + { + $list = collect($profiles) + ->map(fn (array $profile, string $name): string => sprintf( + ' * %-12s %s, %s%s', + $name, + $profile['seed'], + $profile['variant'], + $profile['contrast'] != 0 ? ', contrast '.$profile['contrast'] : '', + )) + ->implode("\n"); + + $css = <<blocks([':root', "[data-theme='light']"], ["[data-theme='dark']"], $profiles[$default]); + + foreach ($profiles as $name => $profile) { + $css .= "\n".$this->blocks( + ["[data-scheme='{$name}']", "[data-scheme='{$name}'][data-theme='light']", "[data-scheme='{$name}'] [data-theme='light']"], + ["[data-scheme='{$name}'][data-theme='dark']", "[data-scheme='{$name}'] [data-theme='dark']"], + $profile, + ); + } + + return $css; + } + + /** + * A light and a dark block of roles under the given selectors. + * + * @param list $light + * @param list $dark + * @param array{light: array, dark: array} $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"; + } } diff --git a/src/Support/Scheme.php b/src/Support/Scheme.php index 7918ce0d..a4fdc900 100644 --- a/src/Support/Scheme.php +++ b/src/Support/Scheme.php @@ -2,9 +2,14 @@ namespace NoNameWeb\LivewireMaterial\Support; +use Closure; +use Illuminate\Support\Str; +use Throwable; + /** * The colour scheme as data, for the places CSS custom properties cannot reach: a mail client - * resolves none, and an error page whose build is missing has no stylesheet to declare them. + * resolves none, an error page whose build is missing has no stylesheet to declare them, and the + * head script has to name the active colour profile before the stylesheet applies. * * The data is the JSON `php artisan material:scheme` writes beside the stylesheet * (`livewire-material.scheme`, resources/css/material-scheme.json by default). Without that @@ -13,18 +18,51 @@ namespace NoNameWeb\LivewireMaterial\Support; * 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. * - * Read on every call and never kept: a regenerated scheme applies at once, and a long-running - * worker holds nothing. + * 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 + * asked on every call, and a name it gives that is not in the file — or a resolver that throws, as + * one reading a database may while an error page for that very database renders — falls back to + * the file's default. + * + * Read on every call and never kept: a regenerated scheme or a newly chosen profile applies at + * once, and a long-running worker holds nothing but the resolver itself. */ class Scheme { /** + * @var (Closure(): ?string)|null + */ + protected static ?Closure $profileResolver = null; + + /** + * Name the active colour profile with the given closure, asked each time a colour is drawn. + * + * @param (Closure(): ?string)|null $resolver + */ + public static function resolveProfileUsing(?Closure $resolver): void + { + static::$profileResolver = $resolver; + } + + /** + * The roles to draw: the given profile's, the active profile's, or the single scheme's. + * * @return array{light: array, dark: array} */ - public static function load(?string $path = null): array + public static function load(?string $path = null, ?string $profile = null): array { - $default = static::read(dirname(__DIR__, 2).'/resources/css/tokens/scheme.json'); - $scheme = static::read($path ?? (string) config('livewire-material.scheme')); + $data = static::data($path); + $profiles = static::profilesFrom($data); + + if ($profiles !== []) { + $name = $profile !== null && isset($profiles[$profile]) ? $profile : static::activeFrom($data, $profiles); + + 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']], @@ -37,22 +75,102 @@ class Scheme * * @return array */ - public static function light(?string $path = null): array + public static function light(?string $path = null, ?string $profile = null): array { - return static::load($path)['light']; + return static::load($path, $profile)['light']; } /** - * @return array{light: array, dark: array} + * The generated colour profiles, in the order they were configured; empty for a single scheme. + * + * @return array, dark: array}> */ - protected static function read(string $path): array + public static function profiles(?string $path = null): array { + return static::profilesFrom(static::data($path)); + } + + /** + * The active profile's name, or null for a single scheme. + */ + public static function profile(?string $path = null): ?string + { + $data = static::data($path); + $profiles = static::profilesFrom($data); + + return $profiles === [] ? null : static::activeFrom($data, $profiles); + } + + /** + * @return array + */ + protected static function data(?string $path): array + { + $path ??= (string) config('livewire-material.scheme'); $data = is_file($path) ? json_decode((string) file_get_contents($path), true) : null; - return [ - 'light' => static::roles($data['light'] ?? null), - 'dark' => static::roles($data['dark'] ?? null), - ]; + return is_array($data) ? $data : []; + } + + /** + * @param array $data + * @return array, dark: array}> + */ + protected static function profilesFrom(array $data): array + { + if (! is_array($data['profiles'] ?? null)) { + return []; + } + + $default = static::defaultScheme(); + $profiles = []; + + 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), + 'light' => [...$default['light'], ...static::roles($profile['light'] ?? null)], + 'dark' => [...$default['dark'], ...static::roles($profile['dark'] ?? null)], + ]; + } + + return $profiles; + } + + /** + * @param array $data + * @param non-empty-array $profiles + */ + protected static function activeFrom(array $data, array $profiles): string + { + try { + $resolved = static::$profileResolver ? (static::$profileResolver)() : null; + } catch (Throwable) { + $resolved = null; + } + + foreach ([$resolved, $data['default'] ?? null] as $candidate) { + if (is_string($candidate) && isset($profiles[$candidate])) { + return $candidate; + } + } + + return (string) array_key_first($profiles); + } + + /** + * The package's own scheme, which fills any role a file lacks. + * + * @return array{light: array, dark: array} + */ + protected static function defaultScheme(): array + { + $data = json_decode((string) file_get_contents(dirname(__DIR__, 2).'/resources/css/tokens/scheme.json'), true); + + return ['light' => static::roles($data['light'] ?? null), 'dark' => static::roles($data['dark'] ?? null)]; } /** diff --git a/tests/Browser/ColourProfilesTest.php b/tests/Browser/ColourProfilesTest.php new file mode 100644 index 00000000..cffd0ac2 --- /dev/null +++ b/tests/Browser/ColourProfilesTest.php @@ -0,0 +1,88 @@ + workbench_path('resources/css/material-scheme.json')]); + + $this->profiles = json_decode(file_get_contents(workbench_path('resources/css/material-scheme.json')), true)['profiles']; +}); + +afterEach(function () { + Scheme::resolveProfileUsing(null); +}); + +/** + * A #rrggbb hex as the rgb() a computed style reports. + */ +function rgb(string $hex): string +{ + [$red, $green, $blue] = sscanf($hex, '#%02x%02x%02x'); + + return "rgb({$red}, {$green}, {$blue})"; +} + +/** + * The page's primary colour role, as resolves it. + */ +function pagePrimary(): string +{ + return "getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-primary').trim()"; +} + +function profilesReady(mixed $page): mixed +{ + return $page->waitForEvent('networkidle') + ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); +} + +it('draws each profile in light and dark, and the default without the attribute', function () { + $page = profilesReady(visit('/material/colour')->inLightMode()) + ->assertScript("document.documentElement.getAttribute('data-scheme') === 'baseline'") + ->assertScript(pagePrimary()." === '{$this->profiles['baseline']['light']['primary']}'"); + + $page->script("document.documentElement.setAttribute('data-scheme', 'teal')"); + $page->assertScript(pagePrimary()." === '{$this->profiles['teal']['light']['primary']}'"); + + $page->script("document.documentElement.setAttribute('data-theme', 'dark')"); + $page->assertScript(pagePrimary()." === '{$this->profiles['teal']['dark']['primary']}'") + // A panel that sets its own theme keeps the page's profile. + ->assertScript("getComputedStyle(document.querySelector('#colour [data-theme=\"light\"] .bg-primary')).backgroundColor === '".rgb($this->profiles['teal']['light']['primary'])."'"); + + $page->script("document.documentElement.removeAttribute('data-scheme')"); + $page->assertScript(pagePrimary()." === '{$this->profiles['baseline']['dark']['primary']}'") + ->assertNoJavaScriptErrors(); +}); + +it('previews a profile from the picker and the showcase menu, and keeps it through wire:navigate', function () { + $page = profilesReady(visit('/material/colour')->inLightMode()); + + $page->click('#colour [data-scheme-option="rose"]') + ->assertScript("document.documentElement.getAttribute('data-scheme') === 'rose'") + ->assertScript(pagePrimary()." === '{$this->profiles['rose']['light']['primary']}'") + ->assertScript("window.eval(\"Alpine.store('theme').scheme\") === 'rose'"); + + $page->click('[data-test="showcase-profiles"] [aria-haspopup="menu"]') + ->click('[data-scheme-preview="teal"]') + ->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'"); + + $page->click('[data-navigation-rail-panel] a[href$="/material/buttons"]') + ->assertScript("location.pathname.endsWith('/material/buttons')") + ->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'") + ->assertNoJavaScriptErrors(); +}); + +it('paints the profile the application resolves from the first frame', function () { + Scheme::resolveProfileUsing(fn (): string => 'rose'); + + profilesReady(visit('/material')->inDarkMode()) + ->assertScript("document.documentElement.getAttribute('data-scheme') === 'rose'") + ->assertScript(pagePrimary()." === '{$this->profiles['rose']['dark']['primary']}'"); +}); diff --git a/tests/Feature/Components/SchemePickerTest.php b/tests/Feature/Components/SchemePickerTest.php new file mode 100644 index 00000000..dea189fc --- /dev/null +++ b/tests/Feature/Components/SchemePickerTest.php @@ -0,0 +1,60 @@ +path = sys_get_temp_dir().'/scheme-picker-'.uniqid().'.json'; + File::put($this->path, json_encode([ + 'default' => 'indigo', + 'profiles' => [ + 'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5', 'secondary' => '#5b5d72', 'tertiary' => '#77536d'], 'dark' => ['primary' => '#c0c1ff', 'secondary' => '#c4c5dd', 'tertiary' => '#e6bad7']], + 'teal' => ['label' => 'Teal', 'light' => ['primary' => '#00897b', 'secondary' => '#4a635f', 'tertiary' => '#456179'], 'dark' => ['primary' => '#80cbc4', 'secondary' => '#b1ccc6', 'tertiary' => '#adcae5']], + ], + ])); + config(['livewire-material.scheme' => $this->path]); +}); + +afterEach(function () { + File::delete($this->path); +}); + +it('draws a radio per generated profile, bound, named and previewing on change', function () { + $html = (string) $this->blade(''); + + expect($html) + ->toContain('Colour profile') + ->toContain('Applies after saving') + ->toContain('data-scheme-option="indigo"') + ->toContain('data-scheme-option="teal"') + ->toMatch('/toContain('x-on:change="$store.theme.previewScheme($event.target.value)"') + ->toContain('>Teal') + ->toContain('style="--swatch-light: #00897b; --swatch-dark: #80cbc4"'); +}); + +it('keeps its inline styles to the scheme file\'s checked colours', function () { + File::put($this->path, json_encode([ + 'default' => 'indigo', + 'profiles' => ['indigo' => ['label' => 'Indigo', 'light' => ['primary' => 'red; background: url(x)'], 'dark' => []]], + ])); + + $html = (string) $this->blade(''); + + preg_match_all('/style="([^"]*)"/', $html, $styles); + + expect($styles[1])->not->toBeEmpty() + ->each->toMatch('/^--swatch-light: #[0-9a-fA-F]{6}; --swatch-dark: #[0-9a-fA-F]{6}$/'); +}); + +it('shows a validation message for the bound property instead of the hint', function () { + $this->withViewErrors(['colorProfile' => ['Choose one of the colour profiles.']]) + ->blade('') + ->assertSee('Choose one of the colour profiles.') + ->assertDontSee('Applies after saving'); +}); + +it('renders nothing for a single scheme', function () { + File::put($this->path, json_encode(['light' => ['primary' => '#123456'], 'dark' => []])); + + expect(trim((string) $this->blade('')))->toBe(''); +}); diff --git a/tests/Feature/Components/ThemeScriptTest.php b/tests/Feature/Components/ThemeScriptTest.php index 0dc0bd8a..424818ac 100644 --- a/tests/Feature/Components/ThemeScriptTest.php +++ b/tests/Feature/Components/ThemeScriptTest.php @@ -1,8 +1,11 @@ blade('') - ->assertSee('({"default":"system","key":"material-theme","legacy":[],"rail":{"default":"expanded","key":"material-rail"}})', false); + ->assertSee('({"scheme":null,"default":"system","key":"material-theme","legacy":[],"rail":{"default":"expanded","key":"material-rail"}})', false); }); it('takes the application\'s default, key and legacy keys', function () { @@ -39,6 +42,28 @@ it('starts a collapsible rail as the application says, expanded otherwise', func it('puts its attributes back on when wire:navigate swaps the page', function () { $this->blade('') ->assertSee("document.addEventListener('livewire:navigating'", false) - ->assertSee("['data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-key']", false) + ->assertSee("['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-key']", false) ->assertSee('event.detail.onSwap(', false); }); + +it('names the active colour profile for , and none for a single scheme', function () { + $path = sys_get_temp_dir().'/theme-script-profiles-'.uniqid().'.json'; + File::put($path, json_encode([ + 'default' => 'indigo', + 'profiles' => ['indigo' => ['label' => 'Indigo', 'light' => [], 'dark' => []], 'teal' => ['label' => 'Teal', 'light' => [], 'dark' => []]], + ])); + config(['livewire-material.scheme' => $path]); + + try { + $this->blade('') + ->assertSee('({"scheme":"indigo",', false) + ->assertSee("root.setAttribute('data-scheme', settings.scheme);", false); + + Scheme::resolveProfileUsing(fn (): string => 'teal'); + + $this->blade('')->assertSee('({"scheme":"teal",', false); + } finally { + Scheme::resolveProfileUsing(null); + File::delete($path); + } +}); diff --git a/tests/Feature/ErrorPagesTest.php b/tests/Feature/ErrorPagesTest.php index 4f5800b1..9077cb1e 100644 --- a/tests/Feature/ErrorPagesTest.php +++ b/tests/Feature/ErrorPagesTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Vite; use Illuminate\Support\Str; use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider; +use NoNameWeb\LivewireMaterial\Support\Scheme; beforeEach(function () { $this->temporary = sys_get_temp_dir().'/livewire-material-errors-'.Str::random(8); @@ -128,6 +129,33 @@ it('still renders, in the application\'s scheme, when the Vite manifest is missi ->assertDontSee('/build/', false); }); +it('draws the active colour profile when the Vite manifest is missing', function () { + File::put($this->temporary.'/material-scheme.json', json_encode([ + 'light' => ['primary' => '#4f46e5'], + 'dark' => ['primary' => '#aaaaff'], + 'default' => 'indigo', + 'profiles' => [ + 'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5'], 'dark' => ['primary' => '#aaaaff']], + 'teal' => ['label' => 'Teal', 'light' => ['primary' => '#00897b'], 'dark' => ['primary' => '#80cbc4']], + ], + ])); + + config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']); + app()->usePublicPath($this->temporary); + Vite::useHotFile($this->temporary.'/hot'); + Scheme::resolveProfileUsing(fn (): string => 'teal'); + + try { + $this->get('/abort/500') + ->assertStatus(500) + ->assertSee('--md-sys-color-primary: #00897b;', false) + ->assertSee('--md-sys-color-primary: #80cbc4;', false) + ->assertDontSee('#4f46e5', false); + } finally { + Scheme::resolveProfileUsing(null); + } +}); + it('shows the message an application passed for 403 and 503', function (int $code, string $message) { withoutSkeletonErrorViews(); diff --git a/tests/Feature/MailThemeTest.php b/tests/Feature/MailThemeTest.php index 051a6362..f7641a50 100644 --- a/tests/Feature/MailThemeTest.php +++ b/tests/Feature/MailThemeTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\View; use Illuminate\Support\Str; use NoNameWeb\LivewireMaterial\LivewireMaterialServiceProvider; +use NoNameWeb\LivewireMaterial\Support\Scheme; class ThemedProbeMail extends Mailable { @@ -113,6 +114,27 @@ it('inlines the application\'s scheme onto the mail', function () { ->not->toContain('color-mix'); }); +it('inlines the active colour profile onto the mail', function () { + File::put($this->temporary.'/material-scheme.json', json_encode([ + 'light' => ['primary' => '#4f46e5'], + 'dark' => ['primary' => '#aaaaff'], + 'default' => 'indigo', + 'profiles' => [ + 'indigo' => ['label' => 'Indigo', 'light' => ['primary' => '#4f46e5'], 'dark' => ['primary' => '#aaaaff']], + 'rose' => ['label' => 'Rose', 'light' => ['primary' => '#c2185b'], 'dark' => ['primary' => '#ffb0c8']], + ], + ])); + + config(['livewire-material.scheme' => $this->temporary.'/material-scheme.json']); + Scheme::resolveProfileUsing(fn (): string => 'rose'); + + try { + expect(inlineStyle((new ThemedProbeMail)->render(), 'button-primary'))->toContain('background-color: #c2185b'); + } finally { + Scheme::resolveProfileUsing(null); + } +}); + it('takes the theme from a mailable too', function () { config(['mail.markdown.theme' => 'default']); diff --git a/tests/Feature/SchemeCommandTest.php b/tests/Feature/SchemeCommandTest.php index 01804dbc..d958812c 100644 --- a/tests/Feature/SchemeCommandTest.php +++ b/tests/Feature/SchemeCommandTest.php @@ -78,3 +78,68 @@ it('fails without writing anything when node cannot run', function () { expect(File::exists($this->stylesheet))->toBeFalse(); }); + +it('generates every configured profile into one stylesheet keyed by data-scheme', function () { + config([ + 'livewire-material.profiles' => [ + 'indigo' => ['label' => 'Indigo', 'seed' => '#4f46e5', 'variant' => 'vibrant'], + 'teal' => ['label' => 'Teal', 'seed' => '#00897b', 'variant' => 'vibrant'], + 'graphite' => ['seed' => '#5f6368', 'variant' => 'neutral'], + ], + 'livewire-material.profile' => 'teal', + ]); + + $this->artisan('material:scheme', ['--output' => $this->stylesheet])->assertSuccessful(); + + $scheme = json_decode(File::get($this->data), true); + $stylesheet = File::get($this->stylesheet); + + expect($scheme['default'])->toBe('teal') + ->and(array_keys($scheme['profiles']))->toBe(['indigo', 'teal', 'graphite']) + ->and($scheme['profiles']['graphite'])->label->toBe('Graphite')->variant->toBe('neutral') + ->and($scheme['profiles']['indigo']['light']['primary'])->not->toBe($scheme['profiles']['teal']['light']['primary']) + // The top level is the default profile, as a 1.0 reader expects. + ->and($scheme['light'])->toBe($scheme['profiles']['teal']['light']) + ->and($scheme['dark'])->toBe($scheme['profiles']['teal']['dark']) + ->and($scheme['seed'])->toBe('#00897b') + ->and($stylesheet)->toContain('php artisan material:scheme'."\n"); + + // The default's plain blocks come first, so a profile's single-attribute selector follows :root. + expect(strpos($stylesheet, ":root,\n[data-theme='light'] {"))->toBeLessThan(strpos($stylesheet, "[data-scheme='indigo'],")); + + foreach ($scheme['profiles'] as $name => $profile) { + foreach (['light' => "[data-scheme='{$name}'] [data-theme='light'] {", 'dark' => "[data-scheme='{$name}'] [data-theme='dark'] {"] as $theme => $selector) { + $block = Str::of($stylesheet)->after($selector)->before('}')->toString(); + + expect($block)->toContain("color-scheme: {$theme};") + ->toContain("--md-sys-color-primary: {$profile[$theme]['primary']};") + ->toContain("--md-sys-color-surface: {$profile[$theme]['surface']};"); + } + } +}); + +it('names the profile a generator error belongs to', function () { + config(['livewire-material.profiles' => ['indigo' => ['seed' => '#4f46e5'], 'broken' => ['seed' => 'teal']]]); + + $this->artisan('material:scheme', ['--output' => $this->stylesheet]) + ->expectsOutputToContain('Profile "broken"') + ->assertFailed(); + + expect(File::exists($this->stylesheet))->toBeFalse(); +}); + +it('asks for a seed or profiles when it has neither', function () { + config(['livewire-material.profiles' => []]); + + $this->artisan('material:scheme', ['--output' => $this->stylesheet]) + ->expectsOutputToContain('livewire-material.profiles') + ->assertFailed(); +}); + +it('refuses a profile name that cannot be an attribute value', function () { + config(['livewire-material.profiles' => ['Ocean Blue' => ['seed' => '#0b57d0']]]); + + $this->artisan('material:scheme', ['--output' => $this->stylesheet]) + ->expectsOutputToContain('"Ocean Blue" is not') + ->assertFailed(); +}); diff --git a/tests/Feature/SchemeTest.php b/tests/Feature/SchemeTest.php new file mode 100644 index 00000000..befcc5ef --- /dev/null +++ b/tests/Feature/SchemeTest.php @@ -0,0 +1,80 @@ + $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'); +}); diff --git a/workbench/app/Providers/WorkbenchServiceProvider.php b/workbench/app/Providers/WorkbenchServiceProvider.php index d5f6398b..d10c6bc3 100644 --- a/workbench/app/Providers/WorkbenchServiceProvider.php +++ b/workbench/app/Providers/WorkbenchServiceProvider.php @@ -23,6 +23,23 @@ class WorkbenchServiceProvider extends ServiceProvider 'workbench/resources/js/app.js', ]]); + // Three colour profiles, generated into workbench/resources/css/material-scheme.css with + // `vendor/bin/testbench material:scheme --output=workbench/resources/css/material-scheme.css`. + // Baseline is the package's own default, so the stylesheet draws it as before; the showcase + // offers the others. Tests start without profiles and point at the file when they need it. + config([ + 'livewire-material.profiles' => [ + 'baseline' => ['label' => 'Baseline', 'seed' => '#6750a4', 'variant' => 'tonal-spot'], + 'teal' => ['label' => 'Teal', 'seed' => '#00897b', 'variant' => 'vibrant'], + 'rose' => ['label' => 'Rose', 'seed' => '#c2185b', 'variant' => 'vibrant'], + ], + 'livewire-material.profile' => 'baseline', + ]); + + if (! $this->app->runningUnitTests()) { + config(['livewire-material.scheme' => workbench_path('resources/css/material-scheme.json')]); + } + Vite::useHotFile(workbench_path('public/hot')); } } diff --git a/workbench/resources/css/app.css b/workbench/resources/css/app.css index bbf052bc..60797de7 100644 --- a/workbench/resources/css/app.css +++ b/workbench/resources/css/app.css @@ -2,6 +2,7 @@ sources so the plans in docs/ are not scanned for class names. */ @import 'tailwindcss' source(none); @import '../../../resources/css/material.css'; +@import './material-scheme.css'; @source '../../../resources/views'; @source '../../../src'; diff --git a/workbench/resources/css/material-scheme.css b/workbench/resources/css/material-scheme.css new file mode 100644 index 00000000..84e0a348 --- /dev/null +++ b/workbench/resources/css/material-scheme.css @@ -0,0 +1,586 @@ +/* + * Material 3 colour profiles, generated by Google's material-color-utilities (spec 2025). + * + * php artisan material:scheme + * + * From livewire-material.profiles. "baseline" is the default, and also stands without a + * data-scheme attribute: + * + * baseline #6750a4, tonal-spot + * teal #00897b, vibrant + * rose #c2185b, vibrant + * + * 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 and data-scheme + * before the first paint. + */ + +:root, +[data-theme='light'] { + color-scheme: light; + + --md-sys-color-background: #fdf7fe; + --md-sys-color-on-background: #34313a; + --md-sys-color-surface: #fdf7fe; + --md-sys-color-surface-dim: #ded8e4; + --md-sys-color-surface-bright: #fdf7fe; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f8f1fa; + --md-sys-color-surface-container: #f2ecf5; + --md-sys-color-surface-container-high: #ece6f0; + --md-sys-color-surface-container-highest: #e7e0ec; + --md-sys-color-on-surface: #34313a; + --md-sys-color-on-surface-variant: #615d68; + --md-sys-color-outline: #7d7983; + --md-sys-color-outline-variant: #b5b0bb; + --md-sys-color-inverse-surface: #0f0d12; + --md-sys-color-inverse-on-surface: #a09ba1; + --md-sys-color-primary: #655789; + --md-sys-color-primary-dim: #594b7c; + --md-sys-color-on-primary: #fdf7ff; + --md-sys-color-primary-container: #d4c3fd; + --md-sys-color-on-primary-container: #493c6c; + --md-sys-color-primary-fixed: #d4c3fd; + --md-sys-color-primary-fixed-dim: #c6b6ee; + --md-sys-color-on-primary-fixed: #352857; + --md-sys-color-on-primary-fixed-variant: #524576; + --md-sys-color-inverse-primary: #d4c3fd; + --md-sys-color-secondary: #625c71; + --md-sys-color-secondary-dim: #565065; + --md-sys-color-on-secondary: #fdf7ff; + --md-sys-color-secondary-container: #e8def8; + --md-sys-color-on-secondary-container: #554f63; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #7b5270; + --md-sys-color-tertiary-dim: #6e4664; + --md-sys-color-on-tertiary: #fff7f9; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #a8364b; + --md-sys-color-error-dim: #6b0221; + --md-sys-color-on-error: #fff7f7; + --md-sys-color-error-container: #f97386; + --md-sys-color-on-error-container: #6e0523; + --md-sys-color-success: #006c45; + --md-sys-color-on-success: #ffffff; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #002112; + --md-sys-color-warning: #7c5800; + --md-sys-color-on-warning: #ffffff; + --md-sys-color-warning-container: #ffdea6; + --md-sys-color-on-warning-container: #271900; + --md-sys-color-info: #005ac4; + --md-sys-color-on-info: #ffffff; + --md-sys-color-info-container: #d8e2ff; + --md-sys-color-on-info-container: #001a42; + --md-sys-color-inverse-error: #f97386; + --md-sys-color-inverse-success: #69dca1; + --md-sys-color-inverse-warning: #fdbb28; + --md-sys-color-inverse-info: #aec6ff; +} + +[data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #0f0d12; + --md-sys-color-on-background: #eae3ef; + --md-sys-color-surface: #0f0d12; + --md-sys-color-surface-dim: #0f0d12; + --md-sys-color-surface-bright: #2e2b34; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #141218; + --md-sys-color-surface-container: #1b181f; + --md-sys-color-surface-container-high: #211e26; + --md-sys-color-surface-container-highest: #27242d; + --md-sys-color-on-surface: #eae3ef; + --md-sys-color-on-surface-variant: #aea9b4; + --md-sys-color-outline: #78737e; + --md-sys-color-outline-variant: #4a4650; + --md-sys-color-inverse-surface: #fdf7fe; + --md-sys-color-inverse-on-surface: #575459; + --md-sys-color-primary: #cdc0ec; + --md-sys-color-primary-dim: #bfb2de; + --md-sys-color-on-primary: #443a5f; + --md-sys-color-primary-container: #574d72; + --md-sys-color-on-primary-container: #e9deff; + --md-sys-color-primary-fixed: #ded0fe; + --md-sys-color-primary-fixed-dim: #d0c3ef; + --md-sys-color-on-primary-fixed: #3c3256; + --md-sys-color-on-primary-fixed-variant: #594e74; + --md-sys-color-inverse-primary: #645980; + --md-sys-color-secondary: #cbc2db; + --md-sys-color-secondary-dim: #beb5cd; + --md-sys-color-on-secondary: #433d51; + --md-sys-color-secondary-container: #3e384c; + --md-sys-color-on-secondary-container: #c4bbd4; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #ffcfef; + --md-sys-color-tertiary-dim: #f4bfe3; + --md-sys-color-on-tertiary: #69415f; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #f97386; + --md-sys-color-error-dim: #c44b5f; + --md-sys-color-on-error: #490013; + --md-sys-color-error-container: #871c34; + --md-sys-color-on-error-container: #ff97a3; + --md-sys-color-success: #69dca1; + --md-sys-color-on-success: #003822; + --md-sys-color-success-container: #005233; + --md-sys-color-on-success-container: #86f9bc; + --md-sys-color-warning: #fdbb28; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #5e4200; + --md-sys-color-on-warning-container: #ffdea6; + --md-sys-color-info: #aec6ff; + --md-sys-color-on-info: #002e6a; + --md-sys-color-info-container: #004396; + --md-sys-color-on-info-container: #d8e2ff; + --md-sys-color-inverse-error: #a8364b; + --md-sys-color-inverse-success: #006c45; + --md-sys-color-inverse-warning: #7c5800; + --md-sys-color-inverse-info: #005ac4; +} + +[data-scheme='baseline'], +[data-scheme='baseline'][data-theme='light'], +[data-scheme='baseline'] [data-theme='light'] { + color-scheme: light; + + --md-sys-color-background: #fdf7fe; + --md-sys-color-on-background: #34313a; + --md-sys-color-surface: #fdf7fe; + --md-sys-color-surface-dim: #ded8e4; + --md-sys-color-surface-bright: #fdf7fe; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #f8f1fa; + --md-sys-color-surface-container: #f2ecf5; + --md-sys-color-surface-container-high: #ece6f0; + --md-sys-color-surface-container-highest: #e7e0ec; + --md-sys-color-on-surface: #34313a; + --md-sys-color-on-surface-variant: #615d68; + --md-sys-color-outline: #7d7983; + --md-sys-color-outline-variant: #b5b0bb; + --md-sys-color-inverse-surface: #0f0d12; + --md-sys-color-inverse-on-surface: #a09ba1; + --md-sys-color-primary: #655789; + --md-sys-color-primary-dim: #594b7c; + --md-sys-color-on-primary: #fdf7ff; + --md-sys-color-primary-container: #d4c3fd; + --md-sys-color-on-primary-container: #493c6c; + --md-sys-color-primary-fixed: #d4c3fd; + --md-sys-color-primary-fixed-dim: #c6b6ee; + --md-sys-color-on-primary-fixed: #352857; + --md-sys-color-on-primary-fixed-variant: #524576; + --md-sys-color-inverse-primary: #d4c3fd; + --md-sys-color-secondary: #625c71; + --md-sys-color-secondary-dim: #565065; + --md-sys-color-on-secondary: #fdf7ff; + --md-sys-color-secondary-container: #e8def8; + --md-sys-color-on-secondary-container: #554f63; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #7b5270; + --md-sys-color-tertiary-dim: #6e4664; + --md-sys-color-on-tertiary: #fff7f9; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #a8364b; + --md-sys-color-error-dim: #6b0221; + --md-sys-color-on-error: #fff7f7; + --md-sys-color-error-container: #f97386; + --md-sys-color-on-error-container: #6e0523; + --md-sys-color-success: #006c45; + --md-sys-color-on-success: #ffffff; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #002112; + --md-sys-color-warning: #7c5800; + --md-sys-color-on-warning: #ffffff; + --md-sys-color-warning-container: #ffdea6; + --md-sys-color-on-warning-container: #271900; + --md-sys-color-info: #005ac4; + --md-sys-color-on-info: #ffffff; + --md-sys-color-info-container: #d8e2ff; + --md-sys-color-on-info-container: #001a42; + --md-sys-color-inverse-error: #f97386; + --md-sys-color-inverse-success: #69dca1; + --md-sys-color-inverse-warning: #fdbb28; + --md-sys-color-inverse-info: #aec6ff; +} + +[data-scheme='baseline'][data-theme='dark'], +[data-scheme='baseline'] [data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #0f0d12; + --md-sys-color-on-background: #eae3ef; + --md-sys-color-surface: #0f0d12; + --md-sys-color-surface-dim: #0f0d12; + --md-sys-color-surface-bright: #2e2b34; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #141218; + --md-sys-color-surface-container: #1b181f; + --md-sys-color-surface-container-high: #211e26; + --md-sys-color-surface-container-highest: #27242d; + --md-sys-color-on-surface: #eae3ef; + --md-sys-color-on-surface-variant: #aea9b4; + --md-sys-color-outline: #78737e; + --md-sys-color-outline-variant: #4a4650; + --md-sys-color-inverse-surface: #fdf7fe; + --md-sys-color-inverse-on-surface: #575459; + --md-sys-color-primary: #cdc0ec; + --md-sys-color-primary-dim: #bfb2de; + --md-sys-color-on-primary: #443a5f; + --md-sys-color-primary-container: #574d72; + --md-sys-color-on-primary-container: #e9deff; + --md-sys-color-primary-fixed: #ded0fe; + --md-sys-color-primary-fixed-dim: #d0c3ef; + --md-sys-color-on-primary-fixed: #3c3256; + --md-sys-color-on-primary-fixed-variant: #594e74; + --md-sys-color-inverse-primary: #645980; + --md-sys-color-secondary: #cbc2db; + --md-sys-color-secondary-dim: #beb5cd; + --md-sys-color-on-secondary: #433d51; + --md-sys-color-secondary-container: #3e384c; + --md-sys-color-on-secondary-container: #c4bbd4; + --md-sys-color-secondary-fixed: #e8def8; + --md-sys-color-secondary-fixed-dim: #dad0ea; + --md-sys-color-on-secondary-fixed: #423c50; + --md-sys-color-on-secondary-fixed-variant: #5f586e; + --md-sys-color-tertiary: #ffcfef; + --md-sys-color-tertiary-dim: #f4bfe3; + --md-sys-color-on-tertiary: #69415f; + --md-sys-color-tertiary-container: #f4bfe3; + --md-sys-color-on-tertiary-container: #5f3956; + --md-sys-color-tertiary-fixed: #f4bfe3; + --md-sys-color-tertiary-fixed-dim: #e5b2d5; + --md-sys-color-on-tertiary-fixed: #4a2642; + --md-sys-color-on-tertiary-fixed-variant: #694260; + --md-sys-color-error: #f97386; + --md-sys-color-error-dim: #c44b5f; + --md-sys-color-on-error: #490013; + --md-sys-color-error-container: #871c34; + --md-sys-color-on-error-container: #ff97a3; + --md-sys-color-success: #69dca1; + --md-sys-color-on-success: #003822; + --md-sys-color-success-container: #005233; + --md-sys-color-on-success-container: #86f9bc; + --md-sys-color-warning: #fdbb28; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #5e4200; + --md-sys-color-on-warning-container: #ffdea6; + --md-sys-color-info: #aec6ff; + --md-sys-color-on-info: #002e6a; + --md-sys-color-info-container: #004396; + --md-sys-color-on-info-container: #d8e2ff; + --md-sys-color-inverse-error: #a8364b; + --md-sys-color-inverse-success: #006c45; + --md-sys-color-inverse-warning: #7c5800; + --md-sys-color-inverse-info: #005ac4; +} + +[data-scheme='teal'], +[data-scheme='teal'][data-theme='light'], +[data-scheme='teal'] [data-theme='light'] { + color-scheme: light; + + --md-sys-color-background: #d3fffd; + --md-sys-color-on-background: #003534; + --md-sys-color-surface: #d3fffd; + --md-sys-color-surface-dim: #87e4e1; + --md-sys-color-surface-bright: #d3fffd; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #bafdfa; + --md-sys-color-surface-container: #adf5f2; + --md-sys-color-surface-container-high: #a2f0ed; + --md-sys-color-surface-container-highest: #96ece8; + --md-sys-color-on-surface: #003534; + --md-sys-color-on-surface-variant: #296463; + --md-sys-color-outline: #46807e; + --md-sys-color-outline-variant: #7db7b5; + --md-sys-color-inverse-surface: #001111; + --md-sys-color-inverse-on-surface: #6da7a5; + --md-sys-color-primary: #00675c; + --md-sys-color-primary-dim: #005a50; + --md-sys-color-on-primary: #c0fff3; + --md-sys-color-primary-container: #00f7df; + --md-sys-color-on-primary-container: #00594f; + --md-sys-color-primary-fixed: #00f7df; + --md-sys-color-primary-fixed-dim: #00e8d1; + --md-sys-color-on-primary-fixed: #00443c; + --md-sys-color-on-primary-fixed-variant: #006359; + --md-sys-color-inverse-primary: #00fee5; + --md-sys-color-secondary: #006765; + --md-sys-color-secondary-dim: #005958; + --md-sys-color-on-secondary: #bcfffc; + --md-sys-color-secondary-container: #38fbf7; + --md-sys-color-on-secondary-container: #005c5a; + --md-sys-color-secondary-fixed: #38fbf7; + --md-sys-color-secondary-fixed-dim: #10ece8; + --md-sys-color-on-secondary-fixed: #004746; + --md-sys-color-on-secondary-fixed-variant: #006765; + --md-sys-color-tertiary: #006386; + --md-sys-color-tertiary-dim: #005675; + --md-sys-color-on-tertiary: #e7f5ff; + --md-sys-color-tertiary-container: #20c0ff; + --md-sys-color-on-tertiary-container: #00374d; + --md-sys-color-tertiary-fixed: #20c0ff; + --md-sys-color-tertiary-fixed-dim: #00b2ee; + --md-sys-color-on-tertiary-fixed: #001e2b; + --md-sys-color-on-tertiary-fixed-variant: #004059; + --md-sys-color-error: #b31b25; + --md-sys-color-error-dim: #9f0519; + --md-sys-color-on-error: #ffefee; + --md-sys-color-error-container: #fb5151; + --md-sys-color-on-error-container: #570008; + --md-sys-color-success: #006c45; + --md-sys-color-on-success: #ffffff; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #002112; + --md-sys-color-warning: #7c5800; + --md-sys-color-on-warning: #ffffff; + --md-sys-color-warning-container: #ffdea6; + --md-sys-color-on-warning-container: #271900; + --md-sys-color-info: #005ac4; + --md-sys-color-on-info: #ffffff; + --md-sys-color-info-container: #d8e2ff; + --md-sys-color-on-info-container: #001a42; + --md-sys-color-inverse-error: #ff716c; + --md-sys-color-inverse-success: #69dca1; + --md-sys-color-inverse-warning: #fdbb28; + --md-sys-color-inverse-info: #aec6ff; +} + +[data-scheme='teal'][data-theme='dark'], +[data-scheme='teal'] [data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #001111; + --md-sys-color-on-background: #b8f3f1; + --md-sys-color-surface: #001111; + --md-sys-color-surface-dim: #001111; + --md-sys-color-surface-bright: #003231; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #001716; + --md-sys-color-surface-container: #001e1d; + --md-sys-color-surface-container-high: #002424; + --md-sys-color-surface-container-highest: #002b2a; + --md-sys-color-on-surface: #b8f3f1; + --md-sys-color-on-surface-variant: #7bb5b3; + --md-sys-color-outline: #457f7d; + --md-sys-color-outline-variant: #0e504f; + --md-sys-color-inverse-surface: #e3fffd; + --md-sys-color-inverse-on-surface: #215e5c; + --md-sys-color-primary: #b4fff1; + --md-sys-color-primary-dim: #00fee5; + --md-sys-color-on-primary: #00665b; + --md-sys-color-primary-container: #00fee5; + --md-sys-color-on-primary-container: #005c53; + --md-sys-color-primary-fixed: #00f7df; + --md-sys-color-primary-fixed-dim: #00e8d1; + --md-sys-color-on-primary-fixed: #00443c; + --md-sys-color-on-primary-fixed-variant: #006359; + --md-sys-color-inverse-primary: #006b60; + --md-sys-color-secondary: #38fbf7; + --md-sys-color-secondary-dim: #10ece8; + --md-sys-color-on-secondary: #005c5a; + --md-sys-color-secondary-container: #006a68; + --md-sys-color-on-secondary-container: #dafffd; + --md-sys-color-secondary-fixed: #38fbf7; + --md-sys-color-secondary-fixed-dim: #10ece8; + --md-sys-color-on-secondary-fixed: #004746; + --md-sys-color-on-secondary-fixed-variant: #006765; + --md-sys-color-tertiary: #68ccff; + --md-sys-color-tertiary-dim: #20c0ff; + --md-sys-color-on-tertiary: #00415a; + --md-sys-color-tertiary-container: #20c0ff; + --md-sys-color-on-tertiary-container: #00374d; + --md-sys-color-tertiary-fixed: #20c0ff; + --md-sys-color-tertiary-fixed-dim: #00b2ee; + --md-sys-color-on-tertiary-fixed: #001e2b; + --md-sys-color-on-tertiary-fixed-variant: #004059; + --md-sys-color-error: #ff716c; + --md-sys-color-error-dim: #d7383b; + --md-sys-color-on-error: #490006; + --md-sys-color-error-container: #9f0519; + --md-sys-color-on-error-container: #ffa8a3; + --md-sys-color-success: #69dca1; + --md-sys-color-on-success: #003822; + --md-sys-color-success-container: #005233; + --md-sys-color-on-success-container: #86f9bc; + --md-sys-color-warning: #fdbb28; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #5e4200; + --md-sys-color-on-warning-container: #ffdea6; + --md-sys-color-info: #aec6ff; + --md-sys-color-on-info: #002e6a; + --md-sys-color-info-container: #004396; + --md-sys-color-on-info-container: #d8e2ff; + --md-sys-color-inverse-error: #b31b25; + --md-sys-color-inverse-success: #006c45; + --md-sys-color-inverse-warning: #7c5800; + --md-sys-color-inverse-info: #005ac4; +} + +[data-scheme='rose'], +[data-scheme='rose'][data-theme='light'], +[data-scheme='rose'] [data-theme='light'] { + color-scheme: light; + + --md-sys-color-background: #fff4f6; + --md-sys-color-on-background: #492136; + --md-sys-color-surface: #fff4f6; + --md-sys-color-surface-dim: #ffc4de; + --md-sys-color-surface-bright: #fff4f6; + --md-sys-color-surface-container-lowest: #ffffff; + --md-sys-color-surface-container-low: #ffecf2; + --md-sys-color-surface-container: #ffe0ec; + --md-sys-color-surface-container-high: #ffd8e8; + --md-sys-color-surface-container-highest: #ffd0e4; + --md-sys-color-on-surface: #492136; + --md-sys-color-on-surface-variant: #7c4d64; + --md-sys-color-outline: #9b6880; + --md-sys-color-outline-variant: #d69db7; + --md-sys-color-inverse-surface: #220215; + --md-sys-color-inverse-on-surface: #c58da6; + --md-sys-color-primary: #ae1d53; + --md-sys-color-primary-dim: #9e0b47; + --md-sys-color-on-primary: #ffeff0; + --md-sys-color-primary-container: #ff7198; + --md-sys-color-on-primary-container: #4d001e; + --md-sys-color-primary-fixed: #ff7198; + --md-sys-color-primary-fixed-dim: #f95a8a; + --md-sys-color-on-primary-fixed: #000000; + --md-sys-color-on-primary-fixed-variant: #5e0027; + --md-sys-color-inverse-primary: #f65787; + --md-sys-color-secondary: #99366e; + --md-sys-color-secondary-dim: #8a2a62; + --md-sys-color-on-secondary: #ffeff3; + --md-sys-color-secondary-container: #ffc0dc; + --md-sys-color-on-secondary-container: #7f2159; + --md-sys-color-secondary-fixed: #ffc0dc; + --md-sys-color-secondary-fixed-dim: #ffabd2; + --md-sys-color-on-secondary-fixed: #670845; + --md-sys-color-on-secondary-fixed-variant: #8b2b63; + --md-sys-color-tertiary: #5c4bb4; + --md-sys-color-tertiary-dim: #503ea7; + --md-sys-color-on-tertiary: #f6f0ff; + --md-sys-color-tertiary-container: #b3a5ff; + --md-sys-color-on-tertiary-container: #301887; + --md-sys-color-tertiary-fixed: #b3a5ff; + --md-sys-color-tertiary-fixed-dim: #a595ff; + --md-sys-color-on-tertiary-fixed: #170059; + --md-sys-color-on-tertiary-fixed-variant: #39248f; + --md-sys-color-error: #b31b25; + --md-sys-color-error-dim: #9f0519; + --md-sys-color-on-error: #ffefee; + --md-sys-color-error-container: #fb5151; + --md-sys-color-on-error-container: #570008; + --md-sys-color-success: #006c45; + --md-sys-color-on-success: #ffffff; + --md-sys-color-success-container: #86f9bc; + --md-sys-color-on-success-container: #002112; + --md-sys-color-warning: #7c5800; + --md-sys-color-on-warning: #ffffff; + --md-sys-color-warning-container: #ffdea6; + --md-sys-color-on-warning-container: #271900; + --md-sys-color-info: #005ac4; + --md-sys-color-on-info: #ffffff; + --md-sys-color-info-container: #d8e2ff; + --md-sys-color-on-info-container: #001a42; + --md-sys-color-inverse-error: #ff716c; + --md-sys-color-inverse-success: #69dca1; + --md-sys-color-inverse-warning: #fdbb28; + --md-sys-color-inverse-info: #aec6ff; +} + +[data-scheme='rose'][data-theme='dark'], +[data-scheme='rose'] [data-theme='dark'] { + color-scheme: dark; + + --md-sys-color-background: #220215; + --md-sys-color-on-background: #ffdcea; + --md-sys-color-surface: #220215; + --md-sys-color-surface-dim: #220215; + --md-sys-color-surface-bright: #4e1737; + --md-sys-color-surface-container-lowest: #000000; + --md-sys-color-surface-container-low: #2a041b; + --md-sys-color-surface-container: #330822; + --md-sys-color-surface-container-high: #3c0d28; + --md-sys-color-surface-container-highest: #45122f; + --md-sys-color-on-surface: #ffdcea; + --md-sys-color-on-surface-variant: #d49bb4; + --md-sys-color-outline: #99667e; + --md-sys-color-outline-variant: #663a50; + --md-sys-color-inverse-surface: #fff8f8; + --md-sys-color-inverse-on-surface: #75475d; + --md-sys-color-primary: #ff8aa7; + --md-sys-color-primary-dim: #ff6b95; + --md-sys-color-on-primary: #620029; + --md-sys-color-primary-container: #ff7198; + --md-sys-color-on-primary-container: #4d001e; + --md-sys-color-primary-fixed: #ff7198; + --md-sys-color-primary-fixed-dim: #f95a8a; + --md-sys-color-on-primary-fixed: #000000; + --md-sys-color-on-primary-fixed-variant: #5e0027; + --md-sys-color-inverse-primary: #b32257; + --md-sys-color-secondary: #ff8ac6; + --md-sys-color-secondary-dim: #ef7db9; + --md-sys-color-on-secondary: #620241; + --md-sys-color-secondary-container: #7f2058; + --md-sys-color-on-secondary-container: #ffbedb; + --md-sys-color-secondary-fixed: #ffc0dc; + --md-sys-color-secondary-fixed-dim: #ffabd2; + --md-sys-color-on-secondary-fixed: #670845; + --md-sys-color-on-secondary-fixed-variant: #8b2b63; + --md-sys-color-tertiary: #aa9bff; + --md-sys-color-tertiary-dim: #9f8ffc; + --md-sys-color-on-tertiary: #290c80; + --md-sys-color-tertiary-container: #9d8cfa; + --md-sys-color-on-tertiary-container: #1c0068; + --md-sys-color-tertiary-fixed: #b3a5ff; + --md-sys-color-tertiary-fixed-dim: #a595ff; + --md-sys-color-on-tertiary-fixed: #170059; + --md-sys-color-on-tertiary-fixed-variant: #39248f; + --md-sys-color-error: #ff716c; + --md-sys-color-error-dim: #d7383b; + --md-sys-color-on-error: #490006; + --md-sys-color-error-container: #9f0519; + --md-sys-color-on-error-container: #ffa8a3; + --md-sys-color-success: #69dca1; + --md-sys-color-on-success: #003822; + --md-sys-color-success-container: #005233; + --md-sys-color-on-success-container: #86f9bc; + --md-sys-color-warning: #fdbb28; + --md-sys-color-on-warning: #412d00; + --md-sys-color-warning-container: #5e4200; + --md-sys-color-on-warning-container: #ffdea6; + --md-sys-color-info: #aec6ff; + --md-sys-color-on-info: #002e6a; + --md-sys-color-info-container: #004396; + --md-sys-color-on-info-container: #d8e2ff; + --md-sys-color-inverse-error: #b31b25; + --md-sys-color-inverse-success: #006c45; + --md-sys-color-inverse-warning: #7c5800; + --md-sys-color-inverse-info: #005ac4; +} diff --git a/workbench/resources/css/material-scheme.json b/workbench/resources/css/material-scheme.json new file mode 100644 index 00000000..560bf52c --- /dev/null +++ b/workbench/resources/css/material-scheme.json @@ -0,0 +1,566 @@ +{ + "seed": "#6750a4", + "variant": "tonal-spot", + "spec": "2025", + "contrast": 0, + "light": { + "background": "#fdf7fe", + "on-background": "#34313a", + "surface": "#fdf7fe", + "surface-dim": "#ded8e4", + "surface-bright": "#fdf7fe", + "surface-container-lowest": "#ffffff", + "surface-container-low": "#f8f1fa", + "surface-container": "#f2ecf5", + "surface-container-high": "#ece6f0", + "surface-container-highest": "#e7e0ec", + "on-surface": "#34313a", + "on-surface-variant": "#615d68", + "outline": "#7d7983", + "outline-variant": "#b5b0bb", + "inverse-surface": "#0f0d12", + "inverse-on-surface": "#a09ba1", + "primary": "#655789", + "primary-dim": "#594b7c", + "on-primary": "#fdf7ff", + "primary-container": "#d4c3fd", + "on-primary-container": "#493c6c", + "primary-fixed": "#d4c3fd", + "primary-fixed-dim": "#c6b6ee", + "on-primary-fixed": "#352857", + "on-primary-fixed-variant": "#524576", + "inverse-primary": "#d4c3fd", + "secondary": "#625c71", + "secondary-dim": "#565065", + "on-secondary": "#fdf7ff", + "secondary-container": "#e8def8", + "on-secondary-container": "#554f63", + "secondary-fixed": "#e8def8", + "secondary-fixed-dim": "#dad0ea", + "on-secondary-fixed": "#423c50", + "on-secondary-fixed-variant": "#5f586e", + "tertiary": "#7b5270", + "tertiary-dim": "#6e4664", + "on-tertiary": "#fff7f9", + "tertiary-container": "#f4bfe3", + "on-tertiary-container": "#5f3956", + "tertiary-fixed": "#f4bfe3", + "tertiary-fixed-dim": "#e5b2d5", + "on-tertiary-fixed": "#4a2642", + "on-tertiary-fixed-variant": "#694260", + "error": "#a8364b", + "error-dim": "#6b0221", + "on-error": "#fff7f7", + "error-container": "#f97386", + "on-error-container": "#6e0523", + "success": "#006c45", + "on-success": "#ffffff", + "success-container": "#86f9bc", + "on-success-container": "#002112", + "warning": "#7c5800", + "on-warning": "#ffffff", + "warning-container": "#ffdea6", + "on-warning-container": "#271900", + "info": "#005ac4", + "on-info": "#ffffff", + "info-container": "#d8e2ff", + "on-info-container": "#001a42", + "inverse-error": "#f97386", + "inverse-success": "#69dca1", + "inverse-warning": "#fdbb28", + "inverse-info": "#aec6ff" + }, + "dark": { + "background": "#0f0d12", + "on-background": "#eae3ef", + "surface": "#0f0d12", + "surface-dim": "#0f0d12", + "surface-bright": "#2e2b34", + "surface-container-lowest": "#000000", + "surface-container-low": "#141218", + "surface-container": "#1b181f", + "surface-container-high": "#211e26", + "surface-container-highest": "#27242d", + "on-surface": "#eae3ef", + "on-surface-variant": "#aea9b4", + "outline": "#78737e", + "outline-variant": "#4a4650", + "inverse-surface": "#fdf7fe", + "inverse-on-surface": "#575459", + "primary": "#cdc0ec", + "primary-dim": "#bfb2de", + "on-primary": "#443a5f", + "primary-container": "#574d72", + "on-primary-container": "#e9deff", + "primary-fixed": "#ded0fe", + "primary-fixed-dim": "#d0c3ef", + "on-primary-fixed": "#3c3256", + "on-primary-fixed-variant": "#594e74", + "inverse-primary": "#645980", + "secondary": "#cbc2db", + "secondary-dim": "#beb5cd", + "on-secondary": "#433d51", + "secondary-container": "#3e384c", + "on-secondary-container": "#c4bbd4", + "secondary-fixed": "#e8def8", + "secondary-fixed-dim": "#dad0ea", + "on-secondary-fixed": "#423c50", + "on-secondary-fixed-variant": "#5f586e", + "tertiary": "#ffcfef", + "tertiary-dim": "#f4bfe3", + "on-tertiary": "#69415f", + "tertiary-container": "#f4bfe3", + "on-tertiary-container": "#5f3956", + "tertiary-fixed": "#f4bfe3", + "tertiary-fixed-dim": "#e5b2d5", + "on-tertiary-fixed": "#4a2642", + "on-tertiary-fixed-variant": "#694260", + "error": "#f97386", + "error-dim": "#c44b5f", + "on-error": "#490013", + "error-container": "#871c34", + "on-error-container": "#ff97a3", + "success": "#69dca1", + "on-success": "#003822", + "success-container": "#005233", + "on-success-container": "#86f9bc", + "warning": "#fdbb28", + "on-warning": "#412d00", + "warning-container": "#5e4200", + "on-warning-container": "#ffdea6", + "info": "#aec6ff", + "on-info": "#002e6a", + "info-container": "#004396", + "on-info-container": "#d8e2ff", + "inverse-error": "#a8364b", + "inverse-success": "#006c45", + "inverse-warning": "#7c5800", + "inverse-info": "#005ac4" + }, + "default": "baseline", + "profiles": { + "baseline": { + "label": "Baseline", + "seed": "#6750a4", + "variant": "tonal-spot", + "spec": "2025", + "contrast": 0, + "light": { + "background": "#fdf7fe", + "on-background": "#34313a", + "surface": "#fdf7fe", + "surface-dim": "#ded8e4", + "surface-bright": "#fdf7fe", + "surface-container-lowest": "#ffffff", + "surface-container-low": "#f8f1fa", + "surface-container": "#f2ecf5", + "surface-container-high": "#ece6f0", + "surface-container-highest": "#e7e0ec", + "on-surface": "#34313a", + "on-surface-variant": "#615d68", + "outline": "#7d7983", + "outline-variant": "#b5b0bb", + "inverse-surface": "#0f0d12", + "inverse-on-surface": "#a09ba1", + "primary": "#655789", + "primary-dim": "#594b7c", + "on-primary": "#fdf7ff", + "primary-container": "#d4c3fd", + "on-primary-container": "#493c6c", + "primary-fixed": "#d4c3fd", + "primary-fixed-dim": "#c6b6ee", + "on-primary-fixed": "#352857", + "on-primary-fixed-variant": "#524576", + "inverse-primary": "#d4c3fd", + "secondary": "#625c71", + "secondary-dim": "#565065", + "on-secondary": "#fdf7ff", + "secondary-container": "#e8def8", + "on-secondary-container": "#554f63", + "secondary-fixed": "#e8def8", + "secondary-fixed-dim": "#dad0ea", + "on-secondary-fixed": "#423c50", + "on-secondary-fixed-variant": "#5f586e", + "tertiary": "#7b5270", + "tertiary-dim": "#6e4664", + "on-tertiary": "#fff7f9", + "tertiary-container": "#f4bfe3", + "on-tertiary-container": "#5f3956", + "tertiary-fixed": "#f4bfe3", + "tertiary-fixed-dim": "#e5b2d5", + "on-tertiary-fixed": "#4a2642", + "on-tertiary-fixed-variant": "#694260", + "error": "#a8364b", + "error-dim": "#6b0221", + "on-error": "#fff7f7", + "error-container": "#f97386", + "on-error-container": "#6e0523", + "success": "#006c45", + "on-success": "#ffffff", + "success-container": "#86f9bc", + "on-success-container": "#002112", + "warning": "#7c5800", + "on-warning": "#ffffff", + "warning-container": "#ffdea6", + "on-warning-container": "#271900", + "info": "#005ac4", + "on-info": "#ffffff", + "info-container": "#d8e2ff", + "on-info-container": "#001a42", + "inverse-error": "#f97386", + "inverse-success": "#69dca1", + "inverse-warning": "#fdbb28", + "inverse-info": "#aec6ff" + }, + "dark": { + "background": "#0f0d12", + "on-background": "#eae3ef", + "surface": "#0f0d12", + "surface-dim": "#0f0d12", + "surface-bright": "#2e2b34", + "surface-container-lowest": "#000000", + "surface-container-low": "#141218", + "surface-container": "#1b181f", + "surface-container-high": "#211e26", + "surface-container-highest": "#27242d", + "on-surface": "#eae3ef", + "on-surface-variant": "#aea9b4", + "outline": "#78737e", + "outline-variant": "#4a4650", + "inverse-surface": "#fdf7fe", + "inverse-on-surface": "#575459", + "primary": "#cdc0ec", + "primary-dim": "#bfb2de", + "on-primary": "#443a5f", + "primary-container": "#574d72", + "on-primary-container": "#e9deff", + "primary-fixed": "#ded0fe", + "primary-fixed-dim": "#d0c3ef", + "on-primary-fixed": "#3c3256", + "on-primary-fixed-variant": "#594e74", + "inverse-primary": "#645980", + "secondary": "#cbc2db", + "secondary-dim": "#beb5cd", + "on-secondary": "#433d51", + "secondary-container": "#3e384c", + "on-secondary-container": "#c4bbd4", + "secondary-fixed": "#e8def8", + "secondary-fixed-dim": "#dad0ea", + "on-secondary-fixed": "#423c50", + "on-secondary-fixed-variant": "#5f586e", + "tertiary": "#ffcfef", + "tertiary-dim": "#f4bfe3", + "on-tertiary": "#69415f", + "tertiary-container": "#f4bfe3", + "on-tertiary-container": "#5f3956", + "tertiary-fixed": "#f4bfe3", + "tertiary-fixed-dim": "#e5b2d5", + "on-tertiary-fixed": "#4a2642", + "on-tertiary-fixed-variant": "#694260", + "error": "#f97386", + "error-dim": "#c44b5f", + "on-error": "#490013", + "error-container": "#871c34", + "on-error-container": "#ff97a3", + "success": "#69dca1", + "on-success": "#003822", + "success-container": "#005233", + "on-success-container": "#86f9bc", + "warning": "#fdbb28", + "on-warning": "#412d00", + "warning-container": "#5e4200", + "on-warning-container": "#ffdea6", + "info": "#aec6ff", + "on-info": "#002e6a", + "info-container": "#004396", + "on-info-container": "#d8e2ff", + "inverse-error": "#a8364b", + "inverse-success": "#006c45", + "inverse-warning": "#7c5800", + "inverse-info": "#005ac4" + } + }, + "teal": { + "label": "Teal", + "seed": "#00897b", + "variant": "vibrant", + "spec": "2025", + "contrast": 0, + "light": { + "background": "#d3fffd", + "on-background": "#003534", + "surface": "#d3fffd", + "surface-dim": "#87e4e1", + "surface-bright": "#d3fffd", + "surface-container-lowest": "#ffffff", + "surface-container-low": "#bafdfa", + "surface-container": "#adf5f2", + "surface-container-high": "#a2f0ed", + "surface-container-highest": "#96ece8", + "on-surface": "#003534", + "on-surface-variant": "#296463", + "outline": "#46807e", + "outline-variant": "#7db7b5", + "inverse-surface": "#001111", + "inverse-on-surface": "#6da7a5", + "primary": "#00675c", + "primary-dim": "#005a50", + "on-primary": "#c0fff3", + "primary-container": "#00f7df", + "on-primary-container": "#00594f", + "primary-fixed": "#00f7df", + "primary-fixed-dim": "#00e8d1", + "on-primary-fixed": "#00443c", + "on-primary-fixed-variant": "#006359", + "inverse-primary": "#00fee5", + "secondary": "#006765", + "secondary-dim": "#005958", + "on-secondary": "#bcfffc", + "secondary-container": "#38fbf7", + "on-secondary-container": "#005c5a", + "secondary-fixed": "#38fbf7", + "secondary-fixed-dim": "#10ece8", + "on-secondary-fixed": "#004746", + "on-secondary-fixed-variant": "#006765", + "tertiary": "#006386", + "tertiary-dim": "#005675", + "on-tertiary": "#e7f5ff", + "tertiary-container": "#20c0ff", + "on-tertiary-container": "#00374d", + "tertiary-fixed": "#20c0ff", + "tertiary-fixed-dim": "#00b2ee", + "on-tertiary-fixed": "#001e2b", + "on-tertiary-fixed-variant": "#004059", + "error": "#b31b25", + "error-dim": "#9f0519", + "on-error": "#ffefee", + "error-container": "#fb5151", + "on-error-container": "#570008", + "success": "#006c45", + "on-success": "#ffffff", + "success-container": "#86f9bc", + "on-success-container": "#002112", + "warning": "#7c5800", + "on-warning": "#ffffff", + "warning-container": "#ffdea6", + "on-warning-container": "#271900", + "info": "#005ac4", + "on-info": "#ffffff", + "info-container": "#d8e2ff", + "on-info-container": "#001a42", + "inverse-error": "#ff716c", + "inverse-success": "#69dca1", + "inverse-warning": "#fdbb28", + "inverse-info": "#aec6ff" + }, + "dark": { + "background": "#001111", + "on-background": "#b8f3f1", + "surface": "#001111", + "surface-dim": "#001111", + "surface-bright": "#003231", + "surface-container-lowest": "#000000", + "surface-container-low": "#001716", + "surface-container": "#001e1d", + "surface-container-high": "#002424", + "surface-container-highest": "#002b2a", + "on-surface": "#b8f3f1", + "on-surface-variant": "#7bb5b3", + "outline": "#457f7d", + "outline-variant": "#0e504f", + "inverse-surface": "#e3fffd", + "inverse-on-surface": "#215e5c", + "primary": "#b4fff1", + "primary-dim": "#00fee5", + "on-primary": "#00665b", + "primary-container": "#00fee5", + "on-primary-container": "#005c53", + "primary-fixed": "#00f7df", + "primary-fixed-dim": "#00e8d1", + "on-primary-fixed": "#00443c", + "on-primary-fixed-variant": "#006359", + "inverse-primary": "#006b60", + "secondary": "#38fbf7", + "secondary-dim": "#10ece8", + "on-secondary": "#005c5a", + "secondary-container": "#006a68", + "on-secondary-container": "#dafffd", + "secondary-fixed": "#38fbf7", + "secondary-fixed-dim": "#10ece8", + "on-secondary-fixed": "#004746", + "on-secondary-fixed-variant": "#006765", + "tertiary": "#68ccff", + "tertiary-dim": "#20c0ff", + "on-tertiary": "#00415a", + "tertiary-container": "#20c0ff", + "on-tertiary-container": "#00374d", + "tertiary-fixed": "#20c0ff", + "tertiary-fixed-dim": "#00b2ee", + "on-tertiary-fixed": "#001e2b", + "on-tertiary-fixed-variant": "#004059", + "error": "#ff716c", + "error-dim": "#d7383b", + "on-error": "#490006", + "error-container": "#9f0519", + "on-error-container": "#ffa8a3", + "success": "#69dca1", + "on-success": "#003822", + "success-container": "#005233", + "on-success-container": "#86f9bc", + "warning": "#fdbb28", + "on-warning": "#412d00", + "warning-container": "#5e4200", + "on-warning-container": "#ffdea6", + "info": "#aec6ff", + "on-info": "#002e6a", + "info-container": "#004396", + "on-info-container": "#d8e2ff", + "inverse-error": "#b31b25", + "inverse-success": "#006c45", + "inverse-warning": "#7c5800", + "inverse-info": "#005ac4" + } + }, + "rose": { + "label": "Rose", + "seed": "#c2185b", + "variant": "vibrant", + "spec": "2025", + "contrast": 0, + "light": { + "background": "#fff4f6", + "on-background": "#492136", + "surface": "#fff4f6", + "surface-dim": "#ffc4de", + "surface-bright": "#fff4f6", + "surface-container-lowest": "#ffffff", + "surface-container-low": "#ffecf2", + "surface-container": "#ffe0ec", + "surface-container-high": "#ffd8e8", + "surface-container-highest": "#ffd0e4", + "on-surface": "#492136", + "on-surface-variant": "#7c4d64", + "outline": "#9b6880", + "outline-variant": "#d69db7", + "inverse-surface": "#220215", + "inverse-on-surface": "#c58da6", + "primary": "#ae1d53", + "primary-dim": "#9e0b47", + "on-primary": "#ffeff0", + "primary-container": "#ff7198", + "on-primary-container": "#4d001e", + "primary-fixed": "#ff7198", + "primary-fixed-dim": "#f95a8a", + "on-primary-fixed": "#000000", + "on-primary-fixed-variant": "#5e0027", + "inverse-primary": "#f65787", + "secondary": "#99366e", + "secondary-dim": "#8a2a62", + "on-secondary": "#ffeff3", + "secondary-container": "#ffc0dc", + "on-secondary-container": "#7f2159", + "secondary-fixed": "#ffc0dc", + "secondary-fixed-dim": "#ffabd2", + "on-secondary-fixed": "#670845", + "on-secondary-fixed-variant": "#8b2b63", + "tertiary": "#5c4bb4", + "tertiary-dim": "#503ea7", + "on-tertiary": "#f6f0ff", + "tertiary-container": "#b3a5ff", + "on-tertiary-container": "#301887", + "tertiary-fixed": "#b3a5ff", + "tertiary-fixed-dim": "#a595ff", + "on-tertiary-fixed": "#170059", + "on-tertiary-fixed-variant": "#39248f", + "error": "#b31b25", + "error-dim": "#9f0519", + "on-error": "#ffefee", + "error-container": "#fb5151", + "on-error-container": "#570008", + "success": "#006c45", + "on-success": "#ffffff", + "success-container": "#86f9bc", + "on-success-container": "#002112", + "warning": "#7c5800", + "on-warning": "#ffffff", + "warning-container": "#ffdea6", + "on-warning-container": "#271900", + "info": "#005ac4", + "on-info": "#ffffff", + "info-container": "#d8e2ff", + "on-info-container": "#001a42", + "inverse-error": "#ff716c", + "inverse-success": "#69dca1", + "inverse-warning": "#fdbb28", + "inverse-info": "#aec6ff" + }, + "dark": { + "background": "#220215", + "on-background": "#ffdcea", + "surface": "#220215", + "surface-dim": "#220215", + "surface-bright": "#4e1737", + "surface-container-lowest": "#000000", + "surface-container-low": "#2a041b", + "surface-container": "#330822", + "surface-container-high": "#3c0d28", + "surface-container-highest": "#45122f", + "on-surface": "#ffdcea", + "on-surface-variant": "#d49bb4", + "outline": "#99667e", + "outline-variant": "#663a50", + "inverse-surface": "#fff8f8", + "inverse-on-surface": "#75475d", + "primary": "#ff8aa7", + "primary-dim": "#ff6b95", + "on-primary": "#620029", + "primary-container": "#ff7198", + "on-primary-container": "#4d001e", + "primary-fixed": "#ff7198", + "primary-fixed-dim": "#f95a8a", + "on-primary-fixed": "#000000", + "on-primary-fixed-variant": "#5e0027", + "inverse-primary": "#b32257", + "secondary": "#ff8ac6", + "secondary-dim": "#ef7db9", + "on-secondary": "#620241", + "secondary-container": "#7f2058", + "on-secondary-container": "#ffbedb", + "secondary-fixed": "#ffc0dc", + "secondary-fixed-dim": "#ffabd2", + "on-secondary-fixed": "#670845", + "on-secondary-fixed-variant": "#8b2b63", + "tertiary": "#aa9bff", + "tertiary-dim": "#9f8ffc", + "on-tertiary": "#290c80", + "tertiary-container": "#9d8cfa", + "on-tertiary-container": "#1c0068", + "tertiary-fixed": "#b3a5ff", + "tertiary-fixed-dim": "#a595ff", + "on-tertiary-fixed": "#170059", + "on-tertiary-fixed-variant": "#39248f", + "error": "#ff716c", + "error-dim": "#d7383b", + "on-error": "#490006", + "error-container": "#9f0519", + "on-error-container": "#ffa8a3", + "success": "#69dca1", + "on-success": "#003822", + "success-container": "#005233", + "on-success-container": "#86f9bc", + "warning": "#fdbb28", + "on-warning": "#412d00", + "warning-container": "#5e4200", + "on-warning-container": "#ffdea6", + "info": "#aec6ff", + "on-info": "#002e6a", + "info-container": "#004396", + "on-info-container": "#d8e2ff", + "inverse-error": "#b31b25", + "inverse-success": "#006c45", + "inverse-warning": "#7c5800", + "inverse-info": "#005ac4" + } + } + } +}