Plan step 36. <x-button> renders data-md-button with its variant, colour, size, shape, icon-button width, selected state and compact FAB as data-md-* attributes, and passes the glyph's size to <x-icon>. button.css draws Button*Tokens' geometry per size, the colour roles through custom properties that the disabled treatment replaces, the state layer, focus ring and 48px target, and the compact FAB below 600px. Its corner rules carry no specificity (:where), so a group's stylesheet reshapes the buttons inside it with any selector; the split button no longer passes an empty corners prop, which is gone. data-icon-button becomes data-md-icon-button, also in groups.css, toolbar.css and theme-toggle's own button (navigation group), and the AppBarTest assertions follow the new hooks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
111 lines
5.1 KiB
PHP
111 lines
5.1 KiB
PHP
{{-- Switches the colour theme through `$store.theme` (resources/js/theme.js), so every toggle on a
|
|
page reads and writes the one choice and none needs an id.
|
|
|
|
`mode`:
|
|
- `toggle` (the default): an icon button between light and dark. The icon is where it takes
|
|
you — a sun while the page is dark, a moon while it is light — and it is a toggle button,
|
|
"Dark theme", pressed while dark.
|
|
- `cycle`: an icon button that steps light → dark → system, showing the choice it is on.
|
|
- `picker`: the three choices as a connected button group, 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 two rows are `<x-group>` — M3 Expressive's connected button group, the successor of the
|
|
segmented button, which the expressive update deprecates. Its segments are native radios, so
|
|
the browser supplies the radiogroup semantics, both arrow axes, the wrap and the roving tab
|
|
stop; the store is bound through an `x-model` accessor on the wrapper, since the level a
|
|
contrast row marks is the resolved one but the level it writes goes through `setContrast()`.
|
|
`label` adds a visible legend (a settings page); without one the row is still named for a
|
|
screen reader. `class` lands on the button or on the group.
|
|
|
|
The theme is the visitor's, known only in the browser, so the parts that depend on it wait for
|
|
Alpine (`x-cloak`, an unchecked radio) rather than render a guess. --}}
|
|
|
|
@props([
|
|
'mode' => 'toggle',
|
|
'label' => null,
|
|
])
|
|
|
|
@php
|
|
$mode = in_array($mode, ['toggle', 'cycle', 'picker', 'contrast'], true) ? $mode : 'toggle';
|
|
|
|
$row = [
|
|
'picker' => [
|
|
'name' => __('Theme'),
|
|
'field' => 'material-theme',
|
|
'read' => '$store.theme.choice',
|
|
'write' => '$store.theme.set(value)',
|
|
'options' => [
|
|
['id' => 'light', 'name' => __('Light'), 'icon' => 'light_mode'],
|
|
['id' => 'dark', 'name' => __('Dark'), 'icon' => 'dark_mode'],
|
|
['id' => 'system', 'name' => __('System'), 'icon' => 'brightness_auto'],
|
|
],
|
|
],
|
|
'contrast' => [
|
|
'name' => __('Contrast'),
|
|
'field' => 'material-contrast',
|
|
'read' => '$store.theme.resolvedContrast',
|
|
'write' => '$store.theme.setContrast(value)',
|
|
'options' => [
|
|
['id' => 'standard', 'name' => __('Standard'), 'icon' => 'contrast'],
|
|
['id' => 'medium', 'name' => __('Medium'), 'icon' => 'contrast_circle'],
|
|
['id' => 'high', 'name' => __('High'), 'icon' => 'contrast_square'],
|
|
],
|
|
],
|
|
][$mode] ?? null;
|
|
@endphp
|
|
|
|
@if ($row !== null)
|
|
<div
|
|
role="group"
|
|
aria-label="{{ $label ?? $row['name'] }}"
|
|
data-theme-toggle="{{ $mode }}"
|
|
x-data="{
|
|
get chosen() {
|
|
return this.{{ $row['read'] }}
|
|
},
|
|
set chosen(value) {
|
|
this.{{ $row['write'] }}
|
|
},
|
|
}"
|
|
>
|
|
<x-livewire-material::group
|
|
inline
|
|
x-model="chosen"
|
|
:label="$label"
|
|
:name="$row['field']"
|
|
:options="$row['options']"
|
|
{{ $attributes }}
|
|
/>
|
|
</div>
|
|
@else
|
|
<button
|
|
type="button"
|
|
x-data
|
|
data-md-icon-button
|
|
data-theme-toggle="{{ $mode }}"
|
|
@if ($mode === 'cycle')
|
|
aria-label="{{ __('Theme') }}"
|
|
x-bind:aria-label="{ light: @js(__('Theme: light')), dark: @js(__('Theme: dark')), system: @js(__('Theme: system')) }[$store.theme.choice]"
|
|
x-on:click="$store.theme.set({ light: 'dark', dark: 'system', system: 'light' }[$store.theme.choice])"
|
|
@else
|
|
aria-label="{{ __('Dark theme') }}"
|
|
aria-pressed="false"
|
|
x-bind:aria-pressed="($store.theme.resolved === 'dark').toString()"
|
|
x-on:click="$store.theme.toggle()"
|
|
@endif
|
|
{{ $attributes->class(['state-layer touch-target focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center rounded-corner-full text-on-surface-variant transition-[border-radius] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast active:rounded-corner-sm']) }}
|
|
>
|
|
@if ($mode === 'cycle')
|
|
<x-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
|
|
<x-livewire-material::icon name="dark_mode" x-cloak x-show="$store.theme.choice === 'dark'" />
|
|
<x-livewire-material::icon name="brightness_auto" x-cloak x-show="$store.theme.choice === 'system'" />
|
|
@else
|
|
<x-livewire-material::icon name="light_mode" x-cloak x-show="$store.theme.resolved === 'dark'" />
|
|
<x-livewire-material::icon name="dark_mode" x-cloak x-show="$store.theme.resolved !== 'dark'" />
|
|
@endif
|
|
</button>
|
|
@endif
|