Merge branch 'worktree-agent-a9537eabbf38e0608'
# Conflicts: # resources/views/components/scheme-picker.blade.php # resources/views/components/theme-script.blade.php # resources/views/showcase/sections/colour.blade.php # tests/Feature/Components/ThemeScriptTest.php
This commit is contained in:
@@ -44,7 +44,18 @@ The scheme is generated, never hand-edited. Regenerate it with the seed and vari
|
||||
php artisan material:scheme "#4f46e5" --variant=tonal-spot
|
||||
```
|
||||
|
||||
Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--spec` is the colour spec: `2025` (default, M3 Expressive) or `2021` (M3's original colour — keep it for a palette generated before Expressive; the library itself uses 2021 for variants 2025 does not define, and the header records the spec actually used). `--success`, `--warning` and `--info` set the source of the state colours; `--contrast` goes from -1 to 1. The header of the stylesheet records the whole command, every option that differs from its default included. The command also writes `material-scheme.json` beside the stylesheet.
|
||||
Variants: `tonal-spot` (M3's default), `vibrant`, `expressive`, `neutral`, `fidelity`, `content`, `monochrome`, `rainbow`, `fruit-salad`. `--spec` is the colour spec: `2025` (default, M3 Expressive) or `2021` (M3's original colour — keep it for a palette generated before Expressive; the library itself uses 2021 for variants 2025 does not define, and the header records the spec actually used). `--success`, `--warning` and `--info` set the source of the state colours; `--harmonize` pulls those three towards the seed (off by default: a state has to stay recognisable). The header of the stylesheet records the whole command, every option that differs from its default included. The command also writes `material-scheme.json` beside the stylesheet.
|
||||
|
||||
`success`, `warning` and `info` are built exactly as M3 builds `error` — dynamic colours on their own tonal palette — so they follow the scheme's spec, its dark tones and its contrast level like every other role. Use them as roles (`bg-success`, `text-on-warning-container`), never as a hex.
|
||||
|
||||
### Contrast levels
|
||||
|
||||
Every scheme is generated at M3's three levels: standard, medium (3:1) and high (7:1), for light and dark and for every profile. `--contrast` (and a profile's `contrast`) moves the **standard** level only and must stay below 0.5; medium and high are Google's fixed levels and are always written, under `[data-contrast='medium']` and `[data-contrast='high']`.
|
||||
|
||||
- `<html data-contrast>` is the level on screen, written by `<x-theme-script>` before the first paint; standard writes no attribute, because the plain blocks are already standard. No stylesheet ever asks `prefers-contrast` — the head script does, once.
|
||||
- `theme.contrast.default` is `system` (follow the operating system), `standard`, `medium` or `high`, kept in localStorage under `theme.contrast.storage_key`.
|
||||
- In Alpine: `$store.theme.contrast` (the choice), `$store.theme.resolvedContrast` (the level showing) and `$store.theme.setContrast('high')`. `<x-theme-toggle mode="contrast">` is the ready-made row of three.
|
||||
- Nothing in a template names a level: a role's value changes underneath it. Never set `data-contrast` on an element to make a corner of the page higher-contrast — the level is the visitor's, page-wide.
|
||||
|
||||
### Colour profiles
|
||||
|
||||
@@ -62,7 +73,7 @@ An installation that switches between several schemes lists them in `config/live
|
||||
php artisan material:scheme
|
||||
```
|
||||
|
||||
- Each profile: `seed`, and optionally `label` (default: the name as a headline), `variant` (default `tonal-spot`), `contrast` (default 0), `spec`, `success`, `warning`, `info` (for these four, without the key the command's `--spec`, `--success`, `--warning`, `--info` or their defaults apply).
|
||||
- Each profile: `seed`, and optionally `label` (default: the name as a headline), `variant` (default `tonal-spot`), `contrast` (default 0, below 0.5), `harmonize`, `spec`, `success`, `warning`, `info` (for these five, without the key the command's `--harmonize`, `--spec`, `--success`, `--warning`, `--info` or their defaults apply). Every profile is generated at all three contrast levels, keyed on `[data-scheme='x'][data-contrast='high']` and so on.
|
||||
- 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:
|
||||
|
||||
@@ -72,7 +83,7 @@ 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()))`.
|
||||
- `<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()))`. Both take a contrast level as their last argument (`Scheme::load($path, $profile, 'high')`, `Scheme::profiles(contrast: 'medium')`); without one they answer with the standard level, which is what a mail wears.
|
||||
- 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
|
||||
@@ -92,9 +103,9 @@ 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`, `theme.meta`. 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.
|
||||
`config/livewire-material.php` → `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys`, `theme.meta`, `theme.contrast` (`default` and `storage_key`, see Contrast levels) and `motion.scheme` (`expressive`, the default, or `standard` — M3's restrained springs, written to `<html data-motion>`). 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. It also holds `contrast`, `resolvedContrast` and `setContrast()` for the contrast level. With colour profiles it holds `scheme` (the profile on screen) and `previewScheme(name)`, which shows another profile on this page without storing anything.
|
||||
|
||||
`theme.meta` (default `false`) keeps the browser's bar in the page's colour, for an installed web app: the head script sets the `content` of every `<meta name="theme-color">` without a `media` attribute to the resolved theme's `surface` — of the profile in `<html data-scheme>` — before the first paint, adding one to `<head>` when there is none. It follows every later change of `data-theme` or `data-scheme` (`$store.theme.set()`/`toggle()`, an OS change while `system`, `previewScheme()`), and paints the next page's meta after `wire:navigate`. A theme-color meta the layout renders itself goes before `<x-theme-script />` (after it, the script has already added one, and the page ends up with two), or is left out. A `media="(prefers-color-scheme: …)"` pair follows the OS instead of the visitor's choice: drop it when turning this on.
|
||||
`theme.meta` (default `false`) keeps the browser's bar in the page's colour, for an installed web app: the head script sets the `content` of every `<meta name="theme-color">` without a `media` attribute to the resolved theme's `surface` — of the profile in `<html data-scheme>`, at the level in `<html data-contrast>` — before the first paint, adding one to `<head>` when there is none. It follows every later change of `data-theme`, `data-scheme` or `data-contrast` (`$store.theme.set()`/`toggle()`, `setContrast()`, an OS change while `system`, `previewScheme()`), and paints the next page's meta after `wire:navigate`. A theme-color meta the layout renders itself goes before `<x-theme-script />` (after it, the script has already added one, and the page ends up with two), or is left out. A `media="(prefers-color-scheme: …)"` pair follows the OS instead of the visitor's choice: drop it when turning this on.
|
||||
|
||||
## Safe areas
|
||||
|
||||
@@ -808,11 +819,11 @@ An avatar that opens a menu: `name`, `email`, `avatar` (image URL or initials; d
|
||||
|
||||
### `<x-theme-toggle>`
|
||||
|
||||
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.
|
||||
Switches `$store.theme`: `mode="toggle"` (default, light/dark icon button), `cycle` (light → dark → system), `picker` (a row of three for settings pages), `contrast` (the same row for M3's standard, medium and high levels, marking the one in force). 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.
|
||||
A choice of colour profile (see Colour profiles): a swatch per generated profile — its name and its primary, secondary and tertiary colour, at the contrast level on screen — 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')" />
|
||||
|
||||
+320
-29
@@ -6,6 +6,11 @@
|
||||
* Regenerate rather than editing a value: every pair here (a role and its on-role) carries
|
||||
* M3's contrast guarantee only as generated. The head script sets data-theme before the
|
||||
* first paint; the light block also stands without it.
|
||||
*
|
||||
* M3's medium and high contrast levels follow the standard blocks, keyed on
|
||||
* data-contrast — which the head script writes from the visitor's choice, or from the
|
||||
* operating system's contrast setting while that choice is `system`. No media query
|
||||
* decides here, as none decides the theme.
|
||||
*/
|
||||
|
||||
:root,
|
||||
@@ -61,22 +66,22 @@
|
||||
--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: #006d46;
|
||||
--md-sys-color-on-success: #e7ffed;
|
||||
--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-on-success-container: #00734a;
|
||||
--md-sys-color-warning: #7c5900;
|
||||
--md-sys-color-on-warning: #fff8f1;
|
||||
--md-sys-color-warning-container: #fab925;
|
||||
--md-sys-color-on-warning-container: #6a4b00;
|
||||
--md-sys-color-info: #005bc5;
|
||||
--md-sys-color-on-info: #f9f8ff;
|
||||
--md-sys-color-info-container: #1c7afc;
|
||||
--md-sys-color-on-info-container: #001435;
|
||||
--md-sys-color-inverse-error: #f97386;
|
||||
--md-sys-color-inverse-success: #69dca1;
|
||||
--md-sys-color-inverse-warning: #fdbb28;
|
||||
--md-sys-color-inverse-info: #aec6ff;
|
||||
--md-sys-color-inverse-success: #3bb27b;
|
||||
--md-sys-color-inverse-warning: #f3b31e;
|
||||
--md-sys-color-inverse-info: #699cff;
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
@@ -131,20 +136,306 @@
|
||||
--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-success: #3bb27b;
|
||||
--md-sys-color-on-success: #002615;
|
||||
--md-sys-color-success-container: #059460;
|
||||
--md-sys-color-on-success-container: #001e10;
|
||||
--md-sys-color-warning: #f3b31e;
|
||||
--md-sys-color-on-warning: #4e3600;
|
||||
--md-sys-color-warning-container: #e4a604;
|
||||
--md-sys-color-on-warning-container: #593f00;
|
||||
--md-sys-color-info: #699cff;
|
||||
--md-sys-color-on-info: #001e4b;
|
||||
--md-sys-color-info-container: #0c74f6;
|
||||
--md-sys-color-on-info-container: #000a23;
|
||||
--md-sys-color-inverse-error: #a8364b;
|
||||
--md-sys-color-inverse-success: #006c45;
|
||||
--md-sys-color-inverse-warning: #7c5800;
|
||||
--md-sys-color-inverse-info: #005ac4;
|
||||
--md-sys-color-inverse-success: #006d46;
|
||||
--md-sys-color-inverse-warning: #7c5900;
|
||||
--md-sys-color-inverse-info: #005bc5;
|
||||
}
|
||||
|
||||
[data-contrast='medium'],
|
||||
[data-contrast='medium'][data-theme='light'],
|
||||
[data-contrast='medium'] [data-theme='light'] {
|
||||
color-scheme: light;
|
||||
|
||||
--md-sys-color-background: #fdf7fe;
|
||||
--md-sys-color-on-background: #25232b;
|
||||
--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: #25232b;
|
||||
--md-sys-color-on-surface-variant: #45414b;
|
||||
--md-sys-color-outline: #615d68;
|
||||
--md-sys-color-outline-variant: #7d7983;
|
||||
--md-sys-color-inverse-surface: #0f0d12;
|
||||
--md-sys-color-inverse-on-surface: #c8c3c9;
|
||||
--md-sys-color-primary: #483b6b;
|
||||
--md-sys-color-primary-dim: #3c305f;
|
||||
--md-sys-color-on-primary: #e1d3ff;
|
||||
--md-sys-color-primary-container: #7b6da0;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-primary-fixed: #7b6da0;
|
||||
--md-sys-color-primary-fixed-dim: #6e6093;
|
||||
--md-sys-color-on-primary-fixed: #ffffff;
|
||||
--md-sys-color-on-primary-fixed-variant: #ffffff;
|
||||
--md-sys-color-inverse-primary: #d4c3fd;
|
||||
--md-sys-color-secondary: #464054;
|
||||
--md-sys-color-secondary-dim: #3a3548;
|
||||
--md-sys-color-on-secondary: #dfd5ef;
|
||||
--md-sys-color-secondary-container: #787187;
|
||||
--md-sys-color-on-secondary-container: #ffffff;
|
||||
--md-sys-color-secondary-fixed: #787187;
|
||||
--md-sys-color-secondary-fixed-dim: #6c657b;
|
||||
--md-sys-color-on-secondary-fixed: #ffffff;
|
||||
--md-sys-color-on-secondary-fixed-variant: #ffffff;
|
||||
--md-sys-color-tertiary: #5c3653;
|
||||
--md-sys-color-tertiary-dim: #502b47;
|
||||
--md-sys-color-on-tertiary: #ffcaee;
|
||||
--md-sys-color-tertiary-container: #936787;
|
||||
--md-sys-color-on-tertiary-container: #ffffff;
|
||||
--md-sys-color-tertiary-fixed: #936787;
|
||||
--md-sys-color-tertiary-fixed-dim: #855b7a;
|
||||
--md-sys-color-on-tertiary-fixed: #ffffff;
|
||||
--md-sys-color-on-tertiary-fixed-variant: #ffffff;
|
||||
--md-sys-color-error: #821830;
|
||||
--md-sys-color-error-dim: #6b0221;
|
||||
--md-sys-color-on-error: #ffcdd1;
|
||||
--md-sys-color-error-container: #c44b5f;
|
||||
--md-sys-color-on-error-container: #ffffff;
|
||||
--md-sys-color-success: #004d30;
|
||||
--md-sys-color-on-success: #7df0b3;
|
||||
--md-sys-color-success-container: #008656;
|
||||
--md-sys-color-on-success-container: #ffffff;
|
||||
--md-sys-color-warning: #593e00;
|
||||
--md-sys-color-on-warning: #ffd385;
|
||||
--md-sys-color-warning-container: #986d00;
|
||||
--md-sys-color-on-warning-container: #ffffff;
|
||||
--md-sys-color-info: #003f8e;
|
||||
--md-sys-color-on-info: #cbd9ff;
|
||||
--md-sys-color-info-container: #006fef;
|
||||
--md-sys-color-on-info-container: #ffffff;
|
||||
--md-sys-color-inverse-error: #ff9da8;
|
||||
--md-sys-color-inverse-success: #5ace94;
|
||||
--md-sys-color-inverse-warning: #f3b31e;
|
||||
--md-sys-color-inverse-info: #98b8ff;
|
||||
}
|
||||
|
||||
[data-contrast='medium'][data-theme='dark'],
|
||||
[data-contrast='medium'] [data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--md-sys-color-background: #0f0d12;
|
||||
--md-sys-color-on-background: #ffffff;
|
||||
--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: #ffffff;
|
||||
--md-sys-color-on-surface-variant: #bcb6c2;
|
||||
--md-sys-color-outline: #96919c;
|
||||
--md-sys-color-outline-variant: #78737e;
|
||||
--md-sys-color-inverse-surface: #fdf7fe;
|
||||
--md-sys-color-inverse-on-surface: #39373c;
|
||||
--md-sys-color-primary: #cdc0ec;
|
||||
--md-sys-color-primary-dim: #bfb2de;
|
||||
--md-sys-color-on-primary: #3a3054;
|
||||
--md-sys-color-primary-container: #7a6f96;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-primary-fixed: #ded0fe;
|
||||
--md-sys-color-primary-fixed-dim: #d0c3ef;
|
||||
--md-sys-color-on-primary-fixed: #180e30;
|
||||
--md-sys-color-on-primary-fixed-variant: #3c3256;
|
||||
--md-sys-color-inverse-primary: #5a4f75;
|
||||
--md-sys-color-secondary: #cbc2db;
|
||||
--md-sys-color-secondary-dim: #beb5cd;
|
||||
--md-sys-color-on-secondary: #393347;
|
||||
--md-sys-color-secondary-container: #787187;
|
||||
--md-sys-color-on-secondary-container: #ffffff;
|
||||
--md-sys-color-secondary-fixed: #e8def8;
|
||||
--md-sys-color-secondary-fixed-dim: #dad0ea;
|
||||
--md-sys-color-on-secondary-fixed: #221d2f;
|
||||
--md-sys-color-on-secondary-fixed-variant: #423c50;
|
||||
--md-sys-color-tertiary: #ffcfef;
|
||||
--md-sys-color-tertiary-dim: #f4bfe3;
|
||||
--md-sys-color-on-tertiary: #5e3855;
|
||||
--md-sys-color-tertiary-container: #f4bfe3;
|
||||
--md-sys-color-on-tertiary-container: #542f4c;
|
||||
--md-sys-color-tertiary-fixed: #f4bfe3;
|
||||
--md-sys-color-tertiary-fixed-dim: #e5b2d5;
|
||||
--md-sys-color-on-tertiary-fixed: #180015;
|
||||
--md-sys-color-on-tertiary-fixed-variant: #4a2642;
|
||||
--md-sys-color-error: #ff9da8;
|
||||
--md-sys-color-error-dim: #ff8695;
|
||||
--md-sys-color-on-error: #5f001c;
|
||||
--md-sys-color-error-container: #c44b5f;
|
||||
--md-sys-color-on-error-container: #ffffff;
|
||||
--md-sys-color-success: #5ace94;
|
||||
--md-sys-color-on-success: #00341e;
|
||||
--md-sys-color-success-container: #008656;
|
||||
--md-sys-color-on-success-container: #ffffff;
|
||||
--md-sys-color-warning: #f3b31e;
|
||||
--md-sys-color-on-warning: #412d00;
|
||||
--md-sys-color-warning-container: #e4a604;
|
||||
--md-sys-color-on-warning-container: #332200;
|
||||
--md-sys-color-info: #98b8ff;
|
||||
--md-sys-color-on-info: #002a62;
|
||||
--md-sys-color-info-container: #006fef;
|
||||
--md-sys-color-on-info-container: #ffffff;
|
||||
--md-sys-color-inverse-error: #821830;
|
||||
--md-sys-color-inverse-success: #004d30;
|
||||
--md-sys-color-inverse-warning: #593e00;
|
||||
--md-sys-color-inverse-info: #003f8e;
|
||||
}
|
||||
|
||||
[data-contrast='high'],
|
||||
[data-contrast='high'][data-theme='light'],
|
||||
[data-contrast='high'] [data-theme='light'] {
|
||||
color-scheme: light;
|
||||
|
||||
--md-sys-color-background: #fdf7fe;
|
||||
--md-sys-color-on-background: #000000;
|
||||
--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: #000000;
|
||||
--md-sys-color-on-surface-variant: #25232b;
|
||||
--md-sys-color-outline: #45414b;
|
||||
--md-sys-color-outline-variant: #54505a;
|
||||
--md-sys-color-inverse-surface: #0f0d12;
|
||||
--md-sys-color-inverse-on-surface: #ffffff;
|
||||
--md-sys-color-primary: #281b49;
|
||||
--md-sys-color-primary-dim: #1e103f;
|
||||
--md-sys-color-on-primary: #e1d3ff;
|
||||
--md-sys-color-primary-container: #584a7b;
|
||||
--md-sys-color-on-primary-container: #ffffff;
|
||||
--md-sys-color-primary-fixed: #584a7b;
|
||||
--md-sys-color-primary-fixed-dim: #4c3f6f;
|
||||
--md-sys-color-on-primary-fixed: #ffffff;
|
||||
--md-sys-color-on-primary-fixed-variant: #ffffff;
|
||||
--md-sys-color-inverse-primary: #d4c3fd;
|
||||
--md-sys-color-secondary: #262134;
|
||||
--md-sys-color-secondary-dim: #1c1729;
|
||||
--md-sys-color-on-secondary: #dfd6ef;
|
||||
--md-sys-color-secondary-container: #554f64;
|
||||
--md-sys-color-on-secondary-container: #ffffff;
|
||||
--md-sys-color-secondary-fixed: #554f64;
|
||||
--md-sys-color-secondary-fixed-dim: #494358;
|
||||
--md-sys-color-on-secondary-fixed: #ffffff;
|
||||
--md-sys-color-on-secondary-fixed-variant: #ffffff;
|
||||
--md-sys-color-tertiary: #391733;
|
||||
--md-sys-color-tertiary-dim: #2e0d28;
|
||||
--md-sys-color-on-tertiary: #ffcbee;
|
||||
--md-sys-color-tertiary-container: #6d4563;
|
||||
--md-sys-color-on-tertiary-container: #ffffff;
|
||||
--md-sys-color-tertiary-fixed: #6d4563;
|
||||
--md-sys-color-tertiary-fixed-dim: #603a57;
|
||||
--md-sys-color-on-tertiary-fixed: #ffffff;
|
||||
--md-sys-color-on-tertiary-fixed-variant: #ffffff;
|
||||
--md-sys-color-error: #500016;
|
||||
--md-sys-color-error-dim: #3d000f;
|
||||
--md-sys-color-on-error: #ffced2;
|
||||
--md-sys-color-error-container: #97283e;
|
||||
--md-sys-color-on-error-container: #ffffff;
|
||||
--md-sys-color-success: #002a18;
|
||||
--md-sys-color-on-success: #7df0b4;
|
||||
--md-sys-color-success-container: #005f3c;
|
||||
--md-sys-color-on-success-container: #ffffff;
|
||||
--md-sys-color-warning: #312100;
|
||||
--md-sys-color-on-warning: #ffd486;
|
||||
--md-sys-color-warning-container: #6c4d00;
|
||||
--md-sys-color-on-warning-container: #ffffff;
|
||||
--md-sys-color-info: #002252;
|
||||
--md-sys-color-on-info: #ccdaff;
|
||||
--md-sys-color-info-container: #004eac;
|
||||
--md-sys-color-on-info-container: #ffffff;
|
||||
--md-sys-color-inverse-error: #ffdddf;
|
||||
--md-sys-color-inverse-success: #89fcbf;
|
||||
--md-sys-color-inverse-warning: #ffe2b1;
|
||||
--md-sys-color-inverse-info: #dde5ff;
|
||||
}
|
||||
|
||||
[data-contrast='high'][data-theme='dark'],
|
||||
[data-contrast='high'] [data-theme='dark'] {
|
||||
color-scheme: dark;
|
||||
|
||||
--md-sys-color-background: #0f0d12;
|
||||
--md-sys-color-on-background: #ffffff;
|
||||
--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: #ffffff;
|
||||
--md-sys-color-on-surface-variant: #eae3ef;
|
||||
--md-sys-color-outline: #bcb6c2;
|
||||
--md-sys-color-outline-variant: #a7a1ad;
|
||||
--md-sys-color-inverse-surface: #fdf7fe;
|
||||
--md-sys-color-inverse-on-surface: #000000;
|
||||
--md-sys-color-primary: #ebe1ff;
|
||||
--md-sys-color-primary-dim: #ded0fe;
|
||||
--md-sys-color-on-primary: #302649;
|
||||
--md-sys-color-primary-container: #aa9dc8;
|
||||
--md-sys-color-on-primary-container: #000000;
|
||||
--md-sys-color-primary-fixed: #ded0fe;
|
||||
--md-sys-color-primary-fixed-dim: #d0c3ef;
|
||||
--md-sys-color-on-primary-fixed: #000000;
|
||||
--md-sys-color-on-primary-fixed-variant: #180e30;
|
||||
--md-sys-color-inverse-primary: #3c3256;
|
||||
--md-sys-color-secondary: #ebe1fb;
|
||||
--md-sys-color-secondary-dim: #ddd3ed;
|
||||
--md-sys-color-on-secondary: #2e293c;
|
||||
--md-sys-color-secondary-container: #a8a0b8;
|
||||
--md-sys-color-on-secondary-container: #000000;
|
||||
--md-sys-color-secondary-fixed: #e8def8;
|
||||
--md-sys-color-secondary-fixed-dim: #dad0ea;
|
||||
--md-sys-color-on-secondary-fixed: #000000;
|
||||
--md-sys-color-on-secondary-fixed-variant: #221d2f;
|
||||
--md-sys-color-tertiary: #ffdbf2;
|
||||
--md-sys-color-tertiary-dim: #fac5e9;
|
||||
--md-sys-color-on-tertiary: #43203b;
|
||||
--md-sys-color-tertiary-container: #f4bfe3;
|
||||
--md-sys-color-on-tertiary-container: #2e0e29;
|
||||
--md-sys-color-tertiary-fixed: #f4bfe3;
|
||||
--md-sys-color-tertiary-fixed-dim: #e5b2d5;
|
||||
--md-sys-color-on-tertiary-fixed: #000000;
|
||||
--md-sys-color-on-tertiary-fixed-variant: #180015;
|
||||
--md-sys-color-error: #ffdddf;
|
||||
--md-sys-color-error-dim: #ffc7cb;
|
||||
--md-sys-color-on-error: #5f001c;
|
||||
--md-sys-color-error-container: #ff798c;
|
||||
--md-sys-color-on-error-container: #000000;
|
||||
--md-sys-color-success: #89fcbf;
|
||||
--md-sys-color-on-success: #00341e;
|
||||
--md-sys-color-success-container: #42b880;
|
||||
--md-sys-color-on-success-container: #000000;
|
||||
--md-sys-color-warning: #ffe2b1;
|
||||
--md-sys-color-on-warning: #3c2900;
|
||||
--md-sys-color-warning-container: #e4a604;
|
||||
--md-sys-color-on-warning-container: #000000;
|
||||
--md-sys-color-info: #dde5ff;
|
||||
--md-sys-color-on-info: #002a62;
|
||||
--md-sys-color-info-container: #74a2ff;
|
||||
--md-sys-color-on-info-container: #000000;
|
||||
--md-sys-color-inverse-error: #500016;
|
||||
--md-sys-color-inverse-success: #002a18;
|
||||
--md-sys-color-inverse-warning: #312100;
|
||||
--md-sys-color-inverse-info: #002252;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,282 @@
|
||||
"seed": "#6750a4",
|
||||
"variant": "tonal-spot",
|
||||
"spec": "2025",
|
||||
"contrast": 0,
|
||||
"harmonize": false,
|
||||
"contrast": {
|
||||
"standard": 0,
|
||||
"medium": {
|
||||
"light": {
|
||||
"background": "#fdf7fe",
|
||||
"on-background": "#25232b",
|
||||
"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": "#25232b",
|
||||
"on-surface-variant": "#45414b",
|
||||
"outline": "#615d68",
|
||||
"outline-variant": "#7d7983",
|
||||
"inverse-surface": "#0f0d12",
|
||||
"inverse-on-surface": "#c8c3c9",
|
||||
"primary": "#483b6b",
|
||||
"primary-dim": "#3c305f",
|
||||
"on-primary": "#e1d3ff",
|
||||
"primary-container": "#7b6da0",
|
||||
"on-primary-container": "#ffffff",
|
||||
"primary-fixed": "#7b6da0",
|
||||
"primary-fixed-dim": "#6e6093",
|
||||
"on-primary-fixed": "#ffffff",
|
||||
"on-primary-fixed-variant": "#ffffff",
|
||||
"inverse-primary": "#d4c3fd",
|
||||
"secondary": "#464054",
|
||||
"secondary-dim": "#3a3548",
|
||||
"on-secondary": "#dfd5ef",
|
||||
"secondary-container": "#787187",
|
||||
"on-secondary-container": "#ffffff",
|
||||
"secondary-fixed": "#787187",
|
||||
"secondary-fixed-dim": "#6c657b",
|
||||
"on-secondary-fixed": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#ffffff",
|
||||
"tertiary": "#5c3653",
|
||||
"tertiary-dim": "#502b47",
|
||||
"on-tertiary": "#ffcaee",
|
||||
"tertiary-container": "#936787",
|
||||
"on-tertiary-container": "#ffffff",
|
||||
"tertiary-fixed": "#936787",
|
||||
"tertiary-fixed-dim": "#855b7a",
|
||||
"on-tertiary-fixed": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#ffffff",
|
||||
"error": "#821830",
|
||||
"error-dim": "#6b0221",
|
||||
"on-error": "#ffcdd1",
|
||||
"error-container": "#c44b5f",
|
||||
"on-error-container": "#ffffff",
|
||||
"success": "#004d30",
|
||||
"on-success": "#7df0b3",
|
||||
"success-container": "#008656",
|
||||
"on-success-container": "#ffffff",
|
||||
"warning": "#593e00",
|
||||
"on-warning": "#ffd385",
|
||||
"warning-container": "#986d00",
|
||||
"on-warning-container": "#ffffff",
|
||||
"info": "#003f8e",
|
||||
"on-info": "#cbd9ff",
|
||||
"info-container": "#006fef",
|
||||
"on-info-container": "#ffffff",
|
||||
"inverse-error": "#ff9da8",
|
||||
"inverse-success": "#5ace94",
|
||||
"inverse-warning": "#f3b31e",
|
||||
"inverse-info": "#98b8ff"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0f0d12",
|
||||
"on-background": "#ffffff",
|
||||
"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": "#ffffff",
|
||||
"on-surface-variant": "#bcb6c2",
|
||||
"outline": "#96919c",
|
||||
"outline-variant": "#78737e",
|
||||
"inverse-surface": "#fdf7fe",
|
||||
"inverse-on-surface": "#39373c",
|
||||
"primary": "#cdc0ec",
|
||||
"primary-dim": "#bfb2de",
|
||||
"on-primary": "#3a3054",
|
||||
"primary-container": "#7a6f96",
|
||||
"on-primary-container": "#ffffff",
|
||||
"primary-fixed": "#ded0fe",
|
||||
"primary-fixed-dim": "#d0c3ef",
|
||||
"on-primary-fixed": "#180e30",
|
||||
"on-primary-fixed-variant": "#3c3256",
|
||||
"inverse-primary": "#5a4f75",
|
||||
"secondary": "#cbc2db",
|
||||
"secondary-dim": "#beb5cd",
|
||||
"on-secondary": "#393347",
|
||||
"secondary-container": "#787187",
|
||||
"on-secondary-container": "#ffffff",
|
||||
"secondary-fixed": "#e8def8",
|
||||
"secondary-fixed-dim": "#dad0ea",
|
||||
"on-secondary-fixed": "#221d2f",
|
||||
"on-secondary-fixed-variant": "#423c50",
|
||||
"tertiary": "#ffcfef",
|
||||
"tertiary-dim": "#f4bfe3",
|
||||
"on-tertiary": "#5e3855",
|
||||
"tertiary-container": "#f4bfe3",
|
||||
"on-tertiary-container": "#542f4c",
|
||||
"tertiary-fixed": "#f4bfe3",
|
||||
"tertiary-fixed-dim": "#e5b2d5",
|
||||
"on-tertiary-fixed": "#180015",
|
||||
"on-tertiary-fixed-variant": "#4a2642",
|
||||
"error": "#ff9da8",
|
||||
"error-dim": "#ff8695",
|
||||
"on-error": "#5f001c",
|
||||
"error-container": "#c44b5f",
|
||||
"on-error-container": "#ffffff",
|
||||
"success": "#5ace94",
|
||||
"on-success": "#00341e",
|
||||
"success-container": "#008656",
|
||||
"on-success-container": "#ffffff",
|
||||
"warning": "#f3b31e",
|
||||
"on-warning": "#412d00",
|
||||
"warning-container": "#e4a604",
|
||||
"on-warning-container": "#332200",
|
||||
"info": "#98b8ff",
|
||||
"on-info": "#002a62",
|
||||
"info-container": "#006fef",
|
||||
"on-info-container": "#ffffff",
|
||||
"inverse-error": "#821830",
|
||||
"inverse-success": "#004d30",
|
||||
"inverse-warning": "#593e00",
|
||||
"inverse-info": "#003f8e"
|
||||
}
|
||||
},
|
||||
"high": {
|
||||
"light": {
|
||||
"background": "#fdf7fe",
|
||||
"on-background": "#000000",
|
||||
"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": "#000000",
|
||||
"on-surface-variant": "#25232b",
|
||||
"outline": "#45414b",
|
||||
"outline-variant": "#54505a",
|
||||
"inverse-surface": "#0f0d12",
|
||||
"inverse-on-surface": "#ffffff",
|
||||
"primary": "#281b49",
|
||||
"primary-dim": "#1e103f",
|
||||
"on-primary": "#e1d3ff",
|
||||
"primary-container": "#584a7b",
|
||||
"on-primary-container": "#ffffff",
|
||||
"primary-fixed": "#584a7b",
|
||||
"primary-fixed-dim": "#4c3f6f",
|
||||
"on-primary-fixed": "#ffffff",
|
||||
"on-primary-fixed-variant": "#ffffff",
|
||||
"inverse-primary": "#d4c3fd",
|
||||
"secondary": "#262134",
|
||||
"secondary-dim": "#1c1729",
|
||||
"on-secondary": "#dfd6ef",
|
||||
"secondary-container": "#554f64",
|
||||
"on-secondary-container": "#ffffff",
|
||||
"secondary-fixed": "#554f64",
|
||||
"secondary-fixed-dim": "#494358",
|
||||
"on-secondary-fixed": "#ffffff",
|
||||
"on-secondary-fixed-variant": "#ffffff",
|
||||
"tertiary": "#391733",
|
||||
"tertiary-dim": "#2e0d28",
|
||||
"on-tertiary": "#ffcbee",
|
||||
"tertiary-container": "#6d4563",
|
||||
"on-tertiary-container": "#ffffff",
|
||||
"tertiary-fixed": "#6d4563",
|
||||
"tertiary-fixed-dim": "#603a57",
|
||||
"on-tertiary-fixed": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#ffffff",
|
||||
"error": "#500016",
|
||||
"error-dim": "#3d000f",
|
||||
"on-error": "#ffced2",
|
||||
"error-container": "#97283e",
|
||||
"on-error-container": "#ffffff",
|
||||
"success": "#002a18",
|
||||
"on-success": "#7df0b4",
|
||||
"success-container": "#005f3c",
|
||||
"on-success-container": "#ffffff",
|
||||
"warning": "#312100",
|
||||
"on-warning": "#ffd486",
|
||||
"warning-container": "#6c4d00",
|
||||
"on-warning-container": "#ffffff",
|
||||
"info": "#002252",
|
||||
"on-info": "#ccdaff",
|
||||
"info-container": "#004eac",
|
||||
"on-info-container": "#ffffff",
|
||||
"inverse-error": "#ffdddf",
|
||||
"inverse-success": "#89fcbf",
|
||||
"inverse-warning": "#ffe2b1",
|
||||
"inverse-info": "#dde5ff"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0f0d12",
|
||||
"on-background": "#ffffff",
|
||||
"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": "#ffffff",
|
||||
"on-surface-variant": "#eae3ef",
|
||||
"outline": "#bcb6c2",
|
||||
"outline-variant": "#a7a1ad",
|
||||
"inverse-surface": "#fdf7fe",
|
||||
"inverse-on-surface": "#000000",
|
||||
"primary": "#ebe1ff",
|
||||
"primary-dim": "#ded0fe",
|
||||
"on-primary": "#302649",
|
||||
"primary-container": "#aa9dc8",
|
||||
"on-primary-container": "#000000",
|
||||
"primary-fixed": "#ded0fe",
|
||||
"primary-fixed-dim": "#d0c3ef",
|
||||
"on-primary-fixed": "#000000",
|
||||
"on-primary-fixed-variant": "#180e30",
|
||||
"inverse-primary": "#3c3256",
|
||||
"secondary": "#ebe1fb",
|
||||
"secondary-dim": "#ddd3ed",
|
||||
"on-secondary": "#2e293c",
|
||||
"secondary-container": "#a8a0b8",
|
||||
"on-secondary-container": "#000000",
|
||||
"secondary-fixed": "#e8def8",
|
||||
"secondary-fixed-dim": "#dad0ea",
|
||||
"on-secondary-fixed": "#000000",
|
||||
"on-secondary-fixed-variant": "#221d2f",
|
||||
"tertiary": "#ffdbf2",
|
||||
"tertiary-dim": "#fac5e9",
|
||||
"on-tertiary": "#43203b",
|
||||
"tertiary-container": "#f4bfe3",
|
||||
"on-tertiary-container": "#2e0e29",
|
||||
"tertiary-fixed": "#f4bfe3",
|
||||
"tertiary-fixed-dim": "#e5b2d5",
|
||||
"on-tertiary-fixed": "#000000",
|
||||
"on-tertiary-fixed-variant": "#180015",
|
||||
"error": "#ffdddf",
|
||||
"error-dim": "#ffc7cb",
|
||||
"on-error": "#5f001c",
|
||||
"error-container": "#ff798c",
|
||||
"on-error-container": "#000000",
|
||||
"success": "#89fcbf",
|
||||
"on-success": "#00341e",
|
||||
"success-container": "#42b880",
|
||||
"on-success-container": "#000000",
|
||||
"warning": "#ffe2b1",
|
||||
"on-warning": "#3c2900",
|
||||
"warning-container": "#e4a604",
|
||||
"on-warning-container": "#000000",
|
||||
"info": "#dde5ff",
|
||||
"on-info": "#002a62",
|
||||
"info-container": "#74a2ff",
|
||||
"on-info-container": "#000000",
|
||||
"inverse-error": "#500016",
|
||||
"inverse-success": "#002a18",
|
||||
"inverse-warning": "#312100",
|
||||
"inverse-info": "#002252"
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"background": "#fdf7fe",
|
||||
"on-background": "#34313a",
|
||||
@@ -53,22 +328,22 @@
|
||||
"on-error": "#fff7f7",
|
||||
"error-container": "#f97386",
|
||||
"on-error-container": "#6e0523",
|
||||
"success": "#006c45",
|
||||
"on-success": "#ffffff",
|
||||
"success": "#006d46",
|
||||
"on-success": "#e7ffed",
|
||||
"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",
|
||||
"on-success-container": "#00734a",
|
||||
"warning": "#7c5900",
|
||||
"on-warning": "#fff8f1",
|
||||
"warning-container": "#fab925",
|
||||
"on-warning-container": "#6a4b00",
|
||||
"info": "#005bc5",
|
||||
"on-info": "#f9f8ff",
|
||||
"info-container": "#1c7afc",
|
||||
"on-info-container": "#001435",
|
||||
"inverse-error": "#f97386",
|
||||
"inverse-success": "#69dca1",
|
||||
"inverse-warning": "#fdbb28",
|
||||
"inverse-info": "#aec6ff"
|
||||
"inverse-success": "#3bb27b",
|
||||
"inverse-warning": "#f3b31e",
|
||||
"inverse-info": "#699cff"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#0f0d12",
|
||||
@@ -120,21 +395,21 @@
|
||||
"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",
|
||||
"success": "#3bb27b",
|
||||
"on-success": "#002615",
|
||||
"success-container": "#059460",
|
||||
"on-success-container": "#001e10",
|
||||
"warning": "#f3b31e",
|
||||
"on-warning": "#4e3600",
|
||||
"warning-container": "#e4a604",
|
||||
"on-warning-container": "#593f00",
|
||||
"info": "#699cff",
|
||||
"on-info": "#001e4b",
|
||||
"info-container": "#0c74f6",
|
||||
"on-info-container": "#000a23",
|
||||
"inverse-error": "#a8364b",
|
||||
"inverse-success": "#006c45",
|
||||
"inverse-warning": "#7c5800",
|
||||
"inverse-info": "#005ac4"
|
||||
"inverse-success": "#006d46",
|
||||
"inverse-warning": "#7c5900",
|
||||
"inverse-info": "#005bc5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
* `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.
|
||||
*
|
||||
* M3's contrast level is the same pair: `contrast` is the choice (standard, medium, high or
|
||||
* system) and `resolvedContrast` the level on screen, which is `standard` wherever <html>
|
||||
* carries no data-contrast — the stylesheet's plain blocks are that level. `setContrast()`
|
||||
* writes both, through the same attribute the head script wrote before the first paint.
|
||||
*
|
||||
* `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.
|
||||
@@ -15,13 +20,18 @@
|
||||
document.addEventListener('alpine:init', () => {
|
||||
const root = document.documentElement
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const contrastMedia = window.matchMedia('(prefers-contrast: more)')
|
||||
const choices = ['light', 'dark', 'system']
|
||||
const levels = ['standard', 'medium', 'high', 'system']
|
||||
|
||||
const resolve = (choice) => (choice === 'system' ? (media.matches ? 'dark' : 'light') : choice)
|
||||
const resolveContrast = (choice) => (choice === 'system' ? (contrastMedia.matches ? 'high' : 'standard') : choice)
|
||||
|
||||
window.Alpine.store('theme', {
|
||||
choice: choices.includes(root.dataset.themeChoice) ? root.dataset.themeChoice : 'system',
|
||||
resolved: root.dataset.theme === 'dark' ? 'dark' : 'light',
|
||||
contrast: levels.includes(root.dataset.contrastChoice) ? root.dataset.contrastChoice : 'system',
|
||||
resolvedContrast: root.dataset.contrast === 'medium' || root.dataset.contrast === 'high' ? root.dataset.contrast : 'standard',
|
||||
scheme: root.dataset.scheme || null,
|
||||
|
||||
get value() {
|
||||
@@ -54,6 +64,29 @@ document.addEventListener('alpine:init', () => {
|
||||
this.set(this.resolved === 'dark' ? 'light' : 'dark')
|
||||
},
|
||||
|
||||
setContrast(choice) {
|
||||
if (!levels.includes(choice)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.contrast = choice
|
||||
this.resolvedContrast = resolveContrast(choice)
|
||||
|
||||
root.dataset.contrastChoice = choice
|
||||
|
||||
if (this.resolvedContrast === 'standard') {
|
||||
delete root.dataset.contrast
|
||||
} else {
|
||||
root.dataset.contrast = this.resolvedContrast
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(root.dataset.contrastKey || 'material-contrast', choice)
|
||||
} catch {
|
||||
// Blocked storage: the page still switches, it just will not remember.
|
||||
}
|
||||
},
|
||||
|
||||
previewScheme(name) {
|
||||
if (typeof name !== 'string' || !/^[a-z0-9-]+$/.test(name)) {
|
||||
return
|
||||
@@ -73,4 +106,12 @@ document.addEventListener('alpine:init', () => {
|
||||
theme.resolved = resolve('system')
|
||||
}
|
||||
})
|
||||
|
||||
contrastMedia.addEventListener('change', () => {
|
||||
const theme = window.Alpine.store('theme')
|
||||
|
||||
if (theme.contrast === 'system') {
|
||||
theme.resolvedContrast = resolveContrast('system')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,10 @@
|
||||
(`$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.
|
||||
checked hexes, light or dark with the page. The inline pair is the standard contrast level,
|
||||
which is what the server can know; Alpine swaps in the level on screen
|
||||
(`$store.theme.resolvedContrast`) so a dot never promises a colour the page would not paint.
|
||||
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. --}}
|
||||
@@ -26,10 +29,32 @@
|
||||
$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)) : [];
|
||||
$roles = ['primary', 'secondary', 'tertiary'];
|
||||
|
||||
// profile => level => theme => role => hex, for the dots. Every level is filled — a profile
|
||||
// the caller passed in by hand, or a scheme file without the levels, repeats its standard
|
||||
// colours — so the binding below never asks whether a level is there.
|
||||
$swatches = [];
|
||||
|
||||
foreach ($profiles === [] ? [] : \NoNameWeb\LivewireMaterial\Support\Scheme::LEVELS as $level) {
|
||||
$generated = $level === 'standard' ? [] : \NoNameWeb\LivewireMaterial\Support\Scheme::profiles(contrast: $level);
|
||||
|
||||
foreach ($profiles as $profile => $scheme) {
|
||||
foreach (['light', 'dark'] as $theme) {
|
||||
foreach ($roles as $role) {
|
||||
$swatches[$profile][$level][$theme][$role] = ($generated[$profile] ?? $scheme)[$theme][$role];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
|
||||
@if ($profiles !== [])
|
||||
<fieldset x-data data-scheme-picker {{ $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except('name')->class('min-w-0') }}>
|
||||
<fieldset
|
||||
x-data="{ swatches: @js($swatches) }"
|
||||
data-scheme-picker
|
||||
{{ $attributes->whereDoesntStartWith(['wire:model', 'x-model'])->except('name')->class('min-w-0') }}
|
||||
>
|
||||
@if (filled($label))
|
||||
<legend class="mb-2 type-label-lg text-on-surface-variant">{{ $label }}</legend>
|
||||
@endif
|
||||
@@ -50,9 +75,13 @@
|
||||
/>
|
||||
|
||||
<span class="flex shrink-0 -space-x-1.5" aria-hidden="true">
|
||||
@foreach (['primary', 'secondary', 'tertiary'] as $role)
|
||||
@foreach ($roles as $role)
|
||||
<span
|
||||
style="--swatch-light: {{ $scheme['light'][$role] }}; --swatch-dark: {{ $scheme['dark'][$role] }}"
|
||||
x-bind:style="{
|
||||
'--swatch-light': swatches['{{ $profile }}'][$store.theme.resolvedContrast].light['{{ $role }}'],
|
||||
'--swatch-dark': swatches['{{ $profile }}'][$store.theme.resolvedContrast].dark['{{ $role }}'],
|
||||
}"
|
||||
class="size-5 rounded-corner-full bg-(--swatch-light) ring-2 ring-surface-container-low in-has-checked:ring-secondary-container dark:bg-(--swatch-dark)"
|
||||
></span>
|
||||
@endforeach
|
||||
|
||||
@@ -11,6 +11,17 @@
|
||||
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.
|
||||
|
||||
M3's contrast level rides the same rails (`livewire-material.theme.contrast`):
|
||||
`standard`, `medium`, `high` or `system`, kept under `contrast.storage_key`. `system` asks
|
||||
the operating system, now and whenever it changes, and takes `more` as high. The result is
|
||||
<html data-contrast>, which the generated stylesheet keys its medium and high blocks on —
|
||||
standard writes no attribute, because the plain blocks are already it. The choice is
|
||||
<html data-contrast-choice>, where `$store.theme` picks it up.
|
||||
|
||||
`livewire-material.motion.scheme` is M3's motion scheme: `standard` is written to
|
||||
<html data-motion> (and swaps the spatial springs for the restrained set), `expressive`,
|
||||
the default, writes nothing.
|
||||
|
||||
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.
|
||||
@@ -27,9 +38,10 @@
|
||||
With `theme.meta` on, the browser's own chrome follows too: the `content` of every
|
||||
<meta name="theme-color"> without a `media` attribute — one is added to <head> when there is
|
||||
none — is the resolved theme's `surface`, from the scheme file (`Scheme`), for the profile in
|
||||
<html data-scheme> (else the active one). A MutationObserver on <html> keeps it in step with
|
||||
whatever changes `data-theme` or `data-scheme` afterwards: `$store.theme.set()` and `toggle()`,
|
||||
an OS change while `system`, a profile preview, the application's own script. A layout's own
|
||||
<html data-scheme> (else the active one) and at the level in <html data-contrast>. A
|
||||
MutationObserver on <html> keeps it in step with whatever changes `data-theme`,
|
||||
`data-scheme` or `data-contrast` afterwards: `$store.theme.set()` and `toggle()`,
|
||||
`setContrast()`, an OS change while `system`, a profile preview, the application's own script. A layout's own
|
||||
theme-color meta belongs before this script: one written after it is only painted on
|
||||
DOMContentLoaded, beside the one added here. Off by default, and then none of it is emitted.
|
||||
|
||||
@@ -43,27 +55,44 @@
|
||||
@php
|
||||
$theme = config('livewire-material.theme');
|
||||
$rail = config('livewire-material.rail');
|
||||
$contrast = $theme['contrast'] ?? [];
|
||||
$levels = \NoNameWeb\LivewireMaterial\Support\Scheme::LEVELS;
|
||||
|
||||
$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'] ?? []),
|
||||
'contrast' => [
|
||||
'default' => in_array($contrast['default'] ?? null, [...$levels, 'system'], true) ? $contrast['default'] : 'system',
|
||||
'key' => $contrast['storage_key'] ?? 'material-contrast',
|
||||
],
|
||||
'motion' => (config('livewire-material.motion.scheme') === 'standard') ? 'standard' : null,
|
||||
'rail' => [
|
||||
'default' => ($rail['default'] ?? null) === 'collapsed' ? 'collapsed' : 'expanded',
|
||||
'key' => $rail['storage_key'] ?? 'material-rail',
|
||||
],
|
||||
];
|
||||
|
||||
// Only the surfaces the meta can show: the active scheme's, and every profile's for a preview.
|
||||
// Only the surfaces the meta can show: the active scheme's, every profile's for a preview,
|
||||
// and each of those at M3's other two contrast levels. `Scheme::load()` resolves the active
|
||||
// profile itself, so it answers for a single scheme and for profiles alike.
|
||||
if ((bool) ($theme['meta'] ?? false)) {
|
||||
$surfaces = fn (array $scheme): array => ['light' => $scheme['light']['surface'], 'dark' => $scheme['dark']['surface']];
|
||||
$schemeProfiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles();
|
||||
|
||||
// One read of the scheme file per level rather than one per profile and level.
|
||||
$profilesAt = collect($levels)->mapWithKeys(fn (string $level): array => [$level => \NoNameWeb\LivewireMaterial\Support\Scheme::profiles(contrast: $level)]);
|
||||
$otherLevels = fn (callable $of): object => (object) collect($levels)->skip(1)->mapWithKeys(fn (string $level): array => [$level => $of($level)])->all();
|
||||
|
||||
$settings['meta'] = [
|
||||
// PHP 8.5 deprecates a null array offset: without profiles the scheme is null.
|
||||
...$surfaces(($settings['scheme'] !== null ? ($schemeProfiles[$settings['scheme']] ?? null) : null) ?? \NoNameWeb\LivewireMaterial\Support\Scheme::load()),
|
||||
'profiles' => (object) collect($schemeProfiles)->map($surfaces)->all(),
|
||||
...$surfaces(\NoNameWeb\LivewireMaterial\Support\Scheme::load()),
|
||||
'contrast' => $otherLevels(fn (string $level): array => $surfaces(\NoNameWeb\LivewireMaterial\Support\Scheme::load(contrast: $level))),
|
||||
'profiles' => (object) collect($profilesAt['standard'])
|
||||
->map(fn (array $scheme, string $name): array => [
|
||||
...$surfaces($scheme),
|
||||
'contrast' => $otherLevels(fn (string $level): array => $surfaces($profilesAt[$level][$name])),
|
||||
])
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
@@ -72,8 +101,11 @@
|
||||
(function (settings) {
|
||||
var root = document.documentElement;
|
||||
var media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
var contrastMedia = window.matchMedia('(prefers-contrast: more)');
|
||||
var valid = function (value) { return value === 'light' || value === 'dark' || value === 'system'; };
|
||||
var validContrast = function (value) { return value === 'standard' || value === 'medium' || value === 'high' || value === 'system'; };
|
||||
var choice = settings.default;
|
||||
var contrast = settings.contrast.default;
|
||||
var rail = settings.rail.default;
|
||||
var railChosen = false;
|
||||
|
||||
@@ -85,6 +117,12 @@
|
||||
railChosen = true;
|
||||
}
|
||||
|
||||
var storedContrast = localStorage.getItem(settings.contrast.key);
|
||||
|
||||
if (validContrast(storedContrast)) {
|
||||
contrast = storedContrast;
|
||||
}
|
||||
|
||||
var stored = localStorage.getItem(settings.key);
|
||||
|
||||
if (valid(stored)) {
|
||||
@@ -111,12 +149,30 @@
|
||||
root.setAttribute('data-theme', current === 'system' ? (media.matches ? 'dark' : 'light') : current);
|
||||
};
|
||||
|
||||
// Standard is the stylesheet's plain blocks, so it is the absence of the attribute.
|
||||
var applyContrast = function () {
|
||||
var current = root.getAttribute('data-contrast-choice');
|
||||
var level = current === 'system' ? (contrastMedia.matches ? 'high' : 'standard') : current;
|
||||
|
||||
if (level === 'medium' || level === 'high') {
|
||||
root.setAttribute('data-contrast', level);
|
||||
} else {
|
||||
root.removeAttribute('data-contrast');
|
||||
}
|
||||
};
|
||||
|
||||
if (settings.scheme) {
|
||||
root.setAttribute('data-scheme', settings.scheme);
|
||||
}
|
||||
|
||||
if (settings.motion) {
|
||||
root.setAttribute('data-motion', settings.motion);
|
||||
}
|
||||
|
||||
root.setAttribute('data-theme-key', settings.key);
|
||||
root.setAttribute('data-theme-choice', choice);
|
||||
root.setAttribute('data-contrast-key', settings.contrast.key);
|
||||
root.setAttribute('data-contrast-choice', contrast);
|
||||
root.setAttribute('data-rail-key', settings.rail.key);
|
||||
root.setAttribute('data-rail', rail);
|
||||
|
||||
@@ -126,19 +182,23 @@
|
||||
root.setAttribute('data-rail-auto', '');
|
||||
}
|
||||
apply();
|
||||
applyContrast();
|
||||
|
||||
media.addEventListener('change', apply);
|
||||
contrastMedia.addEventListener('change', applyContrast);
|
||||
@if (isset($settings['meta']))
|
||||
|
||||
var paintThemeColor = function () {
|
||||
var theme = root.getAttribute('data-theme');
|
||||
var scheme = root.getAttribute('data-scheme');
|
||||
var level = root.getAttribute('data-contrast');
|
||||
|
||||
if ((theme !== 'light' && theme !== 'dark') || !document.head) {
|
||||
return;
|
||||
}
|
||||
|
||||
var colour = (Object.prototype.hasOwnProperty.call(settings.meta.profiles, scheme) ? settings.meta.profiles[scheme] : settings.meta)[theme];
|
||||
var surfaces = Object.prototype.hasOwnProperty.call(settings.meta.profiles, scheme) ? settings.meta.profiles[scheme] : settings.meta;
|
||||
var colour = (Object.prototype.hasOwnProperty.call(surfaces.contrast, level) ? surfaces.contrast[level] : surfaces)[theme];
|
||||
var metas = document.head.querySelectorAll('meta[name="theme-color"]:not([media])');
|
||||
|
||||
if (metas.length === 0) {
|
||||
@@ -157,13 +217,13 @@
|
||||
};
|
||||
|
||||
paintThemeColor();
|
||||
new MutationObserver(paintThemeColor).observe(root, { attributes: true, attributeFilter: ['data-theme', 'data-scheme'] });
|
||||
new MutationObserver(paintThemeColor).observe(root, { attributes: true, attributeFilter: ['data-theme', 'data-scheme', 'data-contrast'] });
|
||||
document.addEventListener('DOMContentLoaded', paintThemeColor);
|
||||
document.addEventListener('livewire:navigated', paintThemeColor);
|
||||
@endif
|
||||
|
||||
document.addEventListener('livewire:navigating', function (event) {
|
||||
var kept = ['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-rail', 'data-rail-auto', 'data-rail-key'].map(function (name) {
|
||||
var kept = ['data-scheme', 'data-theme', 'data-theme-choice', 'data-theme-key', 'data-contrast', 'data-contrast-choice', 'data-contrast-key', 'data-motion', 'data-rail', 'data-rail-auto', 'data-rail-key'].map(function (name) {
|
||||
return [name, root.getAttribute(name)];
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
"Dark theme", pressed while dark.
|
||||
- `cycle`: an icon button that steps light → dark → system, showing the choice it is on.
|
||||
- `picker`: M3's segmented buttons for the three choices, for a settings page.
|
||||
- `contrast`: the same row for M3's three contrast levels — standard, medium (3:1) and high
|
||||
(7:1), the three the scheme is generated in. The row marks the level in force
|
||||
(`resolvedContrast`), so the operating system's setting shows while the choice is
|
||||
`system`, and choosing one is an explicit choice from then on.
|
||||
|
||||
The theme is the visitor's, known only in the browser, so the parts that depend on it wait for
|
||||
Alpine (`x-cloak`) rather than render a guess. `class` lands on the button or the group. --}}
|
||||
@@ -16,10 +20,37 @@
|
||||
])
|
||||
|
||||
@php
|
||||
$mode = in_array($mode, ['toggle', 'cycle', 'picker'], true) ? $mode : 'toggle';
|
||||
$mode = in_array($mode, ['toggle', 'cycle', 'picker', 'contrast'], true) ? $mode : 'toggle';
|
||||
@endphp
|
||||
|
||||
@if ($mode === 'picker')
|
||||
@if ($mode === 'contrast')
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="{{ __('Contrast') }}"
|
||||
x-data
|
||||
data-theme-toggle="contrast"
|
||||
{{ $attributes->class(['inline-flex h-10 rounded-corner-full border border-outline']) }}
|
||||
>
|
||||
@foreach (['standard' => ['Standard', 'contrast'], 'medium' => ['Medium', 'contrast_circle'], 'high' => ['High', 'contrast_square']] as $level => [$text, $icon])
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked="false"
|
||||
x-bind:aria-checked="($store.theme.resolvedContrast === '{{ $level }}').toString()"
|
||||
x-bind:tabindex="$store.theme.resolvedContrast === '{{ $level }}' ? 0 : -1"
|
||||
x-on:click="$store.theme.setContrast('{{ $level }}')"
|
||||
x-on:keydown.arrow-right.prevent="$el.nextElementSibling?.click(); $el.nextElementSibling?.focus();"
|
||||
x-on:keydown.arrow-left.prevent="$el.previousElementSibling?.click(); $el.previousElementSibling?.focus();"
|
||||
data-contrast-option="{{ $level }}"
|
||||
class="state-layer focus-ring inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 border-outline px-3 type-label-lg text-on-surface not-first:border-s first:rounded-s-corner-full last:rounded-e-corner-full aria-checked:bg-secondary-container aria-checked:text-on-secondary-container"
|
||||
>
|
||||
<x-livewire-material::icon name="check" class="hidden size-4.5 in-aria-checked:block" />
|
||||
<x-livewire-material::icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
|
||||
{{ __($text) }}
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
@elseif ($mode === 'picker')
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="{{ __('Theme') }}"
|
||||
|
||||
@@ -11,12 +11,21 @@
|
||||
'Ink and lines' => ['bg-body', 'bg-meta', 'bg-quiet', 'bg-structure', 'bg-chrome', 'bg-divider'],
|
||||
];
|
||||
|
||||
// The roles a contrast level moves most: the page keeps its surface, the ink and the
|
||||
// outlines carry the level.
|
||||
$levels = ['standard' => 'Standard', 'medium' => 'Medium · 3:1', 'high' => 'High · 7:1'];
|
||||
$levelRoles = ['bg-primary', 'bg-on-primary', 'bg-primary-container', 'bg-on-primary-container', 'bg-on-surface-variant', 'bg-outline', 'bg-error', 'bg-success', 'bg-warning', 'bg-info'];
|
||||
$profile = \NoNameWeb\LivewireMaterial\Support\Scheme::profile();
|
||||
|
||||
$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,
|
||||
'Contrast level' => <<<'BLADE'
|
||||
<x-theme-toggle mode="contrast" />
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
|
||||
@@ -34,10 +43,43 @@
|
||||
profiles the picker draws nothing.
|
||||
</p>
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
Every scheme is generated at M3's three contrast levels — standard, medium (3:1) and high (7:1) — and the
|
||||
head script writes the visitor's to <code><html data-contrast></code>, following the operating system
|
||||
until they choose. The page keeps its surface; the ink, the outlines and the containers carry the level.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
<x-showcase::example :$title :$code />
|
||||
@endforeach
|
||||
|
||||
<div class="grid gap-4 large:grid-cols-3">
|
||||
@foreach ($levels as $level => $title)
|
||||
{{-- data-scheme beside data-contrast: a profile's level blocks key on both attributes
|
||||
together, as they do on <html>. --}}
|
||||
<div
|
||||
@if ($profile !== null) data-scheme="{{ $profile }}" @endif
|
||||
@if ($level !== 'standard') data-contrast="{{ $level }}" @endif
|
||||
class="space-y-3"
|
||||
>
|
||||
<h3 class="type-title-sm text-on-surface-variant">{{ $title }}</h3>
|
||||
|
||||
@foreach (['light', 'dark'] as $theme)
|
||||
<div data-theme="{{ $theme }}" class="space-y-2 rounded-corner-lg bg-surface p-3 text-on-surface">
|
||||
<h4 class="type-label-md capitalize text-on-surface-variant">{{ $theme }}</h4>
|
||||
|
||||
@foreach ($levelRoles as $role)
|
||||
<div class="flex items-center gap-2">
|
||||
<span @class(['size-6 shrink-0 rounded-corner-xs border border-outline-variant', $role])></span>
|
||||
<span class="min-w-0 truncate type-body-sm">{{ \Illuminate\Support\Str::after($role, 'bg-') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 expanded: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">
|
||||
|
||||
Reference in New Issue
Block a user