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,79 @@
|
||||
{{-- Who is signed in, and what belongs to them rather than to a page: an avatar that opens a menu.
|
||||
|
||||
`name` and `email` head the menu; `avatar` is an image URL or, by default, the initials of the
|
||||
name. The slot holds the items (`<x-menu-item>`: settings, billing…), a theme item follows unless
|
||||
`:theme="false"`, and the `footer` slot comes last, after a separator — the place for signing
|
||||
out, furthest from the thumb:
|
||||
|
||||
<x-account-menu :name="$user->name" :email="$user->email">
|
||||
<x-menu-item label="Settings" icon="settings" :link="route('settings')" />
|
||||
<x-slot:footer>
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<x-menu-item label="Sign out" icon="logout" type="submit" />
|
||||
</form>
|
||||
</x-slot:footer>
|
||||
</x-account-menu>
|
||||
|
||||
`label` names the button and the menu ("Account"). `position` as on `<x-menu>` (`bottom-end`). --}}
|
||||
|
||||
@props([
|
||||
'name' => null,
|
||||
'email' => null,
|
||||
'avatar' => null,
|
||||
'label' => null,
|
||||
'theme' => true,
|
||||
'position' => 'bottom-end',
|
||||
])
|
||||
|
||||
@php
|
||||
$label ??= __('Account');
|
||||
$image = filled($avatar) && (str_contains((string) $avatar, '/') || str_contains((string) $avatar, '.'));
|
||||
$initials = $image ? null : ($avatar ?? \Illuminate\Support\Str::of((string) $name)->explode(' ')->filter()->take(2)->map(fn (string $word): string => mb_strtoupper(mb_substr($word, 0, 1)))->join(''));
|
||||
@endphp
|
||||
|
||||
<x-menu :$label :$position {{ $attributes }}>
|
||||
<x-slot:trigger>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="{{ $label }}"
|
||||
data-account-menu
|
||||
class="focus-ring inline-flex size-10 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-corner-full bg-primary-container type-label-lg text-on-primary-container"
|
||||
>
|
||||
@if ($image)
|
||||
<img src="{{ $avatar }}" alt="" class="size-full object-cover" />
|
||||
@elseif (filled($initials))
|
||||
{{ $initials }}
|
||||
@else
|
||||
<x-icon name="person" />
|
||||
@endif
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
|
||||
@if (filled($name) || filled($email))
|
||||
<div class="px-3 pt-2 pb-3">
|
||||
@if (filled($name))
|
||||
<p class="truncate type-title-sm text-on-surface">{{ $name }}</p>
|
||||
@endif
|
||||
@if (filled($email))
|
||||
<p class="truncate type-body-sm text-on-surface-variant">{{ $email }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-menu-separator />
|
||||
@endif
|
||||
|
||||
{{ $slot }}
|
||||
|
||||
@if ($theme)
|
||||
<x-menu-item icon="contrast" x-on:click="$store.theme.toggle()" data-account-theme>
|
||||
<span x-text="$store.theme.resolved === 'dark' ? @js(__('Light theme')) : @js(__('Dark theme'))">{{ __('Theme') }}</span>
|
||||
</x-menu-item>
|
||||
@endif
|
||||
|
||||
@isset($footer)
|
||||
<x-menu-separator />
|
||||
|
||||
{{ $footer }}
|
||||
@endisset
|
||||
</x-menu>
|
||||
@@ -0,0 +1,77 @@
|
||||
{{-- M3 Expressive's top app bar: the title of the screen, a navigation button and its actions.
|
||||
|
||||
`variant`: `small` (the default, 64px), `center` (the title centred), `medium` and `large` (a
|
||||
big title under the row that collapses into the small one as the page scrolls), and `search`
|
||||
(a search bar fills the row: put an `<x-search>` in the slot). `title` and `subtitle`; the
|
||||
title is an `<h1>` unless `heading` names another element. Slots: `navigation` (the leading
|
||||
icon button: back, or the menu) and `actions` (trailing icon buttons, an avatar).
|
||||
|
||||
Sticky at the top by default (`:sticky="false"` for one that scrolls away), under the top safe
|
||||
area; the surface turns surface-container once content scrolls under it
|
||||
(resources/css/components/app-bar.css, resources/js/app-bar.js). A collapsing bar needs the
|
||||
window to be what scrolls: an ancestor with `overflow: hidden` or `auto` would stop it
|
||||
sticking (`overflow-x: clip` is fine). --}}
|
||||
|
||||
@props([
|
||||
'title' => null,
|
||||
'subtitle' => null,
|
||||
'variant' => 'small',
|
||||
'sticky' => true,
|
||||
'heading' => 'h1',
|
||||
])
|
||||
|
||||
@php
|
||||
$variant = in_array($variant, ['small', 'center', 'medium', 'large', 'search'], true) ? $variant : 'small';
|
||||
$flexible = in_array($variant, ['medium', 'large'], true);
|
||||
$heading = in_array($heading, ['h1', 'h2', 'h3', 'div', 'p'], true) ? $heading : 'h1';
|
||||
@endphp
|
||||
|
||||
<header
|
||||
x-data="materialAppBar"
|
||||
x-bind:data-scrolled="scrolled ? '' : null"
|
||||
x-bind:data-collapsed="collapsed ? '' : null"
|
||||
x-bind:style="measured"
|
||||
data-app-bar
|
||||
data-variant="{{ $variant }}"
|
||||
@if ($sticky) data-sticky @endif
|
||||
@if (filled($subtitle)) data-subtitled @endif
|
||||
{{ $attributes }}
|
||||
>
|
||||
<div data-app-bar-row>
|
||||
@isset($navigation)
|
||||
<div data-app-bar-leading>{{ $navigation }}</div>
|
||||
@endisset
|
||||
|
||||
@if ($variant === 'search')
|
||||
<div data-app-bar-search>{{ $slot }}</div>
|
||||
@elseif (filled($title))
|
||||
<div data-app-bar-headline @if ($flexible) aria-hidden="true" @endif>
|
||||
@if ($flexible)
|
||||
<span data-app-bar-title>{{ $title }}</span>
|
||||
@else
|
||||
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
|
||||
@if (filled($subtitle))
|
||||
<p data-app-bar-subtitle>{{ $subtitle }}</p>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@isset($actions)
|
||||
<div data-app-bar-trailing>{{ $actions }}</div>
|
||||
@endisset
|
||||
</div>
|
||||
|
||||
@if ($flexible && filled($title))
|
||||
<div data-app-bar-expanded>
|
||||
<{{ $heading }} data-app-bar-title>{{ $title }}</{{ $heading }}>
|
||||
@if (filled($subtitle))
|
||||
<p data-app-bar-subtitle>{{ $subtitle }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($variant !== 'search' && $slot->hasActualContent())
|
||||
{{ $slot }}
|
||||
@endif
|
||||
</header>
|
||||
@@ -0,0 +1,81 @@
|
||||
{{-- The navigation inside one area of an app — the sections of settings, of an admin — which is a
|
||||
single destination in the app's own navigation.
|
||||
|
||||
`items`, a list of `['title' => …, 'url' => …]` with an optional `icon`, `active` and `badge`
|
||||
(an item is current when `active` is true, or when its `url` is the request's). From `sm` they
|
||||
are M3's secondary tabs as links, the current one underlined; below `sm`, where a row of them
|
||||
never fits, a button naming the current section opens a menu of all of them. The same list is
|
||||
rendered for both, and CSS shows one.
|
||||
|
||||
A row too long for its column wraps onto a grid rather than scrolling: below `xl` five or six
|
||||
sections go 3 + 3 and seven or more go four to a row — tabs that scroll hid the last sections on
|
||||
a tablet. `label` names the navigation ("Sections"). Links use `wire:navigate` unless
|
||||
`no-wire-navigate`. --}}
|
||||
|
||||
@props([
|
||||
'items' => [],
|
||||
'label' => null,
|
||||
'noWireNavigate' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$label ??= __('Sections');
|
||||
$isCurrent = fn (array $item): bool => ($item['active'] ?? false) || (filled($item['url'] ?? null) && url()->current() === url($item['url']));
|
||||
$current = collect($items)->first($isCurrent) ?? ($items[0] ?? null);
|
||||
|
||||
$layout = match (true) {
|
||||
count($items) < 5 => 'sm:flex',
|
||||
count($items) < 7 => 'sm:grid sm:grid-cols-3 xl:flex',
|
||||
default => 'sm:grid sm:grid-cols-4 xl:flex',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div {{ $attributes->class(['min-w-0']) }} data-section-nav>
|
||||
@if ($current)
|
||||
<div class="sm:hidden" data-section-picker>
|
||||
<x-menu :$label position="bottom-start">
|
||||
<x-slot:trigger>
|
||||
<button type="button" class="focus-ring flex h-12 w-[min(20rem,calc(100vw-2rem))] cursor-pointer items-center gap-3 rounded-corner-xs border border-outline px-4 text-start type-body-lg text-on-surface">
|
||||
@isset($current['icon'])
|
||||
<x-icon :name="$current['icon']" class="size-5 text-on-surface-variant" />
|
||||
@endisset
|
||||
<span class="min-w-0 flex-1 truncate">{{ $current['title'] }}</span>
|
||||
<x-icon name="arrow_drop_down" class="size-6 text-on-surface-variant" />
|
||||
</button>
|
||||
</x-slot:trigger>
|
||||
|
||||
@foreach ($items as $item)
|
||||
<x-menu-item :label="$item['title']" :icon="$item['icon'] ?? null" :link="$item['url']" :selected="$isCurrent($item)" :no-wire-navigate="$noWireNavigate" />
|
||||
@endforeach
|
||||
</x-menu>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<nav aria-label="{{ $label }}" class="max-sm:hidden">
|
||||
<ul data-tabs-bar data-variant="secondary" class="{{ $layout }}">
|
||||
@foreach ($items as $item)
|
||||
@php($on = $isCurrent($item))
|
||||
|
||||
<li class="flex min-w-0">
|
||||
<a
|
||||
href="{{ $item['url'] }}"
|
||||
data-tab
|
||||
@if ($on) aria-current="page" @endif
|
||||
@unless ($noWireNavigate) wire:navigate @endunless
|
||||
>
|
||||
<span data-tab-content>
|
||||
@isset($item['icon'])
|
||||
<x-icon :name="$item['icon']" :filled="$on" class="size-5" />
|
||||
@endisset
|
||||
<span class="truncate">{{ $item['title'] }}</span>
|
||||
@if (filled($item['badge'] ?? null))
|
||||
<x-badge :value="$item['badge']" />
|
||||
@endif
|
||||
<span data-tab-indicator aria-hidden="true"></span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -0,0 +1,18 @@
|
||||
{{-- One tab's panel, inside `<x-tabs>`, which draws the bar from its own `tabs` list. Shown while
|
||||
its `name` is the chosen one; every panel is rendered, so switching never waits on the server.
|
||||
`class` lands on the panel (it has `pt-6` above its content by default). --}}
|
||||
|
||||
@props([
|
||||
'name',
|
||||
])
|
||||
|
||||
<div
|
||||
x-show="selected === @js($name)"
|
||||
role="tabpanel"
|
||||
x-bind:id="tabsId + '-' + @js($name) + '-panel'"
|
||||
x-bind:aria-labelledby="tabsId + '-' + @js($name)"
|
||||
tabindex="0"
|
||||
{{ $attributes->class(['pt-6 outline-none']) }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -0,0 +1,51 @@
|
||||
{{-- M3 Expressive's toolbar: a set of actions for the current page, in a bar of its own.
|
||||
|
||||
`variant`: `floating` (the default: a pill at elevation 3; `vibrant` in primary-container,
|
||||
`vertical` stands it on end) or `docked` (a full-width bar in surface-container, for the bottom
|
||||
of a screen). `place` puts it over the page: `bottom` (centred above the bottom edge; a docked
|
||||
toolbar spans it) or `end` (centred against the end edge, for a vertical one); without it the
|
||||
toolbar sits where it is written. A `fab` slot sets an `<x-fab>` beside a floating toolbar.
|
||||
`label` names it for screen readers.
|
||||
|
||||
Put `<x-button icon="…" tooltip="…" />` controls in the slot (`:selected` for toggles). It is a
|
||||
`role="toolbar"`: the arrow keys move between its controls (resources/js/toolbar.js,
|
||||
resources/css/components/toolbar.css). --}}
|
||||
|
||||
@props([
|
||||
'variant' => 'floating',
|
||||
'vibrant' => false,
|
||||
'vertical' => false,
|
||||
'place' => null,
|
||||
'label' => null,
|
||||
])
|
||||
|
||||
@php
|
||||
$variant = $variant === 'docked' ? 'docked' : 'floating';
|
||||
$vertical = $vertical && $variant === 'floating';
|
||||
$place = in_array($place, ['bottom', 'end'], true) ? $place : null;
|
||||
$grouped = isset($fab) && $variant === 'floating';
|
||||
@endphp
|
||||
|
||||
@if ($grouped)
|
||||
<div data-toolbar-group @if ($vertical) data-vertical @endif @if ($place) data-toolbar-place="{{ $place }}" @endif>
|
||||
@endif
|
||||
|
||||
<div
|
||||
role="toolbar"
|
||||
x-data="materialToolbar({{ $vertical ? 'true' : 'false' }})"
|
||||
x-on:keydown="move($event)"
|
||||
data-toolbar
|
||||
data-variant="{{ $variant }}"
|
||||
@if ($vertical) data-vertical aria-orientation="vertical" @endif
|
||||
@if ($vibrant && $variant === 'floating') data-vibrant @endif
|
||||
@if ($place && ! $grouped) data-toolbar-place="{{ $place }}" @endif
|
||||
@if (filled($label)) aria-label="{{ $label }}" @endif
|
||||
{{ $attributes }}
|
||||
>
|
||||
{{ $slot }}
|
||||
</div>
|
||||
|
||||
@if ($grouped)
|
||||
{{ $fab }}
|
||||
</div>
|
||||
@endif
|
||||
Reference in New Issue
Block a user