Add app bars, toolbars, tabs and the account and theme controls
tests / browser (chrome, chromium) (push) Successful in 3m34s
tests / browser (safari, webkit) (push) Successful in 6m2s
tests / lint (push) Successful in 59s
tests / feature (8.4) (push) Successful in 1m6s
tests / feature (8.5) (push) Successful in 1m9s
tests / browser (firefox, firefox) (push) Successful in 4m20s

M3 Expressive top app bars (small, centered, medium and large flexible,
search) that collapse with CSS sticky, docked and floating toolbars,
primary and secondary tabs with a view-transition indicator, section
navigation, the account menu and theme toggles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
Andreas Reinhold / reini
2026-09-13 08:26:54 +02:00
co-authored by Claude Opus 5
parent bf00e4c40a
commit f90695cedd
22 changed files with 1603 additions and 1 deletions
@@ -0,0 +1,76 @@
{{-- 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`: M3's segmented buttons for the three choices, for a settings page.
The theme is the visitor's, known only in the browser, so the parts that depend on it wait for
Alpine (`x-cloak`) rather than render a guess. `class` lands on the button or the group. --}}
@props([
'mode' => 'toggle',
])
@php
$mode = in_array($mode, ['toggle', 'cycle', 'picker'], true) ? $mode : 'toggle';
@endphp
@if ($mode === 'picker')
<div
role="radiogroup"
aria-label="{{ __('Theme') }}"
x-data
data-theme-toggle="picker"
{{ $attributes->class(['inline-flex h-10 rounded-corner-full border border-outline']) }}
>
@foreach (['light' => ['Light', 'light_mode'], 'dark' => ['Dark', 'dark_mode'], 'system' => ['System', 'brightness_auto']] as $choice => [$text, $icon])
<button
type="button"
role="radio"
aria-checked="false"
x-bind:aria-checked="($store.theme.choice === '{{ $choice }}').toString()"
x-bind:tabindex="$store.theme.choice === '{{ $choice }}' ? 0 : -1"
x-on:click="$store.theme.set('{{ $choice }}')"
x-on:keydown.arrow-right.prevent="$el.nextElementSibling?.click(); $el.nextElementSibling?.focus();"
x-on:keydown.arrow-left.prevent="$el.previousElementSibling?.click(); $el.previousElementSibling?.focus();"
data-theme-option="{{ $choice }}"
class="state-layer focus-ring inline-flex min-w-0 flex-1 cursor-pointer items-center justify-center gap-2 border-outline px-3 type-label-lg text-on-surface not-first:border-s first:rounded-s-corner-full last:rounded-e-corner-full aria-checked:bg-secondary-container aria-checked:text-on-secondary-container"
>
<x-icon name="check" class="hidden size-4.5 in-aria-checked:block" />
<x-icon :name="$icon" class="size-4.5 in-aria-checked:hidden" />
{{ __($text) }}
</button>
@endforeach
</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 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-icon name="light_mode" x-cloak x-show="$store.theme.choice === 'light'" />
<x-icon name="dark_mode" x-cloak x-show="$store.theme.choice === 'dark'" />
<x-icon name="brightness_auto" x-cloak x-show="$store.theme.choice === 'system'" />
@else
<x-icon name="light_mode" x-cloak x-show="$store.theme.resolved === 'dark'" />
<x-icon name="dark_mode" x-cloak x-show="$store.theme.resolved !== 'dark'" />
@endif
</button>
@endif