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
+5
View File
@@ -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 `<html data-scheme>`. 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. `<x-scheme-picker wire:model="colorProfile" />` 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 `<x-button>`, `<x-card>`… Set `'m'` when a name clashes with the application's own components, and they become `<x-m::button>`. `<x-livewire-material::button>` 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`.
+18
View File
@@ -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 <html data-scheme>. '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
+1 -1
View File
@@ -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 (`<x-mary-*>`) or daisyUI classes (`btn`, `card`, `badge`, `bg-base-200`, `text-base-content`). They compile to nothing and fail silently.
- Every layout includes `<x-theme-script />` in `<head>` before `@vite`. The colour scheme is generated with `php artisan material:scheme` never edit `resources/css/material-scheme.css` by hand.
- Every layout includes `<x-theme-script />` in `<head>` 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
@@ -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 `<html data-scheme>`:
```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'));
```
- `<x-theme-script>` writes the active profile to `<html data-scheme>` 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 `<x-scheme-picker wire:model="colorProfile" />` (see Components). Never set `data-scheme` on an element inside the page expecting a different profile there: profiles key on `<html>`.
## 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.
### `<x-scheme-picker>`
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
<x-scheme-picker :label="__('Colour profile')" wire:model="colorProfile" :hint="__('Applies to every page after saving')" />
```
### `<x-table>`, `<x-sort-header>`
A data table: write plain `<thead>`, `<tr>`, `<th>`, `<td>` inside `<x-table>` (`size="xs"` for a dense one); cell utilities (`text-end`, `whitespace-nowrap`) always win. Scrolling is yours: wrap it in `<div class="overflow-x-auto">`. A row that opens something is `data-list-row` with one `data-list-open` control; a selected row is `aria-selected="true"`.
+14
View File
@@ -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 (<html data-scheme>, 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
@@ -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.
<x-scheme-picker label="Colour profile" wire:model="colorProfile" hint="Applies to every page after saving" />
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 !== [])
<fieldset x-data data-scheme-picker {{ $attributes->only(['class', 'wire:key'])->class('min-w-0') }}>
@if (filled($label))
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
@endif
<div class="grid grid-cols-2 gap-2 sm:grid-cols-4">
@foreach ($profiles as $profile => $scheme)
<label
data-scheme-option="{{ $profile }}"
class="state-layer flex min-w-0 cursor-pointer select-none items-center gap-3 rounded-corner-lg border border-outline-variant bg-surface-container-low p-3 text-on-surface transition-[background-color,border-color] duration-(--md-sys-motion-effects-fast-duration) ease-effects-fast has-checked:border-transparent has-checked:bg-secondary-container has-checked:text-on-secondary-container has-focus-visible:outline-3 has-focus-visible:outline-offset-2 has-focus-visible:outline-secondary"
>
<input
{{ $attributes->whereStartsWith(['wire:model', 'x-model']) }}
type="radio"
name="{{ $name }}"
value="{{ $profile }}"
x-on:change="$store.theme.previewScheme($event.target.value)"
class="peer sr-only"
/>
<span class="flex shrink-0 -space-x-1.5" aria-hidden="true">
@foreach (['primary', 'secondary', 'tertiary'] as $role)
<span
style="--swatch-light: {{ $scheme['light'][$role] }}; --swatch-dark: {{ $scheme['dark'][$role] }}"
class="size-5 rounded-full bg-(--swatch-light) ring-2 ring-surface-container-low dark:bg-(--swatch-dark)"
></span>
@endforeach
</span>
<span class="min-w-0 flex-1 truncate type-label-lg">{{ __($scheme['label']) }}</span>
<x-livewire-material::icon name="check" class="hidden size-5 shrink-0 peer-checked:block" />
</label>
@endforeach
</div>
@if ($messages !== [])
@foreach ($messages as $message)
<p class="mt-1 type-body-sm text-error">{{ $message }}</p>
@endforeach
@elseif (filled($hint))
<p class="mt-1 type-body-sm text-on-surface-variant">{{ $hint }}</p>
@endif
</fieldset>
@endif
@@ -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
<html data-scheme>, which the generated stylesheet keys each profile on.
The rail rides along for the theme's reason: <html data-rail> 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)];
});
+13
View File
@@ -132,6 +132,19 @@
</div>
<x-slot:actions>
{{-- With colour profiles, a preview of each on this page; nothing is stored. --}}
@if (($profiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles()) !== [])
<x-livewire-material::menu label="Colour profile" position="bottom-end" data-test="showcase-profiles">
<x-slot:trigger>
<x-livewire-material::button icon="palette" tooltip="Colour profile" />
</x-slot:trigger>
@foreach ($profiles as $name => $profile)
<x-livewire-material::menu-item :label="$profile['label']" x-on:click="$store.theme.previewScheme('{{ $name }}')" data-scheme-preview="{{ $name }}" />
@endforeach
</x-livewire-material::menu>
@endif
<span class="ms-2 me-3 max-md:hidden"><x-livewire-material::theme-toggle mode="picker" /></span>
<span class="md:hidden"><x-livewire-material::theme-toggle mode="cycle" /></span>
</x-slot:actions>
@@ -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'
<div x-data="{ profile: $store.theme.scheme }" class="w-full max-w-3xl">
<x-scheme-picker label="Colour profile" name="profile" x-model="profile" hint="Previews on this page; an application stores the choice and names it with Scheme::resolveProfileUsing()." />
</div>
BLADE,
];
@endphp
<section id="colour" class="scroll-mt-24 space-y-6">
@@ -20,6 +28,16 @@
<code>php artisan material:scheme</code>. Both themes side by side, whatever the page is showing.
</p>
<p class="max-w-3xl type-body-md text-on-surface-variant">
With colour profiles in <code>livewire-material.profiles</code>, the command generates each one under
<code>&lt;html data-scheme&gt;</code>, and <code>&lt;x-scheme-picker&gt;</code> chooses between them. Without
profiles the picker draws nothing.
</p>
@foreach ($examples as $title => $code)
<x-showcase::example :$title :$code />
@endforeach
<div class="grid gap-4 lg:grid-cols-2">
@foreach (['light', 'dark'] as $theme)
<div data-theme="{{ $theme }}" class="space-y-6 rounded-corner-lg bg-surface p-4 text-on-surface">
+142 -28
View File
@@ -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<string, string>, dark: array<string, string>}|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<string, string>, dark: array<string, string>} $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<string, mixed> $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 <<<CSS
/*
@@ -97,19 +154,76 @@ class SchemeCommand extends Command
* first paint; the light block also stands without it.
*/
:root,
[data-theme='light'] {
color-scheme: light;
{$block($scheme['light'])}
}
[data-theme='dark'] {
color-scheme: dark;
{$block($scheme['dark'])}
}
{$blocks}
CSS;
}
/**
* The default profile as the plain blocks, then every profile under its own data-scheme. A
* profile's two-attribute selectors outrank the plain ones, and its one-attribute selector
* comes after `:root`, which it only ties with: the order is part of the format. The
* descendant selectors keep a nested `data-theme` panel (a light card on a dark page) in the
* page's profile.
*
* @param array<string, array{label: string, seed: string, variant: string, spec: string, contrast: float, light: array<string, string>, dark: array<string, string>}> $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 = <<<CSS
/*
* Material 3 colour profiles, generated by Google's material-color-utilities (spec {$profiles[$default]['spec']}).
*
* php artisan material:scheme
*
* From livewire-material.profiles. "{$default}" is the default, and also stands without a
* data-scheme attribute:
*
{$list}
*
* 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.
*/
CSS;
$css .= "\n".$this->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<string> $light
* @param list<string> $dark
* @param array{light: array<string, string>, dark: array<string, string>} $scheme
*/
protected function blocks(array $light, array $dark, array $scheme): string
{
$roles = fn (array $roles): string => collect($roles)
->map(fn (string $hex, string $role): string => " --md-sys-color-{$role}: {$hex};")
->implode("\n");
return implode(",\n", $light)." {\n color-scheme: light;\n\n".$roles($scheme['light'])."\n}\n\n"
.implode(",\n", $dark)." {\n color-scheme: dark;\n\n".$roles($scheme['dark'])."\n}\n";
}
}
+131 -13
View File
@@ -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<string, string>, dark: array<string, string>}
*/
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,24 +75,104 @@ class Scheme
*
* @return array<string, string>
*/
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<string, string>, dark: array<string, string>}
* The generated colour profiles, in the order they were configured; empty for a single scheme.
*
* @return array<string, array{label: string, light: array<string, string>, dark: array<string, string>}>
*/
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<mixed>
*/
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<mixed> $data
* @return array<string, array{label: string, light: array<string, string>, dark: array<string, string>}>
*/
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<mixed> $data
* @param non-empty-array<string, mixed> $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<string, string>, dark: array<string, string>}
*/
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)];
}
/**
* @return array<string, string>
*/
+88
View File
@@ -0,0 +1,88 @@
<?php
use NoNameWeb\LivewireMaterial\Support\Scheme;
use function Orchestra\Testbench\workbench_path;
/*
* The Workbench's three profiles (baseline, teal, rose), generated into its stylesheet. Tests other
* than these run without profiles, so each one here points the package at the scheme file.
*/
beforeEach(function () {
config(['livewire-material.scheme' => 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 <html> 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']}'");
});
@@ -0,0 +1,60 @@
<?php
use Illuminate\Support\Facades\File;
beforeEach(function () {
$this->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('<x-scheme-picker label="Colour profile" hint="Applies after saving" wire:model="colorProfile" />');
expect($html)
->toContain('<legend class="mb-2 type-label-lg text-on-surface-variant">Colour profile</legend>')
->toContain('Applies after saving')
->toContain('data-scheme-option="indigo"')
->toContain('data-scheme-option="teal"')
->toMatch('/<input\s+wire:model="colorProfile"\s+type="radio"\s+name="colorProfile"\s+value="teal"/')
->toContain('x-on:change="$store.theme.previewScheme($event.target.value)"')
->toContain('>Teal</span>')
->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('<x-scheme-picker wire:model="colorProfile" />');
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('<x-scheme-picker wire:model="colorProfile" hint="Applies after saving" />')
->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('<x-scheme-picker wire:model="colorProfile" />')))->toBe('');
});
+27 -2
View File
@@ -1,8 +1,11 @@
<?php
use Illuminate\Support\Facades\File;
use NoNameWeb\LivewireMaterial\Support\Scheme;
it('follows the operating system until the visitor chooses, by default', function () {
$this->blade('<x-theme-script />')
->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 <html> when wire:navigate swaps the page', function () {
$this->blade('<x-theme-script />')
->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 <html data-scheme>, 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('<x-theme-script />')
->assertSee('({"scheme":"indigo",', false)
->assertSee("root.setAttribute('data-scheme', settings.scheme);", false);
Scheme::resolveProfileUsing(fn (): string => 'teal');
$this->blade('<x-theme-script />')->assertSee('({"scheme":"teal",', false);
} finally {
Scheme::resolveProfileUsing(null);
File::delete($path);
}
});
+28
View File
@@ -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();
+22
View File
@@ -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']);
+65
View File
@@ -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();
});
+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');
});
@@ -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'));
}
}
+1
View File
@@ -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';
+586
View File
@@ -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;
}
@@ -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"
}
}
}
}