Files
livewire-material/resources/views/components/theme-toggle.blade.php
T
Andreas Reinhold / reiniandClaude Sonnet 5 74cbcd2f44 Draw the theme toggle without Tailwind
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
2026-09-15 00:45:09 +02:00

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