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
113 lines
4.8 KiB
PHP
113 lines
4.8 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.
|
|
|
|
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. --}}
|
|
|
|
@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',
|
|
],
|
|
];
|
|
@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;
|
|
|
|
try {
|
|
var storedRail = localStorage.getItem(settings.rail.key);
|
|
|
|
if (storedRail === 'collapsed' || storedRail === 'expanded') {
|
|
rail = storedRail;
|
|
}
|
|
|
|
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);
|
|
apply();
|
|
|
|
media.addEventListener('change', apply);
|
|
|
|
document.addEventListener('livewire:navigating', function (event) {
|
|
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)];
|
|
});
|
|
|
|
event.detail.onSwap(function () {
|
|
kept.forEach(function (attribute) {
|
|
if (attribute[1] !== null) {
|
|
root.setAttribute(attribute[0], attribute[1]);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
})(@json($settings));
|
|
</script>
|