`mode="picker"` hand-rolled an outlined segmented button, point for point the design M3 Expressive deprecates, and its own arrow keys neither wrapped nor handled Up/Down. Both rows are now <x-group>: native radios in a fieldset, so the browser supplies the radiogroup, both axes, the wrap and the roving tab stop, and the store is bound through an x-model accessor — a contrast row still marks the resolved level but writes through setContrast(). A new `label` prop legends the row. Plan: docs/plans/material-3-alignment.md, step 14 (navigation N-04, N-17). 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-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
|