Plan step 36 (navigation group, third batch): <x-theme-toggle>'s icon button (mode="toggle"|"cycle") moves into resources/css/components/ theme-toggle.css, keyed on data-md-theme-toggle="toggle"|"cycle" and still carrying data-md-icon-button, the hook toolbar.css matches generically so the toggle takes a toolbar's own icon colour when placed in one (N-13). The button renders the shared md-state-layer/ md-touch-target/md-focus-ring classes (N-01's 48px target); its corner morphs from full to sm while pressed on the fast spatial spring, the same motion <x-button>'s own icon buttons take. mode="picker"|"contrast" already drew a connected group over native radios (N-04, N-17's fix), so only its wrapper's hook needed renaming. Hook renamed: data-theme-toggle to data-md-theme-toggle, updated in AppBarTest.php, which carries theme-toggle's render tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
111 lines
4.9 KiB
PHP
111 lines
4.9 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-md-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-md-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(['md-state-layer', 'md-touch-target', 'md-focus-ring']) }}
|
|
>
|
|
@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
|