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
|
||||
@@ -21,5 +21,6 @@
|
||||
@include('livewire-material::showcase.sections.fields')
|
||||
@include('livewire-material::showcase.sections.chips')
|
||||
@include('livewire-material::showcase.sections.sliders')
|
||||
@include('livewire-material::showcase.sections.bars')
|
||||
</main>
|
||||
@endsection
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<a href="{{ route('livewire-material.showcase') }}" class="shrink-0 type-title-lg max-sm:hidden">Livewire Material</a>
|
||||
|
||||
<nav class="-my-2 flex min-w-0 flex-1 gap-x-4 overflow-x-auto py-2 whitespace-nowrap type-label-lg text-on-surface-variant [scrollbar-width:none]" aria-label="Sections">
|
||||
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders'] as $anchor => $section)
|
||||
@foreach (['colour' => 'Colour', 'type' => 'Type', 'shape' => 'Shape', 'elevation' => 'Elevation', 'motion' => 'Motion', 'icons' => 'Icons', 'buttons' => 'Buttons', 'menus' => 'Menus', 'communication' => 'Communication', 'progress' => 'Progress', 'containment' => 'Containment', 'carousel' => 'Carousel', 'fields' => 'Fields', 'chips' => 'Chips', 'sliders' => 'Sliders', 'bars' => 'Bars'] as $anchor => $section)
|
||||
<a href="#{{ $anchor }}" class="rounded-corner-xs hover:text-on-surface focus-ring">{{ $section }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
@php
|
||||
$examples = [
|
||||
'Top app bars' => <<<'BLADE'
|
||||
<div class="grid w-full gap-4 [&>*]:overflow-hidden [&>*]:rounded-corner-lg [&>*]:border [&>*]:border-outline-variant">
|
||||
<div>
|
||||
<x-app-bar title="Shares" :sticky="false">
|
||||
<x-slot:navigation><x-button icon="menu" tooltip="Open navigation" /></x-slot:navigation>
|
||||
<x-slot:actions>
|
||||
<x-button icon="search" tooltip="Search" />
|
||||
<x-button icon="more_vert" tooltip="More" />
|
||||
</x-slot:actions>
|
||||
</x-app-bar>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-app-bar variant="center" title="holiday-photos.zip" subtitle="248 MB · expires in 3 days" :sticky="false">
|
||||
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
|
||||
<x-slot:actions><x-button icon="share" tooltip="Share" /></x-slot:actions>
|
||||
</x-app-bar>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-app-bar variant="medium" title="Recipients" subtitle="3 people" :sticky="false">
|
||||
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
|
||||
<x-slot:actions><x-button icon="person_add" tooltip="Add recipient" /></x-slot:actions>
|
||||
</x-app-bar>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-app-bar variant="large" title="Settings" :sticky="false">
|
||||
<x-slot:navigation><x-button icon="arrow_back" tooltip="Back" /></x-slot:navigation>
|
||||
</x-app-bar>
|
||||
</div>
|
||||
|
||||
<div class="!overflow-visible">
|
||||
<x-app-bar variant="search" :sticky="false" class="rounded-corner-lg">
|
||||
<x-slot:navigation><x-button icon="menu" tooltip="Open navigation" /></x-slot:navigation>
|
||||
<x-search placeholder="Search shares" docked />
|
||||
<x-slot:actions>
|
||||
<span class="mx-2 grid size-8 place-items-center rounded-corner-full bg-primary-container type-label-lg text-on-primary-container">AR</span>
|
||||
</x-slot:actions>
|
||||
</x-app-bar>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Toolbars' => <<<'BLADE'
|
||||
<div class="flex w-full flex-wrap items-center gap-6">
|
||||
<x-toolbar label="Formatting">
|
||||
<x-button icon="format_bold" tooltip="Bold" :selected="true" />
|
||||
<x-button icon="format_italic" tooltip="Italic" :selected="false" />
|
||||
<x-button icon="format_underlined" tooltip="Underline" :selected="false" />
|
||||
<x-button icon="link" tooltip="Link" />
|
||||
</x-toolbar>
|
||||
|
||||
<x-toolbar label="Share actions" vibrant>
|
||||
<x-button icon="content_copy" tooltip="Copy link" />
|
||||
<x-button icon="qr_code_2" tooltip="QR code" />
|
||||
<x-button icon="mail" tooltip="Email" />
|
||||
<x-slot:fab><x-fab icon="add" tooltip="New share" color="secondary" /></x-slot:fab>
|
||||
</x-toolbar>
|
||||
|
||||
<x-toolbar label="View" vertical>
|
||||
<x-button icon="grid_view" tooltip-right="Grid" :selected="true" />
|
||||
<x-button icon="view_list" tooltip-right="List" :selected="false" />
|
||||
<x-button icon="zoom_in" tooltip-right="Zoom in" />
|
||||
</x-toolbar>
|
||||
|
||||
<div class="w-full overflow-hidden rounded-corner-lg border border-outline-variant">
|
||||
<x-toolbar variant="docked" label="Selection">
|
||||
<x-button icon="download" tooltip="Download" />
|
||||
<x-button icon="share" tooltip="Share" />
|
||||
<x-button icon="drive_file_move" tooltip="Move" />
|
||||
<x-button icon="delete" tooltip="Delete" />
|
||||
</x-toolbar>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Primary tabs' => <<<'BLADE'
|
||||
<div class="w-full space-y-8">
|
||||
<x-tabs :tabs="[['name' => 'files', 'label' => 'Files'], ['name' => 'recipients', 'label' => 'Recipients', 'badge' => 3], ['name' => 'activity', 'label' => 'Activity'], ['name' => 'settings', 'label' => 'Settings', 'disabled' => true]]">
|
||||
<x-tab name="files">Three files, 248 MB.</x-tab>
|
||||
<x-tab name="recipients">Anna, Ben and Chiara.</x-tab>
|
||||
<x-tab name="activity">Downloaded twice today.</x-tab>
|
||||
</x-tabs>
|
||||
|
||||
<x-tabs stacked selected="photos" :tabs="[['name' => 'all', 'label' => 'All', 'icon' => 'folder'], ['name' => 'photos', 'label' => 'Photos', 'icon' => 'photo'], ['name' => 'videos', 'label' => 'Videos', 'icon' => 'movie']]">
|
||||
<x-tab name="all">Everything shared with you.</x-tab>
|
||||
<x-tab name="photos">Only photos.</x-tab>
|
||||
<x-tab name="videos">Only videos.</x-tab>
|
||||
</x-tabs>
|
||||
</div>
|
||||
BLADE,
|
||||
'Secondary and scrollable tabs' => <<<'BLADE'
|
||||
<div class="w-full space-y-8">
|
||||
<x-tabs variant="secondary" :tabs="[['name' => 'overview', 'label' => 'Overview'], ['name' => 'security', 'label' => 'Security', 'icon' => 'lock'], ['name' => 'billing', 'label' => 'Billing']]">
|
||||
<x-tab name="overview">An overview of the account.</x-tab>
|
||||
<x-tab name="security">Passwords and two-factor sign-in.</x-tab>
|
||||
<x-tab name="billing">Plan and invoices.</x-tab>
|
||||
</x-tabs>
|
||||
|
||||
<div class="max-w-sm">
|
||||
<x-tabs scrollable :tabs="collect(['January', 'February', 'March', 'April', 'May', 'June', 'July'])->map(fn ($month) => ['name' => strtolower($month), 'label' => $month])->all()">
|
||||
<x-tab name="january">January's shares.</x-tab>
|
||||
</x-tabs>
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
'Section navigation, account menu and theme toggles' => <<<'BLADE'
|
||||
<div class="w-full space-y-8">
|
||||
<x-section-nav :items="[
|
||||
['title' => 'Profile', 'icon' => 'person', 'url' => '#bars', 'active' => true],
|
||||
['title' => 'Security', 'icon' => 'lock', 'url' => '#bars'],
|
||||
['title' => 'Notifications', 'icon' => 'notifications', 'url' => '#bars', 'badge' => 2],
|
||||
['title' => 'Billing', 'icon' => 'credit_card', 'url' => '#bars'],
|
||||
]" no-wire-navigate />
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<x-account-menu name="Andreas Reinhold" email="andreas@example.com">
|
||||
<x-menu-item label="Settings" icon="settings" link="#bars" no-wire-navigate />
|
||||
<x-slot:footer>
|
||||
<x-menu-item label="Sign out" icon="logout" />
|
||||
</x-slot:footer>
|
||||
</x-account-menu>
|
||||
|
||||
<x-theme-toggle />
|
||||
<x-theme-toggle mode="cycle" />
|
||||
<x-theme-toggle mode="picker" />
|
||||
</div>
|
||||
</div>
|
||||
BLADE,
|
||||
];
|
||||
@endphp
|
||||
|
||||
<section id="bars" class="scroll-mt-24 space-y-6">
|
||||
<h2 class="type-headline-md">App bars, toolbars and tabs</h2>
|
||||
|
||||
<p class="max-w-3xl type-body-md text-on-surface-variant">
|
||||
<code><x-app-bar></code>, <code><x-toolbar></code>, <code><x-tabs></code>, <code><x-tab></code>, <code><x-section-nav></code>, <code><x-account-menu></code> and <code><x-theme-toggle></code>.
|
||||
</p>
|
||||
|
||||
@foreach ($examples as $title => $code)
|
||||
<x-showcase::example :$title :$code />
|
||||
@endforeach
|
||||
</section>
|
||||
Reference in New Issue
Block a user