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:
co-authored by
Claude Opus 5
parent
7798352cfc
commit
fb42f004b1
@@ -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)];
|
||||
});
|
||||
|
||||
|
||||
@@ -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><html data-scheme></code>, and <code><x-scheme-picker></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">
|
||||
|
||||
Reference in New Issue
Block a user