From bcf0d4f4e3728779abc11c9bc7f4b98995bb4eca Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Sun, 13 Sep 2026 17:59:34 +0200 Subject: [PATCH] Keep the theme-color meta in step with the theme An app installed as a PWA, and a mobile browser, colour their bar from , which the package left alone: the bar kept the server's one colour whatever theme the visitor chose. With the new `theme.meta` option (off by default), sets the content of every theme-color meta without a `media` attribute to the resolved theme's surface, from the scheme data and for the profile in , adding one to when the page has none. A MutationObserver on follows every later change of data-theme or data-scheme: $store.theme's set() and toggle(), an OS change while `system`, a profile preview, an application's own script. wire:navigate's head merge puts the next page's server-rendered meta in place, so it is painted again as the page is swapped in and on livewire:navigated. Turned off, the script is byte-for-byte what it was. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RHoXZSHc8gGpZjFmA5fPc2 --- README.md | 1 + config/livewire-material.php | 8 ++ .../livewire-material-development/SKILL.md | 6 +- .../views/components/theme-script.blade.php | 61 +++++++++++- tests/Browser/ThemeTest.php | 97 +++++++++++++++++++ tests/Feature/Components/ThemeScriptTest.php | 40 ++++++++ 6 files changed, 210 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 969555a3..3fd32d86 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ php artisan vendor:publish --tag=livewire-material-config - `prefix` — components are ``, ``… Set `'m'` when a name clashes with the application's own components, and they become ``. `` always works. - `theme.default` (`light`, `dark` or `system`), `theme.storage_key`, `theme.legacy_keys` (an earlier toggle's localStorage keys, adopted once). +- `theme.meta` — keep `` (an installed web app's or a mobile browser's bar) on the resolved theme's `surface` and the active colour profile, before the first paint and after every change, `wire:navigate` included; one is added when the page has none (default `false`). - `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`). diff --git a/config/livewire-material.php b/config/livewire-material.php index ea3b1b32..3f88ca67 100644 --- a/config/livewire-material.php +++ b/config/livewire-material.php @@ -28,12 +28,20 @@ return [ | localStorage under 'storage_key'; values found under 'legacy_keys' (an | earlier theme toggle's key) are adopted once and then removed. | + | 'meta' keeps (the colour an installed web app + | or a mobile browser gives its bar) on the resolved theme's surface, and + | the active colour profile's: the head script sets it before the first + | paint, adds one when the page has none, and follows every later change, + | wire:navigate included. A theme-color meta with a `media` attribute is + | left alone. + | */ 'theme' => [ 'default' => 'system', 'storage_key' => 'material-theme', 'legacy_keys' => [], + 'meta' => false, ], /* diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md index 1c918e25..58445bb7 100644 --- a/resources/boost/skills/livewire-material-development/SKILL.md +++ b/resources/boost/skills/livewire-material-development/SKILL.md @@ -91,7 +91,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`. 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`. 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. + +`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 `` without a `media` attribute to the resolved theme's `surface` — of the profile in `` — before the first paint, adding one to `` 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 `` (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. ## Toasts @@ -187,7 +189,7 @@ One of M3 Expressive's 35 shapes, filled in the text colour, `aria-hidden`, size ### `` -The theme decided before the first paint. Exactly once per layout, in ``, before `@vite`. No props; configured in `config/livewire-material.php`. +The theme decided before the first paint. Exactly once per layout, in ``, before `@vite`. No props; configured in `config/livewire-material.php`. With `theme.meta` on it also paints `` (see Theme); a layout's own theme-color meta goes before it. ### `` diff --git a/resources/views/components/theme-script.blade.php b/resources/views/components/theme-script.blade.php index f8d73a84..4a36efc6 100644 --- a/resources/views/components/theme-script.blade.php +++ b/resources/views/components/theme-script.blade.php @@ -21,10 +21,21 @@ rail would paint wide and snap shut on every load. `$store.rail` (resources/js/navigation.js) changes it. + With `theme.meta` on, the browser's own chrome follows too: the `content` of every + without a `media` attribute — one is added to when there is + none — is the resolved theme's `surface`, from the scheme file (`Scheme`), for the profile in + (else the active one). A MutationObserver on 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 + 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. + wire:navigate swaps the body, merges the head without running this again, and gives the next page's attributes — which the server rendered without any of these, so Livewire removes them. They are put back as the new page is swapped in (`onSwap`, in the same task, - before anything paints), so this only has to run on a full load. --}} + before anything paints), so this only has to run on a full load. The head merge also puts the + next page's server-rendered theme-color meta in place of the painted one, so it is painted + again there, and once more on `livewire:navigated`. --}} @php $theme = config('livewire-material.theme'); @@ -40,6 +51,17 @@ 'key' => $rail['storage_key'] ?? 'material-rail', ], ]; + + // Only the surfaces the meta can show: the active scheme's, and every profile's for a preview. + if ((bool) ($theme['meta'] ?? false)) { + $surfaces = fn (array $scheme): array => ['light' => $scheme['light']['surface'], 'dark' => $scheme['dark']['surface']]; + $schemeProfiles = \NoNameWeb\LivewireMaterial\Support\Scheme::profiles(); + + $settings['meta'] = [ + ...$surfaces($schemeProfiles[$settings['scheme']] ?? \NoNameWeb\LivewireMaterial\Support\Scheme::load()), + 'profiles' => (object) collect($schemeProfiles)->map($surfaces)->all(), + ]; + } @endphp