Plan step 17, on N-06, N-07 and C-07. The shell now changes at 600, 840 and 1200 and nowhere else: a compact window keeps the navigation bar and the modal rail; `medium` (600-839) gets the collapsed rail in the layout and no bar; `expanded` (840-1199) gets a standard rail, collapsed, whose menu button expands it in place rather than over a scrim; `large` and above start it expanded, which is what M3 prefers once there is room. `data-rail` alone could not say "collapsed at expanded, expanded at large", since it carries `rail.default` for a visitor who never chose. <x-theme-script> now also writes `data-rail-auto` while nothing is stored, the `rail-collapsed:` variant reads it in the 840-1199 band, and `$store.rail.auto` mirrors it for Alpine; the first press of the menu button drops it, so a remembered choice still wins in both bands. `rail.default` and the rest of `$store.rail` are unchanged, and the attribute rides through `wire:navigate` with the others. `--material-margin` carries M3's window margin on the shell -- 16px compact, 24px from `medium` -- and the content region is padded with it, so the showcase pages drop their own gutters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
184 lines
8.5 KiB
PHP
184 lines
8.5 KiB
PHP
{{-- The theme and the navigation rail's width, decided before the first paint. Include it in
|
||
<head>, ahead of @vite.
|
||
|
||
It reads the visitor's choice from localStorage (`livewire-material.theme.storage_key`):
|
||
`light`, `dark` or `system`, falling back to `theme.default`. `system` follows the
|
||
operating system, now and whenever it changes. The result is written to
|
||
<html data-theme>, which is the only thing the stylesheet keys on; the choice itself is
|
||
<html data-theme-choice>, where `$store.theme` picks it up.
|
||
|
||
A value under one of `theme.legacy_keys` — an earlier toggle's key, which may be
|
||
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
|
||
rail would paint wide and snap shut on every load. `$store.rail` (resources/js/navigation.js)
|
||
changes it. <html data-rail-auto> rides with it and says nothing was stored — the value is only
|
||
`rail.default`, not a choice — so `<x-app-shell>`'s adaptive rail can start collapsed in the
|
||
expanded class (840–1199) and expanded from large, as M3 asks, while still obeying a visitor
|
||
who has chosen. `$store.rail` drops it the first time they do.
|
||
|
||
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
|
||
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 <html>
|
||
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. 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');
|
||
$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'] ?? []),
|
||
'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.
|
||
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'] = [
|
||
// 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(),
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
<script>
|
||
(function (settings) {
|
||
var root = document.documentElement;
|
||
var media = window.matchMedia('(prefers-color-scheme: dark)');
|
||
var valid = function (value) { return value === 'light' || value === 'dark' || value === 'system'; };
|
||
var choice = settings.default;
|
||
var rail = settings.rail.default;
|
||
var railChosen = false;
|
||
|
||
try {
|
||
var storedRail = localStorage.getItem(settings.rail.key);
|
||
|
||
if (storedRail === 'collapsed' || storedRail === 'expanded') {
|
||
rail = storedRail;
|
||
railChosen = true;
|
||
}
|
||
|
||
var stored = localStorage.getItem(settings.key);
|
||
|
||
if (valid(stored)) {
|
||
choice = stored;
|
||
} else {
|
||
settings.legacy.forEach(function (key) {
|
||
var legacy = (localStorage.getItem(key) || '').replace(/"/g, '');
|
||
|
||
if (valid(legacy)) {
|
||
choice = legacy;
|
||
localStorage.setItem(settings.key, legacy);
|
||
}
|
||
|
||
localStorage.removeItem(key);
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// Private windows and blocked site data throw on access; the default stands.
|
||
}
|
||
|
||
var apply = function () {
|
||
var current = root.getAttribute('data-theme-choice');
|
||
|
||
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);
|
||
root.setAttribute('data-rail', rail);
|
||
|
||
if (railChosen) {
|
||
root.removeAttribute('data-rail-auto');
|
||
} else {
|
||
root.setAttribute('data-rail-auto', '');
|
||
}
|
||
apply();
|
||
|
||
media.addEventListener('change', apply);
|
||
@if (isset($settings['meta']))
|
||
|
||
var paintThemeColor = function () {
|
||
var theme = root.getAttribute('data-theme');
|
||
var scheme = root.getAttribute('data-scheme');
|
||
|
||
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 metas = document.head.querySelectorAll('meta[name="theme-color"]:not([media])');
|
||
|
||
if (metas.length === 0) {
|
||
var meta = document.createElement('meta');
|
||
|
||
meta.setAttribute('name', 'theme-color');
|
||
document.head.appendChild(meta);
|
||
metas = [meta];
|
||
}
|
||
|
||
Array.prototype.forEach.call(metas, function (meta) {
|
||
if (meta.getAttribute('content') !== colour) {
|
||
meta.setAttribute('content', colour);
|
||
}
|
||
});
|
||
};
|
||
|
||
paintThemeColor();
|
||
new MutationObserver(paintThemeColor).observe(root, { attributes: true, attributeFilter: ['data-theme', 'data-scheme'] });
|
||
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) {
|
||
return [name, root.getAttribute(name)];
|
||
});
|
||
|
||
event.detail.onSwap(function () {
|
||
kept.forEach(function (attribute) {
|
||
if (attribute[1] !== null) {
|
||
root.setAttribute(attribute[0], attribute[1]);
|
||
}
|
||
});
|
||
@if (isset($settings['meta']))
|
||
|
||
paintThemeColor();
|
||
@endif
|
||
});
|
||
});
|
||
})(@json($settings));
|
||
</script>
|