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
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:
co-authored by
Claude Opus 5
parent
bf00e4c40a
commit
f90695cedd
@@ -0,0 +1,90 @@
|
||||
{{-- M3's tabs: the sections of one thing, with the chosen one marked by an active indicator.
|
||||
|
||||
The bar is rendered on the server from `tabs`, a list of `['name' => …, 'label' => …]` with an
|
||||
optional `icon`, `badge` and `disabled` — so it arrives with the page, never a moment after it.
|
||||
The panels are `<x-tab name="…">` in the slot; every panel is rendered, so switching never waits
|
||||
on the server.
|
||||
|
||||
The chosen tab is the Livewire property in `wire:model` (entangled; `wire:model.live` tells the
|
||||
server at once), or, without it, `selected` (the first tab by default) and `x-model`. The bar is
|
||||
a real tablist (resources/js/tabs.js). `variant` is `primary` (the default: the indicator under
|
||||
the label, in primary) or `secondary` (under the whole tab, for tabs inside a section);
|
||||
`stacked` puts a primary tab's icon over its label (64px); `scrollable` lets tabs keep their
|
||||
own width and scroll sideways instead of sharing the width equally
|
||||
(resources/css/components/tabs.css). --}}
|
||||
|
||||
@props([
|
||||
'tabs' => [],
|
||||
'variant' => 'primary',
|
||||
'selected' => null,
|
||||
'stacked' => false,
|
||||
'scrollable' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$model = $attributes->wire('model')->value() ?: null;
|
||||
$variant = $variant === 'secondary' ? 'secondary' : 'primary';
|
||||
$id = $attributes->get('id') ?? 'tabs-'.substr(md5($model.'|'.implode('|', array_column($tabs, 'name'))), 0, 8);
|
||||
|
||||
$initial = $selected ?? (collect($tabs)->first(fn (array $tab): bool => ! ($tab['disabled'] ?? false))['name'] ?? null);
|
||||
if ($model !== null && ($component = \Livewire\Livewire::current()) !== null && filled(data_get($component, $model))) {
|
||||
$initial = data_get($component, $model);
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
@if ($model !== null) selected: @entangle($attributes->wire('model')), @else selected: @js($initial), @endif
|
||||
tabsId: @js($id),
|
||||
...materialTabs(),
|
||||
}"
|
||||
@if ($model === null) x-modelable="selected" @endif
|
||||
{{ $attributes->whereDoesntStartWith('wire:model')->except(['id'])->class(['min-w-0']) }}
|
||||
>
|
||||
<div
|
||||
x-ref="bar"
|
||||
role="tablist"
|
||||
data-tabs-bar
|
||||
data-variant="{{ $variant }}"
|
||||
@if ($stacked && $variant === 'primary') data-stacked @endif
|
||||
@if ($scrollable) data-scrollable @endif
|
||||
style="--tabs-indicator: {{ $id }}-indicator"
|
||||
x-on:keydown.arrow-right.prevent="step(1)"
|
||||
x-on:keydown.arrow-left.prevent="step(-1)"
|
||||
x-on:keydown.home.prevent="edge(false)"
|
||||
x-on:keydown.end.prevent="edge(true)"
|
||||
>
|
||||
@foreach ($tabs as $tab)
|
||||
@php($isInitial = (string) $tab['name'] === (string) $initial)
|
||||
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="{{ $id }}-{{ $tab['name'] }}"
|
||||
data-tab="{{ $tab['name'] }}"
|
||||
aria-controls="{{ $id }}-{{ $tab['name'] }}-panel"
|
||||
aria-selected="{{ $isInitial ? 'true' : 'false' }}"
|
||||
tabindex="{{ $isInitial ? '0' : '-1' }}"
|
||||
x-bind:aria-selected="(selected === @js($tab['name'])).toString()"
|
||||
x-bind:tabindex="selected === @js($tab['name']) ? 0 : -1"
|
||||
x-on:click="choose(@js($tab['name']))"
|
||||
@disabled($tab['disabled'] ?? false)
|
||||
>
|
||||
<span data-tab-content>
|
||||
@isset($tab['icon'])
|
||||
<x-icon :name="$tab['icon']" class="size-6" />
|
||||
@endisset
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
{{ $tab['label'] }}
|
||||
@if (filled($tab['badge'] ?? null))
|
||||
<x-badge :value="$tab['badge']" />
|
||||
@endif
|
||||
</span>
|
||||
<span data-tab-indicator aria-hidden="true"></span>
|
||||
</span>
|
||||
</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{ $slot }}
|
||||
</div>
|
||||
Reference in New Issue
Block a user